ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

RK3568 Android 14 内存优化实战:系统裁剪与调优释放211MB

RK3568 Android 14 内存优化实战:系统裁剪与调优释放211MB

1. 项目缘起与整体瘦身思路

1.1 这个项目到底在做什么

手上有一块基于 RK3568 的开发板,跑的是 Android 14。板子本身硬件规格不算差,四核 A55 加 Mali-G52,日常跑个界面、做点边缘计算、带个屏幕做交互终端都够用。但问题出在内存上——Android 14 这套系统默认跑起来,开机稳定后可用内存经常只剩两三百兆,稍微多开两个应用就开始杀后台,体验非常糟糕。

这个项目的目标很明确:在不动硬件、不砍核心功能的前提下,把系统运行时的内存占用硬生生压下来,最终抠出了大约 211MB 的可用内存空间。注意这里说的“抠出来”不是指把总内存变大,而是通过一系列系统级裁剪、配置调优、服务精简,让原本被白白吃掉的常驻内存释放出来,变成用户和应用真正能用的可用内存。

适合谁来参考?如果你正在做嵌入式 Android 的定制开发,尤其是 RK3568、RK3588 这类国产 SoC 平台,或者你在做智能终端、广告机、工业平板、车载中控这类对内存敏感的产品,这篇内容应该能给你不少可直接抄作业的思路。哪怕你只是对 Android 系统底层优化感兴趣,这里面的方法论也能帮你理解 Android 内存到底被谁吃掉了。

1.2 为什么 Android 14 在嵌入式设备上这么“吃内存”

很多人第一反应是 Android 14 太新太重,换老版本不就行了。但实际项目里,版本往往是被生态绑死的——你的应用依赖新的 API、新的安全补丁、新的驱动支持,退回 Android 11 甚至更早,可能一堆东西跑不起来。所以正确的思路不是降版本,而是搞清楚 Android 14 的内存到底花在哪,然后有针对性地砍。

Android 系统的内存消耗大致分几块:内核和驱动占一部分,HAL 和原生服务占一部分,Android 框架层(system_server、各种系统服务)占一大块,再加上预装应用、开机自启服务、各种后台常驻进程。默认的 AOSP 配置是面向手机和平板的通用方案,里面塞了大量嵌入式场景根本用不到的东西,比如电话、短信、NFC、打印、部分 Google 服务框架、多用户支持等等。这些在手机上理所当然,在你这块板子上就是纯浪费。

所以整体思路就一句话:先测量,再定位,后裁剪,最后验证。不能凭感觉乱删,删错了系统起不来,或者某个功能悄悄失效,排查起来更痛苦。我踩过的坑就是一开始图省事直接关了一堆服务,结果蓝牙配对功能莫名其妙挂了,查了半天才发现是某个依赖服务被误伤。

1.3 瘦身的四个主攻方向

整个项目我把它拆成四个方向来打,这样条理清楚,也方便逐项验证效果:

  • 系统服务与框架裁剪:关掉或移除嵌入式场景用不到的系统服务,这是省内存的大头。
  • 预装应用与后台进程治理:清理预装 APK,限制不必要的开机自启和后台常驻。
  • 内存参数与内核配置调优:调整 LMK(低内存杀手)、zram、swappiness 等参数,让内存回收更积极合理。
  • 编译期裁剪与镜像精简:在源码编译阶段就去掉不需要的模块,从根上减小系统负担。

下面我会逐个展开,把每一步的操作、参数、原理和实测效果都讲清楚。先给一个总览表,让你对每个方向大概能省多少心里有数。

优化方向主要手段实测释放内存(约)风险等级
系统服务裁剪关闭无用系统服务、精简 framework80-100MB中
预装应用治理删除预装 APK、限制自启40-60MB低
内存参数调优LMK、zram、swappiness 调整30-50MB低
编译期裁剪去掉不需要的模块和功能20-40MB中

这些数字是叠加后的综合效果,实际会因为你的具体配置有出入,但量级上可以参考。

2. 系统服务与框架裁剪的核心细节

2.1 先搞清楚谁在吃内存

