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