转载请注明 http://blog.youkuaiyun.com/sinat_30276961/article/details/47314787
上一篇讲了Android Bitmap大量使用不产生OOM之“加载大图片资源优化”,用到了BitmapFactory.decode* 这些解析方法,如果涉及到的图片源来自sd卡或者来自网络,就绝对不要在ui主线程执行。这些数据的加载所耗的时间是不确定的,有很多因素会影响到它:读取sd卡的速度,网络的响应速度,图片的尺寸,还有CPU的强弱。如果其中一种情况导致ui线程阻塞了,就直接ANR了,然后应用跟你说bye bye了。
这一篇讲述在后台处理bitmap的方式,用到了AsyncTask,然后给你展示怎么处理并发。
使用AsyncTask
可以通过AsyncTask这个类很方便的实现后台线程执行一些工作,然后把结果传到ui线程。要使用它,就先继承复写它。下面用代码展现了加载大图片时用到的decodeSampledBitmapFromResource()在AsyncTask执行的过程。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// 使用WeakReference来确保ImageView可以被垃圾回收机制回收
imageViewReference = new WeakReference<ImageView>(imageView);
}
// 在后台解析图片.
@Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// 一旦解析完成,先查看imageview是否已经被回收,没回收就设置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被回收。
这里插一句,解释下官方的这句话。AsyncTask在执行任务时,没执行完是不会被销毁的。但是和它关联的一个activity已经被finish()了,然后这个activity布局里有ImageView。理论上说这个Imageview已经没用了,应该被销毁,但如果AsyncTask引用ImageView时不是用弱引用,用一般的写法:private Imageview imageview;那此时是强引用,这个不会被垃圾回收器回收。
我们无法保证在AsyncTask执行完,这个ImageView是否还在。所以在onPostExecute()那里,我们要判断一下imageview的存在情况。如果用户已经操作退出了这个activity或者configuration改变了(比方说横竖屏切换),但是task还没完成,那么imageview就可能已经不存在了,被回收了。
补充一句,上面这些情况下,被回收是合理的,因为界面已经不存在了,就没必要再赋值了,也浪费资源,所以,最好用弱引用。
那么怎么异步加载图片资源呢?很简单,创建一个task,并执行它。代码:
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
处理并发性
常规的view控件如listview和gridview在像上面阐述的那样使用AsyncTask时,会出现其他问题。系统为了有效的管理内存,这些控件会在用户滑动时回收子view。如果每个子view都触发一个AsyncTask,我们无法保证当它执行完任务,与其关联的view是否已经被回收并被其他子view重复利用。
这里插一句,listview和gridview都有个特性,被滑出的子view会被其回收,然后给新滑入的子view使用。这里的回收,不是指垃圾回收,而是listview或gridview回收并重复使用。
此外,也无法保证,异步task启动的顺序和task完成的顺序一致。
“Multithreading for Performance”这篇博客更详细的阐述了并发的情况,并且给出了一个方案,那就是ImageView储存一个它触发的AsyncTask,然后在task执行完后再检查一下当前的ImageView储存的是之前的AsyncTask还是新的。
下面就阐述这个方案,创建一个继承Drawable的子类专门用来储存一个task的引用。在这里,BitmapDrawable相当于是image,当任务完成了,可以直接显示到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方法是用来检测是否已经有其他task已经和这个imageview关联了。如果是,它会试图取消先去的任务,通过调用cancel()。有一些小概率情况是,新的任务数据和已经存在的task一致,所以这种情况下,就可以啥都不用做。下面就是cancelPotentialWork的详细代码:
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
// 如果bitmap的数据还没设置,或者bitmap已有的数据和新的数据请求不同。
if (bitmapData == 0 || bitmapData != data) {
// 取消先前的任务
bitmapWorkerTask.cancel(true);
} else {
// 如果新的请求数据和已有的数据一致,那就不用再重新获取了。
return false;
}
}
// 当前imageview没有关联的task,或者关联的task已经被cancel了。
return true;
}
getBitmapWorkerTask(),是用来获取到与此ImageView关联的task。
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;
}
最后一步是更新ui,通过BitmapWorkerTask的onPostExecute(),在里面,先判断这个任务是否已经被取消了。如果没有取消,则根据imageView获取到先前与其绑定的task,看这两个任务是否一致,并且还要判断下imageview是否有被垃圾回收器回收。如果task一样,并且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了,也可以用在类似的会回收重复利用子view的控件上。代码不多,使用起来也很简单,只要在要设置imageview图片的地方,调用一下loadBitmap()就行。比方说,gridview控件的话,就在其Adapter的getView()那里调用loadBitmap()就行。
最后,是我补充部分。我们来理理它的流程:
1.每次下载或者加载图片时,我们用到的方法是loadBitmap()
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);
}
}
第2行这里会先去判断这次下载对应的显示控件是否已经有task在执行,如果有task了,就判断下数据源是否一样,不一样的话,就把已经有的task直接cancel了。
这种情况一般是在滑动listview时最上面几个items被滑出了,然后这些items会被下面滑进来的items复用,此时,按道理下面新滑进来的items应该是要发出新的数据源请求了,所以原先创出来的task就可以直接cancel了。这样可以减少不必要的操作,省下内存开销。
第3行开始,说明要么这个imageview没有老的task存在,要么就是有老task存在,但这个task的请求和新的请求不一样,也就是说这个task已经在cancel()的路上了,那么就新建task。
AsyncDrawable的好处是关联了BitmapWorkerTask,并且可以直接给ImageView使用。
第7行,是开始执行task了。
BitmapWorkerTask关联了一个ImageView的弱引用。然后在后台doInBackground可以下载解析图片或者只是解析图片,这个看你需求,再调整下代码。
后台处理完,就是更新ui了onPostExecute:
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
// 一旦解析完成,先查看imageview是否已经被回收,再看task是否一样.
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (this == bitmapWorkerTask && imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
先是判断该task是否已经被cancel了,这在上面分析过了,无非就是请求的数据不一样了。如果被cancel了,把bitmap置为null,方便垃圾回收器回收已经解析出来的bitmap。
然后是判断ImageView里的AsyncDrawable关联的task是否和现在的task一样,一样的话,并且imageview没被回收,再赋值上去。
这里判断task是否一样就是为了防止那些在loadBitmap的cancelPotentialWork里已经被cancel了task,这些就不用再赋值了。
好了,关于“多线程并发加载图片的处理方式”讲完了。下一篇Android Bitmap大量使用不产生OOM之使用缓存机制,感兴趣,可以继续点进去看。