Android App 冷啟動時間檢測

原始文章:https://dev.to/pyricau/android-vitals-when-did-my-app-start-24p4

最近在分析 App 啟動性能,剛好看到這篇文章:Android Vitals – When did my app start? 其中分享了幾個紀錄 App 冷啟時間的方式,有些點也可以幫助理解一下 App 啟動的點。在此翻譯分享一下。

Application.onCreate

最簡單的方式是在 Application create 的時間點紀錄啟動時間,不過這明顯不是一個準確的方法。

class MyApp : Application() {

  var applicationOnCreateMs: Long = 0

  override fun onCreate() {
    super.onCreate()
    applicationOnCreateMs = SystemClock.uptimeMillis()
  }
}


ContentProvider.onCreate()

比 Application.onCreate 稍微再好一點的方法是透過 ContentProvider.onCreate,這個是比較安全的 library 初始化點,Jetpack 提供的 App Startup 就是透過 content provider 來做各種初始化的管理,因為這個時間點其實是比 Application.onCreate 觸發時間還早。

class StartTimeProvider : ContentProvider() {

  var providerOnCreateMs: Long = 0

  override fun onCreate(): Boolean {
    providerOnCreateMs = SystemClock.uptimeMillis()
    return false
  }
}


Class 載入時間點

在任何 class 能使用之前都必須透過 class loader 載入,所以我們還可以利用 static 變數來記錄 class 載入的時間點。

class MyApp : Application() {

  companion object {
    val applicationClassLoadMs = SystemClock.uptimeMillis()
  }
}

不過要注意的是在 Andorid P (Api level 28) 之後第一個被載入的 class 是 AppComponentFactory

@RequiresApi(Build.VERSION_CODES.P)
class StartTimeFactory : androidx.core.app.AppComponentFactory() {

  companion object {
    val factoryClassLoadMs = SystemClock.uptimeMillis()
  }
}
<?xml version="1.0" encoding="utf-8"?>
<manifest 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example">

  <!-- Replace AndroidX appComponentFactory. -->
  <application
      android:appComponentFactory="com.example.StartTimeFactory"
      tools:replace="android:appComponentFactory"
      tools:targetApi="p" />
</manifest>


Process 啟動時間

每一個 App 的 Application 在能 bind 之前都要先有一個來自 Zygote 複製出來的 process,在 linux base 的系統中 process 的資訊會記錄在一個系統檔案中:/proc/[pid]/stat。Process 的啟動時間也記錄在這檔案裡。透過以下的 Process utility code 可以讀取 process 啟動時間:

object Processes {

  fun readProcessForkRealtimeMillis(): Long {
    val myPid = android.os.Process.myPid()
    val ticksAtProcessStart = readProcessStartTicks(myPid)
    // Min API 21, use reflection before API 21.
    // See https://stackoverflow.com/a/42195623/703646
    val ticksPerSecond = Os.sysconf(OsConstants._SC_CLK_TCK)
    return ticksAtProcessStart * 1000 / ticksPerSecond
  }

  // Benchmarked (with Jetpack Benchmark) on Pixel 3 running 
  // Android 10. Median time: 0.13ms
  fun readProcessStartTicks(pid: Int): Long {
    val path = "/proc/$pid/stat"
    val stat = BufferedReader(FileReader(path)).use { reader ->
      reader.readLine()
    }
    val fields = stat.substringAfter(") ")
        .split(' ')
    return fields[19].toLong()
  }
}

雖然我們可以拿到 process 真正的啟動時間,但是以 Android App 冷啟動的分析來說,真正有意義的點是要從 ActivityThread.handleBindApplication() 開始計算,因為以 App 開發的角度,在這之前能做的事情很有限。

另外因為 Android 系統最佳化而使用保留的 process,有可能 App 最後 bind 的 process 是已經 fork 許久的 process。(這個又衍生出了冷啟動的定義究竟是要包含 process create 還是不包含.. )

Bind application time

ActivityThread bind application 時的第一件事情就是設置 process 起始時間:

public class ActivityThread {

  private void handleBindApplication(AppBindData data) {
    // Note when this process has started.
    Process.setStartTimes(
        SystemClock.elapsedRealtime(), 
        SystemClock.uptimeMillis()
    );
    ...
  } 
}

這麼一來我們就可以直接透過對應的 Process.getStartUptimeMillis() 取得 application bind 的時間點。似乎很棒吧?

在 Android 28 之前的確很棒,以此方式測量的啟動時間中位數大約是 250ms。但在 Android 28+ 上,最長可以到14小時。目前暫時還沒理解原因,猜測是 Android 在 bind application 後將 process 停住,因而導致實際的啟動點更晚了。

結論

總結一下我們目前能相對準確的測量 App 冷啟動的時間點:

  • API 25 前:此用 Content Provider class 載入時間
  • API 25 – API 28: 透過 Process.getStartUptimeMillis()
  • API 28+: 透過 Process.getStartUptimeMillis() 但需要過濾異常數值 (比如超過一分鐘才從 bind application 到 Application.onCreate

Cover images source: https://stacksense.io/krishnan/platforms/on-the-serverless-cold-start-problem/



Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *