应用启动和退出是全埋点中最基础也最关键的指标。启动埋点区分冷启动/热启动,退出埋点区分退到后台 vs 进程被杀。借助 ProcessLifecycleOwner 和 ActivityLifecycleCallbacks 可以零侵入地追踪应用前后台状态切换。
一、AOSP 源码视角:应用进程的生命周期 1.1 Android 进程生命周期模型 Android 的进程生命周期由系统管理,开发者没有直接的「进程创建」和「进程销毁」回调。生命周期层次如下:
系统层面(Zygote → ActivityManagerService) ├── 进程创建:Zygote fork → ActivityThread.main() ├── 进程优先级:oom_adj (0 = 前台 → 1000 = 缓存) └── 进程销毁:LMK (Low Memory Killer) 或 ActivityManagerService.killProcess() 应用层面(Application → Activity) ├── Application.onCreate() ← 进程创建的第一个应用层回调 ├── Activity.onCreate/Start/Resume ← 用户可感知的生命周期 ├── Activity.onPause/Stop/Destroy ← 用户离开时的回调(不一定触发) └── ProcessLifecycleOwner ← 全局前台/后台状态
1.2 启动类型定义(AOSP 视角) AOSP 的 ActivityManagerService 中使用 ProcessRecord 来跟踪每个进程的状态。启动类型可以这样定义:
启动类型
进程状态
Activity 状态
首个可见回调
典型耗时
冷启动
进程不存在
无
Application.onCreate → Activity.onResume
500ms-3s
温启动
进程存在
所有 Activity 已销毁
Activity.onCreate (进程存在)
200ms-1s
热启动
进程存在
至少一个 Activity 在后台
Activity.onStart → onResume
50ms-200ms
二、冷启动全埋点实现 2.1 启动时间检测 class TrackingInitProvider : ContentProvider () { companion object { @JvmField var processStartTime: Long = -1L @JvmField var applicationStartTime: Long = -1L } init { if (processStartTime == -1L ) { processStartTime = SystemClock.elapsedRealtime() } } override fun onCreate () : Boolean { processStartTime = SystemClock.elapsedRealtime() val application = context?.applicationContext as ? Application application?.registerActivityLifecycleCallbacks( AppLaunchTracker() ) return true } override fun query (p0: Uri , p1: Array <out String >?, p2: String ?, p3: Array <out String >?, p4: String ?) : Cursor? = null override fun getType (p0: Uri ) : String? = null override fun insert (p0: Uri , p1: ContentValues ?) : Uri? = null override fun delete (p0: Uri , p1: String ?, p2: Array <out String >?) : Int = 0 override fun update (p0: Uri , p1: ContentValues ?, p2: String ?, p3: Array <out String >?) : Int = 0 }
2.2 启动事件采集 class AppLaunchTracker : Application.ActivityLifecycleCallbacks { companion object { private var foregroundActivityCount = 0 private var isAppInForeground = false private var sessionId: String = generateSessionId() private var lastBackgroundTimestamp = 0L private var firstActivityCreatedTime = -1L private var firstActivityResumedTime = -1L fun generateSessionId () : String = UUID.randomUUID().toString() } override fun onActivityCreated (activity: Activity , savedInstanceState: Bundle ?) { if (foregroundActivityCount == 0 ) { val now = SystemClock.elapsedRealtime() val appInitDuration = if (TrackingInitProvider.applicationStartTime > 0 ) { now - TrackingInitProvider.applicationStartTime } else { -1L } val launchType = determineLaunchType(savedInstanceState) val launchSource = detectLaunchSource(activity) if (launchType == "cold_start" ) { sessionId = generateSessionId() } firstActivityCreatedTime = now trackAppLaunch( type = launchType, source = launchSource, processStartTime = TrackingInitProvider.processStartTime, appInitDuration = appInitDuration, activity = activity ) } } override fun onActivityStarted (activity: Activity ) { foregroundActivityCount++ if (!isAppInForeground && foregroundActivityCount == 1 ) { isAppInForeground = true val backgroundDuration = if (lastBackgroundTimestamp > 0 ) { System.currentTimeMillis() - lastBackgroundTimestamp } else { -1L } trackAppLaunch( type = "hot_start" , source = "background" , backgroundDurationMs = backgroundDuration, activity = activity ) } } override fun onActivityResumed (activity: Activity ) { if (firstActivityResumedTime == -1L ) { firstActivityResumedTime = SystemClock.elapsedRealtime() val totalLaunchTime = firstActivityResumedTime - TrackingInitProvider.processStartTime trackLaunchPerformance(totalLaunchTime) } } override fun onActivityPaused (activity: Activity ) {} override fun onActivityStopped (activity: Activity ) { foregroundActivityCount-- if (foregroundActivityCount == 0 ) { isAppInForeground = false lastBackgroundTimestamp = System.currentTimeMillis() trackAppBackground(sessionId, activity) } } override fun onActivitySaveInstanceState (activity: Activity , outState: Bundle ) { } override fun onActivityDestroyed (activity: Activity ) { if (activity.isFinishing && foregroundActivityCount == 0 ) { trackAppExit(type = "user_exit" , reason = "activity_finish" ) } } private fun determineLaunchType (savedInstanceState: Bundle ?) : String { return when { TrackingInitProvider.applicationStartTime == -1L -> "cold_start" savedInstanceState != null -> "warm_start" foregroundActivityCount == 0 && lastBackgroundTimestamp > 0 -> "warm_start" else -> "cold_start" } } private fun detectLaunchSource (activity: Activity ) : String { val intent = activity.intent ?: return "unknown" return when { intent.action == Intent.ACTION_MAIN && intent.hasCategory(Intent.CATEGORY_LAUNCHER) -> { "launcher_icon" } intent.action == Intent.ACTION_VIEW -> { if (intent.data != null ) "deep_link" else "unknown" } intent.hasExtra("notification_id" ) -> "notification" intent.hasExtra("widget" ) -> "widget" intent.action == Intent.ACTION_SEND -> "share" intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0 -> "recent" else -> "other" } } private fun trackAppLaunch ( type: String , source: String , processStartTime: Long = -1 L, appInitDuration: Long = -1 L, backgroundDurationMs: Long = -1 L, activity: Activity ) { AnalyticsSDK.track("app_launch" , mapOf( "launch_type" to type, "launch_source" to source, "session_id" to sessionId, "timestamp" to System.currentTimeMillis(), "process_start_time" to processStartTime, "app_init_duration_ms" to appInitDuration, "background_duration_ms" to backgroundDurationMs, "first_activity" to activity.javaClass.simpleName, "intent_action" to (activity.intent?.action ?: "" ), "referrer" to (activity.referrer?.toString() ?: "" ) )) } private fun trackLaunchPerformance (totalLaunchTime: Long ) { AnalyticsSDK.track("launch_performance" , mapOf( "total_launch_time_ms" to totalLaunchTime, "cold_start" to (TrackingInitProvider.applicationStartTime == -1L ), "timestamp" to System.currentTimeMillis() )) } private fun trackAppBackground (sessionId: String , lastActivity: Activity ) { AnalyticsSDK.track("app_background" , mapOf( "session_id" to sessionId, "last_activity" to lastActivity.javaClass.simpleName, "timestamp" to System.currentTimeMillis() )) } private fun trackAppExit (type: String , reason: String ) { AnalyticsSDK.track("app_exit" , mapOf( "exit_type" to type, "exit_reason" to reason, "session_id" to sessionId, "timestamp" to System.currentTimeMillis() )) } }
三、ProcessLifecycleOwner:全局前后台感知 3.1 原理与配置 dependencies { implementation "androidx.lifecycle:lifecycle-process:2.7.0" }
class AppLifecycleObserver ( private val analyticsSDK: AnalyticsSDK = AnalyticsSDK.getInstance() ) : DefaultLifecycleObserver { private var sessionId = UUID.randomUUID().toString() private var sessionStartTime = System.currentTimeMillis() private var isFirstLaunch = true init { ProcessLifecycleOwner.get ().lifecycle.addObserver(this ) } override fun onCreate (owner: LifecycleOwner ) { analyticsSDK.track("process_create" , mapOf( "session_id" to sessionId, "timestamp" to System.currentTimeMillis() )) } override fun onStart (owner: LifecycleOwner ) { if (isFirstLaunch) { analyticsSDK.track("app_launch" , mapOf( "launch_type" to "cold_start" , "session_id" to sessionId, "timestamp" to System.currentTimeMillis() )) isFirstLaunch = false } else { analyticsSDK.track("app_launch" , mapOf( "launch_type" to "hot_start" , "session_id" to sessionId, "timestamp" to System.currentTimeMillis() )) } } override fun onResume (owner: LifecycleOwner ) { } override fun onPause (owner: LifecycleOwner ) { } override fun onStop (owner: LifecycleOwner ) { val sessionDuration = System.currentTimeMillis() - sessionStartTime analyticsSDK.track("app_background" , mapOf( "session_id" to sessionId, "session_duration_ms" to sessionDuration, "timestamp" to System.currentTimeMillis() )) persistSessionState(sessionId, sessionDuration) } override fun onDestroy (owner: LifecycleOwner ) { analyticsSDK.track("app_exit" , mapOf( "session_id" to sessionId, "timestamp" to System.currentTimeMillis() )) analyticsSDK.flush() } private fun persistSessionState (sessionId: String , duration: Long ) { } }
四、退出检测与异常退出识别 4.1 退出类型 App 退出 ├── 主动退出 │ ├── 用户按返回键退出 (Activity.isFinishing) │ ├── 调用 finishAffinity() / System.exit() │ └── 通过通知栏清理 ├── 退到后台 │ ├── 按 Home 键 │ ├── 切换到其他 App(Task Switcher) │ └── 点击通知栏启动其他 App └── 异常退出(无法正常回调) ├── 系统回收(LMK 杀进程) ├── 用户 Force Stop(设置中强制停止) ├── 崩溃(未捕获异常) ├── ANR 后被系统杀死 └── 设备重启 / 关机
4.2 异常退出检测 class AbnormalExitDetector (private val context: Context) { private val prefs: SharedPreferences = context.getSharedPreferences("tracking_state" , Context.MODE_PRIVATE) companion object { private const val KEY_LAST_SESSION_ID = "last_session_id" private const val KEY_LAST_STATE = "last_state" private const val KEY_LAST_TIMESTAMP = "last_timestamp" private const val KEY_LAST_ACTIVITY = "last_activity" private const val STATE_FOREGROUND = "foreground" private const val STATE_BACKGROUND = "background" private const val STATE_DESTROYED = "destroyed" } fun detectOnLaunch () : AbnormalExitInfo? { val lastSessionId = prefs.getString(KEY_LAST_SESSION_ID, null ) ?: return null val lastState = prefs.getString(KEY_LAST_STATE, STATE_DESTROYED) ?: STATE_DESTROYED val lastTimestamp = prefs.getLong(KEY_LAST_TIMESTAMP, 0L ) val lastActivity = prefs.getString(KEY_LAST_ACTIVITY, "" ) if (lastState != STATE_DESTROYED) { val abnormalType = when (lastState) { STATE_FOREGROUND -> "killed_in_foreground" STATE_BACKGROUND -> "killed_in_background" else -> "unknown" } return AbnormalExitInfo( lastSessionId = lastSessionId, abnormalType = abnormalType, lastState = lastState, lastTimestamp = lastTimestamp, lastActivity = lastActivity ?: "unknown" ) } return null } fun markState (state: String , activityName: String = "" ) { prefs.edit() .putString(KEY_LAST_STATE, state) .putString(KEY_LAST_ACTIVITY, activityName) .putLong(KEY_LAST_TIMESTAMP, System.currentTimeMillis()) .apply() } fun markNormalExit () { prefs.edit() .putString(KEY_LAST_STATE, STATE_DESTROYED) .apply() } data class AbnormalExitInfo ( val lastSessionId: String, val abnormalType: String, val lastState: String, val lastTimestamp: Long , val lastActivity: String ) } class App : Application () { private lateinit var abnormalExitDetector: AbnormalExitDetector override fun onCreate () { super .onCreate() abnormalExitDetector = AbnormalExitDetector(this ) val abnormalInfo = abnormalExitDetector.detectOnLaunch() if (abnormalInfo != null ) { AnalyticsSDK.track("abnormal_exit_detected" , mapOf( "last_session_id" to abnormalInfo.lastSessionId, "abnormal_type" to abnormalInfo.abnormalType, "last_state" to abnormalInfo.lastState, "last_activity" to abnormalInfo.lastActivity )) } abnormalExitDetector.markState("foreground" , "" ) } }
五、启动性能 Vitals 采集 5.1 分段耗时测量 class LaunchPerformanceTracker { companion object { private var appOnCreateStartTime = -1L private var appOnCreateEndTime = -1L private var firstActivityOnCreateStartTime = -1L private var firstFrameRenderedTime = -1L } fun markAppOnCreateStart () { appOnCreateStartTime = SystemClock.elapsedRealtime() } fun markAppOnCreateEnd () { appOnCreateEndTime = SystemClock.elapsedRealtime() } fun markFirstActivityOnCreateStart () { firstActivityOnCreateStartTime = SystemClock.elapsedRealtime() } fun observeFirstFrame (activity: Activity ) { activity.window.decorView.viewTreeObserver .addOnDrawListener(object : ViewTreeObserver.OnDrawListener { override fun onDraw () { if (firstFrameRenderedTime == -1L ) { firstFrameRenderedTime = SystemClock.elapsedRealtime() reportLaunchMetrics() } activity.window.decorView.viewTreeObserver.removeOnDrawListener(this ) } }) } private fun reportLaunchMetrics () { val ttiDuration = firstActivityOnCreateStartTime - appOnCreateStartTime AnalyticsSDK.track("launch_performance" , mapOf( "app_on_create_ms" to (appOnCreateEndTime - appOnCreateStartTime), "first_activity_on_create_ms" to (firstFrameRenderedTime - firstActivityOnCreateStartTime), "tti_duration_ms" to ttiDuration, "ttfd_duration_ms" to (firstFrameRenderedTime - appOnCreateStartTime) )) } }
六、多进程场景处理 class MultiProcessStartupTracker (private val context: Context) { private val authority = "${context.packageName} .tracking.state" fun getSessionId () : String { val uri = Uri.parse("content://$authority /session_id" ) return try { val cursor = context.contentResolver.query(uri, null , null , null , null ) cursor?.use { if (it.moveToFirst()) it.getString(0 ) else createNewSessionId() } ?: createNewSessionId() } catch (e: Exception) { createNewSessionId() } } private fun createNewSessionId () : String { val sessionId = UUID.randomUUID().toString() val uri = Uri.parse("content://$authority /session_id" ) try { val values = ContentValues().apply { put("session_id" , sessionId) } context.contentResolver.insert(uri, values) } catch (_: Exception) {} return sessionId } fun reportProcessBackground (processName: String ) { val uri = Uri.parse("content://$authority /background_report" ) val values = ContentValues().apply { put("process" , processName) } context.contentResolver.insert(uri, values) } }
七、架构流程图 ┌──────────────────────────────┐ │ Zygote fork → 新进程 │ └──────────────┬───────────────┘ │ ┌──────────────▼───────────────┐ │ TrackingInitProvider │ │ onCreate() (最早初始化) │ │ → 记录 processStartTime │ │ → registerActivityLifecycleCB │ └──────────────┬───────────────┘ │ ┌──────────────▼───────────────┐ │ Application.onCreate() │ │ → 记录 appInitStartTime │ │ → 初始化异常退出检测器 │ └──────────────┬───────────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ┌──────────▼─────┐ ┌────────▼──────┐ ┌───────▼──────┐ │ foregroundCnt │ │ foregroundCnt │ │ foregroundCnt│ │ == 0 → 冷启动 │ │ == 0 && │ │ == 0 && │ │ 首次 Activity │ │ lastTimestamp│ │ 从后台返回 │ │ onCreate │ │ → 温启动 │ │ → 热启动 │ └────────────────┘ └───────────────┘ └──────────────┘ │ │ │ └─────────────────┼─────────────────┘ │ ┌──────────────▼───────────────┐ │ trackAppLaunch() │ │ ├── launch_type │ │ ├── launch_source │ │ ├── session_id │ │ └── launch_duration │ └──────────────┬───────────────┘ │ ┌──────────────▼───────────────┐ │ Activity 生命周期变化 │ │ foregroundCount 维护 │ └──────────────┬───────────────┘ │ ┌─────────────────┼─────────────────┐ │ │ │ ┌──────────▼─────┐ ┌────────▼──────┐ ┌───────▼──────┐ │ count 0→1 │ │ count 1→0 │ │ isFinishing │ │ app_launch │ │ app_background │ │ app_exit │ │ (热启动) │ │ (进入后台) │ │ (主动退出) │ └────────────────┘ └───────────────┘ └──────────────┘
八、神策 / Firebase 集成示例 SensorsDataAPI.sharedInstance().track("$AppStart " , JSONObject().apply { put("$is_first_time " , isFirstLaunch) put("$resume_from_background " , !isFirstLaunch) }) FirebaseAnalytics.getInstance(context).apply { setDefaultEventParameters(Bundle().apply { putString("session_id" , sessionId) }) logEvent(FirebaseAnalytics.Event.APP_OPEN, null ) }
九、ProGuard/R8 规则 -keep class com.example.tracking.launch.TrackingInitProvider { *; } -keep class com.example.tracking.launch.AppLaunchTracker { *; } -keep class com.example.tracking.launch.AbnormalExitDetector { *; } -keep class com.example.tracking.launch.LaunchPerformanceTracker { *; }
面试常考问题 Q1:ProcessLifecycleOwner 如何感知所有 Activity 生命周期?
ProcessLifecycleOwner 内部注册了全局的 ActivityLifecycleCallbacks,通过一个 foregroundActivityCount 计数器跟踪前台 Activity 数量。核心逻辑:(1)onActivityStarted 时计数器 +1,若从 0→1 则判定 App 进入前台;(2)onActivityStopped 时计数器 -1,若从 1→0 则通过 Handler 发送一个 700ms 延迟消息;(3)如果 700ms 内没有新的 Activity onStart(即计数器重新从 0→1),则确认 App 已退到后台,触发 Lifecycle.Event.ON_STOP。这个 700ms 延迟是为了防止 Activity 切换(A.finish + B.start)期间的短暂计数器归零导致的误判。源码位于 androidx.lifecycle.ProcessLifecycleOwner.java。
Q2:kill 进程和用户按 Home 键如何区分?
关键矛盾:进程被 kill 时不执行任何生命周期回调。区分策略:(1)在每次 onActivityStopped(退到后台)和 onActivityDestroyed(主动退出)时,使用 SharedPreferences/MMKV 持久化状态标记和时间戳;(2)下次冷启动时(Application.onCreate 或 ContentProvider.onCreate),检查持久化标记——如果上次记录的 state 是「foreground」或「background」,说明进程被异常终止(kill);(3)如果 state 是「destroyed」,说明是正常的主动退出(用户按 Back 键退出最后一个 Activity);(4)按 Home 键会正常触发 onStop,标记为「background」,后续可区分是自然回到前台(正常)还是被 kill 后冷启动(异常)。
Q3:如何区分 launcher 启动和 deeplink 启动?
通过首个 Activity 的 Intent 属性判断:(1)Launcher 启动:Intent.action == ACTION_MAIN && Intent.hasCategory(CATEGORY_LAUNCHER);(2)Deep Link 启动:Intent.action == ACTION_VIEW && Intent.data != null;(3)通知栏点击:检查 Intent.extras 中是否有自定义 key(如 notification_id),或通过 PendingIntent 携带的标识;(4)最近任务列表:Intent.flags & FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0;(5)分享:Intent.action == ACTION_SEND。更精确的判断可以通过 Activity.getReferrer()(API 22+)获取启动来源包名,或使用 Analytics SDK 的 referrer 参数。
Q4:如何准确测量 App 启动耗时(TTID / TTFD)?
Google Vitals 定义了两种启动时间指标:(1)TTID(Time To Initial Display) :从进程创建到首个 Activity 首帧绘制的时间。测量方法:在 ContentProvider.onCreate()(最早执行点)记录起始时间,在首个 Activity 的 DecorView.getViewTreeObserver().addOnDrawListener 回调中记录结束时间;(2)TTFD(Time To Full Display) :从进程创建到首个 Activity 完成所有异步数据加载和布局的时间。通过 Activity.reportFullyDrawn() 手动标记全量显示时间。Android 系统在 adb shell am start -W 中报告的 TotalTime 就是 TTID。注意区分:System.currentTimeMillis() 受系统时间调整影响,应使用 SystemClock.elapsedRealtime()。
Q5:多进程 App 中,每个进程都会触发 app_launch 事件吗?
是的。每个 Android 进程都有独立的 Application 和 ActivityLifecycleCallbacks 实例。如果不做特殊处理,主进程和子进程(如 WebView 进程、推送进程、图片加载进程)都会各自上报 app_launch 事件,导致数据重复。解决方案:(1)通过 ActivityManager.getRunningAppProcesses() 或 getProcessName() 判断当前进程名,只在主进程上报启动事件;(2)使用 ContentProvider 共享全局 sessionId 和前后台状态;(3)服务端通过 sessionId + processName 做去重和关联;(4)注意 android:process 属性声明的多进程,以及 WebView 可能独立启动的 :webview_sandboxed_process 进程。
AOSP 源码路径:ActivityThread.main() 方法中触发 Application.onCreate()(frameworks/base/core/java/android/app/ActivityThread.java),ActivityManagerService 管理进程生命周期(frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java),ProcessRecord 跟踪进程状态(frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java)。