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.
必须同时做两件事:
- 给脚本打标签(
file_contexts) - 建立域转换规则(
.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 | 脚本不执行,dmesg 有 Could 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