深入学习intent和任务

深入学习intent和任务:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“深入学习intent和任务”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“掌握任务与返回栈的深层机制,能通过启动模式和Intent标志精确控制Activity的入栈行为。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用Intent合同、解析目标、权限、返回结果和重复副作用独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

为什么需要理解"任务"?

你用过手机 App 的场景:从桌面点开微信 → 进聊天列表 → 点一个联系人 → 进去聊天。现在按返回键,你会逐层退出:回到聊天列表 → 再回到桌面。这个"一层层进去、一层层退出"的东西,就是 Android 的任务返回栈

工厂流水线类比:每个 App 就像一个独立车间,车间里有一叠待处理的工单(Activity 栈)。最上面的工单就是你现在看到的页面。来一个新工单(打开新页面),就压到最上面。处理完(按返回),就把最上面那张抽走,露出下一张。如果没有这套"工单叠放"机制,页面跳转会乱套——你不知道按返回键会回到哪里,页面可能在后台无限堆积占满内存。

这章要解决的核心问题:当多个 App、多个页面交织跳转时,Android 怎么决定哪个页面进哪个栈?你作为开发者怎么精确控制这个行为?

Task 和返回栈是怎么工作的

Task 是什么

不是一个类或对象——它是一种抽象概念,是 Android 用来把一组 Activity 串成一条「用户操作路径」的容器。

每个 Task 内部维护一个返回栈(Back Stack):后进先出(LIFO)。新 Activity 启动时被推到栈顶;用户按返回键时,栈顶 Activity 被弹出销毁,前一个 Activity 重新显示。

Task A 的返回栈:
┌────────────────────┐
│ ActivityC  (栈顶)   │ ← 用户当前看到的
├────────────────────┤
│ ActivityB           │
├────────────────────┤
│ ActivityA  (栈底)   │ ← Task 的根 Activity
└────────────────────┘

就是你控制这个"工单叠放"规则的两把钥匙——一个在清单文件中声明(静态),一个在代码中设置(动态)。

四种启动模式

Android 提供了四种启动模式,在 AndroidManifest.xml 中通过 <activity> 标签的 android:launchMode 属性指定:

启动模式行为典型场景
standard(默认)每次启动都创建新实例,压入启动它的 Task 的栈顶绝大多数普通页面
singleTop如果要启动的 Activity 已在栈顶,则不创建新实例,直接复用(回调 onNewIntent);否则行为同 standard搜索页面(避免重复打开)
singleTask系统中只保留一个实例。启动时清空它上面的所有 Activity,让它露出来成为栈顶。若不存在则创建App 主页面(MainActivity)
singleInstance跟 singleTask 类似,但更极端——这个 Activity 独占一个 Task,该 Task 中只有它一个来电界面、闹钟响铃界面
同一个动作 ——「当前栈已有 A,再次启动 A」—— 四种 launchMode 各给出什么结果新建实例复用既有standard再压一个新 AAA新实例▲ 栈底绝大多数普通页面留在原 Task(同 affinity)singleTop栈顶 A 复用AonNewIntent▲ 栈底搜索页:避免重复打开不在栈顶则同 standard 会新建singleTask清栈复用 AAclearTop▲ 栈底App 主入口 / 通知点入同 affinity Task 内全局唯一singleInstance独占一个 Task独立 TaskA独占▲ 栈底来电 / 闹钟响铃界面独立 affinity → 自成一个 Task
四种 launchMode 面对同一个动作「栈里已有 A,再启动 A」给出四种结果: standard 再压一个新 A(紫=新建)、singleTop 栈顶复用、 singleTask 清栈复用、singleInstance 独占一个 Task(绿=复用既有)。 是否新建还要看 taskAffinity 把它分进了哪个 Task。

动手看:四种启动模式的行为区别

猜一猜:从 MainActivity(singleTask) 启动 DetailActivity(standard),再从 DetailActivity 启动 MainActivity——返回栈里会有几个 MainActivity 实例?切到第 3 步看答案。

分步1 / 4

① standard:每次都新建

假设当前栈是 [Main → Detail]。从 Detail 再次启动 Detail(standard):系统创建一个新的 Detail 实例,栈变成 [Main → Detail → Detail]。按两次返回才能回到 Main。这是最常见也最容易导致栈无限膨胀的模式。

代码逐段拆解:控制 Activity 的入栈

