卷 I · CH 03 · 深度 03/18

四大组件不是四种页面

它们是操作系统进入你进程的四扇门;后台执行题的核心,是谁承诺这项工作必须活多久。

COMPONENTSWORKMANAGERINTENT
▷ 先答一下

用户按发送后立刻把应用划走。这条医疗消息必须最终上传,允许等网络恢复。首选什么?

  1. viewModelScope.launch
  2. 普通后台 Service
  3. 带网络约束的唯一 WorkManager 工作
  4. 精确 AlarmManager

组件是系统合同

Activity 是有界面、能接收 Intent 的交互入口。Service 不是线程,而是告诉系统「这项没有页面的工作仍有用户可感知价值」的组件;代码默认仍跑主线程。BroadcastReceiver 接受短促事件,回调必须尽快返回。ContentProvider 用 URI 与权限暴露结构化数据,是跨进程数据合同。

Compose 改了 UI 写法,没有改这四种系统合同。你的单 Activity 应用仍靠 Manifest、Intent、任务栈、进程优先级与权限模型活着。

可以带走的判断:组件决定系统如何找到你,线程与协程决定找到你之后代码在哪里跑;两者不能互相替代。

后台工作先问三个问题

  1. 离开页面后还要不要继续?不要:页面或 ViewModel 的协程。
  2. 进程被杀后还要不要完成?要:可持久调度的 WorkManager。
  3. 是否必须立刻、持续、且用户可感知?是:满足类型限制的前台服务,并展示通知。

WorkManager 适合可靠、可延迟、带约束的工作,例如发待同步消息、上传已经落到私有临时目录的附件。它会把调度意图持久化,尊重 Doze 与系统限制,并提供退避重试。它不保证某个精确时刻开始,也不是常驻进程。

需求工具关键理由
页面可见时搜索协程页面消失即可取消
离线消息最终发送WorkManager跨进程、约束、重试
用户正在录音前台服务立刻持续,用户必须知情
日历在准确时刻提醒AlarmManager真正的时间语义
推送到达FCM 接收入口由系统唤起,处理应短

Intent 是能力请求

显式 Intent 指定你自己的组件;隐式 Intent 描述「我想完成什么」,由系统匹配能力。外部输入一律不可信:深链参数、分享 URI、通知 PendingIntent 都要验证。导出组件必须明确 android:exported,敏感能力要加权限或在入口完成身份校验。

医疗图片尤其要避免裸文件路径与广泛存储权限。用系统 Photo Picker 或受限相机流程获得最小访问,用 content:// URI 与临时授权分享,不把临床照片写进公共相册。

⌨ 自己跑一遍

为「待发消息」排一个唯一同步任务。连续点重试时,结果不是创建许多上传,而是同名工作保持唯一:

workName=sync-message-c-1
constraint=CONNECTED
policy=KEEP
result=at most one active chain
fun enqueueMessageSync(context: Context, clientId: String) {
    val request = OneTimeWorkRequestBuilder<SendMessageWorker>()
        .setInputData(workDataOf("clientId" to clientId))
        .setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        )
        .setBackoffCriteria(
            BackoffPolicy.EXPONENTIAL,
            10,
            TimeUnit.SECONDS
        )
        .build()

    WorkManager.getInstance(context).enqueueUniqueWork(
        "sync-message-$clientId",
        ExistingWorkPolicy.KEEP,
        request
    )
}

在 Android Studio 的最小应用里运行,先开飞行模式排队,再恢复网络。用 WorkManager Inspector 确认状态从 ENQUEUED 进入 RUNNINGSUCCEEDED

▸ 在现实里

用户投诉「消息顺序乱」时,根因未必在 RecyclerView 或 Compose。多个 Worker 并发、每次重试生成新 ID、服务端按到达时间排序,都可能把 UI 正确渲染的错误事实送回来。后台调度只负责「再试」,顺序与幂等要由数据协议保证。

✗ 这个直觉是错的

「Service 会自动开后台线程,而且比协程可靠。」Service 默认在主线程;普通后台 Service 还受严格启动限制。

把「执行位置」「组件寿命」「持久承诺」分开回答。协程处理进程内并发,前台服务表达用户可感知的持续工作,WorkManager 表达跨进程的最终完成。

◇ 面试收口

答案是第 3 项。任务允许延迟、需要网络、必须跨进程最终完成,正是 WorkManager 的合同。viewModelScope 随 ViewModel 结束,普通 Service 不等于可靠队列,AlarmManager 只该用于真正精确的闹钟语义。

这一章的一句话

先说这项工作必须活多久、是否立刻、用户是否知情,再说 API 名字。

下一章回到 Kotlin:同一段 Java 风格代码改成 Kotlin 后,真正该少掉的不是字符,而是三个本来能进入运行时的非法分支。