Android源碼解析Handler系列第(一)篇 --- Message全局池

轉(zhuǎn)載請注明文章出處LooperJing洒扎!

1脓恕、UI不能在子線程中更新是個偽命題

我們常說UI需要在主線程中進(jìn)行更新磺送,子線程就不能更新UI嗎?不是谋减,我們并不是說不能在子線程中更新UI牡彻,而是說UI必須要在它的創(chuàng)建線程中進(jìn)行更新,比如下面一段代碼在子線程更新UI就不會報錯出爹。

    new Thread(new Runnable() {
            @Override
            public void run() {
                TextView  textView=new TextView(MainActivity.this);
                textView.setText("Hello");
            }
     }).start();

OK庄吼,那我偏偏不在創(chuàng)建線程中進(jìn)行更新,如下严就!view在主線程中創(chuàng)建总寻,在子線程中add一個Button。

     @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final LinearLayout view = (LinearLayout) LayoutInflater.from(this).inflate(R.layout.activity_main, null, false);
        setContentView(view);

        new Thread(new Runnable() {
            @Override
            public void run() {
                Button btn=new Button(MainActivity.this);
                btn.setText("Hello");
                view.addView(btn);
            }
        }).start();
    }

運行后梢为,Button被順利的加到了根布局中渐行。尼瑪,“UI必須要在它的創(chuàng)建線程中進(jìn)行更新”這種說法也不對爸K钣 !粟害!不是不對蕴忆,而是欠妥,我修改一下代碼悲幅!

     new Thread(new Runnable() {
            @Override
            public void run() {
                Button btn=new Button(MainActivity.this);
                btn.setText("Hello");
                SystemClock.sleep(2000);
                view.addView(btn);
            }
        }).start();

就是在addView之前套鹅,睡眠了一會,結(jié)果還是拋出了 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.這個錯誤汰具!通常這種錯誤卓鹿,就要用Handler向UI線程發(fā)送消息來更新!這個后面會對源碼進(jìn)行分析留荔。說了半天吟孙,進(jìn)入正題,我今天要討論的是Message存谎。要掌握Handler拔疚,先對Handler發(fā)送的Message有所了解。

2既荚、Message的初步認(rèn)識

Message就是一個實現(xiàn)了Parcelable的java類,我的第一感覺是它可以可以用在進(jìn)程間通信(IPC)栋艳,可以用在網(wǎng)絡(luò)中傳輸恰聘,看一下它比較重要的幾個字段。先來幾個熟悉的。

    //當(dāng)有多個Handler發(fā)消息的時候晴叨,或者一個Handler發(fā)多個消息的時候凿宾,可以用what標(biāo)識一個唯一的消息
   public int what;

    //當(dāng)傳遞整形數(shù)據(jù)的時候,不需要使用setData(Bundle)去設(shè)置數(shù)據(jù)兼蕊,使用arg1初厚,arg2開銷更小。
    public int arg1; 

    public int arg2;

    //Message所攜帶的數(shù)據(jù)對象
    public Object obj;

這幾個都是很常用的孙技,要深入了解Message,不得不了解下面幾個产禾。

    //flags表示這個Message有沒有在使用,1表示在池中牵啦,等待復(fù)用亚情,0表示正在被使用,
     int flags;

    //Message發(fā)送之后哈雏,何時才能被處理
     long when;
    
   // Message所攜帶的數(shù)據(jù) 
     Bundle data;

    //表示這個消息被哪個Handler處理
     Handler target;
    
    //我們用Handler.post一個Runnable,這個Runnable也是被包裝成Message對象的
     Runnable callback;
    
    // 作為消息鏈表所用的一個成員
    Message next;

    //sPoolSync是對象鎖楞件,因為Message.obtain方法會在任意線程調(diào)用
    private static final Object sPoolSync = new Object();

    //sPool代表接下來要被重用的Message對象
    private static Message sPool;

    //sPoolSize表示有多少個可以被重用的對象
    private static int sPoolSize = 0;

    //MAX_POOL_SIZE是pool的上限,這里hardcode是50
    private static final int MAX_POOL_SIZE = 50;

對于上面列舉的裳瘪,挑幾個重點分析一下土浸。

Runable是怎么被封裝到Message中的?我們常為會延遲做某個操作寫出下面的代碼

handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                
            }
        },1000);
