You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.9 KiB

Gradle 口水话

目录

  1. 简述
  2. Gradle 构建流程
  3. Gradle Plugin
  4. Gradle 构建优化
  5. App 构建流程

简述

Gradle 构建流程

Gradle 的执行分为三大阶段:Initialization -> Configuration -> Execution。Initialization 阶段主要目的是初始化构建,它分为两个子过程,一个执行 Init Script,另一个是执行 Setting Script。Init Script 会读取全局脚本初始化一些通用的属性,比如 Gradle User Home 目录、Gradle version 等,一般情况下我们不会动这个文件。Setting Script 更常见,它初始化了一次构建所参与的所有模块。

Configuration 阶段会加载项目中所有模块的 build.gradle 文件,根据脚本创建对应的 Task,然后根据 Task 的依赖关系生成有向无环图。在每执行完一个 Project 的 build.gradle 文件后,都会回调每个 build.gradle 文件中配置的 project.afterEvaluate{} 函数。生成任务图的过程也可以通过 gradle.taskGraph.whenReady 来进行 hook。

Execution 阶段才真正进行任务的执行。Gradle 会按照 task graph 中的依赖关系执行每一个任务,对于任务的执行前后也可以添加 hook 函数。项目中我也通过对 assembleRelease 进行 hook,在它执行完把生成的 release Apk 复制到指定目录。

可以在项目中,使用 build --scan 来进行性能分析,查看这三个阶段做了哪些事情以及耗费了多少时间。

Gradle Plugin

起初呢,是因为有一个需求就是,我们的 App 因为权限问题被警告了,组长就希望我能找出 app 模块及其依赖的第三方库里面的权限信息。首先我的思路就是先找出项目中的 Manifest 文件,在正则匹配出权限就行了。对于本地工程,这个很好做,可以直接通过 Variant 直接拿到 Manifest 文件,写个脚本在根目录的 subprojects 跑一遍就可以了。但是对于第三方库就没办法了。这时候我就想到,在 Apk 打包的时候有一个 MergeManifest 的过程,这个时候 Gradle 是肯定知道所有 Manifest 的,然后就去看一下 Gradle 源码是怎么做的。其实呢是在 ProcessApplicationManifest 这个 Task 来做的,Manifest 是放在 VariantData 的 ArtifactCollection 里面的。然后我就把它封装成一个 Task 命令行调用就可以拿到所有权限信息了。

考虑到后面可能还会有一些类似的需求,于是我就写了一个插件。我觉得这是一个非常好的开端,之所以当初组长找到我做这件事是因为我之前在组内分享了 Gradle 的相关知识,包括 Gradle 的构建流程、Gradle 的核心概念 Task 以及利用 Transform API 结合 AspectJ、ASM 进行自动化埋点等。这对我来说呢是一个正向激励,后面我也越来越会做更多的分享。

后面呢,我还在 Plugin 里面添加了自动 TinyPng 资源压缩,考虑到我们的 minApi 19,又做了全量的 png 转 webp 这一步的压缩是包含第三方库里面的图片的,使用的是 Google 开源的 cwebp 工具。通过 png 转 webp,减少了 5.7M,再后来做包体积优化时,发现存在不少的重复资源,也就是遍历生成 MD5 值进行比较,减少了 133 kb;通过配置 resConfigs 只保留 zh、en 减少了 1.1M。

再之后,基于 ASM 做了 MethodTracker,简单使用一个注解就可以查看方法耗时,还有基于 Choreographer 的 FPS 检测。这里当时是遇到一个问题的,我们知道插件里面的类,在外部模块是不能访问到的,然而我这里的注解以及 FPSDetector 都是需要在 app 模块使用的,这时候我就看了 JW 的 Hugo 的实现,它也是基于 AspectJ 做的方法耗时,因为它的注解也是可以在 app 模块使用的,看源码发现它不过是是把这个注解放到外部一个远程库的,然后在插件里面进行依赖的。Hugo 仅仅只有四个类,却有 7k 多的 Star,所以说有时候不能被表面吓着,以为会很麻烦其实很简单。

在我写 Plugin 的之后,滴滴开源了 Booster,然后我就按照它的架构方式,使用 SPI 去自动注册 Task,这就不需要每新增一个 Task 都要去 apply 方法进行注册了。当时是用 Java 写的,也都切成了 Kotlin,并且也用的 kts 构建。

Gradle 构建优化

首先在测试优化效果时,不包含 Configuration 阶段的时间,所以实际上优化程度更高。但是一般来说并不会频繁更新依赖,所以去掉了 Configuration 阶段。结果就是:

全量编译 代码增量 资源增量
优化前 1m 59s 27s 8s
优化后 1m 1s 9s 10s

首先就是使用较高版本的 Gradle 和 Android Gradle Plugin,在项目中,我主导了 Gradle 从 4.10.1 到 6.5.1 的升级。前面说过,Gradle 的构建分为三个阶段,分别是 Initialzation、Configuration 和 Execution 阶段。下面我就从这三个阶段来着手优化。

在 Initialzation 阶段,可以在 gradle.properties 中开启构建缓存和并行构建,也可以适当增加内存分配;

在 Configuration 阶段,避免使用动态版本和快照版本,动态版本即 1.0.+ 这种方式,快照版本即 SNAPSHOT,这两种方式都会迫使 Gradle 链接到远程仓库检查是否有依赖更新,默认有效期是 24h。我在项目中就发现一个友盟的 analyze 库使用了动态版本,然后就改为固定版本号;然后就是调整 repo 顺序并过滤请求,也就是把内部的 maven 仓库放在最前面,因为 Gradle 在查找远程库时,是串行查询所有 repo 中的 maven 地址的,直到找到可用的依赖,所以应该把最快和最高命中率的仓库放在前面。同时,在 Gradle 5.1 开始,可以指定特定 repo 下载特定的包名的依赖,然后我就在我们内部的 maven 库过滤 com.ehi 的依赖。这一操作能有效的减少 Configuration 阶段的时间;然后就是减少不必要的 Plugin,可以通过 build-scan 扫描项目中用到了哪些 Plugin,我在项目中发现我们 app 模块使用了一个 Google 的 osdecetor 的插件,但是搞了好久也没发现是哪里引入的,我怀疑是无良第三方库里面的,但是可惜一直没解决这个问题。

在 Execution 阶段,可以通过 -x 跳过不需要的 Task。我是在自己的 AS 里面配置 -x test -x lint 过滤掉 Test 和 Lint 相关的 Task,并且配置了一个 -PdevBuild 参数,这样测试小伙伴在 Jenkins 打 Debug 时也能享受这种收益,除此之外,我还会根据这个参数来关闭 AAPT2 自带的 png 压缩和 png 的合法性检查、关闭多 abi 和多 density 的构建以及通过 resConfigs 来最小化使用资源文件。resConfigs 不仅可以减少包体积,在 debug 时可以选择只构建中文简体和 xxhdpi 的资源。

App 构建流程