Android寶典|Handler必考知識(shí)點(diǎn)總結(jié)

目錄

  1. 思維導(dǎo)圖
  2. 概述
  3. 基本使用
  4. 源碼分析
    • Handler
    • Message
    • Looper
    • MessageQueue
  5. 常見問題匯總
  6. 總結(jié)
  7. 更新細(xì)節(jié)
    • 設(shè)置同步分割欄
    • Native 層實(shí)現(xiàn)
  8. 參考

思維導(dǎo)圖

image

概述

Handler 的源碼分析文章網(wǎng)上太多了醒串,但是都是掌握一個(gè)大概流程活孩,并沒有深入細(xì)節(jié)陪踩。最開始我也是看一些文章掌握了一個(gè)大概流程廊敌,然后了解了 Handler魁兼、Message、Looper晒喷、MessageQueue委可、ThreadLocal 的作用以及聯(lián)系毒返,現(xiàn)在再自己不看任何資料從頭再看一遍租幕,把一些細(xì)節(jié)都理清楚。比如 Handler 的 sendXxx 和 postXxx 兩者的區(qū)別拧簸、不同的 Handler 的構(gòu)造方法的 handleMessage 的優(yōu)先級(jí)等等劲绪,Message 的排序、回收等盆赤。

回到正文:

Android 應(yīng)用是通過消息驅(qū)動(dòng)運(yùn)行的贾富,在 Android 中一切皆消息,包括觸摸事件牺六,視圖的繪制颤枪、顯示和刷新等等都是消息。Handler 是消息機(jī)制的上層接口淑际,平時(shí)開發(fā)中我們只會(huì)接觸到 Handler 和 Message汇鞭,內(nèi)部還有 MessageQueue 和 Looper 兩大助手共同實(shí)現(xiàn)消息循環(huán)系統(tǒng)。

基本使用

基本使用不用多說庸追,有以下兩種,但是都存在內(nèi)存泄漏的情況台囱,如何避免呢淡溯?可以通過靜態(tài)內(nèi)部類 + 弱引用來避免,文末常見問題匯總會(huì)有示例簿训。

    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if (msg.arg1 == 0x01) {
                Toast.makeText(HandlerActivity.this, "處理消息", Toast.LENGTH_SHORT).show();
            }
            return false;
        }
    });

    @SuppressLint("HandlerLeak")
    private Handler mHandler1 = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

Handler 源碼分析

Handler 源碼其實(shí)并不多咱娶,我們先來看一下構(gòu)造方法:

    //公開的四種構(gòu)造函數(shù)
    public Handler() {
        this(null, false);
    }
    public Handler(Callback callback) {
        this(callback, false);
    }
    public Handler(Looper looper) {
        this(looper, null, false);
    }
    public Handler(Looper looper, Callback callback) {
        this(looper, callback, false);
    }
    
    //實(shí)際上以上前兩種都會(huì)調(diào)用以下構(gòu)造方法
    //后兩種構(gòu)造方法其實(shí)會(huì)在使用 HandlerThread 的時(shí)候會(huì)用到米间,就不多闡述了
    public Handler(Callback callback, boolean async) {
        //初始化 Looper 并關(guān)聯(lián) MessageQueue 對(duì)象
        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;
    }

這里可以看到,如果 mLooper 為 null膘侮,就是我們常見的運(yùn)行時(shí)異常屈糊,提示我們是否忘記調(diào)用 Looper.prepare()。

那 Looper.myLooper 到底做了什么呢琼了?

    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

    public static void prepare() {
        prepare(true);
    }
    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            //一個(gè)線程只能有一個(gè) Looper
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

可以看到 Looper.myLooper() 方法就是從 ThreadLocal 中取 Looper逻锐,而 Looper.prepare() 就是往 ThreadLocal 中存 Looper。ThreadLocal 是用于線程隔離的雕薪,它可以在不同的線程中互不干擾的存儲(chǔ)數(shù)據(jù)昧诱,當(dāng)某些數(shù)據(jù)是以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本時(shí)就可以采用 ThreadLocal,下文中會(huì)分析 ThreadLocal 的源碼所袁。

回到上面盏档,我們知道在主線程是可以直接使用 Handler 的,可是我們并沒有手動(dòng)調(diào)用 Looper.prepare() 方法呀燥爷,為什么沒有報(bào)錯(cuò)呢蜈亩?原因在于 ActivityThread.main 方法已經(jīng)幫我們做了,所以主線程使用 Handler 時(shí)不需要手動(dòng)調(diào)用 Looper.prepare() 方法前翎,而在子線程使用 Handler 時(shí)就需要手動(dòng)調(diào)用了稚配。

ActivityThread.main 方法源碼如下:

    //ActivityThread 類中的 main 方法
    public static void main(String[] args) {
        //...
        Looper.prepareMainLooper();
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    //Looper 類中的 prepareMainLooper 方法
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }

綜上,Handler 的構(gòu)造方法就是初始化 Looper鱼填,并通過 Looper 關(guān)聯(lián) MessageQueue 對(duì)象药有。

熟悉了 Handler 的構(gòu)造方法,然后就是 Handler 的發(fā)送消息的各種方法苹丸,發(fā)生消息可以分為兩種:

  1. post愤惰、postDelayed
  2. sendMessage、sendMessageDelayed赘理、sendEmptyMessage 等
    //第一種方式
    public final boolean post(Runnable r){
        return  sendMessageDelayed(getPostMessage(r), 0);
    }
    public final boolean postDelayed(Runnable r, long delayMillis){
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }
    public final boolean sendMessageDelayed(Message msg, long delayMillis){
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    //第二種方式
    public final boolean sendMessage(Message msg){
        return sendMessageDelayed(msg, 0);
    }
    public final boolean sendEmptyMessage(int what){
        return sendEmptyMessageDelayed(what, 0);
    }
    public final boolean sendMessageDelayed(Message msg, long delayMillis){
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

可以看到宦言,這兩種方式其實(shí)最后都是調(diào)用 sendMessageAtTime 方法:

    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);
        }
        //轉(zhuǎn)到 MessageQueue 的 enqueueMessage 方法
        return queue.enqueueMessage(msg, uptimeMillis);
    }

這里需要注意的是,在調(diào)用 postDelayed 方法時(shí)傳入的延遲時(shí)間需要再加上 SystemClock.uptimeMillis() 再往下傳商模,而 SystemClock.uptimeMillis() 表示的是系統(tǒng)開機(jī)到當(dāng)前的時(shí)間總數(shù)奠旺,單位是毫秒,并且當(dāng)系統(tǒng)進(jìn)入休眠時(shí)時(shí)間就會(huì)停止施流,但是不受時(shí)鐘縮放或者其他節(jié)能機(jī)制的影響响疚。這個(gè)時(shí)間總和是用來給 Message 排序的,下面會(huì)講到瞪醋。至于為什么是從開機(jī)到當(dāng)前時(shí)間呢忿晕?這個(gè)也很好理解,畢竟只有開機(jī)才會(huì)有消息的分發(fā)银受;那為什么不用系統(tǒng)時(shí)間呢(System.currentTimeMilis)践盼,因?yàn)檫@個(gè)可能因?yàn)橛脩粽{(diào)整了系統(tǒng)時(shí)間而改變鸦采,該值并不可靠。

這里需要注意的是第一種方式咕幻,post 一個(gè) Runnable渔伯,它不像第二種方式直接傳 Message 對(duì)象,所以我們可以看 getPostMessage 到底干了啥肄程?

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

OK锣吼,很清晰。只是把我們傳入的 Runnable 賦值給了 Message 的 callback 成員變量绷耍。同時(shí)吐限,這里實(shí)例化 Message 的時(shí)候是通過 Message.obtain 的方式,我們知道直接 new Message() 也是可行的褂始,那么這兩種有什么區(qū)別嘛诸典?分析 Message 的時(shí)候再說~

這里,為什么我要單獨(dú)提一下 Message 的 callback 成員變量呢崎苗?其實(shí)很有用的狐粱,下面會(huì)用到。

Message 源碼分析

Message 作為消息的載體胆数,它的源碼也不是很多:

public final class Message implements Parcelable {
    
   //消息的標(biāo)示
    public int what;
    //系統(tǒng)自帶的兩個(gè)參數(shù)
    public int arg1;
    public int arg2;
    //處理消息的相對(duì)時(shí)間
    long when;
    
    Bundle data;
    Handler target;
    Runnable callback;
    
    Message next;   //消息池是以鏈表結(jié)構(gòu)存儲(chǔ) Message
    private static Message sPool;   //消息池中的頭節(jié)點(diǎn)
    
    //公有的構(gòu)造方法肌蜻,所以我們可以通過 new Message() 實(shí)例化一個(gè)消息了
    public Message() {
    }
    
    //推薦以這種方式實(shí)例化一個(gè) Message,
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                //取出頭節(jié)點(diǎn)返回
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }
    
    //回收消息
    public void recycle() {
        if (isInUse()) {
            if (gCheckRecycle) {
                throw new IllegalStateException("This message cannot be recycled because it "
                        + "is still in use.");
            }
            return;
        }
        recycleUnchecked();
    }

    void recycleUnchecked() {
        //清空消息的所有標(biāo)志信息
        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++;
            }
        }
    }
}