跟蹤進(jìn)去發(fā)現(xiàn)在postDelayed中調(diào)用了getPostMessage
   private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

所以這個run方法只是一個普通的回調(diào)而已彭羹,千萬不要認(rèn)為他是在另外一個線程中黄伊。剛說了Message中的target表示了這個消息要被哪一個Handler所處理,對于Message是怎么被處理的呢皆怕?盡管后面要分析Handler源碼毅舆,這里還是提一下。

Message的處理順序
Looper.loop()方法中Message被從MessageQueue取出來后會調(diào)用msg.target.dispatchMessage(msg)

   /**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

在dispatchMessage方法中愈腾,檢查是否有由Runnable封裝的消息憋活,如果有,首先處理虱黄;其次處理的是mCallback悦即,mCallback是什么?mCallback是Callback的對象橱乱,Callback是Handler中的一個內(nèi)部接口辜梳,這個接口的作用是,當(dāng)你實例化一個Handler對象的時候泳叠,可以避免不去實現(xiàn)Handler的handleMessage方法作瞄。

    /**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     *
     * @param msg A {@link android.os.Message Message} object
     * @return True if no further handling is desired
     */
    public interface Callback {
        public boolean handleMessage(Message msg);
    }

從消息的分發(fā)可以看到,如果返回了true,那么handler中的handleMessge方法是不會被執(zhí)行的,如果返回false或者mCallback沒有被賦值危纫,那么就會回調(diào)Handlerd的handleMessge宗挥,這就是一個Message的分發(fā)流程乌庶。了解了Message的分發(fā)之后,那么Message是怎么被創(chuàng)建的呢契耿。

3瞒大、Message全局池

ANDROID系統(tǒng)中很多操作都是靠發(fā)消息完成的,比如按下返回鍵就是一個消息搪桂,這樣系統(tǒng)會不會構(gòu)建大量的Message對象呢透敌?上面看到getPostMessage方法中,以 Message.obtain()的方式獲取一個消息踢械,而不是通過它的構(gòu)造函數(shù)酗电,獲取消息的代碼很簡單。

 /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

有一個關(guān)鍵詞是sPool裸燎,初步感覺是池的意思顾瞻,別忘記了,在第一節(jié)Message的初步認(rèn)識中德绿,sPool是一個Message對象 荷荤,這里有點蒙逼,一個對象起個名字跟池有什么關(guān)系移稳≡棠桑回顧一下,Message還有一個成員是 next个粱。

// sometimes we store linked lists of these things
   Message next;

這樣每一個Message對象都有一個next指針指向下一個可用的Message古毛,這不就是大學(xué)數(shù)據(jù)結(jié)構(gòu)中的鏈表嘛!所以一個消息池的結(jié)構(gòu)是這樣的都许。

Message鏈表

既然知道Message有一個消息池(我們通常稱這個消息池為全局池)的機(jī)制稻薇,那么它設(shè)計初衷肯定是要做到復(fù)用的,那么還有一個問題,Message何時被入池胶征,何時出池塞椎?(MessageQueue雖然是存儲消息的,但要弄清楚睛低,這里說的消息池跟MessageQueue并沒有什么關(guān)系案狠。)

消息要入池,也就是這個消息被回收到池中钱雷,等待復(fù)用骂铁,所以我們大膽猜測這個消息肯定不在使用之中,如果這個消息正在使用之中罩抗,是肯定不會把它放到全局池里面的拉庵,也就是說只有這個消息完成了它的使命,系統(tǒng)才能把它回收到全局池中套蒂。通過這樣的分析名段,消息入池不是在它的創(chuàng)建階段阱扬,而是在回收階段泣懊。直接看代碼吧伸辟。Message中有一個recycle的方法。

 /**
     * Return a Message instance to the global pool.
     * <p>
     * You MUST NOT touch the Message after calling this function because it has
     * effectively been freed.  It is an error to recycle a message that is currently
     * enqueued or that is in the process of being delivered to a Handler.
     * </p>
     */
    public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

首先判斷消息是否在使用之中

    boolean isInUse() {
        return ((flags & FLAG_IN_USE) == FLAG_IN_USE);
    }

如果在使用之中馍刮,繼續(xù)判斷gCheckRecycle信夫,gCheckRecycle的默認(rèn)值是true,這個一個與版本相關(guān)的常量,在5.0之后的版本這個值是false卡啰,不知道作什么使用静稻,有誰知道可以告訴我一下。在此不糾結(jié)了匈辱。如果不在使用之中振湾,最后會走進(jìn)recycleUnchecked。

