本文以源碼分析+實(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。
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)系:
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-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());
}
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-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-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