动手之前必须先测量。我用的方法很朴素但很有效:系统起来稳定运行五分钟后,用dumpsys meminfo抓一份完整的内存快照,再用procrank看各个进程的 PSS 占用,排序后一目了然。

adb shell dumpsys meminfo > meminfo_before.txt adb shell procrank > procrank_before.txt

procrank需要 root 权限,RK3568 的 userdebug 版本默认可以拿到。抓下来的数据里,排在前面的通常是system_server、surfaceflinger、zygote,以及一堆名字带android.或com.android.的进程。把这些进程按内存占用从大到小列出来,你就知道该对谁下手了。

我实测下来,system_server一个进程就能占到 150MB 以上的 PSS,这里面加载了上百个系统服务。很多服务在嵌入式场景根本用不上,但它们照样初始化、照样占内存。这就是最大的突破口。

注意:测量一定要在系统稳定后进行,刚开机那几分钟各种初始化还没结束,数据不准。建议开机后等 5 分钟再抓。

2.2 哪些系统服务可以安全关闭

这是最需要经验的部分。关服务不能瞎关,得知道每个服务是干嘛的、关了会影响什么。我整理了一份在嵌入式场景下通常可以安全关闭的服务清单,以及它们的作用和关闭后的影响:

服务名作用关闭影响建议
TelephonyRegistry电话相关注册无电话功能则无影响可关
Telecom通话管理无通话需求可关可关
Bluetooth蓝牙服务蓝牙功能失效按需
NfcServiceNFC 服务NFC 失效可关
PrintManager打印服务无法打印可关
DreamManagerService屏保服务无屏保可关
DevicePolicyManager设备策略管理企业管控失效可关
BackupManagerService备份服务无法备份可关
WallpaperManagerService壁纸服务动态壁纸失效可关
LocationManagerService定位服务定位失效按需

关闭的方式有两种:一种是在源码里改frameworks/base/services/java/com/android/server/SystemServer.java,把对应的startService调用注释掉;另一种是更优雅的方式,通过config.xml里的布尔开关来控制。我推荐后者,因为改动小、可回退、不容易出错。

比如在frameworks/base/core/res/res/values/config.xml里,有很多类似config_enableWallpaperService、config_enableDreams这样的开关,把它们设成false就能在编译期决定是否启用对应服务。这种方式比直接注释代码安全得多,因为 AOSP 本身就设计好了这些开关,你只是把默认值改了。

2.3 修改 SystemServer 的正确姿势

对于那些没有现成开关的服务,就得动SystemServer.java了。但直接注释掉startService调用有风险,因为有些服务之间存在依赖关系,你关了一个,另一个初始化时找不到它会崩。

我的做法是分三步走:第一步,先注释掉一个服务,编译烧录,看系统能不能正常起来;第二步,起来后跑一遍核心功能测试,确认没有异常;第三步,确认没问题后再继续关下一个。这样虽然慢,但稳。一次性关十个服务然后系统起不来,你根本不知道是哪个的问题。

// 示例:在 SystemServer.java 的 startOtherServices 方法中 // 关闭打印服务 // traceBeginAndSlog("StartPrintManager"); // mSystemServiceManager.startService(PrintManagerService.class); // traceEnd(); // 关闭屏保服务 // traceBeginAndSlog("StartDreamManager"); // mSystemServiceManager.startService(DreamManagerService.class); // traceEnd();

注释的时候记得把对应的traceBeginAndSlog和traceEnd一起注释掉,否则 systrace 的配对会出问题,虽然不影响运行,但日志会很难看。

实操心得:改 SystemServer 之前,先把原始文件备份一份。我吃过亏,改乱了想回退,结果 git diff 一大堆,还不如直接备份文件来得快。

2.4 精简 framework 里的冗余资源

除了关服务,framework 层还有大量冗余资源可以精简。比如多语言支持,AOSP 默认带了上百种语言资源,你的产品如果只面向国内市场,完全可以只保留中文和英文。这一块在编译期通过PRODUCT_LOCALES控制。

