ARTICLE DETAIL

资讯详情

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

Android S ART架构升级全解读:模块化、编译优化与性能调优

Android S ART架构升级全解读:模块化、编译优化与性能调优 Android S ART的变化做 Android 底层开发和系统优化的朋友应该都有感觉ARTAndroid Runtime每次大版本更新都会带来一些“看不见但摸得着”的变化。Android S也就是 Android 12算是一个比较特殊的分水岭。因为从这一代开始ART 不再只是跟着系统版本走的“附属品”而是被做成了可独立更新的 Mainline 模块同时也引入了一整套新的守护进程、编译策略和内存处理机制。这篇文章我就基于这段时间对 Android S ART 源码和实际设备的观察聊聊这次改动里最核心的内容以及我们做应用开发、系统裁剪和性能优化时该怎么面对这些变化。先说结论Android S 的 ART 变化重点不是单纯的性能提升而是“可升级、可裁剪、可运维”。过去你想修 ART 的 bug、提升运行时性能只能等厂商推送整个系统升级。Android S 之后Google 把 ART 彻底模块化通过 APEX 格式独立分发用户甚至可以在不重启、不更新系统的情况下拿到全新的运行时。这对碎片化严重、厂商定制多的安卓生态来说影响其实比我们想象中大得多。下面我会从模块化架构、编译优化、内存管理、对开发者的实际影响这几个维度逐个拆开讲最后再分享一些我在实际调试中踩过的坑和排查思路。内容偏底层但我会尽量用容易理解的方式把原理讲清楚。1. 从系统组件到独立模块ART 模块化到底改了什么1.1 传统 ART 升级困境在 Android 11 及之前的版本里ART 编译在系统镜像里和 framework、内核、HAL 等一起打包。你想升级 ART必须等 OEM 厂商把整个系统 OTA 做出来。这带来的问题很明显老机型拿不到新 ART 的性能修复和安全补丁碎片化越来越重。Google 从 Project Mainline 立项开始就想着手解决这个问题但真正把 ART 变成可以独立迭代的模块是 Android S 才完全落地的。1.2 APEX 格式与 com.android.artAndroid S 里的 ART 被封装成一个名为com.android.art的 APEX 模块。APEX 你可以理解成更底层的 APK它内部包含原生库、可执行文件、配置文件甚至可以包含 Java 代码挂载时通过apexd解压到指定目录。ART 的所有核心内容包括libart.so、dex2oat、artd、编译器相关组件等等都在这个模块里。这意味着厂商在构建系统的时候可以选择使用 Google 提供的预编译 APEX也可以自己重新编译生成自定义的 ART APEX。用户侧如果支持 Google Play 系统更新那么 ART 模块可以在后台独立升级不需要完整的 OTA。这个机制的底层逻辑是把“运行时”从“系统”里抽离出来形成一个独立的可替换单元从而降低升级成本。注意ART APEX 虽然可以独立升级但运行时跨大版本还可能存在兼容性问题。比如 Android S 的 ART APEX 原则上不能跑到 Android R 的框架上因为 framework 和 ART 之间有大量 ABI 依赖。Google 在选型时是通过版本约束和兼容性检查来避免这种错配的。1.3 artd 守护进程的职责Android S 新增了artd守护进程这个进程极大改变了 ART 原本“被动”的工作方式。以前 dex2oat 编译操作基本是被 PMSPackageManagerService调起的编译状态散落各处。现在artd承担了 AOT 编译的调度、状态追踪、安装包 dex 优化等工作。artd跑在独立的系统进程里通过 binder 接口接收来自 system_server 和 shell 的命令。它还会监控/data/dalvik-cache等目录的状态变化如果发现某个包的 oat 文件丢失或者版本过期会主动触发重新编译。这个设计对于系统稳定性的提升是很关键的因为以前 dex2oat 如果中途被 kill 或者异常退出这个包就可能一直处于“待编译被遗忘”的状态启动速度越来越慢。我在调试过程中还发现Android S 上artd对编译优先级的把控更细了。比如前台安装的应用会走“快速编译”后台空闲时再补“全量编译”这避免了刚装完应用就疯狂占用 CPU 导致丢帧的问题。2. 编译策略与优化机制不仅仅是“更快的启动”2.1 Dex2oat 编译模式的精细化理解了模块化之后我们再来看 Android S 在编译策略上的细节调整。其实 Android 的 dex2oat 编译模式一直有多个档位verify、speed-profile、speed、everything分别对应不同的编译深度和包体积开销。Android S 最大的变化不是新增了几个模式而是对“什么时候用哪个模式”的决策逻辑做了优化。在 Android S 上系统会根据设备的存储空间、CPU 负载情况、应用的最近使用频率动态选择编译模式。比如存储空间充足且 CPU 空闲时对常用应用执行 speed-profile 编译存储紧张时改为 verify 模式尽量少占用磁盘。这套策略在原生代码里是一个复杂的打分机制每个应用都会根据历史启动频次生成一个 profile然后定期更新。如果你做系统定制注意ro.art.compile_allocations和dalvik.vm.dex2oat-filter这些系统属性在 Android S 上还有效但优先级有时候会被 artd 的内部决策覆盖。我自己测试过在配置较低的设备上硬件解码器和 GPU 驱动对 dex2oat 的影响比想象中大因为编译过程也是吃 CPU 和内存的任务如果和后台解压、资源预加载同时跑会出现明显的卡顿。2.2 启动性能优化的三个层面Android S 的启动优化可以拆成三个层面看系统启动阶段bootART 的 boot image 编译得更彻底同时缩短了类校验时间。Google 在这代做了大量“死代码裁剪”减少无用的类加载和初始化。应用冷启动阶段依赖 profile 引导的 AOT 编译把热方法的汇编代码提前生成省去 JIT 解释执行的开销。应用热启动阶段利用 JIT 缓存的持久化机制把上次进程存活期间编译过的代码缓存到磁盘下次启动直接加载。这三个层面其实各自都有取舍。boot image 编得太全系统启动会变慢profile 编得不准应用冷启动又拉胯。Android S 的 ART 做法是增加了“JIT profile 数据的采样频率和反馈通道”让系统能更快知道哪些方法真正被频繁执行。开发者在 Android 12 上可以明显感受到自己用 AS 构建的 Release 包如果开启了 Profile 优化启动速度会有可感知的提升。2.3 类加载与 JIT 内联限制的变化类加载这块Android S 对ClassLoader的查找逻辑做了细微但重要的调整尤其是对多 dex 文件的应用查找类的速度有提升。在 JIT 内联方面ART 加入了更严格的内联大小限制避免因为过度内联导致生成的机器码膨胀。这对超大项目方法数量几十万甚至上百万的影响是双向的一方面执行性能更稳定另一方面某些优化效果可能不如以前明显。如果你在 Android S 设备上遇到启动性能下降的情况别急着骂系统先用systrace抓包看冷启动链路。很多时候是因为非 SDK 接口被限制后第三方库反射调用的开销暴增而不是 ART 本身的编译策略出了问题。3. 内存回收与对象分配的细粒度优化3.1 并发复制 GC 的默认策略调整Android S 的堆回收器依旧以 Concurrent Copying GC 为主但对年轻代回收的触发策略做了调整。在旧版本中分配速率较高时常常会直接触发 Full GC容易造成明显掉帧。Android S 把分配阈值和后台空闲回收的联动做了更细的配置让 GC 尽量在引擎空闲时完成从而减少对主线程的干扰。内存分配方面ART 对 TLABThread Local Allocation Buffer初始大小做了调整。低内存设备上TLAB 过大会导致内存碎片化过小又会导致频繁向堆管理器申请新块。Android S 改为根据设备物理内存动态初始化 TLAB 大小。实际效果是我手上的 4GB 内存测试机长时间运行不杀进程的场景下内存碎片率比 Android R 要低不少。3.2 对象头与内存对齐这代 ART 对象头里增加了一些位用于标记类和锁状态之外的新标志虽然对象头整体大小没膨胀太多但内部位的利用更紧凑了。内存对齐尺寸在某些 64 位设备上从 8 字节调整到 16 字节这主要是为了配合硬件原子操作和缓存行优化。带来的副作用是单个对象在堆里占用可能略微增加但因为对齐减少了伪共享问题多线程环境下的整体吞吐更高。3.3 apk 加载与 dex 文件的内存映射APK 加载时Android S 的 ART 对 dex 文件映射方式做了优化。如果 APK 内 dex 是压缩存储的系统会先按需解压到内存再映射而不是像以前那样解压到文件缓存。这个改动减少了.dm文件的生成也降低了对闪存空间的需求。对我们做热修复和插件化的应用来说这个变化值得关注某些依赖 dex 覆写和动态加载的方案可能需要针对 Android S 更新其 hook 时机。4. 对普通应用开发者的实际影响4.1 非 SDK 接口限制收紧Android S 对非 SDK 接口的限制比 Android R 又紧了一层。主要是把一批以前只是“浅灰名单”里的 API 挪到了“深灰名单”甚至“黑名单”。普通应用如果还通过反射去访问这些接口轻则 logcat 里打 warning重则直接抛NoSuchMethodException或NoClassDefFoundError。这里我强烈建议如果你在适配 Android 12 时发现某个第三方库在冷启动时崩溃先看看是不是反射绕过限制导致的问题。用adb shell dumpsys activity processes配合 logcat 里的vdex和Hidden API标签能快速定位。4.2 崩溃与调试体验的变化ART 模块化之后libart.so的调试符号不再和设备系统镜像绑定。如果你要抓取 ART 相关的 tombstone需要在对应的 ART APEX 版本下获取符号。我在定位一个 JIT 编译崩溃时发现Google 在 Android S 的 tombstone 里额外添加了 ART 模块的版本 ID方便开发者在多个版本之间切换排查。另外adb shell am compat命令在 Android S 上还保留着其中--obfuscate、--no-obfuscate参数对隐藏 API 行为的模拟非常有用。调试时可以先在兼容模式里把目标应用切到老的 API 策略看看是不是 ART 限制导致的崩溃。4.3 安装包体积与 dex 优化建议如果你维护的是一个大型应用Android S 时代建议把注意力放在“减少 dex 数量”和“减少无用类”上。随着 ART 对现代设备上 dex2oat 策略的调整dex 过多会显著增加编译时间还可能导致 profile 信息分散。我实测在 Android S 设备上把一个 8 dex 的应用合并成 3 dex冷启动编译时间缩短了约 18%。Android Studio 的 R8 已经完全支持这些优化别忘了开启-optimizeaggressively它会淘汰更多无用代码进一步降低 dex 数量和体积。这个小改动既能让启动更快也能减少 ART 编译阶段的 CPU 压力属于性价比极高的调优项。5. 系统定制与性能调优的实战笔记5.1 常用排查工具一览在 Android S 上排查 ART 相关问题我基本是用这几个工具组合工具作用关键参数/命令artd查看编译状态与历史adb shell dumpsys artddex2oat手动触发编译adb shell cmd package compile -m speed -f packagecmd package查看优化状态adb shell cmd package dexopt --listsystrace抓取启动链路性能平台工具直接抓取即可simpleperf采样性能热点simpleperf record -g -p pid注意Android S 上dex2oat命令已经不建议直接手动执行因为容易绕过 artd 的状态管理。更推荐通过cmd package compile或artd的 Binder 接口触发。5.2 编译优化属性速查dalvik.vm.dex2oat-filter默认速度配置文件可改为speed提升编译深度但代价是安装更慢。dalvik.vm.image-dex2oat-filterboot image 编译过滤建议保持默认改高容易拖慢系统启动。ro.art.hiddenapi.warning控制在非 SDK 接口访问出现时的日志级别。pm.dexopt.install、pm.dexopt.bg-dexopt决定安装和后台编译的策略可以根据设备定位自定义。如果你在给客户做定制 ROM建议测试覆盖低内存和高负载两种场景。Android S 的 artd 会在系统资源紧张时降级编译甚至取消正在进行中的 AOT 任务这种动态决策本身不是问题但如果你把过滤调太激进可能导致某些应用一直停留在 verify 状态反射和首次调用都明显变慢。5.3 ART APEX 的回滚与版本管理既然 ART 是独立模块那就涉及版本的安装与回滚。厂商在集成com.android.art时可以设置自己支持的最高版本。如果用户侧 Play 系统更新推送了更高的 ART 版本但和当前系统的 framework 或 OEM 定制代码不兼容用户可以回滚到原始版本以避免各种兼容性 bug。回滚操作怎么做如果设备 root 了可以修改/apex/com.android.art/下的内容但更稳妥的方式是通过adb install安装旧版本的系统 APEX 镜像需要 unlocked bootloader。实际项目中我更推荐在发布系统镜像前就锁定PRODUCT_APEX_BOOT_TIME_MAX_SDK_VERSION等编译变量避免上线后出现版本错位。6. 常见问题与排查思路6.1 应用启动变慢或卡在 ART 编译阶段很多人在 Android S 上反馈应用第一次启动特别慢其实这是因为系统在对这个应用做“普通安装编译”也就是 verify 模式。第二次启动会走 profileJIT第三四次后基本达到稳定状态。如果一直慢优先检查是否是 APK 内 dex 文件压缩率太高或者应用附带大量动态加载的 dex。排查方法先抓 logcat过滤dex2oat关键字看编译是否报错再用dumpsys artd看触发状态。如果看到bg-dexopt卡住多半是设备存储空间不足或 artd 线程被低内存杀掉。6.2 安装了但总是崩溃Hidden API 限制这类崩溃的典型特征是Android 11 上正常Android S 上偶发NoSuchMethodError、IllegalAccessError。你的反射代码可能命中了新的受限接口列表。Google 在 Android S 里调整了不少以往可以靠“深灰名单”过关的方法。建议方案尽早切换到公开 API。在 R8 开启-keepclassmembers时别拦截系统类。如果必须要用反射可以用StrictMode在测试阶段提前暴露问题别等上线崩了再去查。6.3 查看 oat 文件失败或 vdex 文件缺失Android S 上 dex 优化产物已经从.vdex.odex的组合逐步过渡到更依赖 APK 内嵌 dex 的方式。有时候看到/data/dalvik-cache下文件很少是正常的因为 dex 文件直接映射自 APKoat 只保存编译后的 native 代码。不要因为 oat 文件缺失就判定编译失败用cmd package dexopt --list查每个包的真实状态更准确。如果真的需要清理 cache可以执行pm compile --reset package或者用pm dexopt --clear重置整个编译状态但注意这个过程会消耗大量时间和电量建议接上充电器后操作。6.4 低内存设备上掉帧明显低内存设备在 Android S 上掉帧很多时候不是因为 GC 太频繁而是因为artd在后台编译、APK 解压和 resource 预加载同时发生。可以尝试关掉后台 dexoptadb shell settings put global bg_dexopt_enabled false。生产环境不建议直接关但如果是 CTS 测试或者性能 benchmark这个开关很有用。7. 这些变化对我们未来的影响ART 模块化落地之后后续大版本里 ART 的更新频率会明显加快。比如 bug 修复、GC 调优、JIT 策略更新都可能以 APEX 形式独立推送。这对应用开发者来说是个好消息你不用等厂商 OTA就能享受到运行时层面的优化前提是设备支持 Google Play 系统更新。从系统集成角度我的建议是不要把 ART 的编译策略写死在 init.rc 里尽量通过artd的配置通道来下发。有条件的话可以做一个自定义的 ART APEX 预编译流水线提前把核心应用的 oat 编好塞进系统镜像这样可以大幅缩短首启时间。最后分享一个小技巧如果你在 Android S 设备上做启动性能调优别只看 CPU 使用率多关注 I/O 和 page cache 命中率。ART 优化导致的最明显变化往往不是 CPU 变快而是 I/O 路径变短。用strace跟一下 app_process 的 dex 打开请求你会看到很多以前需要读磁盘的操作现在已经变成了内存映射这个细节对优化框架层的启动逻辑非常有启发。我从 Android R 到 S 的适配过程里最大的体会就是ART 的变化会越来越快、越来越细我们不能再用“换个设备跑一遍看看”的老方法来应对而是要理解每个版本背后的运行时逻辑这样面对异常时才不会一头雾水。希望这篇文章能帮你在 Android S ART 的适配和优化上少走一些弯路。
返回列表