今日头条 Android '秒' 级编译速度优化

背景介绍

Android 项目通常使用 gradle 做为构建打包工具,而其执行速度慢也一直为人所诟病,对于今日头条 Android 项目这种千万行级别的大型工程来讲,全量编译一次的时间可能高达六七分钟,在某些须要快速验证功能的场景,改动一行代码的增量编译甚至也须要等两三分钟,这般龟速严重影响了开发体验与效率,所以针对 gradle 编译构建耗时进行优化显得尤其重要。java

在今日头条 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 ,总体的优化效果以下:android

历史优化方案

因为 18 年左右客户端基础技术相关同窗已经对今日头条 Android 工程作了许多 gradle 相关的优化,且这些优化是近期优化的基础,所以先挑选几个具备表明性的方案进行介绍,做为下文的背景同步。git

maven 代理优化 sync 时间

背景

gradle 工程每每会在 repositories 中添加一些列的 maven 仓库地址,做为组件依赖获取的查找路径,早期在今日头条的项目中配置了十几个 maven 的地址,可是依赖获取是按照 maven 仓库配置的顺序依次查找的,若是某个组件存在于最后一个仓库中,那前面的十几个仓库得依次发起网络请求查找,并在网络请求返回失败后才查找下一个,若是项目中大多组件都在较后仓库的位置,累加起来的查找时间就会很长。github

优化方案

  1. 使用公司内部搭建的 maven 私服,在私服上设置代理仓库,为其余仓库配置代理(例如 google、jcenter、mavenCentral 等仓库),代理仓库建立好后,在 Negative Cache 配置项中关闭其 cache 开关:若是查找时没有找到某版本依赖库时会缓存失败结果,一段时间内不会从新去 maven 仓库查找对应依赖库,即便 maven 仓库中已经有该版本的依赖库,查找时仍然返回失败的结果。
  2. 创建仓库组,将全部仓库归放到一个统一的仓库组里,依赖查找时只须要去这个组仓库中查找,这样能大大下降屡次发起网络请求遍历仓库的耗时。

模块 aar 化

背景

今日头条项目进行了屡次组件化和模块化的重构,分拆出了 200 多个子模块,这些子模块若是全都 include 进项目,那么在 clean build 的时候,全部子模块的代码须要从新编译,而对于大多数开发人员来讲,基本上只关心本身负责的少数几个模块,根本不须要改动其余模块的代码,这些其余 project 的配置和编译时间就成为了避免必要的代价。web

优化方案

对于以上子模块过多的解决方案是:将全部模块发布成 aar ,在项目中所有默认经过 maven 依赖这些编译好的组件,而在须要修改某个模块时,经过配置项将该模块的依赖形式改成源码依赖,作到在编译时只编译改动的模块。可是这样作会致使模块渐渐的又所有变为源码依赖的形式,除非规定每次修改完对应模块后,开发人员本身手动将模块发布成 aar ,并改回依赖形式。这种严重依赖开发人员自觉,而且在模块数量多依赖关系复杂的时候会显得异常繁琐,所以为了开发阶段的便利,设计了一整套更完整细致的方案:json

  1. 开发时,从主分支拉取的代码必定是全 aar 依赖的,除了 app 模块没有任何子模块是源码引入。
  2. 须要修改对应模块时,经过修改 local.properties 里的 INCLUDES 参数指定源码引入的模块。
  3. 开发完成后,push 代码至远端,触发代码合并流程后,在 ci 预编译过程与合码目标分支对比,检测修改的模块,将这些模块按照依赖关系依次发布成 aar ,并在工程中修改依赖为新版本的 aar, 这一步保证了每次代码合入完成后,主分支上的依赖都是全 aar 依赖的。

收益

经过上述改造,将源码模块切换成 aar 依赖后,clean build 耗时从 7,8 分钟下降至 4,5 分钟,收益接近 50%,效果显著。api

