源碼深度解析 Handler 機(jī)制及應(yīng)用

本文以源碼分析+實(shí)際應(yīng)用的形式老客,詳細(xì)講解了 Handler 機(jī)制的原理拭荤,以及在開發(fā)中的使用場景和要注意的地方茵臭。

一、基本原理回顧

在 Android 開發(fā)中舅世,Handler及相關(guān)衍生類的應(yīng)用經(jīng)常用到旦委,Android的運(yùn)行也是建立在這套機(jī)制上的奇徒,所以了解其中的原理細(xì)節(jié),以及其中的坑對于每位開發(fā)者來說都是非常有必要的缨硝。Handler機(jī)制的五個組成部分:Handler摩钙、Thread(ThreadLocal)、Looper查辩、MessageQueue腺律、Message。

01.jpg

1宜肉、Thread(ThreadLocal)

Handler機(jī)制用到的跟Thread相關(guān)的匀钧,而根本原因是Handler必須和對應(yīng)的Looper綁定,而Looper的創(chuàng)建和保存是跟Thread一一對應(yīng)的谬返,也就是說每個線程都可以創(chuàng)建唯一一個且互不相關(guān)的Looper之斯,這是通過ThreadLocal來實(shí)現(xiàn)的,也就是說是用ThreadLocal對象來存儲Looper對象的遣铝,從而達(dá)到線程隔離的目的佑刷。

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

2、Handler

Handler()
 
Handler(Callback callback)
 
Handler(Looper looper)
 
Handler(Looper looper, Callback callback)
 
Handler(boolean async)
 
Handler(Callback callback, boolean async)
 
Handler(Looper looper, Callback callback, boolean async)

2.1 創(chuàng)建Handler大體上有兩種方式:

一種是不傳Looper

這種就需要在創(chuàng)建Handler前酿炸,預(yù)先調(diào)用Looper.prepare來創(chuàng)建當(dāng)前線程的默認(rèn)Looper瘫絮,否則會報錯。

一種是傳入指定的Looper

這種就是Handler和指定的Looper進(jìn)行綁定填硕,也就是說Handler其實(shí)是可以跟任意線程進(jìn)行綁定的麦萤,不局限于在創(chuàng)建Handler所在的線程里。

2.2 async參數(shù)

這里Handler有個async參數(shù)扁眯,通過這個參數(shù)表明通過這個Handler發(fā)送的消息全都是異步消息壮莹,因?yàn)樵诎严喝腙犃械臅r候,會把這個標(biāo)志設(shè)置到message里.這個標(biāo)志是全局的姻檀,也就是說通過構(gòu)造Handler函數(shù)傳入的async參數(shù)命满,就確定了通過這個Handler發(fā)送的消息都是異步消息,默認(rèn)是false绣版,即都是同步消息胶台。至于這個異步消息有什么特殊的用途,我們在后面講了屏障消息后杂抽,再聯(lián)系起來講诈唬。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

2.3 callback參數(shù)

這個回調(diào)參數(shù)是消息被分發(fā)之后的一種回調(diào),最終是在msg調(diào)用Handler的dispatchMessage時默怨,根據(jù)實(shí)際情況進(jìn)行回調(diào):

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

3讯榕、****Looper

用于為線程運(yùn)行消息循環(huán)的類。默認(rèn)線程沒有與它們相關(guān)聯(lián)的Looper;所以要在運(yùn)行循環(huán)的線程中調(diào)用prepare()愚屁,然后調(diào)用loop()讓它循環(huán)處理消息济竹,直到循環(huán)停止。

private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
     
    public static void loop() {
        ...
     
        for (;;) {
        ...
        }
         
        ...
    }
 
class LooperThread extends Thread {
    public Handler mHandler;
 
    public void run() {
         
        Looper.prepare();  
         
        mHandler = new Handler() { 
            public void handleMessage(Message msg) {
                 
                Message msg=Message.obtain();
            }
        };
        
        Looper.loop(); 
    }
}

既然在使用Looper前霎槐,必須調(diào)用prepare創(chuàng)建Looper送浊,為什么我們平常在主線程里沒有看到調(diào)用prepare呢?這是因?yàn)锳ndroid主線程創(chuàng)建的時候丘跌,在ActivityThread的入口main方法里就已經(jīng)默認(rèn)創(chuàng)建了Looper袭景。

public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
    ...
}

我們再來回顧一下Looper相關(guān)類的之間的聯(lián)系:

02.jpg

4、MessageQueue 和 Message

MessageQueue是一個消息隊列闭树,Handler將Message發(fā)送到消息隊列中耸棒,消息隊列會按照一定的規(guī)則取出要執(zhí)行的Message。Message并不是直接加到MessageQueue的报辱,而是通過Handler對象和Looper關(guān)聯(lián)到一起与殃。

MessageQueue里的message是按時間排序的,越早加入隊列的消息放在隊列頭部碍现,優(yōu)先執(zhí)行幅疼,這個時間就是sendMessage的時候傳過來的,默認(rèn)是用的當(dāng)前系統(tǒng)從啟動到現(xiàn)在的非休眠的時間SystemClock.uptimeMillis()昼接。

sendMessageAtFrontOfQueue 這個方法傳入的時間是0爽篷,也就是說調(diào)用這個方法的message肯定會放到對消息隊列頭部,但是這個方法不要輕易用慢睡,容易引發(fā)問題逐工。

存到MessageQueue里的消息可能有三種:同步消息,異步消息一睁,屏障消息钻弄。

image

[圖片上傳失敗...(image-4756c9-1606874479076)]

4.1 同步消息

我們默認(rèn)用的都是同步消息,即前面講Handler里的構(gòu)造函數(shù)參數(shù)的async參數(shù)默認(rèn)是false者吁,同步消息在MessageQueue里的存和取完全就是按照時間排的,也就是通過msg.when來排的饲帅。

4.2 異步消息

異步消息就是在創(chuàng)建Handler如果傳入的async是true或者發(fā)送來的Message通過msg.setAsynchronous(true);后的消息就是異步消息复凳,異步消息的功能要配合下面要講的屏障消息才有效,否則和同步消息是一樣的處理灶泵。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    // 這個mAsynchronous就是在創(chuàng)建Handler的時候傳入async參數(shù)
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

4.3 Barrier(屏障)消息

屏障(Barrier) 是一種特殊的Message育八,它最大的特征就是target為null(只有屏障的target可以為null,如果我們自己設(shè)置Message的target為null的話會報異常)赦邻,并且arg1屬性被用作屏障的標(biāo)識符來區(qū)別不同的屏障髓棋。屏障的作用是用于攔截隊列中同步消息,放行異步消息。

那么屏障消息是怎么被添加和刪除的呢按声?我們可以看到在MessageQueue里有添加和刪除屏障消息的方法:


private int postSyncBarrier(long when) {
    // Enqueue a new sync barrier token.
    // We don't need to wake the queue because the purpose of a barrier is to stall it.
    synchronized (this) {
        final int token = mNextBarrierToken++;
        final Message msg = Message.obtain();
        msg.markInUse();
        msg.when = when;
        msg.arg1 = token;
 
        Message prev = null;
        Message p = mMessages;
        if (when != 0) {
            // 這里是說如果p指向的消息時間戳比屏障消息小膳犹,說明這個消息比屏障消息先進(jìn)入隊列,
            // 那么這個消息不應(yīng)該受到屏障消息的影響(屏障消息只影響比它后加入消息隊列的消息)签则,找到第一個比屏障消息晚進(jìn)入的消息指針
            while (p != null && p.when <= when) {
                prev = p;
                p = p.next;
            }
        }
        // 上面找到第一個比屏障消息晚進(jìn)入的消息指針之后须床,把屏障消息插入到消息隊列中,也就是屏障消息指向第一個比它晚進(jìn)入的消息p渐裂,
        // 上一個比它早進(jìn)入消息隊列的prev指向屏障消息豺旬,這樣就完成了插入。
        if (prev != null) { // invariant: p == prev.next
            msg.next = p;
            prev.next = msg;
        } else {
        // 如果prev是null柒凉,說明上面沒有經(jīng)過移動族阅,也就是屏障消息就是在消息隊列的頭部了。
            msg.next = p;
            mMessages = msg;
        }
        return token;
    }
}
 
public void removeSyncBarrier(int token) {
    // Remove a sync barrier token from the queue.
    // If the queue is no longer stalled by a barrier then wake it.
    synchronized (this) {
        Message prev = null;
        Message p = mMessages;
        // 前面在插入屏障消息后會生成一個token膝捞,這個token就是用來刪除該屏障消息用的坦刀。
        // 所以這里通過判斷target和token來找到該屏障消息,從而進(jìn)行刪除操作
        // 找到屏障消息的指針p
        while (p != null && (p.target != null || p.arg1 != token)) {
            prev = p;
            p = p.next;
        }
        if (p == null) {
            throw new IllegalStateException("The specified message queue synchronization "
                    + " barrier token has not been posted or has already been removed.");
        }
        final boolean needWake;
        // 上面找到屏障消息的指針p后绑警,把前一個消息指向屏障消息的后一個消息求泰,這樣就把屏障消息移除了
        if (prev != null) {
            prev.next = p.next;
            needWake = false;
        } else {
            mMessages = p.next;
            needWake = mMessages == null || mMessages.target != null;
        }
        p.recycleUnchecked();
 
        // If the loop is quitting then it is already awake.
        // We can assume mPtr != 0 when mQuitting is false.
        if (needWake && !mQuitting) {
            nativeWake(mPtr);
        }
    }
}

4.4 屏障消息的作用

說完了屏障消息的插入和刪除,那么屏障消息在哪里起作用的计盒?它跟前面提到的異步消息又有什么關(guān)聯(lián)呢渴频?我們可以看到MessageQueue的next方法里有這么一段:

// 這里就是判斷當(dāng)前消息是否是屏障消息,判斷依據(jù)就是msg.target==null, 如果存在屏障消息北启,那么在它之后進(jìn)來的消息中卜朗,
// 只把異步消息放行繼續(xù)執(zhí)行,同步消息阻塞咕村,直到屏障消息被remove掉场钉。
if (msg != null && msg.target == null) {
    // Stalled by a barrier.  Find the next asynchronous message in the queue.
    do {
        prevMsg = msg;
        msg = msg.next;
        // 這里的isAsynchronous方法就是前面設(shè)置進(jìn)msg的async參數(shù),通過它判斷如果是異步消息懈涛,則跳出循環(huán)逛万,把該異步消息返回
        // 否則是同步消息,把同步消息阻塞批钠。
    } while (msg != null && !msg.isAsynchronous());
}
03.jpg

