详解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启动第一帧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

时间: 2021-10-12

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

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

Android如何获取视频首帧图片

Android获取视频首帧图片或第n秒的图片,供大家参考,具体内容如下 这里介绍如何获取视频首帧或者第n秒的图片并保存在本地,直接上代码: import android.graphics.Bitmap; import android.media.MediaMetadataRetriever; import android.os.Bundle; import android.os.Environment; import android.support.v7.app.AppCompatActivit

C#获取视频某一帧的缩略图的方法

本文实例讲述了C#获取视频某一帧的缩略图的方法.分享给大家供大家参考.具体实现方法如下: 读取方式:使用ffmpeg读取,所以需要先下载ffmpeg.网上资源有很多. 原理是通过ffmpeg执行一条命令获取视频某一帧的缩略图. 首先,需要获取视频的帧高度和帧宽度,这样获取的缩略图才不会变形. 获取视频的帧高度和帧宽度可以参考:http://www.jb51.net/article/57475.htm. 获取到视频的帧高度和帧宽度后,还需要获取缩略图的高度和宽度,这是按比例缩放的. 比如你存放缩略

Android获取本机各种类型文件的方法

介绍 本篇介绍Android获取本机各种类型文件的方法,已经封装成工具类,末尾有源码下载地址. 提示 获取音乐.视频.图片.文档等文件是需要有读取SD卡的权限的,如果是6.0以下的系统,则直接在清单文件中声明SD卡读取权限即可:如果是6.0或以上,则需要动态申请权限. FileManager的使用 FileManager是封装好的用于获取本机各类文件的工具类,使用方式如:FileManager.getInstance(Context context).getMusics(),使用的是单例模式创建

Android获取手机系统版本等信息的方法

本文实例讲述了Android获取手机系统版本等信息的方法.分享给大家供大家参考.具体如下: String phoneInfo = "Product: " + android.os.Build.PRODUCT; phoneInfo += ", CPU_ABI: " + android.os.Build.CPU_ABI; phoneInfo += ", TAGS: " + android.os.Build.TAGS; phoneInfo += &qu

android获取监听SD Card状态的方法

本文实例讲述了android获取监听SD Card状态的方法.分享给大家供大家参考.具体分析如下: 1. 注册StorageEventListener来监听SD卡状态即onStorageStateChanged()方法,当sd卡状态改变时,调用该方法. 复制代码 代码如下: public void onStorageStateChanged(String path,String oldState,String newState){ if (newState.equals(Environment.

实现Android 获取cache缓存的目录路径的方法

实现Android 获取cache缓存的目录路径的方法 Android开发中,有时需要知道cache缓存的路径.我写了一个静态类,供大家能参考 public class CommonUtil { /** * 获取cache路径 * * @param context * @return */ public static String getDiskCachePath(Context context) { if (Environment.MEDIA_MOUNTED.equals(Environmen

Android获取手机本机号码的实现方法

Android获取手机本机号码的实现方法 反射TelephoneManager 获取本机号码,注意一下提供的接口有的SIM卡没写是获取不到的,该接口只适配Android5.0以上版本 public String getMsisdn(int slotId) { return getLine1NumberForSubscriber(getSubIdForSlotId(slotId)); } 权限 <uses-permission android:name="android.permission

Android获取栈顶的应用包名方法

有时候我们需要判断栈顶的应用是否是我们的应用,于是获取栈顶的应用包名的需求就出现了. 在android5.0之前,系统提供了一套API可以实现这个功能. ActivityManager manager = (ActivityManager) getApplicationContext().getSystemService(ACTIVITY_SERVICE); String currentClassName = manager.getRunningTasks(1).get(0).topActivi

android 获取视频,图片缩略图的具体实现

1.获取视频缩略图有两个方法(1)通过内容提供器来获取(2)人为创建缩略图 (1)缺点就是必须更新媒体库才能看到最新的视频的缩略图 [java] 复制代码 代码如下: /**      * @param context      * @param cr     * @param Videopath     * @return      */     public static Bitmap getVideoThumbnail(Context context, ContentResolver cr