Android進(jìn)階之圖片加載框架搭建(一)消息機(jī)制原理分析及在并發(fā)中的應(yīng)用

引言

目前市面上的圖片加載框架不要太多:universal-imageloader如蚜、Glide探赫、Piccosa撬呢、Freso...簡直群魔亂舞。但是作為一名有追求的coder搁嗓,必須懂其中的實(shí)現(xiàn)原理腺逛,并敢于制造輪子棍矛。由于這個專題涉及的內(nèi)容比較多够委,暫定分兩篇博客講完它茁帽。一個完整的圖片加載框架主要包括三大塊:
1.圖片下載任務(wù)調(diào)度.
2.圖片的縮放.
3.圖片的兩級緩存處理.
其中圖片加載任務(wù)的調(diào)度是框架的核心脐雪,這里結(jié)合Android的Handler機(jī)制單獨(dú)講解恢共,在本篇的結(jié)尾會模擬圖片的加載場景脂信,初步搭建圖片加載并發(fā)框架透硝,第二篇博客會給出完全的實(shí)現(xiàn)代碼濒生。

Android消息機(jī)制分析

提到消息機(jī)制讀者應(yīng)該都不陌生:由于子線程不能直接做UI操作丽声,Handler用于耗時子線程工作完畢觉义,發(fā)送消息給UI線程晒骇,更新UI。這的確沒錯撕氧,但是更新UI只是Handler的一個特殊的使用場景呵曹。我的理解是,它背后的本質(zhì)是一種生產(chǎn)者-消費(fèi)者模型海洼。消息機(jī)制結(jié)合java線程并發(fā)機(jī)制富腊,可以實(shí)現(xiàn)并發(fā)任務(wù)管理是整。所以本節(jié)著重理解一下Handler機(jī)制背后的實(shí)現(xiàn)原理浮入,然后據(jù)此構(gòu)建并發(fā)任務(wù)管理模型事秀。整個消息模型結(jié)構(gòu)如下:MessageQueue是倉庫易迹,Handler是生產(chǎn)者,消費(fèi)者(主要是UI線程睹欲,也可以是綁定Lopper的線程窘疮,如HandlerThread考余,這個后面會講到)通過Looper不斷的從消息隊列中取出消息并將其交給各自的目標(biāo)Handler消費(fèi)掉楚堤。


Looper源碼分析

Looper為線程維護(hù)一個消息循環(huán),先看看它的構(gòu)造方法:

private Looper(boolean quitAllowed) {    
    mQueue = new MessageQueue(quitAllowed);    
    mThread = Thread.currentThread();
}

可以看到構(gòu)造方法為私有衅胀,初始化了消息隊列酥筝,綁定當(dāng)前線程嘿歌,構(gòu)造方法在prepare()方法中被調(diào)用:

/** 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));
}

根據(jù)源碼及注釋丧凤,prepare為當(dāng)前線程創(chuàng)建一個Looper;在開啟消息循環(huán)之前必須調(diào)用該方法;且Looper實(shí)例存放在TheadLocal當(dāng)中;一個線程只能綁定一份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();   
  }
}

循環(huán)體中:
1>從消息隊列中取消息msg
2>msg的target(即Handler)分發(fā)處理消息

Loop的主要作用:
1>與當(dāng)前線程綁定鸳君,保證一個線程只會有一個Looper實(shí)例相嵌,同時一個Looper實(shí)例也只有一個MessageQueue批糟。
2>loop()方法徽鼎,不斷從MessageQueue中去取消息否淤,交給消息的target屬性的dispatchMessage去處理∈眨現(xiàn)在異步消息處理線程已經(jīng)有了消息隊列(MessageQueue)嚎京,也有了在無限循環(huán)體中取出消息的哥們隐解,那么我們看看生產(chǎn)者Handler吧煞茫。

Handler源碼分析

依然是先從構(gòu)造方法看起:

 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()獲取了當(dāng)前線程保存的Looper實(shí)例
2>通過Looper實(shí)例得到消息隊列,于是Handler和Looper蚓曼、消息隊列
3>Handler的創(chuàng)建前提:所在的線程必須已經(jīng)綁定Looper(這個很重要,它是handler實(shí)現(xiàn)線程的切換的關(guān)鍵)建立關(guān)聯(lián)
然后我們在看看消息的產(chǎn)生:

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);
}

最終調(diào)用到enqueueMessage()方法钦扭,這里將消息的目標(biāo)設(shè)為自己辟躏,然后調(diào)用消息隊列的enqueueMessage()方法[記得前面loop()方法的msg.target.dispatchMessage(msg)吧]。
于是一個消息的流向如下:Handler生成的消息進(jìn)入到消息隊列中土全,Looper將其從消息隊列中取出,然后分配給產(chǎn)生它的Handler對象会涎,由dispatchMessage將它處理掉裹匙。下面看看最后消息是如何被消費(fèi)掉的:

public void dispatchMessage(Message msg) {    
    if (msg.callback != null) {        
        handleCallback(msg);    
    } else {        
        if (mCallback != null) {            
            if (mCallback.handleMessage(msg)) {                
                return;            
            }        
    }        
    handleMessage(msg);    
    }
}

最后終于走到我們熟悉一萬遍的handleMessage方法。
大致讀完了主要源碼末秃,我們就明白Handler是如何實(shí)現(xiàn)線程切換的:
1>前提:Handler的創(chuàng)建必須依賴已經(jīng)綁定Looper線程
2>sendMessage方法只是對消息隊列做了入隊操作,可以在其他線程中調(diào)用
3>Looper的loop()方法會最終調(diào)用handlerMessage()方法概页,這個方法就運(yùn)行在handler所依附的線程。
那么細(xì)心的讀者會有這樣的疑惑:既然主線程的Looper進(jìn)入死循環(huán)了,那如何處理其他事務(wù)呢?還是看看主線程ActivityThread的初始化代碼吧:

public static void main(String[] args) {
    ...
    //創(chuàng)建Looper和MessageQueue對象惰匙,用于處理主線程的消息
    Looper.prepareMainLooper();
    //創(chuàng)建ActivityThread對象
    ActivityThread thread = new ActivityThread();
    //建立Binder通道 (創(chuàng)建新線程)
    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);
    //消息循環(huán)運(yùn)行
    Looper.loop();
    ...
}

我們看到main方法里在開啟循環(huán)之前绘盟,調(diào)用了thread.attach(false)锡垄,這里便會創(chuàng)建一個Binder線程(具體是指ApplicationThread,Binder的服務(wù)端程奠,用于接收系統(tǒng)服務(wù)AMS發(fā)送來的事件),該Binder線程通過Handler將Message發(fā)送給主線程距境。

圖片加載框架的任務(wù)調(diào)度模型

明白了android消息機(jī)制原理,我們可以利用它方便的構(gòu)建并發(fā)任務(wù)管理模型:開啟一個后臺線程綁定Looper進(jìn)行輪詢(這里我們采用HandlerThread)诬滩,不停從任務(wù)隊列里查詢?nèi)蝿?wù)庙曙,如果有,則取出交給線程池執(zhí)行,另外可以通過計數(shù)信號量Semaphore來控制并發(fā)數(shù)察皇。代碼如下:

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. 
* 模擬后臺下載圖片的任務(wù)并發(fā)管理代碼 
*/
public class ImageLoader {    
    private static ImageLoader mInstance;    
    /**     
     * 線程池    
     */        
    private ExecutorService mThreadPool;    
    /**     
     * 后臺輪詢線程及Handler,信號量   
     */ 
    private HandlerThread mBackLoopThread;//后臺線程    
    private BackLoopHandler mBackLoopThreadHandler;//發(fā)消息給后臺Looper嗜闻,調(diào)度下載線程    
    private Semaphore  mBackLoopThreadSemaphore;//后臺下載任務(wù)個數(shù)限制的信號量,控制同時下載的數(shù)量    
    private LinkedList<Runnable> mTaskQueue;//所有任務(wù)隊列   
    private Type mType = Type.LIFO;//調(diào)度方式,默認(rèn)后進(jìn)先出   
    private class BackLoopHandler extends Handler {
        BackLoopHandler(Looper looper) {            
            super(looper);        
        }        
        @Override        
        public void handleMessage(Message msg) {            
            //同時間的下載任務(wù)個數(shù)信號量同步,執(zhí)行任務(wù)到達(dá)上限,則阻塞            
            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 同時下載圖片線程個數(shù)     
     * @param type        調(diào)度策略     
     */    
    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);//并發(fā)數(shù)量控制信號量        
        // 創(chuàng)建線程池        
        mThreadPool = Executors.newFixedThreadPool(threadCount);          
        mTaskQueue = new LinkedList();        
        mType = type;    
    }    
    /**     
     * 初始化后臺調(diào)度線程,結(jié)合信號量及調(diào)度策略,實(shí)現(xiàn)并發(fā)下載     
     */    
    private void initTaskDispatchThread() {        
        //后臺輪詢線程初始化HandlerThread        
        mBackLoopThread = new HandlerThread("backthread");
        mBackLoopThread.start();        
        mBackLoopThreadHandler = new BackLoopHandler(mBackLoopThread.getLooper());    
    }    
    /**     
     * 根據(jù)調(diào)度策略取任務(wù)     
     *     
     * @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();//執(zhí)行完,釋放信號量        
          }    
    }    

    /**     
     *模擬下載圖片耗時線程     
     */    
     public void testLoadImage(){        
        runnableIndex ++;        
        Runnable runnable = new TestRunnable(runnableIndex);
        addTask(runnable);    
    }    

    /**     
     * 新建任務(wù)甥桂,添加到后臺任務(wù)隊列     
     *     
     * @param runnable     
     */    
    public synchronized void addTask(Runnable runnable) {
        mTaskQueue.add(runnable);
        mBackLoopThreadHandler.sendEmptyMessage(0x110);//給后臺調(diào)度Looper發(fā)消息,    
    }    
    /**     
     * 任務(wù)調(diào)度類型     
     */    
    public enum Type {        
        FIFO, LIFO;    
    }
}

工作流程如下:
1.初始化工作:綁定mBackLoopThread和mBackLoopThreadHandler办陷,開啟消息循環(huán);
2 addTask()方法將任務(wù)加入隊列,并通過mBackLoopThreadHandler向后臺線程發(fā)送消息,后臺輪詢線程從隊列里取任務(wù);
3.并發(fā)數(shù)未滿上限(這里設(shè)的3),線程池執(zhí)行任務(wù);否則阻塞
4.單個任務(wù)執(zhí)行完成,釋放計數(shù)信號量病附。測試代碼:

findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {    
    @Override    
    public void onClick(View view) {
        mImageLoader.testLoadImage();    
    }
});

測試時快速連續(xù)按按鈕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消息機(jī)制的原理,并加以靈活應(yīng)用,這里提供了它在圖片加載框架里的應(yīng)用,下一篇博客將在此基礎(chǔ)上,討論一下整個圖片加載框架的實(shí)現(xiàn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吗冤,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌腮猖,老刑警劉巖姐赡,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辜限,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)稿黍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門起便,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琳轿,“玉大人寇甸,你說我怎么就攤上這事涵防√耍” “怎么了创译?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長温峭。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么桂塞? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任趴乡,我火速辦了婚禮,結(jié)果婚禮上胖齐,老公的妹妹穿的比我還像新娘剿另。我一直安慰自己溺职,他們只是感情好浪耘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布蝉稳。 她就那樣靜靜地躺著收津,像睡著了一般。 火紅的嫁衣襯著肌膚如雪冒冬。 梳的紋絲不亂的頭發(fā)上摩渺,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天简烤,我揣著相機(jī)與錄音,去河邊找鬼摇幻。 笑死横侦,一個胖子當(dāng)著我的面吹牛挥萌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播枉侧,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼引瀑,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了榨馁?” 一聲冷哼從身側(cè)響起憨栽,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎翼虫,沒想到半個月后屑柔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛙讥,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年锯蛀,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片次慢。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡旁涤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迫像,到底是詐尸還是另有隱情劈愚,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布闻妓,位于F島的核電站菌羽,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏由缆。R本人自食惡果不足惜注祖,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望均唉。 院中可真熱鬧是晨,春花似錦、人聲如沸舔箭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽层扶。三九已至箫章,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間镜会,已是汗流浹背檬寂。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戳表,地道東北人桶至。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓拿诸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親塞茅。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容