4.5 屏障消息的實(shí)際應(yīng)用

屏障消息的作用是把在它之后入隊的同步消息阻塞宇植,但是異步消息還是正常按順序取出執(zhí)行,那么它的實(shí)際用途是什么呢埋心?我們看到ViewRootImpl.scheduleTraversals()用到了屏障消息和異步消息指郁。

TraversalRunnable的run(),在這個run()中會執(zhí)行doTraversal(),最終會觸發(fā)View的繪制流程:measure(),layout(),draw()拷呆。為了讓繪制流程盡快被執(zhí)行闲坎,用到了同步屏障技術(shù)疫粥。

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        // 這里先將主線程的MessageQueue設(shè)置了個消息屏障
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 這里發(fā)送了個異步消息mTraversalRunnable,這個mTraversalRunnable最終會執(zhí)行doTraversal(),也就是會觸發(fā)View的繪制流程
        // 也就是說通過設(shè)置屏障消息腰懂,會把主線程的同步消息先阻塞梗逮,優(yōu)先執(zhí)行View繪制這個異步消息進(jìn)行界面繪制。
        // 這很好理解悯恍,界面繪制的任務(wù)肯定要優(yōu)先库糠,否則就會出現(xiàn)界面卡頓。
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}
 
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }
 
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
 
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            // 設(shè)置該消息是異步消息
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

4.6****我們能用屏障消息做什么涮毫?

那么除了系統(tǒng)中使用到了屏障消息瞬欧,我們在開發(fā)中有什么場景能派上用場嗎? 運(yùn)用屏障消息可以阻塞同步消息的特性,我們可以用來實(shí)現(xiàn)UI界面初始化和數(shù)據(jù)加載同時進(jìn)行罢防。

我們一般在Activity創(chuàng)建的時候艘虎,為了減少空指針異常的發(fā)生,都會在onCreate先setContent咒吐,然后findView初始化控件野建,然后再執(zhí)行網(wǎng)絡(luò)數(shù)據(jù)加載的異步請求,待網(wǎng)絡(luò)數(shù)據(jù)加載完成后恬叹,再刷新各個控件的界面候生。

試想一下,怎么利用屏障消息的特性來達(dá)到界面初始化和異步網(wǎng)絡(luò)數(shù)據(jù)的加載同時進(jìn)行绽昼,而不影響界面渲染唯鸭?先來看一個時序圖:

image

[圖片上傳失敗...(image-83a2dc-1606874479076)]

我們通過下面?zhèn)未a進(jìn)一步加深理解:


// 在上一個頁面里異步加載下一個頁面的數(shù)據(jù)
 // 網(wǎng)絡(luò)請求返回的數(shù)據(jù)
    Data netWorkData;
    // 創(chuàng)建屏障消息會生成一個token,這個token用來刪除屏障消息硅确,很重要目溉。
    int barrierToken;
 
    // 創(chuàng)建異步線程加載網(wǎng)絡(luò)數(shù)據(jù)
    HandlerThread thread = new HandlerThread("preLoad"){
        @Override
        protected void onLooperPrepared() {
            Handler mThreadHandler = new Handler(thread.getLooper());
            // 1、把請求網(wǎng)絡(luò)耗時消息推入消息隊列
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    // 異步耗時操作:網(wǎng)絡(luò)請求數(shù)據(jù)菱农,賦值給netWorkData
                    netWorkData = xxx;
 
                }
            });
 
            // 2缭付、然后給異步線程的隊列發(fā)一個屏障消息推入消息隊列
            barrierToken = thread.getLooper().getQueue().postSyncBarrier();
 
            // 3、然后給異步線程的消息隊列發(fā)一個刷新UI界面的同步消息
            // 這個消息在屏障消息被remove前得不到執(zhí)行的循未。
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    // 回調(diào)主線程, 把netWorkData賦給監(jiān)聽方法陷猫,刷新界面
 
                }
            });
        }
    };
    thread.start();
 
 
// 當(dāng)前界面初始化界面
protected void onCreate(Bundle savedInstanceState) {
    setContentView(view);
 
    // 各種findview操作完成
    Button btn = findViewById(R.id.xxx);
    ...
    // 4、待控件初始化完成的妖,把異步線程設(shè)置的屏障消息remove掉烙丛,這樣異步線程請求數(shù)據(jù)完成后,3羔味、處的刷新UI界面的同步消息就有機(jī)會執(zhí)行,就可以安全得刷新界面了钠右。
    thread.getLooper().getQueue().removeSyncBarrier(barrierToken);
}

但是趣钱,MessageQueue源碼里我們我們看到,屏障消息的創(chuàng)建和刪除都是隱藏方法(@hide)澡屡,我們沒法直接調(diào)用衫生,只能用反射來調(diào)用,所以在實(shí)際使用中還得綜合驗(yàn)證馍盟。

4.7 IdleHandler及應(yīng)用

IdleHandler,字面意思就是空閑的處理器(就是說我是在消息隊列空閑的時候才會執(zhí)行的,如果消息隊列里有其他非IdleHandler消息在執(zhí)行褥芒,則我先不執(zhí)行),它其實(shí)就是一個接口嫡良,我們就認(rèn)為它是空閑消息吧锰扶,只不過它不是存在MessageQueue里,而是以數(shù)組的形式保存的寝受。

public static interface IdleHandler {
    /**
     * Called when the message queue has run out of messages and will now
     * wait for more.  Return true to keep your idle handler active, false
     * to have it removed.  This may be called if there are still messages
     * pending in the queue, but they are all scheduled to be dispatched
     * after the current time.
     */
    boolean queueIdle();
}

我們看到MessageQueue有添加和刪除IdleHandler的方法坷牛,IdleHandler被保存在一個ArrayList里:

private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
 
...
 
public void addIdleHandler(@NonNull IdleHandler handler) {
    if (handler == null) {
        throw new NullPointerException("Can't add a null IdleHandler");
    }
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}
 
public void removeIdleHandler(@NonNull IdleHandler handler) {
    synchronized (this) {
        mIdleHandlers.remove(handler);
    }
}

那么,它是怎么實(shí)現(xiàn)在消息隊列空閑的間隙得到執(zhí)行的呢很澄?還是看next()方法京闰。

// If first time idle, then get the number of idlers to run.
// Idle handles only run if the queue is empty or if the first message
// in the queue (possibly a barrier) is due to be handled in the future.
// pendingIdleHandlerCount < 0是說for循環(huán)只執(zhí)行第一次
// mMessages == null || now < mMessages.when) 是說當(dāng)前消息隊列沒有消息或者要執(zhí)行的消息晚于當(dāng)前時間
// 說明現(xiàn)在消息隊列處于空閑。
if (pendingIdleHandlerCount < 0
        && (mMessages == null || now < mMessages.when)) {
    pendingIdleHandlerCount = mIdleHandlers.size();
}
if (pendingIdleHandlerCount <= 0) {
    // No idle handlers to run.  Loop and wait some more.
    mBlocked = true;
    continue;
}

在上面這段代碼判定當(dāng)前消息隊列處于空閑后甩苛,就會拿到空閑消息的大小蹂楣,下面這段代碼就是把把空閑消息執(zhí)行一遍。

// Run the idle handlers.
// We only ever reach this code block during the first iteration.
for (int i = 0; i < pendingIdleHandlerCount; i++) {
    final IdleHandler idler = mPendingIdleHandlers[i];
    mPendingIdleHandlers[i] = null; // release the reference to the handler
 
    boolean keep = false;
    try {
        // 如果queueIdle返回true讯蒲,則該空閑消息不會被自動刪除痊土,在下次執(zhí)行next的時候,如果還出現(xiàn)隊列空閑爱葵,會再次執(zhí)行施戴。
        // 如果返回false,則該空閑消息會在執(zhí)行完后萌丈,被自動刪除掉赞哗。
        keep = idler.queueIdle();
    } catch (Throwable t) {
        Log.wtf(TAG, "IdleHandler threw exception", t);
    }
 
    if (!keep) {
        synchronized (this) {
            mIdleHandlers.remove(idler);
        }
    }
}
 
// Reset the idle handler count to 0 so we do not run them again.
// 這里把空閑消息標(biāo)志置為0,而不置為-1辆雾,就是說本次已經(jīng)處理完肪笋,防止for循環(huán)反復(fù)執(zhí)行,影響其他消息的執(zhí)行
pendingIdleHandlerCount = 0;
 
// While calling an idle handler, a new message could have been delivered
// so go back and look again for a pending message without waiting.
nextPollTimeoutMillis = 0;

總結(jié)一下:

  • 如果本次循環(huán)拿到的消息為空度迂,或者這個消息是一個延時的消息而且還沒到指定的觸發(fā)時間藤乙,那么,就認(rèn)定當(dāng)前的隊列為空閑狀態(tài)惭墓。
  • 接著就會遍歷mPendingIdleHandlers數(shù)組(這個數(shù)組里面的元素每次都會到mIdleHandlers中去拿)來調(diào)用每一個IdleHandler實(shí)例的queueIdle方法坛梁, 如果這個方法返回false的話,那么這個實(shí)例就會從mIdleHandlers中移除腊凶,也就是當(dāng)下次隊列空閑的時候划咐,不會繼續(xù)回調(diào)它的queueIdle方法了拴念。
  • 處理完IdleHandler后會將nextPollTimeoutMillis設(shè)置為0,也就是不阻塞消息隊列褐缠,當(dāng)然要注意這里執(zhí)行的代碼同樣不能太耗時政鼠,因?yàn)樗峭綀?zhí)行的,如果太耗時肯定會影響后面的message執(zhí)行队魏。