在 Manifest 中声明启动模式

<!-- AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

这段代码把 MainActivity 设为 singleTask——意味着无论在 App 里的哪个页面,只要再次启动 MainActivity,它上面的所有 Activity 都会被清理掉。MainActivity 永远是这个 Task 的"根"或"唯一入口"。

用 Intent Flag 动态控制

有时你不想在 Manifest 里写死启动模式,而是在启动那一瞬间根据上下文决定行为。这时用 Intent flag:

// 从任何 Activity 中启动一个"新 Task"里的 Activity
val intent = Intent(this, TargetActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
startActivity(intent)

FLAG_ACTIVITY_NEW_TASK 告诉系统:把 TargetActivity 放到一个新的 Task 里。FLAG_ACTIVITY_CLEAR_TOP 告诉系统:如果目标 Activity 已经存在于当前 Task 的栈中,把它上面的所有 Activity 清掉,然后复用这个实例。这两个 flag 组合起来的效果类似于 singleTask,但更灵活。

taskAffinity:指定 Activity 在哪个 Task 里

<activity
    android:name=".SpecialActivity"
    android:taskAffinity="com.example.special"
    android:launchMode="singleTask" />

决定了 Activity "愿意"进入哪个 Task。默认情况下所有 Activity 的 taskAffinity 等于应用的包名,所以它们都在同一个 Task 里。

但如果你想让某个 Activity 进入一个独立的 Task(比如在最近任务列表里显示为一张单独的卡片),就用 singleTask + 独特的 taskAffinity

// 用 adb 查看当前的任务栈结构
// 在终端执行:
// adb shell dumpsys activity activities | grep -A 5 "Stack #"

容易踩的坑

小结

  • Task 是组织一组相关 Activity 的抽象容器,内部用返回栈(LIFO)管理页面跳转顺序
  • 四种启动模式控制 Activity 实例的复用策略:standard 每次新建、singleTop 栈顶复用、singleTask 全栈唯一、singleInstance 独占 Task
  • Intent flag 可动态覆盖启动模式:NEW_TASK 进入新 Task、CLEAR_TOP 清栈复用、SINGLE_TOP 等价于 singleTop
  • taskAffinity 配合 singleTaskNEW_TASK 使用,决定 Activity 归属哪个 Task
  • 从 Service/BroadcastReceiver 启动 Activity 必须加 FLAG_ACTIVITY_NEW_TASK

练习

问题 1:“第23章 More About Intents and Tasks”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

Task(任务)

把一组相关的 Activity 串成一条「用户操作路径」的容器。你可以把它理解成一叠工单——最上面的工单是你当前看到的页面,按返回键就把最上面那张抽走。它是 Android 对"应用"这一用户概念的系统级抽象。详见本章"Task 是什么"一节。

启动模式(Launch Mode)

在 AndroidManifest 中声明的一种属性,控制 Activity 实例跟 Task、返回栈怎么互动。有四种:standard(每次都新建)、singleTop(栈顶复用)、singleTask(全栈唯一)、singleInstance(独占一个 Task)。详见本章"四种启动模式"一节。

Intent 标志(Flag)

在代码中通过 intent.setFlags() 设置的属性,可以动态控制 Activity 的启动行为。常见的如 FLAG_ACTIVITY_NEW_TASK(放进新 Task)、FLAG_ACTIVITY_CLEAR_TOP(清栈复用)。代码中设置的 flag 优先级高于 manifest 中声明的 launchMode。详见本章"Intent Flag"一节。

taskAffinity(任务亲和性)

Activity 的一种"归属感"属性,表示它倾向于进入哪个 Task。默认等于应用的包名。配合 singleTask 或 FLAG_ACTIVITY_NEW_TASK 使用时,决定 Activity 被分到哪个 Task。详见本章"taskAffinity"一节。

“深入学习intent和任务”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“深入学习intent和任务”必须回到可观察状态

“深入学习intent和任务”的学习结果不是记住类名,而是能预测“掌握任务与返回栈的深层机制,能通过启动模式和Intent标志精确控制Activity的入栈行为。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

23. More About Intents and Tasks

在“深入学习intent和任务”中,“23. More About Intents and Tasks”跨应用边界时只授予完成任务所需的URI与组件权限,并在日志中移除联系人等个人信息。

Setting Up NerdLauncher

在“深入学习intent和任务”中,验证“Setting Up NerdLauncher”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Resolving an Implicit Intent

在“深入学习intent和任务”中,分析“Resolving an Implicit Intent”要区分Activity返回栈、系统task与进程,flags改变栈行为但不会自动提供业务幂等。

Creating Explicit Intents at Runtime

在“深入学习intent和任务”中,分析“Creating Explicit Intents at Runtime”先冻结设备API、targetSdk和输入,再沿回调与数据流寻找首个状态分叉,不能只描述最终页面。

Tasks and the Back Stack

在“深入学习intent和任务”中,分析“Tasks and the Back Stack”要区分Activity返回栈、系统task与进程,flags改变栈行为但不会自动提供业务幂等。

Using NerdLauncher as a Home Screen

在“深入学习intent和任务”中,“Using NerdLauncher as a Home Screen”跨应用边界时只授予完成任务所需的URI与组件权限,并在日志中移除联系人等个人信息。

For the More Curious: Processes vs Tasks

在“深入学习intent和任务”中,分析“For the More Curious: Processes vs Tasks”要区分Activity返回栈、系统task与进程,flags改变栈行为但不会自动提供业务幂等。

For the More Curious: Concurrent Documents

在“深入学习intent和任务”中,“For the More Curious: Concurrent Documents”用于推翻“掌握任务与返回栈的深层机制,能通过启动模式和Intent标志精确控制Activity的入栈行为。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

Challenge: Icons

在“深入学习intent和任务”中,分析“Challenge: Icons”要追踪资源ID、限定目录、密度缩放与属性解析,避免把预览器结果当成所有设备的合同。

“深入学习intent和任务”验收回顾

“深入学习intent和任务”只有在Intent合同、解析目标、权限、返回结果和重复副作用能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:XML drawable · 下一页:HTTP与后台任务 →

章专属可重放状态实验

先预测“从 NerdLauncher 启动、重复深链、Home 返回与 Back 导航”发生后,ActivityTaskManager 与每个 task 返回栈应怎样改变intent、task ID、Activity 实例、flags 和顶层目的地;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

深入学习intent和任务:状态合同

区分 Activity back stack、task、document 与进程,并验证 launch flags

验证场景

第四版正式目录节点

more-intents-tasks · 正常任务

23. More About Intents and Tasks固定 SDK、设备配置和初始状态,触发“从 NerdLauncher 启动、重复深链、Home 返回与 Back 导航”

状态所有者ActivityTaskManager 与每个 task 返回栈
受控状态intent、task ID、Activity 实例、flags 和顶层目的地
触发事件从 NerdLauncher 启动、重复深链、Home 返回与 Back 导航

冻结入口:23. More About Intents and Tasks

记录ActivityTaskManager 与每个 task 返回栈的初始intent、task ID、Activity 实例、flags 和顶层目的地

观察:task dump、intent flags、实例 ID、返回序列和业务提交计数中的“23. More About Intents and Tasks”轨迹

预期:由ActivityTaskManager 与每个 task 返回栈提交intent、task ID、Activity 实例、flags 和顶层目的地,并持续满足“栈策略决定导航实例但不自动提供业务副作用幂等”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“从 NerdLauncher 启动、重复深链、Home 返回与 Back 导航”。每一步只允许ActivityTaskManager 与每个 task 返回栈按职责提交状态,并持续核对“栈策略决定导航实例但不自动提供业务副作用幂等”。

Deterministic event replay

深入学习intent和任务:事件轨迹

选择一次状态转换1 / 5

不变量:栈策略决定导航实例但不自动提供业务副作用幂等

交付证据:task dump、intent flags、实例 ID、返回序列和业务提交计数

实验三:章专属反例与同输入恢复

注入“滥用 CLEAR_TOP 修复重复界面,却让待保存编辑状态丢失”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有task dump、intent flags、实例 ID、返回序列和业务提交计数一起恢复才算修复。

Fault · cancel · restore

深入学习intent和任务:反例与恢复

故障:滥用 CLEAR_TOP 修复重复界面,却让待保存编辑状态丢失

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“滥用 CLEAR_TOP 修复重复界面,却让待保存编辑状态丢失”

3. 检查所有者一致

栈策略决定导航实例但不自动提供业务副作用幂等

4. 核对结果一致

task dump、intent flags、实例 ID、返回序列和业务提交计数

讨论

评论区加载中…