共计 11027 个字符,预计需要花费 28 分钟才能阅读完成。
本文来自掘金的字节跳动团队
原文链接:https://juejin.im/post/5f144b2f6fb9a07e6f7b7fce#heading-28
背景介绍
=======
Android 我的项目个别应用 gradle 作为构建打包工具,而其执行速度慢也始终为人所诟病,对于今日头条 Android 我的项目这种千万行级别的大型工程来说,全量编译一次的工夫可能高达六七分钟,在某些须要疾速验证性能的场景,改变一行代码的增量编译甚至也须要等两三分钟,这般龟速重大影响了开发体验与效率,因而针对 gradle 编译构建耗时进行优化显得尤为重要。
在今日头条 Android 我的项目上,编译构建速度的优化和好转始终在交替执行,18 年时因为模块化拆分等影响,clean build 一次的耗时达到了高峰 7 分 30s 左右,相干同学通过模块 aar 化,maven 代理减速,以及增量 java 编译等优化伎俩,将 clean build 耗时优化到 4 分钟,增量编译优化到 20~30s。然而前面随着 kotlin 的大规模应用,自定义 transform 以及 apt 库泛滥,又将增量编译速度拖慢到 2 分 30s,且有进一步好转的趋势。为了优化现有不合理的编译耗时,避免进一步的好转,最近的 5,6 双月又针对编译耗时做了一些列专项优化(kapt,transform,dexBuilder,build-cache 等) 并增加了相干的防好转管控计划。从 4.27 截止到 6.29,整体的优化成果如下:
历史优化计划
因为 18 年左右客户端根底技术相干同学曾经对今日头条 Android 工程做了许多 gradle 相干的优化,且这些优化是近期优化的根底,因而先筛选几个具备代表性的计划进行介绍,作为下文的背景同步。
maven 代理优化 sync 工夫
背景
gradle 工程往往会在 repositories 中增加一些列的 maven 仓库地址,作为组件依赖获取的查找门路,晚期在今日头条的我的项目中配置了十几个 maven 的地址,然而依赖获取是依照 maven 仓库配置的程序顺次查找的,如果某个组件存在于最初一个仓库中,那后面的十几个仓库得顺次发动网络申请查找,并在网络申请返回失败后才查找下一个,如果我的项目中大多组件都在较后仓库的地位,累加起来的查找时间就会很长。
优化计划
应用公司外部搭建的 maven 私服,在私服上设置代理仓库,为其余仓库配置代理(例如 google、jcenter、mavenCentral 等仓库),代理仓库创立好后,在 Negative Cache 配置项中敞开其 cache 开关:如果查找时没有找到某版本依赖库时会缓存失败后果,一段时间内不会从新去 maven 仓库查找对应依赖库,即便 maven 仓库中曾经有该版本的依赖库,查找时依然返回失败的后果。
建设仓库组,将所有仓库归放到一个对立的仓库组里,依赖查找时只须要去这个组仓库中查找,这样能大大降低屡次发动网络申请遍历仓库的耗时。
模块 aar 化
背景
今日头条我的项目进行了屡次组件化和模块化的重构,分拆出了 200 多个子模块,这些子模块如果全都 include 进我的项目,那么在 clean build 的时候,所有子模块的代码须要从新编译,而对于大多数开发人员来说,基本上只关怀本人负责的少数几个模块,基本不须要改变其余模块的代码,这些其余 project 的配置和编译工夫就成为了不必要的代价。
优化计划
对于以上子模块过多的解决方案是:将所有模块公布成 aar,在我的项目中全副默认通过 maven 依赖这些编译好的组件,而在须要批改某个模块时,通过配置项将该模块的依赖模式改为源码依赖,做到在编译时只编译改变的模块。然而这样做会导致模块慢慢的又全副变为源码依赖的模式,除非规定每次批改完对应模块后,开发人员本人手动将模块公布成 aar,并改回依赖模式。这种重大依赖开发人员盲目,并且在模块数量多依赖关系简单的时候会显得异样繁琐,因而为了开发阶段的便当,设计了一整套更残缺粗疏的计划:
开发时,从主分支拉取的代码肯定是全 aar 依赖的,除了 app 模块没有任何子模块是源码引入。
须要批改对应模块时,通过批改 local.properties 里的 INCLUDES 参数指定源码引入的模块。
开发实现后,push 代码至远端,触发代码合并流程后,在 ci 预编译过程与合码指标分支比照,检测批改的模块,将这些模块依照依赖关系顺次公布成 aar,并在工程中批改依赖为新版本的 aar, 这一步保障了每次代码合入实现后,主分支上的依赖都是全 aar 依赖的。
收益
通过上述革新,将源码模块切换成 aar 依赖后,clean build 耗时从 7,8 分钟升高至 4,5 分钟,收益靠近 50%,效果显著。
增量 java/kotlin 编译
背景
在非 clean build 的状况下,更改 java/kotlin 代码尽管会做增量编译,然而为了相对的正确性,gradle 会依据一些列依赖关系计算,抉择须要从新编译的代码,这个计算粒度比拟粗,略微改变一个类的代码,就可能导致大量代码从新执行 apt, 编译等流程。
因为 gradle 作为通用框架,其设计的根本准则是相对的正确,因而很容易导致增量编译生效,在理论开发中,为了疾速编译展现后果,能够在编译正确性和编译速度上做一个折中的计划:
禁用原始的 javac/kotlinCompile 等 task, 自行实现代码增量批改判断,只编译批改的代码。
动静禁用 kapt 相干的 task, 升高 kapt,kaptGenerateStub 等 task 的耗时。
以上计划(下文全副简称为 fastbuild) 尽管在波及常量批改,办法签名变更方面 存在肯定的问题(常量内联等),然而能换来增量编译从 2 分多升高至 20~30s,极大的晋升编译效率,且有问题的场景并不常见,因而整体上该计划是利大于弊的。
编译耗时好转
通过上文介绍的几个优化计划和其余优化形式,在 18 年时,今日头条 Android 我的项目的整体编译速度(clean build 4~5min, fast 增量编译 20~30s)在同量级的大型工程中来说是比拟快的,然而前期随着业务倒退的需要,编译脚本增加了很多新的逻辑:
kotlin 大规模应用,kapt 新增了很多注解解决逻辑。
引入对 java8 语法的反对,java8 语法的 desugar(脱糖)操作减少了编译耗时。
大量的字节码插桩需要,增加了许多 transform,大幅度晋升了增量编译耗时。
这些逻辑的引入,使得增量编译耗时好转到 2 分 30s,即便采纳 fastbuild,改变一行代码编译也须要 1 分 30s 之多,开发体验十分差。而下文将着重形容最近一段时间对上述问题的优化过程。
近期优化计划
app 壳模块 kapt 优化
背景
今日头条工程通过屡次模块化,组件化重构后,app 模块 (NewsArticle) 的大部分代码都曾经迁徙到子模块(上文曾经介绍过子模块能够采纳 aar 化用于编译速度优化,app 模块只剩下一个壳而已。
然而从 build profile 数据(执行 gradle 命令时增加 –profile 参数会在编译实现后输入相干 task 耗时的统计文件)中发现到一个异样 case:明明只有 2 个类的 app 模块 kapt(annotationProcessor 注解解决) 相干耗时近 1 分钟。
通过进一步察看,尽管 app 模块拆分后只有 2 个简略类的代码,然而却用了 6 种 kapt 库,且理论失效的只是其中 ServiceImpl 一个注解(外部 ServiceManager 框架,用于批示生产 Proxy 类,对模块之间代码调用进行解耦)。如此一顿操作猛如虎,每次编译却只生成固定的两个 Proxy 类,与 53s 的高耗时相比,投入产出比极低。
优化计划
把固定生成的 Proxy 类从 generate 目录挪动到 src 目录,而后禁止 app 模块中 kapt 相干 task,并增加相干管控计划(如下图: 检测到不合理状况后立即抛出异样),避免其他人增加新增的 kapt 库。
收益
在 mac clean build 中均匀有 40s 收益
在 ci clean build 中均匀有 20s 收益
kapt 隔离优化
背景
通过上文介绍在 app 模块发现的异样的 kapt case,进而发现在工程中为了不便,定义了一个 library.gradle,该文件的作用是定义我的项目中通用的 Android dsl 配置和共有的根底依赖,因而我的项目中所有子模块均 apply 了这个文件,然而这个文件陆陆续续的被不同的业务增加新的 kapt 注解解决库,在全源码编译时,所有子模块都得执行 library 模块中定义的全副 6 个 kapt,即便该模块没有任何注解相干的解决也不例外。
而上述情况的问题在于:相比纯 java 模块的注解解决,kotlin 代码须要先通过 kaptGenerateStub 将 kt 文件转换成为 java,让 apt 处理程序可能对立的面向 java 做注解扫描和解决。然而下面讲到其实有很多模块是基本不会有任何理论 kapt 处理过程的,却白白的做了一次 kt 转 java 的操作,源码引入的模块越多,这种无意义的耗时累加起来也十分可观。
为了可能弄清楚到底有哪些子模块真正用到了 kapt,哪些没用到能够禁用掉 kapt 相干 task,对我的项目中所有子模块进行了一遍扫描:
获取 kapt configuration 的所有依赖,能够失去 kapt 依赖库的 jar 包,利用 asm 获取所有 annotation.
遍历所有 subproject 的 sourceset 下所有 .java,.kt 源文件,解析 import 信息,看是否有步骤 1 中解析的 annotation
package task 实现后遍历 所有 subproject 所有 generate/apt ,generate/kapt 目录下生成的 java 文件
应用上述计划,通过全源码打包最终扫描进去大略是 70+ 模块不会进行任何 kapt 的理论输入,且将这些不会进行输入的 kapt,kaptGenerateStub 的 task 耗时累加起来较高 217s(因为 task 并发执行所以理论总时长可能要少一些).
获取到不理论生成 kapt 内容的模块后,开始对这些模块进行细粒度的拆分,让它们从 apply library.gradle 改为没有 kapt 相干的 library-api.gradle,该文件除了禁用 kapt 外,与 library 逻辑统一。
然而这样做算是在背地偷偷做了些更改,很可能后续新来的同学不晓得有这种优化伎俩,可能新增了注解后却没有任何输入且找不到起因,而优化成果最好是尽量少给业务同学带来困扰。为了防止这种状况,便对这些 library-api 模块依赖的注解做隔离优化,即:把这些模块依赖的注解库全副 主动 exclude 掉,在尝试应用注解时会因获取不到援用(如下图所示),第一工夫发现到依赖被移除的问题。
另一方面在编译呈现谬误时,对应 gradle 插件会主动解析找不到的符号,如果发现该符号是被隔离优化的注解,会提醒将 library-api 替换成 library,尽可能升高优化计划对业务的负面影响。
收益
mac 全源码场景中有 58s 左右的减速收益。
ci 机器上因为 cpu 核数更多,task 并发性能更好,只有 10s 左右的收益。
transform 优化
背景
transform 作为 Android gradle plugin 提供给开发者的 API,用于在 apk 构建过程中,对 class 字节码,resources 等文件内容进行插桩批改,例如官网的 dex, proguard 等性能均由此 api 实现。
对于今日头条这种大型工程来说,有很多诸如性能插桩、主动埋点插桩等相干需要,因而基于此 api 开发了大量 transform,用于实现特定性能,然而这些 transform 基本上都是不反对增量编译的,即便只改变了一行代码,这 些 transform 都会遍历所有 class 文件,解析字节码中的办法字段信息,要害是这类 transform 数量有十几个,将这些遍历耗时乘以 10 累加之后,增量编译耗时天然居高不下。
依据剖析,其中性能插桩等相干 transform 做的一些面向线上的插桩计划是齐全能够只在 release 打包时关上的,因而能够间接在 debug 编译时禁用这些性能,用于晋升开发期间的编译速度。而剩下的 9 个 transform 特色比拟类似,可能在一些插桩细节上有所不同,它们大抵的解决逻辑为:
在各个模块中应用 apt processor 收集模块 xx 注解的 class 信息而后生成一个 xxCollect 类,该类的作用是收集好 apt 阶段解析到的本模块的类信息
将所有模块收集到的信息进行汇总,利用 transform 阶段扫描出所有的 xxCollect 信息,通过 javaassit 或者 asm 往一个 xxCollectMgr 的某个 collectXxx 办法插桩注入之前收到的信息
业务代码可通过 xxCollectMgr 的 collectXxx 办法获取到在各个模块动静生成的所有 xxCollect 信息。(例: 页面路由相干框架便是通过该逻辑收集到所有子模块的路由注册信息)
因为这 9 个自定义 transform 的性能如此相似,便决定将这些 transform 合并成一个,这样同一个文件的读写操作只执行一次,并且能够做定制化的增量编译优化。尽管公司内有相似的 transform 合并优化计划 byteX (已在 github 开源),然而因为今日头条我的项目在 debug 阶段未开启该性能,且 ByteX 做了一些诸如 ClassGrapth 的构建,对类文件做两次遍历等操作,对于实现类信息收集和信息注入 这个性能来说,byteX 显得比拟重,于是依然针对类信息收集注入性能这个细分场景开发了一个收敛框架。
收益
该框架实现了外部 9 品种信息收集注入相干框架的收敛,编译耗时的绝对值减速了 25s 左右,且因为提供了对立的增量缓存性能,使得改变一行代码的耗时能够从 2 分 30s 升高到 35~40s,实现了增量编译速度大的飞跃。最要害的是将所有自定义 transform 对立管控后,后续能够做对立定制化的需要,进一步优化编译速度。
dexBuilder 优化
背景
在 Android debug 编译 过程中,最次要的耗时在 transform 上,而上文 介绍 今日头条我的项目自定义 transform 曾经被高度优化过,剩下的 dexBuilder(将 class 转换成 dex),dexMerge 等 task 耗时就成为了性能瓶颈,dexBuilder 全量编译耗时 60s 左右,增量编译耗时 22s 左右。
依据 DexArchiveBuilderTransform 要害办法 launchProcessing 外面要害一行 isDirectoryBased,如果是目录类型的输出,会依据具体变动 class 文件做增量的 dex 编译,然而如果是 jar 输出类型,那只有 jar 里任何一个类变动,则整个 jar 所有类都须要重执行 dex,然而因为 gradle 的依赖个性,基本上只有 app 模块是目录类的输出,其余 library 都是 jar 输出类型,对于比拟大的业务模块,如果该模块有几千个类,那每改变一次类,就会有几千类连带从新 执行 dex 编译。
dexBuilder 增量成果量化
在优化前为了失去真正的从新执行 dex 编译的数值,做到最佳优化,设计了一套 hook dex 编译流程的办法(该办法实践上能够 hook Android gradle plugin 任意类:大抵就是 hook classLoader,提前用 asm 批改 D8DexArchiveBuilder 中的 convert 办法
通过对 D8DexArchiveBuilder 的 hook,统计到优化前改变一行代码会连带着 24968 个类从新执行 dex 编译,增量成果十分差。
优化计划
既然 jar 输出相比于 目录输出来说增量编译成果十分差,那么能够想到 hook TransformInvocation 中的 input 办法,动静将 project 的 jar 类型输出(JarInput)映射为一个 目录输出(DirectoryInput),那么子模块批改对应代码时,只从新编译目录中被批改的 class 为 dex(而不是原来的整个 jar 内所有 class 从新执行 dex 编译),整体 dex 从新编译的数量将大幅度缩小。实现具体计划如下:
主动发现源码依赖的子模块 project,配置常常须要变更的注入类所在的 SDK jar
hook TransformInvocation 的 input 将下面步骤中的 JarInput 映射为 DirectoryInput
每次 hook input 前查看与上一次须要优化的 project,sdk 是否统一,否则间接抛异样(影响增量判断)
而 jar 转 目录的映射细节为:
如果是新增的 jar,那解压该 jar 所有类文件到目录,将该目录下所有类定义为 ADD
如果是移除的 jar,查看之前解压的目录,该目录下所有类文件定义为 REMOVE
如果 jar 没有变更,那定义为之前解压的目录中没有任何子文件变更 NOT_CHANGE
如果 jar 有批改,须要进一步判断内容有哪些批改,如果 jar 中有的文件在 解压目录不存在,该文件定义为 ADD,如果目录有的文件在 jar 中不存在,该文件定义为 REMOVE,如果都同时存在,比拟文件内容(大小,hash),雷同定义为 NOT_CHANGED 否则为 CHANGED
在第一次增量批改实现后,从新执行 dex 编译的类数量升高至 2152 个,然而其中依然有很多蛊惑的不该执行 dex 编译的类,预期是批改多少类,就从新执行 多少次 dex,因而持续对其中起因进行进一步的摸索
desugarGraph 异样
因为 java8 的字节码有些指令在 Android 虚拟机中并不能失去反对,会在编译流程中,将这些指令进行脱糖,转换成已有的指令,而 d8 中 desugar 的流程合并到了 dexBuilder 中,为了防止某些类 desugar 后,依赖它的类的行为正确,须要把依赖它的所有类从新执行一遍 dex 编译。
而 d8 会依据 DesugaringGraph 查找 desguar 有变动的类及其依赖的 jar 包,如图上面取得到的 addtionalPaths 是 desguar 类可能间接间接相干的 jar 包,即便这些 jar 包没有任何文件内容变更,其中所有类也得从新全副执行一次 dex 编译。
DesugaringGraph 逻辑概述
该类用来辅助获取依赖或间接依赖到变更文件的所有文件,而它的生成逻辑为: 全量或增量编译类的时候记录类型之间的依赖和被依赖关系,依赖关系的判断条件有
父类
间接实现的接口
调用 dynamic 办法指令时的返回类型
DesugaringGraph 不仅记录了类依赖的类,和依赖它的类,同时也记录了一个文件门路蕴含了哪些类
如果文件门路是 class 文件,那门路就蕴含 1 个类
如果门路是 jar 文件,蕴含这个 jar 下所有类。
在增量编译时查看到变动的文件时,会查看这个文件门路蕴含的所有类,而后递归查找所有间接 / 间接依赖它的类,并且找到这些依赖它的类后,会把这个类所在的 jar 包作为额定的解决类型(即便 jar 自身没有任何变动,外面所有的类依然须要从新 dex 编译)
顺着这个解析关系,找到了一个不失常的 jar 包 bdjson_api,这个 jar 只有 3 个文件 (IBDJson,BDJsonCollector, BDJsonConstants)。然而 BDJsonCollector 是一个 stub 类,每次执行 transform 会收集到其余类的信息而后往该类的办法中注入,因而该文件每次编译时都会变动。这个类自身并没有多少间接依赖它的类,次要是 它所在的 jar 包还有个 IBDJson 接口。
依照之前的 DesugaringGraph 依赖关系,所有 IBDJson 接口的实现类被判断为依赖它,而后这些实现类如果呈现在某个 dynamic 办法中,又会被层层查找,查找完了之后,还得计算所有依赖类所在的 jar 包,jar 包中其余没有依赖它的类也会被从新 dex 编译,在这个 case 的依赖查找中,连带从新执行 dex 编译的类数量并不多,大略为 4 个 jar 包共 2000 多个类从新执行了无意义的 dex 流程,然而如果是其余 sdk jar 包,则可能就会给 dexBuilder 增量带来毁灭性的打击。上述问题的解决办法:
把每次都会批改的 Stub 注入类和其余接口类拆散,放在不同 jar 包。(须要革新业务,比拟麻烦)
动静把这个 sdk jar 输出转换成目录输出。(上文介绍的办法,也与下面 jar 转目录的初衷相符,只不过是漏掉了这个 case,然而却意外证实了:除了蕴含业务代码多的 project 的 jar 输出须要转换为目录外,sdk jar 同样有必要)
修复后批改一行代码从新执行 dex 的数量为 10,其中 9 个是每次 transform 会批改的 stub 类,1 个是理论批改类。做到了真正的 改多少类,执行屡次 dex 编译。
收益
assemebleDebug 的增量编译中从原来(上文 transform 优化后)的 35s~40s 是升高至均值 17s,在 fast build 中成果最显著(屏蔽了 apt),第二次增量编译能冲破到 9s 实现秒级编译。
而通过下面所有的优化后,耗时数据里耗时最重大的 dexBuilder 和 dex-merge 根本都升高在 1s 左右,自定义 transform 也是 1s 左右,其余 task 根本都是零点几秒。在不应用 hotfix 计划的状况下(因为今日头条我的项目应用了过多的自定义 transform 和插件计划,所以不好应用 instantrun 等 hostfix 计划),相干 task 的耗时根本达到了优化的极限。
build-cache 优化踩坑
Build-cache 是 gralde 提供的一个编译缓存计划,目标是在构建过程中当两个 task 的输出雷同时,能够复用缓存内容,间接跳过 task 的执行拿到缓存好的执行后果。因为缓存后果既能够放在本地磁盘,也能够从近程获取,因而容易想到利用 ci 提前构建缓存包,在其余 ci 机器和开发时利用缓存包取得减速成果。
那么如何判断 task 能够间接获取 之前 task 的缓存内容作为输入呢?定义为可缓存的 task,会定义一些缓存相干的属性,task 执行时通过文件指纹,缓存属性等一大堆属性计算出缓存 key,用于查找是否命中缓存,计算维度有:
输出属性(如 jvm 参数,sourceCompatibility 等参数)。波及到 各种 ValueSnapShot(值类型快照,string,file,list,等..)计算。以及 task 实现类 classpath 相干
输出文件集相干:波及到 依赖的输出文件的 hash 计算
输入属性相干
不可缓存属性相干
然而原生的 build-cahce 在缓存命中率上惨不忍睹,公司内抖音团队基于 gradle4.x 的源码做过一些进步命中率的批改,不过今日头条用的 gradle 版本是 5.1,受抖音团队的启发,也对 gradle5.1 源码做了些定制化的批改,用于 dump 缓存 key 的计算流程,疾速发现缓存问题。相比于抖音发现的一些影响缓存命中的问题,额定发现了一些诸如 mbox , kapt 元素遍历程序不固定的问题,这里只挑一个典型的 apt 程序不统一的问题进行介绍:
apt 程序不统一导致的缓存生效问题
通过批改 gradle5.1 源码后对编译流程的信息采集,发现有的 task 缓存无奈命中是因为 kapt 时,很多生成代码块逻辑是一样的,然而程序不一样(如下图 demo:上面两个生成办法的逻辑统一,然而判断程序不统一,这应该是在 processor 中通过 RoundEnviroment 获取到 注解元素 elemnts 程序不统一导致的)
其外部的起因可能是文件遍历目录时获取子文件的程序不统一,导致了子文件对应注解元素的程序也不统一,总之这个操作影响了生成文件内代码的程序,也影响了该文件的 hash 计算结果,导致 build-cache 在计算 javac task 的 key 时会错乱导致缓存无奈命中。
解决方案
然而留神到 AbstractProcessor 的外围办法 process 的两个参数都是接口,因而想到能够代理原来的 RoundEnvironment 接口,将其 getElementXx 的办法通过固定排序后返回,使得 apt 注解元素的程序可能固定下来。
因为篇幅影响,其余影响缓存命中相干的 case 略(次要是一些波及到文件绝对路径,classPath 相干的问题)
收益
因为大多开发场景是引入多少模块就批改多少模块内容,很难取得命中缓存,收益很小
次要是全源码场景能稳固取得一些编译减速,基本上在 22~99s 左右。
编译耗时防好转管控
在今日头条这种大型工程中,有很多业务部门参加开发,仅 Android 工程 开发人员就有几百人且人员变动频繁,因而外部任何一项优化工作必然是得搭配上一些管控措施的,否则一边优化一边好转,空节约人力。
为此制订了一些管控计划,首先是 debug 阶段的 新增 transform 管控,设置为白名单模式,如果在开发阶段新增了 transform 间接终止编译流程,通过阐明文档告知管控的规定,当然,管控的目标是尽可能减少一些不必要的不合理的编译问题,并不是与业务团队作对,如果某一个操作拖慢了整体的编译耗时,然而在 app 性能 / 稳定性方面有更大收益,且无奈在编译期做更多的优化,依然是容许增加的,只不过是得提前把这个问题裸露进去而已,能更快的找出更多的解决思路,比方疏导应用 byteX 等 transform 收敛计划。
另一方面的是合码流程方面的阻塞:今日头条 为了保障 app 的性能稳定性,在合码流程上设置了许多自动化的卡点:如 包大小检测,插件依赖变更查看,so 变更查看,启动性能检测等,检测到对应问题(如包大小减少异样)会阻塞合码流程。为了管控编译速度,使其不至于好转的太快,也加上了对应的 基于 task 级别的管控,当某一个 task 耗时异样稳定,或者新增全新类型的 task 时,可能主动的发现问题,通过机器人将相干人员拉到 mr 探讨群中,尽量在 合码实现前能发现问题。
总结
为了继续稳固的放弃较快的编译速度,可能须要做到以下几点:
我的项目须要有良好的工程构造,对业务模块进行适当粒度的拆分,做好 aar/ 源码的切换不仅能节俭 javac/kotlinCompile 的耗时,也是其余优化计划的根底。
工程配置要有区分度,不要所有子模块都用同样的配置,比方基本不会用到 kapt 性能的模块就别关上 kapt task 了。
transform 若无必要,毋庸新加,或者按级别划分,现在日头条在 debug,devMode,release 不同的构建级别用到的 transform 数量是不统一的,尽量让绝大多数人能取得绝对最快的编译速度体验,而不会被用不到的性能拖慢速度。
肯定要新增的 transform 能够先多用现有的增量计划,如 byteX 以及本文提供的类信息注入框架,尽量把不要的文件 io 合并。
很多高耗时的 官网 task(dexBuilder) 都是有间接或间接的方法晋升其效率的,并且如果除了耗时之外有其余的掂量伎俩,如本文提到的从新 dex 率,通过量化数据能够疾速的发现问题,进而找到耗时的罪魁祸首。
与 app 性能优化等工作相似,编译速度优化既须要继续进行,也须要肯定的问题发现伎俩,尽量避免问题呈现很长一段时间后再去查找起因(那时候可能业务依赖水平会十分高,难以批改)。