這里推薦使用 Message.obtain() 方法來實(shí)例化一個(gè) Message必尼,好處在于它會(huì)從消息池中取蒋搜,而避免了重復(fù)創(chuàng)建的開銷。雖然直接實(shí)例化一個(gè) Message 其實(shí)并沒有多大開銷判莉,但是我們知道 Android 是消息驅(qū)動(dòng)的豆挽,這也就說明 Message 的使用量是很大的,所以當(dāng)基數(shù)很大時(shí)券盅,消息池就顯得非常有必要了帮哈。

Looper 源碼分析

前面已經(jīng)分析過了 Looper.myLooper()、Looper.prepare() 锰镀、 Looper.prepareMainLooper() 方法了娘侍,剩下的源碼為:

public final class Looper {
    final MessageQueue mQueue;
    final Thread mThread;
    
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }
    
    public void quit() {
        mQueue.quit(false);
    }

    public void quitSafely() {
        mQueue.quit(true);
    }
    
    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;

        for (;;) {
            //從 MessageQueue 中取消息
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            //通過 Handler 分發(fā)消息
            msg.target.dispatchMessage(msg);
            //回收消息
            msg.recycleUnchecked();
        }
    }
}

毫不夸張的說,消息機(jī)制的核心源碼就在 Looper.loop 方法里泳炉,在這個(gè)方法里做了三件事:

  1. 從 MessageQueue 中取出 Message
  2. 通過 Handler 的 dispatchMessage 分發(fā)消息
  3. 回收消息

它是一個(gè)死循環(huán)憾筏,不斷地從 Message 里取消息,當(dāng)消息為空時(shí)花鹅,根據(jù)注釋可知氧腰,消息隊(duì)列就停止了,也就是說明應(yīng)用已經(jīng)關(guān)閉了。

在死循環(huán)的第一步通過 MessageQueue.next 取消息容贝,可能會(huì)阻塞,具體源碼分析 MessageQueue 的時(shí)候再說之景。

第二步是通過 Handler 來分發(fā)消息斤富,msg.target 即 Handler,看下 Handler.dispatchMessage 做了什么事:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            //通過 handler.postXxx 形式傳入的 Runnable
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                //以 Handler(Handler.Callback) 寫法
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            //以 Handler(){} 內(nèi)存泄露寫法
            handleMessage(msg);
        }
    }

    private static void handleCallback(Message message) {
        message.callback.run();
    }

這里锻狗,可以看到分發(fā)消息的優(yōu)先級(jí)問題满力,Message 的 callback 優(yōu)先級(jí)最高,它是一個(gè) Runnable轻纪,處理消息時(shí)直接 run 就好了油额;然后就是通過 Handler.Callback 寫法,它是由返回值的刻帚,如果返回 true潦嘶,那么在通過 Handler(){} 重寫的方法就不會(huì)執(zhí)行到,這種內(nèi)存泄露的寫法的 handlerMessage 的優(yōu)先級(jí)也是最低的崇众。

