本期作者
肖玲通
哔哩哔哩高级开发工程师
从事 B 站 Android CI/CD 开发,当前聚焦在Android 编译构建方面。
01 背景
在今年年初时,我们发现在 Android Studio 的 Sync(同步)阶段结束后会有一段漫长的等待时间,下图是我们 2021年下半年的同步耗时,大概是在 10 min。
最终我们定位到是同步过程中进行 Jetifier 操作,这个操作主要是将 Support 依赖替换为了 AndroidX 依赖,无论项目是否包含 support 依赖,只要开启了 enableJetifier = true 都会进行转换操作。其实 Android 团队在近期最新的 Chipmuck patch 1 的版本上也已推出了 Jetifier 开启的警告 (https://developer.android.com/studio/releases/gradle-plugin?buildsystem=ndk-build#jetifier-build-analyzer),更早如 gradle-doctor (https://runningcode.github.io/gradle-doctor/) 也已推出针对 Jetifier 开启的警告。
Jetifier was enabled which means your builds are slower by 4-20%.
最终我们将端内的 Support 依赖全部替换为了 AndroidX 依赖,最终降低了我们同步耗时 2’30s。
增量构建耗时降低了 28%
迁移前
迁移后
02 问题分析
从 Gradle 的生命周期开始分析,Gradle 分为三个阶段,初始化,配置,执行三个阶段。
• 初始化阶段会分配 Gradle 实例,执行 init.gradle 脚本,并生成 Setting 对象,确定哪些模块会参到构建流程中
• 配置阶段会访问所有参与构建模块的 build.gradle,从而得到此次任务执行的 DAG(有向无环图)
• 执行阶段会根据配置阶段确定的 DAG,依次执行 TaskGraph 上的 task
我们在分析 sync 过程时断点 gradle.startParameter.taskNames 发现其实是没有 Task 执行的,Sync 过程更类似于执行一个空 task,不过带上了一些奇怪的参数,用命令行来粗略模拟的话, 是这样的,注意这个命令只是粗略模拟,因为这里并没有返回 model 给到 IDE 进行项目索引,而 IDE 也没有提供外部调用命令。
./gradlew -P android.injected.build.model.only.advanced=true -P android.injected.build.model.only=true -P android.injected.build.model.disable.src.download=true -P org.gradle.kotlin.dsl.provider.cid=465291211918416 -P kotlin.mpp.enableIntransitiveMetadataConfiguration=true -P android.injected.invoked.from.ide=true -P android.injected.build.model.only.versioned=3 -P android.injected.studio.version=2022.2.1 -P idea.gradle.do.not.build.tasks=false -D idea.active=true -D idea.version=2022.2 -D java.awt.headless=true -D idea.sync.active=true -D org.gradle.internal.GradleProjectBuilderOptions=omit_all_tasks --init-script /private/var/folders/0c/zkqf7nrj6pl_d40jp4m9h3xm0000gn/T/sync.studio.tooling11.gradle --init-script /private/var/folders/0c/zkqf7nrj6pl_d40jp4m9h3xm0000gn/T/ijmapper.gradle --init-script /private/var/folders/0c/zkqf7nrj6pl_d40jp4m9h3xm0000gn/T/ijinit.gradle
确定了同步阶段没有 task 执行的阶段,我们聚焦分析了配置阶段的过程。在我们反复观察日志输出下,发现在同步结束后会有后置输出,JetifyTransform 的字眼在其中会频繁出现。
> Transform jetified-openDefault-10.10.0.aar (project :weibo) with JetifyTransform
WARNING: [XmlResourcesTransformer] No mapping for: android/support/FILE_PROVIDER_PATHS
WARNING: [XmlResourcesTransformer] No mapping for: android/support/FILE_PROVIDER_PATHS
> Transform jetified-openDefault-10.10.0.aar (project :weibo) with JetifyTransform
WARNING: [XmlResourcesTransformer] No mapping for: android/support/FILE_PROVIDER_PATHS
WARNING: [XmlResourcesTransformer] No mapping for: android/support/FILE_PROVIDER_PATHS
> Transform jetified-openDefault-10.10.0.aar (project :weibo) with JetifyTransform
WARNING: [XmlResourcesTransformer] No mapping for: android/support/FILE_PROVIDER_PATHS
WARNING: [XmlResourcesTransformer] No mapping for: android/support/FILE_PROVIDER_PATHS
推测是 Jetifier 操作导致在同步阶段进行了产物转换,设置 android.enableJetifier=false 再次同步 JetifyTransform 后置输出消失。至此已定位到根因是由于 Jetifier 操作拉长了整体的同步耗时。
Artifact Transform
Jetifier 是通过 Gradle 的 Artifact transform (https://docs.gradle.org/current/userguide/artifact_transforms.html#implementing_incremental_artifact_transforms) 转换实现, Artifact transform 做的主要是依赖产物的转换,不同于 AGP 对中间产物局部的 Transform , Gradle 全局的产物转换可以保证同步后可以使用到转换过的产物, 可以在一次 transform 过程中同时处理 class, layout, manifest 中的 support 依赖,而这在 AGP 的 transform 中是无法实现的。
@CacheableTransform
abstract class JetifyTransform : TransformAction
{ }
Jetifiy Transform 过程输入是 aar/jar,逐个扫描 aar 内文件,进行相应文件类型变换后写入文件,针对 Class 和 Xml 文件的变换可分为:
• Class 文件;根据 mapping 进行 ASM 操作字节码变换
• Xml 文件; 通过正则匹配解析 xml 文件,根据 mapping 进行字符串变化
Transform 结束后输出需要将解压出来的文件再次压缩为会 jeitfier-xxx.aar/jar, 从而供下游的 transform 消费。
04 问题解决
我们知道问题是由 Jetifier 导致的,那么我们只要针对同步阶段,在运行前改掉内存中 android.enableJetifier 中的值理论上就可以做到 hook AGP 读取结果。我和我的同事开始尝试在运行期修改 gradle 的配置。
动态修改 gradle 配置
首先我们尝试添加 android.enableJetifier=false 和 android.useAndroidX=false 参数到 gradle.startParameter.projectProperties 或者 gradle.startParameter.systemPropertiesArgs 中去,这两个配置是 gradle 的全局配置参数。但发现修改没有生效,仍旧输出了 JetifierTransform log。
接下来我们尝试修改了 settings.ext 中 android.enableJetifier=false 的值为 true,修改仍旧未能生效。
通过查阅源码发现这个值是 AGP 通过 BasePlugin#ProjectServices 进行读取的,BasePlugin 是 com.android.library 和 com.android.application 插件的公共基类。那只要我们成功 Hook 这里的读取的值,就可以关闭 Jetifier transform.
// com.android.build.gradle.internal.DependencyConfigurator#configureDependencyChecks
fun configureDependencyChecks(): DependencyConfigurator {
val useAndroidX = projectServices.projectOptions.get(BooleanOption.USE_ANDROID_X)
val enableJetifier = projectServices.projectOptions.get(BooleanOption.ENABLE_JETIFIER)
}
上面读取 ENABLE_JETIFIER 的时机有些靠后,在 Project#afterEvaluate 回调中执行。我们选择在 Project BasePlaugin apply 后尝试进行hook,代码如下:
class TurnOffJetifierPlugin : Plugin
{ override fun apply(project: Project) {
project.plugins.withType(BasePlugin::class.java) {
val service = it.getProjectService() ?: return@withType
val service = it.getProjectService() ?: return@withType
val projectOptions = service.projectOptions
val projectOptionsReflect = Reflect.on(projectOptions)
val optionValueReflect = Reflect.onClass(
"com.android.build.gradle.options.ProjectOptions$OptionValue",
projectOptions.javaClass.classLoader
)
val defaultProvider = DefaultProvider() { false }
val optionValueObj = optionValueReflect.create(projectOptions, BooleanOption.ENABLE_JETIFIER).get
() Reflect.on(optionValueObj)
.set("valueForUseAtConfiguration", defaultProvider)
.set("valueForUseAtExecution", defaultProvider)
val map = getNewMap(projectOptionsReflect, optionValueObj)
projectOptionsReflect.set("booleanOptionValues", map)
}
}
private fun BasePlugin?.getProjectService() =
Reflect.on(this)
.field("projectServices")
.get
() }
在 demo 测试发现是可行的,然后我们准备尝试在所有模块中去应用这个插件:
// 根项目应用插件
allProjects {
apply plugin: TurnOffJetifierPlugin.class
}
// Include builded project 应用插件
settings.gradle.startParameter.initScripts = [initFile]
# initFile
allprojects { Project project ->
apply plugin: TurnOffJetifierPlugin.class
}
我们发现接入在项目内在根项目是成功修改了值的,但是对于 Include builded 的仓库没能修改成功,在上一次《哔哩哔哩 Android 编译优化》一文有提及我们仓库是通过 Composite builds 组织在一起,触发 include build 子仓的初始化是通过 Init Script 实现的,Init Script 中的 apply plugin 是时机也是在 afterEvaluate 中,修改值晚于了读取时机,修改还是失败了。
思索
我们思考即使找到了合适时机修改了值,这样的黑魔法依赖于 AGP 对应实现,增加了维护成本,而且关闭同步阶段的 Jetifier 操作,并没有解决编译打包阶段的 transform,治标不治本。我们真正的诉求是移除所有 support 包,最终彻底关闭所有阶段的 Jetifier 操作。于是我们决定迁移剩余的 support 依赖。
Support 依赖迁移
得益于 MonoRepo 架构,项目内的源代码已经替换 Support 依赖为 androidX 依赖了。通过移除 Configuration 阶段中的 support 依赖,开启 A8检查,从而定位到仓库内剩余的 support 依赖。关于什么是 A8 检查,可参见后续防劣化章节。
遗留的代码包含公司内部游离在大仓外的少数二方库,以及外部的一些三方库。针对这些仓库,我们采取了下面三种迁移措施:
公司内部的二方库
对于公司内部的仓库,通知到代码库的 owner 进行迁移,操作为 AS → Refactor → Migrate to AndroidX 迁移,再手动发布到远端 maven 上,主仓更新二方库版本号。
能获取到源码的三方库
clone 三方库源码后, 如果对方直接支持了 AndroidX 而且 Change Log 针对 API 没有 breaking changes,直接发布三方库升级后的版本;其他的情况我们选择在同一版本号三方库下,通过上述 AS 的自动化迁移操作后发布到公司 nexus 仓库中。最后更新主仓版本号。
不能获取到源码(只有对方的 aar 及 POM)
我们封装了一些常用的 CLI 工具作为 babeltools, 内部集成了Jetifier CLI (https://developer.android.com/studio/command-line/jetifier), 通过执行下面命令可以将含有 support 的 aar/jar 转换为 androidX 的 aar/jar
bJetifier -i [input.aar] -o [output.aar]
此外如果有 POM 文件需要手动修改对应 POM 中的 support 依赖为 androidX. 相应依赖转换可参考 CSV (https://developer.android.com/topic/libraries/support-library/downloads/androidx-class-mapping.csv) 表。
迁移过程遇到的一些坑
包大小变化
在迁移 support 包结束后通过 Android Studio 对比前后包大小,在 32 位上缩小了 6KB, 64 位缩小了 600KB,理论上迁移后相同的 commit 点差异不会超过 1KB.
通过 diffuse (https://github.com/JakeWharton/diffuse) 工具对比前后 apk 差异, 发现包大小的主要影响点来自于两方面:
) 工具对比前后 apk 差异, 发现包大小的主要影响点来自于两方面:
• R.class
• so 大小差异
R.class 的减少主要来自于公司内部的一个二方库,升级为 andoirdX 后 R 文件传递导致,解决方案是开启该模块下的非传递 R。
so 大小差异来自于我们发布 fresco 使用的 ndk 版本和官方发布时使用的 ndk 版本不一致导致,我们通过将之前 AAR 中的 so 覆盖了打包生成的 so,从而沿用了之前的 so,降低了包大小差异
修复了上述问题后,我们再次比较同一 commit 点,前后包大小差异缩小至 1KB 内。
nexus 上传
针对部分三方库只有 jar 和 aar 的情况,比如 com.tencent.tauth:qqopensdk 及 com.huawei.android.hms:security-base 需要手动上传到 nexus,手动上传页面 UI 是长这样的:
上传一个带后缀如 -bili4 的 aar 到 nexus 上时,他会自动补上后缀作为 classifier,这会导致 com.facebook.fresco:imagepipeline:1.13.0-bili4 无法正确匹配, 其对应的依赖是 com.facebook.fresco:imagepipeline:1.13.0:bili4
此外对于 com.github.bmelnychuk:atv:1.2.9 包含 POM 的依赖,手动上传时需要连同 POM 一块上传,POM 描述了模块的依赖信息,丢失了 POM 文件,会因为依赖缺失导致编译报错。
AndroidX 日志检查
在我们关闭 jetifier 操作后,Jetify Transform 的输出日志消失了,但同步的后置阶段仍有大量 androidX 的检查日志,如下所示,仅展示单条数据。
WARNING:Your project has set
android.useAndroidX=<span class="code-snippet__literal" style="box-sizing: border-box;">true</span>
, but jetified-openDefault-10.10.0.aar still contains legacy support libraries, which may cause runtime issues.This behavior will not be allowed in Android Gradle plugin 8.0.
Please use only AndroidX dependencies or set
android.enableJetifier=<span class="code-snippet__literal" style="box-sizing: border-box;">true</span>
in thegradle.properties
file to migrate your project to AndroidX (see https://developer.android.com/jetpack/androidx/migrate for more info).
但通过 jadx 解包发现,该 aar 已经不包含 support 依赖。这个检查并不准确
查阅源码定位到 AGP 针对 enableJetifier 关闭, useAndroidX 开启的情况下, 会给开发者开启 androidX 的检查,检查耗时与工程体量成正比。
class AndroidXEnabledJetifierDisabled(
private val project: Project,
private val configurationName: String,
private val issueReporter: IssueReporter
) : Action
{
private val issueReported =
"${AndroidXEnabledJetifierDisabled::class.java.name}_issue_reported"
override fun execute(resolvableDependencies: ResolvableDependencies) {
// Report only once
if (project.extensions.extraProperties.has(issueReported)) {
return
}
}
}
源码中为防止重复检查加入了开关,我们在 gradle.properties 文件补上如下参数即可关闭 androidX 检查。
com.android.build.gradle.internal.dependency.AndroidXDependencyCheck$AndroidXEnabledJetifierDisabled_issue_reported=true
后续防劣化保证
为了保证后续业务方接入三方 SDK 通过透传依赖方式再次带入 support 依赖,我们在配置阶段会把所有 support 所有依赖剔除,这样可以保证打出的 APK 里一定不再包含 support 依赖,可是如果 APK 里运行时使用到了这部分依赖,APP 在运行时是会崩溃的,我们的解决办法是通过前文《哔哩哔哩 Android 编译优化》中提到的A8 检查,在 pipeline 合码期间检查出缺失的依赖从而阻塞合入,从而防止了后续引入新的 support 依赖。剔除 support 依赖代码如下:
allprojects {
configurations.all { Configuration c ->
if (c.state == Configuration.State.UNRESOLVED) {
exclude group: 'com.android.support'
exclude group: 'android.arch.core'
exclude group: 'android.arch.lifecycle'
exclude group: 'android.arch.persistence.room'
exclude group: 'android.arch.persistence'
exclude group: 'com.squareup.leakcanary', module: "leakcanary-object-watcher-android-support-fragments"
}
}
}
05 结语
源于18年5月的 Hello World, AndroidX (https://android-developers.googleblog.com/2018/05/hello-world-androidx.html), AndroidX 开始进入了 Android 开发的视野中,随着在 28.0 的support 包版本(https://developer.android.com/topic/libraries/support-library/revisions?hl=zh-cn#28-0-0), Support 包停止了维护,所有更新仅在 AndroirdX 上迭代,在近期的 Migrate to AndroidX (https://developer.android.com/jetpack/androidx/migrate) 博文警告:
Caution: As of late 2021, most of the library ecosystem already supports AndroidX natively. This means that your project is most likely already using AndroidX libraries directly and there is no need to follow the steps in this migration guide. Additionally, the enableJetifier (https://developer.android.com/jetpack/androidx/migrate#migrate_an_existing_project_using_android_studio) flag mentioned in this guide can lead to slower build times and should not be used unless it’s necessary.
If your project already has the enableJetifier flag and it’s turned on, you can run Build Analyzer’s Jetifier check to confirm if it’s actually needed. The Build Analyzer check is available starting in Android Studio Chipmunk (https://developer.android.com/studio/preview/features#jetifier-build-analyzer).
如果你的项目中还有少量 Support 的传递依赖,对其进行迁移我认为是有必要的。保持工具链整体是更新迭代,是获取编译性能提升的投入产出比较高的方式。
本文转载自主站产研 哔哩哔哩技术,原文链接:https://mp.weixin.qq.com/s/EExWHagW8f1s2hDIjYmjKg。