Android Init 开机脚本编写指南

以 TiviMateCustom 首启自动安装为例,覆盖 init.rc 编写、shell 脚本编写、SELinux 策略和调试全流程。


一、Android Init 基础

1.1 Init 进程的阶段事件

Android init(PID 1)按固定阶段推进,每个阶段发射一个不可重复的事件:

first_stage
    └── selinux_setup
        └── second_stage
              ├── early-init
              ├── init
              ├── late-init
              │     ├── early-fs
              │     ├── fs
              │     ├── post-fs
              │     ├── late-fs
              │     └── post-fs-data
              ├── early-boot
              ├── boot              ← 文件系统就绪,核心服务准备启动
              └── ...               (Zygote、system_server 等启动)
                                    
              运行时:
              sys.boot_completed=1  ← 开机动画结束,Launcher 就绪

关键区别:

事件类型 触发方式 重放机制 适用场景
阶段事件(on boot, on late-init 等) init 内部发射 — rc 晚加载也会追溯执行 不依赖属性的初始化
property trigger(on property:xxx=y 属性值变化 — 错过就永远错过 rc 必须比属性变化早加载

1.2 RC 文件的加载路径

Init 自动 import 以下目录的 .rc 文件:

/system/etc/init/*.rc        ← 较早(system 分区)
/vendor/etc/init/*.rc        ← 较晚(vendor 分区)
/odm/etc/init/*.rc
/product/etc/init/*.rc       ← 可能更晚

重要: 分区 rc 的加载顺序和时机不固定。vendor/product 分区的 rc 可能晚于某些阶段事件甚至 property trigger。这就是为什么推荐使用 on boot 而非 on property:sys.boot_completed=1


二、init.rc 编写

2.1 文件命名

<name>.rc

放在 vendor/giec/common/ 下,通过 PRODUCT_COPY_FILES 拷贝到 /vendor/etc/init/

2.2 基本语法

oneshot service(执行一次就退出)

service <name> <executable_path>
    class main           # 归属的 class(main / core / late_start)
    user root            # 以哪个用户运行
    group root system    # 附加组
    oneshot              # 执行完就退出,不重启
    disabled             # 不随 class_start 自动启动,等待显式 start

on <trigger>
    start <name>

常见 class:

  • core — 最早启动,设备关键服务
  • main — 核心系统服务就绪后
  • late_start — 开机完成后

daemon service(常驻后台)

service <name> <executable_path>
    class main
    user root
    group root system
    restart <number>     # 退出后重启次数(如 restart 5)

on boot
    start <name>

2.3 选择正确的 trigger

Trigger 时机 适用 陷阱
on early-init 最早,mount 之前 极早期初始化 大部分系统服务未就绪
on boot 文件系统就绪、核心服务启动前 开机脚本最佳选择 此时 PMS 未就绪,脚本内部需要等待
on property:sys.boot_completed=1 开机完成、Launcher 就绪 依赖 PMS 的操作 有错过风险(rc 晚加载时不触发)
on late-init init 阶段后期 需要在 boot 之前的操作 数据分区可能未挂载

推荐做法: on boot + 脚本内部 while boot_completed != 1 等待。两者互补——trigger 保证一定会执行,脚本内部的等待保证 PMS 已就绪。

2.4 完整示例

# vendor/giec/common/init.tivimate.rc

service tivimate-install /vendor/bin/install_tivimate.sh
    class main
    user root
    group root system
    oneshot
    disabled

on boot
    start tivimate-install

三、Shell 脚本编写

3.1 基本要求

#!/system/bin/sh
# 使用 /system/bin/sh,不是 /bin/sh 或 /bin/bash
# Android 上没有 bash,只有 POSIX sh

3.2 等待开机完成

while [ "$(getprop sys.boot_completed)" != "1" ]; do
    sleep 1
done

# 额外等几秒,确保 PMS、ActivityManager 等完全就绪
sleep 2

3.3 防重复执行(idempotency)

用一个标记文件来判断是否已执行过:

FLAG="/data/local/tmp/.my_task_done"

if [ -f "$FLAG" ]; then
    exit 0
fi

# 执行任务...
pm install -r /path/to/app.apk

# 只有成功才写标记,失败时下次启动重试
if [ $? -eq 0 ]; then
    touch "$FLAG"
fi

标记文件位置选择:

路径 持久性 说明
/data/local/tmp/ 跨重启保留,清 data 后丢失 推荐 — 清除数据后重新安装是预期行为
/data/misc/ 跨重启保留 适合系统级标记
/data/vendor/ vendor 专用持久存储 部分设备支持

3.4 错误处理

pm install -r "$APK" && touch "$FLAG"

if [ -f "$FLAG" ]; then
    echo "install: done"
else
    echo "install: failed, will retry on next boot" >&2
fi

使用 && 确保成功才创建标记。即使脚本执行失败,oneshot 服务也不会重试(已退出),但下次启动时标记文件不存在 → 自动重试。

3.5 完整示例

#!/system/bin/sh
# First-boot installer for TiviMateCustom APK.

APK="/product/app/TiviMateCustom/TiviMateCustom.apk"
FLAG="/data/local/tmp/.tivimate_installed"

if [ -f "$FLAG" ]; then
    exit 0
fi

while [ "$(getprop sys.boot_completed)" != "1" ]; do
    sleep 1
done

sleep 2

echo "install_tivimate: installing TiviMateCustom..."
pm install -r "$APK" && touch "$FLAG"

if [ -f "$FLAG" ]; then
    echo "install_tivimate: done"
else
    echo "install_tivimate: failed, will retry on next boot"
fi

四、SELinux 策略配置

4.1 为什么需要 SELinux 策略

init 执行脚本时,需要从 init 安全域切换到脚本对应的域。如果脚本文件没有专门的 SELinux 标签和域转换规则,init 会拒绝执行:

init: Could not start service 'xxx' as part of class 'main':
File /vendor/bin/xxx.sh (labeled "u:object_r:vendor_file:s0")
has incorrect label or no domain transition from u:r:init:s0
to another SELinux domain defined.

必须同时做两件事:

  1. 给脚本打标签(file_contexts
  2. 建立域转换规则(.te 文件)

4.2 file_contexts — 给文件打标签

文件: vendor/<vendor>/common/sepolicy/file_contexts

/vendor/bin/<script_name>\.sh    u:object_r:<domain>_exec:s0

示例:

/vendor/bin/install_tivimate\.sh    u:object_r:tivimate_install_exec:s0

注意:路径中的 . 需要转义为 \.

4.3 .te 文件 — 定义域和转换规则

文件: vendor/<vendor>/common/sepolicy/<domain>.te

# 1. 声明 domain 类型和 exec 类型
type <domain>, domain;
type <domain>_exec, exec_type, vendor_file_type, file_type;

# 2. 建立 init → domain 的转换规则
init_daemon_domain(<domain>)

# 3. 设为宽容模式(开发阶段推荐)
permissive <domain>;

完整示例:

# vendor/giec/common/sepolicy/tivimate_install.te

type tivimate_install, domain;
type tivimate_install_exec, exec_type, vendor_file_type, file_type;
init_daemon_domain(tivimate_install)

permissive tivimate_install;

4.4 关键宏说明

作用
init_daemon_domain(domain) 自动生成 init → domain 的域转换规则
permissive domain 设为宽容模式:允许所有操作,只记录审计日志。调试阶段使用,正式发布应改为显式 allow 规则

4.5 开发步骤

1. 写 .te 文件(定义类型 + init_daemon_domain + permissive)
2. 写 file_contexts 条目
3. 编译,刷机,确认功能正常
4. (可选)根据 audit2allow 生成的日志,用显式 allow 替换 permissive

五、构建系统集成

5.1 device.mk 配置

文件: vendor/<vendor>/device-<vendor>.mk

# 脚本 → /vendor/bin/
# init.rc → /vendor/etc/init/
PRODUCT_COPY_FILES += \
    vendor/giec/executable/install_tivimate.sh:vendor/bin/install_tivimate.sh \
    vendor/giec/common/init.tivimate.rc:vendor/etc/init/init.tivimate.rc

5.2 编译产物位置

源文件 设备上路径
vendor/giec/executable/xxx.sh /vendor/bin/xxx.sh
vendor/giec/common/init.xxx.rc /vendor/etc/init/init.xxx.rc
vendor/giec/common/sepolicy/xxx.te 编译进 /vendor/etc/selinux/

5.3 目录结构总结

vendor/giec/
├── device-giec.mk                          # PRODUCT_COPY_FILES 声明
├── executable/
│   └── install_tivimate.sh                 # 脚本本体
├── common/
│   ├── init.tivimate.rc                    # init 服务定义
│   └── sepolicy/
│       ├── file_contexts                   # 脚本文件标签
│       └── tivimate_install.te             # 域定义和转换规则
└── apps/TiviMateCustom/
    └── TiviMateCustom.apk                  # 待安装的 APK

六、调试方法

6.1 查看 init 是否加载了 rc

# 搜索 init 相关日志
dmesg | grep -iE "init.*import|Parsing.*init"

6.2 查看服务启动状态

# 查看 init 是否尝试启动服务,以及是否被 SELinux 拦截
dmesg | grep -i "<service_name>"

# 如果 SELinux 拦截,会看到类似:
# init: Could not start service 'xxx' ... has incorrect label
# or no domain transition ...

6.3 查看脚本执行日志

# 脚本中的 echo 输出会出现在 logcat 中
logcat -d | grep -i "install_tivimate"

# 或者直接看完整 logcat
logcat -d -s install_tivimate

6.4 查看 SELinux 审计日志

# 查看所有 SELinux denials
logcat -d | grep "avc.*denied"

# 过滤特定域
logcat -d | grep "avc.*denied.*tivimate"

6.5 手动测试

# 确认脚本有执行权限
ls -l /vendor/bin/install_tivimate.sh

# 手动跑脚本验证逻辑
sh /vendor/bin/install_tivimate.sh

# 重置标记以便重新测试
rm /data/local/tmp/.tivimate_installed

# 确认 SELinux 标签已生效
ls -Z /vendor/bin/install_tivimate.sh
# 预期输出: u:object_r:tivimate_install_exec:s0 ...

6.6 验证 property

getprop sys.boot_completed   # 应返回 1

七、常见陷阱速查

# 错误现象 原因 解决
1 脚本完全不执行,dmesg 无任何相关日志 init 没加载 rc 文件 检查 rc 是否拷贝到 /vendor/etc/init/,检查 dmesg 是否有 init import 日志
2 脚本不执行,dmesgCould not start service...has incorrect label SELinux 拦截 1) file_contexts 打标签 2) .te 定义域 + init_daemon_domain 3) permissive
3 脚本执行了但 pm install 失败 PMS 未就绪 脚本中 while boot_completed != 1 + sleep 2 等待
4 on property:sys.boot_completed=1 不触发 rc 加载晚于属性设置 改用 on boot
5 每重启都重新安装 没有防重复机制 用标记文件(/data/local/tmp/)判断
6 脚本看起来跑了但标记文件没生成 pm install 失败,&& 短路 检查 logcat 中 PMS 日志
7 #!/bin/bash 不工作 Android 没有 bash #!/system/bin/sh

八、完整工作流程(本案例总结)

编写阶段:
  1. 写脚本  vendor/giec/executable/install_tivimate.sh
  2.  rc    vendor/giec/common/init.tivimate.rc
  3.  SELinux:
     a. vendor/giec/common/sepolicy/tivimate_install.te
        (定义域 + init_daemon_domain + permissive)
     b. vendor/giec/common/sepolicy/file_contexts
        (脚本打标签)
  4. 注册到构建系统  vendor/giec/device-giec.mk
     PRODUCT_COPY_FILES: 脚本 + rc

编译:
  m  APK 预置到 /product/app/ + 脚本/rc/SELinux 策略写入镜像

设备首启:
  init second_stage
     加载 /vendor/etc/init/init.tivimate.rc
     on boot 触发  start tivimate-install
     init  tiviate_install 域转换(SELinux 通过)
     install_tivimate.sh:
         检查标记文件  不存在
         while boot_completed != 1 等待
         pm install -r  安装到 /data/app/
         touch 标记文件  退出

后续启动:
  init  on boot  start tivimate-install
     检查标记文件  存在  exit 0