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 组件生命周期与埋点触发路径

Compose 组件生命周期与埋点触发流程 组件首次组合(Composition) onAppear 触发 LaunchedEffect(Unit) 是否有参数(key)变化? 无变化 不触发埋点 key 变化 LaunchedEffect 重新触发 组件移除(Dispose) DisposableEffect onDispose 执行 埋点上报完成 key 用 Unit → 仅首次触发 key 用状态 → 状态变化触发

2.6 常见错误与解决方案

  • 错误1: 在 LaunchedEffect 中使用可组合函数的参数作为 key,导致每次重组都重新触发曝光。
  • 错误2: 在 onDispose 中使用 remember 变量时,忘记使用 rememberUpdatedState,导致捕获的是旧值。
  • 错误3: 未考虑列表滚动时子 Item 的 onAppear / onDispose 被频繁触发,造成埋点队列暴涨。
解决方案:
- 对列表 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 协程中,避免阻塞主线程导致掉帧。