Android啟動第一幀

語言: CN / TW / HK

冷啟動結束的時間怎麼確定?

根據 Play Console 文件:

當應用程式的第一幀完全載入時,將跟蹤啟動時間。

從 App 冷啟動時間文件中瞭解到更多資訊:

一旦應用程序完成了第一次繪製,系統程序就會換出當前顯示的背景視窗,用主 Activity 替換它。 此時,使用者可以開始使用該應用程式。

第一幀什麼時候開始排程

  • ActivityThread.handleResumeActivity() 排程第一幀。

  • 在第一幀 Choreographer.doFrame() 呼叫 ViewRootImpl.doTraversal() 執行測量傳遞、佈局傳遞,最後是檢視層次結構上的第一個繪製傳遞。

第一幀

從 API 級別 16 開始,Android 提供了一個簡單的 API 來安排下一幀發生時的回撥:Choreographer.postFrameCallback()。 ``` class MyApp : Application() {

var firstFrameDoneMs: Long = 0

override fun onCreate() { super.onCreate() Choreographer.getInstance().postFrameCallback { firstFrameDoneMs = SystemClock.uptimeMillis() } } } ```

不幸的是,呼叫 Choreographer.postFrameCallback() 具有排程第一次遍歷之前執行的幀的副作用。 所以這裡報告的時間是在執行第一次繪製的幀的時間之前。 我能夠在 API 25 上重現這個,但也注意到它不會在 API 30 中發生,所以這個錯誤可能已經修復。

第一次繪製

ViewTreeObserver

在 Android 上,每個檢視層次結構都有一個 ViewTreeObserver,它可以儲存全域性事件的回撥,例如佈局或繪製。

ViewTreeObserver.addOnDrawListener()

我們可以呼叫 ViewTreeObserver.addOnDrawListener() 來註冊一個繪製監聽器: view.viewTreeObserver.addOnDrawListener { // report first draw }

ViewTreeObserver.removeOnDrawListener()

我們只關心第一次繪製,因此我們需要在收到回撥後立即刪除 OnDrawListener。 不幸的是,無法從 onDraw() 回撥中呼叫 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) ) } } } ```

注意擴充套件函式: view.onNextDraw { // report first draw }

FloatingTreeObserver

如果我們在附加檢視層次結構之前呼叫 View.getViewTreeObserver() ,則沒有真正的 ViewTreeObserver 可用,因此檢視將建立一個假的來儲存回撥: public class View { public ViewTreeObserver getViewTreeObserver() { if (mAttachInfo != null) { return mAttachInfo.mTreeObserver; } if (mFloatingTreeObserver == null) { mFloatingTreeObserver = new ViewTreeObserver(mContext); } return mFloatingTreeObserver; } }

然後當檢視被附加時,回撥被合併回真正的 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

現在我們有一個很好的實用程式來監聽下一次繪製,我們可以在建立 Activity 時使用它。 請注意,第一個建立的 Activity 可能不會繪製:應用程式將蹦床 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
    }
  }
})

} } ```

鎖窗特性

根據 Window.getDecorView() 的文件:

請注意,如 setContentView() 中所述,首次呼叫此函式會“鎖定”各種視窗特徵。

不幸的是,我們正在從 ActivityLifecycleCallbacks.onActivityCreated() 呼叫 Window.getDecorView(),它被 Activity.onCreate() 呼叫。 在一個典型的 Activity 中,setContentView() 在 super.onCreate() 之後被呼叫,所以我們在 setContentView() 被呼叫之前呼叫 Window.getDecorView(),這會產生意想不到的副作用。

在我們檢索裝飾檢視之前,我們需要等待 setContentView() 被呼叫。

Window.Callback.onContentChanged()

我們可以使用 Window.peekDecorView() 來確定我們是否已經有一個裝飾檢視。 如果沒有,我們可以在我們的視窗上註冊一個回撥,它提供了我們需要的鉤子,Window.Callback.onContentChanged():

只要螢幕的內容檢視發生變化(由於呼叫 Window#setContentView() 或 Window#addContentView()),就會呼叫此鉤子。

但是,一個視窗只能有一個回撥,並且 Activity 已經將自己設定為視窗回撥。 所以我們需要替換那個回撥並委託給它。

這是一個實用程式類,它執行此操作並新增一個 Window.onDecorViewReady() 擴充套件函式: ``` 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() [email protected] 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 = newCallback
    newCallback
  }
}

} } ```

利用 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() 文件:

即將繪製檢視樹時呼叫的回撥方法。

繪圖仍然需要一段時間。 我們想知道繪圖何時完成,而不是何時開始。 不幸的是,沒有 ViewTreeObserver.OnPostDrawListener API。

第一幀和遍歷都發生在一個 MSG_DO_FRAME 訊息中。 如果我們可以確定該訊息何時結束,我們就會知道何時完成繪製。

Handler.postAtFrontOfQueue()

與其確定 MSG_DO_FRAME 訊息何時結束,我們可以通過使用 Handler.postAtFrontOfQueue() 釋出到訊息佇列的前面來檢測下一條訊息何時開始: ``` 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()
        }
      }
    }
  }
})

} } ```

編輯:我在大量裝置上測量了生產中的第一個 onNextDraw() 和以下 postAtFrontOfQueue() 之間的時間差,以下是結果:

第 10 個百分位數:25ms

第 25 個百分位數:37 毫秒

第 50 個百分位數:61 毫秒

第 75 個百分位數:109 毫秒

第 90 個百分位數:194 毫秒