/**
     * Recycles a Message that may be in-use.
     * Used internally by the MessageQueue and Looper when disposing of queued Messages.
     */
    void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

這段代碼也不是很長亡脸,很好理解押搪,一開始,把這個消息所有成員賦值成最初的狀態(tài)浅碾,F(xiàn)LAG_IN_USE的值是1一開始說了Message的flags表示這個Message有沒有在使用大州,1表示在池中,等待復(fù)用垂谢,0表示正在被使用厦画。重點看同步鎖中的代碼。
假設(shè)全局池沒有元素時滥朱,我們將第一個消息放到池中根暑,sPool一開始是NULL,next指向了sPool,所以此時的消息的sPool和next都是NULL徙邻,然后sPool指向當(dāng)前的Message對象排嫌,最后池的數(shù)量加1。大致如下圖鹃栽。


全局池中只有一個消息等待被復(fù)用

假設(shè)有來個消息m2躏率,在走一遍同步鎖中的代碼,此時全局池的狀態(tài)如下圖所示民鼓。

全局池中兩個消息等待被復(fù)用

同理薇芝,三個消息的時候,是這樣

全局池中三個消息等待被復(fù)用

看到這丰嘉,相信你已經(jīng)知道了消息是怎么加到全局池中的夯到,那么何時出池呢?再看消息的獲取代碼饮亏,嘗試找出消息何時出池的耍贾。

 /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

obtain經(jīng)常使用在多線程之中阅爽,所以用sPoolSync作為同步鎖,第一次sPool為空荐开,會new一個消息返回付翁,這new的消息會在回收的時候,會被加到全局池中晃听。如果sPool不為空百侧,sPool是什么?sPool是指向全局池的頭指針能扒,sPool不為空佣渴,說明了全局池中有元素。把sPool賦值給一個Message對象m,同時全局池的頭指針向后移初斑,指向下一個被復(fù)用的消息辛润,然后把m的flags賦值為0,表示這個消息被復(fù)用了见秤,池中元素數(shù)量減1砂竖。經(jīng)過這個邏輯,上圖中的消息m3是不是就被出池了呢秦叛?

消息m3離開全局池

OK晦溪,本文分析到這里,一個Message大部分內(nèi)容都被分析了挣跋,我們知道了Message內(nèi)部有一個全局池三圆,保證了開發(fā)者可以不構(gòu)建大量的消息,提高性能避咆。我們也知道了一個消息何時入池舟肉,何時出池。OK查库,Message復(fù)用機(jī)制到此結(jié)束路媚。

Please accept mybest wishes for your happiness andsuccess

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市樊销,隨后出現(xiàn)的幾起案子整慎,更是在濱河造成了極大的恐慌,老刑警劉巖围苫,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件裤园,死亡現(xiàn)場離奇詭異,居然都是意外死亡剂府,警方通過查閱死者的電腦和手機(jī)拧揽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人淤袜,你說我怎么就攤上這事痒谴。” “怎么了铡羡?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵积蔚,是天一觀的道長。 經(jīng)常有香客問我蓖墅,道長库倘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任论矾,我火速辦了婚禮,結(jié)果婚禮上杆勇,老公的妹妹穿的比我還像新娘贪壳。我一直安慰自己,他們只是感情好蚜退,可當(dāng)我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布闰靴。 她就那樣靜靜地躺著,像睡著了一般钻注。 火紅的嫁衣襯著肌膚如雪蚂且。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天幅恋,我揣著相機(jī)與錄音杏死,去河邊找鬼。 笑死捆交,一個胖子當(dāng)著我的面吹牛淑翼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播品追,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼玄括,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肉瓦?” 一聲冷哼從身側(cè)響起遭京,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎泞莉,沒想到半個月后哪雕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡戒财,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年热监,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饮寞。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡孝扛,死狀恐怖列吼,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苦始,我是刑警寧澤寞钥,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站陌选,受9級特大地震影響理郑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咨油,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一您炉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧役电,春花似錦赚爵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至霎挟,卻和暖如春窝剖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酥夭。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工赐纱, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人采郎。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓千所,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒜埋。 傳聞我的和親對象是個殘疾皇子淫痰,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,722評論 2 345

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