早上九点刚到工位,测试群里已经炸了。产品经理甩了张友盟后台截图:
「昨天开始,App 后台存活率从 72% 掉到了 41%。用户反馈推送收不到、音乐切后台就断。你们是不是改了什么?」
我看了一眼 git log。国庆前最后一天合了三个 MR,其中一个是阿凯写的——把首页的一个定时刷新从 Handler.postDelayed 改成了 AlarmManager,说是"更省电"。
当时 code review 我还点了 approve。
谁能想到这一改,直接把进程的 oom_adj 从 200(可见进程级别)拉到了 900(cached 级别),LMK 一刀下去就没了。
这篇讲三件事:oom_adj 到底是什么、AMS 怎么算它、以及我怎么用 dumpsys activity processes 定位到那行代码的。
事故现场:dumpsys 里的数字不对劲
【老周(ROM 厂商总监视角)】:「oom_adj 问题在厂商这边是重灾区。每个月至少有两三个 App 厂商来找我们,说'你们系统杀我进程'。十次里有八次是他们自己代码写得有问题,把进程优先级搞低了。但他们不知道怎么查——因为大部分应用开发根本不知道 oom_adj 这个东西的存在。」
第一步,我在测试机上复现。打开 App → 切到后台 → 等 30 秒 → 看进程还在不在。
在。
等 2 分钟。
没了。被杀了。
好,能复现。接下来抓数据。
# 先拿到进程 pid
adb shell ps -A | grep com.zhichu.app
# u0_a156 12847 1023 ... com.zhichu.app
# 看 oom_score_adj(注意不是 oom_adj,Android 7+ 用的是 oom_score_adj)
adb shell cat /proc/12847/oom_score_adj
# 输出:900900。
我愣了一下。这个数字意味着什么?
第一层:oom_adj 的数字到底代表什么
先把这张表刻进脑子里——这是 Android 14 的 ProcessList.java 里定义的常量:
// frameworks/base/services/core/java/com/android/server/am/ProcessList.java(Android 14)
static final int NATIVE_ADJ = -1000; // native 进程(init/zygote)
static final int SYSTEM_ADJ = -900; // system_server
static final int PERSISTENT_PROC_ADJ = -800; // 常驻进程(电话/蓝牙)
static final int FOREGROUND_APP_ADJ = 0; // 前台 Activity
static final int VISIBLE_APP_ADJ = 100; // 可见但非前台(如被对话框遮挡)
static final int PERCEPTIBLE_APP_ADJ = 200; // 用户可感知(如播放音乐)
static final int PERCEPTIBLE_LOW_APP_ADJ = 250; // 低感知
static final int BACKUP_APP_ADJ = 300; // 备份中
static final int HEAVY_WEIGHT_APP_ADJ = 400; // 重量级后台
static final int SERVICE_ADJ = 500; // 有 Service 在跑
static final int HOME_APP_ADJ = 600; // Launcher
static final int PREVIOUS_APP_ADJ = 700; // 上一个 App
static final int SERVICE_B_ADJ = 800; // 老旧 Service
static final int CACHED_APP_MIN_ADJ = 900; // cached 最低档 ← 我们在这
static final int CACHED_APP_MAX_ADJ = 999; // cached 最高档(最容易被杀)数字越大,越容易被杀。
我们的 App 切后台后 oom_score_adj = 900,意味着它被归类为 cached 进程——系统认为它"没有任何用户可感知的工作在做",内存一紧张就第一批被干掉。
但我们 App 明明有后台音乐播放啊?按理说应该是 200(PERCEPTIBLE_APP_ADJ)才对。
「等等——阿凯那个改动,把 Handler.postDelayed 换成了 AlarmManager……这跟 oom_adj 有什么关系?」
关系大了。
第二层:AMS 怎么计算 oom_adj——OomAdjuster 的核心逻辑
【老韩(AOSP 架构师视角)】:「OomAdjuster 是 AMS 里最复杂的模块之一—— computeOomAdjLSP这个方法超过 800 行。它的设计哲学是:进程的优先级不是静态的,是根据进程当前持有的'组件状态'动态计算的。你有前台 Activity?adj=0。你有前台 Service?adj=200。你什么都没有?adj=900。就这么简单粗暴。」
核心方法是 OomAdjuster.computeOomAdjLSP():
// frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java(Android 14)
private boolean computeOomAdjLSP(ProcessRecord app, int cachedAdj, ...) {
int adj;
int procState;
// 最高优先级:有前台 Activity
if (app.mState.getMaxAdj() <= FOREGROUND_APP_ADJ) {
adj = app.mState.getMaxAdj();
// ...
}
// 有前台 Service(startForeground 调过的)
if (app.mServices.hasForegroundServices()) {
adj = PERCEPTIBLE_APP_ADJ; // 200
procState = PROCESS_STATE_FOREGROUND_SERVICE;
}
// 有正在执行的 Service(被 bindService 绑定、或 onStartCommand 还没结束)
for (ServiceRecord s : app.mServices.getRunningServices()) {
if (s.startRequested) {
adj = Math.min(adj, SERVICE_ADJ); // 500
}
// 如果有 client 绑定了这个 Service,adj 取 client 的 adj
for (ConnectionRecord cr : s.getConnections()) {
ProcessRecord client = cr.binding.client;
// adj = min(adj, client.adj + 偏移)
}
}
// 什么组件都没有 → cached
if (adj >= CACHED_APP_MIN_ADJ) {
adj = cachedAdj; // 900-999
procState = PROCESS_STATE_CACHED_EMPTY;
}
// 写入 /proc/<pid>/oom_score_adj
ProcessList.setOomAdj(app.getPid(), app.info.uid, adj);
}看到没有?oom_adj 的值完全取决于你进程里当前有什么"活跃组件"。
有前台 Activity → 0 有前台 Service(调过 startForeground)→ 200有普通 Service 在跑 → 500 什么都没有 → 900+
第三层:真凶浮出水面
回到我们的 bug。
改动前:首页用 Handler.postDelayed 做定时刷新。Handler 持有 Activity 引用 → Activity 在后台虽然 stopped 但没被 finish → 进程里有一个"stopped 但未销毁的 Activity" → AMS 认为这是 PREVIOUS_APP(adj=700)。
700 虽然不算高,但比 900 好多了——PREVIOUS_APP 有一定保护,不会第一批被杀。
改动后:阿凯把 Handler.postDelayed 换成了 AlarmManager。AlarmManager 是系统服务,不持有 Activity 引用。改完之后,首页 Activity 在后台被系统回收了(onDestroy 被调了)。进程里没有任何活跃组件了——没有前台 Service、没有 Activity、没有 bound Service。
AMS 一算:这进程啥都没有。adj = 900。CACHED_APP_MIN_ADJ。
然后 lmkd 一看内存紧张,先杀 adj 最高的。我们排第一。
没了。
【Kevin(面试官视角)】:「这个 case 我面试时经常用来考人。我会问:'你的 App 切后台后被系统杀了,你怎么排查?'——能答出'先看 oom_score_adj 是多少、再用 dumpsys activity processes 看进程状态'的,不到 20%。大部分人只会说'加前台 Service',但不知道为什么要加、加了之后 adj 会变成多少。」
第四层:dumpsys activity processes 怎么读
这是排查 oom_adj 问题的核武器。我贴一段真实输出(脱敏后):
$ adb shell dumpsys activity processes
Process LRU list (sorted by oom_adj, 43 total):
PERS #42: sys F/ /PERS trm: 0 1023:system/1000 (fixed)
PERS #41: pers F/ /PERS trm: 0 1876:com.android.phone/1001 (fixed)
...
Proc #18: fore T/A/FGS trm: 0 8847:com.zhichu.music/u0a156 (fg-service)
Proc # 5: cch+5 B/ /CEM trm: 0 12847:com.zhichu.app/u0a156 (cch-empty)
...关键字段解读:
fore/ cch+5= 进程当前的 adj 级别(fore=前台,cch=cached)T/A/FGS= 有 Top Activity / 有 Activity / 有 Foreground Service B/ /CEM= Background / 无 Activity / Cached Empty trm: 0= trim level(内存回收级别)
看到了吗?我们的 App(pid 12847)状态是 cch+5 B/ /CEM——Cached Empty。系统认为它是个空壳。
而同一个 uid 下的音乐进程(pid 8847)状态是 fore T/A/FGS——有前台 Service,稳如老狗。
问题清楚了:我们的主进程和音乐进程是分开的。音乐进程有前台 Service 保护,但主进程没有。阿凯那一改,把主进程里最后一个"锚点"(Handler 持有的 Activity 引用)给拔了。
第五层:lmkd 的杀人逻辑
【老韩(AOSP 架构师视角)】:「Android 从 9 开始用用户态的 lmkd 替代了内核态的 lowmemorykiller 驱动。lmkd 通过 PSI(Pressure Stall Information)监控内存压力,压力超过阈值就开始杀进程。杀的顺序就是按 oom_score_adj 从高到低——900 的先死,0 的最后死。这个设计很朴素,但极其有效。」
lmkd 的核心逻辑在 system/memory/lmkd/lmkd.cpp:
// system/memory/lmkd/lmkd.cpp(Android 14,简化)
static int find_and_kill_process(int min_score_adj, ...) {
// 遍历 /proc 下所有进程
// 按 oom_score_adj 从高到低排序
// 杀掉 adj >= min_score_adj 的进程中 RSS 最大的那个
struct proc_info *procp = proc_get_heaviest(min_score_adj);
if (procp) {
kill(procp->pid, SIGKILL); // 直接 SIGKILL,不给机会
return procp->size;
}
return 0;
}注意——lmkd 杀进程是 SIGKILL,不是 SIGTERM。你的 App 不会收到任何回调,没有 onDestroy,没有 onLowMemory,直接死。
记忆点:lmkd 杀进程用 SIGKILL,不是 SIGTERM——App 收不到任何回调,没有 onDestroy,没有 onLowMemory,直接被杀。这就是为什么用户反馈"音乐突然断了"。
这就是为什么用户反馈"音乐突然断了"——进程被 SIGKILL 了,MediaPlayer 来不及做任何清理。
修复方案:三行代码
最终修复很简单:
// 在主进程的 Application.onCreate 里
if (!isMainProcess()) return;
// 启动一个前台 Service 维持进程优先级
Intent intent = new Intent(this, KeepAliveService.class);
startForegroundService(intent);加了一个前台 Service(带 notification),oom_adj 从 900 降到 200。
但这不是最优解。最优解是阿凯那个改动本身就有问题——他不应该把 Handler.postDelayed 换成 AlarmManager。正确做法是用 WorkManager 的 periodic work,它会在合适的时机唤醒进程,而不是让进程一直活着。
最终方案: 回滚阿凯的改动 + 用 WorkManager 替代 Handler.postDelayed + 音乐播放场景保留前台 Service。
三天。从发现问题到修复上线,三天。
本篇最小知识集
必须深刻理解:
- oom_adj 的数字含义
—— 0=前台,200=可感知(前台Service),500=普通Service,900+=cached(随时被杀)。数字越大越危险。 - AMS 根据"活跃组件"动态计算 adj
—— 有前台 Activity/Service/bound Service 的进程 adj 低,什么都没有的进程 adj 高。 OomAdjuster.computeOomAdjLSP()是核心方法。 - lmkd 按 adj 从高到低杀进程
—— SIGKILL,无回调,无清理机会。
记忆点:oom_adj 核心映射——0=前台 Activity,200=前台 Service(PERCEPTIBLE),500=普通 Service,900+=cached 空进程。AMS 的 computeOomAdjLSP 方法遍历进程所有组件,取最优(最低)adj 值。
边界性掌握:
PSI(Pressure Stall Information)是 lmkd 感知内存压力的机制 dumpsys activity processes的输出格式和字段含义 Android 9 从内核 LMK 驱动迁移到用户态 lmkd 的原因 PROCESS_STATE 和 oom_adj 是两个不同维度(前者影响后台限制,后者影响被杀优先级)
面试一答到底
Q1:什么是 oom_adj?AMS 怎么决定一个进程的 oom_adj?
A:oom_adj(实际写入 /proc/<pid>/oom_score_adj)是 AMS 给每个进程打的"重要性分数",lmkd 根据它决定内存紧张时先杀谁。AMS 通过 OomAdjuster.computeOomAdjLSP() 动态计算——核心逻辑是看进程里有什么活跃组件:有前台 Activity=0,有前台 Service=200,有普通 Service=500,什么都没有=900+。每次组件状态变化(Activity pause/resume、Service start/stop)都会触发重新计算。
Q2:lmkd 和旧版内核 LMK 有什么区别?
A:旧版 LMK 是内核模块(drivers/staging/android/lowmemorykiller.c),在内核 OOM 路径里直接杀进程。Android 9 开始迁移到用户态 lmkd——好处是:1)可以用 PSI 做更精细的内存压力感知(不只是看 free pages);2)用户态更容易调试和更新;3)可以做更复杂的策略(比如考虑进程的 RSS 大小,优先杀内存占用大的)。
Q3:前台 Service 为什么能保活?adj 具体是多少?
A:调了 startForeground() 的 Service 会让进程的 adj 降到 PERCEPTIBLE_APP_ADJ(200)。200 在 lmkd 的杀人列表里排位很靠后——只有系统极度缺内存时才会动它。但注意:Android 12+ 对前台 Service 有时间限制(FGS task manager),用户可以手动停止。Android 14 进一步要求声明 foregroundServiceType。
Q4:怎么排查"App 后台被杀"的问题?
A:三步走。1)adb shell cat /proc/<pid>/oom_score_adj 看当前 adj 值;2)adb shell dumpsys activity processes 看进程状态(是 fore/vis/svc 还是 cch);3)adb logcat -b events | grep am_kill 看系统杀进程的日志(会记录被杀进程的 adj 和原因)。如果 adj 异常高,就去查进程里是不是缺少活跃组件。
记忆点:排查 App 后台被杀的三板斧——1) cat /proc/pid/oom_score_adj 看数值;2) dumpsys activity processes 看进程状态;3) logcat -b events | grep am_kill 看杀进程日志。adj 异常高就去查缺少什么活跃组件。
Q5:一个进程同时有 Activity(stopped)和 Service(running),adj 取哪个?
A:取最低的(最有利的)。computeOomAdjLSP 的逻辑是从高到低逐步降 adj——先假设是 cached(900),然后看有没有 Service(降到 500),有没有可见 Activity(降到 100),有没有前台 Activity(降到 0)。最终取计算过程中的最低值。
钩子
今天把 AMS 的"生死大权"——oom_adj + lmkd——拆完了。到这里,AMS 的上下半场都讲完了:上半场是"怎么启动 Activity"(D8),下半场是"怎么决定进程生死"(今天)。
但 Activity 启动之后,用户看到的是什么?是窗口。
窗口是谁管的?WMS(WindowManagerService)。
明天 D10 进入 WMS 领域。我要画一张"一个 View 怎么显示到屏幕"的 7 层架构图——从你写的 TextView 开始,经过 DecorView → ViewRootImpl → WindowManager → WMS → SurfaceFlinger → HWComposer,最终变成屏幕上的像素。每一层我都会贴一段真实代码。
这条链路是 framework 面试的另一座大山。
下篇见。
夜雨聆风