9. 安装流程(下):dex2oat 优化、权限授予、安装完成广播的发送

上一章我们聊到 APK 的解析和签名校验,文件已经拷到了 data 分区。但这时候应用还不能跑——你想想看,Android 用的是 ART 运行时,它不认 DEX 字节码,得先转成本地机器码。这一步,就是 dex2oat 干的活。

9.1 dex2oat:从字节码到机器码

dex2oat 的全称是「Dalvik Executable to Optimized Art Tool」。说白了,它就是个编译器。把 APK 里的 classes.dex 文件,编译成 .oat 文件,也就是 ART 能直接执行的 ELF 格式。

我个人习惯把 dex2oat 分成三个阶段来看:

  1. 输入准备:读取 DEX 文件,加载到内存
  2. 编译优化:根据策略做 AOT 或 JIT 编译
  3. 输出落地:生成 .oat 和 .vdex 文件

这里有个关键参数叫 compiler-filter。它决定了编译到什么程度。我列个表,你一看就明白:

filter 值 含义 典型场景
verify 只做字节码验证 首次安装、低内存设备
quicken 验证 + 快速优化 部分系统应用
speed 全量 AOT 编译 预置应用、Play Store 下载
speed-profile 基于 profile 的热点编译 日常安装(默认)
everything 所有方法都编译 系统镜像构建

嗯,这里要注意。Android 7.0 之后引入了 混合编译 模式。不是一股脑全编译,而是先跑 JIT,收集热点方法,再触发 dex2oat 做 profile-guided 编译。这样既保证了首次安装快,又保证了长期运行性能。

核心流程:PackageManagerService 调用 Installer 的 dexopt 方法 → Installer 通过 socket 通知 installd → installd fork 出 dex2oat 进程 → dex2oat 执行编译 → 返回结果给 PMS。

我在项目中遇到过一个问题:某款手机安装大型游戏时,dex2oat 跑了整整两分钟,用户以为卡死了。后来发现是 compiler-filter 被设成了 speed,全量编译导致。改成 speed-profile 后,安装时间降到 15 秒,性能几乎没有损失。

9.2 权限授予:谁可以碰我的数据?

APK 装好了,代码也优化完了。接下来要处理权限。这一步很多人容易忽略——其实权限授予发生在 应用首次启动之前

权限分两类:

  • 安装时权限(Install Permissions):比如 INTERNETVIBRATE。系统直接授予,不需要用户确认。
  • 运行时权限(Runtime Permissions):比如 CAMERALOCATION。需要弹窗让用户同意。

在安装流程里,PMS 会做这么几件事:

  1. 读取 APK 的 AndroidManifest.xml,提取 <uses-permission> 标签
  2. 检查每个权限是否在 platform.xmlpermissions.xml 中定义
  3. 对于安装时权限,直接写入 packages.xml<perms> 节点
  4. 对于运行时权限,标记为 granted=false,等用户弹窗时再改

小技巧:如果你在开发系统应用,想跳过运行时权限弹窗,可以在 AndroidManifest.xml 里加 android:installPermission="android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS"。不过别乱用,这需要系统签名。

我曾经踩过一个坑:某个第三方应用声明了 READ_EXTERNAL_STORAGE,但它的 targetSdkVersion 是 28,按道理应该弹窗。结果用户反馈说没弹窗,直接就能读 SD 卡。查了半天,发现是厂商在 platform.xml 里把这个权限设成了 granted="true"。嗯,这种「隐式授权」在定制 ROM 里很常见,但不符合 CTS 规范。

9.3 安装完成广播:通知全世界

权限搞定了,最后一步是发广播。PMS 会发送 ACTION_PACKAGE_ADDEDACTION_PACKAGE_REPLACED,告诉所有注册了 BroadcastReceiver 的应用:有新兄弟来了。

广播发送的细节,我总结成三步:

  1. 构造 Intent:带上包名、uid、安装来源等 extra 信息
  2. 设置 flagFLAG_RECEIVER_REGISTERED_ONLYFLAG_RECEIVER_INCLUDE_BACKGROUND
  3. 调用 sendBroadcast:走 ActivityManagerService 的广播分发逻辑

这里有个容易忽略的点:广播是异步的。PMS 发完广播后,安装流程就返回了。但广播接收器可能还没执行。如果你在广播里做耗时操作(比如写数据库),记得用 goAsync() 或者开线程。

注意:Android 8.0 之后,隐式广播(没有指定包名)大部分被禁用了。如果你的应用监听 ACTION_PACKAGE_ADDED,必须用 registerReceiver 动态注册,不能在 manifest 里静态声明。除非你加了 intent-filterpriority 属性——但说实话,这招在 Android 14 上也不灵了。

我记得有一次调试一个 Launcher 应用,它需要监听新安装的应用来更新桌面图标。结果在 Android 12 上,广播死活收不到。后来发现是 Launcher 的 BroadcastReceiver 没有用 ContextCompat.registerReceiver 注册,漏了 RECEIVER_EXPORTED flag。加上就好了。

9.4 整体流程回顾

把这三步串起来,安装流程的后半段就是:

  1. dex2oat 把 DEX 编译成 OAT
  2. PMS 解析权限并写入配置文件
  3. 发送安装完成广播
  4. 返回安装结果给调用方(比如 PackageInstaller)

下面这张图,是我自己画的流程概览:

安装流程后半段:dex2oat → 权限 → 广播 阶段一:dex2oat 优化 PMS 调用 Installer.dexopt() installd fork dex2oat 进程,读取 DEX 文件 根据 compiler-filter 策略编译为 .oat / .vdex 阶段二:权限授予 解析 AndroidManifest.xml 中的 <uses-permission> 安装时权限直接写入 packages.xml 运行时权限标记为 grant=false,等待用户弹窗 阶段三:发送安装完成广播 构造 Intent(ACTION_PACKAGE_ADDED),携带包名、uid 通过 AMS 分发广播给所有注册的接收器 安装流程返回,广播异步执行

你看,整个流程环环相扣。dex2oat 是性能的基石,权限是安全的门槛,广播是生态的桥梁。任何一个环节出问题,轻则应用闪退,重则系统卡死。

我个人建议,如果你在定制 ROM 或者做系统优化,重点关注 dex2oat 的 compiler-filter 配置。很多厂商为了省空间,把 filter 设成 verify,结果应用跑起来卡成 PPT。反过来,如果全设成 everything,首次开机时间又太长。找到平衡点,才是真功夫。

避坑指南:我曾经在 Android 11 上遇到一个 bug——安装应用后广播没发出来。查 log 发现是 PMS 在写 packages.xml 时发生了 IO 异常,导致流程提前 return 了。所以,如果你发现广播收不到,先检查 packages.xml 的完整性。这个文件在 /data/system/ 下,可以用 cat 看看末尾有没有 </packages> 闭合标签。

好了,安装流程的下半段就讲到这里。你理解了 dex2oat 的编译策略、权限的分级授予、以及广播的异步特性,基本上就能应对大部分安装相关的问题了。


公众号:蓝海资料掘金营,微信deep3321