IdleHandler的原理大概就是上面講的那樣公般,那么能力決定用處,從本質(zhì)上講就是趁著消息隊列空閑的時候干點(diǎn)事情胡桨,具體做什么官帘,是在IdleHandler的queueIdle()方法里。那么IdleHandler在系統(tǒng)源碼里使用場景是怎樣的登失?我們可以看到它在主線程生命周期處理中使用比較多遏佣,比如在ActivityThread里有個 就有一個名叫GcIdler的內(nèi)部類,實(shí)現(xiàn)的就是IdleHandler接口揽浙,它的作用就是在主線程空閑的時候?qū)?nèi)存進(jìn)行強(qiáng)制GC状婶。

final class GcIdler implements MessageQueue.IdleHandler {
    @Override
    public final boolean queueIdle() {
        doGcIfNeeded();
        return false;
    }
}
 
 
 
// 這里的意思就是說判斷距離上次GC的時間是否超過5秒,超過則執(zhí)行后臺強(qiáng)制GC
void doGcIfNeeded() {
    mGcIdlerScheduled = false;
    final long now = SystemClock.uptimeMillis();
    //Slog.i(TAG, "**** WE MIGHT WANT TO GC: then=" + Binder.getLastGcTime()
    //        + "m now=" + now);
    if ((BinderInternal.getLastGcTime()+MIN_TIME_BETWEEN_GCS) < now) {
        //Slog.i(TAG, "**** WE DO, WE DO WANT TO GC!");
        BinderInternal.forceGc("bg");
    }
}

我們看看它是在哪里添加到消息隊列的:

// 這個方法是在mH的handleMessage方法里調(diào)的馅巷,也就是說也是通過AMS(ActivityManagerService)把消息發(fā)送到主線程消息隊列
void scheduleGcIdler() {
    if (!mGcIdlerScheduled) {
        mGcIdlerScheduled = true;
        Looper.myQueue().addIdleHandler(mGcIdler);
    }
    mH.removeMessages(H.GC_WHEN_IDLE);
}

還有就是在ActivityThread的performLaunchActivity方法執(zhí)行時膛虫,最終會執(zhí)行到Instrumentation.callActivityOnCreate方法,在這個方法里钓猬,也有用到IdleHandler做一些額外的事情稍刀。

public void callActivityOnCreate(Activity activity, Bundle icicle) {
    prePerformCreate(activity);
    activity.performCreate(icicle);
    postPerformCreate(activity);
}
 
private void prePerformCreate(Activity activity) {
    if (mWaitingActivities != null) {
        synchronized (mSync) {
            final int N = mWaitingActivities.size();
            for (int i=0; i<N; i++) {
                final ActivityWaiter aw = mWaitingActivities.get(i);
                final Intent intent = aw.intent;
                if (intent.filterEquals(activity.getIntent())) {
                    aw.activity = activity;
                    mMessageQueue.addIdleHandler(new ActivityGoing(aw));
                }
            }
        }
    }
}

除此之外,在一些第三方庫中都有使用IdleHandler敞曹,比如LeakCanary账月,Glide中有使用到。

那么對于我們來說澳迫,IdleHandler可以有些什么使用場景呢局齿?根據(jù)它最核心的原理,在消息隊列空閑的時候做點(diǎn)事情橄登,那么對于主線程來講抓歼,我們有很多的一些代碼不是必須要跟隨生命周期方法同步執(zhí)行的,就可以用IdleHandler拢锹,減少主線程的耗時谣妻,也就減少應(yīng)用或者Activity的啟動時間。例如:一些第三方庫的初始化卒稳,埋點(diǎn)尤其是延時埋點(diǎn)上報等蹋半,都可以用IdleHandler添加到消息隊列里。

==好了充坑,提個問題:前面我們說了在主線程創(chuàng)建的main函數(shù)里創(chuàng)建了Handler和Looper湃窍,回顧了上面的Handler機(jī)制的原理闻蛀,我們都知道一般線程執(zhí)行完就會退出,由系統(tǒng)回收資源您市,那Android UI線程也是基于Handler Looper機(jī)制的,那么為什么UI線程可以一直常駐役衡?不會被阻塞呢茵休?==

因?yàn)長ooper在執(zhí)行l(wèi)oop方法里,是一個for循環(huán)手蝎,也就是說線程永遠(yuǎn)不會執(zhí)行完退出榕莺,所以打開APP可以一直顯示,Activity的生命周期就是通過消息隊列把消息一個一個取出來執(zhí)行的棵介,然后因?yàn)镸essageQueue的休眠喚醒機(jī)制钉鸯,當(dāng)消息隊列里沒有消息時,消息隊列會進(jìn)入休眠邮辽,并釋放CPU資源唠雕,當(dāng)又有新消息進(jìn)入隊列時,會喚醒隊列吨述,把消息取出來執(zhí)行岩睁。

二、Handler應(yīng)用之HandlerThread

HandlerThread本質(zhì)上是一個Thread揣云,所不同的是捕儒,它充分利用了Handler機(jī)制,通過在內(nèi)部創(chuàng)建Looper循環(huán)邓夕,外部通過Handler把異步任務(wù)推送給消息隊列刘莹,從而達(dá)到不用重復(fù)創(chuàng)建多個Thread,即能將多個異步任務(wù)排隊進(jìn)行異步執(zhí)行焚刚,它的原理很簡單:

@Override
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}

