作者简介
小明,携程移动开发工程师,专注于移动端框架和基建开发;
黄拉欢,携程移动开发经理,专注于移动端框架建设及性能调优。
一、 背景
Android 项目一般使用 Gradle 作为构建打包工具,随着业务需求的不断迭代,代码量提升的同时,Gradle 编译耗时也在不断的增长,而编译速度会直接决定开发流程效率的高低,影响面主要涉及到开发和测试阶段。
对于火车票项目,经过长期的迭代过程导致模块众多工程庞大,优化前一次干净的全量编译时间可达到10m39s,造成开发和测试都需要长时间等待编译出包,严重影响到开发和测试的效率。因此对火车票 App 进行编译速度优化是件亟待解决的事情。
本次编译速度优化采用的方案是模块AAR方案, 优化目标为: 优化后一次干净的全量编译时间缩减为原来编译时间的50%以下。
二、 模块AAR方案介绍
Google 官网提供了优化构建速度的几种方案,但基本上效果都不明显。业内常用的编译加速方案一般有模块aar、控制增量编译、transform 优化、dexBuilder 优化等,其中有些方案侵入性太强或者 ROI 不高,模块aar方案侵入性低,预计收益显著,并且在大厂已经成为标配方案,可以作为本次编译优化的主方向。
AAR(Android ARchive)文件为 Android 库编译产物,其中包括 class、资源和清单文件,可用作 Android 应用模块依赖项。在Android 的构建流程中,源码编译时会将依赖的 aar 库和应用模块合并,在通过 apkbuilder 输出 apk 产物。
图1:android的构建流程图
Android 项目大多都是多模块 Gradle 项目,构建项目时所有的子模块需要参与编译,导致全量编译时间冗长。实际上,对于大部分的开发人员来说,并不需要关注所有的模块,只需要负责功能相关的业务模块即可,因此这也提供了模块aar方案的切入点。
一般来说,aar方案都大同小异,涉及到模块aar发布,project和module依赖的切换,传递依赖的处理等几个方面。依赖切换我们采用的是 Gradle 官方文档中直接自定义依赖项的解析方案,通过定义依赖替换规则来更改依赖项;aar的发布使用的是maven-publish插件;传递依赖使用的mavenPom文件管理。
火车票项目经过多次模块化的重构后现有20个子模块,可将这些子模块独立打包aar并发布至远程maven仓库中,在需要打包时用aar替换项目的构建,就能将编译的时间给节省下来,以提高打包效率。
图2: 模块aar开发示意图
三、改造过程和遇到的问题
3.1 构建测量指标
良好的优化基于数据的呈现,首先第一步需要做的是分析构建的性能。此处采用的是官方提供的Gradle --profile选项的方式统计数据。基本步骤为:
gradlew clean在每个build之间执行干净编译,确保剖析完整的构建流程;
gradlew--profile--offline--rerun-tasks assembleFlavorDebug为某个变种task启用性能剖析,并确保gradle使用的是缓存依赖项,且强制gradle重新运行所有任务;
构建完成后,会生成html格式的性能剖析报告,可用于分析构建信息和前后构建对比;
3.2 模块aar的改造
因存在单个模块发布的过程,模块间需要尽量的解耦,而解耦首先要进行模块的划分;其次,存在依赖关系的模块间不能互相使用资源,这会造成编译的失败,所以理清模块间的关系是当务之急。
同时,我们需意识到当我们完成 Gradle 脚本的编写后,我们得到的应该是一个app壳+其他模块的依赖树组合,可通过以下步骤进行改造:
1)模块间依赖树重构
首先分析项目各模块依赖关系, 可使用开源脚本工具projectDependencyGraph.gradle生成依赖图,示意图如下:
图3: 模块间依赖树混乱示意图
可直观的看出模块间依赖关系冗余,模块间的依赖不清晰,且基础库一般固定却呈现网状结构。故需对网状结构进行整理,将不经常改动的库直接发布远程,如下:
图4: 模块间依赖树整理示意图
2)历史Gradle脚本重构
因历史原因,火车票项目的 Gradle 逻辑冗余繁琐,无统一管理略显臃肿。为提高 Gradle 的扩展性和灵活性,需要重构 Gradle 代码:
对现有 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 的创建。
3)发布和依赖aar版本的控制
当我们发布aar至远程仓库后,需要一个文件记录已发布模块aar的版本号,打包时通过此文件查找对应版本的aar,此处使用的是 MCD 平台采用 json 格式的 versionPath 文件。示意图如下:
图5:通过versionPath文件控制版本
模块aar的核心在于对依赖项的控制。我们想要的是一个app壳+部分源码模块+部分aar模块+其他依赖的结构,因此下面几点是要考虑到的:
在所有子模块发布 maven 成功后,本地编译使用aar模块且环境参数没有传递 versionPath 参数时,构建脚本会自动拉取Mcd最新的versionPath.json链接作为默认的模块aar信息表。
开发阶段需切换源码/aar,可对项目中所有模块进行分类,分别为:入口模块,源码模块,aar发布模块,可移除模块。另外,合并 gradle 环境参数和local.properties参数共同作为控制参数,然后修改app_bundle.json指定模块类型以达到灵活切换的目的。例如,定义一个sourceProject参数指定哪些模块作为源码构建。
为模块之间的依赖关系建立一个json文件aar_config.json,便于查看和修改模块间依赖关系。
整个模块aar打包流程图如下: