65 定制 Android ROM

本章是全书的收官章节。我们将整合前面 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 GB400 GB500 GB SSD
含编译产物磁盘400 GB600 GB+1 TB NVMe
内存 RAM32 GB64 GB+64 GB
CPU 核心数4 核16 核 +16 核
操作系统Ubuntu 22.04+Ubuntu 24.04 LTSUbuntu 24.04
文件系统ext4(区分大小写)ext4ext4

构建是高度并行化的。每增加一个 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_productslist_releaseslist_variants定义在build/make/envsetup.sh;lunch 支持完整格式 <产品>-<发布版本>-<变体>,不带参数会打印菜单。

lunch 目标格式:<product>-<release>-<variant>

组成取值说明
productsdk_phone64_x86_64在 AndroidProducts.mk 中定义
releasetrunk_staging发布配置
variantuser / userdebug / eng构建变体

三种构建变体控制系统可调试性:

变体ro.debuggableadbroot 权限适用场景
user0默认关闭正式发布版本
userdebug1开启支持 adb root接近正式版本的开发调试
eng1开启默认 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_NAMElunch 目标名称bookphone
PRODUCT_DEVICE板级设备名,对应 BoardConfig.mkbookdevice
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_NAMElunch 目标名称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_NAMESPACESSoong 模块搜索路径目录路径
PRODUCT_ENFORCE_RRO_TARGETS强制启用 RRO 的目标包framework‑res
PRODUCT_ENFORCE_RRO_EXCLUDED_OVERLAYSRRO 强制规则排除的 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 文件路径
presignedtrue/false保留原有签名,不重新签名
certificate"platform"、"shared"、"media" 或文件路径重新签名使用的密钥
privilegedtrue/false安装到 priv‑app 目录
product_specifictrue/false安装到 /product 分区
vendortrue/false安装到 /vendor 分区
dex_preopt.enabledtrue/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/ 下密钥文件:

密钥名密钥文件用途
platformplatform.pk8, platform.x509.pem带有android:sharedUserId="android.uid.system"的系统应用
sharedshared.pk8, shared.x509.pem共享数据应用(联系人、电话)
mediamedia.pk8, media.x509.pem媒体 / 下载类系统应用
testkeytestkey.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 中,删除预置应用只有两种实现方式:

  1. 不引入添加该应用的 Makefile 绝大多数系统预置应用都是通过 $(call inherit-product, ...) 继承链加入到 PRODUCT_PACKAGES 的。选择更精简的父级配置文件,就能从根源上不编译安装这些应用。
  2. 通过替换模块进行覆盖 在 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_defaultNightModeinteger0深色 / 浅色 UI 默认模式
