21、低内存场景优化:LMKD与图形内存回收、低内存下合成策略降级、OOM预防
低内存场景,说白了就是系统快撑不住了。你想想看,手机内存就那么大,App越开越多,图形缓冲区越占越满。这时候如果不做点什么,系统就会直接杀掉前台应用——用户正在打游戏、看视频,突然闪退,那体验就彻底崩了。
我这些年调试过不少低内存引发的图形问题。有一次线上反馈说某款手机在微信视频通话时,切到后台再回来,画面就卡住不动了。查到最后,就是LMKD(Low Memory Killer Daemon)在回收图形内存时,没有处理好缓冲区的生命周期。嗯,今天我们就来聊聊这块。
21.1 LMKD与图形内存回收
LMKD是Android 10之后引入的内存管理守护进程。它替代了旧的lowmemorykiller驱动,能更精细地控制内存回收策略。说白了,它就是个「内存警察」,时刻盯着系统还剩多少内存,一旦低于阈值,就开始抓人——杀掉进程、回收内存。
但图形内存回收有个特殊之处。GPU缓冲区、SurfaceFlinger的Layer、GraphicBuffer这些资源,不是简单的kill进程就能释放干净的。它们有引用计数,有同步 fence,处理不好就会导致画面撕裂、黑屏甚至死机。
核心要点:LMKD在回收图形内存时,必须走一套「温和」的流程——先通知SurfaceFlinger释放空闲缓冲区,再尝试压缩已缓存的图形数据,最后才考虑杀掉进程。
我个人习惯在调试时,先看LMKD的日志输出:
// 查看LMKD事件日志
adb logcat -b events | grep -i lmkd
// 查看内存压力等级
adb shell dumpsys meminfo --lmkd
// 查看SurfaceFlinger的缓冲区占用
adb shell dumpsys SurfaceFlinger --list
我在项目中遇到过一个问题:某款中端机型,在后台同时运行微信、抖音、淘宝三个App后,切回桌面就卡顿。查日志发现LMKD频繁触发medium级别的回收,但SurfaceFlinger的缓冲区占用一直降不下来。原因是什么?
嗯,是App没有及时释放离屏缓冲区。有些App在后台时,仍然持有大量GraphicBuffer,SurfaceFlinger无法强制回收。这时候LMKD只能走最后一步——杀掉进程。
调试技巧:可以用adb shell dumpsys gfxinfo查看每个App的图形内存占用。重点关注Graphics和GL两项,如果后台App的图形内存超过50MB,基本就是泄漏了。
21.2 低内存下合成策略降级
SurfaceFlinger的合成策略,正常情况下会优先使用GPU合成,因为效果更好、功耗更低。但在低内存场景下,GPU合成需要分配额外的渲染目标缓冲区,这反而会加剧内存压力。
所以,低内存时必须降级。降级策略主要有三种:
| 策略 | 正常模式 | 低内存模式 |
|---|---|---|
| 合成方式 | GPU合成(GLES/Vulkan) | HWC合成(硬件叠加层) |
| 缓冲区分配 | 双缓冲/三缓冲 | 单缓冲 + 直接提交 |
| Layer合并 | 按需合并 | 强制合并到最少Layer |
| 帧率控制 | 60fps/120fps | 降帧到30fps甚至15fps |
我曾经调试过一个案例:某款手机在低内存下,SurfaceFlinger仍然坚持使用GPU合成,结果导致分配缓冲区时OOM,直接触发内核的oom_killer,把前台App给杀了。用户反馈就是「玩着玩着突然回到桌面」。
解决方案其实不复杂:在LMKD触发medium级别回收时,SurfaceFlinger就应该主动降级到HWC合成。HWC合成由硬件完成,不需要额外分配GPU缓冲区,内存占用极低。
注意:HWC合成也有坑。有些低端芯片的HWC只支持4个Layer,如果App的UI层级太多,HWC合成会失败,回退到GPU合成。这时候内存压力反而更大。所以降级策略要结合硬件能力来判断。
代码层面,SurfaceFlinger的降级逻辑大致如下:
// SurfaceFlinger合成策略降级伪代码
void SurfaceFlinger::onMemoryPressure(int level) {
if (level >= MEM_PRESSURE_MEDIUM) {
// 降级到HWC合成
mUseGpuComposition = false;
// 减少缓冲区数量
mMaxBufferCount = 1;
// 强制合并Layer
mForceLayerMerge = true;
// 降低帧率
setDesiredFrameRate(30);
} else if (level >= MEM_PRESSURE_LOW) {
// 进一步收紧
mMaxBufferCount = 0; // 使用共享缓冲区
setDesiredFrameRate(15);
}
}
你想想看,降帧到15fps虽然画面会卡,但总比闪退强。用户能接受卡顿,但接受不了崩溃。这是典型的「保底」策略。
21.3 OOM预防
OOM(Out Of Memory)是最后一道防线。一旦触发OOM,内核会直接杀掉最「肥」的进程。但问题是,内核不知道哪个进程对用户最重要。它可能杀掉正在播放视频的App,或者杀掉输入法——然后用户就发现打不了字了。
所以,我们要在OOM发生之前,主动释放图形内存。我总结了一套预防策略:
- 预分配缓冲区池:在系统启动时,提前分配一批GraphicBuffer,放在池子里。低内存时直接从池子里取,避免分配失败。
- 主动压缩缓存:SurfaceFlinger会缓存一些纹理和Layer数据。低内存时,主动清空这些缓存,能释放几十MB内存。
- 限制App图形内存上限:每个App的图形内存占用不能超过总内存的15%。超过就通知App释放,或者直接降级它的合成策略。
- 使用MemoryTracker:Android 12之后引入了MemoryTracker,可以精确追踪每个进程的图形内存占用。LMKD可以基于这个数据做更精准的回收决策。
避坑指南:我曾经在某个项目里,为了预防OOM,把缓冲区池的大小设得太大——结果系统启动时就占用了200MB内存,反而导致低内存场景更早出现。后来我改成动态调整:根据当前可用内存,按比例分配缓冲区池大小。可用内存多就多分配,少就少分配。
还有一个容易被忽略的点:Fence同步。GPU在渲染时,会通过Fence机制同步缓冲区访问。如果Fence超时,缓冲区就一直被占用,无法回收。低内存场景下,Fence超时更容易发生,因为GPU调度变慢。
我建议在低内存时,主动缩短Fence超时时间。默认是1000ms,可以降到200ms。超时的缓冲区直接标记为「可回收」,即使GPU还在用——大不了画面撕裂,也比OOM强。
21.4 知识体系总览
下面这张图,是我梳理的低内存场景优化整体流程。你可以看到,从LMKD触发,到图形内存回收,再到合成策略降级,最后到OOM预防,是一条完整的链路。
这张图把整个流程串起来了。从LMKD触发开始,根据内存压力等级走不同的分支。轻度就释放空闲缓冲区,中度就降级合成策略,重度才杀进程。最终目标只有一个:别让OOM发生,保证前台App能继续运行。
嗯,低内存优化这块,说白了就是「取舍」。你要在用户体验和系统稳定性之间找平衡。图形内存回收不能太激进,否则画面卡顿;也不能太保守,否则OOM。这个度,需要结合具体硬件和场景来调。
我个人习惯是在项目初期就把LMKD的阈值调得保守一些,等稳定了再逐步收紧。这样即使有问题,也是先卡顿,不会直接崩溃。用户骂你卡,总比骂你闪退强——你想想是不是这个道理?