前言
想要创建运行流畅、响应迅速的应用程序,一个好的方式是在主UI线程中尽可能的少做写工作。一个有可能运行很长时间的任务有可能会阻塞整个应用程序,所以它应该在一个单独的线程中运行。经典的例子就是涉及到网络的操作,这些操作有可能会产生不可预测的延迟。用户可能会容忍一些延迟,特别是你能够提供一些提示来告诉他们后台正在运行任务,但是如果只是呆板的僵在那里,用户就不知道应用程序正在做什么。
在本文中,我们将采用这个模式创建一个简单的图片下载应用。我们将会在一个ListView中显示从网络上下载下来的缩略图。在实际运用中,创建一个异步的后台下载任务能够让你的应用程序保持流畅。
图片下载
从网络上下载图片相对来说比较简单,使用系统框架提供的HTTP相关的类就可以了。下面是一个简单的例子:
static Bitmap downloadBitmap(String url) { final AndroidHttpClient client = AndroidHttpClient.newInstance("Android"); final HttpGet getRequest = new HttpGet(url); try { HttpResponse response = client.execute(getRequest); final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode != HttpStatus.SC_OK) { Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url); return null; } final HttpEntity entity = response.getEntity(); if (entity != null) { InputStream inputStream = null; try { inputStream = entity.getContent(); final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); return bitmap; } finally { if (inputStream != null) { inputStream.close(); } entity.consumeContent(); } } } catch (Exception e) { // Could provide a more explicit error message for IOException or IllegalStateException getRequest.abort(); Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e.toString()); } finally { if (client != null) { client.close(); } } return null; }
首先创建client和HTTP request,如果请求成功,那么得到的响应实体中就会包含图片,你可以调用BitmapFactory.decodeStream方法来得到Bitmap。但是首先你必须在manifest文件中添加<uses-permissionandroid:name="android.permission.INTERNET" />从而使你的应用程序能够访问网络资源。
提示:老版本的BitmapFactory.decodeStream方法中的一个Bug可能会阻止它在慢速连接网络中工作。我们可以编写一个FlushedInputStream(inputStream)替代类来解决这个问题。下面是一个例子:
static class FlushedInputStream extends FilterInputStream { public FlushedInputStream(InputStream inputStream) { super(inputStream); } @Override public long skip(long n) throws IOException { long totalBytesSkipped = 0L; while (totalBytesSkipped < n) { long bytesSkipped = in.skip(n - totalBytesSkipped); if (bytesSkipped == 0L) { int byte = read(); if (byte < 0) { break; // we reached EOF } else { bytesSkipped = 1; // we read one byte } } totalBytesSkipped += bytesSkipped; } return totalBytesSkipped; } }
上面的类保证了skip()方法跳过了所有得到的字节,一直到我们读取到了文件的末尾。
如果你直接在ListAdapter的getView中是用上面的文件下载方法,那么当你拖动列表时就会很慢,因为图片在显示时要等待文件下载完成,这影响了程序的流畅运行。
事实上,在最新版本的Android框架中AndroidHttpClient类是不允许在主线程中运行的,如果你直接运行上面的代码,你会得到”This threadforbids HTTP requests”错误信息。
引入异步任务
Android框架提供的AsyncTask类是能够让你创建多线程任务的最简单的方法之一。下面我们创建一个类ImageDownloader,用来管理图片下载任务。它提供一个下载方法,把从网络上下载的图片显示到ImageView上。
public class ImageDownloader { public void download(String url, ImageView imageView) { BitmapDownloaderTask task = new BitmapDownloaderTask(imageView); task.execute(url); } } /* BitmapDownloaderTask类的实现请参照下面的代码*/ }
BitmapDownloaderTask是一个继承AsyncTask的异步类,用来下载图片。这个类用execute方法来启动,在完成后会立刻返回。因为它的主要目的是在UI主线程中被调用,因此运行的特别快(Itis started using execute, which returns immediately hence making this method really fast which is the whole purpose since it will be called from the UIthread.)。下面是一个例子:
class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> { private String url; private final WeakReference<ImageView> imageViewReference; public BitmapDownloaderTask(ImageView imageView) { imageViewReference = new WeakReference<ImageView>(imageView); } @Override // Actual download method, run in the task thread protected Bitmap doInBackground(String... params) { // params comes from the execute() call: params[0] is the url. return downloadBitmap(params[0]); } @Override // Once the image is downloaded, associates it to the imageView protected void onPostExecute(Bitmap bitmap) { if (isCancelled()) { bitmap = null; } if (imageViewReference != null) { ImageView imageView = imageViewReference.get(); if (imageView != null) { imageView.setImageBitmap(bitmap); } } } }
doInBackground是实际执行的方法,它在自己的线程中运行并使用了在本文开始时实现的downloadBitmap的方法。
当任务完成后,UI主线程会调用onPostExecute方法。它将结果Bitmap作为参数传回,这个Bitmap将被显示到存储在BitmapDownloaderTask类中的imageView中(这个imageView是在建立BitmapDownloaderTask任务时作为参数传入的)。请注意这个ImageView被存储为WeakReference,所以后台的下载任务不能保证它不被系统的垃圾回收机制回收,因此我们在onPostExecute中使用imageView和WeakReference时都要检查它们是否为null。
这个简单的例子演示了如何使用AsyncTask,尝试一下你就会发现它明显改善了ListView在滚动时的性能。
但是,一个ListView特有的行为揭露我们上面这个类的一个问题。事实上,为了有效使用内存,在画面滚动时,ListView循环使用了他所显示的View对象。当一个ImageView对象显示在列表中后,它会被使用很多次。每次显示时这个ImageView都会触发对应的图片下载任务,而这个下载任务会改变它本身显示的内容。那么问题在哪里?在单线程的应用中,我们给定的key(本例中为url)是按照顺序来的,但是在多线程并发中程序中(正如本例中),我们不能够保证图片下载任务是按照顺序完成的。那么最终显示在列表中的图片可能并不是我们想象的那个,可能它只是恰好下载的时间比较长而已。如果这个图片你只使用一次的话,那么这并不是问题(比如你要把图片显示在一个单独的ImageView中)。但是在本例这样的列表中则不可以(想象一下再一个群聊的对话列表中,所有的人员的头像全部错位了……),下面让我们来解决这个问题。
处理并发
为了解决上述问题,我们需要记住下载的顺序,所以最后开始的那个任务就是我们要显示的那个。很明显为每个ImageView记住最后下载的那个就足够了。我们使用Drawable的一个子类来把这个扩展信息加入到ImageView中。当后台下载进程在进行中时,我们把这个类暂时绑定到ImageView上。下面是一个例子:
static class DownloadedDrawable extends ColorDrawable { private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference; public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) { super(Color.BLACK); bitmapDownloaderTaskReference = new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask); } public BitmapDownloaderTask getBitmapDownloaderTask() { return bitmapDownloaderTaskReference.get(); } }
这个例子继承了类ColorDrawable,在图片下载中它会显示一个黑色的背景。你可以使用其它信息来代替它向用户提供更好的体验。再一次提醒使用WeakReference来减少对象的互相依赖。
下面让我们使用这个新的类来替换上面的类。首先下载方法会创建一个这个类的实例,然后把它与imageView绑定。
public void download(String url, ImageView imageView) { if (cancelPotentialDownload(url, imageView)) { BitmapDownloaderTask task = new BitmapDownloaderTask(imageView); DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task); imageView.setImageDrawable(downloadedDrawable); task.execute(url, cookie); } }
当imageView上一个新的下载任务开始时,cancelPotentialDownload方法会停止与它相关的其他下载任务。要注意的是这并不能保证最新的图片总是被显示,因为下载任务可能已经停止并且在等待它的onPostExecute方法执行。而onPostExecute方法仍然有可能在新的下载任务完成之后被执行。
private static boolean cancelPotentialDownload(String url, ImageView imageView) { BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView); if (bitmapDownloaderTask != null) { String bitmapUrl = bitmapDownloaderTask.url; if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) { bitmapDownloaderTask.cancel(true); } else { // The same URL is already being downloaded. return false; } } return true; }
cancelPotentialDownload方法使用AsyncTask类的cancel方法来停止下载进程。在大多数情况下这个方法都会终止下载,返回true。但在当前下载进程中的url和最新请求的url一样的时候,我们会希望下载继续进行。注意下这个例子,如果一个ImageView被垃圾回收机制回收,它相关的下载并没有停止,而一个循环的监听器有可能会使用它。
下面是一个帮助类,它返回一个ImageView对应的下载任务:
private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) { if (imageView != null) { Drawable drawable = imageView.getDrawable(); if (drawable instanceof DownloadedDrawable) { DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable; return downloadedDrawable.getBitmapDownloaderTask(); } } return null; }
最后,修改onPostExecute方法, 只有当ImageView仍然存在相关的下载进程时才绑定图片。
if (imageViewReference != null) { ImageView imageView = imageViewReference.get(); BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView); // Change bitmap only if this process is still associated with it if (this == bitmapDownloaderTask) { imageView.setImageBitmap(bitmap); } }
做过以上的修改后,ImageDownloader类基本上可以提供我们所期望的服务。你可以在你的应用程序中使用它类提高响应体验。