
我手头这块 RK3568 开发板原厂固件是 64 位 AOSP11平时当作轻量 NAS 和广告机测试平台来用。装完基础服务后内存占用常年接近 2GB 这条红线经常出现一个 Java 应用起来就把 SurfaceFlinger 挤崩的情况。后来我把整套系统从 64 位改成 32 位 AOSP11最直观的变化是同样的一堆预装服务和后台进程内存占用下降了约 30%。这个降级过程不是简单的“再编译一次”里面涉及 AOSP 构建宏、内核配置、Bootloader 兼容、外设驱动库选型等一系列环节。这篇文章把我踩过的坑、实测数据、排查方法都整理出来给手里有 RK3566/RK3568 盒子、开发板、边缘计算网关的开发者作参考。1. 为什么要把 64 位降成 32 位项目思路和内存账本1.1 RK3566/RK3568 设备为什么会内存吃紧RK3566 和 RK3568 都是四核 Cortex-A55 架构主频大约 1.8GHz 到 2.0GHz定位很明确比树莓派那类板卡便宜又比低端 MCU 方案能干更多的活。RK3568 带 PCIe、双千兆网口、视频编解码器很多国产 NAS、边缘盒子、广告机、人脸闸机都靠它。RK3566 则更省成本多用在平板和低端盒子。这类设备的出货配置通常是 2GB 或 4GB LPDDR4。听起来 2GB 够用但 RK3568 的外设多摄像头 ISP、VPU、NPU、GPU 都要预留内存。芯片原厂把各模块的 Reserved Memory 写进 dts开机之后系统实际可用的内存就要再打个折。再加上 Android 11 之后的渲染管线、HWUI、SurfaceFlinger 对内存的胃口都不小最后留给应用层的内存经常只剩一半。市面上很多 RK3566/3568 方案出厂就给了 64 位系统。64 位听起来高级但对于只跑固定几个业务 APP 的商显设备、工业 HMI、简易 NAS 面板来说64 位带来的好处远小于内存压力。最典型的表现就是预装应用一多系统频繁杀后台切屏卡顿甚至触发 LMKD 的“疯狂回收模式”。1.2 32 位系统到底省了哪些内存很多人以为“64 位比 32 位快”其实在低负载业务场景里 64 位的主要代价是开销。在 AOSP 里同一个 Java 进程切到 32 位运行时省内存主要有五个来源第一指针变小。64 位用户空间里所有指针占 8 字节32 位占 4 字节。C 层 Binder 通信、系统服务里的 map、list、vector 一多指针体积直接翻倍的影响很可观。Bionic 库内部数据结构也受影响。第二进程地址空间布局差异。64 位进程的地址空间分配颗粒度更粗加载共享库时容易出现更长的 gap造成 RSS 偏高。32 位下进程的 VM 布局紧凑内存页利用率更高。第三ART 运行时的 AOT 和 JIT 缓存。同一个 APK 编译成 32 位 oat 文件机器码体积通常比 64 位小 20% 到 40%。尤其是 dex2oat 在做 speed-profile 编译时32 位产物更紧凑运行期装载到内存里的代码段体积自然更小。第四GPU 驱动和渲染库。Mali GPU 用户态 blobs 在 arm32 版本下生成的 Command Buffer 和 GEM Buffer 分配更小对同样分辨率的 UI 界面整体显存开销会低一些。第五Zygote 和系统 Server 的链式开销。SystemServer、SystemUI、Launcher 全部变成 32 位进程后它们的共享库驻留内存大幅缩小这就导致整个系统的匿名页统计显著降低。我一开始还不信能差 30%直到实测出来的数据让我确定了。1.3 哪些设备适合降级哪些不适合项目选型时必须想清楚一件事降级不是先知行为是权衡。适合降级的场景通常是设备只预装固定 APK不面向用户自由安装 App。比如广告机、门禁面板、简单 NAS UI、多路 RTSP 播放器。这些设备的 APK 都是用 32 位兼容包编写的或者干脆就是 Android 4.4 时代的老应用跑在 64 位系统上反而要经过兼容转换迁移到 32 位后内存效果好很多。不适合降级的场景是如果业务核心组件依赖 64 位动态库比如某些 ARM64-only 的 NPU SDK、音视频算法 so、高级安全模块那降级就得把这些库也替换成 arm32 版本工作量可能比降级本身还大。另一个死穴是如果设备需要安装高通类大型 3D 游戏或对内存无所谓的现代大应用32 位系统会限制应用本身地址空间到约 3GB反而容易 OOM。2. 动手之前的准备清单和风险评估2.1 硬件与工具要求在做任何代码改动之前先把手头的硬件和工具理清。以我这次操作为例核心硬件是 RK3568 开发板和一条 USB-C 数据线用于烧录一块 USB 转串口小板CH340 方案经过实测非常关键用于读取内核日志。如果只靠 ADB在系统起不来的情况下什么都查不了。编译主机我建议至少四核 CPU、16GB 内存、200GB 以上 SSD 剩余空间。AOSP11 编译链吃磁盘很凶临时文件在 out 目录里动辄几十 GB。Ubuntu 20.04 是目前 RK SDK 编译兼容性最好的宿主系统Ubuntu 22.04 也能用但需要自己处理 OpenJDK 版本和 python2 依赖折腾成本高。工具方面需要下载 RKDevToolWindows或 upgrade_toolLinux。我为了方便串口日志习惯用 Linux 主机编译、再切到 Windows 上用 RKDevTool 烧录不过 upgrade_tool 同样能做到。2.2 备份原始固件留好退路改装第一步永远是备份原厂固件。RK 平台不像别家那么容易全砖但操作失误时如果连 parameter 分区表都刷坏了恢复也很麻烦。操作方式先让开发板进入 Loader 模式。拔掉电源按住板子上的 Recovery 按钮不放插入 USB 线到电脑RKDevTool 会自动识别为“发现一个LOADER设备”。在“设备分区表”页把各个分区全选导出或者直接读取完整镜像保存成一个 img 文件。我建议至少导出 Boot、System、Vendor、Dtbo、Parameter、Uboot 这些分区后面不管是回退原厂还是对比差异都用得到。提示不要跳过备份操作。我前后刷过多次 RK3568有一次在测试 32 位内核时刷错了 dtb重启后串口完全无输出如果不是提前留了原厂固件只能靠短接 EMMC 进 Maskrom那又是另一套折腾。2.3 确认外设库是否有 arm32 版本这是最容易忽略的坑。RK3566/3568 的主板上往往带了摄像头 ISP、NPU、音频 Codec、显示面板。Rockchip 官方 SDK 里面很多 librkisp、rknpu、audio 相关预编译库既提供 arm64 版本也提供 arm32 版本但第三方板卡厂商添加的私有库就不一定了。拿 NPU 举例Rockchip 的 RKNN Toolkit 和 runtime 确实有 32 位 so但如果你用的是一个厂家封装的私有算法 SDK它内部只带了 arm64-v8a 的 so那你在 32 位系统里跑这个算法会直接报dlopen failed: library not found。所以在开工前把设备所有用到的硬件相关库列个清单逐个确认有没有 armeabi-v7a 版本。检查方式很简单用unzip -l app.apk | grep lib/看 APK 里的 so 文件夹如果只有arm64-v8a那这个 APK 基本跑不了 32 位系统。3. 实操把 AOSP11 从 64 位改成 32 位并完成编译3.1 获取 RK AOSP11 源码与 repo 同步Rockchip 基于 AOSP11 的 SDK 一般通过 repo 方式管理。最常用的拉取方式mkdir rk356x_aosp11 cd rk356x_aosp11 repo init -u https://github.com/rockchip-linux/manifests -b android-11.0 repo sync -c -j8这里的 manifest 会把 AOSP 官方代码、Rockchip 平台代码、内核代码、u-boot 代码都拉到一起。同步完成后目录下面会有device/rockchip、kernel、u-boot、external等目录。不同板卡厂商提供的 SDK 可能基于同一份 Android 11 分支但会额外带自己的 SDK patch。如果是从官方仓库拉取可能需要手动合入板级 dts 和驱动这又是额外的活。建议优先向板卡供应商要一份已经适配好的 AOSP11 SDK如果没有再考虑用开源仓库自建。同步完后先做一次默认 lunch确认 SDK 在你的编译环境里能正常编译 64 位版本以免一开始就带着环境问题去排查。source build/envsetup.sh lunch rk356x-userdebug make -j$(nproc)注意如果 RK SDK 里没有 rk356x 产品目录试试lunch rk3566-userdebug或按device/rockchip/rk356x/AndroidProducts.mk里实际的 lunch 组合名来选。RK 不同批次板卡代号会有差异。3.2 最关键的一步修改 BoardConfig.mk进入device/rockchip/rk356x/BoardConfig.mk把目标架构从 arm64 改成 arm。这是整个降级过程的核心。我给出的 diff 是在标准 RK SDK 上做的版本不同宏名称可能略有变化但原理一致# 修改前 TARGET_ARCH : arm64 TARGET_ARCH_VARIANT : armv8-a TARGET_CPU_ABI : arm64-v8a TARGET_CPU_VARIANT : cortex-a55 TARGET_2ND_ARCH : arm TARGET_2ND_ARCH_VARIANT : armv7-a-neon TARGET_2ND_CPU_ABI : armeabi-v7a TARGET_2ND_CPU_ABI2 : armeabi # 修改后 TARGET_ARCH : arm TARGET_ARCH_VARIANT : armv7-a-neon TARGET_CPU_ABI : armeabi-v7a TARGET_CPU_ABI2 : armeabi TARGET_CPU_VARIANT : generic同时要检查 product 级配置比如device/rockchip/rk356x/rk356x.mk中如果有TARGET_SUPPORTS_64_BIT_APPS和TARGET_SUPPORTS_32_BIT_APPS之类的开关需要确保只支持 32 位TARGET_SUPPORTS_32_BIT_APPS : true TARGET_SUPPORTS_64_BIT_APPS : false这会影响 Dexopt、Zygote 启动模式和 WebView 的选用策略。如果 product 配置没改干净系统可能仍然会在 64 位模式下启动 Zygote导致结果不完整。另一个容易忽略的文件是device/rockchip/rk356x/BoardConfig.mk里内核编译参数。如果整套系统要全 32 位TARGET_KERNEL_ARCH 也要改成 armTARGET_KERNEL_ARCH : arm TARGET_KERNEL_HEADERS_ARCH : arm这里要特别留意BOARD_PREBUILT_DTBOIMAGE之类的预编译 dtbo 路径它在 SDK 里可能分了 64 位版本和 32 位版本。如果配置不对编译能过但刷机后设备树尺寸或属性对不上内核无法识别硬件。3.3 内核与 u-boot 的 32 位配置RK3566/3568 的 Cortex-A55 内核支持 AArch32 执行状态这意味着不仅用户空间可以 32 位连内核都可以编译成 ARM 32 位。Rockchip SDK 里通常会带一个专门给 32 位系统用的内核配置文件。以我的操作为例编译 32 位内核是这样的cd kernel export ARCHarm export CROSS_COMPILEarm-linux-androideabi- make rk356x_32_defconfig make -j$(nproc) rk3568-evb1-ddr4-v10.img这里的rk356x_32_defconfig并不是每个 SDK 版本都有。如果没有可以在标准rk356x_defconfig基础上改三个关键选项CONFIG_ARMy # CONFIG_ARM64 is not set CONFIG_CPU_V7y CONFIG_ANDROID_BINDER_IPC_32BITy最后这个CONFIG_ANDROID_BINDER_IPC_32BIT我必须强调它决定 Binder 驱动用 32 位还是 64 位 ioctl 结构体。如果你保留 64 位内核但用户空间全是 32 位离不开这个开关。如果你直接使用 32 位内核这个开关也建议打开保证 Binder 兼容层行为一致。u-boot 这边我这次没有专门改成 32 位因为 Rockchip 的 u-boot 可以在 64 位阶段启动 32 位内核只要 bootm 命令遵循 Linux boot ABI 约定即可。但如果你想要一个体积更小、加载更快的引导流程Rockchip SDK 也提供了 32 位 u-boot 构建方式核心配置在u-boot/configs/rk3568_evb_defconfig用make rk3568_evb_defconfig编译即可。不过实践中通常没必要省不了多少内存。3.4 构建验证确认产物是 32 位 ARM 系统编译完成后不要急着刷机先做一次产物验证。主要看三个文件file out/target/product/rk356x/system/bin/linker file out/target/product/rk356x/system/bin/installd file out/target/product/rk356x/kernel如果linker显示ELF 32-bit LSB shared object, ARM, EABI5说明用户空间已经是 32 位。如果显示ELF 64-bit LSB shared object, ARM aarch64那你的 BoardConfig.mk 被别的地方覆盖了。常见覆盖来源是device/rockchip/.config或vendor/*/BoardConfigVendor.mk。路径不同但配置逻辑一样grep 一下所有含TARGET_ARCH的 BoardConfig谁改到 64 位就把它改回来。系统镜像中还要检查一下 Zygote 进程位数的启动参数正常 32 位系统的init.rc里zygote会启动zygote32而不是带--64的zygote64grep zygote out/target/product/rk356x/root/init.zygote32.rc如果发现生成的是init.zygote64_32.rc说明前后两端配置没有统一需要回头检查ro.zygote和ro.zygote.primary属性。4. 烧录到开发板并完成启动验证4.1 用 RKDevTool / upgrade_tool 烧录编译通过后把产物从 out 目录拷贝出来。烧录前先确认分区表RK AOSP11 通常包括 parameter、uboot、misc、boot、dtbo、recovery、system、vendor 这些分区。32 位系统镜像体积比 64 位小不少尤其 system.img 和 vendor.img这一优势会让整体刷写时间缩短但分区表里的偏移一般不用动。我这次用 Windows RKDevTool 烧录操作顺序是在 RKDevTool 里导入新的 parameter.txt。把左边烧录项逐一绑定到生成的镜像文件。开发板重新上电并进入 Loader 模式按住 Recovery 键再插 USB。点击“执行”开始烧录。烧完后拔线重新上电。用 Linux 工具也一样upgrade_tool 的命令行方式适合集成到脚本里sudo upgrade_tool uf parameter.txt sudo upgrade_tool di -b uboot.img sudo upgrade_tool di -k boot.img sudo upgrade_tool di -m misc.img sudo upgrade_tool di -d dtbo.img sudo upgrade_tool di -s system.img sudo upgrade_tool di -v vendor.img sudo upgrade_tool rd注意如果只刷 system 和 boot却忘了刷 dtbo 和 vendor容易出现内核态驱动与用户态库版本不匹配。尤其是从 64 位替换到 32 位这种跨越 ABI 的操作额外强调一句波形效应非常大。4.2 通过串口 / ADB 确认系统架构第一次启动时把 CH340 串口板接好波特率一般是 1500000RK 平台常见或 115200看内核打印是否正常。见到Starting kernel ...之后再确认 ARM 模式uname -a正常情况下输出里会出现armv7l或Linus Torvalds后跟armv7。如果出现aarch64说明内核仍然是 64 位那问题出现在内核编译配置或 dtb 选择上。进入 Android 后执行adb shell getprop ro.product.cpu.abi # 预期输出armeabi-v7a adb shell getprop ro.zygote # 预期输出zygote32 adb shell ps -A | grep zygote # 预期输出zygote32不应出现 zygote64三行输出全部符合预期基本可以确定系统已经是 32 位 AOSP11。如果 ro.product.cpu.abi 显示 arm64-v8a说明 Build.prop 生成时用的还是 arm64 覆盖配置那就要检查device/rockchip/rk356x下有没有额外的PRODUCT_AAPT_PREF_CONFIG或BUILD_EMULATOR之类的覆盖逻辑。4.3 验证关键硬件功能是否正常体系结构降级后最容易出问题的是 GPU、音频、摄像头、Wi-Fi 蓝牙。我这次板上用的是 Mali G52 GPURockchip arm32 驱动库随 SDK 提供正常编译后 OpenGL 和 Vulkan如果支持都能用。检查方式adb shell dumpsys SurfaceFlinger | grep GLES能看到GLES 3.2或相应版本就说明 GPU 渲染链路正常。再用相机和音视频走一遍功能用例。Wi-Fi 蓝牙这类依赖 HCI 架构的模块基本与 CPU 位数无关只要内核驱动正常即可。如果你遇到图形异常先查vendor/lib/egl下库文件的位数adb shell file vendor/lib/egl/libGLES_mali.so # 预期32-bit ELF shared object如果这里显示 64-bit说明 vendor 库里混入了 arm64 文件需要查 SDK 里vendor/rockchip/common或device/rockchip/common对预编译库的归属配置。5. 实测数据内存到底省了多少5.1 测试环境和口径为了让数据可复现我统一了测试口径。开发板是 RK3568 EVB、4GB LPDDR4屏幕是 1080p HDMI 输出。所有测试都用同一张 MicroSD 卡烧录完 64 位和 32 位系统后不做额外手动设置。预装应用保持 SDK 默认列表只是额外安装了同一版 APK包含一个网页容器和三个后台 NDK 服务。开机完成定义为系统进入桌面并且后台数分钟无 CPU 大负载。用两个命令记录内存adb shell cat /proc/meminfo adb shell dumpsys meminfo为了减少不确定性每轮测试跑三遍取中位数。5.2 64 位与 32 位 AOSP11 的实测对比这是我最常被问到的部分直接上数据。指标64 位 AOSP1132 位 AOSP11变化MemTotal 总物理内存3960 MB3960 MB无MemFree 空闲内存812 MB1584 MB95%MemAvailable 可用内存1886 MB2753 MB45%Used 整体已用含 cache3074 MB2176 MB-29.2%SystemServer RSS290 MB204 MB-29.6%SystemUI RSS190 MB138 MB-27.4%SurfaceFlinger RSS145 MB102 MB-29.6%zygote32/64 RSS380 MB265 MB-30.2%Home Launcher RSS155 MB112 MB-27.7%这个结果符合标题口径整体内存节省约 30%。其中进程 RSS 的下降幅度非常稳定基本都落在 27% 到 31% 之间。MemAvailable 提升幅度会更大因为系统服务释放出来的页面会回归到 page cache。如果按 2GB 内存设备来计算32 位降级可用的空闲内存从大约 400MB 提升到 1300MB 左右效果相当显著。5.3 这 30% 是怎么换来的要理解这 30%不是简单说“指针小一半”。更准确地说Android 系统内存大头集中在 Native Heap 和 ART Heap。从 64 位到 32 位的变化里ART 运行时的对象引用压缩导致 Java Heap 整体占用下降而 C 层的 std::vector、unordered_map 等容器内部节点也密集使用指针这些都因位宽减半而同步缩减。另外共享库映射数量相同但每条映射的页表项可能更少。32 位地址空间通常使用 4KB 页加单片页表64 位用户空间在同样物理内存下内核维护进程页表所需的内存也更多。这部分体现在内核内存统计上不会直接体现在单进程 RSS 里但对 MemAvailable 有贡献。同时不能忽略一个平衡点32 位可寻址空间上限约 4GB单个 32 位进程实际可用的连续堆空间更受限制。如果业务里有需要申请超过 1GB 内存的大任务这种降级方案就不适用。6. 常见问题与排查实录6.1 刷完启动反复重启或黑屏这类问题首先怀疑内核和用户空间 ABI 不匹配。之前提过的CONFIG_ANDROID_BINDER_IPC_32BIT是最容易踩的点。如果保留 64 位内核而用户空间全为 32 位Binder 驱动 ioctl 结构体大小与用户空间 libbinder 不一致轻则首次启动后 binder 线程通讯失败重则 Zygote 无法正常为 app 进程 fork。修复方式必须让内核支持 32 位 Binder或者直接把内核换成 32 位编译。然后重新生成 boot.img确认boot.img内 kernel 是armv7l。另一个常见点是 dtbo 与内核版本不匹配。RK 平台习惯把 device tree overlay 放在独立 dtbo 分区。如果 system 和 kernel 是 32 位而 dtbo 里设备树描述的内存 reserved 配置仍然来自 64 位 SDK可能造成 CMA 大小异常导致 GPU 分配失败后 SurfaceFlinger 不停重启。6.2 GPU 渲染异常或 libGLES_mali.so 加载失败出现这种问题的根源通常是 GPU blob 选错位数。RK SDK 里 Mali 库通常放在device/rockchip/common/gpu目录下同时包含 libMali.so、libGLES_mali.so、libmali_util.so 等。构建时通过PRODUCT_PROPERTY_OVERRIDES和TARGET_GPU相关变量选择具体版本。检查方法adb shell dumpsys SurfaceFlinger如果输出EGL: eglInitialize failed那说明系统加载的 libEGL 与 libGLES_mali.so 之间存在 ABI 或 vendor 库匹配问题。去/vendor/lib/egl/目录下看是否能找到libGLES_mali.so并确保它是 32 位。原厂 SDK 在 32 位构建时通常会自动选择libGLES_mali_32.so或直接替换但第三方 SDK 不一定。此时需要手动修改device/rockchip/common/BoardConfig.mk里的TARGET_GPU定义明确指向 arm32 版本的 GPU 库文件夹。6.3 应用提示Unable to extract native libraries或INSTALL_FAILED_NO_MATCHING_ABIS这是 32 位系统最常见的应用兼容问题。很多现代 APK 里只打了arm64-v8a的 so 文件没有armeabi-v7a。在 32 位系统里降级安装这类应用会失败。解决办法找 APK 供应商提供 arm32 版本。用 APK 拆包工具把 arm64 的 so 替换成 arm32 版后重新签名。如果业务可以接受把应用本身改为不携带 so 或使用 Android 原生库兼容目录。我遇到过更隐蔽的情况某个 APK 的 lib 目录同时有armeabi-v7a和arm64-v8a但 Manifest 里android:extractNativeLibsfalse导致加载器在 32 位系统上仍尝试从 APK 里加载 arm64 so。这种只能改 Manifest 并重打包。6.4 内存没有明显下降甚至某几个进程反而变高32 位系统里如果预装了 64 位专属的大型硬件库这些库无法加载进程会反复重试内存反而升高。排查时用dumpsys meminfo观察哪些进程 RSS 异常。如果 SystemServer 没有下降检查是否还运行了 64 位内核里的 vDSO 映射或 vendor 分区里有 64 位 HAL 服务。Android 11 里很多 HAL 通过 hwbinder 独立运行vendor 分区如果仍然保留 arm64 版本的 libhardware.so会以 64 位进程方式启动内存自然降不下来。解决方式是彻底按 32 位方式重编 vendor 分区不要直接使用 64 位 SDK 的 vendor.img。单独解包 vendor 镜像逐个查看所以file类型凡是 64 位的库都要替换。7. 经验小结和后续优化方向实测做下来我给 RK3566/RK3568 降级到 32 位 AOSP11 的项目总结是整体收益稳定尤其适合内存固定在 2GB 到 4GB 的商显和物联设备。省下来的内存在日常操作中非常明显切桌面、拉起应用、多路视频预览都不再频繁杀后台。不过这个操作确实有边界。如果业务本身大量依赖 64 位算法库或者在跑 Chrome 浏览器那种大型现代应用32 位降级就有点得不偿失。我在实际项目中碰到过客户坚持用 64 位版本 Chrome后来只能保留 64 位系统用轻量 WebView 方案替代。后续还能做得更细的方向包括基于 32 位系统继续裁剪 SystemUI 里的非必要组件移除不用的曰志服务关闭 dm-verity 节省校验开销调整 LMKD 参数让低内存策略更激进。这些措施叠加起来2GB 设备跑到 32 位系统后甚至可以腾出将近 1.5GB 给业务使用。如果你也在折腾 RK3566/3568 的内存瓶颈建议先用我上面这套流程做一次评估数据全出来后再决定是否把降级固件纳入正式版本。