死磕Android_Handler機制你需要知道的一切

1. 前言

安卓在子線程中不能更新UI,所以大部分情況下,我們需要藉助Handler切換到主線程中去更新消息.而消息機制(即Handler那一坨)在安卓中的地位非常非常重要,我們需要詳細瞭解其原理.這一塊,學過很多次,但是,我覺得還是再學億次,寫成博客輸出.希望對大家有所幫助,有一些新的感悟.

2. ThreadLocal工作原理

ThreadLocal主要是可以在不同的線程中存儲不同的數據,它是將數據存儲在線程內部的,其他線程無法訪問.對於同一個ThreadLocal對象,不同的線程有不同的數據,這些數據互不干擾.比如Handler機制中的Looper,Looper的作用域是線程,ThreadLocal可以將Looper存儲在線程中,然後其他線程是無法訪問到這個線程中的Looper的,只供當前線程自己內部使用.

2.1 ThreadLocal demo

下面簡單舉個例子:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static final ThreadLocal<Integer> INTEGER_THREAD_LOCAL = new ThreadLocal<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //設置ThreadLocal裏面的數據爲1
        INTEGER_THREAD_LOCAL.set(1);
        //獲取ThreadLocal裏面的數據
        Log.w(TAG, "主線程" + INTEGER_THREAD_LOCAL.get());

        new Thread(new Runnable() {
            @Override
            public void run() {
                //獲取ThreadLocal裏面的數據,但是需要注意的是,這裏獲取的數據是子線程中數據,因爲沒有進行初始化,這裏獲取到的數據是null
                Log.w(TAG, "線程1 " + INTEGER_THREAD_LOCAL.get());
            }
        }, "線程1").start();

    }
}

我先在主線程中將INTEGER_THREAD_LOCAL的值設置爲1(相當於主線程中的INTEGER_THREAD_LOCAL值爲1),然後再開啓子線程並在子線程中獲取INTEGER_THREAD_LOCAL的值.因爲子線程中沒有給INTEGER_THREAD_LOCAL附值,所以是null.

2019-05-19 11:12:54.353 12364-12364/com.xfhy.handlerdemo W/MainActivity: 主線程1
2019-05-19 11:12:54.353 12364-12383/com.xfhy.handlerdemo W/MainActivity: 線程1 null

需要注意到的是INTEGER_THREAD_LOCALfinal static的,這裏的ThreadLocal是同一個對象,但是在主線程中獲取到的數據和在子線程中獲取到的數據卻不一樣. 這裏的demo也就證明了: ThreadLocal在不同的線程中存儲的數據,互不干擾,相互獨立.

2.2 ThreadLocal源碼理解

我們從ThreadLocal的set方法開始深入下去(一般讀源碼是從使用處的API開始,這樣會更輕鬆地理清思路)

public void set(T value) {
    //1. 獲取當前線程
    Thread t = Thread.currentThread();
    //2. 獲取當前線程的threadLocals屬性,threadLocals是Thread類裏面的一個屬性,是ThreadLocalMap類型的,專門用來存當前線程的私有數據,這些數據由ThreadLocal維護
    ThreadLocalMap map = getMap(t);
    
    //3. 第一次設置值的時候map肯定是爲null的,初始化了之後map纔不爲null
    //第一次會去createMap()
    if (map != null)
        //4. 將當前ThreadLocal對象和value的值存入map中
        map.set(this, value);
    else
        //4. 這裏將初始化map,並且將value值放到map中.
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

ThreadLocal在設置數據的時候,首先是獲取當前線程的threadLocals屬性,threadLocals是Thread類裏面的一個屬性,是ThreadLocalMap類型的,專門用來存當前線程的私有數據,這些數據由ThreadLocal來維護的. 當第一次設置值的時候,需要初始化map,並將value值放入map中.下面來看一下這部分代碼

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//下面是ThreadLocalMap的代碼

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 * table是ThreadLocalMap裏面存儲數據的地方,如果在數組長度不夠用的時候,會擴容.
 存儲的方式是靠hash值爲數組的索引,將value放到該索引處.
 */
private Entry[] table;

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //初始化table數據數組
    table = new Entry[INITIAL_CAPACITY];
    //計算hash值->存儲數據的索引
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

//將value值存入map中,key爲ThreadLocal
private void set(ThreadLocal<?> key, Object value) {
    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

可以看到createMap方法中就是初始化ThreadLocalMap,而ThreadLocalMap的底部其實是一個數組,它是利用hash值來計算索引,然後存儲數據到該索引處的方式.

此處需要注意的是,我們可以看到ThreadLocal是將數據存儲到Thread的一個threadLocals屬性上面,這個threadLocals每個線程獨有的,那麼存儲數據肯定互不干擾啊,完美.

3. MessageQueue 消息隊列

Handler中的消息隊列,也就是MessageQueue.從名字可以看出這是一個隊列,但是它的底層卻是單鏈表結構.因爲鏈表結構比較適合插入和刪除操作.這個MessageQueue的查詢就是next()方法,它的查詢伴隨着刪除.

3.1 消息隊列插入

消息隊列的插入,對應着的是enqueueMessage方法

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        ....

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        
        //如果  1. 鏈表爲空 || 2. when是0,表示立即需要處理的消息 || 3. 當前需要插入的消息比之前的第一個消息更緊急,在更短的時間內就需要處理
        //滿足上面這3個條件中的其中一個,那麼就是插入在鏈表的頭部
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            // Inserted within the middle of the queue.  Usually we don't have to wake
            // up the event queue unless there is a barrier at the head of the queue
            // and the message is the earliest asynchronous message in the queue.
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            //從頭部開始,直到找出列表的最後一個元素,方便鏈表插入
            for (;;) {
                prev = p;
                p = p.next;
                //找到合適的時間點,插入到這裏
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            //把新的消息插入在鏈表尾部
            msg.next = p; // invariant: p == prev.next
            prev.next = msg;
        }

        // We can assume mPtr != 0 because mQuitting is false.
        if (needWake) {
            // 激活消息隊列去獲取下一個消息  這裏是一個native方法
            nativeWake(mPtr);
        }
    }
    return true;
}