MessageQueue 源碼分析

接下來就是 MessageQueue 的源碼了掂僵,不過 MessageQueue 中存在大量的 native 方法。這里只列舉重要方法:

public final class MessageQueue {
    //存消息顷歌,從 Handler.sendMessageAtTime 會(huì)跳到這
    boolean enqueueMessage(Message msg, long when) {
        
        synchronized (this) {
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            //當(dāng)頭節(jié)點(diǎn)為空或當(dāng)前 Message 需要立即執(zhí)行或當(dāng)前 Message 的相對(duì)執(zhí)行時(shí)間比頭節(jié)點(diǎn)早
            //則把當(dāng)前 Message 插入頭節(jié)點(diǎn)
            if (p == null || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                //根據(jù) when 插入鏈表的合適位置
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; 
                prev.next = msg;
            }
        }
        return true;
    }
    
    //取消息
    Message next() {
        for (;;) {
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //取出頭節(jié)點(diǎn) Message
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                //如果消息不為空锰蓬,還要看 Message 是要立即取出還是延遲取出
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            }
        }
    }
}

MessageQueue 有兩個(gè)重要方法,一個(gè)是 enqueueMessage 用于存消息眯漩,存取消息需要根據(jù)它的 when 相對(duì)時(shí)間進(jìn)行鏈表排序芹扭。另外一個(gè)是 next 方法取出消息,取消息的時(shí)候是通過 for 死循環(huán)不斷取消息赦抖,也是取得頭節(jié)點(diǎn)舱卡,不過需要注意該 Message 是否需要立即取出,如果不是摹芙,那就阻塞灼狰,等到時(shí)間到了在取出消息分發(fā)出去。

到這里浮禾,所有的源碼都分析完了交胚,撒花~

