adb 如何衡量应用启动

1,297 阅读4分钟

可以利用 ActivityTaskManager 的输出来获取应用程序启动持续时间。

每当 Activity 启动时,都会在 logcat 输出中看到类似的内容:

ActivityTaskManager: Displayed
com.android.samples.mytest/.MainActivity: +1s380ms

此持续时间(在此示例中为 1,380 毫秒)表示从启动应用程序到系统认为它“已启动”的时间,其中包括绘制第一帧(因此为“已显示”)。

这篇文章深入探讨了这个问题:

ActivityTaskManager 究竟测量什么?

ActivityTaskManager 测量从 system_process 接收到启动活动的意图到该活动的窗口完成绘制的时间(API < 30 上的正常运行时间,API 30+ 上的实时时间)。

关键要点:

此度量包括应用程序代码和资源加载之前的几百毫秒,即应用程序开发人员无法影响的时间。

可以在应用程序内进行测量而无需额外的时间,我将在最后分享如何进行。

ActivityTaskManager log

ActivityTaskManager: Displayed
com.android.samples.mytest/.MainActivity: +1s380ms

我们知道日志的样子,所以我们可以在 cs.android.com 上搜索它:"Displayed"

这导致我们到 ActivityTaskManager.logAppDisplayed():

private void logAppDisplayed(TransitionInfoSnapshot info) {
  StringBuilder sb = mStringBuilder;
  sb.setLength(0);
  sb.append("Displayed ");
  sb.append(info.launchedActivityShortComponentName);
  sb.append(": ");
  TimeUtils.formatDuration(info.windowsDrawnDelayMs, sb);
  Log.i(TAG, sb.toString());
}

启动持续时间为 TransitionInfoSnapshot.windowsDrawnDelayMs。 它在 TransitionInfoSnapshot.notifyWindowsDrawn() 中计算:

TransitionInfoSnapshot notifyWindowsDrawn(
  ActivityRecord r, 
  long timestampNs
) {  
  TransitionInfo info = getActiveTransitionInfo(r);
  info.mWindowsDrawnDelayMs = info.calculateDelay(timestampNs);
  return new TransitionInfoSnapshot(info);
}

private static final class TransitionInfo {
 int calculateDelay(long timestampNs) {
   long delayNanos = timestampNs - mTransitionStartTimeNs;
   return (int) TimeUnit.NANOSECONDS.toMillis(delayNanos);
 }
}

让我们找出 timestampNs 和 mTransitionStartTimeNs 在哪里被捕获。

ActivityMetricsLogger.notifyActivityLaunching() 捕获活动转换的开始:

private LaunchingState notifyActivityLaunching(
  Intent intent,
  ActivityRecord caller,
  int callingUid
) {
  long transitionStartNs = SystemClock.elapsedRealtimeNanos();
  LaunchingState launchingState = new LaunchingState();
  launchingState.mCurrentTransitionStartTimeNs = transitionStartNs;
  return launchingState;
}

TransitionInfoSnapshot.notifyWindowsDrawn() 由 ActivityRecord.onWindowsDrawn() 调用,后者由 ActivityRecord.updateReportedVisibilityLocked() 调用:

void updateReportedVisibilityLocked() {
  // ...
  if (nowDrawn != reportedDrawn) {
    onWindowsDrawn(nowDrawn, SystemClock.elapsedRealtimeNanos());
    reportedDrawn = nowDrawn;
  }
  // ...
}

我们现在知道在哪里捕获开始和结束时间戳,但不幸的是 ActivityMetricsLogger.notifyActivityLaunching() 和 ActivityRecord.updateReportedVisibilityLocked() 有很多调用点,因此很难在 AOSP 源中进一步挖掘。

调试system_process

我告诉一个朋友,我在查看 Android 资源时遇到了死胡同,他问我:

为什么不设置断点?

我从未尝试过调试 system_process,但我们没有理由不能。 谢谢文森特的主意! 幸运的是,Android Studio 设置为查找应用编译所针对的 Android 版本的源代码。

使用 Root 设备,我可以将调试器连接到 system_process。

