高效加载Bitmap(Displaying Bitmaps Efficiently)

注:看英文版的Api Guide总觉得蛋疼,现在翻译一篇比较重要的内容:Bitmap的高效加载,希望翻译过后能真正理解该篇文章的核心思想。

一般来说,我们在网络上加载的图片的的尺寸都要大于手机屏幕的分辨率,而手机的内存又极其有限,所以,在安卓开发中,对图片进行高效的处理是一块很重要的环节。在安卓开发中有条不成文的规矩:遇到图片,一定要狠狠处理。

高效加载Bitmap

BitmapFatory类提供了许多解析图片的方法:decodeByteArray(), decodeFile(), decodeResource() 等。这些方法可以将图片转换成我们所需要的Bitmap。但一般情况下,手机的分辨率有限,我们并不需要显示一个很大的图片,也并不需要将一个图片完整地加载进内存中,所以在加载图片的过程中需要进行一些处理。而这里,BitmapFactory提供了Option选项,它可以帮我们高效加载图片。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;//这里将inJustDecodeBounds设置为true,意为不全部加载图片,只得到图片的宽和高等基本参数。
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;//得到图片的高
int imageWidth = options.outWidth;//得到图片的宽
String imageType = options.outMimeType;//得到图片的Mime类型

采用上面的代码,我们可以在不加载图片的情况下,提前得到图片的宽高,以便对其进行缩放处理。

图片的大小已经知道了, 现在需要决定是否将完整的图片都加载进来。我们需要思考如下几个部分:

1、想用多大的内存加载图片;
2、ImageView需要显示多大的尺寸;

例如,如果ImageView显示像素为128*96,显然,将一个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;
}

确定了缩放比例,我们就可以部分的加载图片了:

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);

    // 确定缩放比例
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    //真正加载图片的时候一定要把这个参数设为false
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

得到Bitmap,我们就可以放入imageView进行显示了。

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

在异步线程中加载Bitmap

此外,为了使APP更加流畅,我们还需将诸如BitmapFatory.decode*等方法放入非UI线程中进行处理。毕竟加载图片是个相对耗时的操作。在这里,我们可以用我们的老朋友:AsynkTask来进行处理。

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, 128, 96));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        //随时注意imageViewReference弱引用是否被回收
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

在BitmapWorkerTask中,我们定义了一个弱引用,来确保AsyncTask不会阻止ImageView被垃圾回收(WeakReference 弱引用,其中保存的对象实例可以被GC回收掉。这个类通常用于在某处保存对象引用,而又不干扰该对象被GC回收,SoftReference 软引用,它保存的对象实例,除非JVM即将OutOfMemory,否则不会被GC回收。这个特性使得它特别适合设计对象Cache),其实,在处理ImageView、Activity等大对象时,采用弱引用是个非常好的习惯。在这里,我们在后台线程对图片进行加载,加载完毕后再setImageBitmap,这样就避免了对主线程的阻塞。

然后可以这样对图片进行加载:

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

ListView和GridView的图片加载优化

ListView和GridView都继承了AbsListView,它们有一套列表项的复用功能,来优化滚动时的View重复加载。但通过AsynkTask加载列表项图片就会有一个问题,当我们在其中一个列表项加载图片的时候,有可能图片未被加载完就“滚出去了”,这个列表项可能被当做缓存应用到另一个列表项,这就造成了图片的错乱。所以我们要对ListView和GridView的图片加载进行一些并发处理。

看下官方文档的巧妙处理:首先,重写一个BitmapDrawable,它存储了ImageView所对应的BitmapWorkerTask,同样采用弱引用的方式引用BitmapWorkerTask:

static class AsyncDrawable extends BitmapDrawable {

    //弱引BitmapWorkerTask
    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();
    }
}

接口方法loadBitmap。在执行BitmapWorkerTask之前,我们需要定义这个AsyncDrawable并绑定给ImagView:

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);
        //将asyncDrawable绑定给imageView, 此时ImageView还未被加载
        imageView.setImageDrawable(asyncDrawable);
        //再加载imageView
        task.execute(resId);
    }
}

这里的AsyncDrawable将imageView与其对应的AsynkTask一一绑定。loadBitmap方法首先调用了cancelPotentialWork方法,cancelPotentialWork的目的是,如果一个列表项,它的ImageView正在加载中或加载完毕,那么就不干涉,但如果该列表项被复用,并且新加载的ImageView与之前的不同(复用前加载的Image还未完成),就将之前的AsyncTask干掉。实现代码如下:

public static boolean cancelPotentialWork(int data, ImageView imageView) {

    //得到imageView的BitmapWorkerTask 
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
    //如果BitmapWorkerTask为空,证明已经加载完(被回收)或未加载
    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        // 如果要加载的bitmapData 与bitmapWorkerTask过去加载的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;
}

从AsyncDrawable 里得到BitmapWorkerTask 的方法:

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 也要修改:

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

    @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);
            //如是相同的BitmapWorkerTask则加载
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

我们可以在ListView或GridView adapter中的getView()方法调用loadBitmap(),由于getView()方法会频繁调用,loadBitmap会不断地检测要加载的新ImageView与过去的ImageView是否相同,如不同则把之前的任务杀掉,这样就避免了图片的错乱问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值