常見問題匯總

  1. Handler 的 sendXxx 和 postXxx 的區(qū)別?

    相信你心中已經(jīng)有了答案盈电。

  2. Message 的插入以及回收是如何進(jìn)行的蝴簇,如何實(shí)例化一個(gè) Message 呢?

    相信你心中也已經(jīng)有了答案匆帚。

  3. Looper.loop 死循環(huán)不會(huì)造成應(yīng)用卡死嘛熬词?

    如果按照 Message.next 方法的注釋來解釋的話,如果返回的 Message 為空,就說明消息隊(duì)列已經(jīng)退出了互拾,這種情況下只能說明應(yīng)用已經(jīng)退出了歪今。這也正符合我們開頭所說的,Android 本身是消息驅(qū)動(dòng)颜矿,所以沒有消息幾乎是不可能的事寄猩;如果按照源碼分析,Message.next() 方法可能會(huì)阻塞是因?yàn)槿绻⑿枰舆t處理(sendMessageDelayed等)骑疆,那就需要阻塞等待時(shí)間到了才會(huì)把消息取出然后分發(fā)出去田篇。然后這個(gè) ANR 完全是兩個(gè)概念,ANR 本質(zhì)上是因?yàn)橄⑽吹玫郊皶r(shí)處理而導(dǎo)致的箍铭。同時(shí)泊柬,從另外一方面來說,對(duì)于 CPU 來說诈火,線程無非就是一段可執(zhí)行的代碼兽赁,執(zhí)行完之后就結(jié)束了。而對(duì)于 Android 主線程來說柄瑰,不可能運(yùn)行一段時(shí)間之后就自己退出了闸氮,那就需要使用死循環(huán),保證應(yīng)用不會(huì)退出教沾。這樣一想蒲跨,其實(shí)這樣的安排還是很有道理的。

    這里授翻,推薦一個(gè)說法:https://www.zhihu.com/question/34652589/answer/90344494

  4. Handler 如何避免內(nèi)存泄漏

    Handler 允許我們發(fā)送延時(shí)消息或悲,如果在延時(shí)期間用戶關(guān)閉了 Activity,那么該 Activity 泄漏堪唐。這是因?yàn)閮?nèi)部類默認(rèn)持有外部類的引用巡语。

    解決辦法就是:將 Handler 定義為靜態(tài)內(nèi)部類的形式,在內(nèi)部持有 Activity 的弱引用淮菠,并及時(shí)移除所有消息男公。

    public class MainActivity extends AppCompatActivity {
    
        private MyHandler mMyHandler = new MyHandler(this);
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        private void handleMessage(Message msg) {
    
        }
    
        static class MyHandler extends Handler {
            private WeakReference<Activity> mReference;
    
            MyHandler(Activity reference) {
                mReference = new WeakReference<>(reference);
            }
    
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = (MainActivity) mReference.get();
                if (activity != null) {
                    activity.handleMessage(msg);
                }
            }
        }
    
        @Override
        protected void onDestroy() {
            mMyHandler.removeCallbacksAndMessages(null);
            super.onDestroy();
        }
    }
    
  5. 你知道延時(shí)消息的原理嘛?

    發(fā)送的消息 Message 有一個(gè)屬性 when 用來記錄這個(gè)消息需要處理的時(shí)間合陵,when 的值:普通消息的處理時(shí)間是當(dāng)前時(shí)間枢赔;而延時(shí)消息的處理時(shí)間是當(dāng)前時(shí)間 + delay 的時(shí)間。Message 會(huì)按 when 遞增插入到 MessageQueue拥知,也就是越早時(shí)間的排在越前面踏拜。

    在取消息處理時(shí),如果時(shí)間還沒到低剔,就休眠到指定時(shí)間速梗;如果當(dāng)前時(shí)間已經(jīng)到了肮塞,就返回這個(gè)消息交給 Handler 去分發(fā),這樣就實(shí)現(xiàn)處理延時(shí)消息了姻锁。休眠具體時(shí)間是在 MessageQueue 的 nativePollOnce 函數(shù)中處理枕赵,該函數(shù)的第二個(gè)參數(shù)就是指定需要休眠多少時(shí)間。

  6. 主線程的 Looper#loop() 在死循環(huán)位隶,會(huì)很消耗資源嘛烁设?

    如果沒有消息時(shí),就會(huì)調(diào)用 MessageQueue 的 nativePollOnce 方法讓線程進(jìn)入休眠钓试,當(dāng)消息隊(duì)列沒有消息時(shí),無限休眠副瀑;當(dāng)隊(duì)列的第一個(gè)消息還沒到需要處理的時(shí)間時(shí)弓熏,則休眠時(shí)間為 Message.when - 當(dāng)前時(shí)間。這樣在空閑的時(shí)候主線程也不會(huì)消耗額外的資源了糠睡。而當(dāng)有新消息入隊(duì)時(shí)挽鞠,enqueueMessage 里會(huì)判讀是否需要通過 nativeWake 方法喚醒主線程來處理新消息。喚醒最終是通過往 EventFd 發(fā)起一個(gè)寫操作狈孔,這樣主線程就會(huì)收到一個(gè)可讀事件進(jìn)而從休眠狀態(tài)被喚醒信认。

  7. 你知道 IdleHandler 嘛?

    IdleHandler 是通過 MessageQueue.addIdleHandler 來添加到 MessageQueue 的均抽,前面提到當(dāng) MessageQueue.next 當(dāng)前沒有需要處理的消息時(shí)就會(huì)進(jìn)入休眠嫁赏,而在進(jìn)入休眠之前呢,就會(huì)調(diào)用 IdleHandler 接口里的 boolean queueIdle 方法油挥。這個(gè)方法的返回 true 則調(diào)用后保留潦蝇,下次隊(duì)列空閑時(shí)還會(huì)繼續(xù)調(diào)用;而如果返回 false 調(diào)用完就被 remove 了深寥∪疗梗可以用到做延時(shí)加載,而且是在空閑時(shí)加載惋鹅。

總結(jié)

一張圖說明一切:

image

圖片來源:

Android消息機(jī)制1-Handler(Java層)

更新細(xì)節(jié)

