ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android应用oom_adj查看与后台进程被杀排查实操指南

Android应用oom_adj查看与后台进程被杀排查实操指南

做 Android 性能优化或者稳定性排查的兄弟,一定遇到过这类对话:“我明明退到桌面了,过一会儿再进,App 居然重新启动了”、“后台放着歌,走着走着就被切了”。应用层日志翻半天,除了用户操作记录啥也没找到。这时候问题往往不在应用层,而在系统对进程的优先级判断上。Android 里有一套专门给进程打分的机制,其中最关键的一个分数就是 oom_adj(也就是进程的 oom_score_adj,下面统称 adj 值)。它决定了当系统内存吃紧的时候,你的进程排在被杀的哪一位。

这篇文章就把“查看应用 oom_adj 值”这件事彻底讲透:从最基础的查看命令,到完整的观察记录怎么做,再到这些数值背后 AMS 和 lmkd 到底怎么工作。适合正在做系统稳定性、应用性能优化、以及排查后台进程被杀问题的同学,这篇文章就是你的实操参考。

1. 先搞懂 oom_adj 是什么:系统给每个进程打的“生存评分”

1.1 进程不是生来平等的,Android 必须给它们排座次

Android 本质上是一个单前台应用 + 一堆后台进程共存的操作系统。你手机里运行着十几个 App,但屏幕上用户真正在看的只有一个。内存是有限的,当剩余内存低于某个阈值时,系统必须杀掉一些进程来释放空间。问题来了:杀谁?

不能随机杀,也不能按启动顺序杀。Android 的做法是给每个进程算一个分数,分数越低代表这个进程越重要,越不该被碰;分数越高代表它越边缘化,内存不够时优先拿它开刀。这个分数就是 adj 值。

我见过很多做应用开发的同事第一次接触 adj 时一脸懵,觉得这是 ROM 开发的事。实际上当代 App 开发遇到的“后台被杀”“推送收不到”“进入最近任务后进程被回收”,根因全都跟 adj 值有关。你至少得能看懂这个数字,才能判断问题到底是系统策略、厂商策略还是自己应用没处理好。

1.2 adj 值的数值范围:从 -1000 到 +1000

现代 Android 系统里,真正生效的数值是 oom_score_adj,范围是 -1000 到 +1000。这个数值越小越安全。

不过你在老资料里看到的可能是另一种叫法:oom_adj,范围是 -17 到 15。那是 Linux 内核老版本留下来的东西。Android 的 ActivityManagerService(后面统一叫 AMS)在进程重要性变化时,会把换算后的 oom_score_adj 直接写入/proc/<pid>/oom_score_adj。建议实际查看时只看 oom_score_adj,不要用老的 oom_adj 节点,否则会陷入换算坑。

1.3 两个名字容易混淆:AMS 里的 mAdj、内核里真正用的 oom_score_adj

排查过程中你还会在dumpsys activity processes输出里看到一个字段叫 adj,或者 mAdj。它们和 oom_score_adj 是什么关系?

简单说,AMS 内部维护每个进程的 ProcessRecord,里面有个字段记录当前 adj,这个字段也叫 mAdj 或 curAdj。AMS 计算完这个值以后,会调用系统接口把它同步给内核,最后落在/proc/<pid>/oom_score_adj这个节点上。所以你用dumpsys看到的 adj 和直接读文件看到的 oom_score_adj,正常情况下是同一个值的两种呈现方式。

搞清楚这个关系有个实际意义:当你没法 root、读不了/proc时,还可以靠 dumpsys 曲线救国;当你看到 logcat 里出现adj 900这种信息时,也能马上反应过来它说的是什么。

2. 三条查看路径:从 adb 到内核节点,哪条适合你的设备

2.1 最常规操作:通过包名定位 PID,再读 oom_score_adj

先记住基础命令流程。第一步找到目标应用的进程号:

adb shell pidof com.example.app

如果 pidof 没输出,可以用关键词过滤:

adb shell ps -A | grep com.example.app

拿到 PID 之后,直接读内核节点:

adb shell cat /proc/12345/oom_score_adj

输出的就是一个数字,比如 0 代表前台应用,900 代表缓存后台进程。这个命令是我日常排查时用得最多的。

但这里有一个坑:普通量产机上,via adb shell 通常读不到别的应用进程的这个节点,会报 Permission denied。原因很简单,应用进程的敏感/proc信息对 shell 用户是不可见的,Android 从内核和 SELinux 策略上做了隔离。所以你会看到一个很奇怪的现象:模拟器上能读,自己的 root 设备能读,但用户的手机读不了。

