4、System分区详解:系统镜像结构、只读文件系统特性、system分区定制策略

4.1 系统镜像结构:不只是个文件夹

System分区,说白了就是Android系统的「大脑」。所有核心系统服务、框架层代码、预装应用都塞在这里。我第一次接触AOSP时,以为它就是个普通文件夹,后来踩了坑才明白——它是个精心设计的只读文件系统镜像。

典型的system分区结构长这样:

system/
├── app/          # 预装用户应用(.apk)
├── priv-app/     # 特权应用(系统级权限)
├── framework/    # 框架jar包(services.jar, framework.jar)
├── etc/          # 配置文件(permissions, sysconfig)
├── lib/          # 原生库(.so文件)
├── lib64/        # 64位原生库
├── bin/          # 可执行文件(shell工具)
├── xbin/         # 扩展可执行文件(调试工具)
├── media/        # 开机动画、铃声
├── fonts/        # 系统字体
├── usr/          # 键盘布局、ICU数据
└── build.prop    # 系统属性文件

嗯,这里要注意:app/priv-app/的区别。priv-app里的应用拥有更高的权限等级,可以申请signatureOrSystem级别的权限。我在项目中遇到过,把普通应用放到priv-app里,结果权限检查直接报错——因为签名不对。

4.2 只读文件系统特性:为什么不能随便改?

System分区默认挂载为只读,这是有原因的。你想想看,如果系统文件能被随意修改,那恶意应用改个services.jar,整个系统就崩了。Android从设计上就把它锁死了。

具体来说,只读特性体现在:

  • 挂载参数mount -o ro,强制只读
  • 文件系统类型:早期用yaffs2,现在主流是ext4或EROFS
  • dm-verity:内核级别的完整性校验,防止篡改
  • AVB(Android Verified Boot):启动时校验system镜像的哈希树

核心概念:只读不是「不能写」,而是「写完后要重新签名」。定制system分区,本质上就是修改镜像文件,重新打包,再刷入设备。

我记得有一次,客户要求预装一个企业安全应用。我直接改了system/app目录,把apk塞进去,然后重新打包system.img。刷机后设备启动卡在bootloader——因为dm-verity检测到镜像哈希不匹配。从那以后,我每次改system分区都会先关掉AVB,或者重新计算哈希。

4.3 System分区定制策略:实战经验

定制system分区,说白了就是三件事:删、改、加。但每一步都有坑。

4.3.1 删除预装应用

有些厂商预装了一堆垃圾应用,你想删掉。直接删apk文件?不行。因为AndroidManifest.xml里可能声明了sharedUserId,删掉后其他应用会崩溃。

我的做法是:

  1. 先查packages.xml,看应用有没有依赖关系
  2. adb shell pm list packages -f确认路径
  3. device.mk里用PRODUCT_PACKAGES排除掉
# 在device.mk中排除预装应用
PRODUCT_PACKAGES += \
    Launcher3 \
    Settings

# 排除不需要的应用
PRODUCT_PACKAGES -= \
    Email \
    Calendar \
    Music

警告:不要直接删除system/app下的apk然后重新打包。这样做会导致OTA升级失败,因为增量包会检查文件哈希。我曾经因为这个被测试组骂了一周。

4.3.2 修改系统配置文件

比如你想改build.prop,增加一些自定义属性。直接改?可以,但要注意:

  • build.prop是在编译时生成的,修改后要重新打包
  • 有些属性是只读的(如ro.build.version.sdk),改了也没用
  • 建议在device.mk里用PRODUCT_PROPERTY_OVERRIDES添加
# 在device.mk中添加自定义属性
PRODUCT_PROPERTY_OVERRIDES += \
    ro.custom.version=1.0 \
    ro.custom.feature=true

我个人习惯把自定义属性放在system/etc/extra.prop里,然后在init.rc中加载。这样方便维护,不会污染build.prop。

4.3.3 添加预装应用

添加预装应用有两种方式:

方式 优点 缺点
直接放apk到system/app 简单粗暴 无法卸载,占用system空间
通过PRODUCT_PACKAGES编译 可管理,支持卸载 需要源码编译

如果你只是临时测试,直接放apk到system/app目录,然后重新打包镜像就行。但如果是正式产品,我建议用编译方式:

# 在Android.mk中定义预装应用
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := MyApp
LOCAL_SRC_FILES := MyApp.apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_TAGS := optional
LOCAL_CERTIFICATE := PRESIGNED
include $(BUILD_PREBUILT)

小技巧:预装应用最好用PRESIGNED签名,这样不会覆盖原有签名。如果你用platform签名,那这个应用就只能在你的系统上运行了。

4.4 定制后的验证流程

改完system分区后,别急着刷机。先做这几步:

  1. 检查文件大小:system分区有大小限制,超了会刷不进去
  2. 验证签名:用avbtool重新计算哈希
  3. 模拟挂载:用mount -o loop挂载镜像,检查文件权限
  4. 测试启动:先刷到测试机,别直接上量产机

我曾经有一次,改完system分区后忘了重新签名,结果刷了20台设备全部变砖。嗯,从那以后我写了个自动化脚本,每次打包完自动跑一遍验证流程。

总结一下:System分区定制,核心就是理解只读文件系统的特性,然后通过编译系统或直接修改镜像的方式,安全地添加、删除、修改文件。记住,每次修改后都要重新签名和验证,否则就是给自己挖坑。