如果图片源数据是从硬盘,网络或是其他非内存中读取的话,在高效地夹在大Bitmaps中讨论的BitmapFactory.decode*
方法就不能在主线程(UI线程)中执行。这种数据加载进内存的时间是不可预测的,并且还要依赖各种其他因素(磁盘读取速度,网络速度,图片大小,CPU性能等等)。如果这些任务的其中之一阻塞了UI线程,那么系统就认为你的app处于无响应状态并且弹出对话框提示用户关闭app。
本课介绍如何使用 AsyncTask
在后台线程中处理bitmaps,同时粗略讲解如何处理并发事件。
AsyncTask
类提供了一个简单的方法来在后台线程处理任务,并且能够将处理结果在UI线程中返回。要使用它,需创建一个它的子类,然后覆盖几个必要的方法。下面这个例子向你展示如何使用 AsyncTask
和decodeSampledBitmapFromResource()
将大bitmap加载到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]; 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
以及它引用的对象不会被垃圾回收。由于并不能确保 ImageView
在任务执行结束后仍然存在,我们在使用它之前需要对它进行检查。ImageView
有可能已经不存在了,例如,用户切换到其他的activity中又或者在任务结束前发生了配置改变(横竖屏切换)都可能导致上述情况。
要异步地加载这个bitmap,只需要简单地执行下面的代码:
public void loadBitmap(int resId, ImageView imageView) { BitmapWorkerTask task = new BitmapWorkerTask(imageView); task.execute(resId); }
通常的视图组件像ListView
和 GridView
,如果像上一节中示范的那样与 AsyncTask
一起使用会引出其他的问题。为了提高内存使用效率,这些组件在滚动过程中会重用它们的子View。如果每个子View都触发了一个 AsyncTask
,那么这些AsyncTask结束后没有任何确保,它们之前关联的View可能已经被重用了。并且,AsyncTask开始的顺序并不意味着它们的结束顺训。
Multithreading for Performance 这片文章讨论了如何处理并发,并且提供了提供了一个解决方法:将ImageView的引用存储起来,然后等到 AsyncTask
结束后查找对应的引用。使用一个类似的方法,上一节使用的 AsyncTask
将会使用类似的模式进行简单的扩展。
创建一个专用的Drawable
子类来存储任务的引用。在这种情况下,这个BitmapDrawable
的作用是让一个占位的图片在task结束后能够被显示在这个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数据和已经存在的task匹配,即可直接返回。下面是cancelPotentialWork
的示例代码:
public static boolean cancelPotentialWork(int data, ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); 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 { // The same work is already in progress return false; } } // No task associated with the ImageView, or an existing task was cancelled return true; }
getBitmapWorkerTask()
, 用于获取与指定ImageView
关联的任务:
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; }
最后一步是在BitmapWorkerTask 中
更新 onPostExecute()
方法, 检查task是否被取消以及当前的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
方法。例如在ListView对应的Adapter的getView方法中调用。