Android 会话跟踪 — 高级工程师视角:当操作系统对你一无所承诺时

发布: (2025年12月30日 GMT+8 23:09)
6 min read
原文: Dev.to

Source: Dev.to

介绍

Android 并不保证在用户离开你的应用时会通知你。
本文并非讨论技巧,而是关于在 Android 上正确地进行会话跟踪——无论是在生产环境还是 SDK 层面。

大多数 Android 开发者曾在某个时刻相信以下至少一种说法:

  • onStop() 表示用户已离开
  • onDestroy() 表示应用已结束
  • 从最近任务列表滑动退出即结束会话

这些说法根本都是错误的。Android 并不会为你运行应用;你的应用只是一个客体。进程可能在没有回调的情况下被杀死(SIGKILL / LMK),且不提供任何清理保证。生命周期事件仅在进程存活期间存在。

含义

如果你在等待回调来检测会话结束,诸如 Firebase、AdMob、Adjust、AppsFlyer 等服务 并不会 “检测到被杀”。它们的做法是:

  1. 跟踪进程的前台/后台状态
  2. 应用超时启发式规则
  3. 在下次启动时推断会话结束

没有事件,没有保证——只有概率模型。这是一种高级思维:接受不确定性并围绕它进行设计。

会话生命周期

真实的会话具有以下状态:

  • Created(已创建)
  • Active(活跃)
  • Background(后台)
  • Expired(已过期)
  • Restored(已恢复,推断得出)

会话不会在应用被杀死时结束;我们会根据以下因素判断其是否仍然有效:

  • 后台停留时长
  • 下一次启动的时间点
  • 持久化的元数据

ProcessLifecycleOwner 提供:

  • ON_START:进程进入前台
  • ON_STOP:进程进入后台

会提供:

  • 应用被杀死
  • 从最近任务列表滑除
  • 原生崩溃

而且它本不该提供这些。高级工程师不会要求 API 实现不可能的功能。

超时作为业务规则

超时不是一种 hack;它是一条业务规则:

“如果用户离开超过 X ms,视为会话结束。”

具体数值(30 秒、1 分钟、5 分钟……)取决于产品需求——没有统一的正确值。

当应用被杀死时:

  • 没有 onSessionEnd 回调
  • 没有清理,也没有刷新

剩余的内容:

  • 最后一次后台时间戳
  • 会话 ID
  • 推断的原因

在下次启动时,SDK 必须:

  1. 加载持久化状态
  2. 推断上一次会话已结束
  3. 发出逻辑上的会话结束

这种两阶段设计为资深工程师所熟悉。

建模退出原因

  • USER_BACKGROUND_TIMEOUT
  • PROCESS_KILLED_INFERRED
  • APP_UPGRADE
  • CRASH_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:

  1. 在后台持久化会话状态(时间戳 + 会话 ID)
  2. 在下次启动时恢复之前的状态,推断会话已结束,并发出 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 逻辑、AndroidSessionObserverExitReason 模型以及单元测试)可在 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.

Back to Blog

相关文章

阅读更多 »