在線程的run方法里創(chuàng)建了looper循環(huán)点弯,這樣這個線程不主動quit的話,不會銷毀汪榔,有消息則執(zhí)行消息蒲拉,沒有消息根據(jù)MessageQueue休眠機(jī)制,會釋放CPU資源痴腌,進(jìn)入休眠雌团。

使用HandlerThread時,我們注意到士聪,在創(chuàng)建Handler時锦援,是要傳入線程的Looper進(jìn)行綁定的,所以必須先執(zhí)行HandlerThread的start方法剥悟,因?yàn)閳?zhí)行start方法灵寺,才會執(zhí)行HandlerThread的run方法曼库,才會創(chuàng)建線程的Looper,創(chuàng)建Handler傳入的Looper才不會是null略板。

所以我們一般使用是這樣的:

  • 創(chuàng)建HandlerThread后毁枯,調(diào)用start,然后再創(chuàng)建Handler叮称;
  • 從run方法里我們看到有個onLooperPrepared()方法种玛,可以實(shí)現(xiàn)這個方法,在這個方法里創(chuàng)建Handler瓤檐,這樣就不受start位置的限制了赂韵,原理就是以為run方法是在調(diào)用start方法后才會執(zhí)行。

那么怎么回收一個HandlerThread呢挠蛉?我們看到HandlerThread里有個quit方法祭示,這個方法最終會調(diào)用到MessageQueue的quit方法,從而結(jié)束消息分發(fā)谴古,最終終止一個HandlerThread線程质涛。

public boolean quit() {
    Looper looper = getLooper();
    if (looper != null) {
        looper.quit();
        return true;
    }
    return false;
}

三、Handler應(yīng)用之IntentService

IntentService其實(shí)是Service和HandlerThread的結(jié)合體讥电,我們可以看到在onCreate里創(chuàng)建了個HandlerThread并創(chuàng)建了個Handler和該HandlerThread綁定蹂窖,然后在onStat方法里以消息的形式發(fā)送給HandlerThread執(zhí)行

@Override
public void onCreate() {
    // TODO: It would be nice to have an option to hold a partial wakelock
    // during processing, and to have a static startService(Context, Intent)
    // method that would launch the service & hand off a wakelock.
 
    super.onCreate();
    // 創(chuàng)建HandlerThread
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();
 
    // 創(chuàng)建Handler和HandlerThread綁定
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
}
 
@Override
public void onStart(@Nullable Intent intent, int startId) {
    Message msg = mServiceHandler.obtainMessage();
    msg.arg1 = startId;
    msg.obj = intent;
    // 想HandlerThread的消息隊列發(fā)送消息
    mServiceHandler.sendMessage(msg);
}

最終在handleMessage里執(zhí)行

private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
 
        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

所以我們使用IntentService都必須實(shí)現(xiàn)onHandleIntent這個抽象方法,在這個抽象方法里做具體的業(yè)務(wù)操作恩敌。

我們都知道IntentService在執(zhí)行完異步任務(wù)后瞬测,會自動銷毀,這是怎么實(shí)現(xiàn)的纠炮?

public ServiceHandler(Looper looper) {
        super(looper);
    }
 
    @Override
    public void handleMessage(Message msg) {
        onHandleIntent((Intent)msg.obj);
        // 答案在這里:在這里會停止Service
        stopSelf(msg.arg1);
    }
}
 
// 然后在onDestory里會終止掉消息循環(huán)月趟,從而達(dá)到銷毀異步線程的目的:
@Override
public void onDestroy() {
    mServiceLooper.quit();
}

四、Handler.post和View.post

我們先來看個大家平常經(jīng)常使用的案例:獲取View的寬高恢口。

@Override
protected void onCreate(Bundle savedInstanceState) {
 
    // 位置1
    Log.i("view_w_&_h", "onCreate " + mView.getWidth() + " " + mView.getHeight());
    mView.post(new Runnable() {
        @Override
        public void run() {
        // 位置2
            Log.i("view_w_&_h", "onCreate postRun " + mView.getWidth() + " " + mView.getHeight());
        }
    });
 
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
        // 位置3
            Log.i("view_w_&_h", "onCreate Handler " + mView.getWidth() + " " + mView.getHeight());
        }
    });
}
 
@Override
protected void onResume() {
    super.onResume();
    // 位置4
    Log.i("view_w_&_h", "onResume " + mView.getWidth() + " " + mView.getHeight());
 
    new Handler(Looper.getMainLooper()).post(new Runnable() {
        @Override
        public void run() {
            // 位置5
            Log.i("view_w_&_h", "onResume Handler " + mView.getWidth() + " " + mView.getHeight());
        }
    });
}

這幾個位置孝宗,哪些能獲取到mView的寬高?

我們都知道在View被attach到window之前耕肩,是獲取不到View的寬高的因妇,因?yàn)檫@個時候View還沒有被Measure、layout猿诸、draw婚被,所以在onCreate或者onResume直接調(diào)用View的寬高方法,都是0梳虽,Handler.post在onCreate里也是獲取不到址芯,但是在onResume里能獲取到,而View.post無論放在onCreate或者onResume里,都能獲取到View的寬高谷炸,為什么北专?

我們先看個簡版的View的繪制流程:

image