核心內容爲消息列表的插入,也就是鏈表的插入,插入數據的時候是有一定規則的,當滿足下面這3個條件中的其中一個,那麼就是插入在鏈表的頭部

  1. 鏈表爲空
  2. when是0,表示立即需要處理的消息
  3. 當前需要插入的消息比之前的第一個消息更緊急,在更短的時間內就需要處理

其他情況則是插入在鏈表中的合適的位置,找到一個合適的時間點.

3.2 消息隊列查詢(next)

MessageQueue的next方法,也就是獲取下一個消息,這個方法可能會阻塞,當消息隊列沒有消息的時候.直到有消息,然後就會被喚醒,然後繼續取消息.

但是這裏的阻塞是不會ANR的,真正導致ANR的是因爲在handleMessage方法中處理消息時阻塞了主線程太久的時間.這裏的原因,後面再解釋.

Message next() {
    // Return here if the message loop has already quit and been disposed.
    // This can happen if the application tries to restart a looper after quit
    // which is not supported.
    final long ptr = mPtr;
    if (ptr == 0) {
        return null;
    }

    int pendingIdleHandlerCount = -1; // -1 only during first iteration
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        //當消息隊列爲空時,這裏會導致阻塞,直到有消息加入消息隊列,纔會恢復
        //這裏是native方法,利用的是linux的epoll機制阻塞
        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;
            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());
            }
            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;
                    if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }

            .....
        }

       ......
    }
}

核心內容就是取消息隊列的第一個元素(即鏈表的第一個元素),然後將該Message取出來之後,將它從消息隊列中刪除.

4. Looper

Looper在消息機制中主要扮演着消息循環的角色,有消息來了,Looper就取出來,分發.沒有消息,Looper就阻塞在那裏,直到有消息爲止.

4.1 Looper初始化

先來看一下,Looper的構造方法

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

這個構造方法是私有化的,只能在內部調用,直接在裏面初始化了MessageQueue和獲取當前線程.構造方法只會在prepare方法中被調用.

public static void prepare() {
    prepare(true);
}

//sThreadLocal是用`static final`修飾的,意味着sThreadLocal只有一個,但是它卻可以在不同的線程中存儲不同的Looper,妙啊
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {
    //如果說當前線程之前初始化過ThreadLocal,裏面有Looper,那麼就報錯
    //意思就是prepare方法只能調用一次
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //初始化ThreadLocal,將一個Looper存入其中
    sThreadLocal.set(new Looper(quitAllowed));
}

private static Looper sMainLooper;
//這個方法是主線程中調用的,準備主線程的Looper.也是隻能調用一次.
public static void prepareMainLooper() {
    //先準備一下
    prepare(false);
    synchronized (Looper.class) {
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        //將初始化之後的Looper賦值給sMainLooper,sMainLooper是static的,可能是爲了方便使用吧
        sMainLooper = myLooper();
    }
}

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