增量 java/kotlin 编译

背景

在非 clean build 的状况下,更改 java/kotlin 代码虽然会作增量编译,可是为了绝对的正确性,gradle 会根据一些列依赖关系计算,选择须要从新编译的代码,这个计算粒度比较粗,稍微改动一个类的代码,就可能致使大量代码从新执行 apt, 编译等流程。缓存

因为 gradle 做为通用框架,其设计的基本原则是绝对的正确,所以很容易致使增量编译失效,在实际开发中,为了快速编译展现结果,能够在编译正确性和编译速度上作一个折中的方案:性能优化

  1. 禁用原始的 javac/kotlinCompile 等 task, 自行实现代码增量修改判断,只编译修改的代码。
  2. 动态禁用 kapt 相关的 task, 下降 kapt,kaptGenerateStub 等 task 的耗时。

以上方案(下文所有简称为 fastbuild) 虽然在涉及常量修改,方法签名变动方面 存在必定的问题(常量内联等),可是能换来增量编译从 2 分多下降至 20~30s,极大的提高编译效率,且有问题的场景并不常见,所以总体上该方案是利大于弊的。网络

编译耗时恶化

经过上文介绍的几个优化方案和其余优化方式,在 18 年时,今日头条 Android 项目的总体编译速度(clean build 4~5min, fast 增量编译 20~30s)在同量级的大型工程中来讲是比较快的 ,然然后期随着业务发展的需求,编译脚本添加了不少新的逻辑:

  1. kotlin 大规模使用,kapt 新增了不少注解处理逻辑。
  2. 引入对 java8 语法的支持 , java8 语法的 desugar(脱糖)操做增长了编译耗时。
  3. 大量的字节码插桩需求,添加了许多 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 库。

收益

  1. 在 mac clean build 中平均有 40s 收益
  2. 在 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 ,对项目中全部子模块进行了一遍扫描:

  1. 获取 kapt configuration 的全部依赖,能够获得 kapt 依赖库的 jar 包,利用 asm 获取全部 annotation.
  2. 遍历全部 subproject 的 sourceset 下全部 .java,.kt 源文件,解析 import 信息,看是否有步骤 1 中解析的 annotation
  3. 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,尽量下降优化方案对业务的负面影响。

收益

  1. mac 全源码场景中有 58s 左右的加速收益。
  2. 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 特征比较类似,可能在一些插桩细节上有所不一样,它们大体的处理逻辑为:

  1. 在各个模块中使用 apt processor 收集模块 xx 注解的 class 信息而后生成一个 xxCollect 类,该类的做用是收集好 apt 阶段解析到的本模块的类信息
  2. 将全部模块收集到的信息进行汇总,利用 transform 阶段扫描出全部的 xxCollect 信息,经过 javaassit 或者 asm 往一个 xxCollectMgr 的某个 collectXxx 方法插桩注入以前收到的信息
  3. 业务代码可经过 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 逻辑概述

该类用来辅助获取依赖或间接依赖到变动文件的全部文件,而它的生成逻辑为: 全量或增量编译类的时候记录类型之间的依赖和被依赖关系,依赖关系的判断条件有

  1. 父类
  2. 直接实现的接口
  3. 调用 dynamic 方法指令时的返回类型

DesugaringGraph 不只记录了类依赖的类,和依赖它的类,同时也记录了一个文件路径包含了哪些类

  1. 若是文件路径是 class 文件,那路径就包含 1 个类
  2. 若是路径是 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 增量带来毁灭性的打击。 上述问题的解决方法:

  1. 把每次都会修改的 Stub 注入类和其余接口类分离,放在不一样 jar 包。(须要改造业务,比较麻烦)
  2. 动态把这个 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 相关的问题)

收益

  1. 因为大多开发场景是引入多少模块就修改多少模块内容,很难得到命中缓存, 收益很小
  2. 主要是全源码场景能稳定得到一些编译加速,基本上在 22~99s 左右。