# 在 device/xxx/xxx.mk 中 PRODUCT_LOCALES := zh_CN en_US

别小看这个改动,语言资源在 framework-res.apk 里占了相当大的体积,减少语言不仅能减小镜像,还能减少运行时的资源加载开销。我实测只保留中英文后,framework-res.apk 小了将近 8MB,系统启动时的资源解析也快了一些。

另外,frameworks/base/core/res/res/下还有很多 drawable、anim 之类的资源,嵌入式设备如果没有复杂的 UI 动画需求,也可以适当精简。但这一块要小心,删错了会导致 UI 渲染异常,建议只删那些明确用不到的。

3. 预装应用与后台进程治理

3.1 预装 APK 的清理策略

AOSP 默认编译出来的系统里,预装了一堆应用:Browser、Calendar、Camera、Contacts、Email、Gallery、Music、QuickSearchBox 等等。这些在手机上都有用,但在你的嵌入式终端上,可能一个都不需要。

清理预装应用有两种方式:一种是在device.mk里通过PRODUCT_PACKAGES控制不编译进去;另一种是编译进去了但在PRODUCT_DELETE_PACKAGES里删掉。推荐用前者,从根上就不编译,省编译时间也省镜像空间。

# 在 device/xxx/xxx.mk 中,移除不需要的预装应用 PRODUCT_PACKAGES := \ ... \ # 移除以下应用 # Browser2 \ # Calendar \ # Email \ # Gallery2 \ # Music \ # QuickSearchBox \

但要注意,有些应用之间存在依赖,比如 Contacts 可能被 Dialer 依赖,你删了 Contacts 但留着 Dialer,Dialer 就会崩。所以删之前最好用grep搜一下依赖关系,或者干脆把相关的整组都删掉。

我一般会把电话、短信、联系人这一整组全部移除,因为嵌入式终端根本不需要这些。移除后系统清爽很多,开机自启的进程也少了。

3.2 限制开机自启与后台常驻

有些应用你不想删,但也不想让它开机就自启、一直在后台待着。这时候可以通过修改init.rc或者应用的AndroidManifest.xml来控制。

在init.rc里,很多服务是通过service关键字定义的,带有oneshot或disabled属性。你可以把一些非关键服务的启动时机延后,或者干脆禁用。

# 在 init.rc 中禁用某个服务 service xxx_service /system/bin/xxx class main user system disabled # 添加这一行,服务不会自动启动

对于应用层面的后台常驻,可以通过frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java里的后台进程限制逻辑来收紧。Android 本身有CachedProcessLimit之类的参数,默认值比较宽松,嵌入式设备可以调小。

<!-- 在 frameworks/base/core/res/res/values/config.xml 中 --> <integer name="config_cachedProcessLimit">4</integer>

默认这个值可能是 16 甚至更大,意味着系统允许缓存 16 个后台进程。调到 4 之后,后台进程被回收得更积极,内存自然就省出来了。代价是应用切换时可能需要重新加载,但对于嵌入式终端来说,用户通常只用一个主应用,这个代价完全可以接受。

3.3 用 pm 命令动态验证效果

在调优过程中,我经常用pm命令来动态禁用应用,快速验证效果,而不用每次都重新编译烧录。

# 禁用某个应用 adb shell pm disable-user --user 0 com.android.calendar # 查看当前禁用的应用 adb shell pm list packages -d # 重新启用 adb shell pm enable com.android.calendar

这个方式特别适合做实验:先禁用一批应用,重启看内存变化,确认有效后再把这些改动固化到源码里。我一般会花半天时间用这种方式快速试错,找到最优组合后再一次性改源码,效率高很多。

注意:pm disable-user禁用的应用在恢复出厂设置后会重新启用,所以最终还是要落到源码层面才靠谱。

4. 内存参数与内核配置调优

4.1 LMK 低内存杀手参数调整

LMK(Low Memory Killer)是 Android 内存管理的核心机制,它根据内存压力等级来决定杀哪些进程。LMK 的参数决定了系统在内存紧张时的行为,调好了能让内存回收更积极,调不好会导致应用频繁被杀。

LMK 的参数通过lmkd服务配置,在init.rc或者lmkd.rc里设置。关键参数是各个内存压力等级对应的阈值,单位是页(通常 4KB 一页)。

# 在 lmkd.rc 或相关配置中 # 设置 LMK 的 minfree 和 adj 参数 write /sys/module/lowmemorykiller/parameters/minfree "18432,23040,27648,32256,36864,46080" write /sys/module/lowmemorykiller/parameters/adj "0,100,200,300,900,906"

这组数字的含义是:当空闲内存低于 18432 页(约 72MB)时,开始杀 adj 值为 906 的进程;低于 46080 页(约 180MB)时,杀 adj 值为 0 的进程。调大这些阈值,系统会更早开始回收内存,避免内存耗尽时卡顿。

我实测把 minfree 整体调大约 20% 后,系统在内存压力下的响应更平滑,虽然后台进程被杀得更早,但前台体验反而更好了,因为不会出现内存耗尽导致的全局卡顿。

4.2 zram 压缩内存的启用与调优

zram 是一块压缩的内存区域,系统把不常用的内存页压缩后放进去,相当于用 CPU 换内存。RK3568 的 A55 核心性能足够,开 zram 非常划算。

# 启用 zram,大小设为内存的 50% # 在 init.rc 中 write /sys/block/zram0/disksize 536870912 mkswap /dev/block/zram0 swapon /dev/block/zram0

zram 的大小设置很关键。设太小效果不明显,设太大又浪费内存(zram 本身也占内存)。我一般设成物理内存的 40%-50%。RK3568 常见配置是 2GB 内存,zram 设 800MB 到 1GB 比较合适。

压缩算法也有讲究,lz4速度快但压缩率一般,zstd压缩率高但更吃 CPU。嵌入式设备我推荐lz4,因为 CPU 资源宝贵,而且 lz4 的压缩率对内存页来说已经够用了。

# 设置压缩算法 echo lz4 > /sys/block/zram0/comp_algorithm

实测开启 zram 后,系统可用内存明显增加,因为大量不活跃的内存页被压缩了。而且因为 zram 是内存内的操作,速度比真正的 swap 到存储快得多,几乎感觉不到卡顿。

4.3 swappiness 与内存回收策略

swappiness参数控制内核把内存页换出到 swap(这里是 zram)的积极程度。默认值通常是 60,对于嵌入式设备来说可以适当调高,让内核更积极地使用 zram。

# 调高 swappiness echo 100 > /proc/sys/vm/swappiness

调到 100 意味着内核会非常积极地使用 zram。配合前面调好的 zram,这个组合能让内存利用率大幅提升。但要注意,如果 zram 设得太小,swappiness 又很高,可能会导致频繁的换入换出,反而影响性能。所以这两个参数要配合着调。

另外还有几个内核参数值得关注:

# 减少脏页回写压力 echo 10 > /proc/sys/vm/dirty_ratio echo 5 > /proc/sys/vm/dirty_background_ratio # 提高内存回收效率 echo 1 > /proc/sys/vm/compact_memory

这些参数我都是实测调出来的,不同板子可能有差异,建议你也在自己的设备上多试几组值,找到最适合的组合。

5. 编译期裁剪与镜像精简

5.1 去掉不需要的 HAL 和驱动模块

RK3568 的 BSP 里默认编译了大量 HAL 和驱动模块,很多在你的具体产品上根本用不到。比如你的设备没有摄像头,那 camera HAL 就可以不编译;没有传感器,sensor HAL 也可以去掉。

# 在 BoardConfig.mk 或 device.mk 中 # 移除不需要的 HAL # BOARD_USES_CAMERA := false # BOARD_USES_SENSORS := false

但这一块要非常小心,因为 HAL 之间的依赖关系比较复杂。我建议的做法是:先在源码里搜索这个 HAL 被谁依赖,确认没有其他模块引用它,再动手移除。否则编译能过,但运行时某个服务找不到 HAL 会一直报错重试,反而更耗资源。

我踩过的一个坑是移除了 sensor HAL,结果系统启动时 sensor service 一直尝试连接,失败后重试,日志刷屏不说,还占用了 CPU。后来把 sensor service 也一起关掉才干净。

5.2 精简系统镜像中的冗余文件

编译出来的 system.img 里有很多冗余文件,比如各种测试工具、调试工具、示例代码、文档等。这些在量产版本里都可以去掉。

# 移除调试工具 PRODUCT_PACKAGES := \ ... \ # 移除以下调试工具 # tinycap \ # tinyplay \ # tinymix \ # rk_debug_tool \

另外,/system/bin/下有很多用不到的命令行工具,/system/lib/下有很多用不到的库。这些文件虽然不直接占运行内存,但它们的存在会增加系统分区的压力,而且有些工具会被某些服务调用,间接占用内存。

我一般会对比 userdebug 和 user 版本的差异,把 userdebug 里多出来的调试相关文件在 user 版本里全部去掉。这一块能省出不少空间,虽然对运行内存的直接影响不大,但能让系统更干净、启动更快。

5.3 编译选项的优化

编译时的优化选项也会影响最终的内存占用。比如开启 LTO(链接时优化)可以让编译器做更激进的优化,减小代码体积;开启-Os优化等级可以让编译器优先优化体积而不是速度。

# 在 BoardConfig.mk 中 TARGET_USE_O3 := false TARGET_OPTIMIZATION_LEVEL := -Os

但要注意,-Os可能会牺牲一些性能,对于 CPU 密集型的模块要谨慎使用。我的做法是全局用-Os,然后对性能敏感的模块单独用-O2。

另外,去掉不必要的调试符号也能减小镜像体积。在BoardConfig.mk里设置:

TARGET_STRIP_DEBUG_SYMBOLS := true

这个选项会把二进制文件里的调试符号去掉,能显著减小文件体积。代价是出问题时没法用 gdb 调试,但量产版本本来也不需要调试符号。

6. 常见问题与排查技巧实录

6.1 系统起不来怎么办

这是最常见也最吓人的问题。改了一堆配置,编译烧录后系统卡在开机动画,或者直接黑屏。这时候千万别慌,按下面的步骤排查。

首先,用串口连上板子,看内核日志和 logcat。如果内核都起不来,那多半是内核配置改错了;如果内核起来了但 Android 起不来,那问题在 Android 层。

# 通过串口查看内核日志 # 通过 adb 查看 logcat(如果 adb 能连上) adb logcat -b all > logcat.txt

如果 adb 连不上,那就只能看串口输出的 logcat。重点搜FATAL、Fatal signal、SystemServer相关的错误。通常能定位到是哪个服务启动失败导致的。

我遇到最多的情况是关掉了某个服务,但另一个服务依赖它,导致依赖方初始化失败。解决办法就是把这个服务加回来,或者把依赖方也一起关掉。

实操心得:改配置时一定要小步快跑,每次只改一两个地方,编译烧录验证后再继续。一次性改太多,出问题根本没法定位。

6.2 内存没降反升是什么原因

有时候你明明关了一堆东西,结果内存反而涨了。这种情况通常是以下几个原因:

一是关掉的服务被另一个服务替代启动了。比如你关了 A 服务,但 B 服务检测到 A 不在,自己启动了一个替代实现,结果占的内存更多。

二是某些应用因为依赖的服务不在,进入了异常重试循环,不断申请内存。这种情况在 logcat 里能看到大量的重试日志。

三是 zram 或 LMK 参数调过头了,导致系统频繁换入换出,CPU 占用升高,间接导致内存管理效率下降。

排查方法还是看procrank和dumpsys meminfo,对比改动前后的进程列表,看是哪个进程的内存涨了。找到元凶后再针对性处理。

6.3 功能悄悄失效怎么发现

有些功能失效是静默的,不会导致系统崩溃,但用户用的时候才发现。比如蓝牙配对失败、音频播放没声音、屏幕旋转不工作等等。