prepare方法的職責是初始化ThreadLocal,將Looper存儲在其中,一個線程只能有一個Looper,不能重複初始化.sThreadLocal是用static final修飾的,意味着sThreadLocal只有一個,但是它卻可以在不同的線程中存儲不同的Looper.而且官方還提供了主線程初始化Looper的專用方法prepareMainLooper.主線程就是主角,還單獨把它的Looper存到靜態的sMainLooper中.

4.2 Looper#loop

下面開始進入Looper的核心方法loop(),我們知道loop方法就是死循環不斷得從MessageQueue中去取數據.看看方法中的一些細節.

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    //1. 首先是獲取當前線程的Looper  穩,不同的線程,互不干擾
    final Looper me = myLooper();
    
    //2. 如果當前線程沒有初始化,那肯定是要報錯的
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    
    //3. 取出當前線程Looper中存放的MessageQueue
    final MessageQueue queue = me.mQueue;

    .....
    for (;;) {
        //4. 從MessageQueue中取消息,當然 這裏是可能被阻塞的,如果MessageQueue中沒有消息可以取的話
        Message msg = queue.next(); // might block
        
        //5. 如果消息隊列想退出,並且MessageQueue中沒有消息了,那麼這裏的msg肯定是null
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        .....
        //6. 注意啦,這裏開始分發當前從消息隊列中取出來的消息
        msg.target.dispatchMessage(msg);
        ......
    }
}

loop方法非常重要,它首先取到當前線程的Looper,再從Looper中獲取MessageQueue,開啓一個死循環,從MessageQueue的next方法中獲取新的Message.但是在next方法調用的過程中是可能被阻塞的,這裏是利用了linux的epoll機制.取到了消息之後分發下去.分發給Handler的handleMessage方法進行處理. 然後又開始了一個新的輪迴,繼續取新的消息(也可能是阻塞在那裏等).

下面來看一下消息的分發

//Message裏面的代碼

//Message裏的target其實就是發送該消息的那個Handler,666
Handler target;
//下一個消息的引用
Message next;
//Handler裏面的代碼
public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

兄弟萌,它來啦,還是那個熟悉的handleMessage方法,在Looper的loop方法中由Message自己通過Message裏面的target(handler)調用該Handler自己的handleMessage方法.完成了消息的分發. 如果這裏有Callback的話,就通過Callback接口分發消息.

5. Handler

Handler的作用其實就是發送消息,然後接收消息.Handler中任何的發送消息的方法最後都會調用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);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

sendMessageAtTime方法很簡單,其實就是將消息插入MessageQueue.而在Message插入MessageQueue的過程之前,先將Handler的引用存入Message中,方便待會兒分發消息事件,機智機智!

6. 用一句話總結一下安卓的消息機制

在安卓消息機制中,ThreadLocal拿來存儲Looper,而MessageQueue是存儲在Looper中的.所以我們可以在子線程中通過主線程的Handler發送消息,而Looper(主線程中的)在主線程中取出消息,分發給主線程的Handler的handleMessage方法.

7. 消息機制在主線程中的應用

7.1 關於主線程中的死循環

我們知道ActivityThread其實就是我們的主線程,首先我們來看一段代碼,ActivityThread的main方法:

