高效使用Bitmaps(二) 后台加载Bitmap

为何要在后台加载Bitmap? java

有没有过这种体验:你在Android手机上打开了一个带有含图片的ListView的页面,用手猛地一划,就见那ListView嘎嘎地卡,仿佛每个新的Item都是顶着阻力蹦出来的同样?看完这篇文章,你将学会怎样避免这种状况的发生。 网络

在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法能够把图片加载到Bitmap中。但因为这些方法是耗时的,因此多数状况下,这些方法应该放在非UI线程中,不然将有可能致使界面的卡顿,甚至是触发ANR。 并发

通常状况下,网络图片的加载必须放在后台线程中;而本地图片就能够根据实际状况自行决定了,若是图片很少不大的话,也能够在UI线程中操做来图个方便。至于谷歌官方的说法,是只要是从硬盘或者从网络加载Bitmap,通通不该该在主线程中进行。 异步

基础操做:使用AsyncTask async

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

以上代码摘自Android官方文档,是一个后台加载Bitmap并在加载完成后自动将Bitmap设置到ImageView的AsyncTask的实现。有了这个AsyncTask以后,异步加载Bitmap只须要下面的简单代码: ide

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

而后,一句loadBitmap(R.id.my_image, mImageView) 就能实现本地图片的异步加载了。 性能

并发操做:在ListView和GridView中进行后台加载 优化

在实际中,影响性能的每每是ListView和GridView这种包含大量图片的控件。在滑动过程当中,大量的新图片在短期内一块儿被加载,对于没有进行任何优化的程序,卡顿现象必然会随之而来。经过使用后台加载Bitmap的方式,这种问题将被有效解决。具体怎么作,咱们来看看谷歌推荐的方法。 this

首先建立一个实现了Drawable接口的类,用来存储AsyncTask的引用。在本例中,选择了继承BitmapDrawable,用来给ImageView设置一个预留的占位图,这个占位图用于在AsyncTask执行完毕以前的显示。 spa

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

接下来,和上面相似,依然是使用一个loadBitmap()方法来实现对图片的异步加载。不一样的是,要在启动AsyncTask以前,把AsyncTask传给AsyncDrawable,而且使用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);
    }
}

而后在Adapter的getView()方法中调用loadBitmap()方法,就能够为每一个Item中的ImageView进行图片的动态加载了。

loadBitmap()方法的代码中有两个地方须要注意:第一,cancelPotentialWork()这个方法,它的做用是进行两项检查:首先检查当前是否已经有一个AsyncTask正在为这个ImageView加载图片,若是没有就直接返回true。若是有,再检查这个Task正在加载的资源是否与本身正要进行加载的资源相同,若是相同,那就没有必要再进行多一次的加载了,直接返回false;而若是不一样(为何会不一样?文章最后会有解释),就取消掉这个正在进行的任务,并返回true。第二个须要注意的是,本例中的 BitmapWorkerTask 实际上和上例是有所不一样的。这两点咱们分开说,首先咱们看cancelPotentialWork()方法的代码:

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

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // 取消以前的任务
            bitmapWorkerTask.cancel(true);
        } else {
            // 相同任务已经存在,直接返回false,再也不进行重复的加载
            return false;
        }
    }
    // 没有Task和ImageView进行绑定,或者Task因为加载资源不一样而被取消,返回true
    return true;
}

在cancelPotentialWork()的代码中,首先使用getBitmapWorkerTask()方法获取到与ImageView相关联的Task,而后进行上面所说的判断。好,咱们接着来看这个getBitmapWorkerTask()是怎么写的:

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;
}
从代码中能够看出,该方法经过imageView获取到它内部的Drawable对象,若是获取到了而且该对象为AsyncDrawable的实例,就调用这个AsyncDrawable的getBitmapWorkerTask()方法来获取到它对应的Task,也就是经过一个ImageView->Drawable->AsyncTask的链来获取到ImageView所对应的AsyncTask。

好的,cancelPotentialWork()方法分析完了,咱们回到刚才提到的第二个点:BitmapWorkerTask类的不一样。这个类的改动在于onPostExecute()方法,具体请看下面代码:

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);
            }
        }
    }
}
从代码中能够看出,在后台加载完Bitmap以后,它 并非直接把Bitmap设置给ImageView,而是先判断这个ImageView对应的Task是否是本身 (为何会不一样?文章最后会有解释)。若是是本身,才会执行ImageView的setImageBitmap()方法。到此,一个并发的异步加载ListView(或GridView)中图片的实现所有完成。

延伸:文中两个“为何会不一样”的解答

首先,简单说一下ListView中Item和Item对应的View的关系(GridView中同理)。假设一个ListView含有100项,那么它的100个Item应该分别对应一个View用于显示,这样一共是100个View。但Android实际上并无这样作。出于内存考虑,Android只会为屏幕上可见的每一个Item分配一个View。用户滑动ListView,当第一个Item移动到可视范围外后,他所对应的View将会被系统分配给下一个即将出现的Item。

回到问题。

咱们不妨假设屏幕上显示了一个ListView,而且它最多能显示10个Item,而用户在最顶部的Item(不妨称他为第1个Item)使用Task加载Bitmap的时候进行了滑动,而且直到第1个Item消失而第11个Item已经在屏幕底部出现的时候,这个Task尚未加载完成。那么此时,原先与第1个Item绑定的ImageView已经被从新绑定到了第11个Item上,而且第11个Item触发了getItem()方法。在getItem()方法中,ImageView第二次使用Task为本身加载Bitmap,但这时它须要加载的图片资源已经变了(由第1个Item对应的资源变成了第11个Item对应的资源),所以在cancelPotentialWork()方法执行时会判断两个资源不一致。这就是为何相同ImageView却对应了不一样的资源。

同理,一个Task持有了一个ImageView,但因为这个Task有可能已通过时,所以这个ImageView所对应的Task未必就是这个Task自己,也有多是另外一个更年轻的Task。

相关文章
相关标签/搜索