您的位置:首页 > 移动开发 > Android开发

Android 图片异步下载及缓存--Multithreading For Performance

2012-04-27 16:53 489 查看
概述

这篇文章的目的是为了解决

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 of
BitmapFactory.decodeStream
may
prevent this code from working over a slow connection. Decode a new
FlushedInputStream(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<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()方法实际上是运行在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<BitmapDownloaderTask> bitmapDownloaderTaskReference;

public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
super(Color.BLACK);
bitmapDownloaderTaskReference =
new WeakReference<BitmapDownloaderTask>(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就能提供我们所期望的基本服务了,
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: