Android全量编译加速——(透明依赖)

移动开发 Android
在我们平常的开发中构建工程是一个基础的环节,决定着开发效率的高低,然而随着业务代码不断累积,编译耗时也随之增长。虽然已经有许多增量编译加速方案,但不可避免的在很多场景,还是需要全量编译。而对于全量编译加速,我们遇到了一些困难。

1.1. 背景

在我们平常的开发中构建工程是一个基础的环节,决定着开发效率的高低,然而随着业务代码不断累积,编译耗时也随之增长。虽然已经有许多增量编译加速方案,但不可避免的在很多场景,还是需要全量编译。而对于全量编译加速,我们遇到了一些困难:

  • K歌的项目里,总代码量160w行,kotlin代码占比43%左右,编译耗时占比却高达70%,必须要压缩这个比例。

于是需要找到一种方法,既能继续享受kotlin带来的开发便利,也能缓解全量编译时间快速增长的问题。

1.2. 方案

如果能减少kotlin的编译数量,就能降低编译耗时,要么减少代码,要么提前编译代码,后者可行度高。

而Android里支持两种二进制归档文件:JAR、AAR

两种格式里源码都是以.class格式存在,不过jar不包含资源,对于在做组件化的项目不友好,library module在编译后会直接生成aar。

那么只要把所有library module都通过CI/CD工具,持续的自动生成aar,发布到同一个maven仓库,在编译时用这些aar参与编译就成功了。

1.3. 方案初步呈现

 

library modules提前编译成了aar,我们需要把依赖类型由implementation project更改为implementation aar。

如果library module代码改变了,都需要重复执行提前编译aar,修改依赖版本号,很浪费时间,这里能不能取缔这个重复操作环节让程序自动化?

一种更好的方式:编译时判断library module当前代码版本是否有可用的aar,有则使用aar参与编译。拆解流程:

计算当前代码版本所有文件的hash,包含如下:

  • JavaSource
  • JavaResource
  • Assets
  • Resources
  • Aidl
  • JniLibs
  • AndroidManifest.xml
  • Proguard
  • Lint

判断maven仓库里是否有对应hash的aar,寻址 = repository/libraryName/version-md5

修改library module依赖类型为aar。

1.4. 遇到的问题

1. jar重复类冲突

 

可以看到B对C存在直接的依赖关系,这个关系会声明在B.arr的元数据文件.pom,又由于C的代码更改了导致无法匹配远程aar,所以最后C会同时以aar和project两种方式参与编译,如果C里包含了jar,就会冲突。

2. 工程重复类冲突

share_m和share是同一个代码仓库,开发便于验证更改了name,路由不一样代码一样,gradle认为是两个aar,报错重复。

3. 三方库版本冲突

最终编译后share代码版本依然为1.2.0,因为B.aar存在对share:1.2.0依赖。Gradle将考虑所有请求的版本,无论它们出现在依赖关系图中的何处。在这些版本中,它将选择最高的版本。

第一个问题:明显的需要把B(aar)—>C(aar)这个依赖项解除,这里常用有两个办法:

  • 直接从pom里删除该项依赖元数据(K歌采用)。
  • 修改B依赖C的依赖类型改为compileOnly,不过如果B使用了C的资源打包aar会报错。

第二个问题:K歌的做法是要求name保持一致,开发修改代码发布时只改变version。

第三个问题:因为这种模型也会存在正常开发中,对于版本冲突,有以下几项办法:

  • 开发时用更高的版本去覆盖掉参与构建的所有版本。
  • 修改B—>share:1.2.0依赖类型为compileOnly,来解除传递依赖。
  • 如果一定要使用动态版本号+,且低于参与构建的版本,可以提取出白名单,从pom里删除该项依赖,统一由app主module依赖(K歌采用)。
  • B在发布aar时,不保留pom里对三方的任何依赖元数据,编译时统一由app依赖。

想要解决传递依赖的问题还有常见的transitive,force,严格依赖等特性,K歌使用这些特性很少,考虑到要开发透明,保持原有代码,我们采用的都是直接修改pom文件依赖项来解除传递依赖。

从以上问题不难看出,唯一标识=自身内容+依赖关系图,所以在计算md5时,我们也需要把依赖关系算进去。什么时候可以获取依赖图?

Gradle的构建生命周期分为3步:

1、初始化

Gradle支持单项目和多项目构建。在初始化阶段,Gradle确定将要参与构建的项目,并为每个项目创建一个Project实例。

2、配置

在此阶段,将配置项目对象。执行作为构建一部分的所有项目的构建脚本。

3、执行

Gradle确定要在配置阶段创建和配置的任务子集。子集由传递给gradle命令的任务名称参数和当前目录确定。然后Gradle执行每个选定的任务。

明确在配置阶段是执行build.gradle,依赖图生成后,可以在项目评估回调里(afterEvaluate)解析完成我们的操作。

K歌的app module依赖了全局所有的library module,在编译时app最先收到评估回调,只要这时修改app的依赖关系图就能阻断其余library module的后续配置流程,而这时library module并未评估完成,拿不到依赖关系图就无法计算md5,只能手动解析library module的build.gradle文件里的依赖配置。K歌的配置一部分直接申明在dependencies里,一部分提取成了统一管理versionConfigs.gradle。

需要注意的是,每个项目评估完整结束后再修改依赖图是不安全的,Gradle会阻止。

 

1.5. 最终流程

  1. 构建项目,处于配置阶段时会执行每个project的build.gradle,里面会确定下来依赖关系,在评估项目之后(afterEvaluate)收到通知。
  2. 解析配置里对于本地project类型的依赖(DefaultProjectDependency),计算project的md5,计算包含的内容为前面讲诉的aar内容,同时把project的依赖关系也要作为md5计算的范围。
  3. 计算出md5后按照maven库的寻址规则拼接到路径上访问远端maven仓库是否存在此aar。
  4. 存在aar,则将本地project的依赖类型改为远程aar依赖(DefaultExternalModuleDependency)。

 

 

责任编辑:未丽燕 来源: 腾讯音乐技术团队
相关推荐

2010-07-28 10:31:24

ADSL加速方法

2017-08-28 17:50:45

全闪存

2024-01-11 16:02:38

OHOS依赖关系检查编译构建系统

2011-05-31 10:00:21

Android Spring 依赖注入

2009-02-24 09:28:00

2016-01-20 11:22:17

增量部署全量部署运维

2023-03-09 15:15:21

鸿蒙模块编译

2015-12-30 14:17:56

微软Windows 10Windows 7

2019-12-10 09:54:20

高德APP架构全链路

2015-12-08 11:11:33

戴尔云计算

2012-09-29 13:31:26

智汇云应用商店

2022-02-21 14:49:26

OpenHarmon操作系统鸿蒙

2010-02-06 10:14:36

Android Act

2016-12-02 19:00:13

Android FraAndroid

2011-05-31 14:52:13

Android 反编译 方法

2013-05-28 10:52:07

Android开发移动开发移动应用

2021-09-16 17:00:37

工业控制应用

2013-05-14 09:31:06

IBM闪存服务器
点赞
收藏

51CTO技术栈公众号