详解Android启动第一帧

冷启动结束的时间怎么确定?根据 Play Console 文档,当应用程序的第一帧完全加载时,将跟踪启动时间。从 App 冷启动时间文档中了解到更多信息:一旦应用进程完成了第一次绘制,系统进程就会换出当前显示的背景窗口,用主 Activity 替换它。 此时,用户可以开始使用该应用程序。

1、第一帧什么时候开始调度

  • ActivityThread.handleResumeActivity() 调度第一帧。
  • 在第一帧 Choreographer.doFrame() 调用 ViewRootImpl.doTraversal() 执行测量传递、布局传递,最后是视图层次结构上的第一个绘制传递。

2、第一帧

从 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 中发生,所以这个错误可能已经修复。

3、第一次绘制

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() 扩展函数:

= 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() 文档:

即将绘制视图树时调用的回调方法。

绘图仍然需要一段时间。 我们想知道绘图何时完成,而不是何时开始。 不幸的是,没有 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 毫秒

到此这篇关于详解Android启动第一帧的文章就介绍到这了,更多相关Android启动第一帧内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

你可能感兴趣的:(详解Android启动第一帧)