创建一个反应敏捷的应用时,一个重要的技巧就是保证你的UI线程做少量的工作。任何潜在的长耗时任务应该在其他线程中处理。经典的例子就是受不可预料延迟影响的网络操作。用户可以容忍一些打断,特别是当你提供一些反馈说明正在处理一些东西,但是一个冻结的应用对用户来说没有任何提示。
在这篇文章中,我们将创建一个简单的图片下载器来加以演示。我们将从网络下载一些缩略图来填充ListView。创建一个异步任务用来在后台下载将使我们的应用更流畅。
An Image downloader
从网络下载图片很简单,试用framwork提供的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);
} finally {
if (inputStream != null) {
inputStream.close();
}
entity.consumeContent();
}
}
} catch (IOException e) {
getRequest.abort();
Log.w("ImageDownloader", "Error while retrieving bitmap from " + url, e);
} finally {
if (client != null) {
client.close();
}
}
return null;
}
如果你直接在ListAdapter的getView方法直接调用这个方法,将导致滚动效果很不好。每一个新view的显示必须等待图片下载,使得滚动不流畅。
实际上,这是不中糟糕的做法由于AndroidHttpClient不允许在主线程启动。上面的代码将显示“This thread forbids HTTP requsets"错误提示。
异步任务介绍
AsyncTask类提供一种最简单的方式。让我们创建一个ImageDownloader类来负责创建这些任务。它将提供一个download方法来给一个Im设置从指定URL下载的图片
这个BitmapDownloaderTask就是下载图片的AsyncTask。它调用execute来启动,直接返回因此是这个方法非常执行很快。以下是这个类的实现:
class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
private String url;
private final WeakReference<ImageView> imageViewReference;
public BitmapDownloaderTask(ImageView imageView) {
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Actual download method, run in the task thread
@Override
protected Bitmap doInBackground(String... strings) {
// params comes from the execute() call: params[0] is the url
return downloadBitmap(strings[0]);
}
// Once the image is download, associates it to the imageView
@Override
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
doInBackground方法是实际在工作线程中运行的方法。它直接调用我们在文章开头实现的方法。
onPostExecute方法当任务完成是运行在UI线程。它将返回的Bitmap作为参数。注意ImageView使用了弱引用,以使在下载过程中当activity被杀时不会阻止ImageView被垃圾回收。这解释了我们为什么在onPostExecute方法中必须检查弱引用和imageview非空(例如未被回收)。
这个简化的例子介绍了AsyncTask的使用,如果你运行后会发现这几行代码明显提升了ListView的性能使得滑动很流畅。
但是,我们当前的实现ListView的行为会导致一个问题。实际上,由于效率原因,ListView会回收利用当用户滑动时显示的视图。如果fling列表,一个ImageView对象将被使用多次。每次显示相应ImageView时触发一个图片下载任务,最终改变它的图片。所以问题在哪呢?关键问题在于顺序。在我们的例子中,没有任何保证下载任务会按照开启的顺序来完成。这就导致图片最终在列表中显示在上一个item。让我们来解决这个问题。
处理并发
为了解决这个问题,我们应该记住下载的顺序,使得上一个启动下载的将被有效显示。对每个ImageView来说记住它的最近下载很容易。我们在ImageView使用Drawable子类来加入这些信息。当正在下载时,这个Drawable子类将绑定到ImageView。以下是DownloaderDrawable的代码:
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();
}
}
ColorDrable使得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);
}
}
cancelPotentialDownload方法将停止ImageView可能正在进行的下载,因为将启动一个新任务。
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方法停止正在进行的系在。它通常将返回ture。特例就是当同一个URL下载正在进行时。此方法调用了一个helper方法。
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只绑定与它相关的下载回来的Bitmap。
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类完成了我们期待的效果。