我们经常面对的一个问题是显示网络上的图片,这个问题会以不同的形式出现,例如在列表中显示很多图片,这类问题的理想解决方案需要包括以下几个方面:
1. 保持响应灵敏的UI
2. 在应用程序UI线程之外处理网络和磁盘I/O操作
3. 对于ListView,需要支持视图回收。
4. 快速显示图片的缓存机制
解决这个问题的许多方案是使用内存缓存来保存之前加载的图片,并且通过一个线程池为需要加载的图片排队。但是一个经常被忽略的特性是图片被请求加载的顺序。
考虑ListView中每行都包含一个图片的情况。如果用户向下滚动列表,多数图片加载方案会根据图片所在的父视图显示到屏幕的顺序来加载每张图片。结果是,当用户停止滚动操作,当前显示在屏幕上的行,虽然是当前时点最重要的行,但是却会被滞后加载。开发者想要的效果是最后请求显示的行能够通过“插队”被优先处理
官方示例
Android官方文档的培训章节有一篇名为“高效显示Bitmap”的文档,我们就以该文档为起点。该文档涵盖了许多核心概念,比如将图片缩减采样到合适大小,使用LruCache类实现内存缓存,以及在UI线程外执行工作的基本机制。
我们会扩展这个示例程序以达到优先加载最近请求图片的目的。此外,还会对原始版本做一些性能优化,优化方案是移除应用程序适配器每次调用getView()方法时都会创建一个AsyncTask实例的错误用法。当向上或者向下滚动多次后,示例程序有可能发生运行时异常,导致RejectedExecutionException,改异常是由于过多的AsyncTask实例引起的。在这个例子中我们也会解决这个问题
引入executor
使用AsyncTask的解决方案并不使用处理大量图片,也不能让我控制任务的优先级。作为替代方案,我们使用java.util.concurrent包中的执行器服务和一个优先级队列来指定请求图片的顺序。在新的方案中,可以提供与AsnycTask相似的方法,即,当某个任务不需要向屏幕显示信息时,便取消该任务。后进先出(LIFO)实现方案包含两个类:LIFOTask和LIFOThreadPoolProcessor。
新的任务对象会提供一个静态变量counter用于表示创建的实例数量。因为新创建的任务的counter一定更大,所以该变量可以起到标识任务优先级的作用。我们利用counter实现compareTo()方法用于排序
import java.util.concurrent.FutureTask;
public class LIFOTask extends FutureTask<Object> implements Comparable<LIFOTask> {
private static long counter=0;
private final long prioity;
public LIFOTask(Runnable runnable){
super(runnable,new Object());
prioity=counter++;
}
public long getPriorty(){
return prioity;
}
@Override
public int compareTo(LIFOTask o) {
return prioity>o.getPriorty()?-1:1;
}
}
此时,基类的选择非常重要,我们选择继承自FutureTask类,该类会被传入Executor类,因为他暴露了一个取消方法,与使用AsynTask的旧实现非常相似。
创建了LIFOTask类,需要在ThreadPoolExecutor类中使用其compareTo()方法:
import java.util.Comparator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class LIFOThreadPoolProcessor {
private ThreadPoolExecutor executor;
private BlockingQueue<Runnable> opsToRun=
new PriorityBlockingQueue<>(64,new Comparator<Runnable>() {
@Override
public int compare(Runnable r1, Runnable r2) {
if(r1 instanceof LIFOTask && r2 instanceof LIFOTask){
LIFOTask l1=(LIFOTask)r1;
LIFOTask l2=(LIFOTask)r2;
return l1.compareTo(l2);
}
return 0;
}
});
public LIFOThreadPoolProcessor(int threadCount){
executor=new ThreadPoolExecutor(threadCount, threadCount, 0,
TimeUnit.SECONDS, opsToRun);
}
public Future<?> submitTask(LIFOTask task){
return executor.submit(task);
}
public void clean(){
executor.purge();
}
}
该类中需要注意的部分是传入的 ThreadPoolExecutor构造方法的参数。我们让客户端应用程序选择线程池的大小,并且选用PriorityBlockingQueue作为客户端应用程序所提交任务的容器。然后,我们使用LIFOTask对象的compareTo()方法得到期望的优先级顺序。
需要注意的是,这个示例,keepAlive时间参数不适用于给定的核心线程池大小和最大线程池大小
UI线程—–离开返回的无缝衔接
在Android开发中,我们都知道保持UI响应灵敏的重要性,因此我们把类似I/O操作这样的耗时任务放在后台线程中执行。当后台线程执行完毕后,经常需要更新UI。我们都知道Android的UI系统并不是线程安全的。在修改任何ImageView之前,必须返回到应用程序主线程中,试图在主线程之外修改UI都会导致异常。
初始实现的代码使用的是AsyncTask的onPostExecute()方法。因为我们用Executor代替AsynTask,因此需要为宿主Activity准备一个Runnable对象。我们会用到Activity的runOnUiThread()方法,该方法会使用Handler在后台将我们的工作添加在UI消息队列中。
切换到UI线程是需要考虑的,我们应该注意以下几个方面:
1. 如果用户滚动ListView,ImageView的实例有可能被回收
2. 宿主Activity有可能在任务完成前已经被销毁
因此,用于处理图片的Runnable的每一步都需要检查是否应该停止处理图片。如果宿主Activity使用ImageWorker的setExitTaskEarly()方法设置一个标记,就可以检查到停止状态,该方法需要在onPause()方法中调用。此外,如果FutureTask的cancel()方法被调用,也可以停止状态