ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

安卓15 ROM定制:sys.boot_completed属性与系统应用自启脚本实战

安卓15 ROM定制:sys.boot_completed属性与系统应用自启脚本实战 最近在调一个安卓15的定制ROM任务听起来不复杂开机后自动管理一批系统应用再把几个自启脚本跑起来。可真正动手之后我才意识到“什么时候执行”这件事在安卓上远比想象中讲究而一切的起点就是sys.boot_completed1这个属性。如果你做过ROM定制一定见过这个字眼但很多人只是把它当成一个“开机完成”的通知没太当回事。直到我在这台设备上既要控制预装应用、又要保证脚本不早不晚地在正确时机启动才彻底把这条链路捋清楚谁写入的、什么时候写入、在哪里判断、误判会造成什么后果。这篇文章就把整个方案的思路和可复现的脚本实例拆开来讲适合正在做安卓15 ROM定制、以及想搞懂系统应用管理和开机自启脚本原理的朋友。1. sys.boot_completed1 是开机链路里最重要的“时间锚点”1.1 这个属性是谁写入的写入时机在哪安卓系统有一个自己维护的属性服务property service。init进程最早启动然后拉起property_service创建一个全局的属性存储区。用户空间通过SystemProperties.get()读取命令行用getprop写入则通过setprop。属性按前缀分作用域ro.*是只读的persist.*是持久化的而sys.*属于系统运行时属性sys.boot_completed就在这一类里。它真正的“老家”在 SystemServer 里。系统服务启动过程中ActivityManagerService 会推进一个启动状态机从systemReady()开始到bootPhase的各个阶段最后走到finishBooting()/bootLocked()这一串逻辑里在PHASE_BOOT_COMPLETED广播之前执行类似SystemProperties.set(sys.boot_completed, 1)的调用。也就是说它是由 system_server 进程在用户空间启动晚期写入的一个标志位而不是哪个 init.rc 脚本里的静态配置。可以这样理解安卓开机像一场大型会议init是行政前台Zygote 是会议室AMS 是主持人而sys.boot_completed1就是主持人说的那句“到这儿各位可以自行安排后续工作”。在那之前框架层很多服务还在“入场”你贸然让脚本跑起来想管这个管那个大概率是要碰钉子的。1.2 它和 build.prop、ro 属性在定制ROM里的区别定制ROM时很多人有一个误解以为在 build.prop 里加一行sys.boot_completed1就能让系统“提前认为自己启动完毕”。这个操作实际没用。build.prop 是开机早期 init 加载进属性服务的静态文件里面常见的是ro.*这类需要在系统起来之前就确定的属性而sys.boot_completed是 SystemServer 启动后期动态写入的你在 build.prop 里写了要么被后续动态写入覆盖要么根本不会触发对应的逻辑。顺带解决一个老生常谈的混淆ROM 和 RAM。网上总有人问 ram 和 rom 的区别简单说 ROM 在刷机语境里指系统镜像RAM 是运行内存。定制 ROM 就是重新生成系统镜像刷进去而sys.boot_completed1是系统跑起来之后才产生的运行时状态跟你镜像里写了什么没有直接关系。我在整理一加全量包、掌讯车机刷机包这类第三方 ROM 时发现不少包里的 init.rc 都直接监听on property:sys.boot_completed1这个触发器可见它已经成了定制 ROM 内核级脚本的标配。理解了它的写入源你才知道为什么所有“开机后再干活”的逻辑都要等它。1.3 安卓15为什么更依赖这个时间锚点到了安卓15系统分区化更彻底服务模块化程度更高很多组件可以独立更新启动顺序也不再是严格的线性流程。sys.boot_completed1并不等于“所有服务百分百就绪”它更像一个分水岭在此之前PackageManagerService 可能还在扫描应用、用户存储还没解锁此时执行pm命令或操作应用数据很容易出错在此之后大多数用户态服务已经可用你至少可以安全地调pm、cmd或者读写/data下的文件。我在实测中见过最明显的例子在属性没置位前去执行pm disable-user有时候命令看似返回成功了重启后状态根本没生效或者把系统包管理器自己在启动阶段的恢复逻辑搞乱。而等sys.boot_completed1之后再执行基本就是“指哪打哪”。所以它才是我这套自启和管理脚本里真正的起点。2. 系统应用管理为什么不直接删包而是等 boot_completed 之后再下命令2.1 “直接删系统应用”看着爽坑却不少很多精简 ROM 的做法是直接把/system/app或/system/priv-app里的 APK 删掉开机后桌面确实干净了。但作为过来人我要提醒一句删包方案在 OTA 全量包和增量包更新时非常麻烦。增量包是拿着原始文件清单做差分校验的你少了一个文件升级时校验不通过整个升级就会中止全量包刷入后你手工删除的应用又会全部回来等于每次升级都要重新精简一遍。更隐蔽的问题在签名校验和框架依赖。部分“看起来独立”的系统应用其实在 framework 层隐式绑定了组件。你把应用删了但系统里还有 pending intent、provider 引用或动态广播指向它开机后轻则持续刷 crash 日志重则直接卡在启动动画。第三方 ROM 圈子里那些“精简过头”的包多数就是这么翻车的。所以在做系统应用管理时我现在的首选不是删文件而是等系统启动完成后用pm系列命令做“禁用/启用”操作。这样既不影响 ROM 镜像的完整性也不会破坏 OTA 校验出了问题恢复也快。2.2 pm 命令族和 sys.boot_completed1 的组合拳真正干活要用到pmPackageManager命令行工具常用操作如下命令作用适用场景pm list packages -s列出系统应用确认包名做基线清单pm list packages -d列出已禁用应用验证脚本是否生效pm disable-user --user 0 pkg仅禁用当前用户中的应用推荐误操作后恢复容易pm enable pkg重新启用应用恢复被误禁的包pm uninstall -k --user 0 pkg仅对当前用户卸载保留数据希望桌面彻底不显示时pm install-existing pkg恢复已移除用户的应用对应 uninstall 操作这里需要特别解释disable-user和disable的区别。pm disable是对全局禁用系统更新和状态机都可能受影响pm disable-user --user 0只对用户 0 这个主用户禁用应用包本身和组件状态还在恢复起来非常容易和 OTA 的兼容性也更好。定制 ROM 里我几乎都优先用disable-user。这些命令能不能执行成功前提就是 PackageManagerService 已经完成系统应用扫描。普通 shell 权限下等sys.boot_completed1之后再执行基本没有太大风险。这也是为什么所有脚本逻辑要先轮询属性再开始操作。2.3 应用管理的白名单逻辑别一把梭正因为“禁用”比“删除”安全也不代表可以乱来。我的做法是先做一份系统应用清单划分成三类。应用类型建议状态原因系统UI、桌面、设置、输入法必须保留缺失会黑屏或无法操作包安装器、网络栈、认证服务必须保留影响安装APK和联网能力第三方预装、广告组件、语音助手可禁用看具体 ROM 需求举个例子手机上禁用桌面Launcher会导致开机后只有一个壁纸甚至黑屏禁用系统 UI 会直接丢掉状态栏和导航栏。车载设备上更夸张某些 ROM 的系统应用跟方控、倒车影像、CAN 协议深度耦合禁错了可能连启动都进不去。所以在脚本里我会把核心包名放进一个“排除列表”循环禁用前先判断避免误伤。3. 自启脚本实例解析用属性等时机用命令管应用用日志留痕迹3.1 脚本载体怎么选service.sh、init.d、还是 on property 触发器系统应用管理命令有了脚本放在哪里、什么时候被执行又是一个决策点。常见载体有几种Magisk 的post-fs-data.sh时机太早在挂载 data 分区、Zygote 启动之前此时 PackageManagerService 根本不可用。Magisk 的service.sh处于 late_start 服务阶段通常仍在sys.boot_completed置位之前不能直接执行 pm 命令。/data/adb/service.d/*.sh同样是 Magisk 类环境下的 late_start 脚本时机偏早。ROM 内置/system/etc/init/*.rc可以通过on property:sys.boot_completed1精确触发但需要改 ROM 镜像适合编译期集成。普通sh脚本 轮询最通用任何 root 环境都能跑。所以最简单可靠的思路是脚本本身就位内部轮询sys.boot_completed1等到属性置位后再执行后续命令。这样无论载体什么时候拉起脚本最终实际干活的时间点都能对上系统状态。如果你在做 ROM 内置init.rc 里也可以写出这样一段on property:sys.boot_completed1 exec u:r:su:s0 root root -- /system/bin/sh /system/bin/boot_tasks.sh但这种写法会额外受 SELinux 策略限制并不比我下面这个纯 sh 脚本更省事。3.2 一个完整的自启脚本实例这份脚本是我在安卓15定制 ROM 里实际用过的框架去掉具体包名换成可复制的通用版#!/system/bin/sh # boot_tasks.sh # 用途基于 sys.boot_completed1 的系统应用管理 自启任务 # 要求root / shell 权限依赖 getprop、pm、cmd appops DATA_DIR/data/local/tmp/boot_control LOG_FILE$DATA_DIR/boot_tasks.log MARK_FILE$DATA_DIR/.boot_tasks_done # 先建目录保证日志可写 mkdir -p $DATA_DIR log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* $LOG_FILE } # 幂等判断已经执行过就不再跑避免重复禁用导致异常 if [ -f $MARK_FILE ] [ $1 ! --force ]; then exit 0 fi # 等待 boot_completed attempt0 while [ $(getprop sys.boot_completed) ! 1 ]; do attempt$((attempt 1)) if [ $attempt -ge 30 ]; then log watchdog: boot_completed not set in 150s, exit exit 1 fi sleep 5 done # 属性虽然是 1但部分服务仍在收尾再稳一手 sleep 3 log boot_completed detected, starting tasks # 核心保留名单谁都不能禁 EXCLUDE_LISTcom.android.systemui com.android.settings com.android.launcher3 com.android.packageinstaller com.android.inputmethod # 按需禁用的系统应用列表 DISABLE_LISTcom.example.bloat1 com.example.browser com.example.video com.example.voiceassist # 1. 先确保核心组件可用 for pkg in com.android.systemui com.android.launcher3; do pm enable $pkg /dev/null 21 done # 2. 禁用指定的系统应用注意排除保护名单 for pkg in $DISABLE_LIST; do if echo $EXCLUDE_LIST | grep -q $pkg; then log skip protected package: $pkg continue fi # 只对系统应用操作非系统包直接跳过 if pm list packages -s | grep -q ^package:$pkg$; then pm disable-user --user 0 $pkg /dev/null 21 log disabled system package: $pkg else log package not system, skip: $pkg fi done # 3. 设置应用后台运行策略 cmd appops set com.example.browser RUN_IN_BACKGROUND deny /dev/null 21 cmd appops set com.example.video RUN_IN_BACKGROUND deny /dev/null 21 log appops configured # 4. 拉起自己的后台守护脚本 if [ -f /data/local/tmp/my_daemon.sh ]; then nohup sh /data/local/tmp/my_daemon.sh /dev/null 21 log daemon started fi # 5. 全部完成写标记 touch $MARK_FILE log all tasks done这份脚本的逻辑分五块幂等判断、属性等待、核心组件确保、系统应用禁用、自启守护进程拉起。每一步都写日志方便事后通过cat /data/local/tmp/boot_control/boot_tasks.log回溯。3.3 脚本的可靠性细节幂等、权限、可复现有几个细节决定了这份脚本好不好用。幂等非常重要。开机自启脚本如果每次开机都强制pm disable-user一遍虽然不是大问题但会无谓增加启动耗时而且某些应用被反复 disable 后可能在 OTA 或系统恢复逻辑里留下脏状态。我用标记文件配合--force参数平时自动跑一次手动调试时加参数强制重跑。权限问题也容易踩。如果脚本放在/data/local/tmp虽然在很多 ROM 上 shell 能直接执行但部分 SELinux 策略会限制从该路径 exec 新进程导致“文件存在但 sh 报无法执行”。更稳的做法是放到/data/adb/service.d/或者直接编译进 ROM 的/system/bin下并配好上下文。调试阶段可以先在 root shell 里手动执行adb push boot_tasks.sh /data/local/tmp/boot_tasks.sh adb shell su -c sh /data/local/tmp/boot_tasks.sh --force adb shell cat /data/local/tmp/boot_control/boot_tasks.log还有一个很无语的坑Windows 下写的脚本带 CRLF 换行推到设备上执行会报“bad interpreter”或者语法错。我在项目里会先用sed -i s/\r$// boot_tasks.sh或直接用 VS Code 切 LF 再推。4. 安卓15 ROM定制实测包管理、权限边界与场景差异4.1 包管理命令的实测验证方法拿到一个 ROM 后我会先刷进测试机开启 root/adb然后按下面顺序摸清系统应用的家底adb shell pm list packages -s -f | sort这条命令会把所有系统应用以及对应的 APK 路径列出来。-f很关键因为它能让你把包名和/system/app、/system/priv-app下的文件对应上方便反查它在 ROM 里的位置。再执行adb shell pm list packages -d就能看到已经被禁用的应用列表。如果想彻底移除某个应用但保留数据包可以用adb shell pm uninstall -k --user 0 pkg之后万一想恢复adb shell pm install-existing pkg这里要提醒一句安卓15对包可见性和用户状态的校验更严格你在 shell 下执行这些命令时可能遇到SecurityException尤其是尝试操作另一个用户或 device owner 绑定的应用时。定制 ROM 里推荐先确认自己确实有 shell 或者 root 权限再操作出现权限异常优先查 SELinux 的 avc 拒绝日志而不是怀疑命令写错。4.2 手机、车机、机顶盒的定制差异同一套脚本在不同设备上要调整的地方不少。手机场景包括一加、各类安卓原生 ROM 全量包相对标准只要保住电话、短信、系统UI、桌面和设置基本不会出大问题。但第三方精简包还原问题很常见你禁用了某个包下次刷入官方全量包它又回来了脚本里的白名单需要持续维护。车机场景要格外小心。掌讯车机 SD8227 这类平台上的 ROM比如 1024x600 车速版界面很多系统应用跟方控、GPS、麦克风、CAN 总线深度绑定。我在一旁看别人刷这类包时发现一旦禁用了某个系统 UI 或语音组件轻则方向盘按键失灵重则倒车影像不显示。车载 ROM 的管理原则是只动明确的第三方预装和广告组件别碰任何跟“vehicle”“systemui”“launcher”相关的包。机顶盒和智能音箱同理。我曾处理过类似小度8C公开版的设备桌面、语音引擎、DRM 组件都不能动禁用后播放器可能直接起不来。这类设备的 ROM 合集里“救砖临时ROM”尤其要谨慎因为临时包往往不完整有的只负责把系统拉起来不保证所有硬件功能正常。我自己的建议是先备份当前完整系统再刷任何第三方包优先选用官方全量包作为基线临时 ROM 只用于救砖不用于日常配置脚本。4.3 属性权限与 SELinux 对脚本的约束最后必须强调的是sys.boot_completed是一个sys.*运行时属性正常情况只有 system_server 具备写入权限。很多新手脚本会写一句setprop sys.boot_completed 1想“骗过”等待逻辑这在有 root 的 Magisk 环境里可能能改但会引发连锁状态问题。你可能以为跳过了等待实际上系统服务仍在初始化后续 pm 命令照样失败反而把问题隐蔽化。正确的做法是永远只读它不写它把属性当成一个“信号灯”而不是“改选项”。SELinux 的约束也很实际。在普通 Android 上shell domain 执行/data/local/tmp下的脚本有时会触发 exec 权限拒绝。表现就是脚本文件存在、权限是 755但执行时报错或者没有任何输出。处理方式前面说了要么移到 Magisk 的 service.d要么在 ROM 内置时放到/system/bin并配置正确的 file_contexts。定制 ROM 的 userdebug 版本还能方便地临时关闭 SELinux 做对比测试但要记住正式刷机包不能带着setenforce 0这种状态交付。5. 我在实测里踩过的坑以及一套可复用的验证顺序5.1 高频问题排查清单现象根因处理方式日志里sys.boot_completed1已出现但pm disable-user仍提示找不到包或状态异常用户0的存储可能还没完全解锁或 PackageManager 仍在处理动态分区挂载多等几秒或同时检查sys.user.0.ce_available禁用某个包后设备卡在开机动画把桌面或 systemui 拉进了禁用列表进 recovery 或 fastboot用pm enable恢复桌面类包脚本里必须配排除名单脚本执行时报bad interpreter或语法错Windows CRLF 换行或中文注释编码问题统一转 LF脚本内避免非 ASCII 内容或用sed -i s/\r$//清洗/data/local/tmp下脚本无法执行SELinux exec 限制移到/data/adb/service.d或 ROM 内置/system/bin禁用状态在刷全量包后全部恢复禁用状态记录在/data的用户数据里重新刷机后丢失把脚本集成进 ROM 的启动流程开机自动重新执行cmd appops set返回错误包名写错或目标应用没有注册对应权限用cmd appops get pkg先查当前状态再调整这里最值得展开的是第一个坑。“boot_completed 不等于用户0完全解锁”这个现象在安卓10以后越来越常见。你看到属性已经是 1但/data/user/0目录还在解密过程中某些应用的包状态还没有完全恢复。所以脚本里我除了等sys.boot_completed1还建议加一个可选的检查getprop sys.user.0.ce_available等它变成 1 之后再操作应用状态会更稳。5.2 一套可复现的验证顺序踩过几次坑之后我形成了一套固定的测试流程强烈建议你也按这个顺序来。先在备用机上手动验证脚本逻辑不做任何 ROM 改动。推入脚本后手动加--force跑一遍打开日志看每一行的执行结果。这一步能滤掉 90% 的语法和命令错误。再用本机验证时序。不要一次做完整套操作脚本里先把所有禁用动作注释掉只保留日志和属性等待观察boot_completed的实际置位时间以及日志里那个 3 秒 “稳一手” 是否足够。如果设备性能差、启动慢就把 sleep 拉长到 5 秒或直接检查sys.user.0.ce_available。最后才集成进 ROM 全量包。把脚本放到系统路径配置好 SELinux 上下文刷机后做一次冷启动到桌面的完整流程再看日志确认每一步都执行成功。切记不要一上来就在主力机上验证救砖工具和临时 ROM 准备得再全都不如先在测试机上跑通一遍。5.3 自启脚本和“临时ROM/救砖包”的配合要点针对网上那些所谓临时 ROM、救砖刷机包我多说一句临时包因为完整度参差不齐sys.boot_completed的写入时机、属性权限甚至 init 脚本加载方式都可能和官方包有差异。比如有的精简临时包会跳过部分系统服务减少启动时间导致属性置位提前但 PackageManager 还没扫描完。这种环境下脚本的等待逻辑一定要留足冗余超时时间也不要设太短。我的习惯是如果脚本在某个第三方包上反复出现“等不到属性”或“pm 命令一直失败”先手动抓一次adb shell getprop | grep boot_completed和adb shell pm list packages | head对比属性置位和 pm 就绪的时间关系再决定调整等待条件还是放弃在这个包上启用自启管理。6. 最后分享一点个人实操心得刷了这么多 ROM、写了这么多开机脚本我最深的感觉是定制的核心不是“能做什么”而是“在正确的时间做什么”。sys.boot_completed1这个属性看起来简单但只要理解了它是系统状态机的分界点你的脚本就能从“碰运气”变成“有节奏”。实际操作中我一般会把脚本第一版设计成“只打日志不动系统”。先把时间点测准确认属性置位、pm 可用、用户0解锁这些状态都满足再逐步打开禁用和自启功能。宁可多开机测试两次也别在集成 ROM 里一把梭省下的时间往往是救砖时间的好几倍。如果你准备在安卓15 定制 ROM 里做系统应用管理和自启逻辑就把这份脚本当作骨架把包名、白名单、日志路径换成自己的。最后再提醒一句任何脚本在正式编译进 ROM 之前都先在测试机手动跑通这台测试机最好能模拟你目标设备的硬件配置。毕竟车载、机顶盒、电视、手机的系统应用依赖完全不一样一个包名的差异结果可能就是开机黑屏。
返回列表