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

1. 前言

安卓在子線程中不能更新UI,因此大部分狀況下,咱們須要藉助Handler切換到主線程中去更新消息.而消息機制(即Handler那一坨)在安卓中的地位很是很是重要,咱們須要詳細瞭解其原理.這一塊,學過不少次,可是,我以爲仍是再學億次,寫成博客輸出.但願對你們有所幫助,有一些新的感悟.html

2. ThreadLocal工做原理

ThreadLocal主要是能夠在不一樣的線程中存儲不一樣的數據,它是將數據存儲在線程內部的,其餘線程沒法訪問.對於同一個ThreadLocal對象,不一樣的線程有不一樣的數據,這些數據互不干擾.好比Handler機制中的Looper,Looper的做用域是線程,ThreadLocal能夠將Looper存儲在線程中,而後其餘線程是沒法訪問到這個線程中的Looper的,只供當前線程本身內部使用.java

2.1 ThreadLocal demo

下面簡單舉個例子:linux

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.android

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開始,這樣會更輕鬆地理清思路)bash

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中.下面來看一下這部分代碼app

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每一個線程獨有的,那麼存儲數據確定互不干擾啊,完美.less

3. MessageQueue 消息隊列

Handler中的消息隊列,也就是MessageQueue.從名字能夠看出這是一個隊列,可是它的底層倒是單鏈表結構.由於鏈表結構比較適合插入和刪除操做.這個MessageQueue的查詢就是next()方法,它的查詢伴隨着刪除.async

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,加熱並下降設備的速度。

翻譯的不是很好,英語好的同窗仍是看原版吧,,,,,,,,,

相關文章
相關標籤/搜索