2. Compose 组件生命周期与埋点时机:onAppear、onDispose、局部重组触发规则
2.1 理解 Compose 组件的“生命周期”
Compose 的组件没有传统 View 的 onCreate/onStart/onResume,而是围绕 组合(Composition) 与 重组(Recomposition) 两个核心阶段。
埋点需要在以下两个关键时机介入:
| 生命周期事件 | 对应 Compose 概念 | 埋点含义 |
|---|---|---|
| 组件首次进入屏幕 | Composition 完成 + onAppear 等效回调 | 曝光埋点(PV/UV) |
| 组件离开屏幕 | Composition 被移除(Dispose) | 离开埋点、停留时长统计 |
| 参数变化导致局部重组 | Recomposition(仅影响界面局部) | 需要判断是否需要重复曝光,避免错误上报 |
关键认知: Compose 没有
onResume / onPause,因此埋点时机完全由 LaunchedEffect、DisposableEffect 以及重组范围控制。滥用 derivedStateOf 或 remember(key) 会导致埋点频繁触发。
2.2 onAppear 埋点实现:LaunchedEffect + 可见性检测
标准做法:在 Composable 函数内使用 LaunchedEffect(Unit) 来模拟 onAppear,该副作用只在组件首次进入组合时执行一次。
@Composable
fun TrackableScreen(
screenName: String,
content: @Composable () -> Unit
) {
// onAppear:组件加入组合时执行一次
LaunchedEffect(Unit) {
Tracker.reportEvent("screen_appear", mapOf("name" to screenName))
}
// 组件离开组合时执行 onDispose
DisposableEffect(Unit) {
onDispose {
Tracker.reportEvent("screen_dispose", mapOf("name" to screenName))
}
}
content()
}
注意: 如果
LaunchedEffect 的 key 发生变化(例如 screenName 作为 key),则每次 key 改变都会触发重新执行,导致重复上报。正确做法是用 Unit 作为 key,仅在首次组合时触发。
2.3 onDispose 埋点实现:DisposableEffect + 清理逻辑
DisposableEffect 在组件离开组合时自动执行 onDispose 回调,适合上报离开埋点、记录停留时长、取消网络请求等。
@Composable
fun StayDurationTracker(screenId: String) {
val startTime = remember { System.currentTimeMillis() }
DisposableEffect(screenId) {
onDispose {
val duration = System.currentTimeMillis() - startTime
Tracker.reportEvent("screen_duration", mapOf(
"screenId" to screenId,
"durationMs" to duration
))
}
}
}
2.4 局部重组时的埋点触发规则
局部重组不会触发 LaunchedEffect(Unit) 或 DisposableEffect(Unit) 重复执行。
但若 LaunchedEffect 的 key 是可变状态,则重组时 key 变化会导致副作用重复执行,需特别注意:
| 使用场景 | key 选择 | 埋点行为 | 是否推荐 |
|---|---|---|---|
| 只在组件首次出现时埋点 | Unit | 仅执行一次 | ✅ 推荐 |
| 根据状态变化重新埋点 | 观察的状态值 | 状态变化时重新执行 | ⚠️ 谨慎(需防重复) |
| 需要重新订阅 DisposableEffect | 订阅资源 ID | 旧资源清理,新资源绑定 | ✅ 推荐 |
2.5 流程图:Compose 组件生命周期与埋点触发路径
2.6 常见错误与解决方案
- 错误1: 在
LaunchedEffect中使用可组合函数的参数作为 key,导致每次重组都重新触发曝光。 - 错误2: 在
onDispose中使用remember变量时,忘记使用rememberUpdatedState,导致捕获的是旧值。 - 错误3: 未考虑列表滚动时子 Item 的
onAppear/onDispose被频繁触发,造成埋点队列暴涨。
解决方案:
- 对列表 Item 使用
- 引入“防抖”机制:在
- 使用
- 对列表 Item 使用
modifier.fillMaxWidth() + key(item.id) 保证缓存稳定,减少重组次数。- 引入“防抖”机制:在
LaunchedEffect 内使用 delay(50) 合并短时间内的多次触发。- 使用
SnapshotStateObserver 或 modifier.onGloballyPositioned 实现真正的可见性检测(结合 ViewTreeLifecycleOwner)。
2.7 性能与稳定性优化
| 优化方向 | 具体措施 | 效果 |
|---|---|---|
| 减少副作用的频率 | LaunchedEffect 使用 Unit 作为稳定 key;避免在重组中创建新的协程作用域 | 埋点执行次数可控,避免卡顿 |
| 避免内存泄漏 | DisposableEffect 中必须主动释放监听器、关闭数据库游标、取消网络请求 | 防止 OOM 和资源泄露 |
| 合并短时重复事件 | 使用 Flow 的 debounce 或简单的时间戳判断 | 减少上报请求数,降低后端压力 |
| 使用 Modifier 层埋点泛型 | 封装 Modifier.trackScreen(name) 等扩展函数,统一管理生命周期副作用 | 代码复用,减少遗漏 |
2.8 调试建议
- 开启 Compose 重组日志:在
build.gradle.kts添加android.composeOptions.reportsDiffs = true。 - 使用
DisposableEffect的onDispose中打印栈信息,确认是否被意外销毁。 - 在
LaunchedEffect内添加log输出,监控埋点触发时机。
部署注意: 生产环境必须移除调试日志;建议将埋点上报放在
Dispatchers.IO 协程中,避免阻塞主线程导致掉帧。