[圖片上傳失敗...(image-31142a-1606874479076)]

我們都知道View的最終繪制是在performTraversals()方法里,包括measure旬陡、layout拓颓、draw,從上面的圖往上追溯季惩,我們知道录粱,View的繪制是在ActivityThread的handleResumeActivity方法里,這個方法相信大家不會陌生画拾,這個方法就是會回調(diào)Activity的onResume方法的頂級方法。

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ...
    // 這里追溯進(jìn)去菜职,最終會調(diào)用Activity的onStart方法和onResume方法
    r = performResumeActivity(token, clearHide, reason);
     
    ...
     
    // 調(diào)用WindowManager的addView方法青抛,這里就是最終執(zhí)行View繪制的地方
    wm.addView(decor, l);
     
    ...
}

從上面的代碼片段執(zhí)行順序來看,Activity的onStart和onResume被執(zhí)行的時候酬核,其實(shí)界面還沒有開始進(jìn)行繪制(wm.addView(decor, l)還沒執(zhí)行到)蜜另,這里就可以解釋為什么用Handler.post在onCreate里拿不到寬高。因?yàn)镠andler機(jī)制嫡意,它是把消息推送到主線程的消息隊列里去举瑰,在onCreate里把消息推到消息隊列時,onResume的消息都還沒入隊蔬螟,也就沒有執(zhí)行此迅,所以拿不到。那為什么onResume里能拿到呢旧巾?因?yàn)橄㈥犃械臋C(jī)制耸序,Handler.post推送的消息,必須得等上一個消息執(zhí)行完才能得到執(zhí)行鲁猩,所以它必須得等handleResumeActivity執(zhí)行完坎怪,而handleResumeActivity執(zhí)行完成后,View已經(jīng)繪制完成了廓握,當(dāng)然就能拿到寬高了搅窿。

好了,現(xiàn)在解釋第二個疑問隙券,為什么View.post在onCreate里能拿到View的寬高呢男应?我們先看下View.post方法:

public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    // attachInfo不為null,說明View已經(jīng)被attach到window是尔,也就是完成了繪制殉了,所以直接把消息推送到主線程的消息隊列執(zhí)行。
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
 
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    // 關(guān)鍵就在這里拟枚,走到這里薪铜,說明attachInfo為null众弓,也就是現(xiàn)在View還沒attach到window,所以把消息臨時保存到RunQueue里
    getRunQueue().post(action);
    return true;
}

上面我們可以看到隔箍,如果attachInfo為null谓娃,則Runnable會臨時存儲起來,保存到RunQueue里蜒滩,并沒有立即執(zhí)行滨达,那么保存到RunQueue是什么時候被執(zhí)行的呢?

我們看到HandlerActionQueue有個executeActions方法俯艰,這個方法就是用來執(zhí)行保存其中的Runnable的:

public void executeActions(Handler handler) {
    synchronized (this) {
        final HandlerAction[] actions = mActions;
        for (int i = 0, count = mCount; i < count; i++) {
            final HandlerAction handlerAction = actions[i];
            handler.postDelayed(handlerAction.action, handlerAction.delay);
        }
 
        mActions = null;
        mCount = 0;
    }
}

那么這個方法是在什么時機(jī)調(diào)用的呢捡遍?接著往下看:在View的dispatchAttachedToWindow方法里,我們看到調(diào)用了RunQueue的executeActions竹握,執(zhí)行保存在RunQueue里的runnable画株。

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
 
    ...
     
    // Transfer all pending runnables.
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
     
    onAttachedToWindow();
     
    ...
}

那么dispatchAttachedToWindow又是在什么時候被調(diào)用呢?在ViewRootImpl的performTraversals方法里啦辐,我們看到dispatchAttachedToWindow被執(zhí)行谓传。host就是DecorView。

private void performTraversals() {
    ...
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    ...
    performMeasure();
    ...
    performLayout();
    ...
    performDraw();
}

從前面的View繪制的UML時序圖芹关,我們知道续挟,performTraversals是在ActivityThread的handleResumeActivity被調(diào)用的。

總結(jié)一下:

系統(tǒng)在執(zhí)行ActivityThread的handleResumeActivity的方法里侥衬,最終會調(diào)到ViewRootImpl的performTraversals()方法诗祸,performTraversals()方法調(diào)用host的dispatchAttachedToWindow()方法,host就是DecorView也就是View浇冰,接著在View的dispatchAttachedToWindow()方法中調(diào)用mRunQueue.executeActions()方法贬媒,這個方法內(nèi)部會遍歷HandlerAction數(shù)組,利用Handler來post之前存放的Runnable肘习。

這里就可以解釋為什么View.post在onCreate里同樣可以得到View的寬高际乘,是因?yàn)閂iew.post發(fā)出的消息,它被執(zhí)行的時機(jī)是在View被繪制之后漂佩。

==可能有同學(xué)要問了:dispatchAttachedToWindow 方法是在 performMeasure 方法之前調(diào)用的脖含,既然在調(diào)用的時候還沒有執(zhí)行performMeasure來進(jìn)行測量,那么為什么在執(zhí)行完dispatchAttachedToWindow方法后就可以獲取到寬高呢投蝉?==

