Android学习笔记:高效载入大量Bitmap

本文介绍了Android中如何高效加载大量Bitmap,避免内存溢出。通过设置BitmapFactory.Options的inJustDecodeBounds获取图片尺寸,利用inSampleSize进行裁剪,减少内存消耗。同时,文章强调了在UI线程外处理图片的重要性,推荐使用AsyncTask进行异步加载,并讨论了在ListView或GridView中处理图片的并发问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

许多情况下,我们的应用中需要的图片大小总是小于图片的原始大小如果我们不在载入之前做一些处理的话,那么我们会遇到比如图片资源占用大量内存的状况,所以通常在载入图片之前,我们做一些裁剪工作:

一、读取Bitmap的维度和类型

BitmapFactory类提供了一些数据解压方法,比如:decodeByteArray()、decodeFile()、decodeResource()等等。为了以各种来源的图片资源为基础创建位图(Bitmap),我们需要选择最有效的解压方法(具体详见Android官方文档),同时要注意的是,这些方法都会尝试为被构造的位图申请内存资源,因此比较容易出现OutOfMemory异常。每种解压方法都需要程序员自己声明一个BitmapFactory.Options(这其实是一个类),比如设定其中的inJustDecodeBounds属性为true,然后使用decode方法可以获取原图的size,同时会返回一个null的Bitmap对象,以及outWidth、outHeight、outMimeType属性,这个方法对于我们读取原图的大小、类型十分方便,下面是示例:

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

为了避免上面提到的异常,需要在使用decode方法之前确定原图的大小,除非你十分确定原图的大小不小于你想剪裁的大小,但这一般是不可能的

二、载入一个裁剪的位图版本

现在图片的维度(大小)已经知道了,为了告诉decode方法给原图创建一个多大的子位图,我们需要设定inSampleSize参数,这个参数同样包含在BitmapFactory.Options对象之内,关于inSampleSize参数,做出如下说明:

1)参数为正,如果参数<=1,那么子图的最终裁剪大小即是原图的大小(相当于未裁剪)

2)参数大于1,一般是2的倍数,那么子图的长宽都会变为原来的:1/inSampleSize,这里要说明的是,即使我们为inSampleSize设定的参数大小不是2的倍数,它也会当成向下取正最接近的2的倍数,下面的实例就体现了这一点;

为什么我们要做裁剪?比如有一张1024*768像素的图像要被载入内存,然而最终你要用到的图片大小其实只有128*96,那么我们会浪费很大一部分内存,这显然是没有必要的,下面是一个实例:

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}
为了使用以上的方法,我们在裁剪之前,一定要记得使用 inJustDecodeBounder来获取原图大小,在使用完之后,记得将 inJustDecodeBounds置为false:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

在剪裁工作做完后,我们就可以将位图载入 ImageView了,比如我们设定期望大小为100*100:

mImageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

三、将位图处理任务从UI线程中分离出来

在实际工程中,我们不可能将这个工作留在UI线程,如果我们的图片处理过程需要花费一些时间,那么这个时候我们的UI就会处于一个卡死的状态,这显然不会为用户所接受,所以,为图片处理另起一个线程就成了必须了。


Step1.使用AsyncTask

这个类提供了一个简单地方法使得程序能够在后台执行一些工作,关于AsyncTask,更多的可以参考这里AsyncTask,下面是给出一个应用实例:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

ImageView使用 WeakReference可以确保 AsyncTask不会阻止对于 ImageView的资源回收,关于这一点,之后再讲。但是这个不能够保证 ImageView在任务结束后依然存在,所以我们必须在 onPostExecute()方法中确认这个 ImageView的引用依然存在。 ImageView消失的情况可能会是:用户从当前活动离开,或者在任务完成之前发生了一些相关的配置变化,为了异步载入位图( Bitmap),我们可以这样:

public void loadBitmap(int resId, ImageView imageView) {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
}

Step2.位图导入的并发处理

ListView或者GridView中,会遇到需要在每个元素中载入位图的情况,同时在用户下滑动作发生后,有些这类控件会选择回收子元素的资源,如果每个子元素都相应触发一个AsyncTask,这就不能保证当这些线程的图片载入任务完成时,相应的包含这个图片的控件子元素到底有没有完成资源回收,而且,也不能保证这些线程任务的执行顺序。这篇博文MulTithreading for Performance更深入的解释了并发处理的一些问题,同时也提供了解决方案。

这里也给出一个解决方法:

我们可以声明一个Drawable子类来存储之前任务的引用,在这种情况下,使用BitmapDrawable以确保当任务结束时一个图片占位符可以在ImageView中显示:

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

在执行 BitmapWorkerTask之前,我们需要创建一个 AsyncDrawable同时将其与目标 ImageView绑定在一起:

public void loadBitmap(int resId, ImageView imageView) {
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

上面的 cancelPotentialWork方法会检查是否有另外的任务已经和当前的 ImageView关联在一起了,如果已经关联,那么这个方法会通过调用 cancel()方法试图删除之前的任务。在一些小规模事件中,可以作如下处理:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // If bitmapData is not yet set or it differs from the new data
        if (bitmapData == 0 || bitmapData != data) {
            // Cancel previous task
            bitmapWorkerTask.cancel(true);
        } else {
            // The same work is already in progress
            return false;
        }
    }
    // No task associated with the ImageView, or an existing task was cancelled
    return true;
}

关于上面提到的 getBitmapWorkerTask()方法,是为了得到与任务相关的 ImageView

private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
   if (imageView != null) {
       final Drawable drawable = imageView.getDrawable();
       if (drawable instanceof AsyncDrawable) {
           final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
           return asyncDrawable.getBitmapWorkerTask();
       }
    }
    return null;
}

终于到了最后一步:更新 BitmapWorkerTask中的 onPostExecute()方法,以至于能够检查线程任务是否被删除同时当前任务又和一个 ImageView匹配:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

以上方法也适用于其他和 ListViewGridView的能够回收子元素控件资源的控件,在 ImageView中我们只要简单的调用 loadBitmap方法就好了, GridView的实现将会需要适配器调用 getView()方法。

【此篇仍在更新】







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值