Android多线程方式处理图片下载及显示

前言

想要创建运行流畅、响应迅速的应用程序,一个好的方式是在主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类基本上可以提供我们所期望的服务。你可以在你的应用程序中使用它类提高响应体验。


结束语

本文大部分翻译自Gilles Debunne的博客Multithreading For Performance,感兴趣的读者可以在Android开发者博客上查看原文。连接地址为http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html。
由于本人水平有限,如果在翻译中存在一些错误,请见谅。




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值