還是回到Handler機(jī)制最基本的原理养葵,消息是以隊列的形式存在消息隊列里,然后依次等待Loop執(zhí)行的瘩缆,而performTraversals的執(zhí)行它本身就是在一個Runnable消息里关拒,所以performTraversals在執(zhí)行的時候,其他消息得等performTraversals執(zhí)行完了才能得到執(zhí)行,也就是說mRunQueue.executeActions()的消息必須得等performTraversals徹底執(zhí)行完才能得到執(zhí)行着绊,所以View.post(runnable)中的runnable執(zhí)行是要在performTraversals方法執(zhí)行完之后的谐算,并非一調(diào)用dispatchAttachedToWindow就會執(zhí)行。

前面還遺留了一個問題:View.post方法里的mAttachInfo是在什么時候賦值的呢归露?

public ViewRootImpl(Context context, Display display) {
    ...
     
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
        context);
    ...
}

我們看到它是在ViewRootImpl的構(gòu)造函數(shù)里被賦值的洲脂,那么ViewRootImpl是什么時候被創(chuàng)建的呢?順著往上找剧包,我們看到恐锦,它是在WindowManagerGlobal的add方法里被創(chuàng)建的。

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ...
    ViewRootImpl root;
    ...
    root = new ViewRootImpl(view.getContext(), display);
    ...
}

前面也講了WindowManagerGlobal的addView方法是在ActivityThread的handleResumeActivity()方法里被執(zhí)行的疆液,所以問題就解開了一铅,為什么View.post方法里會先判斷mAttachInfo是否為空,不為空堕油,說明View.post的調(diào)用時機(jī)是在onResume之后馅闽,也就是View繪制完成之后,這個時候直接推入主線程消息隊列執(zhí)行就可以馍迄。而如果mAttachInfo為空,說明View還沒繪制完局骤,先暫存起來攀圈,待繪制完后再依次推入主線程執(zhí)行。

要注意的是View.post方法是有坑的峦甩,android版本 < 24,也就是7.0以下的系統(tǒng)赘来。

// 7.0以下系統(tǒng)
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Assume that post will succeed later
    // 注意此處,不同于我7.0及以上系統(tǒng)凯傲,
    ViewRootImpl.getRunQueue().post(action);
    return true;
}

而我們看一下 ViewRootImpl 的RunQueue是怎么實(shí)現(xiàn)的:

static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
static RunQueue getRunQueue() {
    RunQueue rq = sRunQueues.get();
    if (rq != null) {
        return rq;
    }
    rq = new RunQueue();
    sRunQueues.set(rq);
    return rq;
}

結(jié)合前面講的ThreadLocal特性犬辰,它是跟線程相關(guān)的,也就是說保存其中的變量只在本線程內(nèi)可見冰单,其他線程獲取不到幌缝。

好了,假設(shè)有這種場景诫欠,我們子線程里用View.post一個消息涵卵,從上面的代碼看,它會保存子線程的ThreadLocal里荒叼,但是在執(zhí)行RunQueue的時候轿偎,又是在主線程里去找runnable調(diào)用,因?yàn)門hreadLocal線程隔離被廓,主線程永遠(yuǎn)也找不到這個消息坏晦,這個消息也就沒法得到執(zhí)行了。

而7.0及以上沒有這個問題,是因?yàn)樵趐ost方法里把runnable保存在主線程里:getRunQueue().post(action)昆婿。

總結(jié)一下:

上面這個問題的前提有兩個:View被繪制前球碉,且在子線程里調(diào)用View.post。如果View.post是在View被繪制之后挖诸,也就是mAttachInfo非空汁尺,那么會立即推入主線程調(diào)用,也就不存在因線程隔離找不到runnable的問題多律。


作者:He Ying

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末痴突,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狼荞,更是在濱河造成了極大的恐慌辽装,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件相味,死亡現(xiàn)場離奇詭異拾积,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)丰涉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門拓巧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人一死,你說我怎么就攤上這事肛度。” “怎么了投慈?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵承耿,是天一觀的道長。 經(jīng)常有香客問我伪煤,道長加袋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任抱既,我火速辦了婚禮职烧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蝙砌。我一直安慰自己阳堕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布择克。 她就那樣靜靜地躺著恬总,像睡著了一般。 火紅的嫁衣襯著肌膚如雪肚邢。 梳的紋絲不亂的頭發(fā)上壹堰,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天拭卿,我揣著相機(jī)與錄音,去河邊找鬼贱纠。 笑死峻厚,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谆焊。 我是一名探鬼主播惠桃,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辖试!你這毒婦竟也來了辜王?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤罐孝,失蹤者是張志新(化名)和其女友劉穎呐馆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體莲兢,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡汹来,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了改艇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片收班。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谒兄,靈堂內(nèi)的尸體忽然破棺而出闺阱,到底是詐尸還是另有隱情,我是刑警寧澤舵变,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站瘦穆,受9級特大地震影響纪隙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜扛或,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一绵咱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熙兔,春花似錦悲伶、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至舆声,卻和暖如春花沉,著一層夾襖步出監(jiān)牢的瞬間柳爽,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工碱屁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磷脯,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓娩脾,卻偏偏與公主長得像赵誓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子柿赊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

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