为何要在后台加载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。