ListView 加载来自网络的图片的性能优化。这篇文章的目的是为了解决
Android 多线程与图片缓存方面的知识。同时学习
资料来源:
如果无法访问,请点击这里 -> 对!就这里GillesDebunne ‘s android blog
源码:
如果无法访问,请点击这里 -> 对!就这里android-imagedownloader
想要让交互式应用程序表现的更好,UI主线程就要做技能可能少的工作。任何一个可能使你程序挂起(ANR)的长耗时的任务都应该在另外一个线程中进行处理。典型的长耗时任务就是网络操作了,它具有不可知的延迟。当处理一个长耗时的任务时,如果向用户提示任务的进度,那么他们会忍受一定时长的等待;相反地,如果程序假死在那里,会让用户变得烦躁。这篇文章,我们创建一个简单的图片下载的程序来阐述这个模式,我们使用从网上下载的图片来填充ListView中的图标。创建一个异步的任务在后台下载图片会让我们的程序更快。
一个图片下载程序(Image Downloader)网络上下载图片想对很简单,使用Android FrameWork提供的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,如果返回成功,然会的实体的数据流中所包含的bitmap被解码生成Bitmap。Application 的manifest文件中需要加入INTERNET权限保证程序能够进行网络访问。Note: a bug in the previous versions ofBitmapFactory.decodeStream
may prevent this code from working over a slow connection. Decode a newFlushedInputStream(inputStream)
instead to fix the problem. Here is the implementation of this helper class: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()方法能正确地跳过指定的byte数,直到达到文件的结尾。如果在ListAdapter中的getView()方法中直接使用这个方法,滑动列表的时候会非常的卡。每一个新View的加载展现都需要等待图片下载成功,这会阻止列表的平滑滚动。实际上,这是一个很糟糕的想法以至于AndroidHttpClient甚至不允许它在主线程中被调用。上面的代码将会展现 "This thread forbids HTTP requests"的错误消息。如果你真想自找麻烦的话,可以用DefaultHttpClient进行替换。
介绍异步任务( AsyncTask)AsyncTask类为从UI线程中开启一个新的task提供了一种简单的实现方案。我们创建一个ImageDownloader类来负责创建这些tasks。他会提供一个download方法,他会将从制定url下载下来的图片指定给一个ImageView。
public class ImageDownloader { public void download(String url, ImageView imageView) { BitmapDownloaderTask task = new BitmapDownloaderTask(imageView); task.execute(url); } } /* class BitmapDownloaderTask, see below */ }
BitmapDownloaderTask是一个下载图片的AsyncTask。通过调用execute()启动,该方法在UI线程中被调用,很快地执行并返回结果,这样就达到了快速执行的目的。下面是这个类的实现:class BitmapDownloaderTask extends AsyncTask
{ private String url; private final WeakReference imageViewReference; public BitmapDownloaderTask(ImageView imageView) { imageViewReference = new WeakReference (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()方法实际上是运行在task自己独立进程中的,它只是简单地调用downloadBitmap方法,该方法我们在文章的开始出已经实现了。onPostExecute,当task执行完毕后在调用者的UI线程中执行。他以doInBackground返回的Bitmap作为参数,并且该bitmap被关联到了ImageView控件。注意:ImageView使用了软引用(WeakReference)这样就保证了一个进程中的下载不会阻止一个被杀死的Activity的ImageView被系统垃圾回收。这就解释了为什么在onPostExecute中使用软引用和ImageView之前要检查两者都不为null。这个简单的例子阐述了AsyncTask的使用方法,如果你尝试,会发现这些简短的代码改善了列表的性能,现在滑动的非常流畅。 Read Painless threading for more details on AsyncTasks.但是,我们现在的实现暴露了ListView特有的行为缺陷,为了内存的效率因素,ListView会重复使用那些在用户滚动(score)列表过程中展现过的view。如果用户快速滑动(fling)列表一个给定的ImageView对象会被重复多次使用。每当ImageView的一次正确显示都会触发图片下载的任务,这最后会改变他的imsge。那么问题在哪?同大多数的并行程序(parallel applications)类似,关键的问题是在有序化(ordering)。在我们这个情形下,不可能保证下载的task是按照他们开始的顺序结束的。那么很可能有这种结果,ImageView最终展示的图片是先前的某一item的图片,这个item花费了较长的时间图片下载下来。如果下载的图片只被绑定一次,并且都被指定唯一的imageview,这是没有问题的。但是我们还是在ListView中使用这种比较常见的情形中解决以下这个问题吧。并发处理(Handling concurrency)为了解决这个问题,我们需要记录下载的次序,保证最后一次启动请求的图片被有效地展现出来。事实上可以实现让每一个ImageView记录它最后一次的下载。我们将会为ImageView添加这个特别的信息,通过使用自定义的ImageView的子类。他将会在下载过程中临时绑定给ImageView。下面是DownloadedDrawable类:static class DownloadedDrawable extends ColorDrawable { private final WeakReference
bitmapDownloaderTaskReference; public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) { super(Color.BLACK); bitmapDownloaderTaskReference = new WeakReference (bitmapDownloaderTask); } public BitmapDownloaderTask getBitmapDownloaderTask() { return bitmapDownloaderTaskReference.get(); } }
这个实现基于ColorDrawable,他将会导致ImageView在下载过程中展示黑色的背景。当然可以使用“下载中”等提示性图片替换。再一次,注意使用了WeakReference来限制对象间的相互依赖。下面修改原有的代码把这个类考虑进去。首先download方法会创建这个类的实例并将实例关联给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); } }
canclePotentialDownload()方法当一个新的下载的时候,停止这个图片对应的所有可能的下载进程。注意,这个不能充分保证最新的下载就能显示,可能任务已经结束,正在等待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; }
canclePotentialDownload方法使用AsyncTask类的cancle方法来停止在进程中的下载任务。通常情况下会返回true,这样下载就可以在download方法中启动。唯一不希望这种情况发生的情境是具有相同URL的下载已经在进程中,这种情况下我们希望他继续执行。注意这种实现,当一个ImageView已经被系统回收时,与其相关联的下载并没有被停止,一个RecyclerListener可能因此需要被使用。
这个方法使用了辅助方法getBitmapDownloadTask,该方法简单易懂。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与Download process还有关联时,将图片与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就能提供我们所期望的基本服务了,