2.2 量产机没有 root:用 dumpsys 曲线读取

如果你手上是没 root 的量产机,仍然有办法拿到 adj 值——用 dumpsys 看 AMS 的进程记录:

adb shell dumpsys activity processes | grep -B 5 -A 5 "com.example.app"

输出里会有一段进程信息,重点看adj=字段。在某些 Android 版本上这个字段可能是oom adj=或者curAdj=,本质都是一回事。

我实际测试过几个版本,Android 9 到 Android 14 的设备上,只要 dumpsys 能跑,就能看到这个字段。它的数值和直接读 oom_score_adj 是能对上的。

有个前提要注意:dumpsys 输出非常长,应用进程不止一个,最好先过滤包名,再过滤关键行,否则查找效率很低。

2.3 动态观察:写个循环脚本,连续记录 adj 值变化

单次查一次只能看到瞬时值,没什么分析价值。真正要做观察记录,得让 adj 值随时间变化跟状态切换一起输出。

我常用的办法是在 PC 端写一个小脚本,假设设备 adb 已经连好,目标 PID 是 12345:

adb shell "while true; do cat /proc/12345/oom_score_adj; sleep 0.5; done" | while read v; do echo "$(date +%H:%M:%S.%3N) adj=$v" done

输出效果类似:

19:23:01.021 adj=0 19:23:01.520 adj=0 19:23:02.020 adj=700 19:23:02.520 adj=700 19:23:03.520 adj=900

这里每 0.5 秒采样一次。采样间隔不用太短,0.3 秒到 0.5 秒足够捕捉大部分状态切换,太频繁反而会因为 adb 通道延迟导致数据混乱。

在观察期间,让另一个人负责操作手机,你只盯着屏幕输出就行。如果只有你一个人,建议把所有输出重定向到文件,操作完再看:

adb shell "while true; do cat /proc/12345/oom_score_adj; sleep 0.5; done" > adj_record.txt

观察结束按 Ctrl+C 停止,然后慢慢分析。

3. 一次完整的 oom_adj 观察记录:从冷启动到锁屏,数据到底怎么变

3.1 设计实验场景:别盲目操作,按状态切换脚本走

做观察记录最容易犯的错是“乱按一通,拿到一堆数据但说不清对应关系”。正确的做法是提前设计一套操作序列,覆盖典型进程状态。

我的标准操作序列是这样的:

  1. 冷启动应用,进入首页,停留 20 秒。
  2. 按 Home 键回桌面,立即观察 30 秒。
  3. 等 2 分钟,观察缓存后变化。
  4. 再次点击应用图标,切换回前台。
  5. 进入最近任务列表,让目标应用出现在屏幕上。
  6. 退出最近任务,回到应用前台。
  7. 播放一段音频(且使用前台服务),回到桌面。
  8. 锁屏,等待 5 分钟。

这一步的目的是覆盖 FOREGROUND_APP、VISIBLE_APP、PERCEPTIBLE_APP、PREVIOUS_APP、CACHED_APP 这些典型层级。

3.2 实测数据记录:我拿到的一组典型数据

下面是一组在 Android 13 + AOSP 类原生系统上抓到的数据,目标应用是一个普通的三方 App,不涉及厂商特殊后台管理:

操作/状态oom_score_adj对应进程层级
冷启动进入首页0FOREGROUND_APP,正在交互
首页停留 20 秒0无变化,前台
按 Home 回桌面,1 秒内700PREVIOUS_APP,系统保留最近任务
回桌面 30 秒900CACHED_APP_MIN,进入缓存回收队列
回桌面 2 分钟901~906CACHED_APP,LRU 排名逐渐靠后
再次点击图标回前台0恢复前台
进入最近任务列表100~200部分版本会提升到 VISIBLE/PERCEPTIBLE
锁屏后 10 秒200~900视进程是否有活跃组件而定
锁屏 5 分钟900+若无活跃组件,稳定为缓存进程
后台播放音频(前台服务)100 或 200取决于服务类型和版本实现

注意,不要直接照抄这组数据,因为具体数值受 Android 版本、厂商策略、应用是否持有唤醒锁等因素影响很大。这组数据的意义在于告诉你:adj 值并不是一成不变的,它会随着用户操作和系统判定上下跳动。

3.3 关键发现:为什么冷启动一瞬间不一定是 0?

你在观察脚本里可能会看到冷启动时 adj 先是 1001(UNKNOWN_ADJ),再过一会才变成 0。这是因为进程刚创建时,AMS 还没来得及对这个新进程做完整的优先级计算,只能先用一个未知占位值。等 Activity 真正可见,AMS 会触发 updateOomAdjLocked 重算,这时候才会变成 0。

