Android異步消息處理機(jī)制深度解析

對(duì)于Android的異步消息處理機(jī)制,大家一定都不陌生,異步消息處理機(jī)制一個(gè)常見的應(yīng)用場(chǎng)景就是在子線程中更新UI,我們都知道栏赴,Android的UI是線程不安全的,如果在子線程中直接更新UI糟红,便會(huì)導(dǎo)致程序崩潰艾帐。對(duì)于該問題,常見的解決方法是盆偿,在UI線程新建一個(gè)Handler并覆寫其handleMessage方法柒爸,在子線程中獲取Message對(duì)象,通過Message對(duì)象的arg,obj字段以及setData()方法攜帶一些數(shù)據(jù)事扭,之后利用UI線程的Handler將消息發(fā)送出去捎稚,最后便可以在handleMessage方法中獲取到剛剛發(fā)送的消息并進(jìn)行相應(yīng)的處理了,示例代碼如下:

public class MainActivity extends ActionBarActivity {

    private Handler handler1=new Handler(){
        @Override
        public void handleMessage(Message msg){
            //to do sth
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable(){
            @Override
            public void run() {
                Message msg=Message.obtain();
                msg.what=1;
                Bundle bundle = new Bundle();  
                bundle.putString("info", "info");  
                msg.setData(bundle);  
                handler1.sendMessage(msg);
            }
        }).start();
    }

}

這種方法相信大家都已經(jīng)用得很熟了求橄,注意今野,此時(shí)我們是在UI線程創(chuàng)建Handler的,那么現(xiàn)在我們嘗試在子線程創(chuàng)建Handler罐农,看看與之前的UI線程創(chuàng)建Handler有何區(qū)別条霜,示例代碼如下:

public class MainActivity extends ActionBarActivity {

    private Handler handler2=null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable(){
            @Override
            public void run() {
                handler2=new Handler();
            }
        }).start(); 
    }

}

結(jié)果,系統(tǒng)竟然報(bào)錯(cuò)了涵亏,錯(cuò)誤提示信息為Can't create handler inside thread that has not called Looper.prepare() 宰睡。意思是蒲凶,不能在一個(gè)沒有調(diào)用Looper.prepare()的線程內(nèi)創(chuàng)建Handler。那么我們依據(jù)系統(tǒng)的意思拆内,在創(chuàng)建Handler之前調(diào)用Looper.prepare()試試看:

new Thread(new Runnable(){
    @Override
    public void run() {
        Looper.prepare();
        handler2=new Handler();
    }
}).start(); 

果不其然旋圆,這回終于不報(bào)錯(cuò)了。但是麸恍,僅僅解決問題是不夠的灵巧,我們更應(yīng)該去探究問題背后所隱藏的原理,只有這樣我們的能力才會(huì)有一個(gè)質(zhì)的提升抹沪,所以下面刻肄,我將帶領(lǐng)大家從源代碼級(jí)別深入地探究Android的異步消息處理機(jī)制

OK,話不多說采够,讓我們趕快進(jìn)入這美妙的探索之旅吧~~~

前面已經(jīng)講到肄方,在子線程中創(chuàng)建Handler時(shí),如果事先不調(diào)用一下Looper.prepare()蹬癌,系統(tǒng)便會(huì)報(bào)錯(cuò)。那么我們?yōu)槭裁匆欢ㄒ热フ{(diào)一下Looper.prepare()呢虹茶?看來逝薪,只有Handler的構(gòu)造函數(shù)以及Looper.prepare()的源代碼能夠告訴我們答案了,我們先從Handler的構(gòu)造函數(shù)看起蝴罪,Handler的構(gòu)造函數(shù)源代碼如下:

public Handler() {  
    if (FIND_POTENTIAL_LEAKS) {  
        final Class<? extends Handler> klass = getClass();  
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&  
                (klass.getModifiers() & Modifier.STATIC) == 0) {  
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +  
                klass.getCanonicalName());  
        }  
    }  
    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 = null;  
} 

我們從mLooper = Looper.myLooper()這行代碼看起董济,如果拿到的mLooper對(duì)象為空的話,便會(huì)拋出一個(gè)運(yùn)行時(shí)異常要门,提示信息正是剛剛的“Can't create handler inside thread that has not called Looper.prepare()”虏肾,那么mLooper對(duì)象何時(shí)為空呢,這就要去看Looper.myLooper()中的代碼了欢搜,Looper.myLooper()的代碼如下:

public static final Looper myLooper() {  
    return (Looper)sThreadLocal.get();  
}  

顯然封豪,這個(gè)方法是從sThreadLocal對(duì)象中拿出當(dāng)前的Looper對(duì)象,如果拿不到的話則返回null〕次粒現(xiàn)在我們可以大膽猜測(cè)下在什么地方給sThreadLocal對(duì)象設(shè)置Looper的了吹埠,沒錯(cuò),就是在Looper.prepare()方法中疮装,我們趕緊去看一下Looper.prepare()方法的源碼:

public static final void prepare() {  
    if (sThreadLocal.get() != null) {  
        throw new RuntimeException("Only one Looper may be created per thread");  
    }  
    sThreadLocal.set(new Looper());  
}  

在Looper.prepare()中缘琅,會(huì)先去嘗試獲取sThreadLocal中的Looper對(duì)象,如果當(dāng)前能夠獲取到Looper對(duì)象廓推,則拋出運(yùn)行時(shí)異乘⑴郏“Only one Looper may be created per thread”,這也說明了每個(gè)線程最多只能有一個(gè)Looper對(duì)象樊展。如果獲取不到Looper對(duì)象呻纹,則給sThreadLocal設(shè)置Looper對(duì)象堆生。
說到這里,很多朋友可能都會(huì)有疑惑:為什么主線程中沒有調(diào)用Looper.prepare()居暖,卻依然能夠正常地創(chuàng)建Handler呢顽频?
這是因?yàn)槌绦蛟趩?dòng)時(shí),系統(tǒng)已經(jīng)自動(dòng)幫我們調(diào)用了Looper.prepare()了太闺,具體可以參照ActivityThread中的main()方法糯景,這里就不去詳述了,感興趣的朋友可以去自行查閱省骂。
我們繼續(xù)去分析Handler中的源代碼:

  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 = null; 

可以看到蟀淮,在利用Looper.myLooper()獲取到Looper對(duì)象并賦值給Handler的成員變量mLooper之后,又將 mLooper的mQueue賦值給Handler的成員變量mQueue钞澳,由此可見怠惶,Handler中擁有著Looper和MessageQueue兩個(gè)成員變量,Handler的構(gòu)造函數(shù)的主要目的就是初始化這兩個(gè)成員變量轧粟,同時(shí)策治,一個(gè)Looper對(duì)應(yīng)著一個(gè)MessageQueue。
分析完Handler的構(gòu)造函數(shù)以及Looper.prepare()的源代碼兰吟,我們?cè)賮硌芯恳幌孪⒌陌l(fā)送流程通惫,先溫習(xí)一下消息發(fā)送的代碼:

Message msg=Message.obtain();
msg.what=1;
Bundle bundle = new Bundle();  
bundle.putString("info", "info");  
msg.setData(bundle);  
handler1.sendMessage(msg);

看到這里,我們不禁要問:Handler到底將消息發(fā)到哪里混蔼?為什么之后在handleMessage中又可以收到之前發(fā)送的消息履腋?
我們知道,Handler有很多發(fā)送Message的方法惭嚣,其中遵湖,除了sendMessageAtFrontOfQueue方法,其他方法都會(huì)輾轉(zhuǎn)調(diào)用到sendMessageAtTime方法晚吞,sendMessageAtTime方法的源代碼如下:

public boolean sendMessageAtTime(Message msg, long uptimeMillis)  
{  
    boolean sent = false;  
    MessageQueue queue = mQueue;  
    if (queue != null) {  
        msg.target = this;  
        sent = queue.enqueueMessage(msg, uptimeMillis);  
    }  
    else {  
        RuntimeException e = new RuntimeException(  
            this + " sendMessageAtTime() called with no mQueue");  
        Log.w("Looper", e.getMessage(), e);  
    }  
    return sent;  
}  

可以看到延旧,sendMessageAtTime方法有兩個(gè)參數(shù),第一個(gè)參數(shù)為msg,代表著我們發(fā)送的消息载矿,第二個(gè)參數(shù)為uptimeMillis垄潮,代表著發(fā)送消息的時(shí)間,其值為自系統(tǒng)開機(jī)到當(dāng)前時(shí)間的毫秒數(shù)加上延遲時(shí)間闷盔,如果不是調(diào)用的sendMessageDelayed方法弯洗,則延遲時(shí)間為0.之后,將當(dāng)前Handler的MessageQueue對(duì)象(即mQueue)取出逢勾,判斷MessageQueue對(duì)象是否為空牡整,若不為空,則將當(dāng)前消息的target屬性指向當(dāng)前發(fā)送消息的Handler對(duì)象(即this),最后溺拱,調(diào)用我們MessageQueue對(duì)象的enqueueMessage方法讓消息進(jìn)入消息隊(duì)列中逃贝。
這里要稍微解釋下MessageQueue,顧名思義,MessageQueue就是一個(gè)消息隊(duì)列谣辞,其內(nèi)部會(huì)以一個(gè)隊(duì)列的形式存儲(chǔ)我們發(fā)送的消息,并提供了消息的入隊(duì)和出隊(duì)方法沐扳。
接下來就要分析較為關(guān)鍵的enqueueMessage方法了泥从,enqueueMessage方法會(huì)將消息放入消息隊(duì)列中,并按照消息發(fā)送的時(shí)間對(duì)消息進(jìn)行排序沪摄,enqueueMessage方法的源代碼如下:

final boolean enqueueMessage(Message msg, long when) {  
    if (msg.when != 0) {  
        throw new AndroidRuntimeException(msg + " This message is already in use.");  
    }  
    if (msg.target == null && !mQuitAllowed) {  
        throw new RuntimeException("Main thread not allowed to quit");  
    }  
    synchronized (this) {  
        if (mQuiting) {  
            RuntimeException e = new RuntimeException(msg.target + " sending message to a Handler on a dead thread");  
            Log.w("MessageQueue", e.getMessage(), e);  
            return false;  
        } else if (msg.target == null) {  
            mQuiting = true;  
        }  
        msg.when = when;  
        Message p = mMessages;  
        if (p == null || when == 0 || when < p.when) {  
            msg.next = p;  
            mMessages = msg;  
            this.notify();  
        } else {  
            Message prev = null;  
            while (p != null && p.when <= when) {  
                prev = p;  
                p = p.next;  
            }  
            msg.next = prev.next;  
            prev.next = msg;  
            this.notify();  
        }  
    }  
    return true;  
}  

enqueueMessage方法有兩個(gè)參數(shù)躯嫉,第一個(gè)是入隊(duì)的消息,第二個(gè)是該消息發(fā)送的時(shí)間杨拐。前面提到的sendMessageAtFrontOfQueue方法也會(huì)調(diào)用到enqueueMessage方法祈餐,但傳入的消息發(fā)送時(shí)間為0。在enqueueMessage方法的內(nèi)部哄陶,會(huì)將當(dāng)前入隊(duì)消息的when字段設(shè)置為傳入的消息發(fā)送時(shí)間帆阳,取出當(dāng)前的隊(duì)頭消息并賦給變量p,之后判斷如果當(dāng)前隊(duì)頭消息為空或消息發(fā)送時(shí)間為0或消息發(fā)送時(shí)間小于隊(duì)頭消息的時(shí)間,則將當(dāng)前入隊(duì)消息的next指針指向隊(duì)頭消息屋吨,之后將隊(duì)頭消息重新賦值為入隊(duì)消息蜒谤,從而完成了將入隊(duì)消息插入到消息隊(duì)列頭部的操作。如果不滿足上述的三種情況至扰,則按照消息發(fā)送時(shí)間的先后順序?qū)ふ胰腙?duì)消息在隊(duì)列中的插入位置芭逝,之后將入隊(duì)消息插入即可。
上面是對(duì)消息的入隊(duì)操作的分析渊胸,那么出隊(duì)操作在哪里呢?我們就要去分析一下Looper.loop()的源碼了:

public static final void loop() {  
    Looper me = myLooper();  
    MessageQueue queue = me.mQueue;  
    while (true) {  
        Message msg = queue.next(); // might block  
        if (msg != null) {  
            if (msg.target == null) {  
                return;  
            }  
            if (me.mLogging!= null) me.mLogging.println(  
                    ">>>>> Dispatching to " + msg.target + " "  
                    + msg.callback + ": " + msg.what  
                    );  
            msg.target.dispatchMessage(msg);  
            if (me.mLogging!= null) me.mLogging.println(  
                    "<<<<< Finished to    " + msg.target + " "  
                    + msg.callback);  
            msg.recycle();  
        }  
    }  
}  

在Looper.loop()中台妆,會(huì)進(jìn)入一個(gè)死循環(huán)翎猛,不斷調(diào)用當(dāng)前MessageQueue的next()方法取出下一條待處理的消息,如果當(dāng)前沒有待處理的消息則阻塞接剩。之后切厘,當(dāng)消息不為空且消息的target字段不為空的話,調(diào)用消息的target字段的dispatchMessage方法懊缺,注意疫稿,此時(shí)消息的target字段就是當(dāng)初發(fā)送這條消息的Handler對(duì)象。
接下來鹃两,我們進(jìn)入到Handler的dispatchMessage方法的源代碼中:

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

在Handler的dispatchMessage方法中遗座,首先會(huì)去判斷消息的callback字段是否為空,若不為空俊扳,則調(diào)用handleCallback方法對(duì)消息進(jìn)行處理途蒋,若為空,則再去判斷Handler的mCallback字段是否為空(Handler的無參構(gòu)造函數(shù)中mCallback被設(shè)置為空)馋记,若不為空号坡,則調(diào)用mCallback的handleMessage方法懊烤,若mCallback字段為空,則直接調(diào)用 handleMessage方法宽堆。
至此腌紧,我們已經(jīng)從源代碼級(jí)別回答了上面提出的兩個(gè)問題:Handler到底將消息發(fā)到哪里?為什么之后在handleMessage中又可以收到之前發(fā)送的消息畜隶?相信大家一定都有很深的理解了吧_壁肋。
下面介紹一個(gè)Android異步消息處理線程的最標(biāo)準(zhǔn)的寫法,此寫法引自Android官方文檔:

class LooperThread extends Thread {  
      public Handler mHandler;  
      public void run() {  
          Looper.prepare();  
          mHandler = new Handler() {  
              public void handleMessage(Message msg) {  
                  // process incoming messages here  
              }  
          };  
          Looper.loop();  
      }  
  }  

現(xiàn)在代箭,我們來思考一個(gè)非常關(guān)鍵的問題:handleMessage方法何時(shí)運(yùn)行在主線程中墩划,何時(shí)運(yùn)行在子線程中?
有不少朋友可能會(huì)說嗡综,handleMessage方法的定義位于主線程中乙帮,其就會(huì)在主線程中執(zhí)行,handleMessage方法的定義位于子線程中极景,其就會(huì)在子線程中執(zhí)行察净。
事實(shí)真的是這樣嗎?
其實(shí)不然盼樟,我先舉個(gè)反例氢卡。HandlerThread就是一個(gè)典型的反例。我們?cè)谥骶€程中定義Handler并覆寫其handleMessage方法晨缴,在定義Handler時(shí)译秦,我們傳入一個(gè)已經(jīng)啟動(dòng)的HandlerThread對(duì)象的Looper作為參數(shù),那么击碗,我們此時(shí)的handleMessage方法是運(yùn)行在子線程中的筑悴,但此時(shí)我們handleMessage方法的定義是位于主線程中的。
這是怎么回事呢稍途?明明handleMessage方法的定義是位于主線程中的阁吝,怎么會(huì)運(yùn)行在子線程里面呢?看來還得再次分析一下我們的源碼械拍,不過這一次突勇,我們需要對(duì)源碼進(jìn)行逆向分析。
首先坷虑,我們知道甲馋,handleMessage方法是在dispatchMessage方法中被調(diào)用的,而dispatchMessage方法又是在Looper.loop()中調(diào)用的猖吴,也就是說摔刁,如果Looper.loop()運(yùn)行在主線程,handleMessage方法也會(huì)運(yùn)行在主線程,如果Looper.loop()運(yùn)行在子線程共屈,handleMessage方法也會(huì)運(yùn)行在子線程绑谣。那么我們的Looper.loop()到底是運(yùn)行在主線程,還是在子線程呢拗引?其實(shí)借宵,這就要看我們定義的Handler用的是哪個(gè)線程的Looper了,如果我們定義的Handler用的是主線程的Looper矾削,那么它使用的MessageQueue自然也是主線程Looper對(duì)應(yīng)的MessageQueue壤玫,通過該Handler發(fā)送的消息會(huì)進(jìn)入該MessageQueue中,之后會(huì)在主線程的Looper對(duì)應(yīng)的Looper.loop()方法中不斷取出該MessageQueue中的消息進(jìn)行處理哼凯,注意欲间,此時(shí)我們主線程的Looper對(duì)應(yīng)的Looper.loop()方法也是運(yùn)行在主線程中的,所以此時(shí)我們的handleMessage方法也是運(yùn)行在主線程中的断部。定義的Handler用的是子線程的Looper的分析過程同上猎贴。總結(jié)一下,如果創(chuàng)建Handler時(shí)用的是主線程的Looper,則相應(yīng)的handleMessage方法會(huì)運(yùn)行在主線程中蝴光,如果創(chuàng)建Handler時(shí)用的是子線程的Looper,則相應(yīng)的handleMessage方法會(huì)運(yùn)行在子線程中她渴。
看到這里,相信大家已經(jīng)對(duì)Android的異步消息處理機(jī)制有了一個(gè)非常深入的理解了蔑祟,如果對(duì)文中內(nèi)容有疑惑的話請(qǐng)?jiān)诓┛拖路搅粞猿煤模視?huì)盡可能地解答,謝謝大家疆虚,望大家多多支持苛败!

參考:http://blog.csdn.net/guolin_blog/article/details/9991569

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市径簿,隨后出現(xiàn)的幾起案子著拭,更是在濱河造成了極大的恐慌,老刑警劉巖牍帚,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乳蛾,居然都是意外死亡暗赶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門肃叶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹂随,“玉大人,你說我怎么就攤上這事因惭≡浪” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵蹦魔,是天一觀的道長激率。 經(jīng)常有香客問我咳燕,道長,這世上最難降的妖魔是什么乒躺? 我笑而不...
    開封第一講書人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任招盲,我火速辦了婚禮,結(jié)果婚禮上嘉冒,老公的妹妹穿的比我還像新娘曹货。我一直安慰自己,他們只是感情好讳推,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開白布顶籽。 她就那樣靜靜地躺著,像睡著了一般银觅。 火紅的嫁衣襯著肌膚如雪礼饱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評(píng)論 1 299
  • 那天设拟,我揣著相機(jī)與錄音慨仿,去河邊找鬼。 笑死纳胧,一個(gè)胖子當(dāng)著我的面吹牛镰吆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播跑慕,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼万皿,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了核行?” 一聲冷哼從身側(cè)響起牢硅,我...
    開封第一講書人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芝雪,沒想到半個(gè)月后减余,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惩系,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年位岔,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片堡牡。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抒抬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晤柄,到底是詐尸還是另有隱情擦剑,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站惠勒,受9級(jí)特大地震影響赚抡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜捉撮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一怕品、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧巾遭,春花似錦肉康、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至骑素,卻和暖如春炫乓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背献丑。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來泰國打工末捣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人创橄。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓箩做,卻偏偏與公主長得像,于是被迫代替她去往敵國和親妥畏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子邦邦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

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