詳解Android啟動第一幀
冷啟動結(jié)束的時間怎么確定?根據(jù) Play Console 文檔,當應用程序的第一幀完全加載時,將跟蹤啟動時間。從 App 冷啟動時間文檔中了解到更多信息:一旦應用進程完成了第一次繪制,系統(tǒng)進程就會換出當前顯示的背景窗口,用主 Activity 替換它。 此時,用戶可以開始使用該應用程序。
1、第一幀什么時候開始調(diào)度
ActivityThread.handleResumeActivity()調(diào)度第一幀。- 在第一幀
Choreographer.doFrame()調(diào)用ViewRootImpl.doTraversal()執(zhí)行測量傳遞、布局傳遞,最后是視圖層次結(jié)構(gòu)上的第一個繪制傳遞。
2、第一幀
從 API 級別 16 開始,Android 提供了一個簡單的 API 來安排下一幀發(fā)生時的回調(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)度第一次遍歷之前運行的幀的副作用。 所以這里報告的時間是在運行第一次繪制的幀的時間之前。 我能夠在 API 25 上重現(xiàn)這個,但也注意到它不會在 API 30 中發(fā)生,所以這個錯誤可能已經(jīng)修復。
3、第一次繪制
ViewTreeObserver
在 Android 上,每個視圖層次結(jié)構(gòu)都有一個 ViewTreeObserver,它可以保存全局事件的回調(diào),例如布局或繪制。
ViewTreeObserver.addOnDrawListener()
我們可以調(diào)用 ViewTreeObserver.addOnDrawListener() 來注冊一個繪制監(jiān)聽器:
view.viewTreeObserver.addOnDrawListener {
// report first draw
}
ViewTreeObserver.removeOnDrawListener()
我們只關(guān)心第一次繪制,因此我們需要在收到回調(diào)后立即刪除 OnDrawListener。 不幸的是,無法從 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);
}
}
所以我們必須在一個 post 中進行刪除:
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)
)
}
}
}
注意擴展函數(shù):
view.onNextDraw {
// report first draw
}
FloatingTreeObserver
如果我們在附加視圖層次結(jié)構(gòu)之前調(diào)用 View.getViewTreeObserver() ,則沒有真正的 ViewTreeObserver 可用,因此視圖將創(chuàng)建一個假的來存儲回調(diào):
public class View {
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}
}
然后當視圖被附加時,回調(diào)被合并回真正的 ViewTreeObserver。
除了在 API 26 中修復了一個錯誤:繪制偵聽器沒有合并回真實的視圖樹觀察器。
我們通過在注冊我們的繪制偵聽器之前等待視圖被附加來解決這個問題:
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)在我們有一個很好的實用程序來監(jiān)聽下一次繪制,我們可以在創(chuàng)建 Activity 時使用它。 請注意,第一個創(chuàng)建的 Activity 可能不會繪制:應用程序?qū)⒈拇?Activity 作為啟動器 Activity 是很常見的,它會立即啟動另一個 Activity 并自行完成。 我們在 Activity 窗口 DecorView 上注冊我們的繪制偵聽器。
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() 的文檔:
請注意:如 setContentView() 中所述,首次調(diào)用此函數(shù)會“鎖定”各種窗口特征。
不幸的是,我們正在從 ActivityLifecycleCallbacks.onActivityCreated() 調(diào)用 Window.getDecorView(),它被 Activity.onCreate() 調(diào)用。 在一個典型的 Activity 中,setContentView() 在 super.onCreate() 之后被調(diào)用,所以我們在 setContentView() 被調(diào)用之前調(diào)用 Window.getDecorView(),這會產(chǎn)生意想不到的副作用。
在我們檢索裝飾視圖之前,我們需要等待 setContentView() 被調(diào)用。
Window.Callback.onContentChanged()
我們可以使用 Window.peekDecorView() 來確定我們是否已經(jīng)有一個裝飾視圖。 如果沒有,我們可以在我們的窗口上注冊一個回調(diào),它提供了我們需要的鉤子,Window.Callback.onContentChanged():
只要屏幕的內(nèi)容視圖發(fā)生變化(由于調(diào)用 Window#setContentView() 或 Window#addContentView() ),就會調(diào)用此鉤子。
但是,一個窗口只能有一個回調(diào),并且 Activity 已經(jīng)將自己設(shè)置為窗口回調(diào)。 所以我們需要替換那個回調(diào)并委托給它。
這是一個實用程序類,它執(zhí)行此操作并添加一個 Window.onDecorViewReady() 擴展函數(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() 文檔:
即將繪制視圖樹時調(diào)用的回調(diào)方法。
繪圖仍然需要一段時間。 我們想知道繪圖何時完成,而不是何時開始。 不幸的是,沒有 ViewTreeObserver.OnPostDrawListener API 。
第一幀和遍歷都發(fā)生在一個 MSG_DO_FRAME 消息中。 如果我們可以確定該消息何時結(jié)束,我們就會知道何時完成繪制。
Handler.postAtFrontOfQueue()
與其確定 MSG_DO_FRAME 消息何時結(jié)束,我們可以通過使用 Handler.postAtFrontOfQueue() 發(fā)布到消息隊列的前面來檢測下一條消息何時開始:
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è)備上測量了生產(chǎn)中的第一個 onNextDraw() 和以下 postAtFrontOfQueue() 之間的時間差,以下是結(jié)果:
第 10 個百分位數(shù):25ms
第 25 個百分位數(shù):37 毫秒
第 50 個百分位數(shù):61 毫秒
第 75 個百分位數(shù):109 毫秒
第 90 個百分位數(shù):194 毫秒
到此這篇關(guān)于詳解Android啟動第一幀的文章就介紹到這了,更多相關(guān)Android啟動第一幀內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android RecyclerView 數(shù)據(jù)綁定實例代碼
本文主要介紹Android RecyclerView 數(shù)據(jù)綁定的資料,這里詳細說明如何實現(xiàn) Android RecyclerView的數(shù)據(jù)綁定,并附示例代碼,有需要的小伙伴可以參考下2016-09-09
解決Android studio3.6安裝后gradle Download失敗(構(gòu)建不成功)
這篇文章主要介紹了解決Android studio3.6安裝后gradle Download失敗(構(gòu)建不成功),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-03-03
Android官方下拉刷新控件SwipeRefreshLayout使用詳解
這篇文章主要為大家詳細介紹了Android官方下拉刷新控件SwipeRefreshLayout使用方法,實例展示如何使用下拉刷新控件,感興趣的小伙伴們可以參考一下2016-07-07

