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

  • 概述

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

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 {
    private String url;
    private final WeakReference imageViewReference;

    public BitmapDownloaderTask(ImageView imageView) {
        imageViewReference = new WeakReference(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 bitmapDownloaderTaskReference;

    public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
        super(Color.BLACK);
        bitmapDownloaderTaskReference =
            new WeakReference(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就能提供我们所期望的基本服务了,


    

你可能感兴趣的:(Android,Android,开发总结)