詳解Android啟動(dòng)第一幀
冷啟動(dòng)結(jié)束的時(shí)間怎么確定?根據(jù) Play Console 文檔,當(dāng)應(yīng)用程序的第一幀完全加載時(shí),將跟蹤啟動(dòng)時(shí)間。從 App 冷啟動(dòng)時(shí)間文檔中了解到更多信息:一旦應(yīng)用進(jìn)程完成了第一次繪制,系統(tǒng)進(jìn)程就會(huì)換出當(dāng)前顯示的背景窗口,用主 Activity 替換它。 此時(shí),用戶(hù)可以開(kāi)始使用該應(yīng)用程序。
1、第一幀什么時(shí)候開(kāi)始調(diào)度
ActivityThread.handleResumeActivity()
調(diào)度第一幀。- 在第一幀
Choreographer.doFrame()
調(diào)用ViewRootImpl.doTraversal()
執(zhí)行測(cè)量傳遞、布局傳遞,最后是視圖層次結(jié)構(gòu)上的第一個(gè)繪制傳遞。
2、第一幀
從 API 級(jí)別 16 開(kāi)始,Android
提供了一個(gè)簡(jiǎn)單的 API 來(lái)安排下一幀發(fā)生時(shí)的回調(diào):Choreographer.postFrameCallback()。
class MyApp : Application() { var firstFrameDoneMs: Long = 0 override fun onCreate() { super.onCreate() Choreographer.getInstance().postFrameCallback { firstFrameDoneMs = SystemClock.uptimeMillis() } } }
不幸的是,調(diào)用 Choreographer.postFrameCallback()
具有調(diào)度第一次遍歷之前運(yùn)行的幀的副作用。 所以這里報(bào)告的時(shí)間是在運(yùn)行第一次繪制的幀的時(shí)間之前。 我能夠在 API 25
上重現(xiàn)這個(gè),但也注意到它不會(huì)在 API 30
中發(fā)生,所以這個(gè)錯(cuò)誤可能已經(jīng)修復(fù)。
3、第一次繪制
ViewTreeObserver
在 Android
上,每個(gè)視圖層次結(jié)構(gòu)都有一個(gè) ViewTreeObserver
,它可以保存全局事件的回調(diào),例如布局或繪制。
ViewTreeObserver.addOnDrawListener()
我們可以調(diào)用 ViewTreeObserver.addOnDrawListener()
來(lái)注冊(cè)一個(gè)繪制監(jiān)聽(tīng)器:
view.viewTreeObserver.addOnDrawListener { // report first draw }
ViewTreeObserver.removeOnDrawListener()
我們只關(guān)心第一次繪制,因此我們需要在收到回調(diào)后立即刪除 OnDrawListener
。 不幸的是,無(wú)法從 onDraw()
回調(diào)中調(diào)用 ViewTreeObserver.removeOnDrawListener():
public final class ViewTreeObserver { public void removeOnDrawListener(OnDrawListener victim) { checkIsAlive(); if (mInDispatchOnDraw) { throw new IllegalStateException( "Cannot call removeOnDrawListener inside of onDraw"); } mOnDrawListeners.remove(victim); } }
所以我們必須在一個(gè) post 中進(jìn)行刪除:
class NextDrawListener( val view: View, val onDrawCallback: () -> Unit ) : OnDrawListener { val handler = Handler(Looper.getMainLooper()) var invoked = false override fun onDraw() { if (invoked) return invoked = true onDrawCallback() handler.post { if (view.viewTreeObserver.isAlive) { viewTreeObserver.removeOnDrawListener(this) } } } companion object { fun View.onNextDraw(onDrawCallback: () -> Unit) { viewTreeObserver.addOnDrawListener( NextDrawListener(this, onDrawCallback) ) } } }
注意擴(kuò)展函數(shù):
view.onNextDraw { // report first draw }
FloatingTreeObserver
如果我們?cè)诟郊右晥D層次結(jié)構(gòu)之前調(diào)用 View.getViewTreeObserver()
,則沒(méi)有真正的 ViewTreeObserver
可用,因此視圖將創(chuàng)建一個(gè)假的來(lái)存儲(chǔ)回調(diào):
public class View { public ViewTreeObserver getViewTreeObserver() { if (mAttachInfo != null) { return mAttachInfo.mTreeObserver; } if (mFloatingTreeObserver == null) { mFloatingTreeObserver = new ViewTreeObserver(mContext); } return mFloatingTreeObserver; } }
然后當(dāng)視圖被附加時(shí),回調(diào)被合并回真正的 ViewTreeObserver
。
除了在 API 26 中修復(fù)了一個(gè)錯(cuò)誤:繪制偵聽(tīng)器沒(méi)有合并回真實(shí)的視圖樹(shù)觀察器。
我們通過(guò)在注冊(cè)我們的繪制偵聽(tīng)器之前等待視圖被附加來(lái)解決這個(gè)問(wèn)題:
class NextDrawListener( val view: View, val onDrawCallback: () -> Unit ) : OnDrawListener { val handler = Handler(Looper.getMainLooper()) var invoked = false override fun onDraw() { if (invoked) return invoked = true onDrawCallback() handler.post { if (view.viewTreeObserver.isAlive) { viewTreeObserver.removeOnDrawListener(this) } } } companion object { fun View.onNextDraw(onDrawCallback: () -> Unit) { if (viewTreeObserver.isAlive && isAttachedToWindow) { addNextDrawListener(onDrawCallback) } else { // Wait until attached addOnAttachStateChangeListener( object : OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View) { addNextDrawListener(onDrawCallback) removeOnAttachStateChangeListener(this) } override fun onViewDetachedFromWindow(v: View) = Unit }) } } private fun View.addNextDrawListener(callback: () -> Unit) { viewTreeObserver.addOnDrawListener( NextDrawListener(this, callback) ) } } }
DecorView
現(xiàn)在我們有一個(gè)很好的實(shí)用程序來(lái)監(jiān)聽(tīng)下一次繪制,我們可以在創(chuàng)建 Activity
時(shí)使用它。 請(qǐng)注意,第一個(gè)創(chuàng)建的 Activity
可能不會(huì)繪制:應(yīng)用程序?qū)⒈拇?Activity
作為啟動(dòng)器 Activity 是很常見(jiàn)的,它會(huì)立即啟動(dòng)另一個(gè) Activity 并自行完成。 我們?cè)?Activity
窗口 DecorView
上注冊(cè)我們的繪制偵聽(tīng)器。
class MyApp : Application() { override fun onCreate() { super.onCreate() var firstDraw = false registerActivityLifecycleCallbacks( object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (firstDraw) return activity.window.decorView.onNextDraw { if (firstDraw) return firstDraw = true // report first draw } } }) } }
四、鎖窗特性
根據(jù) Window.getDecorView() 的文檔:
請(qǐng)注意:如 setContentView()
中所述,首次調(diào)用此函數(shù)會(huì)“鎖定”各種窗口特征。
不幸的是,我們正在從 ActivityLifecycleCallbacks.onActivityCreated()
調(diào)用 Window.getDecorView(),
它被 Activity.onCreate()
調(diào)用。 在一個(gè)典型的 Activity
中,setContentView()
在 super.onCreate()
之后被調(diào)用,所以我們?cè)?setContentView()
被調(diào)用之前調(diào)用 Window.getDecorView(),
這會(huì)產(chǎn)生意想不到的副作用。
在我們檢索裝飾視圖之前,我們需要等待 setContentView()
被調(diào)用。
Window.Callback.onContentChanged()
我們可以使用 Window.peekDecorView()
來(lái)確定我們是否已經(jīng)有一個(gè)裝飾視圖。 如果沒(méi)有,我們可以在我們的窗口上注冊(cè)一個(gè)回調(diào),它提供了我們需要的鉤子,Window.Callback.onContentChanged():
只要屏幕的內(nèi)容視圖發(fā)生變化(由于調(diào)用 Window#setContentView()
或 Window#addContentView()
),就會(huì)調(diào)用此鉤子。
但是,一個(gè)窗口只能有一個(gè)回調(diào),并且 Activity 已經(jīng)將自己設(shè)置為窗口回調(diào)。 所以我們需要替換那個(gè)回調(diào)并委托給它。
這是一個(gè)實(shí)用程序類(lèi),它執(zhí)行此操作并添加一個(gè) Window.onDecorViewReady()
擴(kuò)展函數(shù):
= newCallback newCallback } class WindowDelegateCallback constructor( private val delegate: Window.Callback ) : Window.Callback by delegate { val onContentChangedCallbacks = mutableListOf<() -> Boolean>() override fun onContentChanged() { onContentChangedCallbacks.removeAll { callback -> !callback() } delegate.onContentChanged() } companion object { fun Window.onDecorViewReady(callback: () -> Unit) { if (peekDecorView() == null) { onContentChanged { callback() return@onContentChanged false } } else { callback() } } fun Window.onContentChanged(block: () -> Boolean) { val callback = wrapCallback() callback.onContentChangedCallbacks += block } private fun Window.wrapCallback(): WindowDelegateCallback { val currentCallback = callback return if (currentCallback is WindowDelegateCallback) { currentCallback } else { val newCallback = WindowDelegateCallback(currentCallback) callback } } }
五、利用 Window.onDecorViewReady()
class MyApp : Application() { override fun onCreate() { super.onCreate() var firstDraw = false registerActivityLifecycleCallbacks( object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (firstDraw) return val window = activity.window window.onDecorViewReady { window.decorView.onNextDraw { if (firstDraw) return firstDraw = true // report first draw } } } }) } }
讓我們看看 OnDrawListener.onDraw()
文檔:
即將繪制視圖樹(shù)時(shí)調(diào)用的回調(diào)方法。
繪圖仍然需要一段時(shí)間。 我們想知道繪圖何時(shí)完成,而不是何時(shí)開(kāi)始。 不幸的是,沒(méi)有 ViewTreeObserver.OnPostDrawListener API
。
第一幀和遍歷都發(fā)生在一個(gè) MSG_DO_FRAME
消息中。 如果我們可以確定該消息何時(shí)結(jié)束,我們就會(huì)知道何時(shí)完成繪制。
Handler.postAtFrontOfQueue()
與其確定 MSG_DO_FRAME
消息何時(shí)結(jié)束,我們可以通過(guò)使用 Handler.postAtFrontOfQueue()
發(fā)布到消息隊(duì)列的前面來(lái)檢測(cè)下一條消息何時(shí)開(kāi)始:
class MyApp : Application() { var firstDrawMs: Long = 0 override fun onCreate() { super.onCreate() var firstDraw = false val handler = Handler() registerActivityLifecycleCallbacks( object : ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { if (firstDraw) return val window = activity.window window.onDecorViewReady { window.decorView.onNextDraw { if (firstDraw) return firstDraw = true handler.postAtFrontOfQueue { firstDrawMs = SystemClock.uptimeMillis() } } } } }) } }
編輯:我在大量設(shè)備上測(cè)量了生產(chǎn)中的第一個(gè) onNextDraw()
和以下 postAtFrontOfQueue()
之間的時(shí)間差,以下是結(jié)果:
第 10 個(gè)百分位數(shù):25ms
第 25 個(gè)百分位數(shù):37 毫秒
第 50 個(gè)百分位數(shù):61 毫秒
第 75 個(gè)百分位數(shù):109 毫秒
第 90 個(gè)百分位數(shù):194 毫秒
到此這篇關(guān)于詳解Android啟動(dòng)第一幀的文章就介紹到這了,更多相關(guān)Android啟動(dòng)第一幀內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android RecyclerView 數(shù)據(jù)綁定實(shí)例代碼
本文主要介紹Android RecyclerView 數(shù)據(jù)綁定的資料,這里詳細(xì)說(shuō)明如何實(shí)現(xiàn) Android RecyclerView的數(shù)據(jù)綁定,并附示例代碼,有需要的小伙伴可以參考下2016-09-09Android中實(shí)現(xiàn)ping功能的多種方法詳解
這篇文章主要介紹了Android中實(shí)現(xiàn)ping功能的多種方法詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03解決Android studio3.6安裝后gradle Download失敗(構(gòu)建不成功)
這篇文章主要介紹了解決Android studio3.6安裝后gradle Download失敗(構(gòu)建不成功),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03簡(jiǎn)單實(shí)現(xiàn)Android放大鏡效果
這篇文章主要教大家簡(jiǎn)單實(shí)現(xiàn)Android放大鏡效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12利用Kotlin的方式如何處理網(wǎng)絡(luò)異常詳解
這篇文章主要 給大家介紹了關(guān)于利用Kotlin的方式如何處理網(wǎng)絡(luò)異常的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07Android?WebView軟鍵盤(pán)遮擋輸入框方案詳解
這篇文章主要介紹了Android?WebView軟鍵盤(pán)遮擋輸入框方案詳解,本文提供了一種新的解決?WebView?輸入框被軟鍵盤(pán)遮擋的思路,不過(guò)這種思路也有它的局限性,目前來(lái)看僅適用于全屏的?WebView?中,需要的朋友可以參考下2022-06-06Android對(duì)話(huà)框AlertDialog詳解
本文詳細(xì)講解了Android對(duì)話(huà)框AlertDialog的實(shí)現(xiàn)方式,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12Android官方下拉刷新控件SwipeRefreshLayout使用詳解
這篇文章主要為大家詳細(xì)介紹了Android官方下拉刷新控件SwipeRefreshLayout使用方法,實(shí)例展示如何使用下拉刷新控件,感興趣的小伙伴們可以參考一下2016-07-07