当我启动我的应用程序时,我遇到了 ActivityMetricsLogger.notifyActivityLaunching() 的断点。

和 TransitionInfoSnapshot.notifyWindowsDrawn() 的另一个断点。

读取堆栈跟踪

第一个堆栈跟踪显示当 system_process 收到启动活动的 Intent 时捕获开始时间戳。

第二个堆栈跟踪显示当该活动的窗口完成绘制时捕获结束时间戳。 相应的帧应在 16 毫秒内在显示屏上可见。

应用启动时间

启动 Activity 的用户体验在用户触摸屏幕时开始,但是应用程序开发人员对 ActivityThread.handleBindApplication() 之前花费的时间几乎没有影响,因此应用程序冷启动监控应该从这里开始。

ActivityThread.handleBindApplication() 加载 APK 和应用程序组件(AppComponentFactory、ContentProvider、Application)。 不幸的是,ActivityTaskManager 使用 ActivityTaskManagerService.startActivity() 作为开始时间,它发生在 ActivityThread.handleBindApplication() 之前的一段时间。

ActivityTaskManager 增加了多少时间?

我展示了我们可以使用 Process.getStartUptimeMillis() 来获取调用 ActivityThread.handleBindApplication() 的时间戳。 我还分享了一个代码片段来读取进程 fork 时间(参见 Processes.readProcessForkRealtimeMillis())。 我们可以将这 2 个值记录到 logcat:

val forkRealtime = Processes.readProcessForkRealtimeMillis()
val nowRealtimeMs = SystemClock.elapsedRealtime()
val nowUptimeMs = SystemClock.uptimeMillis()
val elapsedRealtimeMs = nowRealtimeMs - forkRealtime
val forkUptimeMs = nowUptimeMs - elapsedRealtimeMs
Log.d("AppStart", "$forkUptimeMs fork timestamp")

val processStart = Process.getStartUptimeMillis()
Log.d("AppStart", "$processStart bindApplication() timestamp")

我们还需要记录 ActivityMetricsLogger.mCurrentTransitionStartTime。 我们可以让我们之前的 system_process 断点非挂起并让它记录值。 Evaluate 和 log 的输出进入调试器控制台。 我们希望所有日志都在 logcat 中,因此我们从那里调用 Log.d()。

结果

D/AppStart: 27464211 Intent received
D/AppStart: 27464340 fork timestamp
D/AppStart: 27464533 bindApplication() timestamp
...
I/ActivityTaskManager: Displayed
com.example.logstartup/.MainActivity: +1s185ms

从接收到分叉 zygote 进程的意图需要 129 毫秒,从分叉到 ActivityThread.handleBindApplication() 需要 193 毫秒,即应用程序开始加载其代码和资源之前的 322 毫秒。 在此示例中,这是 ActivityTaskManager 报告的应用启动时间的约 30%。

实际数字低于此值,因为 system_process 正在运行并连接调试器。

从应用程序内部测量应用程序启动时间

我将该时间戳与传递给 TransitionInfoSnapshot.notifyWindowsDrawn() 的时间戳进行了比较,这两个值仅相隔几毫秒。

我们可以把我们学到的东西放在一起来衡量应用程序内的应用程序启动持续时间:

class MyApp : Application() {

  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 name = activity::class.java.simpleName
        val window = activity.window
        window.onDecorViewReady {
          window.decorView.onNextDraw {
            if (firstDraw) return@onNextDraw
            firstDraw = true
            handler.postAtFrontOfQueue {
              val start = Process.getStartUptimeMillis()
              val now = SystemClock.uptimeMillis()
              val startDurationMs = now - start
              Log.d(
                "AppStart", 
                "Displayed $name in $startDurationMs ms"
              )
            }
          }
        }
      }
    })
  }
}
D/AppStart: Displayed MainActivity in 863 ms

结论

ActivityTaskManager 的输出很方便,如果您尝试比较应用程序不同版本的启动时间,则完全值得使用。 请注意,应用程序开发人员无法影响那段时间的重要部分。

可以从应用程序内测量应用程序启动时间。