How to determine the end time of cold start?

According to the Play Console documentation:

When the first frame of the application is fully loaded, the startup time is tracked.

Learn more from the App cold start time documentation:

Once the application process has finished drawing for the first time, the system process will swap out the currently displayed background window and replace it with the main Activity. At this point, the user can start using the application.

What time does frame 1 start dispatching

  • ActivityThread. HandleResumeActivity () scheduling the first frame.

  • The first frame Choreographer.doFrame() calls viewrootimpl.dotraversal () to perform the measurement pass, the layout pass, and finally the first draw pass on the view hierarchy.

The first frame

Starting from the API level 16, Android provides a simple API to arrange at the time of the next frame callback: Choreographer. PostFrameCallback ().

class MyApp : Application() {

  var firstFrameDoneMs: Long = 0

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

Unfortunately, calls the Choreographer. PostFrameCallback () before a scheduling first traversal operation frame of side effects. So the time reported here is before running the frame that was first drawn. I was able to reproduce this on API 25, but also note that it doesn’t happen in API 30, so the bug has probably been fixed.

First drawing

ViewTreeObserver

On Android, each view hierarchy has a ViewTreeObserver, which can hold callbacks to global events, such as layouts or draws.

ViewTreeObserver.addOnDrawListener()

We can call the ViewTreeObserver. AddOnDrawListener () to register a draw listeners:

view.viewTreeObserver.addOnDrawListener { 
  // report first draw
}
Copy the code

ViewTreeObserver.removeOnDrawListener()

We only care about the first draw, so we need to delete the OnDrawListener as soon as we receive the callback. Unfortunately, not from ontouch () callback call 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); }}Copy the code

So we have to delete it in a 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)
      )
    }
  }
}
Copy the code

Note the extension function:

view.onNextDraw { 
  // report first draw
}
Copy the code

FloatingTreeObserver

If we call View.getViewTreeObserver() before attaching the View hierarchy, no real ViewTreeObserver is available, so the View creates a dummy to store the callback:

public class View {
  public ViewTreeObserver getViewTreeObserver() {
    if (mAttachInfo != null) {
      return mAttachInfo.mTreeObserver;
    }
    if (mFloatingTreeObserver == null) {
      mFloatingTreeObserver = new ViewTreeObserver(mContext);
    }
    return mFloatingTreeObserver;
  }
}
Copy the code

Then when the view is attached, the callback is merged back into the real ViewTreeObserver.

Except for a bug fixed in API 26 where the draw listener was not merged back into the real view-tree viewer.

We solve this problem by waiting for the view to be attached before registering our draw listener:

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) ) } } }Copy the code

DecorView

We now have a nice utility that listens for the next drawing, which we can use when creating an Activity. Note that the first Activity created may not be drawn: it is common for an application to use a trampoline Activity as a starter Activity, which will immediately start another Activity and complete itself. We register our draw listener on the Activity window 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
        }
      }
    })
  }
}
Copy the code

Lock the window features

According to the documentation for window.getdecorView () :

Note that the first call to this function “locks” various window characteristics, as described in setContentView().

. Unfortunately, we are from ActivityLifecycleCallbacks onActivityCreated () call Window. GetDecorView (), it is the Activity. The onCreate () call. In a typical Activity, setContentView() is called after super.oncreate (), so we call window.getDecorView () before setContentView() is called, This can have unexpected side effects.

Before we retrieve the decorator view, we need to wait for setContentView() to be called.

Window.Callback.onContentChanged()

We can use window.peekdecorView () to determine if we already have a decorator view. If not, we can register a Callback on the our Window, and it provides the hooks, we need the Window. The Callback. OnContentChanged () :

This hook is called whenever the content view of the screen changes (due to a call to Windows #setContentView() or Windows #addContentView()).

However, a window can only have one callback, and the Activity has set itself up as a window callback. So we need to replace that callback and delegate to it.

This is a utility class that does this and adds a window.ondecorViewReady () extension function:

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 = newCallback newCallback } }}}Copy the code

Using Windows. 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
          }
        }
      }
    })
  }
}
Copy the code

Let’s look at the onDrawListener.ondraw () document:

The callback method called when the view tree is about to be drawn.

Mapping still takes some time. We want to know when the drawing is finished, not when it starts. Unfortunately, there is no ViewTreeObserver OnPostDrawListener API.

The first frame and traversal both occur in an MSG_DO_FRAME message. If we can determine when the message ends, we know when to finish drawing.

Handler.postAtFrontOfQueue()

With MSG_DO_FRAME message when to end, for sure, we can through the use of the Handler. PostAtFrontOfQueue (released) to the front of the message queue to detect the next message when to start:

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()
            }
          }
        }
      }
    })
  }
}
Copy the code

Edit: I measured the time difference between the first onNextDraw() in production and the following postAtFrontOfQueue() on a number of devices, and here are the results:

10th percentile: 25ms

25th percentile: 37 milliseconds

50th percentile: 61 milliseconds

75th percentile: 109 milliseconds

90th percentile: 194 milliseconds