编译耗时防恶化管控

在今日头条这种大型工程中,有不少业务部门参与开发,仅 Android 工程 开发人员就有几百人且人员变更频繁,所以内部任何一项优化工做必然是得搭配上一些管控措施的,不然一边优化一边恶化,空浪费人力。

为此制定了一些管控方案,首先是 debug 阶段的 新增 transform 管控,设置为白名单形式,若是在开发阶段新增了 transform 直接终止编译流程,经过说明文档告知管控的规则,固然,管控的目的是尽量减小一些没必要要的不合理的编译问题,并非与业务团队做对,若是某一个操做拖慢了总体的编译耗时,可是在 app 性能/稳定性方面有更大收益,且没法在编译期作更多的优化,仍然是容许添加的,只不过是得提早把这个问题暴露出来而已,能更快的找出更多的解决思路,好比引导使用 byteX 等 transform 收敛方案。

另外一方面的是合码流程方面的阻塞 : 今日头条 为了保障 app 的性能稳定性,在合码流程上设置了许多自动化的卡点: 如 包大小检测,插件依赖变动检查, so 变动检查,启动性能检测等,检测到对应问题(如包大小增长异常)会阻塞合码流程。为了管控编译速度 ,使其不至于恶化的太快,也加上了对应的 基于 task 级别的管控,当某一个 task 耗时异常波动,或者新增全新类型的 task 时,可以自动的发现问题,经过机器人将相关人员拉到 mr 讨论群中, 尽可能在 合码完成前能发现问题。

总结

为了持续稳定的保持较快的编译速度,可能须要作到如下几点:

  1. 项目须要有良好的工程结构,对业务模块进行适当粒度的拆分,作好 aar/源码的切换不只能节省 javac/kotlinCompile 的耗时,也是其余优化方案的基础。
  2. 工程配置要有区分度,不要全部子模块都用一样的配置,好比根本不会用到 kapt 功能的模块就别打开 kapt task 了。
  3. transform 若无必要,无须新加,或者按级别划分,现在日头条在 debug,devMode,release 不一样的构建级别用到的 transform 数量是不一致的,尽可能让绝大多数人能得到相对最快的编译速度体验,而不会被用不到的功能拖慢速度。
  4. 必定要新增的 transform 能够先多用现有的增量方案,如 byteX 以及本文提供的类信息注入框架, 尽可能把不要的文件 io 合并。
  5. 不少高耗时的 官方 task(dexBuilder) 都是有直接或间接的办法提高其效率的,而且若是除了耗时以外有其余的衡量手段,如本文提到的从新 dex 率,经过量化数据能够快速的发现问题,进而找到耗时的罪魁祸首。
  6. 与 app 性能优化等工做相似,编译速度优化既须要持续进行,也须要必定的问题发现手段,尽可能避免问题出现很长一段时间后再去查找缘由(那时候可能业务依赖程度会很是高,难以修改)。

更多分享

字节跳动分布式表格存储系统的演进

字节跳动自研强一致在线 KV &表格存储实践 - 上篇

字节跳动自研强一致在线 KV &表格存储实践 - 下篇

字节跳动在 Go 网络库上的实践

字节跳动-GIP-Android 平台架构团队

字节跳动-GIP-Android 平台架构团队以服务今日头条产品为主,同时协助公司其余产品,在产品性能、稳定性等用户体验,研发流程,编译优化,架构方向上不断优化和深刻探索,以知足产品快速迭代的同时,保持较高的用户体验。咱们长期招聘 Android 平台架构方向的同窗,在北京,深圳均有相关岗位,想深刻交流或者须要部门内推、投递简历的能够联系邮箱 wangshupeng@bytedance.com (标题注明 : 字节跳动-GIP 平台 android 部门直推)

欢迎关注字节跳动技术团队

相关文章
相关标签/搜索