public static void main(String[] args) {
    ......
    
    //注意看,在main方法的開始,在主線程中就準備好了主線程中的Looper,存入ThreadLocal中.所以我們平時使用Handler的時候並沒有調用prepare方法也不會報錯
    Looper.prepareMainLooper();

    ......
    //直接在主線程中調用了loop方法,並且陷入死循環中,不斷地取消息,不斷地處理消息,無消息時就阻塞.  
    //嘿,你還別說,這裏這個方法還必須要死循環下去纔好,不然就會執行到下面的throw new RuntimeException語句報出錯誤
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

主線程一直處在一個Looper的loop循環中,有消息就會去處理.無消息,則阻塞.

7.2 主線程死循環到底是要接收和處理什麼消息?

有什麼騷東西非要進行死循環才能處理呢?首先我們想想,既然ActivityThread開啓了Looper的loop,那麼肯定有Handler來接收和處理消息,我們一探究竟:

private class H extends Handler {
    public static final int LAUNCH_ACTIVITY = 100;
    public static final int PAUSE_ACTIVITY = 101;
    public static final int PAUSE_ACTIVITY_FINISHING = 102;
    public static final int STOP_ACTIVITY_SHOW = 103;
    public static final int STOP_ACTIVITY_HIDE = 104;
    public static final int SHOW_WINDOW = 105;
    public static final int HIDE_WINDOW = 106;
    public static final int RESUME_ACTIVITY = 107;
    public static final int SEND_RESULT = 108;
    public static final int DESTROY_ACTIVITY = 109;
    public static final int BIND_APPLICATION = 110;
    public static final int EXIT_APPLICATION = 111;
    public static final int NEW_INTENT = 112;
    public static final int RECEIVER = 113;
    public static final int CREATE_SERVICE = 114;
    public static final int SERVICE_ARGS = 115;
    public static final int STOP_SERVICE = 116;
    ...
}

名場面,上面就是API 28以前ActivityThread.H的老樣子,爲什麼是API 28以前?因爲在API 28中重構了H類,把100到109這10個用於Activity的消息,都合併爲159這個消息,消息名爲EXECUTE_TRANSACTION(抽象爲ClientTransactionItem,有興趣瞭解的看這裏)。

在H類中定義了很多消息類型,包含了安卓四大組件的啓動和停止.ActivityThread通過ApplicationThread與AMS進行進程間通信,AMS完成ActivityThread的請求後會回調ApplicationThread中的Binder方法,然後ApplicationThread會向H發送消息,H收到消息就開始在主線程中執行,開始執行諸如Activity的啓動停止等動作,以上就是主線程的消息循環模型.

既然我們知道了主線程是這樣啓動Activity的,那麼我們是不是可以搞點騷操作???俗稱黑科技的插件化:我們Hook掉H類的mCallback對象,攔截這個對象的handleMessage方法。在此之前,我們把插件中的Activity替換爲StubActtivty,那麼現在,我們攔截到handleMessage方法,再把StubActivity換回爲插件中的Activity.當前這只是API 28之前的操作,更多詳情請看這裏

8. 主線程爲什麼沒有被loop阻塞

既然主線程中的main方法內調用了Looper的loop方法不斷地死循環取消息,而且當消息隊列爲空的時候還會被阻塞.那爲什麼主線程中當沒有消息的時候怎麼不卡呢?

此處引出一國外網友的回答,短小精湛.問題回答原地址

簡短版答案:
nativePollOnce方法是用來等待下一個消息可用時的,下一個消息可用則不會再繼續阻塞,如果在這個調用中花費的時間很長,那你的主(UI)線程沒有真正的工作要做,並且等待下一個事件處理。沒必要擔心阻塞問題。

完整版的答案:
因爲主線程負責繪製UI和處理各種事件,所以Runnable有一個處理所有這些事件的循環。循環由Looper管理,其工作非常簡單:它處理MessageQueue中的所有消息。消息被添加到隊列中,例如響應輸入事件,幀渲染回調甚至您自己的Handler.post調用。有時主線程沒有工作要做(即隊列中沒有消息),這可能發生在例如剛完成渲染單幀後(線程剛剛繪製了一幀並準備好下一幀,只需等待一段時間)。 MessageQueue類中的兩個Java方法對我們來說很有趣:Message next()和boolean enqueueMessage(Message,long)。消息next(),顧名思義,接收並返回隊列中的下一條消息。如果隊列爲空(並且沒有任何內容可以返回),則該方法調用native void nativePollOnce(long,int),該塊將阻塞,直到添加新消息。此時你可能會問nativePollOnce如何知道何時醒來。這是一個非常好的問題。將Message添加到隊列時,框架會調用enqueueMessage方法,該方法不僅會將消息插入隊列,還會調用native static void nativeWake(long),如果需要喚醒隊列的話。 nativePollOnce和nativeWake的核心魔力發生在native(實際上是C ++)代碼中。 Native MessageQueue使用名爲epoll的Linux系統調用,該調用允許監視IO事件的文件描述符。 nativePollOnce在某個文件描述符上調用epoll_wait,而nativeWake寫入描述符,這是IO操作之一,epoll_wait等待。然後內核從等待狀態中取出epoll等待線程,並且線程繼續處理新消息。如果您熟悉Java的Object.wait()和Object.notify()方法,您可以想象nativePollOnce是Object.wait()和NativeWake for Object.notify()的粗略等價物,因爲它們的實現完全不同:nativePollOnce使用epoll,Object.wait()使用futex Linux調用。值得注意的是,nativePollOnce和Object.wait()都不會浪費CPU週期,因爲當線程進入任一方法時,它會因線程調度而被禁用。如果這些方法實際上浪費了CPU週期,那麼所有空閒應用程序將使用100%的CPU,加熱並降低設備的速度。

翻譯的不是很好,英語好的同學還是看原版吧,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章