资讯详情

资讯详情

Android S ART 核心更新解析:编译策略、Baseline Profiles 与兼容性排查

前阵子把项目升到 targetSdk 31顺手在一台 Android SAndroid 12的测试机上跑冷启动对比结果让我愣了好一会同一份安装包Android S 上的冷启动时间比 Android R 平均快了不少。我一开始以为是手机性能差异直到把 logcat 里 ART 的日志翻出来才意识到这一代 ART 的更新远比升级日志里那句“运行时优化”要复杂。ART 是整个 Android 的运行时核心负责把 dex 字节码编译成机器码、管理对象生命周期和内存回收Android S 这版里ART 不是小修小补从系统组件形态、编译策略到 GC 行为动了非常多地方。这篇文章就把我梳理到的主干变化和踩到的坑写出来给正在做 Android S 适配的开发者参考。1. 把ART变成可独立更新的Mainline模块一切都从这开始Android S 最容易被忽略的变化是 ART 被正式纳入了 Mainline 模块体系打包成com.android.art这个 APEX 文件。和以前只能跟着系统镜像一起发布的 ART 不同现在 ART 可以通过 Google Play 系统更新或者厂商自己的推送通道独立升级不用等整个 OTA。这背后的意图很明显运行时层面的 bug 和安全问题不能再拖到下一次大版本升级才修复而是要像普通应用一样能够被快速推下去。1.1 从“改不动”到“可交付”在 Android 10 之前ART 直接编译进 system image想要换一个 ART 版本基本等于要等一次完整的系统升级。厂商定制系统还会再拖几个月用户拿到手时往往还是老运行时。Android S 版本里ART 被真正作为一个可交付的 Mainline 模块在维护而不是一个永远静态躺在系统目录里的组件。开发者视角的影响也在悄悄变化即使设备停留在 Android 12 的某个安全补丁版本ART 的运行时行为也可能在后台变化。以前“系统版本相同运行时就相同”的假设从 Android S 开始就已经不成立了。adb shell pm list packages --apex-only | grep art adb shell dumpsys package com.android.art | grep versionCode上面这条命令可以看到设备上真实的 ART 模块版本。实测时我发现两台同为 Android 12 的设备ART 版本号可以差出好几个小版本崩溃行为和性能表现也有差别。所以现在做兼容性测试不能只记录“Android 12”还要把 ART 的版本记进去否则灰度期出的问题很难还原。1.2 APEX 更新对应用兼容性的影响APEX 本质上是一个可以被系统挂载的系统级包更新时不需要重刷整个系统分区。以前你碰到的很多“旧设备上好好的新版本突然崩了”的情况有很大一部分是 ART 行为变了。现在这个变更点更加隐蔽用户某天在后台收到一个系统组件更新你的应用第二天就出问题而且从 Crash 堆栈看完全是 ART 内部的东西。与其等到线上反馈再排查不如在发布流程里加一个“ART 版本检查”把出问题的设备按运行时版本归类很多诡异 Bug 的定位速度会快非常多。2. 编译策略变化JIT、AOT与Baseline Profiles的新分工这节是整篇文章的重头戏。理解 Android S 的 ART 变化绕不开编译模型。ART 采用混合编译解释器负责启动阶段的快速响应JIT 在运行中收集热点方法并生成机器码AOT 通过 dex2oat 在安装阶段或空闲阶段把字节码直接编成 native 代码。Android S 在这个模型里做的最关键调整是让“安装时按默认方式编译”这件事变得更聪明了。2.1 先回顾ART的混合编译模型老派的做法是安装时直接全量 AOT把所有代码都编译成机器码。这样做的优点是运行速度快但安装时间很长ROM 空间占用大而且很多代码用户根本不会执行到白白浪费算力和存储。后来改成默认只做 verify配合 JIT 和 profile 来引导后续的 AOT 编译。大概流程是应用第一次启动时先用解释器跑JIT 慢慢把热点方法编译出来同时记录哪些类和方法被高频调用。等设备空闲了ART 会根据这份 profile 再做一次针对性 AOT把启动路径上的热点方法真正编成机器码。2.2 编译过滤器compile filter的作用dex2oat 通过编译过滤器来控制编译深度常见的有verify、quicken、speed-profile、speed、everything。简单理解过滤器行为安装耗时verify只做字节码校验不编译低quicken做一些轻量优化预热部分指令中speed-profile按 profile 编译热点方法中高speed编译全部方法高everything所有代码都 AOT最高Android 11 以前厂商为了不拖慢安装速度默认对多数应用只做verify剩下的编译工作交给后台空闲任务。Android S 则更进一步如果你的 APK 里带了 Baseline ProfileART 安装时就会直接按speed-profile的级别去编译启动路径上的方法不需要等用户用几天才积累出 profile。2.3 Baseline Profiles 为什么值得每个应用上Baseline Profile 是这套机制里最值得开发者投入的一个点。它的思路是与其等用户在设备上运行应用来收集热点方法不如在开发阶段就把启动路径上的热点方法显式列出来打包进 APK。ART 在安装时拿到的这份“开卷答案”直接决定哪些方法先被 AOT 编译掉。接入方式不复杂Android Gradle Plugin 里启用androidx.baselineprofile插件工具会在基准设备上自动跑一遍启动场景生成baseline-prof.txt然后打进 APK。之后在应用里接入ProfileInstaller让安装后的第一次运行就能把这份 profile 交付给 ART。实测下来配合 R8 full mode 和启动优化冷启动收益在 200ms 级别完全可以复现。对大多数应用来说这是 Android S 上回报最高的性能优化之一。3. 启动、安装与首次运行的速度账Android S 这一版在“启动”这件事上的改进不只是一句“系统更流畅”那么玄学。它把安装阶段的编译任务分摊到了一个更合理的时间轴上安装时能少干就少干空闲时再补编译用户真正启动时再给足提升。3.1 安装阶段的“减负”如果应用没有带 Baseline ProfileAndroid S 在安装时对很多应用只做verify不做完整 AOT。以前那种刚装完打开慢吞吞用几天后逐渐变流畅的现象很大程度上就是 profile 慢慢收集和后台 dexopt 带来的。现在 ART 会把 profile 收集和后台编译的调度做得更激进用户不用等太久就能享受到“被编译过”的启动速度。代价是应用在低内存设备上可能有轻微的资源竞争但总体收益是正面的。3.2 如何确认某个应用当前编译状态排查性能问题前先确认应用到底被编译到了什么程度否则优化方向容易跑偏。adb shell dumpsys package dexopt --check package-name adb shell cmd package compile -m speed -f package-name adb shell am force-stop package-name adb shell am start -W package-name/.MainActivityam start -W输出的TotalTime是应用冷启动时间连续测三次取中位数比肉眼点图标靠谱得多。如果你想模拟带 Baseline Profile 的编译效果可以用cmd package compile -m speed-profile手动触发一次 profile 级别的编译再对比启动耗时。注意这套操作会改变设备上的 dexopt 状态测完最好用cmd package compile --reset恢复。3.3 一个性能对比的理性预期不是说升到 Android S 所有应用启动都会变快。如果你的应用本来就没做启动链路优化类太多、Application 里初始化任务太重ART 能做的只是把 Java/Kotlin 代码更快地变成机器码但业务逻辑该跑多久还是多久。把 Baseline Profile 加上、R8 优化开全、启动器里非必要的初始化都懒加载掉才能肉眼可见地感受到这版 ART 的编译策略优势。4. GC、内存与hidden API运行时层面的暗改ART 的 GC 一直是运行时里最容易被感知、却最难被定位的部分。Android S 这一版没有推出新的回收器但它在现有收集器上的调整对应用峰值内存和卡顿都有实际影响。4.1 GC行为调整并发复制 GCConcurrent Copying Collection在 Android S 里的标记和堆管理逻辑做得更细了体感是低内存设备上 GC 日志出现的频率变高但单次 GC 暂停时间普遍变短。很多开发者看到Background concurrent copying GC freed ...就觉得是内存泄漏其实这行日志只是表明堆内存紧张GC 在后台主动回收。真正要关注的是频繁的Alloc停顿和GC_FOR_ALLOC那才是用户能感知到的卡顿来源。优化方向不要变减少启动阶段的对象分配、避免在onDraw和循环里创建临时对象、谨慎使用自动装箱。ART 的 GC 无论怎么调都不可能把“高分配率”造成的性能损失完全吃掉。4.2 hidden API限制更严格ART 是 hidden API 限制的执行层Android S 对hide接口的反射调用处罚比之前更严格。很多以前只是打警告、还能继续跑的调用升级后在 Android S 设备上直接抛NoSuchMethodException或SecurityException。这类问题在第三方 SDK 里尤其常见比如一些老的埋点库、热修框架、性能监控库内部还在反射ActivityThread、ApplicationInfo等隐藏结构。排查方法很直接在 logcat 里搜Accessing hidden method把所有相关堆栈抓全逐个去查有没有公开 API 可以替代。如果实在绕不过去就需要评估是否放弃这部分功能而不是继续依赖反射越权。Android S 开始运行时对这类行为的容忍度只会越来越低。4.3 JNI与Native层的变化ART 模块独立更新后/system/lib64/libart.so不再是一个稳定的 ABI。任何直接链接到 libart.so 的 Native 代码都应该视为高风险因为一次 Play 系统更新就可能替换整个 ART 模块内部符号和实现细节都会变化。如果必须做 JNI尽量只使用 JNI 标准接口不要绕过 Java 层直接操作 ART 内部对象结构否则下一次用户收到系统组件更新你的 Native 层就可能直接 crash。5. 踩坑实录一次由ART升级引发的崩溃排查这部分分享一个我们真实处理的案例核心教训就是Android S 上 ART 的校验行为变严格了以前能“蒙混过关”的字节码现在会被直接拒之门外。5.1 现象灰度期间一批 Android 12 设备上报同一个崩溃堆栈长这样java.lang.VerifyError: Verifier rejected class com.example.a.Foo: void com.example.a.Foo.parse(java.lang.String) failed to verify: ... at java.lang.Class.getDeclaredMethods(Native Method) ...诡异的是同样的 APK 在 Android R 及以下设备上运行正常也没有改动过相关代码为什么升级系统就崩了5.2 排查链路第一步我先确认这跟 dexopt 状态有没有关系。执行adb shell cmd package compile -m verify -f com.example.app adb shell am force-stop com.example.app adb shell am start -W com.example.app/.MainActivity结果还是崩说明不是单纯的“某个方法漏编译”问题而是 ART 在校验阶段就认为这个类非法。第二步把新旧 APK 里Foo类的 dex 字节码 dump 出来对比。使用dexdump或者直接反编译发现Foo.parse里有一个极长的字符串 switch而 R8 在优化这段代码时产生了一个理论上“不可达”的分支。旧版 ART 对不可达代码的校验比较宽容检查到异常控制流时只是警告Android S 的 ART 校验器把这种情况升级成了硬错误直接在验证阶段拒绝整个类。第三步查构建工具版本。项目用的是 AGP 3.6 配 R8 1.5这在 Android 12 上已经太老了。后面升级到 AGP 8.x重新构建后问题消失同时顺手加了一条 R8 keep 规则防止Foo被进一步混淆触发同类问题。5.3 把这次教训沉淀成兼容性检查项这个 Case 让我把 ART 相关风险点整理成了一张检查清单每次发版前都会过一遍检查项方法AGP/R8 是否过旧升级到 AGP 8.x 或至少 7.4是否依赖 hidden API检查第三方库是否已适配 targetSdk 31是否直接链接 libart.so改为 JNI 标准接口是否使用大量默认接口方法用 R8 full mode避免 keep 规则误删元数据是否出现过 VerifyError快速抓 logcat 里art:W的关键字现在的做法是Android 兼容性测试机里固定放一台 Android S 设备、一台低内存设备跑完 UI 自动测试后再人工检查一遍 dexopt 状态确保关键类都能通过 ART 的验证。这轮之后线上再也没有出现过因为 ART 升级导致的突发批量崩溃。6. 日常排查ART问题的三件套最后把我在日常开发里最常用的三个排查手段整理出来不涉及复杂工具纯命令行就能完成适合放进自己的工具箱。6.1 抓 ART 日志ART 的日志不是每次都很呱噪需要主动打开 verbose 级别才能看到更多细节。平时我一般这样adb logcat -s art:W adb logcat -s art:D遇到 VerifyError 或者 ClassNotFound 相关的诡异问题搜Verifier rejected、Dex checksum、Failed to register这几个关键词基本能快速定位到是哪个类在校验阶段出了问题而不是一头扎进业务堆栈里猜。6.2 用 dexopt 状态做快速判断当你怀疑是“编译状态不对”导致行为差异时重启 dexopt 往往比反复重装 APK 更有效。adb shell cmd package compile --reset -f com.example.app adb shell cmd package compile -m speed-profile -f com.example.app这个操作能强迫 ART 重新走一遍完整的 dexopt 流程。如果重置后问题消失基本可以断定问题与编译状态或 profile 有关如果重置后问题依然存在那就不是 dex2oat 的锅而是业务代码本身的兼容性问题。6.3 记录设备 ART 版本Android S 开始同一个安卓版本在不同设备上的 ART 可能完全不同。我的建议是在测试设备清单里加一列ART versionCode崩溃上报平台上也把build.version.sdk之外再加一个art.version字段。这样遇到“用户设备 A 崩、设备 B 不崩”的情况先按 ART 版本分组再去看 Runtime 层面的行为差异排查速度能快上不少。把 ART 当成会变的东西而不是永远默认的环境。Android S 开始用户在后台接到的系统更新随时可能改变运行时行为。上线前至少在一台低内存设备、一台主流中端机和一台旗舰机上各跑一遍兼容性测试把 ART 版本号记录进测试报告。做到这三件事之后这类由 ART 触发的崩溃基本可以在灰度期就被拦截而不是等线上反馈了再手忙脚乱地复现。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →