fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
# 阶段③·模块A 第1课:AOSP构建系统概述 —— Soong、Blueprint与Ninja的关系
|
||||
|
||||
> **阅读时间**:约10分钟
|
||||
> **前置知识**:Linux基础、C语言基础、了解AOSP源码目录结构
|
||||
> **学习目标**:理解AOSP构建系统的三层架构,能说出Soong/Blueprint/Ninja各自的角色
|
||||
|
||||
---
|
||||
|
||||
## 🎯 本课目标
|
||||
|
||||
学完本课后,你将能够:
|
||||
|
||||
| 序号 | 能力 |
|
||||
|:---:|:-----|
|
||||
| 1 | 说出AOSP构建系统经历了哪三个阶段(Make → Soong → ?) |
|
||||
| 2 | 画出Soong/Blueprint/Ninja三层调用关系图 |
|
||||
| 3 | 解释 `Android.bp` 和 `Android.mk` 的本质区别 |
|
||||
| 4 | 在源码树中找到构建系统的核心入口文件 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么需要专门的构建系统?
|
||||
|
||||
假设你有 **4000+ 个模块**,每个模块有自己的源码、依赖、编译选项。如果手动逐个编译:
|
||||
|
||||
```
|
||||
$ gcc -IlibA/include -IlibB/include -c my_module.c
|
||||
$ gcc my_module.o libA.a libB.so -o my_binary
|
||||
```
|
||||
|
||||
模块一多,噩梦就来了:
|
||||
|
||||
```
|
||||
libA ──→ libC ──→ libF
|
||||
↘ ↘ ↗
|
||||
my_app ──→ libB ──→ libD ─┘
|
||||
↘
|
||||
libE
|
||||
...4000个模块互相交织
|
||||
```
|
||||
|
||||
**构建系统的核心使命**:自动管理依赖关系、并行编译、增量构建、产物管理。
|
||||
|
||||
---
|
||||
|
||||
## 2. AOSP构建系统的三代演进
|
||||
|
||||
```
|
||||
2008 2016 2020+
|
||||
| | |
|
||||
Android.mk Android.bp ???
|
||||
(GNU Make) (Blueprint+Soong) (Bazel?)
|
||||
| | |
|
||||
基于Makefile 像JSON的声明式 Google内部趋势
|
||||
大量宏函数 语法,自动转 (但目前Soong
|
||||
难以维护 Ninja构建文件 仍是主流)
|
||||
```
|
||||
|
||||
### 第一代:Make(2008-2016)
|
||||
|
||||
```
|
||||
[Android.mk] ──→ [GNU Make] ──→ [编译产物]
|
||||
(人写) (解析执行) (out/target/...)
|
||||
```
|
||||
|
||||
- 本质上是 Makefile,用大量 GNU Make 宏函数封装
|
||||
- 典型写法:`include $(BUILD_SHARED_LIBRARY)` 背后展开为几百行Make规则
|
||||
- **痛点**:语法晦涩,错误信息可读性差,扩展困难
|
||||
|
||||
### 第二代:Soong + Blueprint + Ninja(2016-至今)
|
||||
|
||||
```
|
||||
[Android.bp] ──→ [Blueprint] ──→ [Soong] ──→ [build.ninja] ──→ [Ninja] ──→ [编译产物]
|
||||
声明式描述 语法解析+ 模块规则 Ninja构建文件 高速并行
|
||||
依赖分析 生成引擎 执行器
|
||||
```
|
||||
|
||||
> 🔑 **关键转变**:从「告诉系统怎么做」变成「告诉系统我要什么」
|
||||
|
||||
### 第三代:Bazel(探索中)
|
||||
|
||||
Google内部大规模使用Bazel(Blaze的开源版),AOSP也在逐步试验。但目前(Android 14/15)Soong仍是绝对主流。
|
||||
|
||||
---
|
||||
|
||||
## 3. 三层架构深度解析
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 开发者编写 │
|
||||
│ Android.bp / Android.mk (约4000+个文件) │
|
||||
│ "我需要编译 libutils.so,依赖 liblog" │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Blueprint 层 │
|
||||
│ │
|
||||
│ · 解析 Android.bp 语法(类JSON) │
|
||||
│ · 构建模块依赖图(DAG) │
|
||||
│ · 生成中间表示(IR)传给 Soong │
|
||||
│ │
|
||||
│ 位置: build/blueprint/ │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Soong 层 │
|
||||
│ │
|
||||
│ · 用 Go 语言编写(替代 Make 的宏函数) │
|
||||
│ · 每个模块类型对应一个 Go 结构体 │
|
||||
│ · 生成实际的编译规则 │
|
||||
│ │
|
||||
│ 核心模块类型: │
|
||||
│ cc_library, cc_binary, java_library, │
|
||||
│ android_app, prebuilt_binary, ... │
|
||||
│ │
|
||||
│ 位置: build/soong/ │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Ninja 层 │
|
||||
│ │
|
||||
│ · 产物: out/combined-<product>.ninja │
|
||||
│ · 精简化 Make,只处理文件级依赖 │
|
||||
│ · 极快:AOSP全量编译生成约150万行 .ninja 文件 │
|
||||
│ · 并行调度:自动利用多核CPU │
|
||||
│ │
|
||||
│ 特点: 无变量、无函数、无条件,纯粹的文件依赖图 │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
[编译产物]
|
||||
out/target/product/<device>/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Android.bp vs Android.mk —— 新旧对比
|
||||
|
||||
### Android.mk(旧,Make风格)
|
||||
|
||||
```makefile
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := libutils
|
||||
LOCAL_SRC_FILES := \
|
||||
String8.cpp \
|
||||
Unicode.cpp \
|
||||
VectorImpl.cpp
|
||||
LOCAL_SHARED_LIBRARIES := liblog libcutils
|
||||
LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
```
|
||||
|
||||
### Android.bp(新,声明式)
|
||||
|
||||
```json
|
||||
cc_library_shared {
|
||||
name: "libutils",
|
||||
srcs: [
|
||||
"String8.cpp",
|
||||
"Unicode.cpp",
|
||||
"VectorImpl.cpp",
|
||||
],
|
||||
shared_libs: [
|
||||
"liblog",
|
||||
"libcutils",
|
||||
],
|
||||
export_include_dirs: ["include"],
|
||||
}
|
||||
```
|
||||
|
||||
> **一眼就能看出区别**:`Android.bp` 不需要 `CLEAR_VARS`、不需要 `include` 宏展开、不需要关心变量作用域。它就是纯粹的数据描述。
|
||||
|
||||
### 关键差异表
|
||||
|
||||
| 维度 | Android.mk | Android.bp |
|
||||
|:-----|:-----------|:-----------|
|
||||
| 语言 | GNU Make | 类JSON声明式 |
|
||||
| 变量作用域 | 全局污染,需手动清理 | 模块级隔离 |
|
||||
| 条件编译 | Make ifeq/ifneq | soong_config_module_type |
|
||||
| 类型安全 | 无,运行时报错 | 解析阶段即可发现语法错误 |
|
||||
| 可读性 | 差(宏嵌套) | 好(结构化) |
|
||||
| 迁移 | — | 可用 `androidmk` 工具自动转换 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 源码位置速查
|
||||
|
||||
```
|
||||
AOSP源码根目录/
|
||||
│
|
||||
├── build/
|
||||
│ ├── soong/ ← Soong(Go语言写的构建引擎)
|
||||
│ │ ├── cmd/
|
||||
│ │ │ └── soong_ui/ ← 用户入口 soong_ui
|
||||
│ │ ├── android/ ← Android.bp 模块类型定义
|
||||
│ │ └── cc/ ← C/C++ 模块规则
|
||||
│ │
|
||||
│ ├── blueprint/ ← Blueprint(语法解析器)
|
||||
│ └── make/ ← 旧 Make 系统(兼容层)
|
||||
│
|
||||
├── out/
|
||||
│ ├── soong/
|
||||
│ │ └── build.ninja ← 中间 Ninja 文件
|
||||
│ └── combined-<product>.ninja ← 最终 Ninja 构建文件
|
||||
│
|
||||
└── <模块目录>/
|
||||
└── Android.bp ← 每个模块的构建描述文件
|
||||
```
|
||||
|
||||
> 📌 **记忆口诀**:`build/soong` 是引擎,`build/blueprint` 是语法,`Android.bp` 是原料,`out/*.ninja` 是施工图,Ninja 是施工队。
|
||||
|
||||
---
|
||||
|
||||
## 6. 一次构建的完整调用链
|
||||
|
||||
当你在AOSP根目录执行 `m libutils`:
|
||||
|
||||
```
|
||||
Step 1: soong_ui (build/soong/cmd/soong_ui/)
|
||||
│ 用户入口脚本,收集参数
|
||||
▼
|
||||
Step 2: 扫描所有 Android.bp 文件
|
||||
│ Blueprint 解析语法,构建模块名→定义的映射表
|
||||
▼
|
||||
Step 3: Soong 处理 "libutils" 这个模块
|
||||
│ 找到它的模块类型(cc_library_shared)
|
||||
│ 调用对应的 Go 代码生成编译规则
|
||||
▼
|
||||
Step 4: 生成 Ninja 文件片段
|
||||
│ rule cc_compile { command = clang ... }
|
||||
│ build String8.o: cc_compile String8.cpp
|
||||
▼
|
||||
Step 5: 合并所有 Ninja 片段
|
||||
│ → out/combined-<product>.ninja
|
||||
▼
|
||||
Step 6: Ninja 执行
|
||||
│ 读取 .ninja 文件
|
||||
│ 检查依赖 → 只编译改动过的文件(增量)
|
||||
│ 并行调度 → 充分利用CPU
|
||||
▼
|
||||
Step 7: 产物输出
|
||||
out/target/product/<device>/system/lib64/libutils.so
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 💡 AOSP场景加油站
|
||||
|
||||
### 场景1:你修改了 `frameworks/native/libs/binder/Binder.cpp`
|
||||
|
||||
```bash
|
||||
# 你只需要执行:
|
||||
$ m libbinder
|
||||
|
||||
# Ninja 会:
|
||||
# ✓ 检测到 Binder.cpp 的 mtime 变化
|
||||
# ✓ 重新编译 Binder.o
|
||||
# ✓ 重新链接 libbinder.so
|
||||
# ✓ 跳过所有未改动的依赖(可能节省几十分钟)
|
||||
```
|
||||
|
||||
### 场景2:你想知道 `libutils` 被哪些模块依赖
|
||||
|
||||
```bash
|
||||
# 检查反向依赖
|
||||
$ grep -r "libutils" --include="Android.bp" frameworks/ system/
|
||||
|
||||
# 或者用 Soong 自带的分析工具
|
||||
$ build/soong/soong_ui.bash --dumpvar-mode --all-modules | grep libutils
|
||||
```
|
||||
|
||||
### 场景3:旧 mk 转新 bp
|
||||
|
||||
```bash
|
||||
# AOSP 内置转换工具
|
||||
$ androidmk Android.mk > Android.bp
|
||||
# 大多数简单模块能一键转换,复杂宏可能需要手动调整
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 常见误区澄清
|
||||
|
||||
| 误区 | 真相 |
|
||||
|:-----|:-----|
|
||||
| ❌ "Soong 替代了 Ninja" | ✅ Soong 生成 Ninja 文件,Ninja 负责执行 |
|
||||
| ❌ "Android.bp 是 YAML/JSON" | ✅ 是类JSON的自定义语法,不是标准JSON |
|
||||
| ❌ "Android.mk 已经废弃" | ✅ 仍可混用,但新模块应使用 Android.bp |
|
||||
| ❌ "Blueprint 是 Google 发明的" | ✅ 基于 Google 内部 Blueprint 框架,专为 AOSP 定制 |
|
||||
| ❌ "m 命令直接用 Make" | ✅ `m` 最终调用的是 `soong_ui`,Make 只是兼容层 |
|
||||
|
||||
---
|
||||
|
||||
## 📝 本课检查清单
|
||||
|
||||
- [ ] 能画出 Soong → Blueprint → Ninja 的三层调用图
|
||||
- [ ] 理解 Android.bp 声明式语法 vs Android.mk 宏式语法的区别
|
||||
- [ ] 知道 `build/soong/` 和 `build/blueprint/` 的职责分工
|
||||
- [ ] 能说出 `m <module>` 命令背后发生了什么
|
||||
- [ ] 在源码中找到至少 3 个不同模块的 Android.bp 文件
|
||||
|
||||
---
|
||||
|
||||
## 🧪 动手练习
|
||||
|
||||
1. **浏览构建系统源码**:
|
||||
```bash
|
||||
cd $AOSP_ROOT
|
||||
ls build/soong/android/ # 查看所有模块类型定义
|
||||
ls build/blueprint/ # 查看 Blueprint 解析器
|
||||
```
|
||||
|
||||
2. **找找身边的 Android.bp**:
|
||||
```bash
|
||||
find frameworks/native -name "Android.bp" | head -5
|
||||
cat frameworks/native/libs/binder/Android.bp # 选一个阅读
|
||||
```
|
||||
|
||||
3. **尝试 androidmk 转换**(如果你有旧的 Android.mk):
|
||||
```bash
|
||||
androidmk old_Android.mk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
> **下节课预告**:第2课《Android.bp 文件语法详解》—— 深入模块类型、变量作用域、条件编译等核心语法。
|
||||
|
||||
---
|
||||
|
||||
*本课完成时间:约10分钟 | 属于 AOSP 8阶段学习路线 · 阶段③ 模块A*
|
||||
@@ -0,0 +1,630 @@
|
||||
# 阶段⑤ 模块A 第1课:AMS 进程管理 —— 从 Zygote fork 到 OOM Adj 全链路
|
||||
|
||||
> **课程定位**:阶段⑤ System Server 核心服务深度解析 · 模块A AMS深度 第1课
|
||||
> **难度等级**:⭐⭐⭐⭐⭐(深度源码级)
|
||||
> **预计阅读**:25~30 分钟
|
||||
> **前置课程**:阶段③模块B第8课(Zygote与SystemServer)、阶段③模块C第12课(AMS入门)
|
||||
> **源码基准**:AOSP 14.0.0_r50(兼容 Android 13/15 核心逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 课程目标
|
||||
|
||||
学完本课后,你将能够:
|
||||
|
||||
| 序号 | 能力目标 | 检验方式 |
|
||||
|:---:|:---------|:--------|
|
||||
| ① | 画出「点击图标→进程创建→应用启动」的完整时序图 | 默写8步链路 |
|
||||
| ② | 解释 ProcessRecord 中 6 个关键字段的含义与作用 | 源码标注 |
|
||||
| ③ | 手动计算任意进程的 oom_score_adj,并说明依据 | 终端实操 |
|
||||
| ④ | 解释 lmkd 的五级杀进程策略与阈值配置 | 画表对比 |
|
||||
| ⑤ | 使用 `dumpsys activity processes` 诊断进程状态 | 实战输出 |
|
||||
|
||||
---
|
||||
|
||||
## 📐 前置知识检查
|
||||
|
||||
在进入本课前,确认你已经掌握:
|
||||
|
||||
- [x] Zygote 的 socket 通信机制(`/dev/socket/zygote`)
|
||||
- [x] Binder IPC 基本原理(进程间调用链路)
|
||||
- [x] Linux `/proc` 文件系统基础(`/proc/[pid]/oom_score_adj`)
|
||||
- [x] AMS 的启动流程(SystemServer → startBootstrapServices)
|
||||
- [ ] ⚠️ 如果不熟悉以上内容,请先复习阶段③模块B/C 对应课程
|
||||
|
||||
---
|
||||
|
||||
## 🧱 一、整体架构:进程创建全链路概览
|
||||
|
||||
Android 应用进程的诞生,是一条跨越 **4 个进程、3 种 IPC 机制** 的精密流水线:
|
||||
|
||||
```
|
||||
用户点击图标
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ① Launcher (App进程) │
|
||||
│ startActivity(intent) │
|
||||
│ └─ Binder IPC ──────────────────────────────────┐ │
|
||||
└─────────────────────────────────────────────────────│─────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ② system_server (SystemServer进程) │
|
||||
│ ActivityManagerService.startActivity() │
|
||||
│ │ │
|
||||
│ ├─ 解析 Intent / 权限检查 / 栈管理 │
|
||||
│ │ │
|
||||
│ └─ ProcessList.startProcessLocked() │
|
||||
│ │ │
|
||||
│ ├─ 构造 ProcessRecord 对象 │
|
||||
│ ├─ 计算 oom_adj (默认 HOME_APP_ADJ=6) │
|
||||
│ └─ ZygoteProcess.start() │
|
||||
│ └─ Socket IPC ────────────────────┐ │
|
||||
└───────────────────────────────────────────────────│───────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ③ Zygote (Zygote进程,PID=1的子进程) │
|
||||
│ ZygoteServer.runSelectLoop() 收到命令 │
|
||||
│ │ │
|
||||
│ ├─ ZygoteArguments 解析 (uid/gid/类名/参数) │
|
||||
│ ├─ Zygote.forkAndSpecialize() │
|
||||
│ │ └─ nativeForkAndSpecialize() │
|
||||
│ │ └─ fork() 系统调用 ★ 此刻新进程诞生 │
|
||||
│ │ │
|
||||
│ └─ specializeAppProcess() │
|
||||
│ ├─ setgroups/setuid/setgid (设置进程身份) │
|
||||
│ ├─ mount 命名空间隔离 │
|
||||
│ └─ RuntimeInit.applicationInit() │
|
||||
│ └─ ActivityThread.main() ◄────────┐ │
|
||||
└───────────────────────────────────────────────────│───────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ ④ 新应用进程 (com.example.app) │
|
||||
│ ActivityThread.main() │
|
||||
│ │ │
|
||||
│ ├─ Looper.prepareMainLooper() │
|
||||
│ ├─ ActivityThread.attach(false, startSeq) │
|
||||
│ │ └─ Binder IPC ──► AMS.attachApplication() │
|
||||
│ │ │
|
||||
│ └─ Looper.loop() (进入消息循环) │
|
||||
│ │
|
||||
│ AMS 回调链路: │
|
||||
│ AMS.attachApplication() │
|
||||
│ ├─ ApplicationThread.bindApplication() │
|
||||
│ │ └─ makeApplication() → onCreate() │
|
||||
│ └─ realStartActivityLocked() │
|
||||
│ └─ Activity.onCreate() ★ UI 显示 │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
> **关键观察**:从 `fork()` 到 `Activity.onCreate()`,一条调用链跨了 **8 个主要步骤**,涉及 **Socket + Binder** 两种 IPC,中间任意一环出问题都会导致 ANR 或启动崩溃。
|
||||
|
||||
### 1.1 三种 IPC 在进程创建中的角色
|
||||
|
||||
| IPC 类型 | 使用场景 | 传输内容 | 性能特点 |
|
||||
|:---------|:--------|:--------|:--------|
|
||||
| **Binder** | Launcher→AMS、App→AMS | Intent、Binder 对象 | 同步调用,有返回值 |
|
||||
| **Socket** | AMS→Zygote | fork 命令、args 字符串 | 单向命令,无需返回 |
|
||||
| **fork()** | Zygote→子进程 | 整个进程空间(CoW) | 内核级,极快 |
|
||||
|
||||
---
|
||||
|
||||
## 🔬 二、ProcessRecord 数据结构深度解析
|
||||
|
||||
`ProcessRecord` 是 AMS 中描述一个进程的"档案袋",位于:
|
||||
|
||||
```
|
||||
frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java
|
||||
```
|
||||
|
||||
### 2.1 核心字段分类(6大类)
|
||||
|
||||
```
|
||||
ProcessRecord
|
||||
│
|
||||
├─ 📛 [身份标识]
|
||||
│ ├─ pid : int ← Linux 进程 ID
|
||||
│ ├─ uid : int ← Android 用户 ID
|
||||
│ ├─ processName : String ← 如 "com.android.phone"
|
||||
│ ├─ userId : int ← 多用户场景的用户 ID
|
||||
│ └─ info : ApplicationInfo ← 从 PM 查询的 App 信息
|
||||
│
|
||||
├─ 📊 [状态标志]
|
||||
│ ├─ curAdj : int ← 当前 OOM Adj 值 ★核心★
|
||||
│ ├─ setAdj : int ← 被设定的目标 Adj
|
||||
│ ├─ curSchedGroup : int ← 调度组(前台/后台)
|
||||
│ ├─ adjType : String ← Adj 来源类型(如 "activity")
|
||||
│ ├─ adjSource : Object ← Adj 来源对象
|
||||
│ └─ pendingUiClean: boolean ← 是否有待清理 UI
|
||||
│
|
||||
├─ 📞 [Binder 通信]
|
||||
│ ├─ thread : IApplicationThread ← App 端 Binder 句柄
|
||||
│ ├─ procLock : Object ← 进程级锁
|
||||
│ └─ renderThreadTid: int ← 渲染线程 TID
|
||||
│
|
||||
├─ 📦 [组件容器]
|
||||
│ ├─ activities : ArrayList<ActivityRecord>
|
||||
│ ├─ services : ArraySet<ServiceRecord>
|
||||
│ ├─ providers : ArraySet<ContentProviderRecord>
|
||||
│ ├─ receivers : ArraySet<BroadcastFilter>
|
||||
│ └─ pkgList : ArraySet<String> ← 该进程加载的包名
|
||||
│
|
||||
├─ ⏱️ [时间戳]
|
||||
│ ├─ startTime : long ← 进程创建时间
|
||||
│ ├─ lastActivityTime : long ← 最后活跃时间
|
||||
│ ├─ lastPss : long ← 最近一次 PSS 内存值
|
||||
│ └─ whenUnimportant : long ← 变为非关键进程的时刻
|
||||
│
|
||||
└─ 🧠 [内存与状态]
|
||||
├─ maxAdj : int ← 该进程允许的最大 Adj
|
||||
├─ curProcState : int ← 当前进程状态(PROCESS_STATE_*)
|
||||
├─ setProcState : int ← 目标进程状态
|
||||
└─ mProfile : ProfilerInfo ← 性能分析配置
|
||||
```
|
||||
|
||||
### 2.2 curAdj 与 setAdj 的"双值设计"
|
||||
|
||||
这是理解进程管理的关键设计模式:
|
||||
|
||||
```java
|
||||
// ProcessRecord.java (简化逻辑)
|
||||
int curAdj; // ★ 内核中 /proc/[pid]/oom_score_adj 的当前值
|
||||
int setAdj; // ★ AMS 期望设定的值(尚未同步到内核)
|
||||
|
||||
// 同步发生在:
|
||||
void applyOomAdjLocked() {
|
||||
if (curAdj != setAdj) {
|
||||
// 通过 lmkd socket 或直接写 /proc 同步
|
||||
PlatformState.setOomAdj(pid, uid, setAdj);
|
||||
curAdj = setAdj; // 同步完成
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **设计精髓**:`setAdj` 是"意图",`curAdj` 是"现实"。AMS 可以在一个事务中多次修改 `setAdj`,最后只做一次内核同步,大幅减少系统调用。
|
||||
|
||||
### 2.3 进程状态枚举 (PROCESS_STATE_*)
|
||||
|
||||
与 `curAdj` 对应的是 `curProcState`,它比 Adj 更细粒度:
|
||||
|
||||
| 常量名 | 值 | 含义 | 对应 Adj |
|
||||
|:-------|:---|:-----|:--------|
|
||||
| `PROCESS_STATE_PERSISTENT` | -3 | 系统持久进程(永不杀) | -800 |
|
||||
| `PROCESS_STATE_PERSISTENT_UI` | -2 | 系统持久UI进程 | -800 |
|
||||
| `PROCESS_STATE_TOP` | 2 | 前台拥有焦点Activity | 0 |
|
||||
| `PROCESS_STATE_BOUND_TOP` | 1 | 绑定到TOP进程的服务 | 50 |
|
||||
| `PROCESS_STATE_FOREGROUND_SERVICE` | 3 | 前台服务 | 200 |
|
||||
| `PROCESS_STATE_BOUND_FOREGROUND_SERVICE` | 4 | 绑定到前台服务 | 250 |
|
||||
| `PROCESS_STATE_IMPORTANT_FOREGROUND` | 5 | 对前台重要的进程 | 300 |
|
||||
| `PROCESS_STATE_IMPORTANT_BACKGROUND` | 6 | 重要后台进程 | 400 |
|
||||
| `PROCESS_STATE_TRANSIENT_BACKGROUND` | 7 | 短暂后台 | 500 |
|
||||
| `PROCESS_STATE_BACKUP` | 8 | 备份进程 | 600 |
|
||||
| `PROCESS_STATE_SERVICE` | 9 | 有Service在运行 | 700 |
|
||||
| `PROCESS_STATE_RECEIVER` | 10 | 正在接收广播 | 700 |
|
||||
| `PROCESS_STATE_TOP_SLEEPING` | 11 | 休眠的TOP | 325 |
|
||||
| `PROCESS_STATE_HEAVY_WEIGHT` | 12 | 重量级进程 | 800 |
|
||||
| `PROCESS_STATE_HOME` | 13 | Launcher进程 | 900 |
|
||||
| `PROCESS_STATE_LAST_ACTIVITY` | 14 | 上一个Activity | 900 |
|
||||
| `PROCESS_STATE_CACHED_ACTIVITY` | 15 | 已缓存的Activity | 906 |
|
||||
| `PROCESS_STATE_CACHED_RECENT` | 16 | 最近使用的缓存 | 906 |
|
||||
| `PROCESS_STATE_CACHED_ACTIVITY_CLIENT` | 17 | 缓存Activity客户端 | 946 |
|
||||
| `PROCESS_STATE_CACHED_EMPTY` | 18 | 空进程(最容易被杀) | 1001 |
|
||||
| `PROCESS_STATE_NONEXISTENT` | 19 | 不存在 | 1001 |
|
||||
|
||||
---
|
||||
|
||||
## 🎚️ 三、OOM Adj 体系与调整机制
|
||||
|
||||
### 3.1 OOM Adj 的全景分级
|
||||
|
||||
Android 定义了完整的 OOM Adj 范围,从 -1000(绝对不杀)到 1001(随时可杀):
|
||||
|
||||
```
|
||||
Adj 值
|
||||
1001 ┤ CACHED_EMPTY (空进程) ← 首先被杀
|
||||
906 ┤ CACHED_ACTIVITY (已缓存Activity)
|
||||
900 ┤ LAST_ACTIVITY / HOME_APP
|
||||
800 ┤ HEAVY_WEIGHT (重量级)
|
||||
700 ┤ SERVICE / RECEIVER
|
||||
600 ┤ BACKUP (备份)
|
||||
500 ┤ TRANSIENT_BACKGROUND
|
||||
400 ┤ IMPORTANT_BACKGROUND
|
||||
300 ┤ IMPORTANT_FOREGROUND
|
||||
250 ┤ BOUND_FOREGROUND_SERVICE
|
||||
200 ┤ FOREGROUND_SERVICE (前台服务)
|
||||
50 ┤ BOUND_TOP (绑定到TOP)
|
||||
0 ┤ TOP (前台焦点App) ← 最高优先级
|
||||
-10 ┤ PERCEPTIBLE (可感知)
|
||||
-100 ┤ VISIBLE (可见但非焦点)
|
||||
-700 ┤ SYSTEM (系统进程)
|
||||
-800 ┤ PERSISTENT (持久进程) ← 永不杀
|
||||
-1000 ┤ NATIVE (Native守护进程)
|
||||
```
|
||||
|
||||
### 3.2 Adj 调整的核心方法:`computeOomAdjLocked()`
|
||||
|
||||
这是 AMS 中 **最重要、最复杂** 的方法之一,位于:
|
||||
|
||||
```
|
||||
frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java
|
||||
```
|
||||
|
||||
核心逻辑提炼:
|
||||
|
||||
```java
|
||||
// === OomAdjuster.computeOomAdjLocked() 核心流程图 ===
|
||||
//
|
||||
// 输入:ProcessRecord app
|
||||
// │
|
||||
// ▼
|
||||
// ① 检查是否前台 (isForeground)
|
||||
// ├─ 有 TOP Activity? → curAdj=0, procState=TOP
|
||||
// │ └─ return (最快路径)
|
||||
// │
|
||||
// ▼
|
||||
// ② 检查前台服务 (foregroundServices)
|
||||
// ├─ startForeground() 调用过?
|
||||
// │ └─ curAdj=FOREGROUND_SERVICE_ADJ (200)
|
||||
// │
|
||||
// ▼
|
||||
// ③ 检查绑定连接 (connections)
|
||||
// ├─ 被前台进程绑定?
|
||||
// │ └─ curAdj=BOUND_FOREGROUND_SERVICE (250)
|
||||
// │
|
||||
// ▼
|
||||
// ④ 检查 Provider 客户端
|
||||
// ├─ 被前台进程通过 ContentProvider 依赖?
|
||||
// │ └─ curAdj 按连接数加权
|
||||
// │
|
||||
// ▼
|
||||
// ⑤ 检查 Backup
|
||||
// ├─ 正在执行备份?
|
||||
// │ └─ curAdj=BACKUP_APP_ADJ (600)
|
||||
// │
|
||||
// ▼
|
||||
// ⑥ 检查 Service (非前台)
|
||||
// ├─ 有 Service 在运行?
|
||||
// │ └─ curAdj=SERVICE_ADJ (700)
|
||||
// │
|
||||
// ▼
|
||||
// ⑦ 检查广播接收
|
||||
// ├─ 正在接收广播?
|
||||
// │ └─ curAdj=SERVICE_ADJ (700)
|
||||
// │
|
||||
// ▼
|
||||
// ⑧ 检查 ContentProvider
|
||||
// ├─ 有 Provider 被客户端使用?
|
||||
// │ └─ curAdj 按最严重客户端计算
|
||||
// │
|
||||
// ▼
|
||||
// ⑨ 缓存分类
|
||||
// ├─ 有 Activity (已stop) → CACHED_ACTIVITY (906)
|
||||
// ├─ 有 Activity Client → CACHED_ACTIVITY_CLIENT (946)
|
||||
// └─ 空进程 → CACHED_EMPTY (1001)
|
||||
```
|
||||
|
||||
### 3.3 实战:用 dumpsys 观察 Adj 变化
|
||||
|
||||
```bash
|
||||
# 终端1:观察所有进程的 Adj
|
||||
adb shell dumpsys activity processes | grep -E "Proc #|oom:"
|
||||
|
||||
# 输出示例:
|
||||
# Proc #0: fg TOP/A/TPS trm: 0 31526:com.android.chrome/u0a282 (top-activity)
|
||||
# oom: max=1001 curRaw=0 setRaw=0 cur=0 set=0 ← Adj=0,前台
|
||||
# Proc #1: vis FGS/A/TPS trm: 0 31872:com.spotify.music/u0a303 (fg-service)
|
||||
# oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200 ← Adj=200,前台服务
|
||||
# Proc #2: svc SVC/A/TPS trm: 0 32101:com.android.music/u0a123 (service)
|
||||
# oom: max=1001 curRaw=700 setRaw=700 cur=700 set=700 ← Adj=700,Service
|
||||
# Proc #3: cch CEM/A/TPS trm: 0 32055:com.example.app/u0a456 (cch-empty)
|
||||
# oom: max=1001 curRaw=1001 setRaw=1001 cur=1001 set=1001 ← Adj=1001,空缓存
|
||||
|
||||
# 终端2:直接读取 /proc 验证
|
||||
adb shell cat /proc/31526/oom_score_adj # 应输出 0
|
||||
adb shell cat /proc/32055/oom_score_adj # 应输出 1001
|
||||
```
|
||||
|
||||
> **提示**:`curRaw`/`setRaw` 是 AMS 内部值(乘以 1000),`cur`/`set` 是映射后的 lmkd 值。两者通常一致,但在某些 OEM 定制 ROM 中可能不同。
|
||||
|
||||
---
|
||||
|
||||
## 🗡️ 四、lmkd —— 低内存杀手守护进程
|
||||
|
||||
### 4.1 lmkd 的架构定位
|
||||
|
||||
```
|
||||
用户空间 内核空间
|
||||
───────── ─────────
|
||||
AMS (system_server)
|
||||
│ applyOomAdjLocked()
|
||||
│ 写入 /proc/[pid]/oom_score_adj
|
||||
├──────────────────────────────────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
lmkd (native daemon) Linux OOM Killer
|
||||
│ socket: /dev/socket/lmkd │ (内核原生)
|
||||
│ │
|
||||
├─ 监听 memory pressure event │
|
||||
│ (来自内核 PSI/vmpressure) │
|
||||
│ │
|
||||
├─ 策略评估:是否需要杀进程? │
|
||||
│ ├─ 按 adj 从高到低选目标 │
|
||||
│ └─ 按内存占用排序 │
|
||||
│ │
|
||||
└─ kill(pid, SIGKILL) ──────────────► 进程被杀
|
||||
```
|
||||
|
||||
> **历史演进**:Android 10 之前使用内核 LMK(Low Memory Killer)驱动,Android 10+ 逐步迁移到用户空间 lmkd。Android 12+ 全面使用 lmkd + PSI(Pressure Stall Information)监控。
|
||||
|
||||
### 4.2 lmkd 的五级杀进程策略
|
||||
|
||||
```
|
||||
内存压力等级 (由 PSI 提供)
|
||||
═══════════════════════════════════════════════════════
|
||||
LEVEL │ 条件 │ 杀进程 Adj 范围
|
||||
─────────┼───────────────────┼─────────────────────────
|
||||
LOW │ 内存轻微不足 │ adj >= 900 (缓存进程)
|
||||
MEDIUM │ 内存中度不足 │ adj >= 800 (重量级+)
|
||||
HIGH │ 内存严重不足 │ adj >= 400 (重要后台+)
|
||||
CRITICAL │ 内存极度不足 │ adj >= 200 (前台服务+)
|
||||
EMERGENCY│ OOM 即将发生 │ adj >= 0 (全杀!)
|
||||
═══════════════════════════════════════════════════════
|
||||
|
||||
被杀进程顺序:高 adj 优先 → 同 adj 比内存 → RSS 大者先杀
|
||||
```
|
||||
|
||||
### 4.3 lmkd 关键配置
|
||||
|
||||
配置文件位置:
|
||||
|
||||
```
|
||||
系统属性(运行时):
|
||||
ro.lmk.use_minfree_levels = false // 用 PSI 而非 minfree
|
||||
ro.lmk.kill_heaviest_task = true // 同 adj 杀最重的
|
||||
ro.lmk.debug = false // 调试日志
|
||||
|
||||
AOSP 源码默认配置 (lmkd.cpp):
|
||||
// 默认百分比阈值
|
||||
low_ram_device 时:
|
||||
level 阈值: LOW=70 MEDIUM=80 HIGH=90 CRITICAL=95
|
||||
|
||||
正常设备时:
|
||||
level 阈值: LOW=60 MEDIUM=70 HIGH=80 CRITICAL=90
|
||||
```
|
||||
|
||||
### 4.4 实战:监听 lmkd 杀进程
|
||||
|
||||
```bash
|
||||
# 方式1:查看 lmkd 日志
|
||||
adb logcat -s lmkd:* | grep -E "Killing|kill|Low memory"
|
||||
|
||||
# 输出示例:
|
||||
# lmkd: Killing 'com.example.app' (pid=32055, uid=10456),
|
||||
# adj=1001, free memory=102400kB,
|
||||
# to free 204800kB, reason: pressure level HIGH
|
||||
|
||||
# 方式2:通过 dumpsys 查看历史杀进程记录
|
||||
adb shell dumpsys activity lmk | head -50
|
||||
|
||||
# 方式3:查看 /proc 中进程被杀统计
|
||||
adb shell cat /proc/vmstat | grep oom_kill
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 五、进程优先级与杀进程策略总结
|
||||
|
||||
### 5.1 进程"保命"金字塔
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ PERSISTENT │ Adj=-800 系统常驻,永不被杀
|
||||
│ SYSTEM │ Adj=-700
|
||||
├─────────────┤
|
||||
│ VISIBLE │ Adj=-100 可见但非焦点
|
||||
│ PERCEPTIBLE │ Adj=-10 可感知(如通知)
|
||||
├─────────────┤
|
||||
│ TOP │ Adj=0 前台焦点 ★
|
||||
│ BOUND_TOP │ Adj=50 绑定到前台
|
||||
├─────────────┤
|
||||
│ FGS │ Adj=200 前台Service ★安全线
|
||||
│ BOUND_FGS │ Adj=250
|
||||
├──── ────────┤ ← 以上进程几乎不会被杀
|
||||
│ IMPORTANT │ Adj=300~400
|
||||
│ BACKUP │ Adj=600
|
||||
├─────────────┤
|
||||
│ SERVICE │ Adj=700 Service进程 ★危险线
|
||||
├─────────────┤
|
||||
│ CACHED │ Adj=906 缓存Activity
|
||||
│ CCH_EMPTY │ Adj=1001 空进程 ★最先被杀
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### 5.2 开发者必知的 3 条铁律
|
||||
|
||||
1. **前台 Service 是你的保命符**:调用 `startForeground()` 后,进程 Adj=200,普通内存压力下绝不会被杀。但必须显示通知。
|
||||
|
||||
2. **不要滥用 `android:persistent="true"`**:只有系统应用才能持久化,第三方应用即使声明也无效,会被 PackageManager 忽略。
|
||||
|
||||
3. **空进程是"缓存池"不是"泄漏"**:看到 `CACHED_EMPTY` 进程数量多是正常现象,Android 故意保留空进程以加速下次启动。只有内存真的紧张时 lmkd 才会清理。
|
||||
|
||||
---
|
||||
|
||||
## 🧪 六、实战练习
|
||||
|
||||
### 练习 6.1:追踪一次完整的进程创建
|
||||
|
||||
```bash
|
||||
# 清空日志
|
||||
adb logcat -c
|
||||
|
||||
# 启动一个 App(以 Chrome 为例)
|
||||
adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
|
||||
|
||||
# 抓取关键日志
|
||||
adb logcat -d | grep -E "ActivityManager|Zygote|ActivityThread" \
|
||||
| grep -E "Start proc|fork|attachApplication|realStartActivity" \
|
||||
| head -20
|
||||
```
|
||||
|
||||
**预期输出时间线:**
|
||||
|
||||
```
|
||||
ActivityManager: Start proc 32188:com.android.chrome/u0a282 for activity
|
||||
Zygote: Forked child process 32188
|
||||
Zygote: Process 32188 started by zygote
|
||||
ActivityThread: main() called
|
||||
ActivityManager: attachApplication() from pid=32188
|
||||
ActivityManager: realStartActivityLocked: 32188 com.android.chrome/...
|
||||
```
|
||||
|
||||
> **作业**:用 `adb shell cat /proc/32188/status | grep -E "Uid|VmRSS|Threads"` 记录进程创建后 3 秒内的 VmRSS 变化。
|
||||
|
||||
### 练习 6.2:手动触发 lmkd 杀进程
|
||||
|
||||
```bash
|
||||
# 步骤1:找一个缓存进程
|
||||
adb shell dumpsys activity processes | grep "cch-empty" | head -1
|
||||
# 输出示例:Proc #15: cch CEM/A/TPS trm: 0 32055:com.example.app/u0a456 (cch-empty)
|
||||
|
||||
# 步骤2:查看其 oom_score_adj
|
||||
adb shell cat /proc/32055/oom_score_adj # 预期 1001
|
||||
|
||||
# 步骤3:模拟内存压力(需要 root)
|
||||
adb root
|
||||
adb shell "echo 100M > /proc/sys/vm/min_free_kbytes" # 提高最小空闲内存
|
||||
adb shell "echo 3 > /proc/sys/vm/drop_caches" # 清缓存
|
||||
# 然后快速分配内存触发压力
|
||||
adb shell "stress --vm 1 --vm-bytes 500M --timeout 5s"
|
||||
|
||||
# 步骤4:验证进程已被杀
|
||||
adb shell "ps -p 32055" # 预期 "No such process"
|
||||
```
|
||||
|
||||
### 练习 6.3:分析 ANR 与进程管理的关系
|
||||
|
||||
```bash
|
||||
# 模拟 ANR(在主线程 sleep)
|
||||
adb shell "am broadcast -a com.example.ANR_TEST --es action sleep"
|
||||
|
||||
# 获取 ANR trace
|
||||
adb shell ls /data/anr/
|
||||
adb pull /data/anr/anr_$(date +%Y-%m-%d-%H-%M-%S) ./
|
||||
|
||||
# 在 ANR trace 中查找关键行
|
||||
grep -A5 "Cmd line:" anr_* # 进程命令行
|
||||
grep "adj" anr_* # 当时 Adj 值
|
||||
grep "sys.default" anr_* # 系统负载
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 七、课后习题
|
||||
|
||||
### 选择题
|
||||
|
||||
**Q1**:以下哪个 Adj 值对应前台 Activity 所在进程?
|
||||
|
||||
A. -800
|
||||
B. -100
|
||||
C. 0
|
||||
D. 200
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**C**。TOP Activity 所在进程 curAdj=0,对应 PROCESS_STATE_TOP。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q2**:lmkd 在哪个内存压力等级时会开始杀缓存进程(Adj>=900)?
|
||||
|
||||
A. EMERGENCY
|
||||
B. HIGH
|
||||
C. MEDIUM
|
||||
D. LOW
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**D**。LOW 级别就开始清理 adj>=900 的缓存进程,这是最温和的回收手段。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q3**:以下关于 ProcessRecord 的 `curAdj` 和 `setAdj` 说法正确的是?
|
||||
|
||||
A. 它们是同一个值,只是命名不同
|
||||
B. `curAdj` 是内核中的实际值,`setAdj` 是 AMS 期望值
|
||||
C. `curAdj` 总是小于 `setAdj`
|
||||
D. 只有 `setAdj` 会同步到 lmkd
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**B**。`curAdj` 反映 `/proc/[pid]/oom_score_adj` 的当前值,`setAdj` 是 AMS 计算出的目标值,两者通过 `applyOomAdjLocked()` 完成同步。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 简答题
|
||||
|
||||
**Q4**:画出从用户「点击图标」到 `Activity.onCreate()` 被调用,涉及的所有进程和 IPC 通信,标注每一步的进程名和 IPC 类型。(参考答案参考第一节的 ASCII 图)
|
||||
|
||||
---
|
||||
|
||||
**Q5**:解释为什么 Android 要保留 CACHED_EMPTY 进程而不是立即杀掉?
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
CACHED_EMPTY 进程是已经加载了应用框架(ActivityThread、Application 对象已初始化)但没有任何 Activity/Service 的空进程。保留它们的好处:
|
||||
|
||||
1. **热启动加速**:再次启动 App 时,只需创建 Activity 即可,省去 fork+初始化框架的 ~200-500ms。
|
||||
2. **Zygote CoW 特性**:fork 出的子进程与 Zygote 共享只读内存页,空进程的额外内存开销很小。
|
||||
3. **惰性回收**:只在内存真正紧张时才杀,避免频繁的进程创建/销毁抖动。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q6**:如果 `startForegroundService()` 启动后 5 秒内不调用 `startForeground()`,会发生什么?Adj 会受到什么影响?
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
Android 8.0+ 规定:`startForegroundService()` 后必须在 **5 秒** 内调用 `startForeground()` 显示通知。超时后果:
|
||||
|
||||
1. **系统强制停止 Service** 并打印 ANR 警告(虽然不会真正弹 ANR 对话框)。
|
||||
2. **进程 Adj 不会降到 200**,仍保持 SERVICE_ADJ (700) 或更差,变成易被杀状态。
|
||||
3. 在 logcat 中可以看到:`Context.startForegroundService() did not then call Service.startForeground()`。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 实战题
|
||||
|
||||
**Q7**:编写一个简单的 Android App,在 `onCreate()` 中通过读取 `/proc/self/oom_score_adj` 来监控自身进程的 Adj 变化。要求:
|
||||
|
||||
- 在 MainActivity 的 `onResume()` 和 `onPause()` 分别打印 Adj 值。
|
||||
- 观察按 Home 键后 30 秒内 Adj 的变化(提示:在 `logcat` 中过滤你的 TAG)。
|
||||
|
||||
预期结果:前台时 adj=0 → 按 Home 后变为 adj=700~906 → 如果内存不足可能变为 adj=1001 → 被杀。
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资料
|
||||
|
||||
| 资源 | 路径/链接 |
|
||||
|:-----|:--------|
|
||||
| ProcessRecord.java | `frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java` |
|
||||
| OomAdjuster.java | `frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java` |
|
||||
| ProcessList.java | `frameworks/base/services/core/java/com/android/server/am/ProcessList.java` |
|
||||
| ZygoteProcess.java | `frameworks/base/core/java/android/os/ZygoteProcess.java` |
|
||||
| lmkd.cpp | `system/memory/lmkd/lmkd.cpp` |
|
||||
| ZygoteServer.java | `frameworks/base/core/java/com/android/internal/os/ZygoteServer.java` |
|
||||
| ActivityThread.java | `frameworks/base/core/java/android/app/ActivityThread.java` |
|
||||
| Android 进程状态文档 | https://developer.android.com/guide/components/processes-and-threads |
|
||||
|
||||
---
|
||||
|
||||
> **课程小结**:本课深入了 Android 进程管理的完整链路,从 Zygote fork 到 lmkd 杀进程,掌握了 ProcessRecord 的双值设计(curAdj/setAdj)、OOM Adj 的 19 级分级体系、以及 lmkd 基于 PSI 的五级压力杀进程策略。下一课将进入 **Activity 任务栈、启动模式与多窗口源码剖析**。
|
||||
|
||||
---
|
||||
|
||||
*Generated by AOSP Learning Assistant · Stage 5 Module A Lesson 1 · 2026-06-14*
|
||||
@@ -0,0 +1,919 @@
|
||||
# 阶段⑤ 模块A 第2课:Activity 任务栈、启动模式与多窗口源码剖析
|
||||
|
||||
> **课程定位**:阶段⑤ System Server 核心服务深度解析 · 模块A AMS深度 第2课
|
||||
> **难度等级**:⭐⭐⭐⭐⭐(深度源码级)
|
||||
> **预计阅读**:30~35 分钟
|
||||
> **前置课程**:阶段⑤第1课(AMS进程管理)、阶段③模块C第12课(AMS入门)
|
||||
> **源码基准**:AOSP 14.0.0_r50(兼容 Android 12/13/15 核心逻辑)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 课程目标
|
||||
|
||||
学完本课后,你将能够:
|
||||
|
||||
| 序号 | 能力目标 | 检验方式 |
|
||||
|:---:|:---------|:--------|
|
||||
| ① | 画出 WindowContainer → Task → ActivityRecord 的五层继承树 | 默写类图 |
|
||||
| ② | 解释四种 launchMode 在 `ActivityStarter` 中的源码级分支逻辑 | 画出流程图 |
|
||||
| ③ | 追踪 `startActivity()` 到 `realStartActivityLocked()` 的完整 IPC 调用链 | 写出10步链路 |
|
||||
| ④ | 理解 `FLAG_ACTIVITY_NEW_TASK` 与 `TaskAffinity` 的协作机制 | 给出3个场景 |
|
||||
| ⑤ | 掌握多窗口下 `TaskDisplayArea` 的任务栈隔离原理 | 画出分屏任务树 |
|
||||
| ⑥ | 使用 `dumpsys activity activities` 诊断任务栈状态 | 实战输出分析 |
|
||||
|
||||
---
|
||||
|
||||
## 📐 前置知识检查
|
||||
|
||||
在进入本课前,确认你已经掌握:
|
||||
|
||||
- [x] Activity 生命周期基础(onCreate → onResume → onPause → onStop → onDestroy)
|
||||
- [x] Binder IPC 基本调用链路(Client → Server → 回调)
|
||||
- [x] Intent 的基本结构(ComponentName / Action / Category / Flags)
|
||||
- [x] AMS 在 system_server 中的角色定位(第1课内容)
|
||||
- [x] `ActivityThread` 与 `ApplicationThread` 的关系
|
||||
- [ ] ⚠️ 如果不熟悉以上内容,请先复习阶段③模块B/C 对应课程
|
||||
|
||||
---
|
||||
|
||||
## 🧱 一、Task 系统全景架构:五层 WindowContainer 继承树
|
||||
|
||||
### 1.1 从 WindowContainer 到 ActivityRecord 的层级关系
|
||||
|
||||
Android 10 引入了 `WindowContainer` 基类,将所有窗口管理实体统一为一棵树。理解这棵树是理解 Task 系统的前提:
|
||||
|
||||
```
|
||||
WindowContainer (抽象基类 — 树节点)
|
||||
│
|
||||
├── RootWindowContainer (树根 — 整个系统的窗口容器)
|
||||
│ │ 单例,持有所有 Display 和顶层 Task
|
||||
│ │
|
||||
│ └── DisplayContent (每个屏幕一个)
|
||||
│ │ 代表物理/虚拟显示器
|
||||
│ │
|
||||
│ └── TaskDisplayArea (任务显示区域)
|
||||
│ │ 一个 Display 可以有多个 TDA(分屏时为2个)
|
||||
│ │ ★ 多窗口隔离的关键:每个分屏窗口对应一个 TDA ★
|
||||
│ │
|
||||
│ └── Task (任务栈)
|
||||
│ │ 一组 ActivityRecord 的集合
|
||||
│ │ 对应最近任务列表中的一张卡片
|
||||
│ │
|
||||
│ └── ActivityRecord (Activity 记录)
|
||||
│ 单个 Activity 实例的元数据
|
||||
│ 持有 Token、状态、Intent、进程引用
|
||||
```
|
||||
|
||||
### 1.2 WindowContainer 核心能力
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java
|
||||
|
||||
public class WindowContainer<E extends WindowContainer>
|
||||
implements Comparable<WindowContainer>, Animatable {
|
||||
|
||||
// === 树结构字段 ===
|
||||
protected WindowContainer mParent; // 父节点
|
||||
protected final ArraySet<E> mChildren = ...; // 子节点列表
|
||||
|
||||
// === 层级操作 ===
|
||||
void addChild(E child, int index); // 添加子节点
|
||||
void removeChild(E child); // 移除子节点
|
||||
void positionChildAt(int position, E child, boolean includingParents);
|
||||
void moveToFront(String reason); // 移到最前
|
||||
|
||||
// === 可见性控制 ===
|
||||
boolean mVisible; // 当前可见性
|
||||
void setVisible(boolean visible); // 递归设置可见性
|
||||
|
||||
// === Surface 控制 ===
|
||||
SurfaceControl mSurfaceControl; // SurfaceFlinger 图层
|
||||
SurfaceAnimator mSurfaceAnimator; // 动画控制
|
||||
|
||||
// === 遍历 ===
|
||||
void forAllWindows(Consumer<WindowState> callback, boolean traverseTopToBottom);
|
||||
}
|
||||
```
|
||||
|
||||
> **设计精髓**:所有窗口相关操作(添加/移除/显示/隐藏/动画)都在 `WindowContainer` 基类中实现,`Task`、`ActivityRecord`、`WindowState` 只需关注各自特有的逻辑。这种「组合模式」极大简化了窗口管理代码。
|
||||
|
||||
### 1.3 Task 数据结构深度解析
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/Task.java
|
||||
|
||||
public class Task extends WindowContainer<ActivityRecord> {
|
||||
|
||||
// ========== [身份标识] ==========
|
||||
int mTaskId; // 全局唯一 Task ID
|
||||
int mUserId; // 所属用户 ID(多用户)
|
||||
String affinity; // ★ TaskAffinity,决定归属
|
||||
Intent mLastNonFullscreenIntent; // 最后一次非全屏启动的 Intent
|
||||
|
||||
// ========== [状态标志] ==========
|
||||
boolean mIsFloating; // 是否浮动窗口(如气泡)
|
||||
boolean mDragResizing; // 是否正在拖拽调整大小
|
||||
boolean mOnTop; // 是否在最顶层
|
||||
int mResizeMode; // 调整大小模式
|
||||
|
||||
// ========== [结构关系] ==========
|
||||
TaskDisplayArea getTaskDisplayArea(); // 所属 TDA
|
||||
ActivityRecord getTopActivity(); // 栈顶 Activity
|
||||
ActivityRecord getRootActivity(); // 栈底 Activity(根)
|
||||
int getActivityCount(); // Activity 数量
|
||||
|
||||
// ========== [视觉属性] ==========
|
||||
int mBounds; // 任务边界(分屏时非全屏)
|
||||
boolean mMinimized; // 是否最小化(Android 12+)
|
||||
|
||||
// ========== [关键方法] ==========
|
||||
void addActivityToTop(ActivityRecord r); // 压入栈顶
|
||||
void removeActivity(ActivityRecord r); // 移除 Activity
|
||||
ActivityRecord getActivity(IBinder token); // 按 Token 查找
|
||||
void performClearTop(ActivityRecord r, ...); // 清栈到指定 Activity
|
||||
void performClearTask(); // 清空整个 Task
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 ActivityRecord 核心字段
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java
|
||||
|
||||
public class ActivityRecord extends WindowContainer<WindowState> {
|
||||
|
||||
// ========== [身份] ==========
|
||||
IBinder appToken; // ★ WMS 侧 Token,跨进程传递
|
||||
Task task; // 所属 Task
|
||||
ActivityInfo info; // AndroidManifest.xml 解析结果
|
||||
|
||||
// ========== [Intent] ==========
|
||||
Intent intent; // 启动该 Activity 的 Intent(可包含 flags)
|
||||
String resolvedType; // MIME 类型
|
||||
|
||||
// ========== [状态机] ==========
|
||||
enum State { INITIALIZING, STARTED, RESUMED, PAUSING, PAUSED,
|
||||
STOPPING, STOPPED, FINISHING, DESTROYING, DESTROYED, RESTARTING }
|
||||
State mState; // 当前生命周期状态
|
||||
|
||||
// ========== [进程关联] ==========
|
||||
ProcessRecord app; // 所在进程(null = 进程不存在)
|
||||
WindowProcessController mWindowProcessController; // Android 10+
|
||||
|
||||
// ========== [启动模式] ==========
|
||||
int launchMode; // 从 ActivityInfo 读取
|
||||
String taskAffinity; // 从 AndroidManifest 或默认包名
|
||||
|
||||
// ========== [配置] ==========
|
||||
Configuration configuration; // 屏幕配置(旋转/分屏时变化)
|
||||
boolean mIsExiting; // 是否正在退出
|
||||
|
||||
// ========== [窗口] ==========
|
||||
ActivityRecord mTaskOverlay; // 是否作为 Task 遮罩层
|
||||
boolean mShowForAllUsers; // 是否对所有用户可见
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 二、startActivity() 全链路追踪:10步跨越3进程
|
||||
|
||||
### 2.1 完整调用链路图
|
||||
|
||||
```
|
||||
用户调用 startActivity(intent)
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ App 进程 (com.example.app) │
|
||||
│ │
|
||||
│ ① Activity.startActivity(intent) │
|
||||
│ └─ startActivityForResult(intent, -1) │
|
||||
│ └─ Instrumentation.execStartActivity() │
|
||||
│ └─ ActivityTaskManager.getService() │
|
||||
│ .startActivity(...) │
|
||||
│ └─ Binder IPC ─────────────────┐ │
|
||||
└───────────────────────────────────────────────────│───────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ system_server 进程 │
|
||||
│ │
|
||||
│ ② ActivityTaskManagerService.startActivity() │
|
||||
│ └─ startActivityAsUser(...) │
|
||||
│ │
|
||||
│ ③ ActivityStarter.executeRequest() ★ 核心入口 │
|
||||
│ │ │
|
||||
│ ├─ ④ 解析 Intent / 权限检查 / resolveActivity() │
|
||||
│ │ │
|
||||
│ ├─ ⑤ 确定目标 Task:computeStackFocus() │
|
||||
│ │ ├─ standard → getOrCreateTask() │
|
||||
│ │ ├─ singleTop → findTask() / 栈顶判断 │
|
||||
│ │ ├─ singleTask→ findTask() / clearTop() │
|
||||
│ │ └─ singleInstance → createNewTask() │
|
||||
│ │ │
|
||||
│ ├─ ⑥ 创建 ActivityRecord │
|
||||
│ │ │
|
||||
│ ├─ ⑦ 将 ActivityRecord 放入 Task │
|
||||
│ │ Task.addActivityToTop(activityRecord) │
|
||||
│ │ │
|
||||
│ └─ ⑧ resumeFocusedTasksTopActivities() │
|
||||
│ └─ RootWindowContainer.resumeFocusedTasks... │
|
||||
│ └─ Task.resumeTopActivityInnerLocked() │
|
||||
│ └─ ⑨ startSpecificActivity() │
|
||||
│ ├─ 进程存在?→ realStartActivity()│
|
||||
│ └─ 进程不存在?→ startProcess() │
|
||||
│ (回到第1课的进程创建链路) │
|
||||
│ │
|
||||
│ ⑩ realStartActivityLocked() │
|
||||
│ └─ app.thread.scheduleTransaction() ── Binder ─┐ │
|
||||
└──────────────────────────────────────────────────────│────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ App 进程 │
|
||||
│ │
|
||||
│ ApplicationThread.scheduleTransaction() │
|
||||
│ └─ ActivityThread.handleLaunchActivity() │
|
||||
│ └─ performLaunchActivity() │
|
||||
│ ├─ makeApplication() │
|
||||
│ ├─ Activity.attach() │
|
||||
│ └─ Activity.onCreate() ★ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 ActivityStarter:所有启动逻辑的「调度中心」
|
||||
|
||||
```java
|
||||
// frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
|
||||
|
||||
class ActivityStarter {
|
||||
|
||||
int executeRequest(Request request) {
|
||||
// === 第一阶段:合法性检查 ===
|
||||
// ① 检查调用者权限
|
||||
checkStartAnyActivityPermission(intent);
|
||||
// ② 解析 Activity 信息
|
||||
resolveActivity(intent, resolvedType, ...);
|
||||
// ③ 检查是否允许启动(后台限制等)
|
||||
checkBackgroundActivityStart(...);
|
||||
|
||||
// === 第二阶段:确定目标 Task ===
|
||||
// ④ 根据 launchMode 和 flags 找或创建 Task
|
||||
Task targetTask = computeStackFocus(activityInfo, intent, ...);
|
||||
|
||||
// === 第三阶段:处理复用逻辑 ===
|
||||
// ⑤ 如果是 singleTop / singleTask,检查是否复用
|
||||
ActivityRecord reused = findReusableActivity(targetTask, intent, ...);
|
||||
if (reused != null) {
|
||||
// onNewIntent 路径
|
||||
deliverNewIntent(reused, intent);
|
||||
return START_SUCCESS;
|
||||
}
|
||||
|
||||
// === 第四阶段:创建并放置 ActivityRecord ===
|
||||
// ⑥ new ActivityRecord
|
||||
ActivityRecord r = new ActivityRecord(..., intent, activityInfo, ...);
|
||||
// ⑦ 放入 Task
|
||||
targetTask.addActivityToTop(r);
|
||||
|
||||
// === 第五阶段:启动流程 ===
|
||||
// ⑧ Resume
|
||||
mRootWindowContainer.resumeFocusedTasksTopActivities(...);
|
||||
|
||||
return START_SUCCESS;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Task/TaskDisplayArea 的查找逻辑
|
||||
|
||||
```java
|
||||
// ActivityStarter.java → computeStackFocus() 简化逻辑
|
||||
|
||||
Task computeStackFocus(ActivityInfo aInfo, Intent intent, int launchMode) {
|
||||
|
||||
TaskDisplayArea targetTDA = getTaskDisplayArea(intent, activityOptions);
|
||||
// ── 多窗口时的 TDA 选择逻辑 ──
|
||||
// 如果指定了 displayId → 找到对应 TDA
|
||||
// 如果是分屏启动 → 选择用户指定的分屏 TDA
|
||||
// 否则 → 使用当前焦点的 TDA
|
||||
|
||||
String targetAffinity = aInfo.taskAffinity != null
|
||||
? aInfo.taskAffinity
|
||||
: aInfo.packageName;
|
||||
|
||||
switch (launchMode) {
|
||||
case LAUNCH_SINGLE_INSTANCE:
|
||||
// ★ 强制创建新 Task,不与任何现有 Task 共享
|
||||
return createNewTask(targetTDA, targetAffinity);
|
||||
|
||||
case LAUNCH_SINGLE_TASK:
|
||||
// ★ 在整个 TDA 中查找 affinity 匹配的 Task
|
||||
Task existing = targetTDA.findTaskByAffinity(targetAffinity);
|
||||
if (existing != null) {
|
||||
// 找到 → 将 existing 之上的 Activity 全部清掉 (clearTop)
|
||||
existing.performClearTop(existing.getRootActivity());
|
||||
return existing;
|
||||
}
|
||||
// 没找到 → 创建新 Task
|
||||
return createNewTask(targetTDA, targetAffinity);
|
||||
|
||||
case LAUNCH_SINGLE_TOP:
|
||||
// ★ 检查当前栈顶是否是目标 Activity
|
||||
Task topTask = targetTDA.getTopTask();
|
||||
if (topTask != null && topTask.getTopActivity().isSameIntent(intent)) {
|
||||
// 复用栈顶 → onNewIntent
|
||||
return topTask;
|
||||
}
|
||||
// 栈顶不匹配 → 走 standard 逻辑
|
||||
// (注意:是否需要 NEW_TASK 取决于调用者的 Task)
|
||||
return getOrCreateTask(targetTDA, intent);
|
||||
|
||||
case LAUNCH_MULTIPLE: // standard
|
||||
default:
|
||||
// ★ 默认行为:根据 FLAG_ACTIVITY_NEW_TASK 决定
|
||||
if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0) {
|
||||
return getOrCreateTask(targetTDA, targetAffinity);
|
||||
} else {
|
||||
// 复用调用者的 Task
|
||||
return callerActivity.getTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎭 三、四大启动模式:源码级行为对比
|
||||
|
||||
### 3.1 总览对比表
|
||||
|
||||
| 模式 | Manifest 常量 | Task 行为 | onNewIntent | 多实例 | 典型场景 |
|
||||
|:-----|:------------|:---------|:-----------|:------|:--------|
|
||||
| **standard** | `LAUNCH_MULTIPLE` | 放入调用者 Task | 不触发 | 允许 | 普通页面 |
|
||||
| **singleTop** | `LAUNCH_SINGLE_TOP` | 栈顶匹配则复用 | 触发 | 允许(不在栈顶时) | 搜索页、详情页 |
|
||||
| **singleTask** | `LAUNCH_SINGLE_TASK` | 独占 Task,clearTop | 触发 | 不允许(Task内唯一) | 主页、浏览器 |
|
||||
| **singleInstance** | `LAUNCH_SINGLE_INSTANCE` | 完全独占 Task | 触发 | 不允许(全局唯一) | 拨号、地图导航 |
|
||||
|
||||
### 3.2 每种模式的 Task 栈变化示意图
|
||||
|
||||
```
|
||||
场景:依次启动 A(standard), B(singleTop), C(singleTask), D(singleInstance)
|
||||
|
||||
═══════════════════════════════════════════════════════════
|
||||
TDA (TaskDisplayArea)
|
||||
═══════════════════════════════════════════════════════════
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────────┐
|
||||
│ Task #1 │ │ Task #2 │ │ Task #3 │
|
||||
│ affinity │ │ affinity │ │ affinity │
|
||||
│ =com.a │ │ =com.c │ │ =com.d │
|
||||
│ │ │ │ │ │
|
||||
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌────────┐ │
|
||||
│ │ A │ │ │ │ C │ │ │ │ D │ │
|
||||
│ │(std) │ │ │ │(sTsk)│ │ │ │(sInst) │ │
|
||||
│ └──────┘ │ │ └──────┘ │ │ └────────┘ │
|
||||
│ ┌──────┐ │ └──────────┘ └──────────────┘
|
||||
│ │ B │ │
|
||||
│ │(sTop)│ │
|
||||
│ └──────┘ │
|
||||
└──────────┘
|
||||
|
||||
关键观察:
|
||||
- A 和 B 在同一个 Task(B 有 affinity=com.a 或未设置,默认=包名)
|
||||
- C 有 affinity=com.c + singleTask → 独立 Task #2
|
||||
- D 有 singleInstance → 独占 Task #3,且该 Task 只能有 D 一个 Activity
|
||||
```
|
||||
|
||||
### 3.3 singleTask 的 clearTop 算法源码还原
|
||||
|
||||
```java
|
||||
// Task.java → performClearTop() 简化逻辑
|
||||
|
||||
void performClearTop(ActivityRecord target, String reason) {
|
||||
// target: 要被保留在栈顶的 Activity
|
||||
// 遍历栈中所有在 target 之上的 Activity
|
||||
|
||||
List<ActivityRecord> aboveActivities = new ArrayList<>();
|
||||
for (int i = mChildren.size() - 1; i >= 0; i--) {
|
||||
ActivityRecord r = mChildren.get(i);
|
||||
if (r == target) break; // 遇到 target,停止
|
||||
aboveActivities.add(r); // 收集 target 之上的 Activity
|
||||
}
|
||||
|
||||
// 依次 finish 掉这些 Activity
|
||||
for (ActivityRecord r : aboveActivities) {
|
||||
r.finishIfPossible(reason, false);
|
||||
removeActivity(r);
|
||||
}
|
||||
|
||||
// target Activity 收到 onNewIntent
|
||||
target.deliverNewIntent(newIntent);
|
||||
|
||||
// 将 target 移到栈顶
|
||||
positionChildAtTop(target);
|
||||
}
|
||||
```
|
||||
|
||||
**实例说明**:
|
||||
|
||||
```
|
||||
启动前:Task#1 → [A] → [B] → [C] → [D] (D是栈顶)
|
||||
调用 startActivity(singleTask-C)
|
||||
|
||||
执行 performClearTop(C):
|
||||
1. 找到 C 在栈中的位置 (index=2)
|
||||
2. 收集 C 之上的 Activity: [D]
|
||||
3. finish D
|
||||
4. C.deliverNewIntent()
|
||||
5. C 移到栈顶
|
||||
|
||||
启动后:Task#1 → [A] → [B] → [C] (C是栈顶,D已销毁)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏷️ 四、Intent Flags:启动行为的「微调旋钮」
|
||||
|
||||
### 4.1 核心 Flags 一览
|
||||
|
||||
| Flag 常量 | 值 | 作用 | 与 launchMode 关系 |
|
||||
|:----------|:---|:-----|:------------------|
|
||||
| `FLAG_ACTIVITY_NEW_TASK` | 0x10000000 | 在新 Task 中启动 | ≈ singleTask(但不强制 clearTop) |
|
||||
| `FLAG_ACTIVITY_CLEAR_TOP` | 0x04000000 | 清除目标之上的 Activity | 常与 NEW_TASK 组合 |
|
||||
| `FLAG_ACTIVITY_SINGLE_TOP` | 0x20000000 | 栈顶匹配时复用 | ≈ singleTop |
|
||||
| `FLAG_ACTIVITY_CLEAR_TASK` | 0x00008000 | 先清空整个 Task | 必须与 NEW_TASK 组合 |
|
||||
| `FLAG_ACTIVITY_REORDER_TO_FRONT` | 0x00020000 | 将已有实例移到栈顶 | 不创建新实例 |
|
||||
| `FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS` | 0x00800000 | 不显示在最近任务 | Task 不出现在 Recents |
|
||||
|
||||
### 4.2 经典组合效果矩阵
|
||||
|
||||
```
|
||||
调用形式 │ Task 行为 │ Activity 行为
|
||||
───────────────────────────────────┼─────────────────────┼────────────────────
|
||||
NEW_TASK │ 新Task或匹配affinity │ 创建新实例(不一定clear top)
|
||||
NEW_TASK + CLEAR_TOP │ 匹配Task + clearTop │ 目标之上全清 + onNewIntent
|
||||
CLEAR_TASK + NEW_TASK │ 先清空再使用 │ 全新实例,旧的全部销毁
|
||||
SINGLE_TOP │ 复用调用者Task │ 栈顶匹配则onNewIntent
|
||||
REORDER_TO_FRONT │ 不改变Task结构 │ 直接移到栈顶(不recreate)
|
||||
NEW_TASK + SINGLE_TOP + CLEAR_TOP │ 匹配Task + clearTop │ 三合一效果
|
||||
```
|
||||
|
||||
### 4.3 FLAG_ACTIVITY_NEW_TASK + TaskAffinity 协作场景
|
||||
|
||||
```java
|
||||
// 场景:从 App-A 启动 App-B 的 Activity
|
||||
|
||||
// App-A (包名: com.app.a, 默认 affinity: com.app.a)
|
||||
// App-B (包名: com.app.b)
|
||||
|
||||
// === 场景 1: 不带 NEW_TASK ===
|
||||
Intent intent = new Intent(context, AppBActivity.class);
|
||||
startActivity(intent);
|
||||
// 结果:AppBActivity 被放入 App-A 的 Task 中!
|
||||
// Task#1(affinity=com.app.a) → [AppA-Act] → [AppB-Act]
|
||||
// 按返回键会回到 App-A
|
||||
|
||||
// === 场景 2: 带 NEW_TASK ===
|
||||
Intent intent = new Intent(context, AppBActivity.class);
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
// 结果:创建新 Task,affinity=com.app.b
|
||||
// Task#2(affinity=com.app.b) → [AppB-Act]
|
||||
// 两个 Task 独立存在于 Recents
|
||||
|
||||
// === 场景 3: AppB-Act 声明了 taskAffinity="com.app.b.custom" ===
|
||||
// Manifest: <activity android:name=".AppBActivity"
|
||||
// android:taskAffinity="com.app.b.custom"/>
|
||||
Intent intent = new Intent(context, AppBActivity.class);
|
||||
intent.addFlags(FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
// 结果:创建新 Task,affinity=com.app.b.custom (= 声明的值)
|
||||
// 下次再启动这个 Activity 时,会匹配到同一个 Task
|
||||
```
|
||||
|
||||
> **关键规则**:`TaskAffinity` 是 Task 的「护照」。`NEW_TASK` 告诉 AMS "我要出国(新Task)",`TaskAffinity` 告诉 AMS "我要去哪个国家(哪个Task)"。
|
||||
|
||||
---
|
||||
|
||||
## 🪟 五、多窗口/分屏下的任务栈管理
|
||||
|
||||
### 5.1 Android 12+ 的分屏架构
|
||||
|
||||
```
|
||||
RootWindowContainer
|
||||
│
|
||||
├── DisplayContent (主屏幕)
|
||||
│ │
|
||||
│ ├── TaskDisplayArea #1 (上半屏 / 左半屏)
|
||||
│ │ ├── Task #1 → [Activity-A1] → [Activity-A2]
|
||||
│ │ └── Task #2 → [Activity-A3]
|
||||
│ │
|
||||
│ └── TaskDisplayArea #2 (下半屏 / 右半屏)
|
||||
│ ├── Task #3 → [Activity-B1]
|
||||
│ └── Task #4 → [Activity-B2] → [Activity-B3]
|
||||
│
|
||||
└── DisplayContent (外接显示器)
|
||||
└── TaskDisplayArea #3 (全屏)
|
||||
└── Task #5 → [Activity-C1]
|
||||
```
|
||||
|
||||
> **核心理解**:每个分屏窗口对应一个 `TaskDisplayArea`,TDA 之间**任务栈完全隔离**。这意味着上半屏的 Task 和下半屏的 Task 互不干扰,各自拥有独立的返回栈。
|
||||
|
||||
### 5.2 分屏模式下的 startActivity 行为
|
||||
|
||||
```java
|
||||
// ActivityStarter.java → getTaskDisplayArea()
|
||||
|
||||
TaskDisplayArea getTaskDisplayArea(Intent intent, ActivityOptions options) {
|
||||
if (options != null && options.getLaunchDisplayId() != INVALID_DISPLAY) {
|
||||
// 明确指定了 Display → 找到对应 Display 的焦点 TDA
|
||||
DisplayContent dc = getDisplayContent(options.getLaunchDisplayId());
|
||||
return dc.getDefaultTaskDisplayArea();
|
||||
}
|
||||
|
||||
if (options != null && options.getLaunchTaskDisplayArea() != null) {
|
||||
// 指定了 TDA(如从分屏的某个窗口启动)
|
||||
return options.getLaunchTaskDisplayArea();
|
||||
}
|
||||
|
||||
if (inMultiWindowMode() && callerActivity != null) {
|
||||
// ★ 分屏时:默认将新 Activity 放入调用者所在的 TDA
|
||||
// 这是用户直觉所在——在哪个窗口操作,就在哪个窗口显示
|
||||
return callerActivity.getTask().getTaskDisplayArea();
|
||||
}
|
||||
|
||||
// 非分屏:使用默认 TDA(全屏的那个)
|
||||
return mRootWindowContainer.getDefaultTaskDisplayArea();
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Task 在 TDA 之间的移动
|
||||
|
||||
```java
|
||||
// Task.java → reparent()
|
||||
|
||||
void reparent(TaskDisplayArea newParent, boolean onTop, boolean animate) {
|
||||
// 场景1:用户将分屏窗口拖拽到全屏
|
||||
// Task 从一个 TDA 移动到另一个 TDA
|
||||
|
||||
TaskDisplayArea oldParent = getTaskDisplayArea();
|
||||
if (oldParent == newParent) return; // 同一 TDA,无需操作
|
||||
|
||||
// 从旧 TDA 移除
|
||||
oldParent.removeChild(this);
|
||||
|
||||
// 加入新 TDA
|
||||
if (onTop) {
|
||||
newParent.positionChildAtTop(this);
|
||||
} else {
|
||||
newParent.addChild(this);
|
||||
}
|
||||
|
||||
// ★ 注意:当前焦点 Activity 需要重新 Resume
|
||||
// 因为 TDA 切换意味着窗口焦点可能变化
|
||||
mRootWindowContainer.resumeFocusedTasksTopActivities();
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 分屏时的 Adj 计算差异
|
||||
|
||||
分屏模式下,两个 TDA 的 `TOP` Activity 都可能获得 `adj=0`:
|
||||
|
||||
```
|
||||
非分屏(全屏):
|
||||
TDA#1(全屏) → Task#1 → [TOP-Act] adj=0
|
||||
其他不可见 Activity adj=700~1001
|
||||
|
||||
分屏模式:
|
||||
TDA#1(上半屏) → Task#1 → [TOP-Act-A] adj=0 ← 两个TOP!
|
||||
TDA#2(下半屏) → Task#2 → [TOP-Act-B] adj=0 ← 两个TOP!
|
||||
|
||||
// 但实际只有一个是「焦点窗口」,另一个是「可见但非焦点」
|
||||
// curProcState 会区分:
|
||||
// 焦点窗口:PROCESS_STATE_TOP (2)
|
||||
// 非焦点可见窗口:PROCESS_STATE_TOP_SLEEPING (11)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 六、实战练习
|
||||
|
||||
### 练习 6.1:用 dumpsys 观察四种启动模式的 Task 变化
|
||||
|
||||
```bash
|
||||
# === 实验准备:清理环境 ===
|
||||
adb shell am force-stop com.android.chrome
|
||||
adb shell am force-stop com.android.calculator2
|
||||
|
||||
# === 步骤1:启动 Chrome(standard) ===
|
||||
adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
|
||||
# 查看任务栈
|
||||
adb shell dumpsys activity activities | grep -A30 "Display #0" | grep -E "Task{|ActivityRecord{"
|
||||
|
||||
# 预期输出:
|
||||
# Task{#1 type=standard U=0 visible=true ...
|
||||
# ActivityRecord{#1 u0 com.android.chrome/... t1}
|
||||
|
||||
# === 步骤2:观察 singleTask ===
|
||||
# Chrome 的 MainActivity 通常声明为 singleTask
|
||||
adb shell am start -n com.android.chrome/org.chromium.chrome.browser.ChromeTabbedActivity
|
||||
# 再次启动同一个 Activity → 不会创建新实例
|
||||
adb shell dumpsys activity activities | grep "com.android.chrome"
|
||||
# 输出只有 1 个实例 → onNewIntent 被触发
|
||||
|
||||
# === 步骤3:用 adb 模拟 NEW_TASK flag ===
|
||||
adb shell am start -f 0x10000000 -n com.android.calculator2/.Calculator
|
||||
# -f 0x10000000 = FLAG_ACTIVITY_NEW_TASK
|
||||
# 计算器会在自己的 Task 中启动
|
||||
|
||||
# === 步骤4:观察多个 Task ===
|
||||
adb shell dumpsys activity activities | grep -E "^\s+\* Task{"
|
||||
# 输出至少2个 Task(Chrome的 + 计算器的)
|
||||
```
|
||||
|
||||
### 练习 6.2:编写 Activity 验证 taskAffinity 作用
|
||||
|
||||
```java
|
||||
// === 在 AndroidManifest.xml 中声明 ===
|
||||
<activity android:name=".MainActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity="com.example.custom_affinity" />
|
||||
|
||||
<activity android:name=".SecondActivity"
|
||||
android:launchMode="standard" /> <!-- 默认 affinity=包名 -->
|
||||
|
||||
// === MainActivity.java ===
|
||||
public class MainActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle saved) {
|
||||
super.onCreate(saved);
|
||||
|
||||
// 打印当前 Task 信息
|
||||
Log.d("TASK_DEBUG", "Task ID: " + getTaskId());
|
||||
Log.d("TASK_DEBUG", "isTaskRoot: " + isTaskRoot());
|
||||
Log.d("TASK_DEBUG", "TaskAffinity: " +
|
||||
getPackageManager().getActivityInfo(getComponentName(), 0).taskAffinity);
|
||||
|
||||
// 启动 SecondActivity(不带 NEW_TASK)
|
||||
// 由于 SecondActivity 的 affinity=包名(默认),
|
||||
// 而 MainActivity 的 affinity=custom,
|
||||
// 不带 NEW_TASK → SecondActivity 进入 MAIN 的 Task? NO!
|
||||
//
|
||||
// 实际上:不带 NEW_TASK 时,新 Activity 总是进入调用者的 Task
|
||||
// taskAffinity 只在 NEW_TASK 时起作用!
|
||||
findViewById(R.id.btn).setOnClickListener(v -> {
|
||||
startActivity(new Intent(this, SecondActivity.class));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === SecondActivity.java ===
|
||||
public class SecondActivity extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle saved) {
|
||||
super.onCreate(saved);
|
||||
Log.d("TASK_DEBUG", "SecondActivity - Task ID: " + getTaskId());
|
||||
// 如果从 MainActivity 不带 NEW_TASK 启动 → Task ID 与 MainActivity 相同
|
||||
// 如果带 NEW_TASK → 会创建新 Task(affinity=包名)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 运行后查看日志
|
||||
adb logcat -s TASK_DEBUG
|
||||
```
|
||||
|
||||
### 练习 6.3:用 dumpsys 分析分屏模式下的 TDA
|
||||
|
||||
```bash
|
||||
# === 步骤1:进入分屏 ===
|
||||
# 方式A:在设备上通过手势/最近任务进入分屏
|
||||
# 方式B:模拟分屏(Android 12+)
|
||||
adb shell am start --windowingMode 5 -n com.android.chrome/...
|
||||
|
||||
# === 步骤2:检查分屏状态 ===
|
||||
adb shell dumpsys activity activities | grep -E "Display|TaskDisplayArea|windowingMode"
|
||||
|
||||
# 预期输出:
|
||||
# Display #0 (activities from top to bottom):
|
||||
# TaskDisplayArea #1 (windowingMode=5) ← 分屏上半
|
||||
# * Task{#1 ...}
|
||||
# TaskDisplayArea #2 (windowingMode=5) ← 分屏下半
|
||||
# * Task{#2 ...}
|
||||
|
||||
# === 步骤3:观察两个 TDA 的 Task 栈 ===
|
||||
adb shell dumpsys activity activities | awk '/TaskDisplayArea/,/^$/' | head -40
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 七、课后习题
|
||||
|
||||
### 选择题
|
||||
|
||||
**Q1**:以下哪种情况会触发 `onNewIntent()` 而不是 `onCreate()`?
|
||||
|
||||
A. standard 模式,同一个 Activity 被启动两次
|
||||
B. singleTop 模式,目标 Activity 已在栈顶
|
||||
C. singleTask 模式,目标 Activity 不在任何 Task 中
|
||||
D. singleInstance 模式,第一次启动该 Activity
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**B**。singleTop 在栈顶匹配时触发 onNewIntent。A:standard 每次新建;C:不在 Task 中则新建;D:第一次必然新建。
|
||||
|
||||
**补充**:singleTask 模式下,如果目标 Activity 已在其 Task 中存在(即使不在栈顶)且使用了 CLEAR_TOP,也会触发 onNewIntent。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q2**:关于 `FLAG_ACTIVITY_NEW_TASK`,以下说法**错误**的是?
|
||||
|
||||
A. 加上此 flag 后一定会创建新的 Task
|
||||
B. 如果已有 affinity 匹配的 Task,会复用该 Task
|
||||
C. 通常应与 `FLAG_ACTIVITY_CLEAR_TOP` 配合使用
|
||||
D. 从非 Activity 的 Context(如 Service)启动 Activity 时必须使用
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**A**。NEW_TASK 不保证一定创建新 Task。如果有 affinity 匹配的已有 Task,会复用该 Task。选项 D 正确——非 Activity Context 启动 Activity 必须加 NEW_TASK,否则会抛异常。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q3**:分屏模式下,两个 TDA 各有一个前台 Activity。以下说法正确的是?
|
||||
|
||||
A. 两个 Activity 的 oom_score_adj 都是 0
|
||||
B. 只有一个 Activity 的 adj=0,另一个的 adj=-100(VISIBLE)
|
||||
C. 两个 Activity 的 adj 都是 -100
|
||||
D. 两个 Activity 的 curProcState 完全相同
|
||||
|
||||
<details>
|
||||
<summary>答案</summary>
|
||||
**A** 最接近真相。在 Android 的分屏实现中,两个可见的 TOP Activity 的 `curAdj` 通常都是 0(或非常接近 0),但 `curProcState` 不同:焦点窗口为 `PROCESS_STATE_TOP (2)`,非焦点可见窗口为 `PROCESS_STATE_TOP_SLEEPING (11)`。所以 D 错误。B 错误——可见非焦点 ≠ VISIBLE(-100),那是被完全遮挡但仍在前台 Task 中的情况。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 简答题
|
||||
|
||||
**Q4**:画出 `WindowContainer` → `RootWindowContainer` → `DisplayContent` → `TaskDisplayArea` → `Task` → `ActivityRecord` → `WindowState` 的完整继承树,并标注每个层级在分屏和非分屏场景下的实例数量。
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
|
||||
```
|
||||
非分屏场景:
|
||||
RootWindowContainer (1个,单例)
|
||||
└─ DisplayContent (1个,主屏)
|
||||
└─ TaskDisplayArea (1个,全屏)
|
||||
├─ Task #1 → [ActivityRecord #1] → [WindowState #1]
|
||||
├─ Task #2 → [ActivityRecord #2] → [WindowState #2]
|
||||
└─ Task #3 → [ActivityRecord #3] → [WindowState #3]
|
||||
|
||||
分屏场景:
|
||||
RootWindowContainer (1个,单例)
|
||||
└─ DisplayContent (1个,主屏)
|
||||
├─ TaskDisplayArea #1 (上/左 半屏)
|
||||
│ ├─ Task #1 → [ActivityRecord #1] → [WindowState #1]
|
||||
│ └─ Task #2 → [ActivityRecord #2] → [WindowState #2]
|
||||
└─ TaskDisplayArea #2 (下/右 半屏)
|
||||
└─ Task #3 → [ActivityRecord #3] → [WindowState #3]
|
||||
```
|
||||
|
||||
关键:非分屏时 TDA=1,分屏时 TDA=2。Task 永远隶属于某个 TDA,不能同时出现在两个 TDA 中。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q5**:解释 `singleTask` + `CLEAR_TOP` 与 `singleTask` 不带 `CLEAR_TOP` 的行为差异。给出具体的栈变化示例。
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
|
||||
```
|
||||
初始状态:Task#1 → [A] → [B] → [C] → [D] (D是栈顶,A是singleTask)
|
||||
|
||||
场景1:startActivity(A) — singleTask 不带 CLEAR_TOP
|
||||
① AMS 找到 Task#1(affinity匹配)
|
||||
② 发现 A 已在栈中(非栈顶)
|
||||
③ 执行 clearTop:finish 掉 B、C、D
|
||||
④ A 收到 onNewIntent
|
||||
⑤ A 移到栈顶
|
||||
结果:Task#1 → [A] ← singleTask 默认隐式带 clearTop!
|
||||
|
||||
场景2:startActivity(A) — singleTask 显式带 FLAG_ACTIVITY_CLEAR_TOP
|
||||
与场景1完全相同,因为 singleTask 本身隐含了 clearTop 行为
|
||||
|
||||
★ 关键认知:singleTask 在目标 Activity 已在栈中时,默认就会 clearTop,
|
||||
不需要额外加 FLAG_ACTIVITY_CLEAR_TOP。但如果目标不在栈中(新的),
|
||||
则 CLEAR_TOP 无意义——因为没有东西需要清。
|
||||
|
||||
场景3:singleTask 但 A 不在栈中
|
||||
初始状态:Task#1 → [X] → [Y]
|
||||
startActivity(A) — singleTask
|
||||
① AMS 查找 affinity 匹配的 Task → 找到 Task#1
|
||||
② A 不在 Task#1 中
|
||||
③ 创建新的 ActivityRecord A,放入 Task#1
|
||||
结果:Task#1 → [X] → [Y] → [A]
|
||||
注意:这次没有 clearTop!因为 A 不在栈中,直接新建。
|
||||
|
||||
场景4:singleTask 但目标在栈中,且不需要清栈
|
||||
// 这种情况在标准 singleTask 中不存在
|
||||
// 如果开发者想在 singleTask 的 Task 中保留栈,只能:
|
||||
// ① 不用 singleTask,改用 standard + 手动管理
|
||||
// ② 或者在 onNewIntent 中自行处理而不依赖系统 clearTop
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
**Q6**:在分屏模式中,用户在「上半屏 Task-A」中点击按钮启动 Activity-X(standard 模式)。Activity-X 会被放入哪个 TDA?为什么?
|
||||
|
||||
<details>
|
||||
<summary>参考答案</summary>
|
||||
|
||||
Activity-X 会被放入**上半屏的 TDA #1**。原因:
|
||||
|
||||
1. `ActivityStarter` 在确定 TDA 时,优先检查调用者 Activity 所在的 TDA
|
||||
2. 由于调用者(Task-A 中的按钮所在的 Activity)位于 TDA #1
|
||||
3. `standard` 模式且不带 `NEW_TASK` flag → 复用调用者的 Task
|
||||
4. 调用者的 Task 属于 TDA #1 → Activity-X 进入 TDA #1
|
||||
|
||||
如果要让 Activity-X 进入下半屏的 TDA #2,需要:
|
||||
- 使用 `ActivityOptions.setLaunchDisplayArea(TDA#2)` (非公开 API)
|
||||
- 或者使用 `Intent.FLAG_ACTIVITY_NEW_TASK` + `ActivityOptions.makeBasic().setLaunchDisplayId(...)`(Android 12+)
|
||||
|
||||
这体现了 **TDA 隔离原则**:默认情况下,启动操作不会跨越 TDA 边界。
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 实战编程题
|
||||
|
||||
**Q7**:编写一个 App,包含 3 个 Activity(A、B、C),验证以下场景:
|
||||
|
||||
- A 声明为 `singleTask`,`taskAffinity="com.example.custom"`
|
||||
- B 声明为 `standard`(默认 affinity)
|
||||
- C 声明为 `singleInstance`
|
||||
|
||||
要求:
|
||||
1. 在 A 中启动 B,再在 B 中启动 C,使用 `dumpsys` 观察 Task 结构
|
||||
2. 在 C 中启动 A(带 `FLAG_ACTIVITY_NEW_TASK`),观察 A 回到自己的 Task 还是 C 的 Task
|
||||
3. 按下 Home 键后,用 `adb shell dumpsys activity activities` 观察三个 Task 的 `visible` 状态
|
||||
|
||||
提交:`dumpsys` 输出截图 + 注释说明每个 Task/AcitivtyRecord 的含义。
|
||||
|
||||
<details>
|
||||
<summary>预期结果提示</summary>
|
||||
|
||||
```
|
||||
初始:启动 A(singleTask, affinity=custom)
|
||||
Task#1(affinity=custom) → [A]
|
||||
|
||||
在 A 中启动 B(standard,无 NEW_TASK):
|
||||
Task#1(affinity=custom) → [A] → [B] ← B 进入 A 的 Task!
|
||||
|
||||
在 B 中启动 C(singleInstance):
|
||||
Task#1(affinity=custom) → [A] → [B]
|
||||
Task#2(affinity=包名) → [C] ← C 独占一个 Task
|
||||
|
||||
在 C 中启动 A(带 NEW_TASK):
|
||||
Task#1(affinity=custom) → [A] → [B] → [A_new?]
|
||||
等等——A 是 singleTask + affinity=custom,Task#1 匹配!
|
||||
Task#1(affinity=custom) → [A] ← clearTop!(B被清掉,A收到onNewIntent)
|
||||
Task#2(affinity=包名) → [C] ← 不受影响
|
||||
|
||||
按 Home 键后:
|
||||
两个 Task 都变为不可见(visible=false),但都不销毁
|
||||
dumpsys 中 visible=false,但 Activity 状态变为 STOPPED
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 🔗 参考资料
|
||||
|
||||
| 资源 | 路径/链接 |
|
||||
|:-----|:--------|
|
||||
| ActivityStarter.java | `frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java` |
|
||||
| ActivityRecord.java | `frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java` |
|
||||
| Task.java | `frameworks/base/services/core/java/com/android/server/wm/Task.java` |
|
||||
| TaskDisplayArea.java | `frameworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java` |
|
||||
| RootWindowContainer.java | `frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java` |
|
||||
| WindowContainer.java | `frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java` |
|
||||
| ActivityStackSupervisor.java | `frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java` |
|
||||
| ActivityThread.java | `frameworks/base/core/java/android/app/ActivityThread.java` |
|
||||
| Android 任务与返回栈 | https://developer.android.com/guide/components/activities/tasks-and-back-stack |
|
||||
| Android 多窗口支持 | https://developer.android.com/guide/topics/large-screens/multi-window-support |
|
||||
|
||||
---
|
||||
|
||||
> **课程小结**:本课以 `WindowContainer` 五层继承树为起点,逐层剖析了 `Task` → `ActivityRecord` → `WindowState` 的数据结构与层级关系。完整追踪了 `startActivity()` 跨越三进程的 10 步调用链,重点分析了 `ActivityStarter` 中四种 launchMode 的分支逻辑和 `performClearTop` 的清栈算法。深入对比了 6 个核心 Intent Flags 的组合效果,解析了 `TaskDisplayArea` 在多窗口下的隔离机制。下一课将进入 **WMS 窗口管理:WindowState、SurfaceFlinger 与 VSync 渲染管线**。
|
||||
|
||||
---
|
||||
|
||||
*Generated by AOSP Learning Assistant · Stage 5 Module A Lesson 2 · 2026-06-14*
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user