为什么需要 Baseline Profiles ?
应用启动运行时解释执行(JIT)代码,并将热点代码翻译为机器代码,此过程需要时间,因此会降低性能,那 Baseline Profiles 是做什么的呢?就是将原本运行时解释执行的热点代码提前准备好,在应用处于空闲时,可以直接将热点代码编译成机器代码,这时在下次运行时可被直接执行,省去了运行时解释代码的过程,为应用启动提升性能。
如下是官方使用 Baseline Profiles 在应用启动上提升的百分比,来自 《 Performance best practices for Jetpack Compose》:
官方提供的数据非常吸引人,但具体落地到项目中是否能和 Google 提供的数据一样还需要自己用 Macrobenchmark 来自测。
Baseline Profiles 流程图
Baseline Profiles 从编写、编译到安装的整体流程图
本流程图更专注于 Baseline Profiles 在开发层面的执行过程,像官方罗列的 Cloud 部分,本文不阐述。接下来,我们来讲述这三个部分。
1、编写时
在官方文档中有介绍如何通过 Macrobenchmark 来获取自己项目的 baselie-profile.txt ,但需要注意的是,该方式需要准备一台 Android 9 及其以上 root 过的手机。既然是文本文件的话,那是不是我们自己也可以手动去写?答案是的,但不是很建议,因为项目代码一直在变,手动录入的话会导致维护困难,官方有列出 baseline-profile 的格式:
具体的规则可以查看官方的 Rule syntax 章节,接下来,我们需要探索下 Compose 项目中,这个文件是放置在哪的。
稍微改了下 checkPlugin 插件,只打印 aar 中有含有 baseline-prof.txt 文件的依赖:
list.forEach { path -> ... while (zipInputStream.nextEntry.also { ze = it } != null) { if (ze!!.isDirectory) { continue } // 打印 aar 里面含 baseline-prof.txt 的依赖 if(ze!!.name == "baseline-prof.txt"){ println(path+" --> name="+ze!!.name) } } zipInputStream.closeEntry() input.close() } 复制代码
打印结果如下:
从结果上看,Compose 相关的依赖基本都含有一份自己的 baseline-profile.txt 文件,我们看下 compose.ui 的 baseline-profile.txt,看到了熟悉的 AndroidComposeView:
baseline-profile.txt 在模块目录中的结构如下,与 AndroidManifest.xml 同级:
2、编译时
在编译阶段,AGP 会将所有的依赖的 baseline-profile.txt 合并成一个文件,然后编译输出 baseline.prof 文件
从 AGP 7.0 源码来看,最主要的两个 task 为 CompileArtProfileTask 和 MergeArtProfileTask,我们这对这两个 task 简单的跟踪下:
这个地方可以注意一下这个判断,如果不想启用 ArtProfile task 的话,可以设置 android.enableArtProfiles 为 false,或者 deuggable 设置成 true。
MergeArtProfileTask
获取所有模块的 baseline-profile.txt 文件:
然后将所有模块的 baseline-profile.txt 内容进行合并:
合并很简单,就是将所有的文件内容汇总写入到 outputFile 里,我们来看下这个最终输出的文件:
CompileArtProfileTask:
编译解析合并后的 baseline-profile.txt 中的规则,具体可以看该 Task 下的 HumanReadableProfile 类,将提取出来的规则与 DexFile 比对,找出匹配的访问标识、 profile method 和 profile class 存储到 profileData 中, 最终用 ArtProfile 包裹起来 save 到 baseline.prof 中 ,这个地方的写入是有格式的(例如魔数),具体可以看 ArtProfileSerializer,下面贴个图:
所以,如果 baseline-profile.txt 文件描述的类和方法与实际的 class 不一致的话,则不会参与最终的 profile 优化,因为会被剔除掉,这个需要注意。
baseline.prof 的产物如下:
最终打包的时候,会将该文件添加到 assets/dexopt 目录下参与打包,打包效果:
如何检查自己的 AGP 是否支持 Baseline Profiles 的打包呢(前提是打非 debuggable 的包)?
- 检查 gradle task 的输出,是否有如上两个 task,例如 app:mergeReleaseArtProfile 和 :app:compileReleaseArtProfile
这个地方需要注意,在我之前的文章中有介绍 AGP 4.2.x 版本是支持正式版 Compose 的,但在看 4.2.x 版本源码的时候,是没有 ART Profiles 相关的 task 的,这也说明,在 AGP 4.2.x 生成不了 baseline.prof 文件,继而享受不到 Baseline Profile 带来的优化。 不过也有解决办法,那就是在高版本的 AGP 中打包,然后将 apk 中的 assets 里的 baseline.prof 文件提取出来,放入到自己项目即可。
3、运行时
项目运行时,会通过 profileinstaller 模块将 assets/dexopt/baseline.prof 文件写入到 /data/misc/profiles/cur/0/包名/primary.prof
下,profileinstaller 模块在哪引入的呢?我们来打印下依赖树:
profileinstaller 依赖被 compose.ui 模块给带进去了,并且 profileinstaller 还把 startup 库也给带进去了,来看下最终 apk 包的清单文件:
应用启动时,通过 startup 库将 ProfileInstallerInitializer 启动起来,我们可以简单看下 ProfileInstallerInitializer 类:
- Android 7.0 以下不支持,直接返回不处理
- 为了避免因为写入 baseline.prof 影响到应用的启动,固注册在第一帧之后再延迟 5s 左右执行写入操作
看下写入的操作:
- 判断是否强制写入或是已经写入过,强制写入默认是 false,如果已经写入则不处理
- transcodeAndWrite 在子线程中开始执行写入操作
profileinstaller 的源码非常简单,总共才 10 个类,但这里面需要注意一些细节:
- 在 transcodeAndWrite 方法内有判断
/data/misc/profiles/cur/0/包名
路径是否可写,如果各厂商把这块设置成不可写的话,则无法写入 - profileinstaller 是在应用安装完成第一次启动的时候会做写入操作,在打开应用尚未写入完成时,这个时候是无法享受 AOT 带来的优化,所以,这次启动数据会有一定的劣化,不过,只有第一次安装打开时才会,尚可忽略
衡量 Baseline Profiles 带来的提升
我们需要测量 Compose 项目有无 Baseline Profiles 加持时性能的对比,默认我们的 compose 项目就有了 Profiles 加持,我们需要移除 Profiles 能力来测试启动性能,有两种办法可以解决:
1、从 baseline.prof 入手
我们只需要解决不将 baseline.prof 文件打入 apk 即可,或是说即使打入进去了,不将 profileinstaller 依赖打进 apk 也可以,这样的话,在运行期间就不会将 prof 文件写入到本地。 最简单的方式就是 gradle.properties 中配置 ArtProfiles 为 false:
android.enableArtProfiles=false 复制代码
该值对应到上文编译章节开头描述的
variant.service.projectOptions[BooleanOption.ENABLE_ART_PROFILES]
判断,该值默认是个 true,我们设置 false 即可不启用 ArtProfile task,在产出包之后,通过 adb 命令即可获得启动数据:
adb shell am start -W 包名/启动类
2、MacroBenchmark 测试
上面的测试可能会比较麻烦,可以利用 MacroBenchmark 来自动化测试有无 Baseline Profiles 加持的启动数据,单元测试如下:
测量结果:
测试 10 组数据,中位数的值比没有 Profiles 加持快 30ms 左右,
这里就贴一个样本吧,因为在多次的测试过程中,大部分都是有 Profiles 加持的情况下比没有的快,但也遇到一次奇葩的时候:
测试 10 组数据,中位数的值比没有 Profiles 加持慢了 70ms 左右,
这让我对 MacroBenchmark 保持了怀疑的态度,后面有时间,等我用 adb 的方式测一下吧。