本章是全书的收官章节。我们将整合前面 62 章讲解的全部内容 —— 构建系统、init 进程、硬件抽象层 HAL、系统服务、SystemUI、模拟器、安全机制、签名机制 —— 串联成一套完整的端到端实操流程:构建、定制、签名、分发一套功能完整的定制 ROM。
我们选用的目标设备是 AOSP Goldfish 模拟器(sdk_phone64_x86_64)。这是刻意做出的选择:本书的每位读者都可以使用笔记本或工作站运行该模拟器,无需实体硬件。本章实现的所有内容,包括定制设备树、资源覆盖层、应用、服务、开机动画、内核调优、HAL 修改,同样适用于物理设备;仅有 BoardConfig.mk 与内核二进制文件需要改动。
本章中所有文件路径、命令、代码片段,均基于 Android 17(android17‑release)AOSP 源码树验证。引用源码文件时会给出源码树内完整相对路径,你可以在自己的源码副本中对照查看。
65.1 规划你的定制 ROM
65.1.1 什么是 “定制 ROM”
定制 ROM 是经过修改的 Android 构建产物,会改动下面一层或多层:
| 层级 | 示例 | 复杂度 |
|---|---|---|
| 产品配置 | 品牌标识、默认应用、壁纸 | 低 |
| 资源覆盖层 | 状态栏颜色、快捷设置布局、配置标记 | 低 |
| 预编译应用 | 新增 / 移除系统预装 APK | 低 |
| 框架行为 | 新增系统服务、修改 API | 中 |
| SystemUI | 定制状态栏、导航栏、主题 | 中 |
| 开机动画 | 自定义启动画面 | 低 |
| 内核 | 自定义模块、调度器调优 | 高 |
| 硬件抽象层 HAL | 自定义硬件抽象层 | 高 |
| 签名与分发 | 发布密钥、OTA 升级包 | 中 |
社区主流 ROM(LineageOS、/e/OS、GrapheneOS、CalyxOS、PixelExperience)均融合了上述全部层级的定制。本章会覆盖每一个层级。
65.1.2 定义 ROM 目标
编写任何代码之前,请回答下面这些问题:
- 开发目的是什么?侧重隐私?性能优化?企业设备管理?还是仅作为学习实践?
- 目标支持哪些设备?本章使用模拟器(goldfish/ranchu);实体设备则需要厂商二进制文件与内核源码。
- Android 版本?我们基于 AOSP main 分支构建,本书编写时对应 Android 17(平台 SDK 版本 37,最新稳定版本 17)。锁定这些版本的发布配置存放于 build/release/;
build/release/flag_values/trunk_staging/RELEASE_PLATFORM_VERSION_LAST_STABLE.textproto内容为 “17”,RELEASE_PLATFORM_SDK_VERSION.textproto内容为 “37”。 - 品牌标识?定制 ROM 名称、设备型号字符串、构建指纹。
- 默认预装哪些应用?保留哪些 AOSP 原生应用、移除哪些、新增哪些第三方 APK?
- 框架层改动?新增服务、修改原有行为、调整配置。
65.1.3 我们将要构建的 ROM
本章全程构建 “AospBook ROM” 这套定制 ROM,包含以下内容:
- 继承自 Goldfish 的定制设备配置
- 自定义品牌信息(产品名称、型号、构建指纹)
- 一个第三方预编译应用
- 编译进系统镜像的自定义示例应用
- 修改框架与 SystemUI 默认配置的运行时资源覆盖层 RRO
- 通过 AIDL 对外提供接口的自定义系统服务
- 自定义开机动画
- SystemUI 主题修改
- 自定义签名密钥
- OTA 升级包
- 自定义内核模块
- 自定义 HAL 实现
65.1.4 架构概览
AospBook ROM 架构

65.1.5 目录结构
下面是本章实操过程中将要创建的完整目录树:
device/AospBook/bookphone/
AndroidProducts.mk
bookphone.mk # 产品Makefile
BoardConfig.mk
device.mk # 设备层级配置
overlay/
frameworks/
base/
core/res/res/values/config.xml
BookSystemUIOverlay/
AndroidManifest.xml
Android.bp
res/values/config.xml
apps/
BookSampleApp/
Android.bp
AndroidManifest.xml
src/...
res/...
prebuilt/
BookReader/
Android.bp
BookReader.apk
services/
BookService/
Android.bp
aidl/...
src/...
bootanimation/
desc.txt
part0/
part1/
hal/
booklight/
Android.bp
aidl/...
default/...
sepolicy/
vendor/
file_contexts
bookservice.te
booklight.te
keys/
releasekey.pk8
releasekey.x509.pem
platform.pk8
platform.x509.pem
shared.pk8
shared.x509.pem
media.pk8
media.x509.pem
65.2 搭建构建环境
65.2.1 硬件需求
编译 AOSP 对资源消耗很高,需求如下:
| 资源 | 最低配置 | 推荐配置 | 本章使用配置 |
|---|---|---|---|
| 源码磁盘 | 250 GB | 400 GB | 500 GB SSD |
| 含编译产物磁盘 | 400 GB | 600 GB+ | 1 TB NVMe |
| 内存 RAM | 32 GB | 64 GB+ | 64 GB |
| CPU 核心数 | 4 核 | 16 核 + | 16 核 |
| 操作系统 | Ubuntu 22.04+ | Ubuntu 24.04 LTS | Ubuntu 24.04 |
| 文件系统 | ext4(区分大小写) | ext4 | ext4 |
构建是高度并行化的。每增加一个 CPU 核心都可以缩短完整编译耗时。内存是第二关键指标 —— 链接器 lld 以及 javac/d8 编译阶段,每个并行任务会消耗 2‑4GB 内存。
工具链说明:AOSP 自带宿主侧预编译工具链,不需要你从系统包管理器安装 JDK 或者 C/C++ 编译器。构建会使用源码内的 OpenJDK(全局默认 jdk21,源码树中同时提供 jdk25 可供选择;参考build/soong/ui/build/config.go的 ConfigJavaEnvironment)以及源码内置 clang。下面列出的宿主软件包,仅仅是构建脚本调用的支撑库与工具。
65.2.2 Ubuntu/Debian 所需软件包
AOSP 构建系统依赖一组特定宿主软件包,执行下面命令全部安装:
# 更新软件源索引
sudo apt-get update
# 核心编译依赖包
sudo apt-get install -y \
git-core gnupg flex bison build-essential \
zip curl zlib1g-dev libc6-dev-i386 \
x11proto-core-dev libx11-dev lib32z1-dev \
libgl1-mesa-dev libxml2-utils xsltproc unzip \
fontconfig libncurses5 procps python3 python3-pip \
rsync libssl-dev
# 64位主机运行32位预编译二进制所需库
sudo apt-get install -y \
lib32ncurses-dev lib32readline-dev lib32z1-dev
# 模拟器GPU硬件加速依赖
sudo apt-get install -y \
libvulkan-dev mesa-vulkan-drivers \
libpulse0 libgl1
# (可选)内核编译依赖
sudo apt-get install -y \
bc cpio kmod libelf-dev
# Python依赖(部分构建脚本要求特定版本)
sudo apt-get install -y \
python3-protobuf python3-setuptools
65.2.3 安装 repo 工具
repo 工具用于管理 AOSP 数百个 Git 仓库:
# 在家目录创建bin目录
mkdir -p ~/bin
# 下载repo启动器
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
# 添加可执行权限
chmod a+x ~/bin/repo
# 加入PATH(写入~/.bashrc可以永久生效)
export PATH=~/bin:$PATH
# 验证安装
repo version
repo 启动器是一个 Python 脚本,会从谷歌仓库拉取完整 repo 工具。要求 Python 3.6 及以上版本。
65.2.4 初始化 AOSP 源码树
# 创建工作目录
mkdir -p ~/aosp && cd ~/aosp
# 使用main分支初始化repo
repo init -u https://android.googlesource.com/platform/manifest \
-b main \
--partial-clone \
--clone-filter=blob:limit=10M
# 同步全部仓库(高速网络下耗时1‑3小时)
repo sync -c -j$(nproc) --no-tags --no-clone-bundle
关键参数说明:
| 参数 | 作用 |
|---|---|
-b main | 跟踪 main 开发分支 |
--partial-clone | 开启 Git 部分克隆,节省磁盘 |
--clone-filter=blob:limit=10M | 初始仅下载小于 10MB 的二进制对象 |
-c | 仅同步当前分支 |
-j$(nproc) | 使用全部 CPU 核心并行 |
--no-tags | 不下载 Git 标签,节省时间空间 |
--no-clone-bundle | 不使用 bundle 包,部分网络环境下速度更快 |
65.2.5 完整环境部署脚本
下面是可在全新 Ubuntu22.04 机器直接运行的幂等部署脚本:
#!/bin/bash
# setup_aosp_build_env.sh -- AOSP完整编译环境部署脚本
# 用法: sudo ./setup_aosp_build_env.sh
set -euo pipefail
echo "=== AOSP Build Environment Setup ==="
# 1. 系统软件包安装
echo "[1/6] Installing system packages..."
apt-get update -qq
apt-get install -y -qq \
git-core gnupg flex bison build-essential \
zip curl zlib1g-dev libc6-dev-i386 \
x11proto-core-dev libx11-dev lib32z1-dev \
libgl1-mesa-dev libxml2-utils xsltproc unzip \
fontconfig libncurses5 procps python3 python3-pip \
rsync libssl-dev bc cpio kmod libelf-dev \
lib32ncurses-dev lib32readline-dev lib32z1-dev \
python3-protobuf python3-setuptools \
libvulkan-dev mesa-vulkan-drivers libpulse0 libgl1
# 2. Java工具链说明
echo "[2/6] Java toolchain..."
# 无需在宿主安装JDK:AOSP构建使用源码树内预编译OpenJDK
# (prebuilts/jdk/jdk21,同时内置jdk25)
# 执行source build/envsetup.sh与lunch时会自动设置JAVA_HOME
# 3. Git基础配置
echo "[3/6] Configuring Git..."
git config --global user.email "${GIT_EMAIL:-builder@example.com}"
git config --global user.name "${GIT_NAME:-AOSP Builder}"
git config --global color.ui auto
# 4. 安装repo
echo "[4/6] Installing repo tool..."
REPO_BIN="/usr/local/bin/repo"
if [ ! -f "$REPO_BIN" ]; then
curl -s https://storage.googleapis.com/git-repo-downloads/repo > "$REPO_BIN"
chmod a+x "$REPO_BIN"
fi
echo "repo version: $(repo version 2>/dev/null | head -1)"
# 5. 配置ccache(可选,强烈推荐)
echo "[5/6] Configuring ccache..."
apt-get install -y -qq ccache
echo 'export USE_CCACHE=1' >> /etc/profile.d/aosp.sh
echo 'export CCACHE_EXEC=/usr/bin/ccache' >> /etc/profile.d/aosp.sh
echo 'export CCACHE_DIR=$HOME/.ccache' >> /etc/profile.d/aosp.sh
ccache -M 50G
# 6. 大编译场景内核参数调优
echo "[6/6] Tuning kernel parameters..."
# 提高inotify监听文件上限(构建系统依赖)
echo 'fs.inotify.max_user_watches=524288' >> /etc/sysctl.d/99-aosp.conf
# 提高进程打开文件数上限
echo '* soft nofile 65536' >> /etc/security/limits.d/99-aosp.conf
echo '* hard nofile 65536' >> /etc/security/limits.d/99-aosp.conf
sysctl -p /etc/sysctl.d/99-aosp.conf
echo ""
echo "=== Setup Complete ==="
echo "Next steps:"
echo " 1. mkdir ~/aosp && cd ~/aosp"
echo " 2. repo init -u https://android.googlesource.com/platform/manifest -b main"
echo " 3. repo sync -c -j\$(nproc)"
echo " 4. source build/envsetup.sh"
echo " 5. lunch <target>"
echo " 6. m"
65.2.6 配置 ccache
编译缓存工具 ccache 可以大幅缩短重编译时间。首次全编译耗时约 2‑4 小时,之后仅改动少量文件的增量编译可以在数分钟完成。
# 设置缓存大小(AOSP推荐50GB)
export USE_CCACHE=1
export CCACHE_EXEC=/usr/bin/ccache
export CCACHE_DIR=$HOME/.ccache
ccache -M 50G
# 将配置写入 ~/.bashrc 永久生效
cat >> ~/.bashrc << 'EOF'
export USE_CCACHE=1
export CCACHE_EXEC=/usr/bin/ccache
export CCACHE_DIR=$HOME/.ccache
EOF
编译完成后查看缓存统计:
ccache -s
# 示例输出:
# cache hit (direct) 123456
# cache hit (preprocessed) 12345
# cache miss 23456
# cache hit rate 84.56 %
65.2.7 初始化构建环境
每次新开终端,都需要初始化构建环境:
cd ~/aosp
# 加载环境脚本,实际文件路径 build/make/envsetup.sh
source build/envsetup.sh
# 执行后获得下面常用命令:
# lunch - 选择编译目标
# m - 在源码根目录执行构建
# mm - 在当前目录执行构建
# mmm - 编译指定目录
# croot - 快速切回源码根目录
# godir - 跳转到包含指定文件的目录
envsetup.sh脚本位于build/make/envsetup.sh,为 shell 注入构建所需函数与环境变量。它会扫描所有device/*/以及vendor/*/目录下的vendorsetup.sh并加载,以此注册额外 lunch 编译目标。
65.2.8 理解 lunch 编译目标
# lunch不带参数弹出交互菜单;也可以用辅助命令查看目标各组成部分
list_products # 列出源码树全部产品(TARGET_PRODUCT)
list_releases # 发布配置(trunk_staging, next, …)
list_variants # user, userdebug, eng
# 选择模拟器目标,本章ROM以此为基础
lunch sdk_phone64_x86_64-trunk_staging-userdebug
list_products、list_releases、list_variants定义在build/make/envsetup.sh;lunch 支持完整格式 <产品>-<发布版本>-<变体>,不带参数会打印菜单。
lunch 目标格式:<product>-<release>-<variant>
| 组成 | 取值 | 说明 |
|---|---|---|
| product | sdk_phone64_x86_64 | 在 AndroidProducts.mk 中定义 |
| release | trunk_staging | 发布配置 |
| variant | user / userdebug / eng | 构建变体 |
三种构建变体控制系统可调试性:
| 变体 | ro.debuggable | adb | root 权限 | 适用场景 |
|---|---|---|---|---|
| user | 0 | 默认关闭 | 无 | 正式发布版本 |
| userdebug | 1 | 开启 | 支持 adb root | 接近正式版本的开发调试 |
| eng | 1 | 开启 | 默认 root | 完整开发调试 |
我们后续会创建属于自己的 lunch 目标,替换sdk_phone64_x86_64。
65.2.9 构建流程概览

Android17 默认使用 Siso 而非传统 Ninja 执行最终构建步骤。build/soong/ui/build/config.go设置NINJA_DEFAULT = NINJA_SISO。设置环境变量SOONG_NINJA=ninja可以回退至传统 Ninja;macOS CI 构建机器也会自动回退。Siso 兼容 Soong 输出的 Ninja 清单,对于 ROM 开发者,使用体验和 Ninja 几乎一致 —— 同样使用 m 命令,out 目录结构不变,只是新增了远程执行与缓存钩子。本章全部内容在两种执行器下均可正常工作。
65.3 创建设备配置
设备配置目录是定制 ROM 的核心。它告诉构建系统需要编译什么、如何编译、每个分区放入哪些内容。
65.3.1 理解 Goldfish 设备树
在创建自己的设备配置之前,先了解现有模拟器设备树。Goldfish 模拟器配置位于: device/generic/goldfish/
目录结构:
device/generic/goldfish/
AndroidProducts.mk # 全部产品Makefile清单
64bitonly/
product/
sdk_phone64_x86_64.mk # 产品定义
board/
BoardConfigCommon.mk # 通用板级配置
emu64x/
BoardConfig.mk # x86_64专属板级配置
details.mk # 内核与fstab配置
product/
phone.mk # phone产品基础配置
handheld.mk # 手持设备基础
base_handheld.mk # 带系统音效的手持设备基础
generic.mk # vendor/generic配置
versions.mk # 出厂API等级
overlay/ # 静态资源覆盖层
rro_overlays/ # 运行时资源覆盖层RRO
ConnectivityOverlay/
RanchuCommonOverlay/
TetheringOverlay/
hals/ # HAL实现
audio/
camera/
sensors/
radio/
sepolicy/ # SELinux策略
init/ # init脚本
init.ranchu.rc
sdk_phone64_x86_64.mk产品继承链:

关键文件解读:
device/generic/goldfish/AndroidProducts.mk,登记全部产品 Makefile:
PRODUCT_MAKEFILES := \
$(LOCAL_DIR)/64bitonly/product/sdk_phone64_x86_64.mk \
$(LOCAL_DIR)/64bitonly/product/sdk_phone64_arm64.mk \
...
device/generic/goldfish/64bitonly/product/sdk_phone64_x86_64.mk 顶层产品定义:
PRODUCT_USE_DYNAMIC_PARTITIONS := true
BOARD_EMULATOR_DYNAMIC_PARTITIONS_SIZE ?= $(shell expr 1800 \* 1048576 )
BOARD_SUPER_PARTITION_SIZE := $(shell expr ... + 8388608 )
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit_only.mk)
$(call inherit-product, device/generic/goldfish/board/emu64x/details.mk)
$(call inherit-product, device/generic/goldfish/product/phone.mk)
PRODUCT_BRAND := Android
PRODUCT_NAME := sdk_phone64_x86_64
PRODUCT_DEVICE := emu64x
PRODUCT_MODEL := Android SDK built for x86_64
这四个PRODUCT_*变量定义构建目标身份
| 变量 | 用途 | 本章取值 |
|---|---|---|
| PRODUCT_NAME | lunch 目标名称 | bookphone |
| PRODUCT_DEVICE | 板级设备名,对应 BoardConfig.mk | bookdevice |
| PRODUCT_BRAND | 设置中显示的品牌 | AospBook |
| PRODUCT_MODEL | 设置中显示的型号 | AospBook Phone |
device/generic/goldfish/board/BoardConfigCommon.mk,所有 Goldfish 目标共用的硬件配置(关键片段):
include build/make/target/board/BoardConfigGsiCommon.mk
BOARD_VENDOR_SEPOLICY_DIRS += device/generic/goldfish/sepolicy/vendor
TARGET_BOOTLOADER_BOARD_NAME := goldfish_$(TARGET_ARCH)
BUILD_EMULATOR_OPENGL := true
BUILD_QEMU_IMAGES := true
USE_OPENGL_RENDERER := true
# 模拟器不支持稀疏镜像格式
TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true
# 模拟器为非A/B升级设备
AB_OTA_UPDATER := none
# 模拟器需要super.img
BOARD_BUILD_SUPER_IMAGE_BY_DEFAULT := true
# 8G + 8M
BOARD_SUPER_PARTITION_SIZE ?= 8598323200
BOARD_SUPER_PARTITION_GROUPS := emulator_dynamic_partitions
BOARD_EMULATOR_DYNAMIC_PARTITIONS_PARTITION_LIST := \
system \
system_dlkm \
system_ext \
product \
vendor
65.3.2 创建我们的设备目录
设备目录约定格式 device/<厂商>/<设备名>:
mkdir -p device/AospBook/bookphone
65.3.3 AndroidProducts.mk
该文件向构建系统注册我们的产品:
# device/AospBook/bookphone/AndroidProducts.mk
# 构建系统扫描此文件发现产品Makefile
# PRODUCT_MAKEFILES内每一项对应一个合法lunch目标
PRODUCT_MAKEFILES := \
$(LOCAL_DIR)/bookphone.mk
构建系统扫描 device 目录时会查找 AndroidProducts.mk。PRODUCT_MAKEFILES列表中的每个路径,对应一个 lunch 目标,目标名称取自该 makefile 内部设置的PRODUCT_NAME。
65.3.4 产品 Makefile:bookphone.mk
顶层产品定义。继承 Goldfish 获得全部模拟器基础设施,再叠加我们的定制内容。
# device/AospBook/bookphone/bookphone.mk
# AospBook Phone顶层产品Makefile
# 定义lunch目标 bookphone
# ============================================================
# 继承Goldfish模拟器配置
# ============================================================
# 使用动态分区,尺寸与原生模拟器保持一致
PRODUCT_USE_DYNAMIC_PARTITIONS := true
BOARD_EMULATOR_DYNAMIC_PARTITIONS_SIZE ?= $(shell expr 2400 \* 1048576)
BOARD_SUPER_PARTITION_SIZE := \
$(shell expr $(BOARD_EMULATOR_DYNAMIC_PARTITIONS_SIZE) + 8388608)
# 仅64位配置
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit_only.mk)
# Goldfish板级细节(内核、fstab等)
$(call inherit-product, device/generic/goldfish/board/emu64x/details.mk)
# Goldfish phone配置(HAL、权限、vendor应用)
$(call inherit-product, device/generic/goldfish/product/phone.mk)
# 引入我们自己的设备配置
$(call inherit-product, device/AospBook/bookphone/device.mk)
# ============================================================
# 产品身份信息
# ============================================================
PRODUCT_BRAND := AospBook
PRODUCT_NAME := bookphone
PRODUCT_DEVICE := bookdevice
PRODUCT_MODEL := AospBook Phone
PRODUCT_MANUFACTURER := AospBook
# 构建指纹(设置‑关于手机中展示)
BUILD_FINGERPRINT := AospBook/bookphone/bookdevice:17/BP1A.250505.005/eng.builder:userdebug/dev-keys
# ============================================================
# 额外产品属性
# ============================================================
PRODUCT_PROPERTY_OVERRIDES += \
ro.build.display.id=AospBook-1.0 \
ro.aospbook.version=1.0.0 \
ro.aospbook.build.type=development \
persist.sys.timezone=America/Los_Angeles
65.3.5 设备 Makefile:device.mk
存放设备专属软件包、文件拷贝规则、属性配置。
# device/AospBook/bookphone/device.mk
# AospBook Phone设备层级配置
# 在这里添加自定义模块、覆盖层、文件拷贝
# ============================================================
# 自定义软件包
# ============================================================
PRODUCT_PACKAGES += \
BookSampleApp \
BookReader
# 自定义系统服务
PRODUCT_PACKAGES += \
BookService
# 自定义资源覆盖层
PRODUCT_PACKAGES += \
BookFrameworkOverlay \
BookSystemUIOverlay
# 自定义开机动画
PRODUCT_COPY_FILES += \
device/AospBook/bookphone/bootanimation/bootanimation.zip:$(TARGET_COPY_OUT_PRODUCT)/media/bootanimation.zip
# ============================================================
# 自定义产品属性
# ============================================================
PRODUCT_PRODUCT_PROPERTIES += \
ro.aospbook.features.dark_mode_default=true \
ro.aospbook.features.custom_qs=true
# ============================================================
# SELinux策略
# ============================================================
BOARD_VENDOR_SEPOLICY_DIRS += device/AospBook/bookphone/sepolicy/vendor
# ============================================================
# Soong命名空间(让构建系统找到我们的模块)
# ============================================================
PRODUCT_SOONG_NAMESPACES += device/AospBook/bookphone
65.3.6 BoardConfig.mk
板级配置用于定义硬件层面参数。由于我们目标是模拟器设备,因此继承 Goldfish 的板级配置:
# device/AospBook/bookphone/BoardConfig.mk
# AospBook Phone 的板级配置(基于模拟器)
# 继承 Goldfish x86_64 板级配置并对分区大小、SELinux 做自定义
# x86_64 模拟器架构
TARGET_CPU_ABI := x86_64
TARGET_ARCH := x86_64
TARGET_ARCH_VARIANT := x86_64
TARGET_2ND_ARCH_VARIANT := x86_64
# 引入 Goldfish 通用板级配置
include device/generic/goldfish/board/BoardConfigCommon.mk
# ============================================================
# 分区大小
# ============================================================
# 开发环境扩大 userdata 分区(2 GB)
BOARD_USERDATAIMAGE_PARTITION_SIZE := 2147483648
# ============================================================
# SELinux
# ============================================================
BOARD_VENDOR_SEPOLICY_DIRS += device/AospBook/bookphone/sepolicy/vendor
# ============================================================
# 内核
# ============================================================
# 使用与 Goldfish 一致的预编译内核
#(第 65.11 节介绍如何编译自定义内核)
TARGET_KERNEL_USE ?= 6.12
# ============================================================
# Recovery
# ============================================================
TARGET_NO_RECOVERY := true
# ============================================================
# 验证启动
# ============================================================
BOARD_AVB_ENABLE := true
65.3.7 构建系统如何识别我们的产品
执行 lunch bookphone‑trunk_staging‑userdebug 时,构建系统执行流程:

65.3.8 验证产品注册
创建完上述文件后,确认构建系统可以识别你的产品:
# 导入环境
source build/envsetup.sh
# 确认产品出现在已注册产品列表
list_products | grep bookphone
# 预期输出:bookphone
# 选择编译目标(产品‑发布分支‑编译变体三元组)
lunch bookphone‑trunk_staging‑userdebug
# 校验环境变量
echo "TARGET_PRODUCT=$TARGET_PRODUCT" # bookphone
echo "TARGET_BUILD_VARIANT=$TARGET_BUILD_VARIANT" # userdebug
echo "TARGET_ARCH=$TARGET_ARCH" # x86_64
printconfig
65.3.9 Product 变量命名空间
构建系统定义大量 PRODUCT_* 变量,ROM 开发中最常用如下:
| 变量 | 作用 | 示例 |
|---|---|---|
PRODUCT_NAME | lunch 目标名称 | bookphone |
PRODUCT_DEVICE | 关联板级配置目录 | bookdevice |
PRODUCT_BRAND | 品牌字符串 | AospBook |
PRODUCT_MODEL | 机型字符串 | AospBook Phone |
PRODUCT_MANUFACTURER | 厂商字符串 | AospBook |
PRODUCT_PACKAGES | 需要编译打包进镜像的模块 | BookSampleApp |
PRODUCT_COPY_FILES | 复制到镜像的文件,格式源:目标 | src:dest |
PRODUCT_PROPERTY_OVERRIDES | /system 分区系统属性 | ro.foo=bar |
PRODUCT_PRODUCT_PROPERTIES | /product 分区属性 | ro.foo=bar |
PRODUCT_VENDOR_PROPERTIES | /vendor 分区属性 | ro.foo=bar |
PRODUCT_SOONG_NAMESPACES | Soong 模块搜索路径 | 目录路径 |
PRODUCT_ENFORCE_RRO_TARGETS | 强制启用 RRO 的目标包 | framework‑res |
PRODUCT_ENFORCE_RRO_EXCLUDED_OVERLAYS | RRO 强制规则排除的 overlay 路径 | overlay paths |
65.3.10 继承机制
$(call inherit‑product, ...) 是产品配置的核心函数。它会将被引入 Makefile 的变量值追加到当前 Makefile 的同名变量中。
# parent.mk 文件内容
PRODUCT_PACKAGES += ParentApp
# child.mk 文件内容
$(call inherit‑product, parent.mk)
PRODUCT_PACKAGES += ChildApp
# 最终结果:PRODUCT_PACKAGES = ParentApp ChildApp
它和普通 include 不同:inherit‑product 使用命名空间机制,避免多个 Makefile 定义同一变量时发生冲突。
还有 $(call inherit‑product‑if‑exists, ...):如果目标文件不存在不会报错静默跳过,适合用于可选厂商 overlay。
65.3.11 通用系统镜像与设备 bring‑up
我们的 bookphone 产品会编译完整设备镜像,但 Android 还提供通用系统镜像(GSI):它是一套遵循 Treble 接口契约的独立 system.img,可以运行在任何支持 Treble 的 vendor 分区之上。
GSI 的用途:校验新设备 vendor 实现是否符合原生 AOSP;运行 CTS‑on‑GSI、VTS 测试,验证 system/vendor 分区分离是否正常。
GSI 产品定义路径:build/make/target/product/
lunch aosp_arm64‑trunk_staging‑userdebug # arm64 GSI
lunch aosp_x86_64‑trunk_staging‑userdebug # x86_64 GSI
build/make/target/product/aosp_arm64.mk 注释说明:aosp_arm64‑userdebug 的 system 镜像就是 GSI;它引入 generic_system.mk + gsi_release.mk;板级配置来自 build/make/target/board/BoardConfigGsiCommon.mk。Android 17 还提供 aosp_arm64_fullmte.mk,是开启 Arm 内存标记扩展的 GSI,用于堆内存损坏问题测试。
使用 GSI 进行设备 bring‑up 流程:

刷写 GSI 一般使用 fastbootd(用户态 fastboot),因为 system 属于动态分区:
fastboot reboot fastboot # 进入 fastbootd
fastboot flash system system.img # 刷入 GSI
fastboot -w # 擦除 userdata(更换 system 必须操作)
fastboot reboot
通用系统镜像(GSI)是用于调试与兼容性验证的工具,并非日常使用的刷机包:它不包含任何厂商应用,完全依赖设备现有的 vendor 分区运行。我们在定制 ROM 开发时会编译完整的设备镜像,但在将定制 ROM 移植到全新硬件平台的过程中,掌握 GSI 的使用方案至关重要。
65.4 添加自定义应用
65.4.1 理解 PRODUCT_PACKAGES
所有写在 PRODUCT_PACKAGES 里的模块,都会被编译并打包到对应分区镜像。模块名对应 Android.bp(Soong)或者 Android.mk 中定义的编译规则。
基础系统包定义在 build/make/target/product/base_system.mk:
# build/make/target/product/base_system.mk(片段)
PRODUCT_PACKAGES += \
abx \
am \
app_process \
atrace \
bootanimation \
bootstat \
bugreport \
cmd \
...
这些是 Android 正常运行所需最小软件包集合。我们的产品通过继承 Goldfish 手机配置链自动获得这些包。
65.4.2 引入预编译 APK
假设你有一个第三方 APK(例如 PDF 阅读器)想要内置进 ROM。创建预编译模块目录:
device/AospBook/bookphone/apps/prebuilt/BookReader/
Android.bp
BookReader.apk
Android.bp 定义预编译应用模块:
// device/AospBook/bookphone/apps/prebuilt/BookReader/Android.bp
// BookReader PDF阅读器预编译APK
// 通过 PRODUCT_PACKAGES += BookReader 将该模块打包进 system 镜像
android_app_import {
name: "BookReader",
// 当前目录下的apk文件
apk: "BookReader.apk",
// 安装到 /product 分区,不是 /system
product_specific: true,
// 允许 Play商店更新该应用
overrides: [],
// presigned:保留APK原有签名
presigned: true,
// 可选:如果需要特权权限则开启
// privileged: true,
// 编译阶段预优化DEX
dex_preopt: {
enabled: true,
},
}
android_app_import 关键属性:
| 属性 | 取值 | 说明 |
|---|---|---|
apk | 文件名 | APK 文件路径 |
presigned | true/false | 保留原有签名,不重新签名 |
certificate | "platform"、"shared"、"media" 或文件路径 | 重新签名使用的密钥 |
privileged | true/false | 安装到 priv‑app 目录 |
product_specific | true/false | 安装到 /product 分区 |
vendor | true/false | 安装到 /vendor 分区 |
dex_preopt.enabled | true/false | 编译期预优化 DEX |
如果 APK 没有正确签名,则使用 certificate 代替 presigned:
android_app_import {
name: "BookReader",
apk: "BookReader.apk",
product_specific: true,
certificate: "platform", // 使用 platform密钥重新签名
dex_preopt: {
enabled: true,
},
}
密钥名称对应 build/make/target/product/security/ 下密钥文件:
| 密钥名 | 密钥文件 | 用途 |
|---|---|---|
| platform | platform.pk8, platform.x509.pem | 带有android:sharedUserId="android.uid.system"的系统应用 |
| shared | shared.pk8, shared.x509.pem | 共享数据应用(联系人、电话) |
| media | media.pk8, media.x509.pem | 媒体 / 下载类系统应用 |
| testkey | testkey.pk8, testkey.x509.pem | 开发版本默认签名密钥 |
65.4.3 将源码应用编译进镜像
下面创建一个源码编译的自定义应用,作为 AOSP 编译流程的一部分完成编译、签名并打包进镜像。
创建应用目录结构:
device/AospBook/bookphone/apps/BookSampleApp/
Android.bp
AndroidManifest.xml
res/
layout/
activity_main.xml
values/
strings.xml
mipmap‑xxxhdpi/
ic_launcher.png
src/
com/
aospbook/
sample/
MainActivity.java
Android.bp(Soong 编译脚本)
// device/AospBook/bookphone/apps/BookSampleApp/Android.bp
// 源码构建的自定义示例应用,打包进系统镜像
android_app {
name: "BookSampleApp",
// 源码文件
srcs: ["src/**/*.java"],
// 编译依赖的Android SDK版本
sdk_version: "current",
// 安装到 /product 分区
product_specific: true,
// 使用platform密钥签名
certificate: "platform",
// 资源目录
resource_dirs: ["res"],
// 依赖静态库
static_libs: [
"androidx.appcompat_appcompat",
"com.google.android.material_material",
],
// R8代码混淆压缩
optimize: {
enabled: true,
shrink: true,
optimize: true,
proguard_flags_files: ["proguard‑rules.pro"],
},
// DEX预优化
dex_preopt: {
enabled: true,
},
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf‑8"?>
<!-- device/AospBook/bookphone/apps/BookSampleApp/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aospbook.sample">
<application
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:theme="@style/Theme.AppCompat.DayNight">
<activity
android:name=".MainActivity"
android:exported="true">
<intent‑filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent‑filter>
</activity>
</application>
</manifest>
MainActivity.java
// device/AospBook/bookphone/apps/BookSampleApp/src/com/aospbook/sample/MainActivity.java
package com.aospbook.sample;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.widget.TextView;
/**
* 演示内置到AOSP ROM的自定义应用。
* Activity展示系统信息,验证自定义ROM运行状态。
*/
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView textView = new TextView(this);
textView.setPadding(32, 32, 32, 32);
textView.setTextSize(18);
textView.setText(buildInfoString());
setContentView(textView);
}
private String buildInfoString() {
StringBuilder sb = new StringBuilder();
sb.append("AospBook ROM Info\n");
sb.append("=================\n\n");
sb.append("Brand: ").append(Build.BRAND).append("\n");
sb.append("Model: ").append(Build.MODEL).append("\n");
sb.append("Device: ").append(Build.DEVICE).append("\n");
sb.append("Product: ").append(Build.PRODUCT).append("\n");
sb.append("Build ID: ").append(Build.DISPLAY).append("\n");
sb.append("Android Version: ").append(Build.VERSION.RELEASE).append("\n");
sb.append("SDK Level: ").append(Build.VERSION.SDK_INT).append("\n");
sb.append("Build Type: ").append(Build.TYPE).append("\n");
sb.append("Fingerprint: ").append(Build.FINGERPRINT).append("\n");
// 读取自定义系统属性
String romVersion = System.getProperty("ro.aospbook.version", "unknown");
sb.append("\nROM Version: ").append(romVersion).append("\n");
return sb.toString();
}
}
res/values/strings.xml
<?xml version="1.0" encoding="utf‑8"?>
<resources>
<string name="app_name">AospBook Sample</string>
</resources>
res/layout/activity_main.xml
<?xml version="1.0" encoding="utf‑8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/info_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:fontFamily="monospace" />
</LinearLayout>
65.4.4 移除默认应用
原生 AOSP 本身并不提供「移除指定软件包」的产品变量(你在 LineageOS 源码树里见到的 PRODUCT_PACKAGES_REMOVE 属于厂商扩展语法;build/make/core/product.mk 仅定义了 PRODUCT_PACKAGES,以及它对应的 _DEBUG/_ENG/_TESTS 变体)。在原生 AOSP 中,删除预置应用只有两种实现方式:
- 不引入添加该应用的 Makefile 绝大多数系统预置应用都是通过
$(call inherit-product, ...)继承链加入到PRODUCT_PACKAGES的。选择更精简的父级配置文件,就能从根源上不编译安装这些应用。 - 通过替换模块进行覆盖 在 Soong 构建脚本中配置
overrides: ["Browser2"](旧 Make 体系使用LOCAL_OVERRIDES_PACKAGES)。当两个模块都需要安装时,被声明覆盖的模块会从最终安装列表中剔除:
// 写入替换应用的 Android.bp
android_app {
name: "BookBrowser",
// 两者同时存在时,从安装列表移除 Browser2
overrides: ["Browser2"],
// ……其余配置
}
使用 overrides 的注意点:被替换的应用仍然会参与编译(它可能是其他模块的依赖),只是不会打包写入系统镜像。
第一种方案的示例:通过不继承完整功能配置,直接剔除一整类 AOSP 应用。例如不需要电话相关应用:
# 不再继承 full_base_telephony.mk,改用 full_base.mk
$(call inherit-product, $(SRC_TARGET_DIR)/product/full_base.mk)
65.4.5 特权应用与权限
安装在 /system/priv‑app/ 或 /product/priv‑app/ 的应用,可以获取普通应用无法拿到的特权权限。
Android.bp 将应用设置为特权:
// In Android.bp
android_app {
name: "BookPrivilegedApp",
privileged: true,
product_specific: true,
certificate: "platform",
// ...
}
特权应用需要权限白名单。创建权限配置文件: device/AospBook/bookphone/permissions/privapp‑permissions‑bookphone.xml
<?xml version="1.0" encoding="utf‑8"?>
<permissions>
<privapp‑permissions package="com.aospbook.privileged">
<permission name="android.permission.MANAGE_USERS" />
<permission name="android.permission.INTERACT_ACROSS_USERS" />
</privapp‑permissions>
</permissions>
在 device.mk 将文件拷贝进镜像:
# In device.mk
PRODUCT_COPY_FILES += \
device/AospBook/bookphone/permissions/privapp‑permissions‑bookphone.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/permissions/privapp‑permissions‑bookphone.xml
65.4.6 应用安装位置

Soong 属性与安装路径映射:
| Android.bp 属性 | 最终安装路径 |
|---|---|
| 无(默认) | /system/app/<name>/ |
privileged: true | /system/priv‑app/<name>/ |
product_specific: true | /product/app/<name>/ |
product_specific: true + privileged: true | /product/priv‑app/<name>/ |
vendor: true | /vendor/app/<name>/ |
system_ext_specific: true | /system_ext/app/<name>/ |
65.5 修改 Framework 行为
65.5.1 运行时资源覆盖层 RRO
Runtime Resource Overlay(RRO):推荐的 Framework 定制方案,无需修改 framework 源码。RRO 是一个仅包含资源的小 APK,可以覆盖目标包的默认资源值。
Framework 大量可配置项位于:
frameworks/base/core/res/res/values/config.xml
该文件包含数千条配置,RRO 可以覆盖其中任意资源。
RRO 工作流程

