Android开机脚本避坑指南,这些错误别再犯了
在Android系统定制和深度开发中,让自定义脚本在设备启动时自动运行,是很多工程师的刚需。比如初始化硬件参数、预加载服务、配置网络环境、启动守护进程等。但看似简单的“开机执行一段shell”,实际落地时却常常卡在各种隐蔽问题上——脚本根本没跑、报Permission denied、init.rc语法错误、SELinux拒绝访问、属性设置不生效……更糟的是,这些问题往往只在重启后才暴露,调试周期长、日志难抓、复现困难。
本文不是从零讲原理的教程,而是基于真实项目经验(覆盖Android 8.0至13的多款MTK与高通平台)整理出的一份实战避坑清单。它不罗列所有理论,只聚焦那些你90%概率会踩、但官方文档不会明说、社区帖子一笔带过、调试时让人抓狂的典型错误。每一条都对应一个真实失败场景、一个可验证的修复动作、一个能立刻用上的检查项。
如果你曾遇到“明明写好了脚本,push进去手动能跑,一加到init.rc就静默失败”“setprop设置了属性,adb shell里查不到”“selinux关闭了还是起不来服务”,那么这篇文章就是为你写的。
1. 脚本本身:第一道关,错在开头就全盘皆输
很多人以为shell脚本只要语法对就能跑,但在Android init环境中,解释器路径、换行符、执行权限这三样,任何一个不对,脚本连第一行都不会执行。而init进程不会输出任何错误提示,只会默默跳过。
1.1 解释器路径必须严格匹配系统实际路径
Android的init进程调用脚本时,不走PATH环境变量查找,而是直接按你写的路径去执行。常见错误写法:
#!/bin/sh # 错误!绝大多数Android系统没有/bin/sh #!/system/xbin/sh # 错误!xbin目录在Android 8.0+已废弃,且多数系统不挂载正确写法(以Android 8.0+主流系统为准):
#!/system/bin/sh验证方法:在已root的设备上执行
ls -l /system/bin/sh,确认该文件存在且是可执行文件。如果看到指向/apex/com.android.runtime/bin/sh的软链接,说明路径正确;若报No such file,则需根据你的系统实际路径调整(如部分AOSP分支可能为/apex/com.android.runtime/bin/sh,此时脚本头也必须同步修改)。
1.2 文件换行符必须是LF(Unix格式),绝不能是CRLF(Windows格式)
这是最容易被忽略的“隐形杀手”。用Windows记事本、Notepad++未设LF、VS Code未切换行符模式保存的脚本,上传到Android后,init会因读取到\r字符而解析失败,完全静默退出,无任何日志。
验证与修复方法:
- 在Linux/macOS终端执行:
file init.test.sh,输出应为init.test.sh: POSIX shell script, ASCII text executable
若显示with CRLF line terminators,则已中毒。 - 修复命令(Linux/macOS):
dos2unix init.test.sh - VS Code用户:右下角状态栏点击
CRLF→ 选择LF→ 保存
1.3 执行权限必须由chmod显式设置,不能依赖adb push -r
adb push默认上传的文件权限是644(rw-r--r--),而init要求脚本必须有+x权限。即使你adb shell里手动chmod +x过,一旦设备重启,/system分区重新挂载为只读,权限又会丢失。
正确做法:将脚本放入/system/bin/或/vendor/bin/前,必须通过adb root && adb remount后,用adb shell chmod 755 /system/bin/init.test.sh显式赋权。更稳妥的做法是,在编译阶段将脚本作为PRODUCT_COPY_FILES的一部分,并在Android.mk中指定$(TARGET_COPY_OUT_SYSTEM)/bin/init.test.sh:system/bin/init.test.sh,由构建系统自动处理权限。
2. init.rc配置:语法、时机与位置,三者缺一不可
init.rc不是普通配置文件,它是init进程的“运行时蓝图”。语法错误、时机错配、位置不当,都会导致service注册失败或无法触发。
2.1 service声明必须完整,oneshot与disabled不能共存
常见错误写法:
service test_service /system/bin/init.test.sh class main user root group root oneshot disabled # 错误!disabled表示该service永不自动启动,与oneshot矛盾正确逻辑:
- 如果希望每次开机都执行一次(最常用),去掉
disabled,保留oneshot; - 如果希望仅在特定trigger下执行(如
on property:sys.boot_completed=1),则去掉oneshot,加上disabled,并在对应trigger块中用start test_service显式启动; - 如果希望常驻后台持续运行,则去掉
oneshot,并确保脚本内有死循环或exec阻塞逻辑。
2.2 trigger时机必须晚于关键系统服务就绪
很多脚本依赖/data分区、/sdcard挂载、网络模块或property服务。若在on early-init或on init阶段启动,/data尚未挂载,setprop会失败,touch /data/misc/test.flag会报No such file or directory。
推荐trigger时机(按优先级排序):
on property:sys.boot_completed=1—— 系统UI已启动,所有核心服务就绪,最安全通用;on property:dev.bootcomplete=1—— 更早一点,但部分服务可能未完全ready;on boot—— 仅当脚本不依赖/data、/sdcard、网络等资源时使用。
示例(推荐):
on property:sys.boot_completed=1 start test_service2.3 不要硬改/system/etc/init.rc,优先使用厂商专用rc文件
init.rc是AOSP核心文件,直接修改会导致OTA升级失败、diff冲突、维护困难。主流芯片平台(MTK、Qualcomm、Rockchip)均提供客户扩展入口。
正确做法:
- MTK平台:在
device/mediatek/common/init/init.mtXXXX.rc(XXXX为芯片型号)中添加; - 高通平台:在
device/qcom/common/rootdir/etc/init.qcom.rc中添加; - 通用方案:在
/system/etc/init/目录下新建test_service.rc(Android 8.0+支持),内容格式与init.rc一致,init进程会自动加载。
3. SELinux策略:关闭SELinux≠万事大吉,策略缺失仍会失败
这是最让开发者困惑的一点:明明getenforce返回Permissive甚至Disabled,脚本还是起不来。原因在于,SELinux策略缺失会导致init进程在创建service时就拒绝注册,根本不会走到执行脚本那一步。
3.1file_contexts规则必须精确匹配脚本路径与类型
错误写法(路径不匹配):
/(system|vendor)/bin/init.test.sh u:object_r:test_service_exec:s0 # system和vendor是并列关系,不是system/vendor正确写法(以脚本放在/system/bin/为例):
/system/bin/init.test.sh u:object_r:test_service_exec:s0或(兼容system/vendor双路径):
/(system|vendor)/bin/init\.test\.sh u:object_r:test_service_exec:s0注意:正则中
.需转义为\.,否则匹配任意字符。
3.2te文件中必须声明init_daemon_domain,且domain名与service名一致
错误写法(domain名不一致):
type my_test_service, coredomain; # domain名是my_test_service init_daemon_domain(my_test_service); # 匹配 # 但init.rc中写的是: # service test_service /system/bin/init.test.sh # service名是test_service,不匹配!正确做法:te文件中的domain名、init.rc中的service名、file_contexts中的type名,三者必须完全一致(建议统一用test_service):
type test_service, coredomain; type test_service_exec, exec_type, vendor_file_type, file_type; init_daemon_domain(test_service);service test_service /system/bin/init.test.sh # service名与domain名一致3.3seclabel必须显式指定,不能省略
init.rc中service块末尾的seclabel行,是告诉init“这个service应该以哪个SELinux context运行”。即使你认为SELinux已关,也必须写。
错误(缺失):
service test_service /system/bin/init.test.sh class main user root group root oneshot # 缺少seclabel!init会使用默认context,大概率拒绝正确:
service test_service /system/bin/init.test.sh class main user root group root oneshot seclabel u:object_r:test_service_exec:s0 # 必须与file_contexts中定义的type一致4. 调试与验证:如何快速定位问题根源
当脚本没执行时,不要盲目改代码。按以下顺序逐层排查,90%的问题能在5分钟内定位。
4.1 第一层:确认service是否被init识别
执行:
adb shell getprop | grep test # 查看是否有test.prop adb shell ls -l /system/bin/init.test.sh # 检查文件存在、权限、路径 adb shell cat /proc/1/cmdline # 确认init进程已加载最新rc文件(输出含test_service即已加载)4.2 第二层:检查init日志(最有效)
adb logcat -b events | grep -i "test_service\|init" # 查看init事件流 adb logcat -b kernel | grep -i "avc\|selinux" # 查看SELinux拒绝日志(即使Permissive模式也会打印)- 若
logcat -b events中完全没有test_service相关输出→ 问题在init.rc语法、trigger时机、service未注册; - 若有
test_service: starting但无后续 → 问题在脚本执行失败(路径、权限、解释器); - 若有
avc: denied→ SELinux策略缺失,根据log中的{ execute }等字段补授权。
4.3 第三层:最小化验证脚本
将原脚本临时替换为最简版,排除脚本内部逻辑干扰:
#!/system/bin/sh # 最小验证脚本 setprop test.debug 1 echo "test_service started at $(date)" > /data/local/tmp/test.log然后adb shell cat /data/local/tmp/test.log,若文件生成且内容正确,则脚本执行成功,问题在原脚本逻辑;若文件不存在,则问题在执行环节。
5. 进阶建议:让开机脚本更健壮、更易维护
避开坑只是第一步,真正工程化还需要考虑长期可维护性。
5.1 用/data/local/tmp/替代/system/存放临时数据
/system是只读分区,脚本内写文件(如log、flag)必然失败。所有运行时产生的数据,必须存放在/data、/cache或/dev等可写分区。/data/local/tmp/是专为开发者设计的可写目录,无需额外权限。
5.2 使用start/stop命令替代硬编码trigger
在脚本中避免sleep 10等待其他服务,改用wait_for_prop:
#!/system/bin/sh # 等待property服务就绪 wait_for_prop sys.boot_completed 1 # 等待/data挂载 wait_for_prop ro.crypto.state encrypted # 再执行业务逻辑 setprop test.ready 15.3 将脚本纳入Android构建系统,而非手动push
在Android.mk中添加:
PRODUCT_COPY_FILES += \ device/mycompany/common/init.test.sh:/system/bin/init.test.sh:0755 \ device/mycompany/common/test_service.rc:/system/etc/init/test_service.rc:0644这样每次m编译,脚本和rc文件都会自动打包进system.img,彻底规避手动操作失误。
总结
Android开机脚本不是“写完就能跑”的简单任务,而是一条横跨shell、init机制、SELinux、分区挂载、系统启动流程的综合链路。本文梳理的五大类问题——脚本解释器与格式、init.rc语法与时机、SELinux策略细节、精准调试方法、工程化实践建议——全部来自真实产线踩坑记录。它们共同指向一个核心原则:在Android世界里,“能跑”不等于“写对了”,只有每一层都严丝合缝,才能实现真正的稳定启动。
记住这三条铁律:
- 脚本头必须是
#!/system/bin/sh,文件必须LF换行,权限必须755; - init.rc中service名、te文件domain名、file_contexts type名,三者必须一字不差;
- 调试先看
logcat -b events,再看logcat -b kernel | grep avc,最后动脚本。
当你下次再为开机脚本发愁时,不妨打开这篇指南,对照着逐条检查。大多数时候,问题就藏在那一个没注意的斜杠、一个没转义的点、一行被注释掉的seclabel里。
--- > **获取更多AI镜像** > > 想探索更多AI镜像和应用场景?访问 [CSDN星图镜像广场](https://ai.csdn.net/?utm_source=mirror_blog_end),提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。