在上一节课程Android高性能加载大量图片系列课程1-高效加载大图中我们介绍了BitmapFactory类中的一系列decode方法。注意!如果加载的图片数据是从本地磁盘或者从远程服务器读取的话(也就是说除了从内存以外的任何地方读取图片数据),请不要在主线程(UI线程)中调用这些方法!为什么???因为从这些地方读区图片数据所花费的时间是不可预测的,会收到很多因素的制约。比如,磁盘的读取速度、网速快慢、图片大小、CPU的处理性能等等因素。如果在主线程中加载大量的图片,如果有任何一个加载任务阻塞了主线程,Android系统会将你的App标示为无响应,然后你的用户就会看到这样的一个窗口。
然后,然后就没有然后了,用户可以选择强行关闭你的App。
那么,这节课将和大家一起来研究一下如何来使用Android SDK中为我们提供的AsyncTask类来在子线程中加载图片。并且,我还将向你展示如何处理图片并发的问题,说的简单一点,如何在ListView或者GridView中处理图片错乱的问题。Alright,Let’s get started!
使用AsyncTask在子线程中加载图片
AsyncTask类提供了很简单的方式去在子线程执行任务并且将执行的结果返回给主线程。AsyncTask类使用起来比较简单,只要继承AsyncTask类,并重写相关的方法即可。OK,接下来就是通过使用AsyncTask来在子线程中加载大图到ImageView中。
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];
// decodeSampledBitmapFromResource方法在上一节课中有讲解
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对象被GC回收(亲,想知道弱引用、软引用、强引用之间的区别吗?自己动手、丰衣足食)。子线程执行完毕之后,不能保证ImageView还在内存中,所以得在AsyncTask的onPostExecute()方法中去检查ImageView是否为null,因为如果在子线程中还没有执行完毕的时候,用户退出了当前Activity的话,那么ImageView可能就会被GC回收了,不存在内存中。
OK,现在让我们来看看异步加载图片有多么简单吧!
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
处理并发问题,这是个大问题
加载单个图片到ImageView中,使用刚才我们的BitmapWorkerTask类很简单,但是在ListView或者GridView中使用的话就会引入一些问题。导致这些问题还得从ListView和GridView的实现机制说起。
为了合理的使用内存,像ListView和GridView这样的控件在用户滚动的时候重用了它们的子视图。举个例子,ListView一屏刚好可显示5行,那么在滑动到第7行的时候,实际上是重用了第1行的视图。(为什么是第7行,而不是第6行呢?)如果每个子视图都触发一个AsyncTask去加载图片话,那么第1行会触发一个AsyncTask去加载的图片1,第7行也会触发一个AsyncTask去加载图片7。假设第7行的AsyncTask执行速度快,此时会讲加载到的图片7填充到视图中,之后呢,第1行的AsyncTask执行完毕,此时会将加载到的图片1填充到视图中,因为第7行重用了第1行视图,那么图片1会填充到第7行中,结果导致第7行显示的是第1行的图片,这样图片就错乱了。
这里有篇关于处理并发问题的博文,Multithreading for Performance(翻墙先),推荐给大家。接下来我们对前面的BitmapWorkerTask做一些改进。首先梳理一下整体的思路。正如上面提到的例子,当ListView滚动到第7行的时候,第7行的视图实际上是第1行重用过来的。那么在执行第7行对应的AsyncTask之前,我们希望把第1行的AsyncTask取消掉,这样就不会出现上面出现的图片错乱问题。既然可以根据子视图来取消对应AsyncTask,那么必然子视图有对AsyncTask的引用,让我们一起来看看下面巧妙的解决方案:
第一步,创建一个继承Drawable类的子类,子类中保存AsyncTask的引用。
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();
}
}
这么做有两个好处,第一,可以通过ImageView来获取对应的AsyncTask对象;第二,在AsyncTask执行的过程中为ImageView设置一个正在加载中的图片。
第二步,修改上面的loadBitmap方法。
public void loadBitmap(int resId, ImageView imageView) {
// 在执行BitmapWorkerTask之前先取消掉之前绑定的BitmapWorkerTask
// 具体的取消逻辑,请看下面的代码片段
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
第二步辅助方法:取消ImageView之前绑定的AsyncTask。
public static boolean cancelPotentialWork(int data, ImageView imageView) {
// 首先根据ImageView获取之前绑定的AsyncTask对象,透过这里的getBitmapWorkerTask方法,就能体会到我们第一步设计的巧妙之处
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
// 这里一定要验证bitmapWorkerTask是否为空,因为AsyncDrawable对象保存的是BitmapWorkerTask对象的弱引用(弱引用、软引用、强引用自个查阅资料,这里就不详细解释了)
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 {
// 只有一种情况不取消之前绑定的AsyncTask,那就是两次加载的图片是同一张图片,这肯定是合理的。
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
// 如果ImageView没有绑定任何的AsyncTask,默认也返回true。当然如果绑定的被取消了也返回true
return true;
}
第二步辅助方法:提供根据ImageView来获取绑定的AsyncTask对象。
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;
}
第三步,很重要的一步,这里做了两次检测。第一次,检测当前的AsyncTask自身是否已经被取消掉。第二次,检测当前的AsyncTask是否和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);
}
}
}
}
以上的实现方式和设计思想不仅仅局限于ListView和GridView这样的控件,也可以使用在任何类似ListView和GridView重用子视图的控件中。如果是单纯地将一张图片加载到ImageView中,那么简单地调用上面的loadBitmap即可。如果是ListView和GridView则需要在Adapter的getView()方法中调用。
至此,《Android高性能加载大量图片系列课程》第2讲-在非UI线程中处理图片就和大家分享到这里!
欢迎关注我的新浪微博和我交流:@Will_Edward
觉得这篇文章对你有用就顶我一下吧!Thanks!