注意:OverlayManagerService 仅在安装 / 启用资源覆盖包阶段发挥作用:它负责生成 idmap 文件,并通知应用的 ResourcesManager 需要加载哪些 overlay 的 ApkAssets。 单条资源的查找逻辑完全运行在应用自身进程的 AssetManager(源码路径:frameworks/base/libs/androidfw/)内部;读取资源的整条调用链路不会向 system_server 发起跨进程 IPC 请求。
创建 Framework RRO overlay
目录结构
device/AospBook/bookphone/overlay/BookFrameworkOverlay/
Android.bp
AndroidManifest.xml
res/
values/
config.xml
bools.xml
Android.bp
// device/AospBook/bookphone/overlay/BookFrameworkOverlay/Android.bp
// Framework(android包)的RRO,覆盖系统默认配置
runtime_resource_overlay {
name: "BookFrameworkOverlay",
// overlay 作用目标包名,android代表framework‑res
sdk_version: "current",
// 安装到product分区
product_specific: true,
}
AndroidManifest.xml
<!-- device/AospBook/bookphone/overlay/BookFrameworkOverlay/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aospbook.overlay.framework">
<application android:hasCode="false" />
<overlay
android:targetPackage="android"
android:isStatic="true"
android:priority="10"
/>
</manifest>
manifest overlay 标签关键属性
| 属性 | 取值 | 含义 |
|---|---|---|
targetPackage | "android" | 目标包,这里代表 framework |
isStatic | "true" | 静态 overlay,开机永久启用,用户不可关闭 |
priority | "10" | 优先级;多个 overlay 共存,数值高的优先生效 |
res/values/config.xml 示例,覆盖系统默认配置
<?xml version="1.0" encoding="utf‑8"?>
<!-- 覆盖framework配置 -->
<resources>
<!-- 默认开启深色模式自动:0=关闭,1=开启,2=自动 -->
<integer name="config_defaultNightMode">2</integer>
<!-- 默认壁纸组件 -->
<string name="default_wallpaper_component" translatable="false">
com.aospbook.wallpaper/.DefaultWallpaperService
</string>
<!-- 默认开启AOD息屏显示 -->
<bool name="config_dozeAlwaysOnDisplayAvailable">true</bool>
<!-- 屏幕亮度默认值 -->
<integer name="config_screenBrightnessSettingDefault">128</integer>
<integer name="config_screenBrightnessSettingMinimum">10</integer>
<!-- 允许锁屏旋转 -->
<bool name="config_enableLockScreenRotation">true</bool>
<!-- 电源键长按行为:1弹出电源菜单 -->
<integer name="config_longPressOnPowerBehavior">1</integer>
<!-- 状态栏默认显示电池百分比 -->
<bool name="config_defaultBatteryPercentageSetting">true</bool>
<!-- 文本选择触觉反馈默认开启 -->
<bool name="config_enableHapticTextHandle">true</bool>
</resources>
完整可覆盖资源请查看:
frameworks/base/core/res/res/values/config.xml
ROM 开发经常修改的资源:
| 资源名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| config_defaultNightMode | integer | 0 | 深色 / 浅色 UI 默认模式 |
| config_longPressOnPowerBehavior | integer | 1 | 电源键长按动作 |
| config_dozeAlwaysOnDisplayAvailable | bool | false | 息屏 AOD 是否可用 |
| config_enableLockScreenRotation | bool | false | 锁屏是否允许旋转 |
| config_screenBrightnessSettingDefault | integer | 随设备 | 默认屏幕亮度 |
| config_defaultBatteryPercentageSetting | bool | false | 状态栏电池百分比 |
| config_enableHapticTextHandle | bool | false | 文本选择震动反馈 |
65.5.2 校验 RRO 是否生效
编译完成后,模拟器 / 真机上执行:
# 查看已加载overlay列表
adb shell cmd overlay list
# 预期输出包含
# com.aospbook.overlay.framework
# [x] com.aospbook.overlay.framework (targeting android, priority 10)
# 查看该overlay详细dump
adb shell cmd overlay dump com.aospbook.overlay.framework
# 直接查询被覆盖的资源对应系统设置项
adb shell settings get system screen_brightness
65.5.3 修改 Framework 源码
RRO 无法完成的定制,需要直接修改 framework 源码。这种侵入性改动在升级 AOSP 版本时维护成本很高,需要谨慎。
示例:在设置关于手机页面展示自定义 ROM 版本号。Build.java 路径:frameworks/base/core/java/android/os/Build.java。
在 Build 类中增加字段:
// frameworks/base/core/java/android/os/Build.java 类内部添加
/**
* ROM自定义版本号,由ROM构建者设置。
* 读取系统属性 ro.aospbook.version
*/
public static final String AOSPBOOK_VERSION =
SystemProperties.get("ro.aospbook.version", "unknown");
在产品配置中设置该属性(bookphone.mk/device.mk)
# In device.mk or bookphone.mk
PRODUCT_PROPERTY_OVERRIDES += ro.aospbook.version=1.0.0
65.5.4 添加新系统服务
系统服务运行于 system_server 进程,通过 Binder IPC 对外提供应用可调用 API,属于 Framework 深度定制。
系统服务整体架构:

Step1 定义 AIDL 接口
目录
device/AospBook/bookphone/services/BookService/
Android.bp
aidl/
com/aospbook/service/
IBookService.aidl
src/
com/aospbook/service/
BookService.java
BookServiceManager.java
IBookService.aidl
// device/AospBook/bookphone/services/BookService/aidl/com/aospbook/service/IBookService.aidl
package com.aospbook.service;
/**
* AospBook ROM专属系统服务接口。
* 运行在system_server,向应用提供ROM专属API
*/
interface IBookService {
/**
* 获取ROM版本字符串
*/
String getRomVersion();
/**
* 获取ROM构建时间戳(epoch秒)
*/
long getBuildTimestamp();
/**
* 设置服务内部存储的自定义偏好
* @param key 键
* @param value 值
*/
void setPreference(String key, String value);
/**
* 获取自定义偏好
* @param key 键
* @return 存储的值,不存在返回null
*/
String getPreference(String key);
/**
* 获取已启用的AospBook特性列表
*/
List<String> getEnabledFeatures();
}
Step2 服务实现 BookService.java
// device/AospBook/bookphone/services/BookService/src/com/aospbook/service/BookService.java
package com.aospbook.service;
import android.content.Context;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* AospBook自定义系统服务实现。
* 在ServiceManager注册服务名 "aospbook"。
* 在SystemServer.java的startOtherServices阶段完成注册。
*/
public class BookService extends IBookService.Stub {
private static final String TAG = "BookService";
private static final String SERVICE_NAME = "aospbook";
private final Context mContext;
private final Map<String, String> mPreferences;
public BookService(Context context) {
mContext = context;
mPreferences = new HashMap<>();
Log.i(TAG, "BookService initialized");
}
@Override
public String getRomVersion() throws RemoteException {
return SystemProperties.get("ro.aospbook.version", "unknown");
}
@Override
public long getBuildTimestamp() throws RemoteException {
String timestamp = SystemProperties.get("ro.build.date.utc", "0");
try {
return Long.parseLong(timestamp);
} catch (NumberFormatException e) {
return 0;
}
}
@Override
public void setPreference(String key, String value) throws RemoteException {
// 生产版本这里应当持久化存储并做权限校验
enforceCallerPermission();
synchronized (mPreferences) {
mPreferences.put(key, value);
}
Log.d(TAG, "Preference set: " + key + " = " + value);
}
@Override
public String getPreference(String key) throws RemoteException {
synchronized (mPreferences) {
return mPreferences.get(key);
}
}
@Override
public List<String> getEnabledFeatures() throws RemoteException {
List<String> features = new ArrayList<>();
if (SystemProperties.getBoolean(
"ro.aospbook.features.dark_mode_default", false)) {
features.add("dark_mode_default");
}
if (SystemProperties.getBoolean(
"ro.aospbook.features.custom_qs", false)) {
features.add("custom_qs");
}
return features;
}
/**
* 返回向ServiceManager注册的服务名称
*/
public static String getServiceName() {
return SERVICE_NAME;
}
private void enforceCallerPermission() {
// 生产环境在这里校验自定义权限
// mContext.enforceCallingOrSelfPermission(
// "com.aospbook.permission.MANAGE_PREFERENCES",
// "BookService");
}
}
Step3 Android.bp
// device/AospBook/bookphone/services/BookService/Android.bp
// AIDL接口库
java_library {
name: "aospbook‑service‑aidl",
srcs: ["aidl/**/*.aidl"],
sdk_version: "system_current",
product_specific: true,
}
// 服务实现,运行于system_server
java_library {
name: "BookService",
srcs: ["src/**/*.java"],
static_libs: [
"aospbook‑service‑aidl",
],
libs: [
"framework",
"services.core",
],
product_specific: true,
}
Step4 将服务注册进 SystemServer
SystemServer(路径:frameworks/base/services/java/com/android/server/SystemServer.java)是启动所有系统服务的入口。如果要新增自定义服务,我们需要修改 startOtherServices () 方法:
// In frameworks/base/services/java/com/android/server/SystemServer.java
// Add to the startOtherServices(TimingsTraceAndSlog t) method, near the end.
// `t` is the TimingsTraceAndSlog parameter every startup step uses:
// AospBook custom service
t.traceBegin("StartBookService");
try {
ServiceManager.addService("aospbook",
new com.aospbook.service.BookService(mSystemContext));
} catch (Throwable e) {
reportWtf("starting BookService", e);
}
t.traceEnd();
另一种侵入性更小的实现方式:继承 SystemService 类,并通过 SystemServiceManager 完成服务注册。
// device/AospBook/bookphone/services/BookService/src/com/aospbook/service/BookSystemService.java
package com.aospbook.service;
import android.content.Context;
import android.os.ServiceManager;
import com.android.server.SystemService;
/**
* BookService的SystemService包装类
* 使用SystemServiceManager生命周期回调
*/
public class BookSystemService extends SystemService {
private BookService mService;
public BookSystemService(Context context) {
super(context);
}
@Override
public void onStart() {
mService = new BookService(getContext());
ServiceManager.addService(BookService.getServiceName(), mService);
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
// 需要依赖其他服务的初始化逻辑放这里
}
}
}
Step5 客户端 Manager 类
应用程序通过管理类与该服务进行交互。
// device/AospBook/bookphone/services/BookService/src/com/aospbook/service/BookServiceManager.java
package com.aospbook.service;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import java.util.Collections;
import java.util.List;
/**
* AospBook系统服务客户端管理类
* 使用示例:
* BookServiceManager manager = BookServiceManager.getInstance();
* String version = manager.getRomVersion();
*/
public class BookServiceManager {
private static final String TAG = "BookServiceManager";
private static volatile BookServiceManager sInstance;
private final IBookService mService;
private BookServiceManager(IBinder service) {
mService = IBookService.Stub.asInterface(service);
}
/**
* 获取单例。非AospBook ROM返回null
*/
public static BookServiceManager getInstance() {
if (sInstance == null) {
synchronized (BookServiceManager.class) {
if (sInstance == null) {
IBinder binder = ServiceManager.getService("aospbook");
if (binder != null) {
sInstance = new BookServiceManager(binder);
}
}
}
}
return sInstance;
}
public String getRomVersion() {
try {
return mService.getRomVersion();
} catch (RemoteException e) {
Log.e(TAG, "Failed to get ROM version", e);
return "unknown";
}
}
public long getBuildTimestamp() {
try {
return mService.getBuildTimestamp();
} catch (RemoteException e) {
Log.e(TAG, "Failed to get build timestamp", e);
return 0;
}
}
public void setPreference(String key, String value) {
try {
mService.setPreference(key, value);
} catch (RemoteException e) {
Log.e(TAG, "Failed to set preference", e);
}
}
public String getPreference(String key) {
try {
return mService.getPreference(key);
} catch (RemoteException e) {
Log.e(TAG, "Failed to get preference", e);
return null;
}
}
public List<String> getEnabledFeatures() {
try {
return mService.getEnabledFeatures();
} catch (RemoteException e) {
Log.e(TAG, "Failed to get enabled features", e);
return Collections.emptyList();
}
}
}
65.5.5 自定义服务 SELinux 策略
新增系统服务必须配置 SELinux 策略;现代 Android 默认 SELinux enforcing 模式,缺少策略服务会被拒绝执行。
# device/AospBook/bookphone/sepolicy/vendor/bookservice.te
# Define the BookService type
type bookservice, domain;
type bookservice_exec, exec_type, file_type, system_file_type;
# Allow system_server to register and access the service
allow system_server bookservice_service:service_manager { add find };
# Allow apps to find the service
allow untrusted_app bookservice_service:service_manager find;
allow platform_app bookservice_service:service_manager find;
# Allow the service to read system properties
allow bookservice system_prop:file { read open getattr };
get_prop(bookservice, system_prop)
接着在 service_contexts 中注册该服务:
# device/AospBook/bookphone/sepolicy/vendor/service_contexts
aospbook u:object_r:bookservice_service:s0
在 service.te 里定义服务类型:
# device/AospBook/bookphone/sepolicy/vendor/service.te
type bookservice_service, service_manager_type;
添加文件上下文规则:
# device/AospBook/bookphone/sepolicy/vendor/file_contexts
/product/framework/BookService\.jar u:object_r:system_file:s0
65.5.6 System Service 生命周期
system_server 启动后服务启动阶段顺序:

65.6 自定义开机动画
65.6.1 bootanimation 格式
开机动画是以 ZIP 压缩包形式存放在以下路径之一(查找顺序由 frameworks/base/cmds/bootanimation/BootAnimation.cpp 代码决定):
/apex/com.android.bootanimation/etc/bootanimation.zip/product/media/bootanimation.zip(文件名可通过系统属性ro.product.bootanim.file自定义)/oem/media/bootanimation.zip/system/media/bootanimation.zip
完整格式规范定义在 frameworks/base/cmds/bootanimation/FORMAT.md(注意文档里旧版的两段式路径列表已经过时;上面这套查找顺序才是当前代码的真实逻辑)。
该压缩包内部包含:
bootanimation.zip
desc.txt # 动画描述脚本
part0/ # 第一组动画帧
00000.png
00001.png
...
part1/ # 第二组动画帧
00000.png
00001.png
...
audio.wav # 可选音频,每组part目录可单独带音频
65.6.2 desc.txt 文件
第一行全局参数
WIDTH HEIGHT FPS [PROGRESS]
后续每一行描述一组动画片段
TYPE COUNT PAUSE PATH [FADE [#RGBHEX [CLOCK1 [CLOCK2]]]]
TYPE 取值说明
| 类型 | 行为 |
|---|---|
p | 循环播放直到开机完成停止 |
c | 完整播放一遍,无论开机是否结束 |
f | 类似 p;被中断时执行淡出效果 |
示例 desc.txt
1080 1920 30
c 1 0 part0
p 0 0 part1
含义:
- 分辨率 1080*1920,帧率 30fps;
- part0 完整播放一次无停顿;
- part1 无限循环直到开机结束。
65.6.3 创建自定义开机动画
我先按「AOSP 技术书 / 开源 ROM」做个简单但专业的开机动画。
Step1 准备帧图片
你可以使用任意图像编辑工具(GIMP、Photoshop、Inkscape)制作动画帧,也可以通过代码自动生成帧图片。每一帧都必须是 PNG 格式,分辨率需要和 desc.txt 中定义的保持一致。
# 创建目录
mkdir -p device/AospBook/bookphone/bootanimation/part0
mkdir -p device/AospBook/bookphone/bootanimation/part1
# part0:logo淡入动画,30帧,30fps共1秒
for i in $(seq -w 0 29); do
opacity=$(echo "scale=2; $i / 29 * 100" | bc)
convert -size 1080x1920 xc:black \
-fill white -gravity center \
-pointsize 72 -annotate 0 "AospBook" \
-channel A -evaluate set "${opacity}%" \
"device/AospBook/bookphone/bootanimation/part0/${i}.png"
done
# part1:呼吸点循环动画,60帧=2秒
for i in $(seq -w 0 59); do
phase=$(echo "scale=4; $i / 60 * 3.14159 * 2" | bc)
convert -size 1080x1920 xc:black \
-fill white -gravity center \
-pointsize 48 -annotate 0 "AospBook" \
"device/AospBook/bookphone/bootanimation/part1/${i}.png"
done
Step2 desc.txt
//device/AospBook/bookphone/bootanimation/desc.txt
1080 1920 30
c 1 10 part0
p 0 0 part1
1080 1920 30 -- 分辨率1080×1920,帧率30帧每秒
c 1 10 part0 -- 将part0文件夹内的动画完整播放1次,结束后暂停10帧(0.33秒)
p 0 0 part1 -- 循环播放part1文件夹的动画直到开机完成,循环之间无停顿
Step3 打包 zip,
该压缩包必须采用存储模式(不执行 deflate 压缩),因为 PNG 文件本身已经是压缩格式:
cd device/AospBook/bookphone/bootanimation
# Create the ZIP with store (no compression)
zip -0qry -i \*.txt \*.png \*.wav @ bootanimation.zip *.txt part*
# Verify the contents
unzip -l bootanimation.zip
-0 参数至关重要。如果使用默认压缩方式,开机动画播放器将无法高效读取帧画面,进而出现卡顿或者加载失败的问题。
Step4 加入 ROM 编译(device.mk)
PRODUCT_COPY_FILES += \
device/AospBook/bookphone/bootanimation/bootanimation.zip:$(TARGET_COPY_OUT_PRODUCT)/media/bootanimation.zip
65.6.4 测试开机动画,
你可以测试开机动画,无需全量编译
# adb push到模拟器直接测试
adb root
adb remount
adb push bootanimation.zip /product/media/bootanimation.zip
# 重启bootanim服务播放动画
adb shell setprop service.bootanim.exit 0
adb shell start bootanim
# 停止动画
adb shell setprop service.bootanim.exit 1
65.6.5 带声音的开机动画
每个 part 目录下放 audio.wav,该 part 开始播放时自动播放音频
bootanimation.zip
desc.txt
part0/
audio.wav
00000.png
...
part1/
00000.png
...
wav 格式硬性要求:
- PCM 无压缩
- 16bit /24bit 位深
- 采样率任意,推荐 44100Hz
- 支持单声道、立体声
65.6.6 动态着色(Android12+)
desc.txt 分辨率行之后增加 dynamic_colors 指令
1080 1920 30
dynamic_colors part1 #1A73E8 #34A853 #FBBC04 #EA4335
c 1 10 part0
p 0 0 part1
part1 图片 RGBA 通道会被当作蒙版,真实颜色读取系统属性:
- persist.bootanim.color1
- persist.bootanim.color2
- persist.bootanim.color3
- persist.bootanim.color4
65.6.7 bootanimation 源码位置
播放器源码路径 frameworks/base/cmds/bootanimation/
frameworks/base/cmds/bootanimation/
BootAnimation.cpp # Main animation player
BootAnimation.h
BootAnimationUtil.cpp # Utility functions
bootanimation_main.cpp # Entry point
audioplay.cpp # Audio playback
bootanim.rc # init service definition
The bootanim.rc file defines the init service:
# frameworks/base/cmds/bootanimation/bootanim.rc
service bootanim /system/bin/bootanimation
class core animation
user graphics
group graphics audio
disabled
oneshot
65.7 定制 SystemUI
SystemUI 负责状态栏、通知下拉面板、快捷设置、锁屏、导航栏,是 ROM 定制中用户感知最强的模块。
源码路径:frameworks/base/packages/SystemUI/
65.7.1 SystemUI 架构概览

65.7.2 使用 RRO 定制(低侵入)
自定义 SystemUI 最简便的方式就是使用 RRO(资源覆盖替换)。SystemUI 在自身的 config.xml 中开放了大量可配置项: frameworks/base/packages/SystemUI/res/values/config.xml
创建 SystemUI overlay 目录
device/AospBook/bookphone/overlay/BookSystemUIOverlay/
Android.bp
AndroidManifest.xml
res/
values/
config.xml
dimens.xml
colors.xml
Android.bp
// device/AospBook/bookphone/overlay/BookSystemUIOverlay/Android.bp
runtime_resource_overlay {
name: "BookSystemUIOverlay",
sdk_version: "current",
product_specific: true,
}
AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aospbook.overlay.systemui">
<application android:hasCode="false" />
<overlay
android:targetPackage="com.android.systemui"
android:isStatic="true"
android:priority="10"
/>
</manifest>
res/values/config.xml -- SystemUI configuration overrides:
<?xml version="1.0" encoding="utf‑8"?>
<resources>
<!-- QS快捷设置列数 -->
<integer name="quick_settings_num_columns">4</integer>
<!-- QS最大行数 -->
<integer name="quick_settings_max_rows">3</integer>
<!-- 折叠面板最多显示tile数量 -->
<integer name="quick_qs_panel_max_tiles">6</integer>
<!-- 折叠面板最大行数 -->
<integer name="quick_qs_panel_max_rows">2</integer>
<!-- 导航栏死区开关 -->
<bool name="config_useDeadZone">false</bool>
<!-- 壁纸不可见时导航栏自动变暗 -->
<bool name="config_navigation_bar_enable_auto_dim_no_visible_wallpaper">false</bool>
<!-- 锁屏显示超时毫秒 -->
<integer name="config_lockScreenDisplayTimeout">15000</integer>
<!-- 启用锁屏自定义快捷方式 -->
<bool name="custom_lockscreen_shortcuts_enabled">true</bool>
<!-- 长按锁屏进入自定义设置 -->
<bool name="long_press_keyguard_customize_lockscreen_enabled">true</bool>
</resources>
res/values/dimens.xml -- dimension overrides:
<?xml version="1.0" encoding="utf‑8"?>
<resources>
<!-- 状态栏高度,增大方便触摸 -->
<dimen name="status_bar_height">28dp</dimen>
<!-- 状态栏左右padding -->
<dimen name="status_bar_padding_start">8dp</dimen>
<dimen name="status_bar_padding_end">8dp</dimen>
<dimen name="status_bar_padding_top">0dp</dimen>
<!-- QS tile水平边距 -->
<dimen name="qs_tile_margin_horizontal">4dp</dimen>
<!-- QS圆角半径 -->
<dimen name="qs_corner_radius">16dp</dimen>
</resources>
res/values/colors.xml -- color overrides:
<?xml version="1.0" encoding="utf‑8"?>
<resources>
<!-- 浅色模式状态栏图标颜色 -->
<color name="light_mode_icon_color_single_tone">#FF212121</color>
<!-- 深色模式状态栏图标颜色 -->
<color name="dark_mode_icon_color_single_tone">#FFFAFAFA</color>
</resources>
资源名称描述的是图标着色,而非 UI 模式本身:这两份资源定义在 frameworks/base/packages/SettingsLib/res/values/colors.xml(SettingsLib 以静态链接方式集成到 SystemUI)。其中 light_mode_icon_color_single_tone 为浅色着色值,dark_mode_icon_color_single_tone 为深色着色值。
65.7.3 修改状态栏布局
状态栏布局文件路径:frameworks/base/packages/SystemUI/res/layout/status_bar.xml
根节点 PhoneStatusBarView,主要容器结构:
<PhoneStatusBarView>
<ImageView android:id="@+id/notification_lights_out" />
<LinearLayout android:id="@+id/status_bar_contents">
<FrameLayout android:id="@+id/status_bar_start_side_container">
<!-- 通知图标、时钟 -->
</FrameLayout>
<android.widget.Space />
<LinearLayout android:id="@+id/status_bar_end_side_content">
<!-- wifi、电池等系统图标 -->
</LinearLayout>
</LinearLayout>
</PhoneStatusBarView>
不需要修改源码的定制方式:
- 时钟居中 / 靠右:RRO overlay 替换 layout 文件
- 新增状态栏图标:overlay drawable 资源
- 修改电池图标样式:overlay
battery_percentage_view.xml
深度定制(新增指示器)需要直接修改 SystemUI Java 源码。
65.7.4 修改快捷设置 Tile
QS tile 由 SystemUI Dagger 依赖注入图注册。新增自定义 Tile 步骤:
Step1 编写 Tile 类 BookModeTile.java
// frameworks/base/packages/SystemUI/src/com/android/systemui/qs/tiles/BookModeTile.java
package com.android.systemui.qs.tiles;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.service.quicksettings.Tile;
import android.view.View;
import com.android.internal.logging.MetricsLogger;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.res.R;
import javax.inject.Inject;
/**
* BookMode快捷开关,阅读模式,调节色温降低蓝光
*/
public class BookModeTile extends QSTileImpl<QSTile.BooleanState> {
private boolean mEnabled = false;
@Inject
public BookModeTile(
QSHost host,
@Background Looper backgroundLooper,
@Main Handler mainHandler) {
super(host, backgroundLooper, mainHandler);
}
@Override
public BooleanState newTileState() {
return new BooleanState();
}
@Override
protected void handleClick(View view) {
mEnabled = !mEnabled;
refreshState();
}
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
state.value = mEnabled;
state.label = "Book Mode";
state.contentDescription = "Book Mode";
state.state = mEnabled ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
state.icon = ResourceIcon.get(mEnabled
? R.drawable.ic_book_mode_on
: R.drawable.ic_book_mode_off);
}
@Override
public int getMetricsCategory() {
return MetricsLogger.QS_CUSTOM;
}
@Override
public Intent getLongClickIntent() {
return new Intent("com.aospbook.action.BOOK_MODE_SETTINGS");
}
@Override
public CharSequence getTileLabel() {
return "Book Mode";
}
}
步骤 2:在磁贴工厂中注册该快捷开关
快捷面板磁贴(tile)必须注册到 QSModule 或 QSTileHost 中,才能完成实例化。具体注册机制随 AOSP 版本有所差异 ——新版 AOSP 使用 Dagger 的 @IntoMap 注解实现注册。
65.7.5 SystemUI 主题 Overlay(Material You)
Material You(Android 12 及以上版本)采用动态取色机制。如果要为你的 ROM 设置默认配色方案,可以使用主题覆盖包。 system_accent* 调色板定义在框架资源中(路径:frameworks/base/core/res/res/values/colors_dynamic.xml,包名为 android),因此这类资源覆盖需要放在框架层 RRO(示例:BookFrameworkOverlay,配置 android:targetPackage="android")——目标包为 com.android.systemui 的覆盖包无法修改这些配色。
<!-- device/AospBook/bookphone/overlay/BookFrameworkOverlay/res/values/colors.xml -->
<resources>
<!-- Override the default accent color seed -->
<!-- This affects Material You theming when no wallpaper-extracted color is available -->
<color name="system_accent1_500">#1A73E8</color> <!-- Google Blue -->
<color name="system_accent2_500">#5F6368</color> <!-- Gray -->
<color name="system_accent3_500">#34A853</color> <!-- Green -->
</resources>
65.7.6 导航栏定制
导航栏模式由系统属性 + framework config 控制。在 device.mk 强制手势导航:
# In device.mk -- force gesture navigation as default
PRODUCT_PRODUCT_PROPERTIES += \
ro.boot.vendor.overlay.theme=com.android.internal.systemui.navbar.gestural
# Or force 3-button navigation:
# ro.boot.vendor.overlay.theme=com.android.internal.systemui.navbar.threebutton
导航栏图标资源位于 frameworks/base/packages/overlays/NavigationBarMode*,可以 RRO 覆盖 drawable。
65.7.7 SystemUI 编译集成
SystemUI 编译脚本 frameworks/base/packages/SystemUI/Android.bp。
如需纳入你的修改内容,请确保所有新增源码文件都添加至构建文件中的 srcs 列表,或者放置在已经通过通配符模式纳入编译的目录下。
65.8 编译与刷写镜像
65.8.1 编译命令
完成设备配置之后,编译 ROM:
# 导入环境(如尚未执行)
source build/envsetup.sh
# 选择编译目标
lunch bookphone-trunk_staging-userdebug
# 完整编译
# 'm' 是AOSP编译命令,对Soong/Ninja做了封装
m
# 或者显式指定并行编译线程数:
m -j$(nproc)
m 命令的执行流程:
- 运行 Soong 处理所有
Android.bp文件 - 运行 Kati 处理所有
Android.mk文件 - 在输出目录生成 Ninja 格式的编译构建图
- 调用编译执行器(Android 17 默认使用 Siso;若设置
SOONG_NINJA=ninja则使用传统 Ninja)执行编译方案
65.8.2 编译输出目录结构
编译成功后,产物位于 out/target/product/bookdevice/:
out/target/product/bookdevice/
android-info.txt # fastboot使用的编译信息
boot.img # 内核 + 内存盘ramdisk
vendor_boot.img # 厂商内存盘
super.img # 动态分区容器镜像
system.img # system分区镜像
system_ext.img # system_ext系统扩展分区镜像
vendor.img # vendor厂商分区镜像
product.img # product产品分区镜像
userdata.img # 空userdata用户数据分区镜像
cache.img # cache缓存分区镜像
ramdisk.img # 根内存盘
kernel-ranchu # 内核二进制文件
system/ # system分区的中间暂存目录
vendor/ # vendor分区的中间暂存目录
product/ # product分区的中间暂存目录
obj/ # 编译中间对象文件
symbols/ # 未剥离符号的二进制文件(用于调试)
65.8.3 分区镜像说明

65.8.4 启动模拟器
AOSP 编译体系内置emulator命令,可以直接加载刚刚编译完成的镜像启动 Android 模拟器:
# 使用定制ROM启动模拟器
emulator
# 携带额外参数启动:
emulator \
-gpu swiftshader_indirect \ # 软件GPU渲染(虚拟机无GPU环境可用)
-memory 4096 \ # 分配4GB内存
-cores 4 \ # 4个CPU核心
-no-snapshot \ # 不使用快照,全新启动
-verbose # 输出调试日志
模拟器会自动读取环境变量$ANDROID_PRODUCT_OUT,即out/target/product/bookdevice/目录下的镜像。
常用模拟器参数
| 参数 | 说明 |
|---|---|
-gpu host | 使用主机 GPU 硬件加速(速度最快,需要 GPU 支持) |
-gpu swiftshader_indirect | 软件渲染,所有环境均可运行 |
-memory <MB> | 设置虚拟机内存大小,单位 MB |
-cores <N> | 设置虚拟机 CPU 核心数 |
-no-snapshot | 不使用快速启动快照 |
-wipe-data | 重置 userdata 用户数据分区 |
-writable-system | 允许对 system 分区进行写入 |
-show-kernel | 在终端输出内核日志 |
-logcat '*:V' | 在终端打印 logcat 日志 |
-selinux permissive | 将 SELinux 设置为宽容模式(调试使用) |
65.8.5 向物理设备烧录镜像
物理设备使用 fastboot 工具进行烧录:
# 重启设备进入bootloader引导加载模式
adb reboot bootloader
# 一次性刷写全部镜像
fastboot flashall
# 或者单独刷写各个分区镜像:
fastboot flash boot boot.img
fastboot flash vendor_boot vendor_boot.img
fastboot flash super super.img
# fastboot flash userdata userdata.img # 警告:该命令会清空用户全部数据!
# 重启设备
fastboot reboot
Android10 及以上支持动态分区的设备,需要使用 fastbootd 用户态 fastboot 模式:
# 进入fastbootd(用户空间fastboot)
fastboot reboot fastboot
# 刷写动态分区镜像
fastboot flash system system.img
fastboot flash system_ext system_ext.img
fastboot flash vendor vendor.img
fastboot flash product product.img
# 重启设备
fastboot reboot
65.8.6 增量编译
首次完整编译完成后,增量编译只会重新编译发生变更的模块,编译速度会大幅提升。
# 仅重新编译发生变更的模块
m
# 编译指定单个模块
m BookSampleApp
# 仅编译SystemUI
m SystemUI
# 在模块重新编译完成后,重新打包system镜像
m systemimage
# 重新打包product镜像
m productimage
# 编译指定镜像
make vendorimage
增量编译耗时参考
| 操作 | 耗时 | 编译命令 |
|---|---|---|
| 首次完整编译 | 2‑4 小时 | m |
| 开启 ccache 完整编译 | 30‑60 分钟 | m |
| Java 代码修改后重编译 | 1‑5 分钟 | m 模块名 |
| C++ 代码修改后重编译 | 2‑10 分钟 | m 模块名 |
| 重新打包 system 镜像 | 5‑15 分钟 | m systemimage |
| 资源文件修改重编译 | 1‑3 分钟 | m 模块名 |
| Makefile 脚本修改 | 10‑30 分钟 | m(Soong 重新分析) |
65.8.7 编译变体及其影响
表格
| 编译变体 | ro.debuggable | adb root | 优化 | 使用场景 |
|---|---|---|---|---|
| user | 0 | 关闭 | 完整优化(开启 Proguard、代码压缩) | 正式发布版本 |
| userdebug | 1 | 开启 | 部分优化,保留部分调试信息 | 开发调试 |
| eng | 1 | 开启,默认 root | 最小优化,不开启 Proguard | 深度调试 |
编译变体影响:
adb root是否可用- system 分区是否可写
- Proguard/R8 代码混淆优化等级
- 是否包含 strace、valgrind 等调试工具
- SELinux 模式(eng 版本有时默认宽容模式 permissive)
# 编译release正式user版本镜像
lunch bookphone-trunk_staging-user
m
# 编译调试eng版本镜像
lunch bookphone-trunk_staging-eng
m
65.8.8 编译系统问题排查
常见编译报错与解决方案
| 报错 | 根因 | 处理方案 |
|---|---|---|
| No rule to make target | 模块未找到 | 检查PRODUCT_PACKAGES以及Android.bp中的模块名称 |
| ninja: error: depends on nonexistent | 依赖缺失 | 在static_libs或shared_libs添加对应依赖 |
| SELinux denials | SELinux 策略缺失 | 添加 allow 规则,使用audit2allow生成策略 |
| FAILED: out/.../module.jar | Java 编译错误 | 检查源码语法、导入包 |
| Insufficient disk space | 编译产物磁盘占满 | 清理磁盘或将out/目录迁移至更大磁盘分区 |
| Killed (out of memory) | 链接阶段内存 OOM | 降低‑j并行线程数或者配置 swap 交换分区 |
65.8.9 16KB 页大小
Android17 ROM 中一个会影响所有原生二进制程序的改动:迁移至16KB 内存页。现代 arm64 SoC MMU 可以使用 16KB 基础内存页,替代传统 4KB 页;减少 TLB 缓存缺失,提升整体吞吐量。 这要求所有原生代码(.so动态库、可执行程序)的加载段必须按 16KB 对齐。
在 64 位目标设备上,编译系统默认开启该特性。build/make/core/config.mk配置 arm64、x86_64 支持最大页大小为 16384。
# build/make/core/config.mk(关键片段)
else ifeq (,$(filter arm64 x86_64,$(TARGET_ARCH)))
# > 4096 仅支持arm64与x86_64架构
TARGET_MAX_PAGE_SIZE_SUPPORTED := 4096
else
# 用户空间二进制默认对齐大小为16384
TARGET_MAX_PAGE_SIZE_SUPPORTED := 16384
endif
通过 Soong 编译生成的二进制已经兼容 16KB 页。风险点来自预编译产物:嵌入预编译 APK 内部的.so库、老工具链编译的 vendor 厂商二进制 blob,这些以 4KB 对齐的库在 4KB 内核可以正常加载,但在 16KB 页内核会映射失败。
开发测试阶段的关键配置开关:
| 配置项 | 配置文件位置 | 作用 |
|---|---|---|
TARGET_MAX_PAGE_SIZE_SUPPORTED | build/make/core/config.mk自动配置 | 原生模块段对齐;arm64/x86_64 默认 16384 |
TARGET_BOOTS_16K | BoardConfig.mk | 标记板子实际启动 16KB 页内核 |
PRODUCT_16K_DEVELOPER_OPTION | product makefile | 开启开发者选项 “使用 16KB 页启动”,设置属性ro.product.build.16k_page.enabled |
模拟器提供了专门的 16KB 页产品配置,无需真实硬件即可完成对齐校验。 device/generic/goldfish/64bitonly/product/sdk_phone16k_x86_64.mk(以及对应的 arm64 版本)继承了 16KB 硬件板级配置 device/generic/goldfish/board/emu64x16k/details.mk,并搭载 16KB 内核启动参数配置 device/generic/goldfish/board/kernel/x86_64_16k.mk。编译并启动这类目标版本,即可验证预编译文件能否正常完成内存映射:
lunch sdk_phone16k_x86_64-trunk_staging-userdebug
m
emulator
# 在设备上确认运行时页大小
adb shell getconf PAGE_SIZE # 16KB构建会输出16384
adb shell getprop ro.product.build.16k_page.enabled
本次校验仅使用 getconf PAGE_SIZE。系统属性 ro.product.build.16k_page.enabled 仅用于标识产品是否开启了 PRODUCT_16K_DEVELOPER_OPTION(双启动开发者开关),不能用来判断镜像本身是否为 16KB 页对齐。以 sdk_phone16k_x86_64 为例:该产品并未配置此选项,因此该属性值为 false,但实际编译产物本身就是真正的 16KB 页版本。
如果预编译产物在 16KB 页设备上加载失败,可以采用两种方案:基于源码重新编译(编译流程会自动适配 16KB 页对齐),或是在重链接时指定 max‑page‑size 为 16KB。将全部预编译文件校验为支持 16KB 页,已经是 Android 17 ROM 版本升级时的标准流程。
65.9 ROM 调试
65.9.1 logcat —— 首要调试工具
logcat 是 Android 通用调试工具,可以读取内核环形缓冲区与 Android 日志守护进程输出。
# 基础logcat,输出全部日志
adb logcat
# 根据Tag过滤日志
adb logcat -s BookService:V
# 根据日志级别过滤(Verbose、Debug、Info、Warn、Error、Fatal)
adb logcat '*:W' # 仅输出警告及更高级别日志
# 多Tag同时过滤
adb logcat BookService:V BookSampleApp:D '*:S'
# 日志输出格式选项
adb logcat -v threadtime # 输出线程ID与时间戳
adb logcat -v color # 彩色输出
adb logcat -v long # 详细格式输出
# 保存日志至文件
adb logcat -d > logcat.txt # 抓取全部日志并退出
adb logcat -f /sdcard/logcat.txt # 日志直接写至设备存储
# 清空日志缓冲区
adb logcat -c
# 查看内核日志(等价dmesg)
adb logcat -b kernel
65.9.2 dumpsys —— 查询系统服务状态
dumpsys 打印系统服务内部状态,排查服务问题的核心工具。
# 列出全部可用系统服务
adb shell dumpsys -l
# 查询指定系统服务
adb shell dumpsys activity
adb shell dumpsys window
adb shell dumpsys package com.aospbook.sample
adb shell dumpsys overlay # RRO覆盖层状态
# 自定义服务(实现dump()接口后)
adb shell dumpsys aospbook
# 设置超时时间,用于卡死的服务
adb shell dumpsys -t 10 activity
# ROM调试常用dumpsys目标
adb shell dumpsys activity activities # Activity任务栈
adb shell dumpsys window displays # 显示信息
adb shell dumpsys package # 全部包信息
adb shell dumpsys meminfo # 内存占用
adb shell dumpsys battery # 电池状态
adb shell dumpsys alarm # 闹钟定时任务
adb shell dumpsys jobscheduler # JobScheduler定时任务
adb shell dumpsys notification # 通知状态
65.9.3 bugreport —— 完整系统快照
bugreport 抓取某一时刻完整系统状态:
# Generate a bugreport (saves to device, then pulls)
adb bugreport bugreport.zip
# The ZIP contains:
# bugreport-<device>-<date>.txt # Main report (huge)
# dumpstate_board.bin # Board-specific dump
# FS/ # File system snapshots
# proto/ # Protobuf data
bugreport 整合 logcat、所有 dumpsys 输出、内核日志、进程列表、文件系统信息等,是问题分析最重要的产物。
65.9.4 Perfetto —— 性能追踪
Perfetto 是 AOSP 新一代追踪系统,替代旧版 systrace 工具。
# 录制10秒常用分类的性能trace
adb shell perfetto \
--txt \
--config - \
--out /data/misc/perfetto-traces/trace.perfetto-trace \
<< 'EOF'
buffers: {
size_kb: 63488
fill_policy: RING_BUFFER
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "power/suspend_resume"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_wakeup_new"
ftrace_events: "sched/sched_process_exit"
ftrace_events: "sched/sched_process_free"
ftrace_events: "task/task_newtask"
ftrace_events: "task/task_rename"
atrace_categories: "am"
atrace_categories: "wm"
atrace_categories: "view"
atrace_categories: "gfx"
atrace_categories: "input"
}
}
}
data_sources: {
config {
name: "linux.process_stats"
}
}
duration_ms: 10000
EOF
# 将trace文件拉取到主机
adb pull /data/misc/perfetto‑traces/trace.perfetto‑trace .
# 在Perfetto网页UI打开
# https://ui.perfetto.dev/
65.9.5 Winscope —— 窗口与图层追踪
Winscope 用于捕获窗口管理器与 SurfaceFlinger 的状态变更事件,是排查 UI 布局类问题的关键工具:
如今两类追踪数据全部通过 Perfetto 数据源完成采集。原有旧接口已经移除:执行窗口追踪的 Shell 命令只会输出提示信息「Shell commands are ignored. Any type of action should be performed through perfetto.」(源码位置:frameworks/base/services/core/java/com/android/server/wm/WindowTracingPerfetto.java);而调用 SurfaceFlinger 服务编号 1025 的图层追踪切换接口时,会返回 NAME_NOT_FOUND(源码位置:frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp)。
# Write a Perfetto config enabling the WM and SF data sources:
cat > winscope.cfg <<'EOF'
buffers { size_kb: 65536 fill_policy: RING_BUFFER }
data_sources { config { name: "android.windowmanager" } }
data_sources { config { name: "android.surfaceflinger.layers" } }
data_sources { config { name: "android.surfaceflinger.transactions" } }
duration_ms: 30000
EOF
# Capture (reproduce the issue while it runs):
adb push winscope.cfg /data/misc/perfetto-configs/winscope.cfg
adb shell perfetto --txt -c /data/misc/perfetto-configs/winscope.cfg \
-o /data/misc/perfetto-traces/winscope.perfetto-trace
# Collect the trace
adb pull /data/misc/perfetto-traces/winscope.perfetto-trace .
# Open the .perfetto-trace in the Winscope / Perfetto UI:
# https://ui.perfetto.dev/
# Or use the local Winscope included in the AOSP tree:
# development/tools/winscope/
65.9.6 开机问题调试
定制 ROM 无法启动排查逻辑:

