詳細(xì)分析android的MessageQueue.IdleHandler
我們知道android是基于Looper消息循環(huán)的系統(tǒng),我們通過Handler向Looper包含的MessageQueue投遞Message, 不過我們常見的用法是這樣吧?

一般我們比較少接觸MessageQueue, 其實它內(nèi)部的IdleHandler接口有很多有趣的用法,首先看看它的定義:

簡而言之,就是在looper里面的message暫時處理完了,這個時候會回調(diào)這個接口,返回false,那么就會移除它,返回true就會在下次message處理完了的時候繼續(xù)回調(diào),讓我們看看它有哪些有趣的用法吧~~
一、提供一個android沒有的聲明周期回調(diào)時機(jī)
如果有這種需求,想要在某個activity繪制完成去做一些事情,那這個時機(jī)是什么時候呢?有同學(xué)可能覺得onResume()是一個合適的機(jī)會,不是可是這個onResume() 真的是各種繪制都已經(jīng)完成才回調(diào)的嗎?No, too naive ~~

你看谷老師說了,onStart是用戶可見,onResume是用戶可交互,谷老師可沒說onResume是繪制完成吧~那么android那些耗時的measure, layout, draw是在什么時候執(zhí)行的呢?它們跟onResume()又有何關(guān)系呢?讓我們先來看看源碼吧~
1. ActivityThread.java
我們知道app的進(jìn)程其實是ActivityThread, 那么activity的生命周期自然是它來執(zhí)行了,

performResumeActivity就是回調(diào)onResume了, 我們繼續(xù)看wm.addView方法, 這個ViewManager是一個接口,其實現(xiàn)者是WindowManagerImpl
2.WindowManagerImpl.java

這個mGlobal是WindowManagerGlobal對象,我們繼續(xù)
3.WindowManagerGlobal.java

這里我們new 出了ViewRootImpl對象, 我們知道這個對象就是android view的根對象了,負(fù)責(zé)view繪制的measure, layout, draw的巨長的方法 performTraversals就是這個類的,我們繼續(xù)看setView方法
4.ViewRootImpl.java

這個函數(shù)調(diào)用了關(guān)鍵方法requestLayout(), 我們繼續(xù)跟蹤,順便說下,后面一連串的BadTokenException就是我們常常遇到的dialog相關(guān)拋出的,也有些特殊場景也會出這個異常,可以到這里查看線索。

調(diào)用了scheduleTraversals, 從名字就能看出來了吧:

它往Choreographer里面post了一個runnable, 這個Choreographer是android負(fù)責(zé)幀率刷新相關(guān)的東西,我們暫時可以不關(guān)注它,可以理解為往主線程post一個消息是一樣的,順便說下這個Choreographer可以做幀率檢測相關(guān)的東西,,可以用于卡頓檢測什么的···


我們看這個runnable果然是去執(zhí)行了那個巨長無比的函數(shù)performTraversals函數(shù), 現(xiàn)在我們可以總結(jié)下流程了:

結(jié)論:所以如果我們想在界面繪制出來后做點什么,那么在onResume里面顯然是不合適的,它先于measure等流程了, 有人可能會說在onResume里面post一個runnable可以嗎?還是不行,因為那樣就會變成這個樣子

所以你的行為一樣會在繪制之前執(zhí)行,這個時候我們的主角IdleHandler就發(fā)揮作用了,我們前面說了,它是在looper里面message暫時執(zhí)行完畢了就會回調(diào),顧名思義嘛,Idle就是隊列為空的意思,那么我們的onResume和measure, layout, draw都是一個個message的話,這個IdleHandler就提供了一個它們都執(zhí)行完畢的回調(diào)了,大概就是這樣

說了這么多,那么現(xiàn)在獲取到這個時機(jī)有什么用呢? look??!
這個是我們地圖的公交詳情頁面, 進(jìn)入之后產(chǎn)品要求左邊的頁卡需要展示,可以看到左邊的頁卡是一個非常復(fù)雜的布局,那么進(jìn)入之后的效果可以明顯看到頭部的展示信息是先顯示空白再100毫秒左右之后才展示出來的,原因就是這個頁卡的內(nèi)容比較復(fù)雜,用數(shù)據(jù)向它填充的時候花了較長時間,代碼如下:


可以看到這個detailView就是這個側(cè)滑的頁卡了,填充里面的數(shù)據(jù)花了90ms,如果這個時間是用在了界面view繪制之前的話,就會出現(xiàn)以上的效果了,view先是白的,再出現(xiàn),這樣就體驗不好了,如果我們把它放到IdleHandler里面呢?代碼如下:

效果是這樣的:
看出不同了嗎?頂部的頁卡先展示出來了,這樣體驗是不是會更好一些呢。雖然只有短短90ms,不過我們做app也應(yīng)該關(guān)注這種細(xì)節(jié)優(yōu)化的,是吧~ 這個做法也提供了一種思路,android本身提供的activity框架和fragment框架并沒有提供繪制完成的回調(diào),如果我們自己實現(xiàn)一個框架,就可以使用這個IdleHandler來實現(xiàn)一個onRenderFinished這種回調(diào)了。
二、可以結(jié)合HandlerThread, 用于單線程消息通知器
我們先思考一個問題,如果有一個model數(shù)據(jù)管理模塊,怎么設(shè)計?比如地圖的收藏模塊的model部分。就是下面這個圖的小星星:
它原來的model設(shè)計大概是這個樣子的:

