Android ART:那位让 App 飞起来的超级翻译官
Bug #7788一台 Android 7 的骁龙 660 测试机。第一局前 40 秒掉帧开镜有黏滞感加载多花 3 秒。第二局丝滑一切正常。测试同学把它报成偶现卡顿扔进了低优先级列表。但它不是 bug。那是你的 APK 在第一次运行时正在现场学习怎么翻译自己。而这个翻译官就是 Android Runtime——ART。序幕一个关于翻译的寓言你的 App 是一本用dex字节码写成的原文书。CPU 只懂机器码。所以这本书必须被翻译。问题只有一个什么时候翻Dalvik 时代的做法Android 4.4 之前请一位同声传译。App 一边跑这位翻译一边逐句口译优点拿到书立刻开讲安装极快包体极小缺点慢。同一段代码跑一万次他就翻一万次补救Android 2.2 加了JIT即时编译——发现某几句翻来覆去地说就记下来编译成机器码放进缓存。这叫热路径Hot Path编译ART 的做法Android 5.0 起成为默认提前把整本书翻译好。安装时用dex2oat把dex全部编译成机器码写进OAT 文件本质是一个 ELF 文件可以 mmap 直接执行。APK 里的 classes.dex ↓ 安装时或空闲时dex2oat /data/app/.../oat/arm64/base.oat ← 编译好的机器码 /data/app/.../oat/arm64/base.vdex ← 校验过的原始 dex /data/app/.../oat/arm64/base.art ← 预初始化的对象镜像Image收益是实打实的App 启动、方法调用、电量全面优于 Dalvik。代价同样实打实安装变慢、占用空间变大。于是 ART 的核心故事就是一场持续十年的什么时候翻、翻多少的博弈。第一幕三挡变速的翻译官ART 不是全 AOT它是一台三挡变速箱。理解它就能解释你遇到的每一种卡顿。挡位机制速度何时启用① 解释器逐条执行 dex⚠️ 慢冷门代码、未编译方法、反优化Deopt后② JIT运行时编译热方法中 → 快频繁调用的方法③ AOTOAT已编译的机器码最快profile 命中的方法关键机制一JIT 会攒简历JIT 在跑的时候会把哪些方法被反复调用记进一个Profile 文件/data/misc/profiles/cur/0/包名/primary.prof这份 profile就是这位翻译官的工作笔记——它记录了读者最常翻的是哪几十页。关键机制二用户睡觉时他偷偷加班Android 7.0Nougat引入了混合编译Hybrid Compilation首次安装 → 只做 verify几乎不编译→ 安装飞快 运行时 → 解释器 JIT 起步默默累积 profile 设备空闲 → JobScheduler 触发后台 dexopt 按 profile 只编译【真正用到的那部分】这就是为什么第二局就丝滑了——第一次对局期间profile 写完了后台 dexopt 跑完第二次进游戏热路径全是 AOT 机器码。关键机制三编译器滤镜Compiler Filter决定翻多少滤镜翻多少用在哪verify只校验不编译极速安装speed-profile只翻 profile 命中的Android 8 默认甜点speed尽量全翻追求极致启动everything全部含冷代码罕见记住这一条从 Android 8 开始默认就是speed-profile。也就是说没有 profile你的代码就不会被编译。这直接引出了第三幕最重要的工具。关键机制四Art 也会反悔——反优化DeoptimizationAOT 编译时假设 A 类只有一个子类运行时真出现了第二个子类已编译的代码就失效了。ART 会原地退回解释器重新走 JIT。这在游戏里有个经典表现某个模块比如新上线的活动 SDK第一次被触发时帧率突然塌一下——不是它慢是它在从 AOT 退回解释器。第二幕预翻译的四笔账AOT 不是免费的。它把成本从运行时搬到了安装时和磁盘上。账目Dalvik纯 JITART纯 AOTART 混合Android 7安装时间极快很慢大包可到 30~60 秒快只 verify磁盘占用小大OAT 可超 dex 数倍中按 profile 编译首次启动慢边跑边翻快慢 →首局有 JIT 预热稳态性能中最快快更新代价无每次升级重编译后台增量 dexopt安装时间这笔账对游戏是致命的一个 2GB 的手游如果安装时要dex2oat全量编译——用户在应用商店页面看着进度条等 40 秒然后卸载。这就是 Google 在 Android 7 转向混合编译的真实动机它不是技术更先进而是安装体验不能被牺牲。另一个隐藏的账DEX 变多 编译变多。Google Play 的 dexopt 分档 install-time 仅 verify快用户无感 bg-dexopt 空闲时按 profile 编译用户无感最常用 cmdline 开发者手动adb第三幕Profile 革命——把大家都翻哪几页送给每个新读者这是过去五年 Android 性能优化最重要的一件事也是游戏团队最容易忽略的一环。Baseline Profile基线档案—— 随包附赠的常用页清单问题新用户安装完第一次打开profile 是空的。所以启动路径全是解释器 JIT。首启动必然慢。解法把启动路径该编译哪些方法提前写进 APK。// 模块的 baseline-prof.txt人类可读的热方法列表 HSPLcom/example/game/MainActivity;-onCreate(Landroid/os/Bundle;)V HSPLcom/example/game/SdkManager;-init()V Lcom/example/game/net/PacketDecoder;构建时由profgen编译成二进制打包进APK/assets/dexopt/baseline.prof Android 9首次启动时由androidx.profileinstaller.ProfileInstaller安装触发一次按 profile 的 AOT 编译。收益Google 官方与大量实测的量级冷启动减少 15%~30%有时更多——而且与写入 profile 的覆盖率直接成正比。怎么生成这份清单两步不要手写① 手动跑一遍关键路径冷启动 → 主界面 → 首次对局 adb shell cmd package compile -m speed -f 包名 # 先全量编译 → 在设备上正常操作让 profile 累积 → adb shell profgen 或使用 Macrobenchmark 采集 ② 用 Jetpack Macrobenchmark 自动采集 Test fun collectBaselineProfile() { baselineProfileRule.collect(packageName com.example.game) { startActivityAndWait() // 走完启动路径 } }Cloud Profile云档案—— 汇集几百万读者的翻页习惯Google Play 会匿名汇总所有用户的 profile对头部 App 生成云 profile在安装时分发。对游戏的意义如果你的玩家量足够大冷启动路径几乎自动被优化——但这需要量大 上架 Play国内渠道享受不到。第四幕射击游戏里的六个战场战场一冷启动 3.2 秒 —— 主线程上的 SDK 坟场现象am start -W测得冷启动 3.2 秒而onCreate里只写了 20 行代码。真相那 20 行里藏着 6 个 SDK 的init()——崩溃上报、广告、统计分析、归因、推送、热更。全是 Java 层全在主线程全是 IO。注意一个关键事实如果你的游戏是Unity IL2CPP或 UE——游戏逻辑早就编译成 native 机器码了ART 根本不碰它。所以对这类游戏ART 影响的不是游戏本身而是那层薄薄的 Java 壳。而这层壳恰恰是冷启动的全部。优化四件套// ① 延迟初始化SDK 初始化丢到后台线程首帧不等它newThread(()-Analytics.init()).start();// ② 合并排队Android 12 用 App Startup 库统一初始化少开线程// ③ 关键路径别碰磁盘读配置走 SharedPreferences 的 apply()不是 commit()// ④ ★ 加 Baseline Profile把 onCreate 到首帧的方法全写进去实测组合收益步骤冷启动基线3.2 s SDK 异步化2.4 s Baseline Profile1.6 s 首帧渲染不等 SDK占位图1.1 s验证工具adb shell am start -W -n 包名/Activity或Jetpack Macrobenchmark的StartupTimingMetric。必须用 Release 包 真机测Debug 包的 JIT 行为完全不同。战场二首局前 40 秒掉帧 —— JIT 编译尖刺回到序幕。根因是 Android 7 的混合编译首局在跑 JIT。JIT 的代价不是平均慢而是尖刺第 N 帧 16ms ← 正常 第 N1 帧 48ms ← JIT 在编译一个热方法停帧 第 N2 帧 16ms ← 恢复玩家感受到的是每隔几秒顿一下。四种解法按推荐度方案说明代价① 触发后台 dexopt进游戏时用JobScheduler触发一次 idle 编译需用户设备空闲有延迟② 引导式预热首启动后在登录/更新/资源下载页停留 30 秒期间在后台线程反复调用关键方法把 profile 喂饱需要设计这段停留③ 用speed滤镜全量编译后台执行cmd package compile -m speed -f 包名⚠️ 需特殊权限仅自家渠道可用④ Baseline Profile 覆盖对局路径最正规且覆盖首局需要采集②号方案的实现要点这是很多国内团队的实际做法// 在资源更新页面后台线程预热热路径newThread(()-{for(inti0;i3000;i){PacketDecoder.decode(dummyBuffer);// 反复调用让 JIT 编译它EventBus.dispatch(dummyEvent);// ★ 一定要真的调用纯反射不行}}).start();⚠️注意预热必须调用真实代码路径。用反射Method.invoke去调用不但不产生有效的 profile 命中反射本身还会被 JIT 单独优化得到的编译结果不一样。战场三GC 尖刺 —— Java 层的隐形杀手现象对局中每约 30 秒掉一次帧帧时间从 16ms 跳到 40ms。根因GC垃圾回收。好消息是ART 的 GC 比 Dalvik 强很多——Android 8.0 引入了分代并发复制 GCGenerational Concurrent Copying GCGoogle 当时公布的停顿时间改善是数量级的从几十毫秒降到个位毫秒。但个位毫秒在 60fps 的 16.6ms 预算里仍然是半帧。最常见的元凶Java 层的热路径// ❌ 每帧都在制造垃圾voidonUpdate(){Vector3posnewVector3(...);// 每帧 newStringtextHP: hp;// 字符串拼接JSONObjectobjparser.parse(packet);// 网络包每帧解析}// ✅ 复用 原始类型 二进制协议privatefinalfloat[]posBufnewfloat[3];// 复用privatefinalByteBufferpktByteBuffer.allocateDirect(4096);// 堆外不参与 GC三个原则热路径零分配——每帧 new 的 Java 对象就是给 GC 递刀子网络协议用二进制ByteBuffer/ protobuf-lite别用 JSON——JSON 解析是分配工厂ByteBuffer.allocateDirect堆外内存不参与 Java GC代价是分配慢所以要池化复用。观测工具adb shell dumpsys meminfo包名# 堆大小 / GC 次数# Perfetto 里看 Heap profile 和 ART 的 GC 事件轨道战场四JNI 边界 —— 那条看不见的收费站Unity 游戏的日常Java 层每帧把触摸事件、传感器数据、网络包递给 native 层。// ❌ 每帧 N 次 JNI 往返for(inti0;itouches.length;i){nativeOnTouch(touches[i].x,touches[i].y);// 每次都是一次跨界}JNI 的成本一次调用大约几十到几百纳秒——单看不贵。但它有安全检查 栈切换且不能被内联如果每次调用还要FindClass/GetMethodID那才是灾难这两个是按字符串查找微秒级。两条铁律// ① 批量传递不要逐条跨界nativeOnTouchBatch(float[]xs,float[]ys,intcount);// 一次调用一整批// ② 缓存一切反射产物静态初始化一次staticjclass gCls;staticjmethodID gOnFrame;JNIEXPORTvoidJNICALLJNI_OnLoad(JavaVM*vm,void*){gCls(jclass)env-NewGlobalRef(env-FindClass(com/game/Bridge));gOnFrameenv-GetStaticMethodID(gCls,onFrame,(F)V);}关键认知ART 编译得再好也优化不了 JNI 边界。那条线是架构决定的不是编译决定的。战场五热更新的困境 —— ART 在收窄那扇门Java 层的插件化/热更把新 dex 动态加载进来曾经很流行。但 ART 一直在关这扇门。版本变化Android 5–7反射PathClassLoader加载外部 dex可行Android 8限制加载可写目录的 dex → 必须拷到应用私有只读目录或用InMemoryDexClassLoaderAndroid 14动态代码加载DCL限制所有动态加载的代码必须是只读文件更要命的是性能动态加载进来的 dex没有 OAT 编译全部以解释器 JIT执行。结果热更上来的那段代码比包内代码慢一个档次。所以成熟做法是把热更范围限制在非热路径——活动页、文案、UI 逻辑绝不要热更战斗核心逻辑。游戏业的正确姿势游戏逻辑的热更应该走引擎自己的资源/脚本方案Lua、YooAsset、AssetBundle而不是在 Java 层做 dex 手术。战场六内嵌 H5 活动页 —— WebView 是 ART 的重度受益者场景游戏内嵌活动页、排行榜、社区。WebView 是 Java 层最重的组件——Chromium 内核 JNI 大量 Java 胶水代码。Baseline Profile 在这里的收益特别夸张因为WebView 初始化路径长且高度固定每次都走同一条路一旦命中 profile前 100ms 的解释器执行被整段消灭。做法把WebViewActivity的创建路径、WebView.loadUrl的调用链全部采集进 baseline profile。实测活动页首屏从 1.8s →1.1s。终幕Checklist必做收益最大成本最低接入 Baseline Profile覆盖冷启动 首次进对局 WebView用 Macrobenchmark 自动采集用ProfileInstaller确保安装CI 里加回归baseline profile 必须随包发布冷启动主线程上不留任何 IO / SDK 阻塞热路径零 Java 对象分配复用 ByteBuffer 二进制协议JNI批量传递缓存所有反射产物架构IL2CPP/UE 游戏把 ART 优化预算集中在薄薄的 Java 壳上热更范围限于非热路径核心战斗逻辑不碰 dex首局预热在加载页后台线程真调用热路径方法测量不做测量的优化都是猜am start -W/Macrobenchmark测冷启动Release 真机Perfetto 看art::gc与art::jit轨道抓尖刺dumpsys package dexopt看编译状态statusspeed-profile✅ /statusverify⚠️意味着它在解释执行分机型档位测试低端机的 JIT 预热时间可能是旗舰的 3 倍回到那台骁龙 660。它没有坏游戏也没有 bug。它只是第一天上班——那位翻译官正在一边听一边记笔记还没把整本常用词表背下来。而你要做的不是催他是提前把词表塞给他一份 Baseline Profile几十 KB写进 APK就能让一千万个新玩家在你的游戏里第一局就打出手感。这就是 ART 最迷人的地方它把翻译这件事从运行时的一道固定税变成了一个可以被设计、被采集、被提前交付的工程问题。而理解了什么时候翻、翻多少、翻哪些——你才算真正拿到了 Android 性能优化的钥匙。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →