引言
目前市面上的图片加载框架不要太多:universal-imageloader、Glide、Piccosa、Freso...简直群魔乱舞。但是作为一名有追求的coder,必须懂其中的实现原理,并敢于制造轮子。由于这个专题涉及的内容比较多,暂定分两篇博客讲完它。一个完整的图片加载框架主要包括三大块:
1.图片下载任务调度.
2.图片的缩放.
3.图片的两级缓存处理.
其中图片加载任务的调度是框架的核心,这里结合Android的Handler机制单独讲解,在本篇的结尾会模拟图片的加载场景,初步搭建图片加载并发框架,第二篇博客会给出完全的实现代码。
Android消息机制分析
提到消息机制读者应该都不陌生:由于子线程不能直接做UI操作,Handler用于耗时子线程工作完毕,发送消息给UI线程,更新UI。这的确没错,但是更新UI只是Handler的一个特殊的使用场景。我的理解是,它背后的本质是一种生产者-消费者模型。消息机制结合java线程并发机制,可以实现并发任务管理。所以本节着重理解一下Handler机制背后的实现原理,然后据此构建并发任务管理模型。整个消息模型结构如下:MessageQueue是仓库,Handler是生产者,消费者(主要是UI线程,也可以是绑定Lopper的线程,如HandlerThread,这个后面会讲到)通过Looper不断的从消息队列中取出消息并将其交给各自的目标Handler消费掉。
Looper源码分析
Looper为线程维护一个消息循环,先看看它的构造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
可以看到构造方法为私有,初始化了消息队列,绑定当前线程,构造方法在prepare()方法中被调用:
/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
根据源码及注释,prepare为当前线程创建一个Looper;在开启消息循环之前必须调用该方法;且Looper实例存放在TheadLocal当中;一个线程只能绑定一份Looper,否则会抛出异常。
再看看loop()方法
/** * Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper;
Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is. Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
}
final long traceTag = me.mTraceTag;
if (traceTag != 0) { T
race.traceBegin(traceTag, msg.target.getTraceName(msg));
}
try {
msg.target.dispatchMessage(msg);
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
} msg.recycleUnchecked();
}
}
循环体中:
1>从消息队列中取消息msg
2>msg的target(即Handler)分发处理消息
Loop的主要作用:
1>与当前线程绑定,保证一个线程只会有一个Looper实例,同时一个Looper实例也只有一个MessageQueue。
2>loop()方法,不断从MessageQueue中去取消息,交给消息的target属性的dispatchMessage去处理。现在异步消息处理线程已经有了消息队列(MessageQueue),也有了在无限循环体中取出消息的哥们,那么我们看看生产者Handler吧。
Handler源码分析
依然是先从构造方法看起:
public Handler() {
this(null, false);
}
public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
...
}
}
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
1> 通过Looper.myLooper()获取了当前线程保存的Looper实例
2>通过Looper实例得到消息队列,于是Handler和Looper、消息队列
3>Handler的创建前提:所在的线程必须已经绑定Looper(这个很重要,它是handler实现线程的切换的关键)建立关联
然后我们在看看消息的产生:
public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException( this + "sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
最终调用到enqueueMessage()方法,这里将消息的目标设为自己,然后调用消息队列的enqueueMessage()方法[记得前面loop()方法的msg.target.dispatchMessage(msg)吧]。
于是一个消息的流向如下:Handler生成的消息进入到消息队列中,Looper将其从消息队列中取出,然后分配给产生它的Handler对象,由dispatchMessage将它处理掉。下面看看最后消息是如何被消费掉的:
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
最后终于走到我们熟悉一万遍的handleMessage方法。
大致读完了主要源码,我们就明白Handler是如何实现线程切换的:
1>前提:Handler的创建必须依赖已经绑定Looper线程
2>sendMessage方法只是对消息队列做了入队操作,可以在其他线程中调用
3>Looper的loop()方法会最终调用handlerMessage()方法,这个方法就运行在handler所依附的线程。
那么细心的读者会有这样的疑惑:既然主线程的Looper进入死循环了,那如何处理其他事务呢?还是看看主线程ActivityThread的初始化代码吧:
public static void main(String[] args) {
...
//创建Looper和MessageQueue对象,用于处理主线程的消息
Looper.prepareMainLooper();
//创建ActivityThread对象
ActivityThread thread = new ActivityThread();
//建立Binder通道 (创建新线程)
thread.attach(false);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
//消息循环运行
Looper.loop();
...
}
我们看到main方法里在开启循环之前,调用了thread.attach(false),这里便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。
图片加载框架的任务调度模型
明白了android消息机制原理,我们可以利用它方便的构建并发任务管理模型:开启一个后台线程绑定Looper进行轮询(这里我们采用HandlerThread),不停从任务队列里查询任务,如果有,则取出交给线程池执行,另外可以通过计数信号量Semaphore来控制并发数。代码如下:
package com.qicode.backloopthreaddemo;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.util.LinkedList;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;
/** * Created by chenming on 16/12/2.
* 模拟后台下载图片的任务并发管理代码
*/
public class ImageLoader {
private static ImageLoader mInstance;
/**
* 线程池
*/
private ExecutorService mThreadPool;
/**
* 后台轮询线程及Handler,信号量
*/
private HandlerThread mBackLoopThread;//后台线程
private BackLoopHandler mBackLoopThreadHandler;//发消息给后台Looper,调度下载线程
private Semaphore mBackLoopThreadSemaphore;//后台下载任务个数限制的信号量,控制同时下载的数量
private LinkedList mTaskQueue;//所有任务队列
private Type mType = Type.LIFO;//调度方式,默认后进先出
private class BackLoopHandler extends Handler {
BackLoopHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
//同时间的下载任务个数信号量同步,执行任务到达上限,则阻塞
try {
mBackLoopThreadSemaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
mThreadPool.execute(getTask());
}
}
public static ImageLoader getInstance(Context context) {
if (mInstance == null) {
synchronized (ImageLoader.class) {
if (mInstance == null) {
mInstance = new ImageLoader(context, 3, Type.LIFO);
}
}
}
return mInstance;
}
/**
* @param threadCount 同时下载图片线程个数
* @param type 调度策略
*/
private ImageLoader(Context context, int threadCount, Type type) {
init(context, threadCount, type);
}
private void init(Context context, int threadCount, Type type) {
initTaskDispatchThread();
mBackLoopThreadSemaphore = new Semaphore(threadCount);//并发数量控制信号量
// 创建线程池
mThreadPool = Executors.newFixedThreadPool(threadCount);
mTaskQueue = new LinkedList();
mType = type;
}
/**
* 初始化后台调度线程,结合信号量及调度策略,实现并发下载
*/
private void initTaskDispatchThread() {
//后台轮询线程初始化HandlerThread
mBackLoopThread = new HandlerThread("backthread");
mBackLoopThread.start();
mBackLoopThreadHandler = new BackLoopHandler(mBackLoopThread.getLooper());
}
/**
* 根据调度策略取任务
*
* @return
*/
private Runnable getTask() {
if (mType == Type.FIFO) {
return mTaskQueue.removeFirst();
}
return mTaskQueue.removeLast();
}
private int runnableIndex;
private class TestRunnable implements Runnable{
private int mIndex;
public TestRunnable(int index){
mIndex = index;
}
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("TAG", "LoadImage:" + mIndex + "下载完成");
mBackLoopThreadSemaphore.release();//执行完,释放信号量
}
}
/**
*模拟下载图片耗时线程
*/
public void testLoadImage(){
runnableIndex ++;
Runnable runnable = new TestRunnable(runnableIndex);
addTask(runnable);
}
/**
* 新建任务,添加到后台任务队列
*
* @param runnable
*/
public synchronized void addTask(Runnable runnable) {
mTaskQueue.add(runnable);
mBackLoopThreadHandler.sendEmptyMessage(0x110);//给后台调度Looper发消息,
}
/**
* 任务调度类型
*/
public enum Type {
FIFO, LIFO;
}
}
工作流程如下:
1.初始化工作:绑定mBackLoopThread和mBackLoopThreadHandler,开启消息循环;
2 addTask()方法将任务加入队列,并通过mBackLoopThreadHandler向后台线程发送消息,后台轮询线程从队列里取任务;
3.并发数未满上限(这里设的3),线程池执行任务;否则阻塞
4.单个任务执行完成,释放计数信号量。测试代码:
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mImageLoader.testLoadImage();
}
});
测试时快速连续按按钮6次,得出的Log如下:
12-02 11:57:07.810 17956-18071/com.qicode.backloopthreaddemo E/TAG: LoadImage:1下载完成
12-02 11:57:07.980 17956-18077/com.qicode.backloopthreaddemo E/TAG: LoadImage:2下载完成
12-02 11:57:08.130 17956-18082/com.qicode.backloopthreaddemo E/TAG: LoadImage:3下载完成
12-02 11:57:12.815 17956-18071/com.qicode.backloopthreaddemo E/TAG: LoadImage:6下载完成
12-02 11:57:12.980 17956-18077/com.qicode.backloopthreaddemo E/TAG: LoadImage:5下载完成
12-02 11:57:13.135 17956-18082/com.qicode.backloopthreaddemo E/TAG: LoadImage:4下载完成
希望通过本篇博客,读者能布局理解Android消息机制的原理,并加以灵活应用,这里提供了它在图片加载框架里的应用,下一篇博客将在此基础上,讨论一下整个图片加载框架的实现。