23、车载内存优化:内存泄漏检测(LeakCanary/MAT)、LMK与OOM策略定制、ZRAM与Swap优化、内存碎片整理
车载系统的内存,说实话,是个老大难问题。
我最早接触车载项目时,总觉得手机上的那套内存优化经验可以直接搬过来。结果呢?车机跑着跑着,导航卡死、音乐断流、倒车影像延迟……用户直接投诉到车厂。后来我才明白,车载内存优化的核心逻辑和手机完全不同——手机可以杀后台,车机你敢杀吗?仪表盘和中控屏的进程,一个都不能挂。
这一章,我们就来聊聊车载内存优化的四个关键方向:泄漏检测、LMK/OOM策略、ZRAM/Swap、碎片整理。说白了,就是怎么让车机在有限的物理内存下,跑得稳、跑得久。
核心观点:车载内存优化的目标不是“省内存”,而是“保证关键进程不挂”。一切策略都要围绕这个目标展开。
23.1 内存泄漏检测:LeakCanary 与 MAT 实战
内存泄漏,说白了就是对象该回收时没被回收。在车载系统里,泄漏的后果比手机严重得多——手机重启一下就行,车机你敢让用户重启吗?
LeakCanary 的使用
我个人习惯在开发阶段直接集成 LeakCanary 2.x。它不需要手动初始化,加个依赖就行:
// build.gradle
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
启动后,LeakCanary 会自动监控 Activity、Fragment、ViewModel 等生命周期对象。一旦发现泄漏,它会生成一个 heap dump 并展示泄漏链。
我的经验:在车载项目中,我遇到过最典型的泄漏是「单例持有 Context」。比如一个全局的导航管理类,持有了 Activity 的 Context,导致 Activity 退出后无法回收。LeakCanary 直接定位到了那个单例的引用链,省了我大量排查时间。
MAT 深度分析
LeakCanary 适合日常开发,但遇到复杂泄漏(比如匿名内部类、Handler 消息队列),就需要 MAT 上场了。
MAT 的使用流程我简单说一下:
- 用
adb shell am dumpheap <pid> /data/local/tmp/heap.hprof导出堆快照 - 用 Android Studio 的 Profile 工具或 MAT 打开 hprof 文件
- 查看 Histogram,按对象数量排序,找异常多的类
- 右键查看 Merge Shortest Paths to GC Roots,排除软/弱引用
举个例子,我曾经发现 Message 对象数量异常多,追下去发现是一个 Handler 在后台线程不断发送延迟消息,而该 Handler 所在的 Activity 已经销毁了。这就是典型的「Handler 泄漏」。
注意:在车载系统上 dump heap 要小心。heap dump 本身会触发一次 GC,如果内存已经紧张,dump 过程可能导致系统卡死。我建议在系统空闲时(比如车辆熄火后)进行离线分析。
23.2 LMK 与 OOM 策略定制
LMK(Low Memory Killer)是 Android 内核层的内存回收机制。它根据进程的 oom_adj 值来决定杀谁。车载系统的问题在于:默认的 LMK 阈值对车机不友好。
LMK 阈值调参
默认的 LMK 阈值(比如 /sys/module/lowmemorykiller/parameters/minfree)是按手机场景设计的。手机可以杀后台应用,但车机上的导航、蓝牙电话、倒车影像都是前台进程,不能随便杀。
我建议的做法是:
- 提高系统进程(system_server、surfaceflinger)的 oom_adj 值,让 LMK 最后才杀它们
- 降低第三方应用的 oom_adj,让它们成为优先回收对象
- 调整 minfree 阈值,给关键进程留出更多内存余量
// 示例:设置 system_server 的 oom_adj 为 -12(不可杀)
echo -12 > /proc/$(pidof system_server)/oom_adj
// 设置导航进程的 oom_adj 为 -8(极低优先级被杀)
echo -8 > /proc/$(pidof com.example.navi)/oom_adj
核心原则:车载系统的 LMK 策略应该是「保关键进程,杀非关键进程」。不要一刀切地使用手机默认配置。
OOM 策略定制
当 LMK 都来不及回收时,系统会触发 OOM(Out Of Memory)。默认的 OOM killer 行为是随机杀进程,这在车机上绝对不行。
我曾在项目中定制过 OOM 策略:
- 在 kernel 层修改
oom_badness()函数,给车载关键进程(如仪表盘、HMI)增加权重 - 在用户空间注册一个
lowmemorykiller监听器,当内存低于阈值时,主动释放缓存和后台服务
// 内核层修改示例(简化)
unsigned long oom_badness(struct task_struct *p, ...) {
if (is_car_critical_process(p)) {
return 0; // 永远不被 OOM killer 选中
}
return default_oom_badness(p);
}
警告:修改内核 OOM 策略需要谨慎。我曾经因为把某个系统服务设为「不可杀」,导致内存耗尽后系统完全死锁。建议保留一个「最后防线」——比如让 watchdog 在内存极低时主动重启非关键服务。
23.3 ZRAM 与 Swap 优化
ZRAM 是用压缩内存来模拟交换空间。说白了,就是把不常用的内存页压缩后存起来,腾出物理内存给活跃进程用。车载系统内存小,ZRAM 几乎是必选项。
ZRAM 配置
ZRAM 的大小和压缩算法需要根据硬件调整。我一般这样配置:
// 设置 ZRAM 大小为物理内存的 25%
echo 512M > /sys/block/zram0/disksize
// 选择 lz4 压缩算法(比 lzo 更快)
echo lz4 > /sys/block/zram0/comp_algorithm
// 启用 ZRAM
mkswap /dev/block/zram0
swapon /dev/block/zram0
为什么选 lz4?因为车载系统对延迟敏感。lz4 压缩率略低,但解压速度比 lzo 快 30% 左右。你想想看,如果用户正在操作导航,突然因为换页导致卡顿,体验会很差。
我的经验:ZRAM 不是越大越好。我见过有人把 ZRAM 设为物理内存的 50%,结果压缩和解压占用了大量 CPU,导致系统整体响应变慢。建议控制在 20%-30% 之间。
Swap 分区
如果车机有 eMMC 或 UFS 存储,可以考虑使用 Swap 分区作为二级交换。但要注意:闪存的写入寿命有限,频繁 swap 会加速存储老化。
我建议的做法是:
- 优先使用 ZRAM(内存压缩),不涉及闪存写入
- 只在内存极度紧张时(比如同时开启导航+音乐+语音助手)才启用 Swap
- 设置 swappiness 值(比如 10),让系统尽量少用 Swap
// 设置 swappiness 为 10(默认 60)
echo 10 > /proc/sys/vm/swappiness
23.4 内存碎片整理
内存碎片,说白了就是物理内存被分割成很多小块,导致无法分配大块连续内存。车载系统长时间运行后,碎片问题尤其严重。
碎片检测
通过 /proc/buddyinfo 可以查看内存碎片情况。我一般这样分析:
cat /proc/buddyinfo
// 输出示例:
// Node 0, zone Normal 123 45 12 3 1 0 0 0 0 0 0
数字从左到右代表 2^0 到 2^10 页的可用块数。如果高阶块(比如 2^4 以上)数量很少,说明碎片严重。
碎片整理策略
Android 内核提供了 compact_memory 接口,可以手动触发内存压缩:
echo 1 > /proc/sys/vm/compact_memory
但我不建议频繁调用。因为 compact 过程会锁住内存分配器,导致系统短暂卡顿。我一般在以下时机触发:
- 车辆熄火后(系统空闲)
- 应用安装/卸载完成后
- 系统升级后首次启动
避坑指南:我曾经在某个项目中,每 5 分钟执行一次 compact_memory,结果导致系统频繁卡顿。后来改为「碎片指数超过阈值时触发」,才解决问题。碎片指数可以通过 /proc/pagetypeinfo 计算。
长期运行建议
对于车载系统这种 7x24 小时运行的场景,我建议:
- 使用
vmalloc代替kmalloc分配大块内存(vmalloc 不要求物理连续) - 在应用层避免频繁创建和销毁大对象
- 定期(比如每天一次)在系统空闲时执行 compact
嗯,这一章的内容就到这里。内存优化是个系统工程,泄漏检测、LMK 策略、ZRAM 配置、碎片整理,每个环节都不能忽视。我在实际项目中,往往是从泄漏检测入手,先解决「内存去哪了」的问题,再逐步调整策略。希望这些经验对你有帮助。
一句话总结:车载内存优化的核心不是「省」,而是「保」——保住关键进程,保住用户体验。