Skip to content

应用进程模型:优先级、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典型场景被杀顺序
前台进程 Foreground0 附近屏幕上的 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 前台服务。用户回到桌面后:

  1. B 因前台服务落在 perceptible 档(~200);
  2. 绑定关系要求 B 不低于绑定方 A,B 被抬到与 home 相当的档位;
  3. A 自己因为 home 身份享受远高于普通缓存进程的优先级;
  4. 内存告急时 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 之后的时间线:

  1. onPause → onStop → onSaveInstanceState:此刻状态已持久化到系统侧(写入 binder 事务),进程死亡可随时恢复。
  2. 进入 cached:adj 升到 900+;一段时间无动作后被 freezer 冻结。
  3. 内存告急:lmkd 按 PSI 选杀——通常是 adj 最高、贡献颠簸度最大的先走,没有任何回调,onDestroy 不会执行。
  4. 用户从最近任务回来:进程冷启动 → 系统按保存的 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 让缓存进程先冻后杀;「保活」是对这套模型的误读,正解是让组件选对、状态存好,进程死了也能秒复活。

参考链接 ​

基于 Android 16 / API 36 · 内容为原创整理,转载请注明出处