详解Android启动第一帧

目录
  • 1、第一帧什么时候开始调度
  • 2、第一帧
  • 3、第一次绘制
    • ViewTreeObserver
    • ViewTreeObserver.addOnDrawListener()
    • ViewTreeObserver.removeOnDrawListener()
    • FloatingTreeObserver
    • DecorView
  • 四、锁窗特性
    • Window.Callback.onContentChanged()
  • 五、利用 Window.onDecorViewReady()
    • Handler.postAtFrontOfQueue()

冷启动结束的时间怎么确定?根据 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启动第一帧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • android 获取视频第一帧作为缩略图的方法

    今天,简单讲讲android里如何获取一个视频文件的第一帧作为缩略图显示在界面上. 之前,我说个最近需要从服务器下载视频文件,但是下载后肯定需要显示视频的缩略图在界面上给用户看,于是想到显示视频的第一帧作为缩略图.但是我不知道具体怎么写,于是在网上查找资料,最终是解决了问题.这里记录一下. 一.使用MediaMetadataRetriever获取视频的第一帧作为缩略图 /** * 获取视频文件截图 * * @param path 视频文件的路径 * @return Bitmap 返回获取的Bit

  • 详解Android启动第一帧

    目录 1.第一帧什么时候开始调度 2.第一帧 3.第一次绘制 ViewTreeObserver ViewTreeObserver.addOnDrawListener() ViewTreeObserver.removeOnDrawListener() FloatingTreeObserver DecorView 四.锁窗特性 Window.Callback.onContentChanged() 五.利用 Window.onDecorViewReady() Handler.postAtFrontOf

  • 详解Android Activity的启动流程

    前言 activity启动的流程分为两部分:一是在activity中通过startActivity(Intent intent)方法启动一个Activity:二是我们在桌面通过点击应用图标启动一个App然后显示Activity:第二种方式相较于第一种方式更加全面,所以本文会以第二种流程来分析. 简要 我们手机的桌面是一个叫做Launcher的Activity,它罗列了手机中的应用图标,图标中包含安装apk时解析的应用默认启动页等信息.在点击应用图标时,即将要启动的App和Launcher.AMS

  • 详解Android中的ActivityThread和APP启动过程

    ActiviryThread ActivityThread的初始化 ActivityThread即Android的主线程,也就是UI线程,ActivityThread的main方法是一个APP的真正入口,MainLooper在它的main方法中被创建. //ActivityThread的main方法 public static void main(String[] args) { ... Looper.prepareMainLooper(); ActivityThread thread = ne

  • 详解Android性能优化之启动优化

    1.为什么要进行启动优化 网上流行一种说法,就是8秒定律,意思是说,如果用户在打开一个页面,在8秒的时间内还没有打开,那么用户大概的会放弃掉,意味着一个用户的流失.从这里就可以看出,启动优化的重要性了. 2.启动的分类 2.1 冷启动 先来看看冷启动的流程图 从图中可以看出,APP启动的过程是:ActivityManagerProxy 通过IPC来调用AMS(ActivityManagerService),AMS通过IPC启动一个APP进程,ApplicationThread通过反射来创建App

  • 详解Android壁纸服务的启动过程

    壁纸基础 android中的壁纸分为动态壁纸和静态壁纸两种,两种类型的壁纸都以Service的类型运行在系统后台. 静态壁纸:仅以图片的形式进行展示对于静态壁纸,可以使用WallpaperManager中的getDrawable()等接口获取到当前的bitmap图像. 动态壁纸:显示的内容为动态的内容,同时可以对用户的操作做出响应对于动态壁纸的实时图像,是没办法通过android中原生的接口获取到,需要获取到动态壁纸的图像得自己修改源码. 壁纸实现时涉及的几个主要的类: WallpaperSer

  • 详解Android广播Broadcast的启动流程

    目录 正文 广播的注册 广播的解注册 广播的发送 总结 正文 本文整体阅读下来相对Activity和Service的启动流程较容易,比较贴近我们日常代码开发习惯.我们曾经有个整机项目,多个APP跨进程交互,本来想采用AIDL进行的,但最终考虑到项目工期和其他同事的能力,最终在采用广播方式进行IPC. 那时,自己也在想,这么多个APP相互发信息,数据量也大,对整机性能有影响么?会不会存在丢失和内存问题.一脸茫然,网上也不会有类似信息告诉总结这种情况,本文也不会总结这个答案,因为看完之后心中自然有数

  • 详解Android GLide图片加载常用几种方法

    目录 缓存浅析 GLide图片加载方法 图片加载周期 图片格式(Bitmap,Gif) 缓存 集成网络框架 权限 占位符 淡入效果 变换 启动页/广告页 banner 固定宽高 圆角 圆形 总结 缓存浅析 为啥要做缓存? android默认给每个应用只分配16M的内存,所以如果加载过多的图片,为了 防止内存溢出 ,应该将图片缓存起来. 图片的三级缓存分别是: 1.内存缓存 2.本地缓存 3.网络缓存 其中,内存缓存应优先加载,它速度最快:本地缓存次优先加载,它速度也快:网络缓存不应该优先加载,它

  • 详解Android中图片的三级缓存及实例

    详解Android中图片的三级缓存及实例 为什么要使用三级缓存 如今的 Android App 经常会需要网络交互,通过网络获取图片是再正常不过的事了 假如每次启动的时候都从网络拉取图片的话,势必会消耗很多流量.在当前的状况下,对于非wifi用户来说,流量还是很贵的,一个很耗流量的应用,其用户数量级肯定要受到影响 特别是,当我们想要重复浏览一些图片时,如果每一次浏览都需要通过网络获取,流量的浪费可想而知 所以提出三级缓存策略,通过网络.本地.内存三级缓存图片,来减少不必要的网络交互,避免浪费流量

  • 详解Android 中AsyncTask 的使用

    详解Android 中AsyncTask 的使用 1.首先我们来看看AsyncTask 的介绍:   Handler 和 AsyncTask 都是android 中用来实现异步任务处理的方式:其中: Handler 实例向 UI 线程发送消息,完成界面更新, 优点:对整个过程控制的比较精细:         缺点:代码相对臃肿,多个任务同时执行时,不易对线程进行精确的控制: AsyncTask :比Handler 更轻量级一些,适用于简单的异步处理: 优点:简单 | 快捷 | 过程可控:    

  • Activity实例详解之启动activity并返回结果

    先给大家展示下效果展示图: 1 简介 如果想在Activity中得到新打开Activity 关闭后返回的数据,需要使用系统提供的startActivityForResult(Intent intent, int requestCode)方法打开新的Activity,新的Activity 关闭后会向前面的Activity传回数据,为了得到传回的数据,必须在前面的Activity中重写onActivityResult(int requestCode, int resultCode, Intent d

随机推荐