目录
  1. 1. 一、ProcessLifecycleOwner:应用级生命周期
  2. 2. 二、启动与退出追踪实现
  3. 3. 三、冷/温/热启动的区分
  4. 4. 四、启动耗时测量
  5. 5. 面试常考问题
【全埋点方案系列】AppLauncher、AppExit全埋点

应用启动和退出是全埋点中最基础也最关键的指标。启动埋点区分冷启动/热启动,退出埋点区分退到后台 vs 进程被杀。借助 ProcessLifecycleOwnerActivityLifecycleCallbacks 可以零侵入地追踪应用前后台状态切换。

一、ProcessLifecycleOwner:应用级生命周期

ProcessLifecycleOwner 是 Lifecycle 库提供的全局生命周期感知组件,它监听所有 Activity 的生命周期,在以下时机触发:

  • ON_CREATE:首个 Activity 进入 created 状态
  • ON_START:首个 Activity 进入 started 状态(应用来到前台)
  • ON_RESUME:首个 Activity 进入 resumed 状态
  • ON_PAUSE:最后一个 Activity 进入 paused 状态
  • ON_STOP:最后一个 Activity 进入 stopped 状态(应用退到后台)

二、启动与退出追踪实现

class AppLifecycleTracker : Application.ActivityLifecycleCallbacks {

companion object {
private var foregroundActivityCount = 0
private var isBackground = true
private var coldStartTimestamp = 0L
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (foregroundActivityCount == 0) {
// 应用冷启动(进程首次创建 Activity)
coldStartTimestamp = System.currentTimeMillis()
trackAppLaunch(
type = if (savedInstanceState == null) "cold_start" else "warm_start",
launchSource = detectLaunchSource(activity)
)
}
}

override fun onActivityStarted(activity: Activity) {
foregroundActivityCount++
if (isBackground && foregroundActivityCount == 1) {
isBackground = false
// 从后台回到前台(热启动)
trackAppLaunch(type = "hot_start", source = "background")
}
}

override fun onActivityStopped(activity: Activity) {
foregroundActivityCount--
if (foregroundActivityCount == 0) {
isBackground = true
// 进入后台
trackAppExit(
type = "background",
duration = System.currentTimeMillis() - coldStartTimestamp,
reason = detectExitReason()
)
}
}

override fun onActivityDestroyed(activity: Activity) {
// 如果 isFinishing 且是最后一个 Activity → 进程即将结束
if (activity.isFinishing && foregroundActivityCount == 0) {
trackAppExit(type = "exit", reason = "user_exit")
}
}

private fun detectLaunchSource(activity: Activity): String {
return when {
activity.intent?.action == Intent.ACTION_MAIN -> "launcher"
activity.intent?.action == Intent.ACTION_VIEW -> "deep_link"
activity.intent?.hasExtra("notification_id") == true -> "notification"
else -> "unknown"
}
}

private fun detectExitReason(): String {
return when {
isFinishing -> "user_exit"
isBackground -> "background"
else -> "unknown"
}
}

private fun trackAppLaunch(type: String, source: String) {
AnalyticsSDK.track("app_launch", mapOf(
"type" to type,
"source" to source,
"timestamp" to System.currentTimeMillis(),
"session_id" to generateSessionId()
))
}

private fun trackAppExit(type: String, duration: Long, reason: String) {
AnalyticsSDK.track("app_exit", mapOf(
"type" to type,
"duration_ms" to duration,
"reason" to reason
))
}
}

// 在 Application 中注册
class App : Application() {
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(AppLifecycleTracker())
}
}

三、冷/温/热启动的区分

启动类型 条件 特征
冷启动(Cold) 进程不存在 Application.onCreate → 首个 Activity.onCreate
温启动(Warm) 进程存在但首个 Activity 被销毁 Activity.onCreate 且 Application 已初始化
热启动(Hot) Activity 在后台 onStart → onResume,不经过 onCreate

温启动判断:进程存在 + savedInstanceState != null(Activity 重建)或 foregroundActivityCount == 0coldStartTimestamp > 0

四、启动耗时测量

// 在 Application.onCreate 开始时打点
class App : Application() {
override fun onCreate() {
val startTime = SystemClock.elapsedRealtime()
super.onCreate()
// ...
val appInitTime = SystemClock.elapsedRealtime() - startTime
// 等首个 Activity 首帧渲染后,加上 appInitTime 即为完整启动时间
}
}

AOSP 中 ActivityManagerService 使用 ActivityRecord.getLaunchTime() 计算启动耗时,定义在 frameworks/base/services/core/java/com/android/server/am/ActivityRecord.java


面试常考问题

Q1:ProcessLifecycleOwner 如何感知所有 Activity 生命周期?

ProcessLifecycleOwner 内部注册了 ActivityLifecycleCallbacks,通过计数器跟踪前台 Activity 数量。当计数器从 0→1 时判定 app 进入前台(ON_START),从 1→0 时判定 app 进入后台(ON_STOP)。它使用 Handler 延迟 700ms 来防止 Activity 切换瞬间的误判。源码:androidx.lifecycle.ProcessLifecycleOwner

Q2:kill 进程和用户按 Home 键如何区分?

无法直接监听 kill 信号。按 Home 键会在 onStop 中捕获;进程被 kill(如系统回收、force stop)则不会有任何回调。做法是在 onActivityStopped / onSaveInstanceState 中持久化时间戳,下次冷启动时检查上次退出是否有正常记录,若无则为异常退出。

Q3:如何区分 launcher 启动和 deeplink 启动?

通过首个 Activity 的 intent.actionintent.data 判断:action 为 MAIN 且 category 含 LAUNCHER 时为 launcher 启动;action 为 VIEW 且 data 不为 null 时为 deeplink;通过 referrerPendingIntent 中自定义 extra 字段可区分通知栏点击。

打赏
  • 微信
  • 支付宝

评论