Android加载图片完成后的处理方式
概述
本篇主要讲述的是Android将图片加载之后如何来显示图片,以及GridView或者ListView批量异步加载图片后出现错位、重复、闪烁的原因分析,最终给出目前比较流行也是常用的解决方案。
GridView或者ListView在填充内容时一般都会自定义一个适配器,重写BaseAdapter或者其他的Adapter,然后在getView方法中为了节省内存占用,复用View,同时为了减少查询View的次数,我们多数情况下此时又会设置一个placeholder,然后开启一个子线程去异步加载网络图片复制给ImageView。
//复用view 使用convertView public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = View.inflate(context, R.layout.item_image, null); holder.imageView = (ImageView) convertView.findViewById(R.id.imageView); ... } else { holder = (ViewHolder) convertView.getTag(); } ... return convertView; } //placeholder类 class ViewHolder { ImageView imageView; }
最大的问题就在ImageView更新图片上面,一般来说有三种方式:
- 将ImageView传递给一个执行的异步任务如AsyncTask,在task执行完成后直接将加载成功后的图片传递到ImageView。
- 为每一个ImageView设置一个tag标签,在异步任务加载完成后通过标签tag查询到ImageView然后将图片赋值给ImageView。
- 通过缓存技术,就是通过WeakReference来缓存ImageView,最后只要确定ImageView和AsyncTask是否一一对应,只要是一一对应,我们就更新,该方式也是个人认为最为优雅的一种解决方式,事实上它是Google官方文档中的demo采用的一种方式,一旦后台的处理逻辑搞定,我们就可以用最少的代码最更多的事。
方式一
该种方式应该是最简单的一种方式了,但是简单就带来了问题,就如同文章开始部分所描述的部分,我们有时候会复用view,先提前说明,如果不服用view,可以说在显示上不会出现任何问题,每一个图片的url对应一个ImageView,一一对应,但是如果真的采取这种方式,相信即使是一个初入Android的开发者都会嗤之以鼻,因为在刚接触Adapter的时候,我们都被这样强烈建议着要复用view,减少内存占用,如果不服用view,比如一个极端情况,10万张图片+10万个ImageView,这种情况下Android手机OOM的可能性有多大。好吧,那就复用view,这时候就会出现下面这些情况:错位、重复、闪烁。
错位、重复、闪烁原因分析
事实上这些原因的产生都是一样的,就是复用view导致的,罪魁祸首都是BaseAdapter中的getView方法,ListView滑动到第2行会异步加载某个图片,但是加载很慢,加载过程中ListView已经滑动到了第14行,且滑动过程中该图片加载结束。
第2行已不在屏幕内,根据上面介绍的缓存原理,第2行的View对象可能被第14行复用,这样我们看到的就是第14行显示了本该属于第2行的图片,造成显示重复。
这时候第14行Item显示了不属于该行Item而是第2行的图片,当然就错位了。如果第14行图片又很快加载结束,所以我们看到第14行先显示了复用的第2行的图片,立马又显示了自己的图片进行覆盖造成闪烁错乱。
解决方式
通过上面分析我们知道主要原因是view复用导致的,如果每次给复用的对象view一个标识,在异步加载完成时通过该view对应得Tag获取,然后赋值就可以了。首先给ImageView设置一个Tag,这个Tag中设置的是图片的url或者id,然后在加载图片成功时候通过findViewWithTag得这个url或者Id对应的ImageView,然后赋值上去就搞定了,该种解决方式就我下面要讲解的方式二。
方式二
在上面已经分析过该种方式的原理了,接下来看一下它的具体实现吧,这里我们要用到异步任务,需要在子线程中下载图片,所以先写一个接口以便回调加载成功后的图片:
public interface ImageListener { /** * 下载成功后图片bitmap * @param bitmap * * ImageView设置的tag * @param tag */ void onLoadSuccess(Bitmap bitmap, Object tag); }
然后我们将ImageListener通过Adapter传递给AsyncTask,在图片加载成功后调用回调函数。
public class ViewTagAdapter extends BaseAdapter { private Context context; private ImageListener listener; ... @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = View.inflate(context, R.layout.item_image, null); holder.imageView = (ImageView) convertView.findViewById(R.id.imageView); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } //设置加载之前的默认图片 holder.imageView.setImageBitmap(BitmapFactory.decodeResource(context.getResources(), R.drawable.default_image)); //设置tag标签,在图下载完成后通过tag找到ImageView然后赋值 holder.imageView.setTag(Images.imageThumbUrls[position]); //将listener传递给异步任务,加载图片 new MyTask(listener).execute(Images.imageThumbUrls[position]); return convertView; } //异步任务加载图片 private class MyTask extends AsyncTask<String, Void, Bitmap> { private ImageListener listener; private Object tag; public MyTask(ImageListener listener) { this.listener = listener; } protected Bitmap doInBackground(String... params) { tag = params[0]; return Utils.downloadBitmapFromUrl(params[0]); } @Override protected void onPostExecute(Bitmap result) { if (listener != null) { //成功之后的监听回调 listener.onLoadSuccess(result, tag); } } } }
接下来在UI线程中我们处理就简单多了。
adapter = new ViewTagAdapter(this); gridView.setAdapter(adapter); //监听回调显示图片 adapter.setListener(new ImageListener() { public void onLoadSuccess(Bitmap bitmap, Object tag) { ImageView imageView = (ImageView) gridView.findViewWithTag(tag); if (imageView != null) { imageView.setImageBitmap(bitmap); } } });
该种处理方式与方式三的处理方式比较,虽然逻辑上简单了许多,但是在最终调用的时候个人感觉不够简洁,该处我们在set方法中将回调接口实现,当然了如果想让UI界面更简洁,可以将GridView对象直接传递给ViewTagAdapter,然后在ViewTagAdapter中处理回调,相对来说又简洁了一些,但是这种实现扔然离不开GridView。简洁而又功能全面的实现方式才是我们程序员的追求目标,所以第三种方式也是个人最为推崇的一种方式了。
方式三
重要的东西总是最后登场,这可是Google官方Demo中的一种实现方式,让我们来看一下它的实现方式吧,先看在UI线程中的实现:
adapter = new WeakReferenceAdapter(this); gridView.setAdapter(adapter);
非常简洁,没有看到传递任何对象到后台去,仅仅需要new一个Adapter然后赋值给GridView就行了,不但简单还很权威,Google的大咖就是这么处理的。
先看一下Adapter中的处理方式:
public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { holder = new ViewHolder(); convertView = View.inflate(context, R.layout.item_image, null); holder.imageView = (ImageView) convertView.findViewById(R.id.imageView); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } //处理图片工具类 BitmapUtils.getInstance(context).loadImage(Images.imageThumbUrls[position], holder.imageView); return convertView; }
跟平常的处理并无大异,是的,这里是仿写的xUtils中的处理方式,跟ImageWorker的处理方式也是如出一辙,应该是xUtils仿写的别人ImageWorkder的,这里我们还是分析一下BitmapUtils类。
public void loadImage(Object data, ImageView imageView) { if (cancelPotentialWork(data, imageView)) { //① final BitmapLoadTask task = new BitmapLoadTask(imageView); //②将BitmapLoadTask的对象传递给 final AsyncDrawable asyncDrawable = new AsyncDrawable(context.getResources(), BitmapFactory.decodeResource( context.getResources(), R.drawable.default_image), task); //③设置正在加载中图片 imageView.setImageDrawable(asyncDrawable); //④执行异步任务 task.execute(data); } }
上面前两步离不开一个东西就是WeakReference,BitmapLoadTask持有一个弱引用WeakReference的ImageView,ImageView又可以通过imageView.getDrawable()获取一个AsyncDrawable对象,AsyncDrawable巧妙的引用持有弱引用WeakReference的BitmapLoadTask,这样BitmapLoadTask、ImageView、AsyncDrawable三者的关系就建立起来了,只要他们三个是一一对应的,我们就可以放心的更新ImageView中的图片。
public void loadImage(Object data, ImageView imageView) { if (cancelPotentialWork(data, imageView)) { //① final BitmapLoadTask task = new BitmapLoadTask(imageView); //②将BitmapLoadTask的对象传递给 final AsyncDrawable asyncDrawable = new AsyncDrawable(context.getResources(), BitmapFactory.decodeResource( context.getResources(), R.drawable.default_image), task); //③设置正在加载中图片 imageView.setImageDrawable(asyncDrawable); //④执行异步任务 task.execute(data); } } /** * @param data * @param imageView * @return * 通过data和ImageView判断是否已经存在相对应的BitmapLoadTask */ public static boolean cancelPotentialWork(Object data, ImageView imageView) { final BitmapLoadTask bitmapWorkerTask = getBitmapLoadTask(imageView); if (bitmapWorkerTask != null) { final Object bitmapData = bitmapWorkerTask.data; if (bitmapData == null || !bitmapData.equals(data)) { bitmapWorkerTask.cancel(true); } else { return false; } } return true; } private class BitmapLoadTask extends AsyncTask<Object, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private Object data; public BitmapLoadTask(ImageView imageView) { imageViewReference = new WeakReference<ImageView>(imageView); } @Override protected Bitmap doInBackground(Object... params) { data = params[0]; final String urlString = String.valueOf(data); Bitmap bitmap = Utils.downloadBitmapFromUrl(urlString); return bitmap; } protected void onPostExecute(Bitmap bitmap) { final ImageView imageView = getAttachedImageView(); if (bitmap != null && imageView != null) { imageView.setImageBitmap(bitmap); } } //通过BitmapLoadTask获取与ImageView相对应得ImageView private ImageView getAttachedImageView() { final ImageView imageView = imageViewReference.get(); final BitmapLoadTask bitmapWorkerTask = getBitmapLoadTask(imageView); if (this == bitmapWorkerTask) { return imageView; } return null; } } //通过ImageView获取与之相关联的BitmapLoadTask private static BitmapLoadTask getBitmapLoadTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; //通过AsyncDrawable获取到 return asyncDrawable.getBitmapWorkerTask(); } } return null; } private static class AsyncDrawable extends BitmapDrawable { private final WeakReference<BitmapLoadTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapLoadTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<BitmapLoadTask>(bitmapWorkerTask); } public BitmapLoadTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } }
小结
本篇文章只是讨论了下载后如何处理图片的显示,并没有涉及太多的图片缓存技术,可以以后有时间再追加相应的学习笔记。这里加载图片多数是使用AsyncTask来处理图片的,如果对它比较熟悉的开发者应该知道,在Android3.0之前,AsyncTask是可以同时并发5个线程的,但是从3.0开始,Android又开始让它顺序执行了,如果要并行可以自己写一个线程池。如果直接使用默认兼容以前版本的方法机会出现在不同版本上面加载图片的方式不同,而且使用AsyncTask网络不好还很容易引起下面的异常。
java.util.concurrent.RejectedExecutionException: pool=128/128, queue=10/10 at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:1961) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:794) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1315) at android.os.AsyncTask.execute(AsyncTask.java:394)
线程池中已经超过了128个线程,等待执行的序列数目也已经超过了10条,通常的做法为了兼容高低版本我们是直接重写更改AsyncTask的源码,像xUtils直接将默认的最大线程数量增加了一倍。
参考资料
Android ListView异步加载图片错位、重复、闪烁分析以及解决方案