如果你用脚本采样,会在启动瞬间看到 1001,但那不代表应用真的处于低优先级。看到这个数字时要结合当前操作阶段去理解,而不是直接判定“应用启动时会被杀”。

3.4 关键发现:回桌面后为什么不是立刻 900?

按 Home 回桌面,1 秒内读取到的值通常还是 700 而不是 900。这是 PREVIOUS_APP_ADJ 的作用:系统认为你可能马上要切回来,先让这个进程保持一个“准后台”的状态,既不占前台资源,也别太快被回收。

等这个“冷门期”过去,系统才会把进程降到 900 以上的缓存档位。这个过程不是瞬间完成的,可能几十秒到几分钟不等。很多做保活的同学看到退后台 30 秒内 adj 还是 700,就误以为“系统很保护我”,结果过了 2 分钟回来一看变成了 906,就懵了。观察记录要拉足时长,至少要覆盖 5 分钟以上。

4. 数值背后的机制:AMS 怎么打分,lmkd 怎么用这个分数杀人

4.1 状态决定数值:影响 adj 的几大因素

AMS 每次计算一个进程的 adj,不是看某个单一状态,而是综合整个进程里所有组件的情况打分。我总结下来以下几类因素影响最直接:

  • 是否有 Activity 处于 resumed 状态:有则前台,adj 为 0。
  • 是否有 Activity 可见但不在前台:adj 通常为 100。
  • 是否有前台 Service:adj 通常 100 或 200,取决于是否可感知。
  • 是否正在被用户感知(如播放媒体、录音、导航):adj 会落在 PERCEPTIBLE 档位(200,部分版本有 50)。
  • 是否刚被用户使用过,作为 Previous App:adj 700。
  • 是否只处于后台缓存状态:adj 900 以上。

所以如果你想提升应用在后台的存活率,核心逻辑就变成了:想办法让系统判定你的进程属于“前台服务”或“可感知”层级,而不是空后台。

4.2 为什么缓存进程还有 900 到 906 这么细的档位?

很多人不理解,既然都已经 900 了,为什么还要分 900、901、902、903……906?

AOSP 的 ProcessList 里,CACHED_APP_MIN_ADJ 是 900,CACHED_APP_MAX_ADJ 是 906。这个区间是给缓存进程按“最近最少使用”(LRU)排序用的。同样是后台缓存进程,排在前面(靠近 900)的说明刚被用过,排在后面(靠近 906)的说明更久没用了。

当系统内存不足需要杀缓存进程时,会优先从 906 甚至当前已经排到最大值的进程开始杀;如果还不够,再逐步往前推进。这就是为什么有些应用退后台后被杀,有些却能活到最后——不是因为应用本身有什么魔法,只是它在 LRU 队列里的位置不同。

4.3 lmkd 拿到数值之后,杀人的标准是什么?

内核不会每时每刻都清理进程。真正杀进程的工作主要由 lmkd(low memory killer daemon)完成。lmkd 会监听系统的内存压力事件,当内存低于阈值时,就会扫描当前所有进程的 oom_score_adj,找到分数最高、最适合杀的目标,然后通知内核把它干掉。

杀的时候会打日志,常见格式类似:

lowmemorykiller: Killing 'com.example.app' (12345), adj 900, to free 345678kB on behalf of 'lmkd'

或者在新版本上:

lmkd: Killed 'com.example.app' (12345), uid 10123, oom_score_adj 900 to free 345678kB

排查“为什么被杀”时,第一步就是到 logcat 里搜lmkd或lowmemorykiller关键字,看看系统记录的 adj 是多少、释放了多少内存。这一步能帮你区分是系统正常回收,还是厂商后台管理机制在干预。

4.4 adj 低不等于拿到免死金牌

很多文章会说“adj=0 的前台进程不会被杀”,这种说法在极端情况下不成立。

前台进程确实优先级最高,但当系统内存耗尽到连启动 system_server 都困难的程度时,内核内存管理逻辑会进入最后手段,连前台进程也逃不掉。只不过在绝大多数正常设备上,你已经把它调到 0,就不会在常规内存压力下被回收。

真正几乎永不被杀的,是 android:persistent 系统进程或者 adj 为 -800、-900 甚至 -1000 的核心进程。普通应用想都不要想把自己调成负值,乱改只会导致系统不稳定或者直接崩溃。

5. 从观察记录定位三个真实问题的排查思路

5.1 问题 A:退到后台就被杀,每次都很快

观察到的现象:应用退后台后 10 分钟再回来,冷启动。我用记录脚本抓到的数据是:回桌面 30 秒后 adj 已经变成 900,再往后 900 以上,内存水位较低时系统很快把它杀了。logcat 里能看到 lmkd 明确打印了 Kill。

这个问题的根源不是应用做错了什么,而是设备内存本身就紧张,系统优先回收高 adj 的缓存进程。此时优化方向有两类:

  • 让重要任务放到前台服务里,把 adj 拉回 100/200 档位。
  • 把不紧急的任务改成 WorkManager 或 JobScheduler 调度,不要在后台直接跑大任务。

注意区分:如果应用根本没有经常被用户重新打开的需求,那它被杀其实比保活更合理。不要无脑做保活。

5.2 问题 B:最近任务列表里能看到应用,进程照样被回收

有一次某开发者反馈:“我的应用都出现在最近任务列表里了,为什么还是被杀?”

我让他去观察进入最近任务前后的 adj 值。结果发现,目标应用虽然在最近任务界面能看到缩略图,但在部分 Android 版本上,仅仅“列表可见”并不会把进程提升到高优先级,实际读取到的 adj 可能只有 100 到 200。这个优先级比前台 0 要低得多,如果系统内存压力大,照样会被回收。

处理方案要看具体版本和厂商策略。很多国产 ROM 在最近任务里还加了“下拉锁定”功能,锁定后的进程 adj 会被抬到很低;AOSP 原生则没有这种设计。如果应用有必须保活的场景,不能依赖“最近任务可见”这种弱信号,还是得用前台服务。

5.3 问题 C:前台进程还是被切掉,但 lmkd 日志里什么都没有

这种情况下,观察到的 adj 一直为 0,logcat 里也没有 lmkd 的 kill 记录,但应用还是退出了。这时就要把怀疑方向从“内存回收”转向“主动强停”或“进程崩溃”。

查看系统日志有没有类似的记录:

ActivityManager: Killing ... (pid): user request

或者包管理相关的 force stop 记录。常见原因包括用户手动清除、系统电池优化策略强制停止、以及应用自身 ANR 后被系统判定无响应并关闭。此时再去看 adj 值已经没有意义,方向应该是崩溃日志和 ANR trace。

6. 实操中值得注意的几条经验,都是踩过的坑换来的

6.1 读 /proc 节点前,先确认权限

不同设备差异极大。模拟器、工程机、root 过的手机都能直接读;量产机大概率不行。如果第一步 cat 就报 Permission denied,别浪费时间,直接切到 dumpsys 方案。

另外要注意,观察时别开着开发者选项里的“不保留活动”或者把“后台进程限制”改成一两个进程。这些系统级限制会直接让你测出来的 adj 分布变得异常,所有数据都不可信。

6.2 厂商 ROM 的 adj 分布和 AOSP 对不上,必须实测

有些厂商会在自己的系统里改 AMS 逻辑,最典型的是把所有第三方后台 App 的 adj 或者在缓存队列里的排名统一调高,甚至直接拉满。你在 AOSP 资料上看到的 900 只是理论值,实际设备可能退后台几分钟就变成 1000。

换句话说,任何跟“后台被杀”相关的排查都要以当台设备的实测数据为准。网上搜到的数值分布可以参考,但别直接拿去和用户设备对比。

6.3 Android 新版不只靠 adj:PSI 信号和进程冻结也在参与

Android 较新版本的低内存回收逻辑不再简单看水位线,而是会参考 PSI(Pressure Stall Information)信号来感知内存压力。adj 仍然是核心指标,但已经不是唯一信号。这意味着:你观察到 adj=900,但不一定马上被杀;adj=900 的进程在某些场景下也可能被冻结而不是被杀。

冻结和被杀是两回事。Android 的缓存进程冻结机制会把后台进程放进 freezer,暂停其 CPU 调度。从用户角度看应用“死了”,但从进程角度看它还活着,解冻之后数据还在。处理这类问题时,建议结合/proc/<pid>/status里的 freezer 状态来判断。

6.4 给从未做过记录的新手的建议:先跑半小时基线数据

第一次做观察记录,不要上来就抱着“找问题”的目标。先把目标应用放在一台干净的设备上,跑一次我从第 3 节描述的标准操作序列,拿到一份完整的 adj 变化曲线。这份曲线就是你理解这台设备如何对待后台进程的基线。

后续再遇到生产过程问题,把基线和异常曲线放在一起对比,差异点一眼就能看出来。这个方法比直接搜 logcat 或者猜“是不是系统杀的”要精准得多,也是我过去几年里排查相关问题时最受用的手段。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进