Appearance
应用进程模型:优先级、oom_adj 与低内存回收
本页导读:本页讲 Android 如何把「应用进程」当成有限的系统资源来统一调度:四种进程类型与优先级梯队、oom_adj 的计算与传递、LMKD 在内存吃紧时的选杀逻辑,以及传闻最多的「进程保活」与缓存进程回收的真相。前置阅读:架构篇 system_server 与 Binder 两页。
一、进程是系统资源,不是应用私产
Android 的整个应用框架建立在一个前提上:任何应用进程都可能被系统在任意时刻杀死,且不会收到任何回调。这不是缺陷,而是设计——RAM 是全设备共享的稀缺资源,桌面、相机、导航都可能需要立刻拿到它。
- 应用进程全部由 Zygote 派生(
fork自 zygote,见架构篇 init/Zygote 页),启动快,代价是必须接受统一调度。 - 系统的「保活」不是保住进程,而是保住状态:Activity 栈、savedInstanceState、最近任务缩略图都在系统侧,进程死了可以从状态重建。
- 一个进程的内存成本要看 PSS(含 Zygote 共享页摊薄后的私有部分),不是 RSS;这也是系统敢同时缓存几十上百个进程的原因之一。
理解这一点,后面所有机制——优先级、adj、LMKD、freezer——都只是这套资源调度策略的展开。
顺带说清三个易混概念:
- 进程优先级 ≠ 调度优先级:adj 决定「谁先死」,cgroup/调度组决定「谁多跑」,两者都由 Framework 统一划分。
- cached ≠ background:8.0 后的「后台执行」特指不可见且无豁免的执行窗口期,和 adj 900+ 的缓存梯队不是一回事。
- LMK 杀进程 ≠ 划任务:划掉最近任务会 kill 进程并清栈;LMKD 杀进程保留任务栈,属于「可恢复死亡」。
二、四种进程类型与优先级梯队
Framework 按「用户此刻是否需要你」把进程分成四个基本梯队(文档口径,源码中细分远多于此,完整常量以 ProcessList.java 为准):
| 进程类型 | 典型 oom_adj | 典型场景 | 被杀顺序 |
|---|---|---|---|
| 前台进程 Foreground | 0 附近 | 屏幕上的 Activity、正在执行生命周期回调/IntentReceiver | 最后 |
| 可见进程 Visible | ~100 | 不可见但用户可感知(弹窗 Activity、foreground service 提升) | 倒数第三 |
| 服务进程 Service | 负值到正值之间细分(以 AOSP 为准) | startService 启动、被其他进程绑定 | 倒数第二 |
| 缓存进程 Cached | ~900 起 | 按 Home 后退出界面的应用 | 最先 |
两个特殊梯队不属于「应用」语义但常一起出现:
- persistent 进程(如 system_server、电话):adj ≈ -800,几乎不会被杀,仅系统应用可申请
android:persistent。 - native 进程:由 init 直接管理,不在 AMS 记账体系内(NATIVE_ADJ = -10000 只是记账哨兵值)。
梯队内部还有大量细分槽位(AOSP ProcessList 里能看到 home、next-severe-home、heavy weight、service A/B、previous、backup、cached 多级分级等),用途各异:
- home:桌面进程,受特殊保护但仍可回收;
- heavy weight:声明了大内存占用属性的进程(视频剪辑、导航类),回收顺位只高于普通缓存进程;
- backup:正在跑
BackupAgent的进程,备份完立刻降级; - cached 内部分级:同为缓存,900~1000 之间的具体数值编码了「离开前台多久、被 LMK 杀过几次」等启发式信息(
ProcessList/CachedAppOptimizer,具体公式以 AOSP 为准),越靠后越先死。
版本注记:基于 Android 16 / API 36;Android 12 起大量 adj/调度常量向
AppPriorityConstants与进程感知调度(uapi)统一口径收敛,四梯队模型与数值量级未变;Android 5 以前内核侧使用 0~15 的旧oom_adj,2013 年后统一为-1000~1000的oom_score_adj体系。
三、oom_adj 是怎么算出来的
adj 不是应用自己声明的,而是 system_server(com.android.server.am 包,调度逻辑集中在 ProcessList/ActivityManagerService)维护的一张依赖图每轮重算的结果:
java
// frameworks/base/services/core/java/com/android/server/am/ProcessList.java(节选)
public final class ProcessList {
// adj 越小越重要,LMKD 从大到小挑软柿子杀
public static final int NATIVE_ADJ = -10000; // 不受 AMS 管理的 native 进程
public static final int PERSISTENT_PROC_ADJ = -800; // 系统 persistent 进程
public static final int FOREGROUND_APP_ADJ = 0; // 前台应用
public static final int VISIBLE_APP_ADJ = 100; // 可见应用
public static final int PERCEPTIBLE_APP_ADJ = 200; // 可感知(如前台服务)
public static final int PREVIOUS_APP_ADJ = 700; // 上一任前台
public static final int CACHED_APP_MIN_ADJ = 900; // 缓存进程起点,上探约 1000
// 其余细分(home、heavy weight、backup、service A/B 等)以当前版本源码为准
}参与计算的因素(任一命中都会把 adj 往小里拉):
- ActivityRecord:有 resumed/paused/可见的 Activity;
- 窗口:有可见窗口(WMS 上报
requestVisibility,含悬浮窗、input method); - ActiveServices:运行前台服务、正在执行
onStartCommand/onBind; - 绑定关系:被别的进程 bind(
ConnectionRecord),绑定会把被绑进程拉到不低于绑定方的优先级——「绑我者强于我」; - ContentProvider:正在被跨进程查询或已建立连接;
- 广播:正在分发给该进程(队列持有 adj 提升);
- 近期交互:刚退出前台的进程先进入 previous(~700)再落入 cached。
每轮计算由状态变化触发(resume、bind、broadcast enqueue……),最终写内核 /proc/<pid>/oom_score_adj。
一个具体算例:桌面 A(home 档)bind 了音乐播放器 B 的服务,B 同时在播歌、开了 mediaPlayback 前台服务。用户回到桌面后:
- B 因前台服务落在 perceptible 档(~200);
- 绑定关系要求 B 不低于绑定方 A,B 被抬到与 home 相当的档位;
- A 自己因为 home 身份享受远高于普通缓存进程的优先级;
- 内存告急时 LMKD 先动的是 adj 900+ 的无关缓存进程,A/B 都安全——这就是「绑定传播」的实战含义。
设备上可直接验证:
bash
adb shell ps -A -o PID,RSS,NAME | grep com.example.app
adb shell cat /proc/<pid>/oom_score_adj
adb shell dumpsys activity processes # 看各进程当前 oom adj 与 adj 来源四、LMKD:从内核驱动到用户态 PSI 判官
低内存回收的执行者是 lmkd(AOSP 仓库 system/memory/lmkd,早期在 frameworks/base/cmds/lmkd,已迁出 framework):
- 旧方案:内核 lowmemorykiller 驱动监听内存水位,按
oom_score_adj阈值挑进程杀。问题:只看 free 水位,看不见「抖动」。 - 现行方案(Android 10+,新内核已彻底删除低内存驱动):lmkd 在用户态监视
/proc/pressure/memory(PSI,Pressure Stall Information),以 thrashing(颠簸度)——即进程花在换页/回收上的时间占比——为主要判据:内存「少」不可怕,「转不动」才可怕。 - 选杀顺序:优先杀高 adj 缓存进程,且会综合 thrashing 贡献、占用大小(含 swap);同一 UID 的进程按进程组处理;两次击杀间有节流窗口,防止雪崩(具体阈值参数以 AOSP
ro.lmk.*/lmkd README 为准)。 - kill 原因可读:logcat 中
Kill ... due to ...的原因字段能区分「绝对内存不足」与「颠簸度过高」,排查机型差异先看它(字符串以 AOSP 为准)。 - 兜底:内核 OOM killer 只在 lmkd 也救不回来时出场;persistent/native 进程理论上永不出现在受害者名单里。
工程上常用的观测手段:adb shell dmesg/logcat 中 lmkd 的 Kill '<proc>', uid, ..., adj, ... 行,以及 perfetto 的 lmkd 数据源(trace 中可看到 PSI 事件与击杀原因)。典型决策参数族(示例性质,名称与取值以 lmkd README 与当前 AOSP 为准):
bash
adb shell getprop ro.lmk.use_minfree_boosting
adb shell getprop ro.lmk.swap_free_in_bytes
adb shell getprop ro.lmk.thrashing_limit
adb shell cat /proc/pressure/memory # PSI:some/full stall 统计
# 实时看击杀事件:adb logcat | grep -iE "lmkd|Kill .* due to"五、冻而不杀:cached app freezer
Android 12(API 31)引入 cached app freezer:对纯缓存进程用 cgroup freezer 直接冻结——进程还活着、内存还在,但不再消耗一帧 CPU。
- 目的:省电优于省内存。冻结进程被 Binder 调用命中时自动解冻,对调用方透明。
- 自查方法:
adb shell dumpsys activity processes | grep -i freezer或看cgroup.freeze相关状态(命令输出格式随版本变化,以设备为准);被冻结时该进程 CPU% 应恒为 0。 - 触发条件:进程处于 cached 且一段时间未活动;被绑、正在跑 Job 的进程不冻结(细节以 AOSP
CachedAppOptimizer/ProcessList 为准)。 - 副作用:冻结进程上的 Handler 消息、网络收包会延迟到解冻,做「后台静默上报」的开发者最容易在这里踩坑。
版本注记:基于 Android 16 / API 36;freezer 自 Android 12 灰度引入、后续版本默认逐步放开并迁移 cgroup v2;Android 12~15 上不同 OEM 的冻结激进度差异较大,以设备实测为准。
六、进程保活的真相
「保活」这个词本身就是误用:Android 的内存模型里不存在合法的「让我的进程永远不死」,存在的是「让系统认为你还重要」。逐条拆穿:
| 保活偏方 | 现状 |
|---|---|
| 双进程/多进程互相拉起 | Android 5.0 起从最近任务划掉即强杀整个 task 的进程组;互相拉起的进程 adj 相同,LMKD 一锅端 |
| 1 像素透明 Activity | 骗不过 adj 计算(需要真实可见窗口),且属于厂商风控封杀对象 |
| JobScheduler/Alarm 广播互唤醒 | Android 8 隐式广播限制 + Doze + App Standby Bucket 层层限频 |
| 常驻前台服务 | Android 8 起后台起服务受限;Android 12 后台起前台服务抛 ForegroundServiceStartNotAllowedException;Android 14 必须声明 FGS 类型;Android 16 进一步收紧(详见 services 页) |
| 提升优先级(persistent) | android:persistent 仅对系统签名/特权应用生效,三方应用无从申请 |
Google Play 政策还把「无用户价值的保活」列为违规。国内 ROM 的自启动管理、后台白名单是厂商私有能力:用户可以手动加白名单,但应用侧没有任何公开 API 能申请它——把「在小米上能保活」当成架构假设,换一台 Pixel 就全线崩塌。
正确姿势是选对组件:即时长任务用带正确类型的 foreground service,可延迟任务用 WorkManager,数据同步用 JobScheduler——让系统知道你在干什么,系统才给你对应档期的豁免。
七、缓存进程的完整一生
一个普通应用按 Home 之后的时间线:
onPause→onStop→onSaveInstanceState:此刻状态已持久化到系统侧(写入 binder 事务),进程死亡可随时恢复。- 进入 cached:adj 升到 900+;一段时间无动作后被 freezer 冻结。
- 内存告急:lmkd 按 PSI 选杀——通常是 adj 最高、贡献颠簸度最大的先走,没有任何回调,
onDestroy不会执行。 - 用户从最近任务回来:进程冷启动 → 系统按保存的 Activity 栈重建 →
onCreate(savedInstanceState)恢复。用户看到「一切如初」,只有启动耗时骗不了人(这就是冷启动优化、reportFullyDrawn、Baseline Profile 的全部意义)。
两个工程推论:不要把「进程还活着」当成前提写代码(静态变量、进程内缓存随时归零);要把所有跨进程死亡的数据走 savedInstanceState/ViewModel+SavedStateHandle/落盘三件套。
冷启动代价是这件事唯一能被用户感知的部分:LMK 越频繁,「越用越卡」越明显——这也是 Google 把 Baseline Profile、启动优化列为一等公民指标的原因。
八、常见误区问答
Q1:oom_score_adj 能自己改吗? 不能。只有 system_server(ProcessList.setOomAdj)经特权路径写这个节点;三方进程写它会被 SELinux 直接拒绝。所谓「保活 SDK 改 adj」都是玄学宣传。
Q2:为什么我的 App 被杀后能恢复,隔壁团队的不能? 恢复要同时满足:任务栈仍在系统侧、onSaveInstanceState 存下了数据、重建时组件路径不变。用单例存业务数据、FLAG_ACTIVITY_CLEAR_TASK、android:noHistory 都会破坏其中一环。
Q3:拆多个进程(:remote、push 进程)能提高存活率吗? 不能。同 UID 进程组在 LMKD 面前按同一把尺子量,子进程没有独立 adj 优势,只是抬高总 PSS、让自己更先被挑中。
Q4:如何量化线上被 LMK 杀的频率? 用 ActivityManager#getHistoricalProcessExitReasons(API 30+,Debug.REASON_OTHER/低内存相关退出原因)做线上统计;本地复现用 adb shell am send-trim-memory 模拟回收、perfetto 抓 lmkd 事件。
Q5:freezer 和 Doze 是一回事吗? 不同层:Doze 限制唤醒源(alarm/网络/扫描,见架构篇电源页);freezer 直接掐掉 cached 进程的 CPU。被冻结进程连 Doze 退出广播都要等解冻才能处理。
Q6:怎么看某个进程当前的 adj 与它的来源?adb shell dumpsys activity processes 会按进程打印 mAdj=... (src=...) 样式的行,括号里的来源标记(如 FGS、BIND、CPL 即 ContentProvider 连接)直接对应本页三节的计算因子,比读源码快得多。
本页小结:Android 用 adj 把全设备进程排成一个优先级梯队,adj 由 AMS 按依赖图实时重算并写进内核;LMKD 以 PSI 颠簸度为用户态执行者,freezer 让缓存进程先冻后杀;「保活」是对这套模型的误读,正解是让组件选对、状态存好,进程死了也能秒复活。
参考链接
- https://developer.android.com/guide/components/processes-and-threads
- https://developer.android.com/docs/guide/components/process-lifecycle
- https://cs.android.com/android/platform/frameworks/base/+/main:services/core/java/com/android/server/am/ProcessList.java
- https://cs.android.com/android/platform/system/memory/+/main:lmkd
- https://developer.android.com/topic/performance/memory-engage-users(内存与冷启动最佳实践)
- 站内:Activity 全生、Service 双轨、内存与 GC、架构篇 init 与 Zygote、system_server