由于這個model是單例的,而且是多線程可以訪問的,所以它的增刪改查都加上了鎖,而且由于外部訪問需要遍歷有哪些收藏點,所以外部遍歷列表也需要加鎖,大概是這樣的:

因為是多線程可訪問的,如果遍歷不加鎖的話,其他線程刪除了一個收藏,就會crash的,原來的這樣設(shè)計有幾個不好的地方:
1. 外部使用者需要關(guān)系鎖的使用,增加了負(fù)擔(dān),不用還不安全
2. 如果在主線程加鎖的話,可能另一個線程執(zhí)行操作會阻塞主線程造成anr
總之,多線程代碼就是容易出錯,而且真的出錯的時候查起來太費勁了,目前收藏夾模塊就有N多bug,所以我想用單線程來解決這個問題,由于model層的訪問需要數(shù)據(jù)庫和網(wǎng)絡(luò)等,所以需要異步線程,那么單線程隊列+異步線程,首先想到的就是HandlerThread, 大概架構(gòu)如下:

現(xiàn)在,我們把原來多線程的邏輯改到了單線程里面,各種收藏的model共用一個HandlerThread,這樣我們增刪改查都不用加鎖了,出錯幾率大大減小,而且這種model的設(shè)計有點類似插件的意思,可以很方便的增加其他收藏。
Ok, 那么跟我們的主題IdleHandler有什么關(guān)系呢?思考這樣一個問題,地圖上的小星星需要實時更新,也就是model的任何變化都需要顯示到地圖上,那么收藏的小星星就應(yīng)該作為model的觀察者,以前的做法是向收藏model注冊監(jiān)聽,在每一個增刪改查操作后都對觀察者回調(diào),大概是這樣:

這樣有一個小小的問題,就是如果有一個操作生成10個快速連續(xù)的增刪改查操作,那么我們的UI就會收到10次回調(diào),而這種場景下我們其實只需要最后一次回調(diào)就夠了,中間操作其實不用刷新UI的。
那么現(xiàn)在改成單線程模型,我們又該如何處理這個問題呢?當(dāng)然我們也能在每個post到異步線程的runnable里面去回調(diào)觀察者,但這樣未免不夠優(yōu)雅,所以這個時候IdleHandler不就又可以發(fā)揮作用了嗎?它是在消息暫時處理完的時候回調(diào)的呀,不是很符合我們的時機(jī)么,對吧?

就是這個樣子了,這里為什么不用第一個場景下的Looper.myQueue().addIdleHandler()呢?注意這個地方Looper.myQueue()如果在主線程調(diào)用就會使用主線程looper了,所以我選擇反射這個HandlerThread的looper來設(shè)置它,這個IdleHandler我們返回了true, 表示我們要長期監(jiān)聽消息隊列,因為返回false,下次就沒有回調(diào)了哦。
好了,結(jié)論是這個地方IdleHandler用作了一個消息的觸發(fā)器,是不是挺有意思的呢?
三、 結(jié)語
如果你沒有用過它,從今天開始試試吧,這篇文章只是我個人的一點小思路,說不定這個IdleHandler有很多其他的用法呢~~
騰訊WeTest提供上千臺真實手機(jī),隨時隨地進(jìn)行測試,保障應(yīng)用/手游品質(zhì)。節(jié)省百萬硬件費用,加速敏捷研發(fā)流程。
同時騰訊WeTest兼容性測試團(tuán)隊積累了10年的手游測試經(jīng)驗,旨在通過制定針對性的測試方案,精準(zhǔn)選取目標(biāo)機(jī)型,執(zhí)行專業(yè)、完整的測試用例,來提前發(fā)現(xiàn)游戲版本的兼容性問題,針對性地做出修正和優(yōu)化,來保障手游產(chǎn)品的質(zhì)量。目前該團(tuán)隊已經(jīng)支持所有騰訊在研和運營的手游項目。
相關(guān)文章
Android開發(fā)之開門狗在程序鎖中的應(yīng)用實例
這篇文章主要介紹了Android開發(fā)之開門狗在程序鎖中的應(yīng)用,以完整實例形式分析了程序鎖的使用技巧,需要的朋友可以參考下2016-02-02
Android的RV列表刷新詳解Payload與Diff方式異同
這篇文章主要為大家介紹了Android的RV列表刷新詳解Payload與Diff方式異同,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Android簡單的利用MediaRecorder進(jìn)行錄音的實例代碼
MediaRecorder可以進(jìn)行簡單的錄音,由于操作簡單所以可以用來進(jìn)行基本的錄音。下面提供一個簡單的例子,記得在Mainfest文件中添加權(quán)限2013-08-08
Flutter利用Hero組件實現(xiàn)自定義路徑效果的動畫
本篇介紹了如何利用Hero動畫組件的createRectTween屬性實現(xiàn)自定義路徑效果的動畫。文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-06-06
Android直播系統(tǒng)平臺搭建之圖片實現(xiàn)陰影效果的方法小結(jié)
這篇文章主要介紹了Android直播系統(tǒng)平臺搭建, 圖片實現(xiàn)陰影效果的若干種方法,本文給大家?guī)砣N方法,每種方法通過實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08
Android框架Volley使用之Json請求實現(xiàn)
這篇文章主要介紹了Android框架Volley使用之Json請求實現(xiàn),本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值 ,需要的朋友可以參考下2019-05-05
android Web跳轉(zhuǎn)到app指定頁面并傳遞參數(shù)實例
這篇文章主要介紹了android Web跳轉(zhuǎn)到app指定頁面并傳遞參數(shù)實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03