上面已經(jīng)把 Java 層的說清楚了则酝,但是 Native 細(xì)節(jié)并沒有講到。其次闰集,還有一些 Java 層的細(xì)節(jié)沒有講到沽讹。

設(shè)置同步分隔欄: MessageQueue.postSyncBarrier()

同步分割欄的原理其實(shí)很簡單,本質(zhì)上就是通過創(chuàng)建一個(gè) target 成員為 null 的 Message 并插入到消息隊(duì)列中返十,這樣在這個(gè)特殊的 Message 之后的消息就不會(huì)被處理了妥泉,只有當(dāng)這個(gè) Message 被移除后才會(huì)繼續(xù)執(zhí)行之后的 Message。

最經(jīng)典的實(shí)現(xiàn)就是 ViewRootImpl 調(diào)用 scheduleTraversals 方法進(jìn)行視圖更新時(shí)的使用:

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 執(zhí)行分割操作后會(huì)獲取到分割令牌洞坑,使用它可以移除分割欄
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 發(fā)出一個(gè)有異步標(biāo)志的Message盲链,避免被分割
        // postCallback 里面會(huì)把 Message 設(shè)置為異步消息
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

在執(zhí)行 doTraversal 方法后,才會(huì)移除分割欄:

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        performTraversals();

        ...
    }
}

這樣做的原因是,doTraversal 的操作是通過 Handler 進(jìn)行處理的刽沾,然而這個(gè)消息隊(duì)列卻是整個(gè)主線程公用的本慕,比如說四大組件的各個(gè)生命周期的調(diào)用,然后 doTraversal 的內(nèi)容是更新 UI侧漓,這個(gè)任務(wù)無疑是最高優(yōu)先級(jí)的锅尘,所以在這之前,需要確保隊(duì)列中其它同步消息不會(huì)影響到它的執(zhí)行布蔗。

看一下 MessageQueue.postSyncBarrier() 的實(shí)現(xiàn):

public int postSyncBarrier() {
    return postSyncBarrier(SystemClock.uptimeMillis());
}

private int postSyncBarrier(long when) {
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
        // 注意這里藤违,并沒有為target成員進(jìn)行初始化

        Message prev = null;
        Message p = mMessages;
        // 插入到隊(duì)列中
        if (when != 0) {
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}

可以看到,設(shè)置分割欄和普通的 post Message 是一樣的纵揍,不同的是 target 為空顿乒。

分割欄真正起作用的地方是在:

Message next() {
    ...
    for (;;) {
        ...
        // 進(jìn)行隊(duì)列遍歷
           Message msg = mMessages;
        if (msg != null && msg.target == null) {
            do {
                prevMsg = msg;
                msg = msg.next;
            // 如果target為NULL,將會(huì)陷入這個(gè)循環(huán)泽谨,除非是有異步標(biāo)志的消息才會(huì)跳出循環(huán)
            } while (msg != null && !msg.isAsynchronous());
        }
        ...
    }
}

Native 層實(shí)現(xiàn)

首先需要清楚璧榄,在 Java 層,在 Looper 的構(gòu)造方法里面初始化了一個(gè) MessageQueue:

public final class MessageQueue {
    private long mPtr;
    
    MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
    }
}

通過 nativeInit 關(guān)聯(lián)了 Native 層的 MessageQueue吧雹,在 Native 層的 MessageQueue 中創(chuàng)建了 Native 層的 Looper骨杂。

Looper::Looper(bool allowNonCallbacks) {
    mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    rebuildEpollLocked();
}
void Looper::rebuildEpollLocked() {
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);
    struct epoll_event eventItem;
    memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
    eventItem.events = EPOLLIN;
    eventItem.data.fd = mWakeEventFd;
    int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem);
}

首先調(diào)用 epoll_create 來創(chuàng)建一個(gè) epoll 實(shí)例,并且將它保存在 mEpollFd 中雄卷,然后將前面所創(chuàng)建的管道的讀端描述符添加到這個(gè) epoll 實(shí)例中搓蚪,以便可以對(duì)它所描述的管道的寫操作進(jìn)行監(jiān)聽。