常见启动故障与处理
- 开机动画无限循环
# 查看system_server崩溃日志
adb logcat -b all | grep -E "FATAL|System.err|AndroidRuntime" | head -50
# 最常见原因:SELinux策略缺失
# 调试时临时切换为宽容模式
adb shell setenforce 0
# 查找avc拒绝日志
adb logcat | grep "avc: denied"
- 自定义服务发生崩溃
# 观察system_server是否反复重启
adb logcat -s ActivityManager | grep "Start proc"
# 查看目标服务日志
adb logcat -s BookService
# 完整异常堆栈
adb logcat -s AndroidRuntime
- 模块 / 库文件缺失
# 检查产物是否部署到设备
adb shell ls /product/app/BookSampleApp/
adb shell ls /system/framework/BookService.jar
# 核对编译输出目录
ls out/target/product/bookdevice/product/app/BookSampleApp/
65.9.7 SELinux 调试
SELinux 权限拒绝是定制 ROM 最常见的问题。
# 查看SELinux运行模式
adb shell getenforce
# 预期输出:Enforcing(强制模式)
# 查看SELinux拒绝日志
adb logcat | grep "avc: denied"
# 示例拒绝日志
# avc: denied { add } for service=aospbook pid=1234
# scontext=u:r:system_server:s0
# tcontext=u:object_r:default_android_service:s0
# tclass=service_manager
# 使用audit2allow根据拒绝日志生成策略规则
adb logcat -d | grep "avc: denied" | audit2allow -p out/target/product/bookdevice/vendor/etc/selinux/
# 将输出的allow规则添加至对应.te策略文件
65.9.8 调试工具链

65.9.9 ROM 开发常用 adb 命令
# ==============================
# 系统属性
# ==============================
adb shell getprop ro.build.fingerprint
adb shell getprop ro.aospbook.version
adb shell getprop | grep aospbook
# ==============================
# 包管理
# ==============================
adb shell pm list packages | grep aospbook
adb shell pm path com.aospbook.sample
adb shell pm dump com.aospbook.sample | head -50
# ==============================
# 系统服务管理
# ==============================
adb shell service list | grep aospbook
adb shell service check aospbook
# ==============================
# 进程信息
# ==============================
adb shell ps -A | grep -E "system_server|aospbook"
adb shell dumpsys meminfo system_server
# ==============================
# 文件系统
# ==============================
adb shell mount | grep -E "system|vendor|product"
adb shell df -h
adb shell ls -la /product/overlay/
# ==============================
# RRO覆盖层状态
# ==============================
adb shell cmd overlay list
adb shell cmd overlay dump com.aospbook.overlay.framework
adb shell cmd overlay enable com.aospbook.overlay.framework
adb shell cmd overlay disable com.aospbook.overlay.framework
# ==============================
# 启动相关诊断
# ==============================
adb shell dmesg | tail -100
adb shell cat /proc/bootconfig
adb shell cat /proc/cmdline
adb shell uptime
65.10 ROM 发布打包
65.10.1 签名概述
Android 使用代码签名保证 APK 与系统镜像完整性、真实性。AOSP 内置 4 组关键签名密钥。
| 密钥 | 文件对 | 用途 |
|---|---|---|
| testkey | testkey.pk8 / testkey.x509.pem | 默认开发调试签名 |
| platform | platform.pk8 / platform.x509.pem | android.uid.system系统应用 |
| shared | shared.pk8 / shared.x509.pem | 共享 UID 数据的应用(联系人、电话) |
| media | media.pk8 / media.x509.pem | 媒体 / 下载类应用 |
默认测试密钥存放路径:build/make/target/product/security/
重要提示:该目录下的密钥是公开的测试密钥。使用这类密钥发布的 ROM 存在极高安全风险 —— 任何人都可以使用相同密钥对恶意 APK 进行签名,该恶意程序会被系统当作系统更新信任并安装。
build/make/target/product/security/README 文档中明确给出警告:
本目录中的测试密钥仅用于开发阶段,严禁在对外发布的镜像中用来给程序包签名。
65.10.2 生成发布版本密钥
生成专属密钥对:
# 创建密钥存放目录
mkdir -p device/AospBook/bookphone/keys
cd device/AospBook/bookphone/keys
# AOSP自带make_key工具
SUBJECT='/C=US/ST=California/L=Mountain View/O=AospBook/OU=ROM/CN=AospBook/emailAddress=rom@aospbook.example.com'
# 生成各组密钥对
# 正式发布密钥建议设置强密码;开发调试可直接回车不设置密码
../../../../../../development/tools/make_key releasekey "$SUBJECT"
../../../../../../development/tools/make_key platform "$SUBJECT"
../../../../../../development/tools/make_key shared "$SUBJECT"
../../../../../../development/tools/make_key media "$SUBJECT"
# 校验生成文件
ls -la
# 预期输出文件
# releasekey.pk8
# releasekey.x509.pem
# platform.pk8
# platform.x509.pem
# shared.pk8
# shared.x509.pem
# media.pk8
# media.x509.pem
make_key每执行一次输出两份文件:
*.pk8:PKCS#8 DER 格式私钥*.x509.pem:X.509 PEM 格式公钥证书
development/tools/make_key支持第三个可选参数指定密钥算法,默认 rsa;也支持 ec 椭圆曲线密钥。APK 签名 v3/v4、AVB 都支持 EC 密钥。示例:make_key platform "$SUBJECT" ec。生成后.pk8 / .x509.pem的使用方式完全一致。
65.10.3 配置编译使用发布密钥
修改device/AospBook/bookphone/bookphone.mk(或者device.mk),替换默认 testkey:
# 使用自定义签名密钥
PRODUCT_DEFAULT_DEV_CERTIFICATE := device/AospBook/bookphone/keys/releasekey
# APK签名密钥映射关系
PRODUCT_CERTIFICATE_OVERRIDES := \
testkey:device/AospBook/bookphone/keys/releasekey \
platform:device/AospBook/bookphone/keys/platform \
shared:device/AospBook/bookphone/keys/shared \
media:device/AospBook/bookphone/keys/media
65.10.4 编译产物签名
有两种签名方案:
方案 1:编译过程中直接签名(开发阶段)
配置好PRODUCT_DEFAULT_DEV_CERTIFICATE,编译阶段所有 APK 会自动签名,流程最简单。
方案 2:编译完成后再签名(正式发布流程)
正式版本推荐:先编译未签名产物,再单独执行签名。
# 步骤1:编译target‑files完整包
m dist
# target‑files输出路径:
# out/dist/bookphone‑target_files‑<build_id>.zip
# 步骤2:对target‑files包中所有APK重签名
# -d <dir>:密钥目录映射,原本使用testkey/platform/shared/media签名的APK,替换为目录内对应密钥
# -o:同时替换OTA校验密钥
python3 build/make/tools/releasetools/sign_target_files_apks.py \
-o \
-d device/AospBook/bookphone/keys \
out/dist/bookphone‑target_files‑*.zip \
out/dist/bookphone‑target_files‑signed.zip
# 步骤3:从已签名target‑files生成可刷写镜像包
python3 build/make/tools/releasetools/img_from_target_files.py \
out/dist/bookphone‑target_files‑signed.zip \
out/dist/bookphone‑img‑signed.zip
65.10.5 OTA 升级包生成
OTA(Over‑The‑Air)实现给已部署设备分发更新。
完整 OTA 包(包含全部镜像)
# 从已签名target‑files生成完整OTA
python3 build/make/tools/releasetools/ota_from_target_files.py \
--package_key device/AospBook/bookphone/keys/releasekey \
out/dist/bookphone‑target_files‑signed.zip \
out/dist/bookphone‑ota‑full.zip
增量 OTA 包(仅保存新旧版本差异部分)
# v1版本升级到v2版本的增量OTA
python3 build/make/tools/releasetools/ota_from_target_files.py \
--package_key device/AospBook/bookphone/keys/releasekey \
-i out/dist/bookphone‑v1‑target_files‑signed.zip \
out/dist/bookphone‑v2‑target_files‑signed.zip \
out/dist/bookphone‑ota‑v1‑to‑v2.zip
build/make/tools/releasetools/ 目录下还提供了若干轻量 OTA 辅助脚本,在 ROM 移植调试阶段十分实用:
ota_from_raw_img.py 可以直接基于一组 .img 镜像文件构建 A/B 升级负载包,无需完整的 target‑files 压缩包。当仅需要推送一两个重新分区后的镜像时,该工具非常便捷。
create_brick_ota.py 用于生成一个恢复包,它会擦除 AVB /vbmeta 分区,刻意使设备变砖(多用于返修 RMA、安全擦除流程)。这也体现了一个本质:OTA 升级负载本质就是一套经过签名的分区操作指令集。
以上工具作为补充方案,并不会替代本节后文介绍的 ota_from_target_files.py 标准编译流程。
OTA 完整发布流水线

65.10.6 OTA 包内部结构
OTA 是一个经过签名的 zip 压缩包:
ota_package.zip
META‑INF/
com/
android/
metadata.pb # OTA元数据protobuf
metadata # 旧版元数据
google/
android/
update‑binary # OTA安装二进制程序
updater‑script # 安装脚本
payload.bin # 真正的更新payload数据
payload_properties.txt # payload元信息
care_map.pb # dm‑verity块映射信息
65.10.7 Verified Boot 与 AVB 验证启动
Android Verified Boot(AVB)保证 boot 镜像以及各个分区没有被篡改。定制 ROM 适配实体设备需要配置 AVB。
# 生成AVB签名密钥
openssl genrsa -out avb_custom_key.pem 4096
# 提取公钥,供bootloader内置
avbtool extract_public_key --key avb_custom_key.pem --output avb_custom_key.bin
在BoardConfig.mk配置 AVB 密钥:
# 使用自定义AVB密钥
BOARD_AVB_KEY_PATH := device/AospBook/bookphone/keys/avb_custom_key.pem
BOARD_AVB_ALGORITHM := SHA256_RSA4096
65.10.8 Build Fingerprint 编译标识与系统属性
Build fingerprint 是 ROM 唯一标识,格式: BRAND/PRODUCT/DEVICE:VERSION/BUILD_ID/BUILD_NUMBER:VARIANT/KEYS
示例: AospBook/bookphone/bookdevice:17/BP1A.250505.005/eng.builder.20250505:userdebug/release‑keys
在产品 mk 文件中自定义属性:
# 自定义build属性
PRODUCT_PROPERTY_OVERRIDES += \
ro.build.display.id=AospBook‑1.0‑$(shell date +%Y%m%d) \
ro.build.version.incremental=$(shell date +%Y%m%d%H%M%S) \
ro.aospbook.version=1.0.0
当前版本的 AOSP 已经不再提供 PRODUCT_BUILD_PROP_OVERRIDES 变量。如需配置 build.prop 参数,请按照上文方式使用 PRODUCT_SYSTEM_PROPERTIES / PRODUCT_PRODUCT_PROPERTIES / PRODUCT_PROPERTY_OVERRIDES。
而 release‑keys / test‑keys 标识来源于 Make 变量 BUILD_VERSION_TAGS,该变量由 build/make/core/sysprop.mk 读取;它由编译环境赋值,无法通过产品配置 mk 文件进行修改。
65.10.9 ROM 对外发布检查清单
公开发布定制 ROM 之前逐项确认:
- 生成独立发布密钥(禁止使用测试 testkey)
- 全部 APK 使用 release 发布密钥签名
- 使用
user变体编译(禁止 userdebug /eng) - 确认 SELinux 处于 Enforcing 强制模式:
adb shell getenforce - 移除所有调试工具、后门
- 完整 OTA 与增量 OTA 更新路径测试
- 全部应用正常启动,功能可用
- 权限功能全部正常
- 开机时间、基础性能测试
- 为所有发布文件生成 SHA256 校验和
- 编写 Release Notes 记录变更
- 部署 OTA 更新服务器
65.10.10 发布包校验和生成
# 生成发布文件sha256校验
sha256sum out/dist/bookphone‑ota‑full.zip > checksums.txt
sha256sum out/dist/bookphone‑img‑signed.zip >> checksums.txt
# 使用GPG对校验文件签名增强可信度
gpg --sign --armor checksums.txt
65.11 进阶:内核定制
65.11.1 AOSP 中的内核
AOSP 模拟器使用 GKI 通用内核镜像。x86_64 模拟器内核配置定义在:device/generic/goldfish/board/kernel/x86_64.mk
片段示例:
# device/generic/goldfish/board/kernel/x86_64.mk(关键片段)
TARGET_KERNEL_USE ?= 6.12
KERNEL_ARTIFACTS_PATH := prebuilts/qemu‑kernel/x86_64/$(TARGET_KERNEL_USE)
EMULATOR_KERNEL_FILE := $(KERNEL_ARTIFACTS_PATH)/kernel‑$(TARGET_KERNEL_USE)
预编译内核存放目录:prebuilts/qemu‑kernel/x86_64/6.12/
prebuilts/qemu‑kernel/x86_64/6.12/
kernel‑6.12 # 内核二进制
gki_modules/ # GKI内核模块
goldfish_modules/ # 模拟器专用内核模块
65.11.2 GKI 通用内核架构
GKI 将内核拆分为:

KMI(内核模块接口)是 GKI 通用内核与厂商模块之间一套稳定的应用二进制接口,支持二者各自独立升级。
65.11.3 编译自定义内核
为模拟器编译自定义内核:
# 拉取内核源码
mkdir -p ~/kernel && cd ~/kernel
repo init -u https://android.googlesource.com/kernel/manifest \
-b common‑android‑mainline
repo sync -j$(nproc)
# 使用Kleaf(基于Bazel,新版标准编译方案;旧版BUILD_CONFIG build/build.sh已废弃)
# x86_64 GKI内核编译
tools/bazel run //common:kernel_x86_64_dist -- --dist_dir=out/x86_64/dist
# arm64版本命令
# tools/bazel run //common:kernel_aarch64_dist -- --dist_dir=out/aarch64/dist
Kleaf 通过 Bazel 规则封装内核构建流程;tools/bazel 是仓库内提交保存的启动脚本,各类 *_dist 编译目标会将内核镜像与模块输出到你指定的 --dist_dir 目录中。 原先存放在 build.config.* 文件里的构建配置,现在改为在内核源码树中以 Bazel 的 kernel_build 目标来定义。
内核构建产物会输出至你传入的 --dist_dir 路径:
Kleaf 将编译产物输出到--dist_dir指定目录:
out/x86_64/dist/
bzImage # x86_64内核可执行
vmlinux # 未压缩内核(调试使用)
System.map # 符号映射表
*.ko # 内核模块
65.11.4 模拟器加载自定义内核
# 方式1:替换goldfish预编译内核
cp out/x86_64/dist/bzImage \
prebuilts/qemu‑kernel/x86_64/6.12/kernel‑6.12
# 方式2:模拟器启动参数直接指定内核文件
emulator -kernel /path/to/custom/bzImage
# 方式3:修改编译配置文件x86_64.mk
# EMULATOR_KERNEL_FILE := /path/to/custom/kernel
65.11.5 添加自定义内核模块
内核模块可以扩展内核能力,不需要完整重编内核。模拟器厂商模块加载来自 vendor ramdisk。
示例简单内核模块源码 device/AospBook/bookphone/kernel_modules/bookmodule/bookmodule.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AospBook");
MODULE_DESCRIPTION("AospBook sample kernel module");
MODULE_VERSION("1.0");
static int __init bookmodule_init(void)
{
printk(KERN_INFO "BookModule: Loaded (AospBook ROM kernel module)\n");
return 0;
}
static void __exit bookmodule_exit(void)
{
printk(KERN_INFO "BookModule: Unloaded\n");
}
module_init(bookmodule_init);
module_exit(bookmodule_exit);
外部模块 Makefile:
# device/AospBook/bookphone/kernel_modules/bookmodule/Makefile
obj-m += bookmodule.o
KERNEL_SRC ?= /path/to/kernel/source
all:
$(MAKE) -C $(KERNEL_SRC) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_SRC) M=$(PWD) clean
编译与模拟器临时测试:
cd device/AospBook/bookphone/kernel_modules/bookmodule
make KERNEL_SRC=~/kernel/common ARCH=x86_64 CROSS_COMPILE=x86_64‑linux‑gnu‑
# 产物 bookmodule.ko
ls bookmodule.ko
# 模拟器上测试
adb root
adb push bookmodule.ko /data/local/tmp/
adb shell insmod /data/local/tmp/bookmodule.ko
adb shell dmesg | grep BookModule
# 预期打印:BookModule: Loaded (AospBook ROM kernel module)
# 卸载模块
adb shell rmmod bookmodule
65.11.6 将内核模块集成进 ROM 编译
两种部署方式,修改device.mk:
# In device.mk
PRODUCT_COPY_FILES += \
device/AospBook/bookphone/kernel_modules/bookmodule/bookmodule.ko:$(TARGET_COPY_OUT_VENDOR)/lib/modules/bookmodule.ko
# Or add to the vendor ramdisk modules (loaded at boot):
BOARD_VENDOR_RAMDISK_KERNEL_MODULES += \
device/AospBook/bookphone/kernel_modules/bookmodule/bookmodule.ko
如果希望开机自动加载,写 init rc 脚本:
# device/AospBook/bookphone/init/init.bookphone.rc
on boot
insmod /vendor/lib/modules/bookmodule.ko
65.11.7 内核配置调优
内核配置(defconfig)用来控制哪些功能会被编译进内核。针对模拟器场景:
# 从运行设备导出当前内核配置
adb shell cat /proc/config.gz | gunzip > current_config.txt
# 或者从内核编译树获取
cat out/android‑mainline/.config
定制 ROM 常用内核配置项
| 配置 | 默认 | 推荐修改 | 作用 |
|---|---|---|---|
CONFIG_HZ | 250 | 1000 | 更高时钟节拍,提升响应 |
CONFIG_SCHED_AUTOGROUP | n | y | 调度自动分组 |
CONFIG_TCP_CONG_BBR | n | y | BBR 拥塞控制算法 |
CONFIG_ZRAM | m | y | zram 内存压缩 swap |
CONFIG_KSM | n | y | 内核同页合并 |
CONFIG_TRANSPARENT_HUGEPAGE | n | y | 透明巨页 |
65.11.8 内核模块加载顺序

