Android开发必备技能:开机启动脚本编写与调试技巧
在Android系统定制和深度开发中,让自定义服务或脚本在设备上电后自动运行是一项基础但关键的能力。无论是实现硬件初始化、日志采集、远程监控,还是为车载、IoT设备添加专属功能,掌握开机启动脚本的完整链路——从编写、集成到SELinux适配与调试——都是Android系统工程师绕不开的核心技能。
本文不讲抽象理论,不堆砌源码片段,而是以一个真实可复现的“测试开机启动脚本”镜像为蓝本,带你走通从零编写、部署、验证到排障的全流程。所有步骤均基于Android 8.0及以上主流版本(AOSP及MTK平台实测通过),代码简洁、路径明确、权限清晰,小白照着做就能看到getprop test.prop返回值,老手也能从中获得调试思路和避坑经验。
1. 为什么不能只写个.sh就完事?
很多开发者第一次尝试时会发现:脚本手动执行没问题,放进init.rc却完全没反应。这不是你的脚本写错了,而是Android早已不是Linux桌面环境。
Android启用了强制性的SELinux策略,且init进程以u:r:init:s0域运行,它不会无条件执行任意路径下的可执行文件。此外,init.rc语法有严格规范,服务声明、执行上下文、用户组设置、启动时机(early-init/init/late-init)都直接影响脚本能否真正跑起来。
所以,一个能“开机就动”的脚本,必须同时满足四个条件:
- 脚本本身语法正确、路径可访问、解释器声明无误
- SELinux类型(type)已正确定义
- 文件上下文(file_context)已绑定该类型
init.rc中服务声明符合语法,且seclabel指向正确域
缺一不可。下面我们就按这个逻辑顺序,逐层构建、逐层验证。
2. 编写一个最小可行脚本
我们不追求复杂功能,先确保“能跑”。目标很明确:设备启动后,设置一个系统属性test.prop,值为111。后续只需执行adb shell getprop test.prop,返回111即代表成功。
2.1 创建脚本文件
新建文件init.test.sh,内容如下:
#!/system/bin/sh # 注意:Android系统默认shell路径是 /system/bin/sh 或 /system/xbin/sh # 不要写成 #!/bin/sh —— 这在Android上会直接失败 # 设置一个测试属性(推荐方式:避免创建文件引发权限问题) setprop test.prop 111 # 可选:写入日志便于调试(需确保logd已就绪) log -t "INIT_TEST" "Script executed successfully, test.prop = $(getprop test.prop)"2.2 关键细节说明
- 解释器路径必须准确:
/system/bin/sh是AOSP标准路径;部分厂商可能用/system/xbin/sh(如旧版BusyBox)。若不确定,可先adb shell ls /system/bin/sh /system/xbin/sh确认。 - 不要创建文件或修改系统分区:初学者常试图
echo "ok" > /data/misc/test.log,但/data挂载完成晚于init阶段,此时写入会失败。用setprop是最轻量、最稳妥的验证手段。 - 务必先手动验证:将脚本push到设备并赋予执行权限:
手动能跑,才说明脚本逻辑和权限无问题,再进入下一步。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 test.prop # 应输出 111
3. 定义SELinux类型与上下文
Android 8.0起全面启用sepolicy(SELinux策略),任何新服务都必须被策略识别,否则init会拒绝启动。
3.1 创建TE(Type Enforcement)策略文件
新建test_service.te,内容如下:
# 定义服务域类型 type test_service, coredomain; # 定义脚本文件类型(可执行) type test_service_exec, exec_type, vendor_file_type, file_type; # 允许init域过渡到该服务域(关键!) init_daemon_domain(test_service); # 允许shell域(adb shell)读取该脚本(仅用于调试,非必需) # allow shell test_service_exec:file { read open getattr execute };说明:
init_daemon_domain()是宏,它自动添加了init对test_service的transition、dyntransition等必要权限。比手动写allow init test_service:process ...更安全、更标准。
3.2 绑定文件上下文
在file_contexts中为脚本文件指定类型。路径取决于你使用的平台:
- 高通平台:通常在
device/qcom/sepolicy/vendor/file_contexts - MTK平台:如参考文档所述,在
device/mediatek/sepolicy/basic/non_plat/file_contexts - 通用AOSP:可放在
device/<vendor>/<product>/sepolicy/vendor/file_contexts
添加一行(注意空格和缩进):
/system/bin/init\.test\.sh u:object_r:test_service_exec:s0重要提醒:
- 正则转义:
.sh中的点号必须写成\.,否则匹配失败; - 路径必须与
init.rc中service声明的路径完全一致; - 即使你临时关闭了SELinux(
setenforce 0),这行也必须添加,否则init无法识别该文件类型,服务仍不会启动。
4. 在init.rc中声明服务
不要直接修改system/core/rootdir/init.rc主文件——它由AOSP维护,易被覆盖。应使用厂商推荐的扩展机制。
4.1 推荐做法:使用独立的init.XXX.rc
大多数芯片方案都预留了扩展入口。例如:
- MTK平台:在
device/mediatek/common/init/init.mt6765.rc(或对应芯片名)末尾添加 - 高通平台:在
device/qcom/msmxxx/init.qcom.rc中添加 - 通用方式:在
device/<vendor>/<product>/rootdir/init.<product>.rc中添加
添加以下服务声明:
# 测试开机启动服务 service test_service /system/bin/init.test.sh class main user root group root oneshot seclabel u:object_r:test_service_exec:s04.2 参数详解
| 字段 | 说明 |
|---|---|
class main | 归入main类,随init主流程启动(早于late-init) |
user root/group root | 以root身份运行,确保权限足够(测试阶段推荐) |
oneshot | 执行一次即退出,适合初始化类脚本;若需常驻,请改用disabled+start xxx触发 |
seclabel | 必须与file_contexts中定义的类型一致,否则SELinux拒绝加载 |
4.3 启动时机选择
on early-init:内核刚启动、/dev未挂载前 → 仅限极底层硬件操作on init:/dev挂载后、/system挂载前 → 少用,风险高on late-init:/system、/data均已挂载 →推荐绝大多数场景on property:sys.boot_completed=1:系统UI就绪后 → 适合依赖Framework的服务
本例用class main即可,它在late-init之前、/system已挂载后执行,安全且及时。
5. 编译、刷机与快速验证
完成上述三步后,需重新编译并刷写boot.img或system.img。具体命令依平台而异,但核心流程一致:
5.1 编译步骤(以AOSP为例)
# 1. 确保te文件和file_contexts已加入编译系统 # (通常在Android.mk或BoardConfig.mk中引用) # 2. 编译system.img(含init.rc和脚本) m systemimage # 3. 编译boot.img(含init程序和sepolicy) m bootimage # 4. 刷写(谨慎操作!) fastboot flash system system.img fastboot flash boot boot.img fastboot reboot5.2 刷机后验证流程
设备重启后,按顺序执行以下命令,每一步都是关键检查点:
# 步骤1:确认服务是否被init识别 adb shell 'ls -Z /system/bin/init.test.sh' # 应显示 u:object_r:test_service_exec:s0 # 步骤2:确认init是否加载了该服务 adb shell 'getprop | grep test.prop' # 若已启动,此处应立即出现 test.prop=111 # 步骤3:若未出现,检查init日志(最有效!) adb shell dmesg | grep -i "test\|avc\|init" # 关键线索:AVC denied 表示SELinux拦截;init: starting service 'test_service' 表示已触发 # 步骤4:手动触发服务(调试用) adb shell start test_service adb shell getprop test.prop # 应返回111调试黄金法则:永远先看
dmesg,再查logcat。dmesg输出init和kernel级日志,AVC avc: denied错误会直接告诉你缺哪条SELinux规则。
6. 常见问题与实战排障指南
即使严格按步骤操作,仍可能遇到“无声失败”。以下是高频问题及对应解法,全部来自真实项目踩坑总结。
6.1 问题:getprop test.prop始终为空
| 可能原因 | 检查方法 | 解决方案 |
|---|---|---|
| 脚本未被init执行 | `adb shell ps | grep test_service` |
| SELinux拦截(最常见) | adb shell dmesg | grep avc | 输出类似avc: denied { execute } for path="/system/bin/init.test.sh"→ 需补充allow init test_service_exec:file execute;到test_service.te |
| 脚本路径错误 | adb shell ls -l /system/bin/init.test.sh | 确认文件存在、权限为-rwxr-xr-x、路径与init.rc中完全一致 |
init.rc未生效 | adb shell cat /proc/1/cmdline | 若显示init而非init --second-stage,说明未加载新版init.rc,检查编译是否包含该文件 |
6.2 问题:脚本执行了,但log命令无输出
log命令依赖logd服务,它在late-init阶段才启动。若脚本在early-init或init阶段运行,log会静默失败。- 解决方案:改用
echo "msg" > /dev/kmsg(内核日志)或坚持用setprop(最可靠)。
6.3 问题:MTK平台编译报错“type test_service is not defined”
- 原因:
test_service.te未被Android.mk或BoardConfig.mk引用。 - 解决方案:在
device/mediatek/sepolicy/basic/non_plat/Android.mk中添加:
BOARD_SEPOLICY_DIRS += \ device/mediatek/sepolicy/basic/non_plat7. 进阶建议:让脚本更健壮、更工程化
完成基础功能只是起点。在实际项目中,还需考虑以下几点:
7.1 权限最小化原则
- 生产环境禁止使用
user root。应创建专用用户(如testuser),并在init.rc中指定:user testuser group testuser system inet net_admin - 对应在
sepolicy中定义该用户类型,并授予最小必要权限(如仅net_admin而非root)。
7.2 支持热更新与调试开关
在脚本开头加入判断逻辑,避免每次刷机:
#!/system/bin/sh # 检查是否启用测试模式(可通过adb setprop动态控制) if [ "$(getprop test.enable)" = "1" ]; then setprop test.prop 111 log -t "INIT_TEST" "Test mode enabled" else log -t "INIT_TEST" "Test mode disabled" fi然后调试时只需:
adb shell setprop test.enable 1 adb shell stop test_service && adb shell start test_service7.3 日志归档与错误捕获
增强脚本鲁棒性:
#!/system/bin/sh exec 2>/dev/log/test_init.log # 重定向stderr到日志文件 set -e # 遇错退出 setprop test.prop 111 || { log -t "INIT_TEST" "Failed to set property" exit 1 }8. 总结:掌握开机启动,就是掌握Android系统控制权
写一个开机脚本,表面看是几行代码和几处配置,背后却是对Android启动流程、SELinux机制、init语法、系统分区结构的综合理解。本文带你走通的不是“如何让test.prop变111”,而是一套可复用、可迁移、可调试的系统级开发方法论:
- 从最小脚本入手,手动验证先行,杜绝“黑盒”调试
- SELinux不是障碍,而是安全护栏——用
dmesg读懂它的语言 init.rc是系统脉搏,理解class、oneshot、seclabel才能精准施控- 所有配置必须闭环验证:
ls -Z→ps→getprop→dmesg
当你能自信地为任意Android设备添加专属启动行为时,你就真正跨过了系统开发的门槛。下一步,可以尝试让脚本拉起一个守护进程、监听USB设备、或与HAL层交互——那将是另一个精彩世界。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。