Linux 系統(tǒng)的 epoll 機(jī)制是為了同時(shí)監(jiān)聽多個(gè)文件描述符的 IO 讀寫事件而設(shè)計(jì)的丁鹉,它是一個(gè)多路復(fù)用 IO 接口陕凹,類似于 Linux 系統(tǒng)的 select 機(jī)制,但是它是 select 機(jī)制的增強(qiáng)版鳄炉。如果一個(gè) epoll 實(shí)例監(jiān)聽了大量的文件描述符的 IO 讀寫事件杜耙,但是只有少量的文件描述符是活躍的,那么這個(gè) epoll 實(shí)例可以顯著減少 CPU 的使用率拂盯,從而提高系統(tǒng)的并發(fā)處理能力佑女。

可是前面所創(chuàng)建的 epoll 實(shí)例只監(jiān)聽了一個(gè)文件描述符的 IO 寫事件,這值得使用 epoll 機(jī)制來實(shí)現(xiàn)嘛谈竿?其實(shí)团驱,以后我們還可以調(diào)用 C++ 層的 Looper 類的成員函數(shù) addFd 向這個(gè) epoll 實(shí)例中注冊(cè)更多的文件描述符,以便可以監(jiān)聽它們的 IO 讀寫事件空凸,這樣就可以充分利用一個(gè)線程的消息循環(huán)來做其他事情了嚎花。在后面分析 Android 應(yīng)用程序的鍵盤消息處理機(jī)制時(shí),我們就會(huì)看到 C++ 層的 Looper 類的成員函數(shù) addFd 的使用場(chǎng)景呀洲。

在看 MessageQueue 的 next 方法:

public class MessageQueue {
    final Message next() {
        for(;;){
            nativePollOnce(mPtr, nextPollTimeoutMillis);
        }
    }
}

nativePollOnce 是用來檢查當(dāng)前線程的消息隊(duì)列中是否有新的消息需要處理紊选,nextPollTimeoutMills 用來描述當(dāng)消息隊(duì)列沒有新的消息需要處理時(shí)啼止,當(dāng)前線程需要進(jìn)去睡眠等待狀態(tài)的時(shí)間。

// Native 層的 Looper
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
    int result = 0;
    for(;;){
        if(result != 0){
            return result;
        }
        result = pollInner(timeoutMillis);
    }
}

int Looper::pollInner(int timeoutMillis) {
    int result = POLL_WAKE;
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
    int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
        for (int i = 0; i < eventCount; i++) {
        int fd = eventItems[i].data.fd;
        uint32_t epollEvents = eventItems[i].events;
        if (fd == mWakeEventFd) {
            if (epollEvents & EPOLLIN) {
                awoken();
            }
        }
    }
}

如果監(jiān)聽的文件描述符沒有發(fā)生 IO 讀寫事件兵罢,那么當(dāng)前線程就會(huì)在 epoll_wait 中進(jìn)入睡眠等待狀態(tài)献烦,等待的時(shí)間由最后一個(gè)參數(shù) timeoutMillis 來指定。

從函數(shù) epoll_wait 返回來之后卖词,接下來第 11 行到第 21 行的 for 循環(huán)就檢查是哪一個(gè)文件描述符發(fā)生了 IO 讀寫事件巩那,如果是 mWakeReadPipeFd 并且發(fā)生的 IO 讀寫事件的類型是 EPOLLIN,就說明其他線程向當(dāng)前線程所關(guān)聯(lián)的一個(gè)管道寫入了新的數(shù)據(jù)此蜈。

當(dāng)其他線程向當(dāng)前線程的消息隊(duì)列發(fā)送一個(gè)消息之后即横,它們就會(huì)向與當(dāng)前線程所關(guān)聯(lián)的一個(gè)管道寫入一個(gè)新的數(shù)據(jù),目的就是將當(dāng)前線程喚醒裆赵,以便它可以及時(shí)的去處理剛剛發(fā)送到它的消息隊(duì)列的消息令境。