Goldfish 模拟器在 device/generic/goldfish/board/kernel/x86_64.mk 中定义了专属的内存盘(ramdisk)模块,这些模块是设备早期启动阶段必不可少的组件。
RAMDISK_KERNEL_MODULES := \
virtio_dma_buf.ko \
virtio‑rng.ko
RAMDISK_SYSTEM_KERNEL_MODULES := \
virtio_blk.ko \
virtio_console.ko \
virtio_pci.ko \
virtio_pci_legacy_dev.ko \
virtio_pci_modern_dev.ko \
vmw_vsock_virtio_transport.ko
这些模块会在第一阶段初始化(first‑stage init)期间加载,此时 system 分区甚至还未挂载;它们提供访问磁盘所需的虚拟硬件驱动。
65.12 进阶:HAL 硬件抽象层定制
65.12.1 Android HAL 架构
HAL 层位于 Android Framework 与 Linux 内核之间,提供稳定硬件访问接口。

现代 Android 使用 AIDL HAL(替代老旧的 HIDL HAL)。HAL 接口定义存放于: hardware/interfaces/
该目录包含音频、相机、传感器、健康信息、图形以及数十种其他硬件子系统的 AIDL 定义文件。
Goldfish 模拟器在独立路径下提供了它自己的 HAL 实现:
device/generic/goldfish/hals/
audio/ # Audio HAL
camera/ # Camera HAL
fingerprint/ # Fingerprint HAL
gnss/ # GNSS GPS HAL
gralloc/ # 图形内存分配
hwc3/ # Hardware Composer 3
radio/ # 通话Radio HAL
sensors/ # Sensors传感器HAL
65.12.2 创建自定义 AIDL HAL
案例:实现一个 “阅读灯 book light” 自定义 HAL,完整流程:接口 AIDL 定义、C++ 默认实现、init 服务、SELinux 策略、VINTF manifest 配置。
目录树:
device/AospBook/bookphone/hal/booklight/
Android.bp
aidl/
com/aospbook/hardware/light/
IBookLight.aidl
BookLightState.aidl
default/
Android.bp
BookLight.cpp
BookLight.h
booklight‑default.rc
booklight‑default.xml
65.12.3 定义 AIDL 接口
IBookLight.aidl
// device/AospBook/bookphone/hal/booklight/aidl/com/aospbook/hardware/light/IBookLight.aidl
package com.aospbook.hardware.light;
import com.aospbook.hardware.light.BookLightState;
/**
* HAL interface for the AospBook reading light.
* This is a custom AIDL HAL that demonstrates how to define
* and implement a hardware abstraction layer.
*/
@VintfStability
interface IBookLight {
/**
* Get the current light state.
*/
BookLightState getState();
/**
* Set the light brightness (0‑255).
*/
void setBrightness(int brightness);
/**
* Set the color temperature in Kelvin (2700‑6500).
*/
void setColorTemperature(int kelvin);
/**
* Enable or disable the reading light.
*/
void setEnabled(boolean enabled);
/**
* Get the supported color temperature range.
* Returns [min, max] in Kelvin.
*/
int[] getSupportedTemperatureRange();
}
BookLightState.aidl
// device/AospBook/bookphone/hal/booklight/aidl/com/aospbook/hardware/light/BookLightState.aidl
package com.aospbook.hardware.light;
/**
* Parcelable representing the current state of the book light.
*/
@VintfStability
parcelable BookLightState {
boolean enabled;
int brightness; // 0‑255
int colorTemperature; // Kelvin (2700‑6500)
}
AIDL 接口编译Android.bp
aidl_interface {
name: "com.aospbook.hardware.light",
vendor_available: true,
srcs: ["aidl/com/aospbook/hardware/light/*.aidl"],
stability: "vintf",
backend: {
cpp: {
enabled: true,
},
java: {
enabled: true,
sdk_version: "module_current",
},
ndk: {
enabled: true,
},
},
versions: ["1"],
}
65.12.4 HAL 默认实现
头文件 BookLight.h
// device/AospBook/bookphone/hal/booklight/default/BookLight.h
#pragma once
#include <aidl/com/aospbook/hardware/light/BnBookLight.h>
#include <aidl/com/aospbook/hardware/light/BookLightState.h>
namespace aidl::com::aospbook::hardware::light {
/**
* Default implementation of the BookLight HAL.
* For the emulator, this is a software‑only implementation that
* stores state in memory. On a real device, this would interface
* with kernel drivers via sysfs or ioctl.
*/
class BookLight : public BnBookLight {
public:
BookLight();
ndk::ScopedAStatus getState(BookLightState* _aidl_return) override;
ndk::ScopedAStatus setBrightness(int32_t brightness) override;
ndk::ScopedAStatus setColorTemperature(int32_t kelvin) override;
ndk::ScopedAStatus setEnabled(bool enabled) override;
ndk::ScopedAStatus getSupportedTemperatureRange(
std::vector<int32_t>* _aidl_return) override;
private:
std::mutex mLock;
BookLightState mState;
static constexpr int32_t kMinTemperature = 2700;
static constexpr int32_t kMaxTemperature = 6500;
static constexpr int32_t kDefaultTemperature = 4000;
static constexpr int32_t kMaxBrightness = 255;
};
} // namespace aidl::com::aospbook::hardware::light
实现文件 BookLight.cpp
// device/AospBook/bookphone/hal/booklight/default/BookLight.cpp
#include "BookLight.h"
#include <android‑base/logging.h>
namespace aidl::com::aospbook::hardware::light {
BookLight::BookLight() {
mState.enabled = false;
mState.brightness = 0;
mState.colorTemperature = kDefaultTemperature;
LOG(INFO) << "BookLight HAL initialized";
}
ndk::ScopedAStatus BookLight::getState(BookLightState* _aidl_return) {
std::lock_guard<std::mutex> lock(mLock);
*_aidl_return = mState;
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus BookLight::setBrightness(int32_t brightness) {
if (brightness < 0 || brightness > kMaxBrightness) {
return ndk::ScopedAStatus::fromExceptionCode(
EX_ILLEGAL_ARGUMENT);
}
std::lock_guard<std::mutex> lock(mLock);
mState.brightness = brightness;
LOG(DEBUG) << "BookLight brightness set to " << brightness;
// On a real device, write to hardware:
// write_to_sysfs("/sys/class/leds/booklight/brightness", brightness);
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus BookLight::setColorTemperature(int32_t kelvin) {
if (kelvin < kMinTemperature || kelvin > kMaxTemperature) {
return ndk::ScopedAStatus::fromExceptionCode(
EX_ILLEGAL_ARGUMENT);
}
std::lock_guard<std::mutex> lock(mLock);
mState.colorTemperature = kelvin;
LOG(DEBUG) << "BookLight color temperature set to " << kelvin << "K";
// On a real device, write to hardware:
// write_to_sysfs("/sys/class/leds/booklight/color_temp", kelvin);
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus BookLight::setEnabled(bool enabled) {
std::lock_guard<std::mutex> lock(mLock);
mState.enabled = enabled;
LOG(DEBUG) << "BookLight " << (enabled ? "enabled" : "disabled");
// On a real device:
// write_to_sysfs("/sys/class/leds/booklight/enable", enabled ? 1 : 0);
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus BookLight::getSupportedTemperatureRange(
std::vector<int32_t>* _aidl_return) {
_aidl_return->clear();
_aidl_return->push_back(kMinTemperature);
_aidl_return->push_back(kMaxTemperature);
return ndk::ScopedAStatus::ok();
}
} // namespace aidl::com::aospbook::hardware::light
服务入口 service.cpp
// device/AospBook/bookphone/hal/booklight/default/service.cpp
#include "BookLight.h"
#include <android‑base/logging.h>
#include <android/binder_manager.h>
#include <android/binder_process.h>
using aidl::com::aospbook::hardware::light::BookLight;
int main() {
// Configure logging
android::base::SetDefaultTag("booklight‑hal");
android::base::SetMinimumLogSeverity(android::base::DEBUG);
LOG(INFO) << "BookLight HAL service starting";
// Start the binder thread pool
ABinderProcess_setThreadPoolMaxThreadCount(0);
// Create the HAL instance
std::shared_ptr<BookLight> bookLight =
ndk::SharedRefBase::make<BookLight>();
// Register with the service manager
const std::string instance =
std::string() + BookLight::descriptor + "/default";
binder_status_t status = AServiceManager_addService(
bookLight‑>asBinder().get(), instance.c_str());
CHECK_EQ(status, STATUS_OK)
<< "Failed to register BookLight HAL service";
LOG(INFO) << "BookLight HAL service registered: " << instance;
// Join the binder thread pool (blocks forever)
ABinderProcess_joinThreadPool();
// Should never reach here
LOG(FATAL) << "BookLight HAL service died unexpectedly";
return EXIT_FAILURE;
}
实现层 Android.bp
// device/AospBook/bookphone/hal/booklight/default/Android.bp
cc_binary {
name: "com.aospbook.hardware.light-service",
relative_install_path: "hw",
vendor: true,
srcs: [
"BookLight.cpp",
"service.cpp",
],
shared_libs: [
"libbase",
"libbinder_ndk",
"liblog",
"com.aospbook.hardware.light-V1-ndk",
],
init_rc: ["booklight-default.rc"],
vintf_fragments: ["booklight-default.xml"],
}
65.12.5 init 服务配置
booklight‑default.rc
# device/AospBook/bookphone/hal/booklight/default/booklight‑default.rc
service vendor.booklight‑default /vendor/bin/hw/com.aospbook.hardware.light‑service
class hal
user system
group system
capabilities SYS_NICE
65.12.6 VINTF 清单片段
VINTF(厂商接口)清单声明当前设备提供哪些 HAL 接口:
booklight‑default.xml
<manifest version="1.0" type="device">
<hal format="aidl">
<name>com.aospbook.hardware.light</name>
<version>1</version>
<interface>
<name>IBookLight</name>
<instance>default</instance>
</interface>
</hal>
</manifest>
65.12.7 HAL 的 SELinux 策略
# device/AospBook/bookphone/sepolicy/vendor/booklight.te
# 定义HAL域
type hal_booklight_default, domain;
type hal_booklight_default_exec, exec_type, vendor_file_type, file_type;
# 允许init启动HAL服务
init_daemon_domain(hal_booklight_default)
# 允许HAL向hwservicemanager完成注册
hal_server_domain(hal_booklight_default, hal_booklight)
# 允许HAL使用binder
binder_use(hal_booklight_default)
# 允许system_server查找并调用HAL
binder_call(system_server, hal_booklight_default)
allow system_server hal_booklight_service:service_manager find;
# HwBinder访问权限
hwbinder_use(hal_booklight_default)
增加文件上下文配置:
# device/AospBook/bookphone/sepolicy/vendor/file_contexts
/vendor/bin/hw/com\.aospbook\.hardware\.light-service u:object_r:hal_booklight_default_exec:s0
65.12.8 将 HAL 纳入编译构建
device.mk
PRODUCT_PACKAGES += \
com.aospbook.hardware.light-service
65.12.9 HAL 测试
# 开机后,验证HAL服务正在运行
adb shell ps -A | grep booklight
# 预期输出:vendor.booklight‑default
# 检查VINTF清单
adb shell cat /vendor/etc/vintf/manifest.xml | grep booklight
# 使用AIDL测试客户端或者调用该HAL的框架服务对HAL服务进行测试
# 检查服务注册情况
adb shell service list | grep booklight
65.12.10 修改现有 HAL
除从零创建全新 HAL 之外,有时需要修改已有 HAL。例如,为模拟器定制传感器 HAL:
现有传感器 HAL 路径:
device/generic/goldfish/hals/sensors/
Android.bp
entry.cpp # HAL入口
multihal_sensors.cpp # 多HAL传感器实现
multihal_sensors_epoll.cpp
multihal_sensors_qemu.cpp
sensor_list.cpp # 可用传感器列表
sensor_list.h
添加自定义传感器(例如 “阅读姿态传感器”):
// 添加至 device/generic/goldfish/hals/sensors/sensor_list.cpp
//(也可以为自定义传感器新建文件)
namespace {
constexpr SensorInfo kCustomSensors[] = {
{
.sensorHandle = 100,
.name = "AospBook Reading Posture Sensor",
.vendor = "AospBook",
.version = 1,
.type = SensorType::ADDITIONAL_INFO,
.typeAsString = "com.aospbook.sensor.reading_posture",
.maxRange = 1.0f,
.resolution = 0.1f,
.power = 0.001f, // 毫安
.minDelay = 100000, // 微秒(10赫兹)
.maxDelay = 1000000,
.fifoReservedEventCount = 0,
.fifoMaxEventCount = 0,
.requiredPermission = "",
.flags = SensorFlagBits::ON_CHANGE_MODE,
},
};
} // namespace
65.12.11 使用 VTS 测试 HAL
厂商测试套件(VTS)用于验证 HAL 实现是否符合接口契约:
# 编译VTS测试
m VtsHalBookLightTargetTest
# 在设备上执行测试
adb push out/target/product/bookdevice/data/nativetest64/VtsHalBookLightTargetTest \
/data/local/tmp/
adb shell /data/local/tmp/VtsHalBookLightTargetTest
65.12.12 完整 HAL 架构

65.13 整合全部组件
65.13.1 完整构建流程
下面是 AospBook ROM 编译、测试、打包的完整执行序列:
#!/bin/bash
# build_aospbook.sh -- AospBook ROM完整构建脚本
set -euo pipefail
AOSP_ROOT=~/aosp
PRODUCT=bookphone
VARIANT=userdebug
RELEASE=trunk_staging
echo "=== Building AospBook ROM ==="
echo "Product: $PRODUCT"
echo "Variant: $VARIANT"
echo "Date: $(date)"
echo ""
# 步骤1:初始化编译环境
cd "$AOSP_ROOT"
source build/envsetup.sh
# 步骤2:选择编译目标
lunch "${PRODUCT}-${RELEASE}-${VARIANT}"
# 步骤3:清理(发布版本编译可选)
# make clean
# 步骤4:执行编译
echo "[BUILD] Starting full build..."
time m -j$(nproc) 2>&1 | tee build.log
# 步骤5:校验编译输出产物
echo ""
echo "[VERIFY] Checking build output..."
OUT_DIR="$ANDROID_PRODUCT_OUT"
for img in boot.img vendor_boot.img super.img system.img vendor.img product.img; do
if [ -f "$OUT_DIR/$img" ]; then
size=$(du -sh "$OUT_DIR/$img" | cut -f1)
echo " OK: $img ($size)"
else
echo " MISSING: $img"
fi
done
# 步骤6:校验自定义内容
echo ""
echo "[VERIFY] Checking custom content..."
if [ -d "$OUT_DIR/product/app/BookSampleApp" ]; then
echo " OK: BookSampleApp installed"
fi
if [ -d "$OUT_DIR/product/app/BookReader" ]; then
echo " OK: BookReader prebuilt installed"
fi
if [ -f "$OUT_DIR/product/media/bootanimation.zip" ]; then
echo " OK: Custom boot animation installed"
fi
if [ -d "$OUT_DIR/product/overlay/BookFrameworkOverlay" ]; then
echo " OK: Framework overlay installed"
fi
if [ -d "$OUT_DIR/product/overlay/BookSystemUIOverlay" ]; then
echo " OK: SystemUI overlay installed"
fi
echo ""
echo "[BUILD] Complete!"
echo "Output directory: $OUT_DIR"
echo ""
echo "To launch the emulator:"
echo " emulator"
echo ""
echo "To generate OTA package:"
echo " m dist"
65.13.2 测试检查清单
编译完成后,系统性校验每一项定制修改:
#!/bin/bash
# test_aospbook.sh -- 在运行的模拟器上校验AospBook ROM
echo "=== AospBook ROM Verification ==="
echo ""
# 1. 校验编译标识
echo "--- Build Identity ---"
adb shell getprop ro.build.display.id
adb shell getprop ro.product.brand
adb shell getprop ro.product.model
adb shell getprop ro.product.device
adb shell getprop ro.aospbook.version
# 2. 校验自定义属性
echo ""
echo "--- Custom Properties ---"
adb shell getprop ro.aospbook.features.dark_mode_default
adb shell getprop ro.aospbook.features.custom_qs
# 3. 校验已安装应用
echo ""
echo "--- Custom Apps ---"
adb shell pm list packages | grep aospbook
adb shell pm path com.aospbook.sample 2>/dev/null && echo " BookSampleApp: OK" || echo " BookSampleApp: MISSING"
# 4. 校验资源覆盖层
echo ""
echo "--- RRO Overlays ---"
adb shell cmd overlay list | grep -A1 aospbook
# 5. 校验自定义服务
echo ""
echo "--- Custom Service ---"
adb shell service list | grep aospbook && echo " BookService: OK" || echo " BookService: NOT FOUND"
# 6. 校验开机动画
echo ""
echo "--- Boot Animation ---"
adb shell ls -la /product/media/bootanimation.zip 2>/dev/null && echo " Custom boot animation: OK" || echo " Custom boot animation: MISSING"
# 7. 校验SELinux状态
echo ""
echo "--- SELinux ---"
adb shell getenforce
# 8. 检查与本项目代码相关的SELinux拒绝日志
echo ""
echo "--- SELinux Denials (our code) ---"
adb logcat -d | grep "avc: denied" | grep -i "aospbook\|book" | tail -5
if [ $? -ne 0 ]; then
echo " No denials found (good!)"
fi
# 9. 校验HAL(如已实现)
echo ""
echo "--- Custom HAL ---"
adb shell ps -A | grep booklight && echo " BookLight HAL: RUNNING" || echo " BookLight HAL: NOT RUNNING"
echo ""
echo "=== Verification Complete ==="
65.13.3 发布版编译流水线

65.13.4 常见问题与解决方案
| 问题场景 | 现象 | 解决方案 |
|---|---|---|
模块未加入PRODUCT_PACKAGES | 应用 / HAL 不在镜像内 | 在device.mk添加模块 |
| 签名密钥不匹配 | 应用安装失败 | 证书需要与sharedUserId保持匹配 |
| 缺少 SELinux 策略 | 服务崩溃,logcat 打印avc: denied | 编写.te规则,使用audit2allow辅助生成 |
| 缺少 VINTF 清单片段 | 框架无法发现 HAL | 在Android.bp添加vintf_fragments |
| Overlay 目标包名错误 | 资源未被覆盖替换 | 检查 manifest 中的android:targetPackage |
| Overlay 非静态 | 用户可以关闭 Overlay | 设置android:isStatic="true" |
| 缺少 Soong 命名空间 | 编译时找不到模块 | 路径加入PRODUCT_SOONG_NAMESPACES |
| 循环依赖 | 编译报错 | 重构模块依赖关系 |
| 切换分支后 ccache 命中率低 | 重编译速度慢 | 属于正常现象,ccache 会逐步重新缓存 |
| 发布版本使用测试密钥 | 存在安全风险 | 生成并使用正式发布密钥 |
65.13.5 AOSP 版本迭代下维护 ROM
定制 ROM 最大的挑战之一是跟进上游 AOSP 变更,下面是可行策略:
- 尽量少修改框架源码:优先使用 RRO 与资源覆盖层,而不是直接修改框架源代码。覆盖层在 AOSP 版本 rebase 合并时冲突更少。
- 设备树改动保持隔离:
device/AospBook/bookphone/下所有文件属于项目自有,不会和上游产生冲突。 - 使用 repo 主题分支管理框架修改,每个功能单独一个主题分支:
# 为框架修改创建主题分支
cd frameworks/base
repo start aospbook-dark-mode .
# 完成修改并提交
git add -A && git commit -m "AospBook: default dark mode"
- 定期 rebase:同步最新 AOSP,对主题分支执行 rebase,手动解决冲突。
repo sync -j$(nproc)
repo rebase
# 处理冲突
- 记录每一处框架修改:维护变更日志,记录每一处框架修改对应的业务目的,rebase 迭代时明确哪些改动需要移植。
65.13.6 完整目录树
下面是 AospBook ROM 设备配置完整目录结构:
device/AospBook/bookphone/
|-- AndroidProducts.mk
|-- BoardConfig.mk
|-- bookphone.mk
|-- device.mk
|-- apps/
| |-- BookSampleApp/
| | |-- Android.bp
| | |-- AndroidManifest.xml
| | |-- res/
| | | |-- layout/activity_main.xml
| | | |-- values/strings.xml
| | | +-- mipmap-xxxhdpi/ic_launcher.png
| | +-- src/com/aospbook/sample/MainActivity.java
| +-- prebuilt/BookReader/
| |-- Android.bp
| +-- BookReader.apk
|-- bootanimation/
| |-- desc.txt
| |-- bootanimation.zip
| |-- part0/
| | |-- 00000.png ... 00029.png
| +-- part1/
| |-- 00000.png ... 00059.png
|-- hal/booklight/
| |-- Android.bp
| |-- aidl/com/aospbook/hardware/light/
| | |-- IBookLight.aidl
| | +-- BookLightState.aidl
| +-- default/
| |-- Android.bp
| |-- BookLight.cpp
| |-- BookLight.h
| |-- service.cpp
| |-- booklight-default.rc
| +-- booklight-default.xml
|-- keys/
| |-- releasekey.pk8
| |-- releasekey.x509.pem
| |-- platform.pk8
| |-- platform.x509.pem
| |-- shared.pk8
| |-- shared.x509.pem
| |-- media.pk8
| +-- media.x509.pem
|-- overlay/
| |-- BookFrameworkOverlay/
| | |-- Android.bp
| | |-- AndroidManifest.xml
| | +-- res/values/config.xml
| +-- BookSystemUIOverlay/
| |-- Android.bp
| |-- AndroidManifest.xml
| +-- res/values/
| |-- config.xml
| |-- dimens.xml
| +-- colors.xml
|-- permissions/
| +-- privapp-permissions-bookphone.xml
|-- sepolicy/vendor/
| |-- booklight.te
| |-- bookservice.te
| |-- file_contexts
| |-- service.te
| +-- service_contexts
+-- services/BookService/
|-- Android.bp
|-- aidl/com/aospbook/service/IBookService.aidl
+-- src/com/aospbook/service/
|-- BookService.java
|-- BookServiceManager.java
+-- BookSystemService.java
65.14 案例研究:MaruOS 融合型定制 ROM
前面十三节搭建了一套通用定制 ROM:基于 AOSP 衍生,包含定制应用、框架微调、定制 SystemUI、自定义内核、发布分发全流程。MaruOS 可以直观展示这套模板可以拓展到什么程度。MaruOS 是开源定制 ROM,设计目标为 “你的手机就是 PC”:外出时它是手机;接上显示器后,设备变为桌面电脑。将支持机型(Pixel、HTC10)通过 USB‑C/HDMI 外接显示器,外部屏幕会完整运行 Debian GNU/Linux 桌面会话,手机屏幕继续运行原生 Android。同一套内核、同一台设备,同时运行两套面向用户的操作系统。

MaruOS 不属于传统定制 ROM,它的价值就在于它脱离了本章前面的模板,作为章节末尾的实例,展示 AOSP 的 Overlay 模型能够给 ROM 开发者带来的拓展空间。
65.14.1 MaruOS 特殊之处
标准定制 ROM(LineageOS、GrapheneOS、/e/OS、ParanoidAndroid)会保留 AOSP 整体架构不变,仅修改内容:替换应用 APK、修改框架默认配置、定制 SystemUI、调整安全策略、对内核进行安全加固。用户所能感知到的运行模型依然是:zygote → Activity 组件 → SystemUI → 桌面启动器。
MaruOS 在手机原生这套运行模型保持不变的基础上,额外叠加了第二套运行环境:一个运行在 LXC 容器内的 Debian 系统,由 Android 端按需拉起。该容器拥有独立的 X11 桌面环境、独立的软件包管理器以及一套完整独立的开机与登录流程。项目 README 中对此有明确描述:
「它采用轻量级操作系统虚拟化(容器)技术,按需启动虚拟系统」——maruos/maruos 项目说明文档
因此除了普通 ROM 自带的定制内容(设备配置仓库、厂商二进制闭源文件、定制品牌应用)之外,MaruOS 还额外提供:
- 一套 LXC 容器管理守护进程,以及配套的 Android 端系统服务。
- 名为 Perspective 的适配层,打通 Android 的显示、输入、音频数据流与容器之间的通路,使容器能够在外接屏幕上输出完整连贯的桌面界面。
- 一套独立于 AOSP 编译体系的 Debian 容器镜像构建流水线。
- 设备接入钩子逻辑,用于判断何时启动容器,以及将桌面画面输出到哪一块显示屏。
- 新增 SELinux 安全策略,防止容器内的 Linux 进程破坏 Android 的安全域隔离机制。
该系统支持的设备列表刻意控制在较小范围;后续衍生版本仅维护少数几代 Pixel 机型以及 HTC 10。其设备配置仓库从 LineageOS 复刻而来,复用现成的硬件适配层,而非从零开始维护。可查看 maruos/manifest 清单获取当前支持机型;不同 maru‑0.x 版本所适配的设备存在差异。
整个项目组织全部采用 Apache‑2.0 开源协议,与 AOSP 保持一致。
65.14.2 双环境运行模型
概念架构是本案例最重要的部分:两套用户空间,共享同一个 Linux 内核,内核之上全部用户态组件相互独立。

三个关键架构要点:
- 单内核:Android 与 Debian 共用内核版本。容器依靠 PID、挂载、网络、IPC、用户 ID 命名空间实现隔离,不会启动独立内核镜像。这是 LXC 的特性:相比虚拟机开销极低,但内核漏洞可以越过容器边界进行逃逸。
- Maru 桥接守护进程运行在 Android 侧:普通 Android 进程(Java/JNI + C/C++ 原生代码),监听显示接入事件,调用 Linux 标准 API 拉起 LXC 容器。守护进程不在容器内部,容器仅感知自身已经被启动。
- 两块显示屏,两套所有者:手机内置屏幕继续渲染 Android SystemUI。外接显示器不再运行 Android UI;SurfaceFlinger 将显示输出转交 Linux 侧 mflinger,显示器上画面完全由X.org与容器桌面环境渲染。桌面运行期间手机正常运行 Android。
这和原生 Android 桌面模式(三星 DeX、Android12 外接窗口模式)存在本质区别:原生桌面模式只存在一套 Android 运行时;MaruOS 在同一个内核上同时运行两套完整操作系统。
65.14.3 LXC 所需内核配置
MaruOS 依靠 LXC 在不启动第二个内核的前提下并行运行 Debian 根文件系统,但是原生 Android 内核默认关闭很多 LXC 必需内核特性。基于 LXC 的定制 ROM 必须在内核镜像开启对应配置项。
MaruOS 非特权 LXC 容器所需内核配置:
| 特性 | 内核配置项 | MaruOS 用途 |
|---|---|---|
| 用户命名空间 | CONFIG_USER_NS=y | 将容器内 root UID 映射为主机高权限非特权 UID,非特权 LXC 基础依赖 |
| PID 命名空间 | CONFIG_PID_NS=y | 容器内部 init 进程 PID 为 1,而不是复用主机 init PID |
| 挂载命名空间 | CONFIG_NAMESPACES=y | 每个容器拥有独立/proc、/sys与根文件系统视图 |
| 网络命名空间 | CONFIG_NET_NS=y | 容器独立网络协议栈 |
| UTS 命名空间 | CONFIG_UTS_NS=y | 容器独立主机名 |
| IPC 命名空间 | CONFIG_IPC_NS=y | 容器独立 System V IPC 与 POSIX 消息队列 |
| Cgroups | CONFIG_CGROUPS=y,开启 memory、cpu、devices、freezer 子系统 | 容器内部资源统计与资源限制 |
| Seccomp | CONFIG_SECCOMP_FILTER=y | LXC 限制容器可以调用的系统调用 |
| uinput(可选) | CONFIG_INPUT_UINPUT=y | 输入桥接层做跨显示屏事件坐标重映射,常规场景不需要 |
| OverlayFS(可选) | CONFIG_OVERLAY_FS=y | 分层容器根文件系统,避免完整复制 |
重点是CONFIG_USER_NS。Android 官方生产内核通常关闭该选项:用户命名空间历史上出现过大量内核 CVE 漏洞;Android 安全策略尽可能裁剪平台不需要的攻击面。LineageOS 设备内核一般继承该默认关闭行为。MaruOS 想要运行非特权 LXC,必须手动打开CONFIG_USER_NS,主动接受由此带来更大攻击面。
其余配置项 Android 内核大多默认开启:ActivityManager 使用 cgroups 内存、freezer 子系统;low‑memory killer 使用 cgroup v2 内存控制器;seccomp 过滤器是 Android 沙箱强制要求;命名空间是现代 Linux 标配。
因此 MaruOS 内核改动范围有限:打开CONFIG_USER_NS;确认其余命名空间与 cgroup 控制器已启用;需要坐标合成输入时开启CONFIG_INPUT_UINPUT,重新编译内核镜像。 这就是 Maru 设备构建会使用 Maru 修改版内核仓库,而不是直接复用 LineageOS 内核。内核定制流程参考 65.11 节:通过 defconfig 片段叠加修改设备基础 defconfig,重新编译 boot 镜像。
代价需要清醒认知:开启CONFIG_USER_NS会打开 AOSP 主动关闭的攻击面。这是 Maru 实现融合模型必须付出的成本。任何计划使用 LXC / 命名空间扩展的 ROM 项目,需要明确威胁模型;非特权 LXC 离不开该内核开关,打开开关必然扩大内核攻击面。
65.14.4 仓库拓扑
MaruOS 的代码分散托管在 maruos/ 组织下的多个代码仓库中。理清这套仓库的拓扑依赖关系,开发者浏览源码时就无需反复猜测模块关联。
下文为基于 maru‑0.7 版本分支绘制的 MaruOS 仓库依赖关系图。

仓库概览(分支 maru‑0.7):
- maruos/manifest:repo 清单,
repo init -u https://github.com/maruos/manifest.git -b maru‑0.7完成初始化。 - maruos/maruos:项目根仓库,README、文档、顶层脚本。许可证 Apache‑2.0。
- maruos/vendor_maruos:硬件无关 Overlay,Maru 业务逻辑(约 50% C++,25% Makefile,15% shell,10% C)。
- maruos/blueprints:独立 shell 构建流水线,产出容器运行的 Debian 根文件系统;插件驱动,标准实现为 blueprint/debian。
- device_<vendor>_<device>:fork 自 LineageOS 的设备仓库,提供 HAL、传感器配置、厂商二进制、boot 镜像配置。Maru 不会重写硬件适配,跟随 LineageOS 维护进度。
仓库拓扑带来启示:Maru 业务逻辑集中于vendor_maruos与blueprints;Android 基础能力来自上游 AOSP/LineageOS;硬件适配交给 fork 后的 Lineage 设备仓库。各层可以按各自节奏迭代。
65.14.5 vendor_maruos Overlay 目录剖析
vendor_maruos最能体现 MaruOS 的设计思路,maru‑0.7 分支顶层目录结构:
vendor_maruos/
├── Android.bp
├── Android.mk
├── BoardConfigVendor.mk
├── LICENSE
├── README.md
├── container/ # Android侧LXC管理逻辑
├── device‑maru.mk # 硬件无关产品定义
├── include/perspective/ # Perspective层头文件
├── init.maru.rc # Maru init服务定义
├── maru_build.mk # Maru编译胶水逻辑
├── mlogwrapper/ # Maru日志工具
├── overlay/ # AOSP资源覆盖层
├── overrides/ # 编译期文件整体替换
├── perspective/ # Android ↔ 容器桥接核心
├── prebuilts/ # 预编译二进制
├── privapp‑permissions‑maru.xml # 特权权限白名单
├── scripts/ # 编译辅助脚本
└── sepolicy/ # Maru SELinux策略
每个目录在这套融合式系统架构中各司其职。按照依赖顺序依次解读,会比按字母顺序浏览更容易理解整体设计:
perspective/ 是整个方案的核心抽象层。命名本身就体现了设计思想:一台设备具备多种「使用视角」(手机屏幕、桌面显示器,甚至仅耳机输出模式),MaruOS 的核心职责就是在不同模式之间完成切换。该目录下包含 C++ 与 Java 混合实现的守护进程,负责监听显示设备事件、判定用户是否启用「桌面视角」,并调度下层所有组件协同工作。
container/ 负责 LXC 容器相关逻辑:封装 lxc‑start 的生命周期管理、根文件系统挂载逻辑、cgroup 资源控制配置、将宿主端资源绑定挂载至容器的规则,以及容器销毁回收流程。这一层直接对接 LXC 接口。
overlay/ 采用 AOSP 标准的资源覆盖机制(参见 65.5 小节),替换框架或应用内的指定资源。常见修改对象包括设备的 config.xml 配置、默认语言地区、品牌文字以及预装应用的启动默认参数。
overrides/ 与 overlay/ 配套,用于处理资源覆盖机制无法实现的完整文件替换。Overlay 只能修改 APK 内部资源取值,而 overrides 用于直接替换完整文件或脚本。
sepolicy/ 存放增量 SELinux 安全策略,赋予容器守护进程调用 LXC、挂载根文件系统、操作 cgroup、转发输入事件、获取帧缓冲的权限;原生 AOSP 策略并不开放这些权限,因为原生系统没有任何组件需要执行这类操作。
prebuilts/ 存放 AOSP 编译流程无法自行生成的预编译产物,一般包含 LXC 工具链二进制文件、无需按设备重新编译的辅助脚本,以及 Debian 环境所需的第三方依赖组件。
mlogwrapper/ 是一个小型工具,将 Maru 守护进程的日志重定向接入 Android 日志服务(logd /logcat),开发者调试该服务时,可以直接使用常规 Android 调试工具。
scripts/ 存放仓库级辅助脚本,运行于标准 m/make 编译流程之外,例如从构建产物拉取 Debian 根文件系统,在系统镜像打包前放置到对应产品目录。
65.14.6 blueprints 容器构建工具
容器运行的 Debian 根文件系统不由 Soong 编译,由独立仓库maruos/blueprints实现,一套自包含 shell 流水线:
build.sh:构建入口,读取 blueprint 配置,调用对应钩子,输出 tarball 镜像。build‑with‑docker.sh:在 docker 内执行 build.sh,编译主机没有 debootstrap 等 Debian 工具链时使用。plugin.sh:插件模板,每个 blueprint 必须实现blueprint_build与blueprint_cleanup两个 shell 函数。blueprint/debian/:标准 Debian 镜像构建插件。
这套拆分设计目的清晰:AOSP 构建系统擅长 Android 系统镜像与 boot 镜像,并不了解 Debian 根文件系统。Maru 维护两套独立构建流程:Soong/Make 负责 Android;shell/debootstrap 负责 Debian;最后组装脚本把 rootfs 产物放到对应产品目录。
给 ROM 开发者的启发:当定制内容引入完全异构运行环境(容器、虚拟机、其他 libc),更合适的做法是独立一套构建系统输出 tar 包,而不是强行在 Soong 内部描述异构环境。
65.14.7 “Perspective” 桥接层
perspective/ 与 include/perspective/ 目录存放整套设计中最核心的调度组件:运行在 Android 侧的守护进程,驱动整套融合桌面的完整业务流程。从代码结构(Android 用户空间、C++/Java 混合编码、源码配套头文件目录)以及其他模块对它的依赖关系来看,该组件至少承担以下职责:
- 外接显示屏检测 监听
DisplayManager的onDisplayAdded回调(或是底层等价事件),判断接入的屏幕是否属于桌面级显示设备 —— 尺寸足够大、属于外置屏幕,满足条件后触发桌面模式。 - 容器生命周期管理 首次接入桌面显示器时调用
container/模块启动 LXC 容器;最后一块桌面显示器断开时关闭容器;同时可根据 MaruOS 策略,在熄屏时将容器挂起休眠。 - 输入路由转发 把 Android 输入子系统收到的键盘、鼠标(以及触控)事件转发至容器,保证外接键鼠的输入可以送达 Debian 系统。
- 音频路由协商 在手机自带扬声器与 HDMI 显示器自带音频设备之间切换、协商音频输出通路。
- 权限与身份校验 提供校验机制,由用户确认当前接入的显示设备允许启动桌面会话;MaruOS 不会在任意外接设备插入时直接暴露 Debian 桌面。
从仓库架构可以看出:perspective 层扮演总指挥,container/ 则是执行助手。前者决策业务逻辑,后者实际操控 LXC 容器。 这种分层设计模式对定制 ROM 开发者非常有参考价值,适合实现需要长期驻留后台、管理非 Android 用户态资源的系统服务。
65.14.8 mflinger 与 mclient:图形桥接
当 Debian 容器内部的 X11 服务器向帧缓冲区绘制画面时,像素数据存放在容器的地址空间中 —— 二者挂载命名空间、/dev 视图、GPU 内存视图彼此隔离。负责外接屏幕画面输出的 Android SurfaceFlinger 完全无法识别这块缓冲区。因此必须有组件完成两端桥接,MaruOS 采用的这套桥接方案设计十分精巧:它让 GPU 感知不到 LXC 命名空间的隔离边界。
mflinger 独立托管在单独的代码仓库:GitHub - maruos/mflinger: Graphics buffer bridge for Maru OS. · GitHub,和 vendor_maruos 相互分离。项目自述仅有一句话:「Maru OS 的图形缓冲区桥接组件」。代码以 C 语言为主(C 约占 64%,C++ 约 21%),目录包含 src/、include/、lib/、tests/、scripts/,整体是一个小型 Linux 系统服务的典型工程结构。
命名本身就点明了设计类比关系:Android 的 SurfaceFlinger 作为消费者,通过标准 ANativeWindow / BufferQueue 生产者协议,接收来自应用、相机 HAL、视频解码器等各个来源的图形缓冲区,完成画面合成并输出到屏幕。而 mflinger 充当容器画面的生产者:向 SurfaceFlinger 请求一块基于 ANativeWindow 的 Surface,将底层图形缓冲区交给 Linux 直接渲染,缓冲区就绪后通知 SurfaceFlinger 完成显示提交。
mflinger 架构拆分为两端实现:
- mflinger(Android 侧守护进程) 由
init.maru.rc启动,作为 Android 用户空间服务运行。它创建绑定至外接显示器的 Android Surface,调用标准ANativeWindowC API 取出空闲的GraphicBuffer插槽。每一块缓冲区都由 gralloc 分配实现,新版 AOSP 设备上一般采用 dma‑buf;mflinger 可以将该缓冲区对应的文件描述符传递给另一个命名空间内的进程。 - mclient(容器侧客户端) 打包部署在 Debian 根文件系统内部。它通过 Unix 域套接字,借助 Linux 的
SCM_RIGHTS附属数据机制,从 mflinger 获取 dma‑buf 文件描述符 —— 这是内核层面跨进程传递文件句柄的标准方式。该机制可以穿透 LXC 命名空间隔离,因为内核是对底层内核对象做引用计数,而非依赖文件路径。随后 mclient 对 dma‑buf 执行内存映射(或是通过 DRI3 将其导入为像素图),把这块内存区域交给 X.org,作为根窗口的底层绘图缓存。
整套方案最核心的关键点:X.org 直接渲染写入 SurfaceFlinger 最终要显示的同一块物理 GPU 内存。X 服务提交一帧画面后,mclient 通知 mflinger 缓冲区就绪;mflinger 调用 ANativeWindow 的 queueBuffer;SurfaceFlinger 获取该缓冲区,在外接屏幕的下一次合成流程中将其渲染输出,用户就能看到桌面画面。不存在一块需要跨边界拷贝的中间容器帧缓冲区;文件描述符就是跨隔离层的唯一媒介,GPU 内存实现真正共享。
这套生产者‑消费者模型与 Android 原生相机、解码器、OpenGL 绘制表面所用机制完全一致;mflinger 的扩展创新,就是把生产者一端交给运行在另一个挂载命名空间中的进程使用。
单帧图形桥接完整流程。

该设计有两点值得重点提炼的特性:
LXC 边界实现零拷贝 像素数据不会在 Android 与容器的地址空间之间进行拷贝传输。桥接通道仅传递文件描述符和简短的协议消息。端到端性能开销由 GPU 渲染能力决定,不受内存带宽或跨命名空间带来的额外损耗制约。
每帧仅执行一次合成 由于 X.org 直接绘制到 SurfaceFlinger 最终提交使用的图形缓冲区,不再存在独立的「容器帧缓冲区 → 宿主帧缓冲区」二次合成步骤。画面合成次数与普通 Android Surface 保持一致,因此这套方案即便驱动 1920×1080 的外接显示器,在配置一般的硬件上也不会形成性能瓶颈。
vendor_maruos/sepolicy/ 目录下 Android 侧的 SELinux 策略(65.14.11 小节)赋予了 mflinger 获取 ANativeWindow 绘图表面、通过 Binder 和 SurfaceFlinger 通信、经由 Unix 域套接字传递文件描述符的权限;缺少这些规则,守护进程在启动阶段就会被安全机制拦截。 而实现 mclient 与 mflinger 之间的套接字跨容器绑定挂载,是由 vendor_maruos/container/ 下的容器模块在 LXC 启动时完成配置的(65.14.5 小节)。
65.14.9 Linux 与 Android 之间输入映射
图形桥接的配套是输入桥接;得益于内核共享,输入桥代码量远小于图形桥。
键盘、鼠标、触摸硬件在内核输入子系统,对应/dev/input/event*设备节点。因为内核唯一,这些节点全局唯一,不存在独立 Android 输入设备与独立 Linux 输入设备。Android 内部InputReader(运行于 system_server)打开/dev/input/节点;LXC 启动时 Maru 通过 bind‑mount 把同一批节点暴露到容器内部/dev/input/;容器内X.org使用 evdev 驱动读取事件,和标准 Debian 桌面完全一致。图形桥接没有像素拷贝,输入桥接没有事件拷贝。桥接层真正做的工作是设备所有权路由,全部基于标准 Linux 机制:
- 设备枚举:USB / 蓝牙键鼠触摸屏接入,内核生成
/dev/input/eventN;udev 热插拔事件同时传递给 Android 输入层,bind‑mount 之后也传递到容器。 - 按视角分配设备所有权:perspective 守护进程根据当前激活 “视角” 决定设备归属:
- 手机内置触摸屏永远归属 Android,驱动内置屏幕 SystemUI 与应用,绝不交给桌面。
- 电源、音量硬件按键永远归属 Android。
- 外接 USB / 蓝牙键鼠:桌面会话激活时归属桌面,未激活时归属 Android。
- 外接触摸显示器输入设备,桌面运行时归属桌面。
- EVIOCGRAB 排他抢占:设备归属方对 event 节点执行 ioctl
EVIOCGRAB,1。没有抢占时 evdev 为多播模式,所有打开 fd 都收到全部事件,Android 与X.org会同时响应同一个按键;执行抢占后,内核只向持有 grab 的 fd 投递事件,直到释放 grab。 - 跨显示屏触摸坐标转换:触摸事件使用对应面板原始绝对坐标。手机内置 1080×1920 触摸坐标,对外接 1920×1080 X 服务器没有意义。常规场景不会触发该逻辑:内置屏归 Android,外接触摸显示器归桌面。特殊场景 “把手机屏幕作为桌面触摸板”:perspective 守护进程读取内置屏幕触摸事件,坐标缩放转换为桌面坐标系,通过虚拟设备合成新输入事件;需要开启
CONFIG_INPUT_UINPUT;常规使用场景不需要。
整套管线两端完全透明:Android InputReader 枚举自有设备正常分发事件;容器X.org枚举自有设备正常分发事件;中间只有 perspective 守护进程维护所有权表。
架构启示:两套用户态栈共享同一个内核时,内核已经抽象好的能力(输入事件、网络套接字、fd、字符设备)只需要做路由,不需要转发复制。内核没有原生抽象的(Surface 背后 GPU 内存),则需要像 mflinger 那样显式传递 dma‑buf fd 完成桥接。Maru 输入桥代码精简,依靠内核完成事件分发;图形桥轻量化,复用内核 dma‑buf fd 传递,不重复造像素传输。
65.14.10 Init 与开机集成
init.maru.rc定义开机集成逻辑。Android init 进程开机解析/system/etc/init/、/vendor/etc/init/下 rc 文件,定义服务与触发器。MaruOS 该文件主要包含:
- perspective 守护进程 service 条目:配置正确 user、group、seclabel,SELinux 将进程放置 maru 域;init 完成进程托管重启。
- on 触发器:
on boot、on property:sys.boot_completed=1、on property:maru.container.requested=1,控制守护进程启动时机与前置初始化。 - socket 或 oneshot 定义:perspective 守护进程外部调用辅助命令。
从冷启动到 X11 窗口的完整流程:

用户插拔显示器这个外部事件驱动整条链路。容器不会开机就常驻;perspective 层按需启动,闲置时关闭。对应 README 描述 “按需启动虚拟系统”。
65.14.11 SELinux 带来的影响
原生 AOSP SELinux 策略假设 Android 进程不会拉起 LXC 容器、挂载 rootfs、操作 Android lmkd/freezer 之外的 cgroup。Marusepolicy/目录存放策略增量:
- 新建域:
maru_perspective、maru_container用于守护进程与辅助子进程。 - 域允许规则:允许该域执行 LXC 工具链、挂载 Maru 专属路径 rootfs、操作容器用到的 cgroup。
- 允许守护进程抢占外接显示器 framebuffer/SurfaceFlinger 资源,供容器 X11 输出画面。
- 允许输入管道从 Android 输入域流向 perspective 域,保证输入转发正常。
ROM 开发者经验:任何新增拥有提升权限的系统服务、守护进程,必须配套新增 SELinux 策略。工作量不在于编写.te文件(Maru sepolicy 体量不大),而在于开发阶段定位每一条 avc 拒绝日志。每一条拒绝首次出现打印日志,后续会静默放行。Maru 移植到新设备,需要完整审计一遍 SELinux 拒绝。
65.14.12 Maru 系统应用的特权权限
privapp‑permissions‑maru.xml放在 vendor‑overlay 作用域,由 Android 特权权限白名单机制解析。Maru 配套 UI APK(确认桌面访问、容器设置、桌面激活通知提示)放在/system/priv‑app/,声明特权权限,必须在该白名单显式授权。
文件示例格式(AOSP 强制格式,ROM 项目通用):
<permissions>
<privapp‑permissions package="com.maruos.perspective">
<permission name="android.permission.SYSTEM_ALERT_WINDOW"/>
<permission name="android.permission.MANAGE_DEVICE_ADMINS"/>
<permission name="android.permission.WRITE_SECURE_SETTINGS"/>
<!-- 其他权限 -->
</privapp‑permissions>
</permissions>
定制 ROM 特权系统 App 缺少该条目,开机 PackageManagerService 做 dexopt 阶段就会报权限拒绝。
65.14.13 编译流程汇总
完整编译 MaruOS 源码步骤:
# 1. 使用Maru manifest初始化AOSP工作树
mkdir maru && cd maru
repo init -u https://github.com/maruos/manifest.git -b maru‑0.7
repo sync -j$(nproc)
# 2. 独立编译Debian rootfs(另一套构建系统)
cd vendor/maruos/blueprints
./build.sh blueprint/debian
# 输出rootfs tar包;脚本把产物放到对应product目录,打包进系统镜像
# 3. 标准AOSP编译流程
cd $TOP
source build/envsetup.sh
lunch maru_<device>‑userdebug
m
对比 65.8 常规 ROM 构建,三点明显区别:
- 两套独立构建系统:Soong/Make 不会调用 blueprints;开发者脚本或者手动编排两套构建。
- 容器镜像属于编译产物,不是源码:Debian rootfs 每个发布版本编译一次,打包到 Maru 系统镜像;设备运行时不会现场执行 debootstrap。
- lunch 组合内置 Maru 能力:
maru_<device>product 继承device‑maru.mk与maru_build.mk,引入vendor_maruos,开启 Maru 编译开关。执行普通lineage_<device>lunch 则不会编译 Maru 层。
65.14.14 MaruOS 给 ROM 开发者的启示
MaruOS 作为实例,提炼三条通用经验,不限于本项目:
- Overlay 模型能力上限很高:前面章节介绍 Overlay 修改资源值、预装 APK;Maru 证明只依靠 vendor overlay + sepolicy + init.rc + 配套独立构建流水线,不需要 fork 修改 AOSP 源码,就可以完整加入第二套操作系统。只要你的定制可以表达为放置到
/system/、/vendor/、/product/、/system_ext/的文件,配合 init 规则与 SELinux 策略,就不必改动 AOSP 源码。 - 硬件适配工作尽量委托上游:Maru fork LineageOS 设备树,而不是从零维护 HAL。Maru 维护团队可以专注融合层,复用 Lineage 季度设备更新。任何 ROM 项目如果从头维护硬件适配,会背负无休止工作量;尽可能复用上游硬件适配成果。
- 异构环境不要硬塞给 Soong:Debian 容器构建流水线使用 shell,独立仓库。Soong 不需要理解 debootstrap。两套构建系统只在 tar 包产物处对接。当 ROM 需要交付非 Android 镜像产物(容器 rootfs、独立协议固件、给协处理器 Buildroot 镜像),设计模式就是独立构建系统输出文件,交给 AOSP 打包进镜像。
65.14.15 局限与权衡
MaruOS 同时也展示这套方案需要付出的代价,客观权衡点:
- 设备支持范围有限:跨多设备维护融合层,需要跟踪每一个 LineageOS 设备分支,验证 LXC 内核特性,重测显示路由。Maru 正式版本一次只支持少量机型;适合小团队,无法做到全设备兼容。
- 共享内核等于共享攻击面:LXC 相比虚拟机开销小,但是内核完全共享。Debian 容器触发的内核漏洞可以逃逸影响 Android 用户空间与 bootloader。隔离能力比不上硬件虚拟化桌面方案。
- 无法隔离 Android 文件系统:LXC 容器可以配置暴露主机大量文件系统。Maru 容器 /perspective 桥接层必须谨慎选择 bind‑mount 路径,一旦配置错误 Debian 应用就可以访问 Android 用户数据。
- 维护债务:两套构建系统、两套用户态栈、定制桥接层;Android 大版本迭代时维护成本高。每年 Android 版本更新,
DisplayManager、输入调度、SELinux 行为会变化,perspective 守护进程必须适配改动。 - 上游演进不确定性:Maru 设计早于 Android Computer Control 框架与官方外接桌面模式。原生平台桌面模式逐步完善,未来有可能让 Maru 这套方案淘汰,或者抬升同类方案的实现基线。本案例视作 Android12 时代 AOSP 原生下实现实项目快照。
和 65.14.14 启示对称:Overlay 机制带来极高灵活性,但是每增加一层能力,就增加一层维护成本。追求高复杂度定制 ROM,需要先规划现实的设备列表与发布节奏。
65.15 动手实践
使用 Android17(android17‑release)代码仓库完成下面练习;练习前后依赖,前面练习是后面练习的前置条件。
- 注册并 lunch 自定义产品。创建
device/AospBook/bookphone/目录,放入 65.3 节四个配置文件:AndroidProducts.mk、bookphone.mk、device.mk、BoardConfig.mk。执行source build/envsetup.sh;执行list_products | grep bookphone;lunch bookphone‑trunk_staging‑userdebug。printconfig确认TARGET_PRODUCT=bookphone。 - 确认编译执行器。执行
m nothing,Android17 默认执行器为 Siso。设置SOONG_NINJA=ninja m nothing依旧编译成功。查看build/soong/ui/build/config.go确认NINJA_DEFAULT = NINJA_SISO。 - 校验平台版本三元组。镜像启动后
adb shell getprop ro.build.version.sdk得到 37;getprop ro.build.version.release得到 17。溯源配置文件:build/release/flag_values/trunk_staging/RELEASE_PLATFORM_SDK_VERSION.textproto、RELEASE_PLATFORM_VERSION_LAST_STABLE.textproto。 - 编译 16KB 页大小镜像。编译
sdk_phone16k_x86_64‑trunk_staging‑userdebug,启动模拟器;adb shell getconf PAGE_SIZE预期 16384;查看getprop ro.product.build.16k_page.enabled。放入 4KB 对齐预编译.so观察加载失败;源码重新编译该库即可正常加载。 - 签名打包发布版本。使用
development/tools/make_key生成密钥;配置PRODUCT_DEFAULT_DEV_CERTIFICATE/PRODUCT_CERTIFICATE_OVERRIDES;执行m dist;sign_target_files_apks.py重新签名;ota_from_target_files.py生成完整 OTA 包;check_ota_package_signature.py校验 OTA 签名。 - Kleaf 编译自定义内核。同步
common‑android‑mainline;tools/bazel run //common:kernel_x86_64_dist -- --dist_dir=out/x86_64/dist;将输出 bzImage 放到prebuilts/qemu‑kernel/x86_64/6.12/;模拟器使用自定义内核启动;adb logcat -b kernel确认 printk 打印输出。 - 新增自定义 AIDL HAL。完整实现 65.12 节 IBookLight HAL,VINTF 片段,SELinux 策略。镜像运行后
adb shell lshal | grep booklight确认框架可以发现 HAL;logcat 不存在相关avc: denied拒绝。
拓展深入,下面表格对应练习子系统源码位置:
| 主题 | 源码路径 | 说明 |
|---|---|---|
| 构建系统 | build/make/core/ | GNU Make 编译规则 |
| Soong 构建 | build/soong/Blueprint/ | Soong 构建系统 |
| 编译执行器 | build/soong/ui/build/config.go | Siso/Ninja 选择逻辑 |
| 发布版本配置 | build/release/Version/ | SDK 版本标记 |
| 产品配置 | build/make/target/product/ | 基础 product makefile |
| Goldfish 模拟器设备 | device/generic/goldfish/ | 模拟器设备树 |
| 框架配置 | frameworks/base/core/res/res/values/config.xml | 可覆盖框架配置值 |
| SystemUI | frameworks/base/packages/SystemUI/ | SystemUI 源码 |
| 开机动画 | frameworks/base/cmds/bootanimation/ | bootanimation 播放器 |
| HAL 接口定义 | hardware/interfaces/ | AIDL HAL 接口定义 |
| 发布工具 | build/make/tools/releasetools/ | 签名与 OTA 工具 |
| 签名密钥默认 | build/make/target/product/security/ | 默认测试签名密钥 |
| SELinux 基础策略 | system/sepolicy/ | AOSP 基础 SELinux 策略 |
| init 进程 | system/core/init/ | init 进程源码 |
| SystemServer | frameworks/base/services/java/com/android/server/SystemServer.java | 系统服务启动入口 |
65.16 本章小结
本章完整讲解从零搭建 Android 定制 ROM 全流程:
| 章节 | 标题 | 核心产出 |
|---|---|---|
| 65.1 | 规划 | 定义 AospBook ROM 范围与整体架构 |
| 65.2 | 环境准备 | 编译主机配置,树内 JDK,Android17 默认 Siso 编译执行器 |
| 65.3 | 设备配置 | AndroidProducts.mk、device.mk、BoardConfig.mk产品基础配置 |
| 65.4 | 定制应用 | 源码编译应用、预编译 APK 打包进镜像 |
| 65.5 | 框架行为修改 | RRO 资源覆盖层、源码修改、新增系统服务 |
| 65.6 | 开机动画 | bootanimation.zip 制作与集成 |
| 65.7 | SystemUI 定制 | 状态栏、快捷设置、主题 Overlay |
| 65.8 | 编译与烧写 | m 编译、模拟器、fastboot 刷写、16KB 页面大小 |
| 65.9 | 调试手段 | logcat、dumpsys、Perfetto、Winscope、SELinux 排错 |
| 65.10 | 版本分发 | 密钥生成、签名、OTA 升级包 |
| 65.11 | 内核定制 | Kleaf 编译内核,内核模块 |
| 65.12 | HAL 开发 | 自定义 AIDL HAL 定义与完整实现 |
| 65.13 | 整合全部组件 | 端到端编译、测试、发布流水线 |
| 65.14 | 案例研究 | MaruOS 融合型 ROM 实例 |
关键要点总结
- 优先 Overlay,尽量少修改源码。RRO 与 product 配置就可以完成大量定制,不改动框架源码,AOSP 版本迭代维护成本更低。
- device 设备树属于你的独立域:
device/*下所有内容独立于 AOSP 上游,repo sync不会覆盖你的改动。 - SELinux 不可省略:每一个自定义服务、HAL、守护进程必须配套 SELinux 策略。先用
audit2allow从拒绝日志生成初始策略,再收紧规则做到最小权限。 - 对外发布绝对不要使用测试密钥。分发 ROM 前生成专属签名密钥。
- 模拟器是最佳开发载体。95% 的 ROM 定制可以在模拟器开发验证,之后再上真实硬件。
本章技术是主流定制 ROM 项目通用基础。无论是隐私向 ROM、企业设备管理方案,还是学习理解 Android 底层原理,能够完整编译、定制、签名、分发 Android 系统镜像,代表对 AOSP 达到深度掌握。

451

被折叠的 条评论
为什么被折叠?



