Android 会话跟踪 — 高级工程师视角:当操作系统对你一无所承诺时
Source: Dev.to
介绍
Android 并不保证在用户离开你的应用时会通知你。
本文并非讨论技巧,而是关于在 Android 上正确地进行会话跟踪——无论是在生产环境还是 SDK 层面。
大多数 Android 开发者曾在某个时刻相信以下至少一种说法:
onStop()表示用户已离开onDestroy()表示应用已结束- 从最近任务列表滑动退出即结束会话
这些说法根本都是错误的。Android 并不会为你运行应用;你的应用只是一个客体。进程可能在没有回调的情况下被杀死(SIGKILL / LMK),且不提供任何清理保证。生命周期事件仅在进程存活期间存在。
含义
如果你在等待回调来检测会话结束,诸如 Firebase、AdMob、Adjust、AppsFlyer 等服务 并不会 “检测到被杀”。它们的做法是:
- 跟踪进程的前台/后台状态
- 应用超时启发式规则
- 在下次启动时推断会话结束
没有事件,没有保证——只有概率模型。这是一种高级思维:接受不确定性并围绕它进行设计。
会话生命周期
真实的会话具有以下状态:
- Created(已创建)
- Active(活跃)
- Background(后台)
- Expired(已过期)
- Restored(已恢复,推断得出)
会话不会在应用被杀死时结束;我们会根据以下因素判断其是否仍然有效:
- 后台停留时长
- 下一次启动的时间点
- 持久化的元数据
ProcessLifecycleOwner 提供:
ON_START:进程进入前台ON_STOP:进程进入后台
它 不 会提供:
- 应用被杀死
- 从最近任务列表滑除
- 原生崩溃
而且它本不该提供这些。高级工程师不会要求 API 实现不可能的功能。
超时作为业务规则
超时不是一种 hack;它是一条业务规则:
“如果用户离开超过 X ms,视为会话结束。”
具体数值(30 秒、1 分钟、5 分钟……)取决于产品需求——没有统一的正确值。
当应用被杀死时:
- 没有
onSessionEnd回调 - 没有清理,也没有刷新
剩余的内容:
- 最后一次后台时间戳
- 会话 ID
- 推断的原因
在下次启动时,SDK 必须:
- 加载持久化状态
- 推断上一次会话已结束
- 发出逻辑上的会话结束
这种两阶段设计为资深工程师所熟悉。
建模退出原因
USER_BACKGROUND_TIMEOUTPROCESS_KILLED_INFERREDAPP_UPGRADECRASH_DETECTED
关键词是 inferred。优秀的 SDK 不会声称“用户做了 X”。
如果进行会话跟踪:
- 依赖于 Activities
- 依赖系统回调
- 依赖实时性
→ 这不可测试,因此不可靠。
正确的 SessionTracker
一个合适的 SessionTracker:
- 纯粹的逻辑
- 注入其时钟
- 接受伪造的生命周期信号
可测试意味着易于理解。
SDK 设计目标
- 避免对 Activity 的依赖
- 在进程被杀死时永不崩溃
- 永不阻塞主线程
SDK 必须在 Application.onCreate() 中初始化。
// src/main/kotlin/com/example/App.kt
class App : Application() {
private lateinit var sessionObserver: AndroidSessionObserver
override fun onCreate() {
super.onCreate()
sessionObserver = AndroidSessionObserver(
context = this,
timeoutMs = 30_000L,
callback = object : SessionTracker.Callback {
override fun onSessionStart(session: Session) {
// analytics / ads init
}
override fun onSessionEnd(
session: Session,
reason: ExitReason
) {
// flush analytics / revenue sync
}
}
)
ProcessLifecycleOwner.get()
.lifecycle
.addObserver(sessionObserver)
}
}
关键点
- 不使用 Activity 生命周期
- 不引用 UI
- 如果应用被杀死,回调可能永远不会触发(SDK 已基于此假设进行设计)
相反,SDK:
- 在后台持久化会话状态(时间戳 + 会话 ID)
- 在下次启动时恢复之前的状态,推断会话已结束,并发出
ExitReason.PROCESS_KILLED_INFERRED。
退出原因示例
USER_BACKGROUND_TIMEOUT→ 基于时间的证据PROCESS_KILLED_INFERRED→ 缺少恢复推断
分析和后端系统必须将这些视为推断数据。SDK 并未隐藏此信息。
可测试的逻辑
All session logic:
- 与框架无关
- 注入时间
- 完全可测试
// src/test/kotlin/com/example/SessionTrackerTest.kt
@Test
fun `session ends after timeout`() {
val fakeClock = FakeClock()
val tracker = SessionTracker(fakeClock, timeoutMs = 30_000)
tracker.onForeground()
fakeClock.advance(31_000)
tracker.onForeground()
assertTrue(tracker.lastExitReason is ExitReason.Timeout)
}
可测试的逻辑产生确定性的行为。
仓库
完整的 SDK(包括纯 SessionTracker 逻辑、AndroidSessionObserver、ExitReason 模型以及单元测试)可在 GitHub 上获取:
🔗 https://github.com/vinhvox/ViO---Android-Session-Tracker
该仓库的设计目标是:
- 阅读起来像文档
- 便于复制粘贴
- 不假装 Android 可被控制
高级工程师的最后寄语
Session tracking on Android is never perfect. Good SDKs:
- Do not hide their limits
- State their constraints clearly
- Model uncertainty explicitly
- Help the business make correct—not just pretty—decisions
Android does not give you absolute truth, but it provides enough signals to infer—if you design correctly.