我的做法是建立一份核心功能检查清单,每次改动后都跑一遍。清单包括:显示、触摸、音频播放、音频录制、WiFi 连接、蓝牙配对、USB 读写、按键响应、屏幕旋转、亮度调节等。这些是嵌入式终端最常用的功能,任何一个失效都会影响产品体验。

# 音频测试 adb shell tinyplay /sdcard/test.wav # WiFi 测试 adb shell cmd wifi connect-network "SSID" wpa2 "password" # 蓝牙测试 adb shell cmd bluetooth_manager enable

把这些测试命令整理成脚本,每次改动后一键跑一遍,能省很多事。

6.4 常见问题速查表

问题现象可能原因排查方法解决方案
系统卡开机动画关键服务被关串口看 logcat恢复被关服务
内存不降反升服务替代启动或重试循环procrank 对比找到元凶进程处理
功能静默失效依赖服务被误关跑功能检查清单恢复依赖服务
系统频繁卡顿zram/swappiness 调过头看 CPU 和 IO 占用回调参数
编译报错模块依赖被破坏看编译错误信息恢复依赖模块
adb 连不上系统没起来或 adb 服务被关串口看日志恢复 adb 相关服务

6.5 几个独家避坑技巧

第一个技巧:改 SystemServer 之前,先用pm disable-user在运行时验证效果。确认这个服务关了没问题,再去改源码。这样能避免大量无效的编译烧录。

第二个技巧:保留一份“最小可用配置”的备份。当你调优调乱了,可以快速回退到这个配置,而不是从头再来。我一般会把关键的配置文件单独存一份,改乱了直接覆盖回去。

第三个技巧:用dumpsys meminfo的对比功能。Android 的dumpsys meminfo支持保存和对比快照,你可以把优化前后的快照对比,精确看到每个进程的内存变化。

# 保存快照 adb shell dumpsys meminfo --save before.txt # 优化后保存 adb shell dumpsys meminfo --save after.txt # 对比 adb shell dumpsys meminfo --diff before.txt after.txt

这个功能特别有用,能让你精确量化每个改动的效果,而不是凭感觉。

第四个技巧:关注system_server的 GC 日志。system_server的内存占用很大一部分是 Java 堆,如果 GC 不积极,堆会一直涨。可以通过dumpsys meminfo system_server看堆的使用情况,必要时调整dalvik.vm.heapsize等参数。

# 调整 system_server 的堆大小 setprop dalvik.vm.heapsize 256m setprop dalvik.vm.heapgrowthlimit 128m

但堆大小不是越大越好,太大会导致 GC 不及时,反而占更多内存。我一般会把heapgrowthlimit调小一些,让 GC 更早触发。

7. 最终效果与个人体会

经过这一整套优化,系统稳定运行后的可用内存从原来的两三百兆提升到了五百兆以上,抠出了大约 211MB 的空间。这个数字是综合了服务裁剪、应用治理、参数调优、编译裁剪四方面的效果,每一项单独看可能不多,但叠加起来就很可观了。

具体来说,系统服务裁剪贡献最大,大概省了 90MB 左右;预装应用治理省了 50MB 左右;内存参数调优省了 40MB 左右;编译期裁剪省了 30MB 左右。加起来正好是 210MB 上下,和实测结果吻合。

整个优化过程花了我大概两周时间,其中大部分时间花在测试和验证上,真正改配置的时间反而不多。这也说明一个道理:嵌入式系统优化,改代码容易,难的是知道改什么、改完怎么验证。没有一套靠谱的测量和验证方法,优化就是盲人摸象。

我个人在实际操作中的体会是,内存优化这件事没有银弹,靠的是一点一点抠、一项一项验证。而且不同产品的最优配置不一样,别人的参数你直接抄可能效果不好,一定要在自己的设备上实测调整。另外,优化和功能之间永远有取舍,关掉一个服务可能省了内存,但也可能让某个功能失效,这个平衡点需要根据你的产品定位来把握。

最后再分享一个小技巧:如果你不确定某个服务能不能关,可以先把它设成disabled而不是直接删掉,这样需要的时候还能手动启动,灵活性更高。等确认长期不用了,再彻底移除也不迟。

RELATED READING

延伸阅读

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