config_longPressOnPowerBehaviorinteger1电源键长按动作
config_dozeAlwaysOnDisplayAvailableboolfalse息屏 AOD 是否可用
config_enableLockScreenRotationboolfalse锁屏是否允许旋转
config_screenBrightnessSettingDefaultinteger随设备默认屏幕亮度
config_defaultBatteryPercentageSettingboolfalse状态栏电池百分比
config_enableHapticTextHandleboolfalse文本选择震动反馈

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 代码决定):

  1. /apex/com.android.bootanimation/etc/bootanimation.zip
  2. /product/media/bootanimation.zip(文件名可通过系统属性 ro.product.bootanim.file 自定义)
  3. /oem/media/bootanimation.zip
  4. /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)必须注册到 QSModuleQSTileHost 中,才能完成实例化。具体注册机制随 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 命令的执行流程

  1. 运行 Soong 处理所有Android.bp文件
  2. 运行 Kati 处理所有Android.mk文件
  3. 在输出目录生成 Ninja 格式的编译构建图
  4. 调用编译执行器(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.debuggableadb root优化使用场景
user0关闭完整优化(开启 Proguard、代码压缩)正式发布版本
userdebug1开启部分优化,保留部分调试信息开发调试
eng1开启,默认 root最小优化,不开启 Proguard深度调试

编译变体影响:

  1. adb root是否可用
  2. system 分区是否可写
  3. Proguard/R8 代码混淆优化等级
  4. 是否包含 strace、valgrind 等调试工具
  5. 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_libsshared_libs添加对应依赖
SELinux denialsSELinux 策略缺失添加 allow 规则,使用audit2allow生成策略
FAILED: out/.../module.jarJava 编译错误检查源码语法、导入包
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_SUPPORTEDbuild/make/core/config.mk自动配置原生模块段对齐;arm64/x86_64 默认 16384
TARGET_BOOTS_16KBoardConfig.mk标记板子实际启动 16KB 页内核
PRODUCT_16K_DEVELOPER_OPTIONproduct 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 无法启动排查逻辑:

常见启动故障与处理

  1. 开机动画无限循环
# 查看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"
  1. 自定义服务发生崩溃
# 观察system_server是否反复重启
adb logcat -s ActivityManager | grep "Start proc"

# 查看目标服务日志
adb logcat -s BookService

# 完整异常堆栈
adb logcat -s AndroidRuntime
  1. 模块 / 库文件缺失
# 检查产物是否部署到设备
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 组关键签名密钥。

密钥文件对用途
testkeytestkey.pk8 / testkey.x509.pem默认开发调试签名
platformplatform.pk8 / platform.x509.pemandroid.uid.system系统应用
sharedshared.pk8 / shared.x509.pem共享 UID 数据的应用(联系人、电话)
mediamedia.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_HZ2501000更高时钟节拍,提升响应
CONFIG_SCHED_AUTOGROUPny调度自动分组
CONFIG_TCP_CONG_BBRnyBBR 拥塞控制算法
CONFIG_ZRAMmyzram 内存压缩 swap
CONFIG_KSMny内核同页合并
CONFIG_TRANSPARENT_HUGEPAGEny透明巨页

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 清单片段框架无法发现 HALAndroid.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 变更,下面是可行策略:

  1. 尽量少修改框架源码:优先使用 RRO 与资源覆盖层,而不是直接修改框架源代码。覆盖层在 AOSP 版本 rebase 合并时冲突更少。
  2. 设备树改动保持隔离device/AospBook/bookphone/下所有文件属于项目自有,不会和上游产生冲突。
  3. 使用 repo 主题分支管理框架修改,每个功能单独一个主题分支:
# 为框架修改创建主题分支
cd frameworks/base
repo start aospbook-dark-mode .
# 完成修改并提交
git add -A && git commit -m "AospBook: default dark mode"
  1. 定期 rebase:同步最新 AOSP,对主题分支执行 rebase,手动解决冲突。
repo sync -j$(nproc)
repo rebase
# 处理冲突
  1. 记录每一处框架修改:维护变更日志,记录每一处框架修改对应的业务目的,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 还额外提供:

  1. 一套 LXC 容器管理守护进程,以及配套的 Android 端系统服务。
  2. 名为 Perspective 的适配层,打通 Android 的显示、输入、音频数据流与容器之间的通路,使容器能够在外接屏幕上输出完整连贯的桌面界面。
  3. 一套独立于 AOSP 编译体系的 Debian 容器镜像构建流水线。
  4. 设备接入钩子逻辑,用于判断何时启动容器,以及将桌面画面输出到哪一块显示屏。
  5. 新增 SELinux 安全策略,防止容器内的 Linux 进程破坏 Android 的安全域隔离机制。

该系统支持的设备列表刻意控制在较小范围;后续衍生版本仅维护少数几代 Pixel 机型以及 HTC 10。其设备配置仓库从 LineageOS 复刻而来,复用现成的硬件适配层,而非从零开始维护。可查看 maruos/manifest 清单获取当前支持机型;不同 maru‑0.x 版本所适配的设备存在差异。

整个项目组织全部采用 Apache‑2.0 开源协议,与 AOSP 保持一致。

65.14.2 双环境运行模型

概念架构是本案例最重要的部分:两套用户空间,共享同一个 Linux 内核,内核之上全部用户态组件相互独立。

三个关键架构要点:

  1. 单内核:Android 与 Debian 共用内核版本。容器依靠 PID、挂载、网络、IPC、用户 ID 命名空间实现隔离,不会启动独立内核镜像。这是 LXC 的特性:相比虚拟机开销极低,但内核漏洞可以越过容器边界进行逃逸。
  2. Maru 桥接守护进程运行在 Android 侧:普通 Android 进程(Java/JNI + C/C++ 原生代码),监听显示接入事件,调用 Linux 标准 API 拉起 LXC 容器。守护进程不在容器内部,容器仅感知自身已经被启动。
  3. 两块显示屏,两套所有者:手机内置屏幕继续渲染 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 消息队列
CgroupsCONFIG_CGROUPS=y,开启 memory、cpu、devices、freezer 子系统容器内部资源统计与资源限制
SeccompCONFIG_SECCOMP_FILTER=yLXC 限制容器可以调用的系统调用
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):

  1. maruos/manifest:repo 清单,repo init -u https://github.com/maruos/manifest.git -b maru‑0.7完成初始化。
  2. maruos/maruos:项目根仓库,README、文档、顶层脚本。许可证 Apache‑2.0。
  3. maruos/vendor_maruos:硬件无关 Overlay,Maru 业务逻辑(约 50% C++,25% Makefile,15% shell,10% C)。
  4. maruos/blueprints:独立 shell 构建流水线,产出容器运行的 Debian 根文件系统;插件驱动,标准实现为 blueprint/debian。
  5. device_<vendor>_<device>:fork 自 LineageOS 的设备仓库,提供 HAL、传感器配置、厂商二进制、boot 镜像配置。Maru 不会重写硬件适配,跟随 LineageOS 维护进度。

仓库拓扑带来启示:Maru 业务逻辑集中于vendor_maruosblueprints;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_buildblueprint_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 混合编码、源码配套头文件目录)以及其他模块对它的依赖关系来看,该组件至少承担以下职责:

  1. 外接显示屏检测 监听 DisplayManageronDisplayAdded 回调(或是底层等价事件),判断接入的屏幕是否属于桌面级显示设备 —— 尺寸足够大、属于外置屏幕,满足条件后触发桌面模式。
  2. 容器生命周期管理 首次接入桌面显示器时调用 container/ 模块启动 LXC 容器;最后一块桌面显示器断开时关闭容器;同时可根据 MaruOS 策略,在熄屏时将容器挂起休眠。
  3. 输入路由转发 把 Android 输入子系统收到的键盘、鼠标(以及触控)事件转发至容器,保证外接键鼠的输入可以送达 Debian 系统。
  4. 音频路由协商 在手机自带扬声器与 HDMI 显示器自带音频设备之间切换、协商音频输出通路。
  5. 权限与身份校验 提供校验机制,由用户确认当前接入的显示设备允许启动桌面会话;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 架构拆分为两端实现:

  1. mflinger(Android 侧守护进程)init.maru.rc 启动,作为 Android 用户空间服务运行。它创建绑定至外接显示器的 Android Surface,调用标准 ANativeWindow C API 取出空闲的 GraphicBuffer 插槽。每一块缓冲区都由 gralloc 分配实现,新版 AOSP 设备上一般采用 dma‑buf;mflinger 可以将该缓冲区对应的文件描述符传递给另一个命名空间内的进程。
  2. mclient(容器侧客户端) 打包部署在 Debian 根文件系统内部。它通过 Unix 域套接字,借助 Linux 的 SCM_RIGHTS 附属数据机制,从 mflinger 获取 dma‑buf 文件描述符 —— 这是内核层面跨进程传递文件句柄的标准方式。该机制可以穿透 LXC 命名空间隔离,因为内核是对底层内核对象做引用计数,而非依赖文件路径。随后 mclient 对 dma‑buf 执行内存映射(或是通过 DRI3 将其导入为像素图),把这块内存区域交给 X.org,作为根窗口的底层绘图缓存。

整套方案最核心的关键点:X.org 直接渲染写入 SurfaceFlinger 最终要显示的同一块物理 GPU 内存。X 服务提交一帧画面后,mclient 通知 mflinger 缓冲区就绪;mflinger 调用 ANativeWindowqueueBuffer;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 域套接字传递文件描述符的权限;缺少这些规则,守护进程在启动阶段就会被安全机制拦截。 而实现 mclientmflinger 之间的套接字跨容器绑定挂载,是由 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 机制:

  1. 设备枚举:USB / 蓝牙键鼠触摸屏接入,内核生成/dev/input/eventN;udev 热插拔事件同时传递给 Android 输入层,bind‑mount 之后也传递到容器。
  2. 按视角分配设备所有权:perspective 守护进程根据当前激活 “视角” 决定设备归属:
    • 手机内置触摸屏永远归属 Android,驱动内置屏幕 SystemUI 与应用,绝不交给桌面。
    • 电源、音量硬件按键永远归属 Android。
    • 外接 USB / 蓝牙键鼠:桌面会话激活时归属桌面,未激活时归属 Android。
    • 外接触摸显示器输入设备,桌面运行时归属桌面。
  3. EVIOCGRAB 排他抢占:设备归属方对 event 节点执行 ioctl EVIOCGRAB,1。没有抢占时 evdev 为多播模式,所有打开 fd 都收到全部事件,Android 与X.org会同时响应同一个按键;执行抢占后,内核只向持有 grab 的 fd 投递事件,直到释放 grab。
  4. 跨显示屏触摸坐标转换:触摸事件使用对应面板原始绝对坐标。手机内置 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 该文件主要包含:

  1. perspective 守护进程 service 条目:配置正确 user、group、seclabel,SELinux 将进程放置 maru 域;init 完成进程托管重启。
  2. on 触发器:on booton property:sys.boot_completed=1on property:maru.container.requested=1,控制守护进程启动时机与前置初始化。
  3. socket 或 oneshot 定义:perspective 守护进程外部调用辅助命令。

从冷启动到 X11 窗口的完整流程:

用户插拔显示器这个外部事件驱动整条链路。容器不会开机就常驻;perspective 层按需启动,闲置时关闭。对应 README 描述 “按需启动虚拟系统”。

65.14.11 SELinux 带来的影响

原生 AOSP SELinux 策略假设 Android 进程不会拉起 LXC 容器、挂载 rootfs、操作 Android lmkd/freezer 之外的 cgroup。Marusepolicy/目录存放策略增量:

  1. 新建域:maru_perspectivemaru_container用于守护进程与辅助子进程。
  2. 域允许规则:允许该域执行 LXC 工具链、挂载 Maru 专属路径 rootfs、操作容器用到的 cgroup。
  3. 允许守护进程抢占外接显示器 framebuffer/SurfaceFlinger 资源,供容器 X11 输出画面。
  4. 允许输入管道从 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 构建,三点明显区别:

  1. 两套独立构建系统:Soong/Make 不会调用 blueprints;开发者脚本或者手动编排两套构建。
  2. 容器镜像属于编译产物,不是源码:Debian rootfs 每个发布版本编译一次,打包到 Maru 系统镜像;设备运行时不会现场执行 debootstrap。
  3. lunch 组合内置 Maru 能力maru_<device> product 继承device‑maru.mkmaru_build.mk,引入vendor_maruos,开启 Maru 编译开关。执行普通lineage_<device> lunch 则不会编译 Maru 层。

65.14.14 MaruOS 给 ROM 开发者的启示

MaruOS 作为实例,提炼三条通用经验,不限于本项目:

  1. Overlay 模型能力上限很高:前面章节介绍 Overlay 修改资源值、预装 APK;Maru 证明只依靠 vendor overlay + sepolicy + init.rc + 配套独立构建流水线,不需要 fork 修改 AOSP 源码,就可以完整加入第二套操作系统。只要你的定制可以表达为放置到/system//vendor//product//system_ext/的文件,配合 init 规则与 SELinux 策略,就不必改动 AOSP 源码。
  2. 硬件适配工作尽量委托上游:Maru fork LineageOS 设备树,而不是从零维护 HAL。Maru 维护团队可以专注融合层,复用 Lineage 季度设备更新。任何 ROM 项目如果从头维护硬件适配,会背负无休止工作量;尽可能复用上游硬件适配成果。
  3. 异构环境不要硬塞给 Soong:Debian 容器构建流水线使用 shell,独立仓库。Soong 不需要理解 debootstrap。两套构建系统只在 tar 包产物处对接。当 ROM 需要交付非 Android 镜像产物(容器 rootfs、独立协议固件、给协处理器 Buildroot 镜像),设计模式就是独立构建系统输出文件,交给 AOSP 打包进镜像。

65.14.15 局限与权衡

MaruOS 同时也展示这套方案需要付出的代价,客观权衡点:

  1. 设备支持范围有限:跨多设备维护融合层,需要跟踪每一个 LineageOS 设备分支,验证 LXC 内核特性,重测显示路由。Maru 正式版本一次只支持少量机型;适合小团队,无法做到全设备兼容。
  2. 共享内核等于共享攻击面:LXC 相比虚拟机开销小,但是内核完全共享。Debian 容器触发的内核漏洞可以逃逸影响 Android 用户空间与 bootloader。隔离能力比不上硬件虚拟化桌面方案。
  3. 无法隔离 Android 文件系统:LXC 容器可以配置暴露主机大量文件系统。Maru 容器 /perspective 桥接层必须谨慎选择 bind‑mount 路径,一旦配置错误 Debian 应用就可以访问 Android 用户数据。
  4. 维护债务:两套构建系统、两套用户态栈、定制桥接层;Android 大版本迭代时维护成本高。每年 Android 版本更新,DisplayManager、输入调度、SELinux 行为会变化,perspective 守护进程必须适配改动。
  5. 上游演进不确定性:Maru 设计早于 Android Computer Control 框架与官方外接桌面模式。原生平台桌面模式逐步完善,未来有可能让 Maru 这套方案淘汰,或者抬升同类方案的实现基线。本案例视作 Android12 时代 AOSP 原生下实现实项目快照。

和 65.14.14 启示对称:Overlay 机制带来极高灵活性,但是每增加一层能力,就增加一层维护成本。追求高复杂度定制 ROM,需要先规划现实的设备列表与发布节奏。

65.15 动手实践

使用 Android17(android17‑release)代码仓库完成下面练习;练习前后依赖,前面练习是后面练习的前置条件。

  1. 注册并 lunch 自定义产品。创建device/AospBook/bookphone/目录,放入 65.3 节四个配置文件:AndroidProducts.mkbookphone.mkdevice.mkBoardConfig.mk。执行source build/envsetup.sh;执行list_products | grep bookphonelunch bookphone‑trunk_staging‑userdebugprintconfig确认TARGET_PRODUCT=bookphone
  2. 确认编译执行器。执行m nothing,Android17 默认执行器为 Siso。设置SOONG_NINJA=ninja m nothing依旧编译成功。查看build/soong/ui/build/config.go确认NINJA_DEFAULT = NINJA_SISO
  3. 校验平台版本三元组。镜像启动后adb shell getprop ro.build.version.sdk得到 37;getprop ro.build.version.release得到 17。溯源配置文件:build/release/flag_values/trunk_staging/RELEASE_PLATFORM_SDK_VERSION.textprotoRELEASE_PLATFORM_VERSION_LAST_STABLE.textproto
  4. 编译 16KB 页大小镜像。编译sdk_phone16k_x86_64‑trunk_staging‑userdebug,启动模拟器;adb shell getconf PAGE_SIZE预期 16384;查看getprop ro.product.build.16k_page.enabled。放入 4KB 对齐预编译.so观察加载失败;源码重新编译该库即可正常加载。
  5. 签名打包发布版本。使用development/tools/make_key生成密钥;配置PRODUCT_DEFAULT_DEV_CERTIFICATE / PRODUCT_CERTIFICATE_OVERRIDES;执行m distsign_target_files_apks.py重新签名;ota_from_target_files.py生成完整 OTA 包;check_ota_package_signature.py校验 OTA 签名。
  6. Kleaf 编译自定义内核。同步common‑android‑mainlinetools/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 打印输出。
  7. 新增自定义 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.goSiso/Ninja 选择逻辑
发布版本配置build/release/Version/SDK 版本标记
产品配置build/make/target/product/基础 product makefile
Goldfish 模拟器设备device/generic/goldfish/模拟器设备树
框架配置frameworks/base/core/res/res/values/config.xml可覆盖框架配置值
SystemUIframeworks/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 进程源码
SystemServerframeworks/base/services/java/com/android/server/SystemServer.java系统服务启动入口

65.16 本章小结

本章完整讲解从零搭建 Android 定制 ROM 全流程:

章节标题核心产出
65.1规划定义 AospBook ROM 范围与整体架构
65.2环境准备编译主机配置,树内 JDK,Android17 默认 Siso 编译执行器
65.3设备配置AndroidProducts.mkdevice.mkBoardConfig.mk产品基础配置
65.4定制应用源码编译应用、预编译 APK 打包进镜像
65.5框架行为修改RRO 资源覆盖层、源码修改、新增系统服务
65.6开机动画bootanimation.zip 制作与集成
65.7SystemUI 定制状态栏、快捷设置、主题 Overlay
65.8编译与烧写m 编译、模拟器、fastboot 刷写、16KB 页面大小
65.9调试手段logcat、dumpsys、Perfetto、Winscope、SELinux 排错
65.10版本分发密钥生成、签名、OTA 升级包
65.11内核定制Kleaf 编译内核,内核模块
65.12HAL 开发自定义 AIDL HAL 定义与完整实现
65.13整合全部组件端到端编译、测试、发布流水线
65.14案例研究MaruOS 融合型 ROM 实例

关键要点总结

  1. 优先 Overlay,尽量少修改源码。RRO 与 product 配置就可以完成大量定制,不改动框架源码,AOSP 版本迭代维护成本更低。
  2. device 设备树属于你的独立域device/*下所有内容独立于 AOSP 上游,repo sync不会覆盖你的改动。
  3. SELinux 不可省略:每一个自定义服务、HAL、守护进程必须配套 SELinux 策略。先用audit2allow从拒绝日志生成初始策略,再收紧规则做到最小权限。
  4. 对外发布绝对不要使用测试密钥。分发 ROM 前生成专属签名密钥。
  5. 模拟器是最佳开发载体。95% 的 ROM 定制可以在模拟器开发验证,之后再上真实硬件。

本章技术是主流定制 ROM 项目通用基础。无论是隐私向 ROM、企业设备管理方案,还是学习理解 Android 底层原理,能够完整编译、定制、签名、分发 Android 系统镜像,代表对 AOSP 达到深度掌握。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值