英文原文:Gilles Debunne,编译:ImportNew - 赵荣
有一个好方法可以让你的应用保持快速响应,那就是让主UI线程尽量少做事情,如果在UI线程中做一个耗时过长的处理,会导致UI僵死,因此对于有可能耗时过长的任务应该另起一个线程处理。这种典型的应用场景就是做网络相关的操作,因为网络传输过程中可能有意料不到的延迟。通常来说,用户可以忍受反馈时的一小段等待,但界面僵死就是另外一回事了。
本文就根据这种设计模式实现一个简单的图片下载应用,我们将实现一个带有图片预览功能的列表,这些图片都是从互联网上下载的,下载操作是在后台异步下载的。
图片下载应用
从网上下载图片非常简单,使用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) {
// 可以在这里提供更多更详细的关于IOException和IllegalStateException的错误信息
getRequest.abort();
Log.w("ImageDownloader", "Error while retrieving bitmap from " + url + e.toString());
} finally {
if (client != null) {
client.close();
}
}
return null;
}
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 bytes = read();
if (bytes < 0) {
break; //读到文件结束
} else {
bytesSkipped = 1; // 读一个字节
}
}
totalBytesSkipped += bytesSkipped;
}
return totalBytesSkipped;
}
}
使用异步任务
public class ImageDownloader {
public void download(String url, ImageView imageView) {
BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
task.execute(url);
}
}
BitmapDownloaderTask类继承自AsynTask,它提供下载图片的功能。它的execute方法可以即时返回,因此速度非常快,从UI线程调用的时候就不会感觉到有什么卡顿的感觉,而这正是我们的目的。下面是BitmapDownloaderTask的实现代码:
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
// 这里是在下载线程中真正要执行的代码
protected Bitmap doInBackground(String... params) {
// 参数params是从execute方法传递过来的,params[0]就是要下载的图片url
return downloadBitmap(params[0]);
}
@Override
// 一旦图片下载完成,就将它在ImageView控件上显示出来
protected void onPostExecute(Bitmap bitmap) {
if (isCancelled()) {
bitmap = null;
}
if (imageViewReference != null) {
ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
处理并发问题
为了解决这个问题,我们需要记下每个下载任务的顺序,确保最后一个开始下载的图片是最终显示的图片。这样的话就需要每一个ImageView对象都记得它最后一次下载的是哪张图片。我们现在就用一个包装过的Drawable子类来保存这一信息,然后在下载过程中将这个Drawable子类对象与ImageView控件绑定,下面给出DowloadedDrawable类的实现代码:
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();
}
}
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方法,如果一个新的下载任务开始时,发现上一个下载任务还没有结束的话,就通过该方法结束上一个下载任务。但是请注意,即使这么做也不能保证最新下载的图片一定会被最终显示出来,因为每个任务结束后会执行它的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 {
// 相同的URL已经在下载过程中了,因此不取消,因为不管是先下载还是后下载,反正下载的都是同一个文件
return false;
}
}
return true;
}
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还是不是依然与本任务相对应,只有当它们依然对应时,才会将下载下来的图片在对应的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);
}
}
Demo
![](http://static.oschina.net/uploads/space/2012/1204/114703_AMo6_245415.png)
下一步工作
我们发送HTTP请求的代码非常简单,我们可以考虑为了某些特定的网站添加一些必须的参数或者cookie。(【 译者注】:比如说有些网站禁止外来访问,可以添加HTTP协议中的referer或者某些网站需要验证才能下载的等等)。
本文使用的AsyncTask类是可以非常简单方便的从UI线程中触发异步任务。你也可以考虑使用Handler类来对你想做的事情做更好的控制,比如说控制下载线程数的总量等等。( 【译者注】: 现在的AsyncTask配合Executor使用完全可以做到控制总线程数的功能,并且如果在非UI线程中更新UI会导致“android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views”这样的异常,因此个人建议尽量使用AsyncTask比较好。)