干货 | 掌控 Android 编译利器,携程火车票AAR 编译速度优化实践
一、 背景
gradlew clean在每个build之间执行干净编译,确保剖析完整的构建流程; gradlew--profile--offline--rerun-tasks assembleFlavorDebug为某个变种task启用性能剖析,并确保gradle使用的是缓存依赖项,且强制gradle重新运行所有任务; 构建完成后,会生成html格式的性能剖析报告,可用于分析构建信息和前后构建对比;
对现有 Gradle 脚本按功能职责拆分成单个Gradle文件,需要使用的地方通过apply关键字应用即可; 基于原插件com.android.application和com.android.library建立两套基准Gradle文件,统一控制不同类型的模块; 抽取共有基础依赖项作为独立的 Gradle 文件,在subprojects中让所有模块都应用这些依赖项; 整理libs目录中本地的aar文件,移动至一个目录,统一依赖本地aar; 建立app_bundle.json文件记录所有模块项目的信息及类型,便于模块项目是源码还是aar的切换控制; 开发一套模拟参数机制,支持合并环境输入参数和local.properties参数 以便于 Gradle 功能的控制;如开启某个transform,指定哪些模块为aar等功能; 重构 flavor 变体功能,动态控制 flavor 的创建。
在所有子模块发布 maven 成功后,本地编译使用aar模块且环境参数没有传递 versionPath 参数时,构建脚本会自动拉取Mcd新的versionPath.json链接作为默认的模块aar信息表。 开发阶段需切换源码/aar,可对项目中所有模块进行分类,分别为:入口模块,源码模块,aar发布模块,可移除模块。另外,合并 gradle 环境参数和local.properties参数共同作为控制参数,然后修改app_bundle.json指定模块类型以达到灵活切换的目的。例如,定义一个sourceProject参数指定哪些模块作为源码构建。 为模块之间的依赖关系建立一个json文件aar_config.json,便于查看和修改模块间依赖关系。
//此处采用伪代码形式,正式场景佳实践应抽取到plugin中并提供extension和log功能
//通过app_bundles.json信息决定是否应用替换规则
gradle.ext.appBundles.each { bundleName, bundleInfo ->
if (bundleInfo.buildType == 'aar') {
gradle.ext.subsitituteMap[":$bundleName"] = "$specialGroupID:$bundleName:${bundleInfo.versionName}"
}
}
//制定自定义解析规则,用模块替换项目依赖项
configurations.all { conf ->
resolutionStrategy.dependencySubstitution {
gradle.ext.subsitituteMap.each {
substitute project(it.key) with module(it.value)
}
}
}
//使用maven-publish插件发布aar
apply plugin: 'maven-publish'
...
publishing{
...
//自定义pom文件处理传递依赖
pom {
packaging = "aar"
withXml {
pomGenerate(it)
}
}
}
//获取配置的依赖 configuration => dependenciesMap
def ascribeDependencies(String confName) {
def dependencySet = project.configurations.getByName(confName).getAllDependencies()
def dependenciesMap = [:]
//获取module类型依赖,待写入pom文件
dependencySet.withType(ExternalModuleDependency)
.findAll {
if (it.group != null && it.name != null && it.version != null) {
return it
}
return null
}
.each {
...
dependenciesMap.put("${it.group}:${it.name}", it)
}
return dependenciesMap
}
//拼接pom文件
def pomGenerate(XmlProvider xp) {
def dependenciesMap = [:]
dependenciesMap.putAll(ascribeDependencies("implementation"))
dependenciesMap.putAll(ascribeDependencies("api"))
...
// 添加节点的闭包
def addDependencyNode = { rootNode, entry ->
...
Dependency target = entry.value
//写入基本依赖信息
def node = rootNode.appendNode('dependency')
node.appendNode('groupId', target.group)
node.appendNode('artifactId', target.name)
node.appendNode('version', target.version)
node.appendNode('scope', 'compile')
//如果有,写入classifier
def artifacts = target.getArtifacts()
if (!artifacts.isEmpty()) {
DependencyArtifact artifact = artifacts.first()
artifact.type && node.appendNode("type", artifact.type)
artifact.classifier && node.appendNode("classifier", artifact.classifier)
}
//如果有,写入exclude规则
Set<ExcludeRule> excludes = target.getExcludeRules()
if (!excludes.isEmpty()) {
def exclusions = node.appendNode("exclusions")
excludes.each { rule ->
def exclusion = exclusions.appendNode("exclusion")
rule.group && exclusion.appendNode("groupId", rule.group)
rule.module && exclusion.appendNode("artifactId", rule.module)
}
}
}
//添加dependencies节点
def dependenciesNode = xp.asNode().appendNode('dependencies')
dependenciesMap.each {
addDependencyNode(dependenciesNode, it)
}
}
setting入口处合并参数
//加载local.properties属性
gradle.ext.localProperties = gradle.ext.loadProperties("$rootDir/local.properties")
//因为gradle版本兼容问题,startParameter.projectProperties不能注入直接注入,
//类型为com.google.common.collect.ImmutableMap,需要统一。
def inputProjectProperties = new HashMap()
if (startParameter.projectProperties != null) {
for (key in startParameter.projectProperties.keySet()) {
def value = startParameter.projectProperties.get(key)
if (value != null) {
println "注入ProjectProperty >> $key :${utf(value)}"
inputProjectProperties.put(key, value)
}
}
}
//local property 参数注入,优先级高于环境参数
if (gradle.ext.localProperties) {
gradle.ext.localProperties.each { k, v ->
println "注入LocalProperty >> $k :${utf(v)}"
inputProjectProperties.put(k, utf(v))
}
}
入口处更新全模块信息app_bundle.json指定模块打包类型
//解析初始模块信息app_bundles.json
gradle.ext.appBundles = gradle.ext.getConfigJson("$rootDir/app_bundles.json")
//利用输入参数更新模块信息
gradle.ext.appBundles.each { bundleName, bundleInfo ->
...
// App混合编译打包
if (gradle.ext.sourceProjects.contains(bundleName)) {
//强制指定源码编译的模块
bundleInfo.buildType = 'lib'
bundleInfo.include = true
} else if ((!bundleInfo.required) && gradle.ext.excludeProjects.contains(bundleName)) {
//指定可移除模块
bundleInfo.buildType = 'lib'
bundleInfo.include = false
} else if (gradle.ext.versionPathConfig) {
// AAR 混合编译,versionPath决定aar信息
def that = gradle.ext.versionPathConfig.find { item -> bundleName == item.bundleCode }
if (that) {
// AAR编译的模块
bundleInfo.buildType = 'aar'
bundleInfo.include = true
bundleInfo.versionName = that.version
} else {
// 其他模块
bundleInfo.buildType = 'lib'
bundleInfo.include = false
if (bundleInfo.required) {
println("Build bundle missing,bundleName:" + bundleName)
}
}
} else {
// 纯源码编译
bundleInfo.buildType = 'lib'
bundleInfo.include = true
}
}
在各个模块的ext扩展中定义统一的模块依赖方法,方便修改
//ext扩展(接口为ExtraPropertiesExtension.java)中定义统一方法
def compileBundle(Project project, String depName = null) {
def projectName = depName ?: project.name
def dependenciesHandler = project.configurations.implementation
if (projectName != project.name) {
//指定单个模块依赖
def findProject = project.rootProject.findProject(projectName)
if (findProject != null) {
dependenciesHandler.dependencies.add(
project.dependencies.create(project.rootProject.project(projectName))
)
println("compileBundle direct >> ${project.name} $projectName")
} else {
println("compileBundle direct skip >> ${project.name} $projectName")
}
} else {
//传递模块依赖,moduleTransitives即为解析的aar_config.json对象
if (gradle.ext.moduleTransitives != null) {
def depList = gradle.ext.moduleTransitives[projectName]
depList?.each {
def findProject = project.rootProject.findProject(it.depName)
if (it.depName != null && findProject != null) {
dependenciesHandler.dependencies.add(
project.dependencies.create(project.rootProject.project(it.depName))
)
println("compileBundle relationship >> ${projectName} ${it.depName}")
} else {
println("compileBundle relationship skip >> ${projectName} ${it.depName}")
}
}
}
}
}
//模块gradle使用处
dependencies {
...
compileBundle(project)
}
File资源的冲突导致在运行时会因为同名资源中 id 找不到而 crash。 Value资源的冲突导致 UI 不符合预期。
赋予每一个非 assets 资源一个ID值,这些ID值以常量的形式定义在一个 R.java 文件中 生成一个 resources.arsc 文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表
使用对模块具有性(或在所有项目模块之间具有性)的前缀命名 其他一致的命名方案
一是如何找出冲突的资源进行重命名处理 二是对什么样的同名资源处理 三方库和三方库中的资源冲突该怎么办
//ModuleSelectorStringNotationConverter.java
@Override
protected ComponentSelector parseType(String notation) {
...
String[] split = notation.split(":");
if (split.length < 2 || split.length > 3) {
throw new UnsupportedNotationException(notation);
}
...
return DefaultModuleComponentSelector.newSelector(moduleIdentifierFactory.module(group, name), DefaultImmutableVersionConstraint.of(version));
}
-
Gradle 存在一个ServiceRegistry的注册中心, 且 封装了一系列类似于JavaSpi机制的Services类,其中一个DependencyServices类提供了DependencyFactory依赖工厂的构建方法; 默认的DependencyFactory工厂实现类,包含通过静态工厂方法创建的DependencyNotationParser对象,其中组装了我们常用的StringNotation和ProjectNotation,返回一个组合的标记解析类NotationParser;
-
当我们通过 api或implement 声明依赖时,实际上是通过Project中的DependencyHandler对象来进行处理的,这个对象就是ServiceRegistry提供的,这就和 DependencyServices 等功能类建立了联系; DependencyHandler 类中并没有 implement和api 方法,因为Groovy是一个动态语言,允许运行时去catch未定义方法的调用。我们只需知道如果遇到这种找不到的方法会交给此类中的DynamicAddDependencyMethods对象去处理,终会使用 DependencyNotationParser 中构建的标记解析类去解析。
//提供方法跳过替换后的模块gradle逻辑
def isSubstitute() {
return gradle.ext.subsitituteMap.containsKey(":${project.name}".toString())
}
提交并同时构建所有的aar;此种方案优点是发布aar迅速,但因aar依赖顺序问题会出现打包失败,此时只能再打一次包解决此问题; 提交并按依赖顺序构建aar;此种方案能解决打包失败的问题,但总的aar发布时间缓慢,导致整个打包流程的时间剧增,特定情况下会大幅超过源码构建时间。此种情况可对模块进行细粒度的拆分,减少开发业务时频繁的进行aar模块的传递性打包。
开始次编译
开始第二次编译
本地电脑测试去除干扰项后,次构建源码平均耗时为 8m47s,aar平均耗时为 4m24s, 总包时间缩减率为 50%;第二次构建源码平均耗时为 5m50s,aar平均耗时为 3m15s,总包时间缩减率为 44%。
从总包构建时间上来看,通过模块AAR方案改造后 release 总包时间缩减率可达50%左右,时间缩减明显。开发阶段关闭transform等耗时操作且支持部分模块以AAR引入,编译时间会进一步缩减,可有效提高开发效率。 但从整个流程上来看,打包流程从传统的源码构建分成了发布aar和构建aar总包两部分。其中发布aar的平均时间为2m左右,如果发布一个aar后再打总包,此时整个打包流程的时间缩减率在13%~27%之间,而自动化的aar打包会有依赖问题,存在多个aar顺序打包后再打总包的情况,故整个流程上的时间缩减并不明显并大有可能会超过源码编译时间。
项目在保持良好的工程结构的同时,对业务模块进行适当粒度的拆分,可以让项目结构更清晰,更加有利于项目的更新; 可对高耗时的task,如 dexBuilder,单独做优化; 添加工程构建监控机制,及时对编译过程分析和处理;
相关文章