Android开机脚本开发全流程,从编写到测试
在Android系统定制和深度优化过程中,开机自启动脚本是实现设备初始化、服务预加载、硬件配置等关键任务的常用手段。但很多开发者第一次尝试时会遇到脚本不执行、权限被拒、SELinux拦截、init.rc语法错误等问题,反复调试耗时又低效。本文以真实工程实践为基础,完整梳理从脚本编写、SELinux策略配置、init.rc集成到真机验证的全流程,所有步骤均基于Android 8.0+主流平台(含高通、MTK)验证通过,不依赖ADB shell手动触发,真正实现“烧录即生效”的可靠启动。
全文不讲抽象理论,只聚焦可落地的操作细节:为什么/system/bin/sh不能写成/bin/sh,为什么file_contexts里路径要加空格,oneshot和disabled怎么选,以及如何用串口日志快速定位SELinux拒绝原因。每一步都附带验证方法和典型报错对照,帮你避开90%的坑。
1. 开机脚本编写:从第一行开始就踩对点
Android的init进程在系统启动早期就解析并执行shell脚本,但它使用的shell环境与普通adb shell完全不同——它不加载.bashrc,不支持高级语法,甚至对换行符和BOM头都极其敏感。因此,脚本编写不是简单复制Linux经验,而是要严格遵循Android init的运行约束。
1.1 脚本基础结构与关键规范
新建脚本文件init.test.sh,保存为UTF-8无BOM格式,绝对不要用Windows记事本编辑。内容如下:
#!/system/bin/sh # 注意:必须使用 /system/bin/sh 或 /system/xbin/sh # Linux下的 /bin/sh 在Android中不存在,硬写会导致静默失败 # 设置调试属性,便于后续验证是否执行 setprop sys.boot.test_script 1 # 执行实际业务逻辑(示例:创建临时标记文件) touch /data/misc/test_boot_flag chmod 644 /data/misc/test_boot_flag # 可选:记录时间戳便于分析启动时序 echo "$(date): test script executed" >> /data/misc/boot_log.txt必须遵守的3个硬性规则:
- 解释器路径必须准确:Android系统中
/system/bin/sh是busybox提供的精简版ash,功能有限但稳定;/bin/sh路径根本不存在,写错后init会直接跳过该service,无任何日志提示。 - 禁止使用bash特有语法:如
[[ ]]、$(())、数组、函数定义等。只使用POSIX标准语法:[ ]、$(( ))、if/then/else/fi、while/do/done。 - 路径必须绝对且可写:
/system分区默认只读,/data和/dev是安全写入区。避免向/system/etc或/vendor/bin写文件,否则会因挂载权限失败。
1.2 本地快速验证:不烧机也能确认脚本有效性
在push到设备前,请先在本地模拟验证语法正确性:
# 在Linux/macOS终端执行(需安装busybox) busybox sh -n init.test.sh # 仅语法检查,不执行 # 输出为空表示语法合法 # 进一步验证执行逻辑(需adb连接) adb push init.test.sh /data/local/tmp/ adb shell "chmod +x /data/local/tmp/init.test.sh" adb shell "/data/local/tmp/init.test.sh" adb shell "getprop sys.boot.test_script" # 应输出 1 adb shell "ls -l /data/misc/test_boot_flag" # 应存在且权限正确这一步能提前发现90%的脚本级错误,比如setprop拼写错误、路径不存在、权限不足等,避免反复烧机浪费时间。
2. SELinux策略配置:绕过“Permission denied”的核心防线
Android 8.0后强制启用SELinux,即使setenforce 0临时关闭,init进程仍按策略启动服务。未声明的脚本会被avc: denied拦截,现象是service看似启动了,但脚本一行都不执行,logcat里只有零星的init: starting service 'xxx'...日志。
2.1 定义服务类型与执行文件类型
在device/your_company/sepolicy/vendor/non_plat/目录下(不同平台路径略有差异,MTK在basic/non_plat,高通在vendor/non_plat),新建test_service.te文件:
# 定义服务域类型 type test_service, domain; # 定义脚本文件类型 type test_service_exec, exec_type, vendor_file_type, file_type; # 允许该服务作为init守护进程运行 init_daemon_domain(test_service); # 允许test_service域读取并执行test_service_exec类型的文件 allow test_service test_service_exec:file { read open getattr execute }; # 允许test_service设置系统属性(对应脚本中的setprop) allow test_service system_file:file { read }; allow test_service system_prop:property_service { set }; # 允许写入/data分区(对应touch和echo操作) allow test_service data_file:dir { add_name write }; allow test_service data_file:file { create read write open getattr };关键说明:
domain比coredomain更精准,避免过度授权;vendor_file_type确保类型适用于/vendor/bin路径(若脚本放/system/bin,则用system_file_type);property_service { set }是setprop必需权限,漏掉将导致属性设置失败但无明显报错。
2.2 关联文件路径与SELinux上下文
在同目录下的file_contexts文件中添加一行(注意路径后有空格和类型声明):
/system/bin/init\.test\.sh u:object_r:test_service_exec:s0重要细节:
- 路径使用正则转义
.,写成init\.test\.sh而非init.test.sh; - 行尾必须有空格,然后才是
u:object_r:...,缺少空格会导致sepolicy编译失败; - 若脚本放在
/vendor/bin/,路径应为/vendor/bin/init\.test\.sh; - 修改后需重新编译sepolicy,执行
m sepolicy或完整编译。
3. init.rc集成:让脚本真正被init进程识别
init.rc是Android启动的核心配置文件,但直接修改system/core/rootdir/init.rc风险极高。推荐做法是利用厂商预留的扩展机制,在独立的init.<chip>.rc中声明service。
3.1 创建独立init配置文件
在device/your_company/<platform>/目录下(如device/mediatek/mt6765/),新建init.test.rc:
# 声明service service test_service /system/bin/init.test.sh class main user root group root oneshot seclabel u:object_r:test_service_exec:s0 # 触发时机:在main类服务启动后立即执行 on property:sys.boot_completed=1 start test_service参数详解:
class main:归入main服务组,确保在Zygote启动前执行;user root&group root:以root权限运行,避免文件操作权限问题;oneshot:执行完即退出,适合初始化类脚本;若需常驻,改用disabled+start xxx命令;seclabel:必须与file_contexts中定义的类型完全一致;on property:sys.boot_completed=1:作为备用触发条件,确保即使init.rc顺序异常也能执行。
3.2 注册配置文件到构建系统
在device/your_company/<platform>/Android.mk或BoardConfig.mk中添加:
# 将init.test.rc打包进ramdisk PRODUCT_COPY_FILES += \ device/your_company/<platform>/init.test.rc:root/init.test.rc或使用Android.bp(Android 9.0+):
// device/your_company/<platform>/Android.bp prebuilt_etc { name: "init.test.rc", src: "init.test.rc", sub_dir: "init", }编译后检查out/target/product/<product>/root/init.test.rc是否存在,确认已正确打包。
4. 真机测试与问题排查:从日志定位每一处失败
烧录新镜像后,不要急于看结果,先通过串口或logcat捕获关键日志,这是高效调试的基石。
4.1 必查三类日志源
| 日志来源 | 查看命令 | 关键信息 |
|---|---|---|
| init启动日志 | `adb logcat -b events | grep init` |
| SELinux拒绝日志 | `adb logcat -b events | grep avc` |
| 脚本执行日志 | `adb logcat | grep "test script"` |
4.2 典型问题与速查解决方案
问题1:logcat无任何
test_service相关日志
→ 检查init.test.rc是否成功打包进ramdisk:adb shell ls /init.test.rc;
→ 检查init.rc中是否包含import /init.test.rc(部分平台需显式导入)。问题2:有
starting service日志,但getprop sys.boot.test_script返回空
→ 检查SELinux:adb shell getenforce,若为Enforcing,查看avc日志;
→ 检查脚本路径:adb shell ls -Z /system/bin/init.test.sh,确认SELinux上下文为test_service_exec。问题3:脚本执行但
/data/misc/test_boot_flag未生成
→ 检查/data/misc/目录权限:adb shell ls -ld /data/misc/,应为drwxr-xr-x;
→ 检查脚本中touch命令是否被SELinux拦截:avc日志中搜索data_file。问题4:串口日志显示
init: cannot find '/system/bin/init.test.sh'
→ 脚本未push到正确路径:adb push init.test.sh /system/bin/;
→/system分区未remount:adb remount后再push。
5. 工程化建议:让开机脚本更健壮、可维护
一个能上产线的开机脚本,不能只满足“能跑”,还需考虑稳定性、可追溯性和升级兼容性。
5.1 脚本自身增强实践
添加执行锁机制:防止多实例并发(尤其在
restart场景下)if [ -f /data/misc/test_boot_lock ]; then exit 0 fi touch /data/misc/test_boot_lock # ... 主逻辑 ... rm -f /data/misc/test_boot_lock记录详细执行日志:
exec >> /data/misc/test_boot.log 2>&1 echo "[$(date)] START init.test.sh (pid $$)"版本标识与降级保护:
# 在脚本开头声明版本 SCRIPT_VERSION=1.0.0 # 检查系统版本兼容性 ANDROID_VERSION=$(getprop ro.build.version.release | cut -d. -f1) [ "$ANDROID_VERSION" -lt "8" ] && exit 0
5.2 构建与测试自动化
将验证步骤写入CI脚本,每次提交自动检查:
# verify_boot_script.sh set -e adb wait-for-device adb shell getprop sys.boot.test_script | grep -q "1" || { echo "FAIL: script not executed"; exit 1; } adb shell ls /data/misc/test_boot_flag | grep -q "test_boot_flag" || { echo "FAIL: flag file missing"; exit 1; } echo "PASS: Boot script verified"结合CSDN星图镜像广场的预置环境,可一键部署包含完整sepolicy和init.rc的测试镜像,大幅缩短验证周期。
6. 总结:一条可复用的开机脚本交付链路
回顾整个流程,我们构建的不是单个脚本,而是一套标准化交付链路:
编写 → 本地语法/逻辑验证 → SELinux策略定义 → init.rc集成 → ramdisk打包 → 真机日志驱动调试 → 自动化回归测试。
这条链路的关键在于“分层验证”:脚本层验证语法和逻辑,SELinux层验证权限模型,init层验证启动时序,日志层验证最终效果。任一环节失败,都能在对应层级快速定位,避免陷入“黑盒式”反复烧机。
最后提醒:永远优先使用/data而非/system进行写操作;永远用getprop和ls -Z验证状态而非凭经验猜测;永远把串口日志当作第一手证据。当你能从avc denied日志中一眼看出缺哪条allow规则时,你就真正掌握了Android底层启动的脉搏。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。