Android进阶之图片加载框架搭建(一)消息机制原理分析及在并发中的应用

引言

目前市面上的图片加载框架不要太多: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<Runnable> 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消息机制的原理,并加以灵活应用,这里提供了它在图片加载框架里的应用,下一篇博客将在此基础上,讨论一下整个图片加载框架的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容