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

来源:互联网 发布:剪切数据丢失怎么恢复 编辑:程序博客网 时间:2024/05/20 05:09

  • 概述

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

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 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<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就能提供我们所期望的基本服务了,


    

原创粉丝点击