在分析 Java 層的 MessageQueue#enqueueMessage 時(shí),我們知道顾瞪,一個(gè)線程講一個(gè)消息插入到一個(gè)消息隊(duì)列之后,可能需要將目標(biāo)線程喚醒抛蚁,這需要分兩種情況來討論:

  1. 插入的消息在目標(biāo)隊(duì)列中間
  2. 插入的消息在目標(biāo)隊(duì)列頭部

第一種情況下陈醒,由于保存在目標(biāo)消息隊(duì)列頭部的消息沒有發(fā)生變化,因此當(dāng)前線程無論如何都不需要對(duì)目標(biāo)線程執(zhí)行喚醒操作瞧甩。

第二種情況钉跷,由于保存在目標(biāo)消息隊(duì)列頭部的消息發(fā)生了變化,因此肚逸,當(dāng)前線程就需要將目標(biāo)線程喚醒爷辙,以便它可以對(duì)保存在目標(biāo)消息隊(duì)列頭部的新消息進(jìn)行處理。但是如果這時(shí)目標(biāo)線程不是正處于睡眠等待狀態(tài)朦促,那么當(dāng)前線程就不需要對(duì)它進(jìn)行喚醒膝晾,當(dāng)前線程是否處于睡眠等待狀態(tài)由 mBlocked 來記錄。

// Native 層 MessageQueue#wake
void NativeMessageQueue::wake() {
    mLooper->wake();
}

void Looper::wake() {
    uint64_t inc = 1;
    ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t)));
}

也就是調(diào)用 write 函數(shù)寫一個(gè) 1务冕,這時(shí)候目標(biāo)線程就會(huì)因?yàn)檫@個(gè)管道發(fā)生了一個(gè) IO 寫事件而被喚醒血当。

消息的處理過程就簡單了:當(dāng)一個(gè)線程沒有新的消息需要處理時(shí),它就會(huì)在 C++ 層的 Looper 類的成員函數(shù) pollInner 中進(jìn)入睡眠等待狀態(tài)禀忆,因此臊旭,當(dāng)這個(gè)線程有新的消息需要處理時(shí),它首先會(huì)在 C++ 層的 Looper 類的成員函數(shù) pollInnter 中被喚醒箩退,然后沿著之前的調(diào)用路徑一直返回到 Java 層的 Looper 類的靜態(tài)成員函數(shù) loop 中离熏,最后就可以對(duì)新的消息進(jìn)行處理了。

Native 層分析完畢戴涝,總的來說就是利用 Linux 的 epoll 機(jī)制滋戳。

參考

Android SDK 26 源碼钻蔑。

Handler 都沒搞懂,拿什么去跳槽半使稀矢棚?

Android消息機(jī)制,你真的了解Handler嗎府喳?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蒲肋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子钝满,更是在濱河造成了極大的恐慌兜粘,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弯蚜,死亡現(xiàn)場(chǎng)離奇詭異孔轴,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)碎捺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門路鹰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人收厨,你說我怎么就攤上這事晋柱。” “怎么了诵叁?”我有些...
    開封第一講書人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵雁竞,是天一觀的道長。 經(jīng)常有香客問我拧额,道長碑诉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任侥锦,我火速辦了婚禮进栽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘恭垦。我一直安慰自己泪幌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開白布署照。 她就那樣靜靜地躺著祸泪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪建芙。 梳的紋絲不亂的頭發(fā)上没隘,一...
    開封第一講書人閱讀 49,985評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音禁荸,去河邊找鬼右蒲。 笑死阀湿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瑰妄。 我是一名探鬼主播陷嘴,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼间坐!你這毒婦竟也來了灾挨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤竹宋,失蹤者是張志新(化名)和其女友劉穎劳澄,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蜈七,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秒拔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了飒硅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片砂缩。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖三娩,靈堂內(nèi)的尸體忽然破棺而出庵芭,到底是詐尸還是另有隱情,我是刑警寧澤尽棕,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站彬伦,受9級(jí)特大地震影響滔悉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜单绑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一回官、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧搂橙,春花似錦歉提、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至废离,卻和暖如春侄泽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜻韭。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來泰國打工悼尾, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柿扣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓闺魏,卻偏偏與公主長得像未状,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子析桥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

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