Android 编译必须得说的那些事儿

作为一名 Android 工程师,咱们每天都会阅历无数次编译。关于小项目来说,半分钟或许1,2分钟即可编译完结,而关于大型项目来说,每次编译或许需求花去一杯咖啡的时刻。或许我讲详细的数字你会更有领会,其时我在微信团队时,全量编译 Debug 包需求 5 分钟,而编译 Release 包更是要超越 15 分钟。

假如每次编译可以大幅度削减 1 分钟,对微信整个 Android 团队来说就可以节省 1200 分钟(团队 40 人 × 每天编译 30 次 × 1 分钟)。所以说优化编译速度,关于提高整个团队的开发功率是很重要的。

那应该怎么样优化编译速度呢?微信、Google、Facebook 等国内外大厂都做了哪些尽力呢?除了编译速度之外,关于编译你还有必要了解哪些常识呢?

编译

虽然咱们每天都在编译,那终究什么是编译呢?

你可以把编译简略理解为,将高档言语转化为机器或许虚拟机所能辨认的低级言语的进程。关于 Android 来说,这样的一个进程便是把 Java 或许 Kotlin 转变为 Android 虚拟机可以运转的Dalvik 字节码的进程。

编译的整一个完好的进程会触及词法剖析、语法剖析 、语义查看和代码优化等进程。关于底层编译原理感兴趣的同学,你可以应战一下编译原理的三大经典巨著:龙书、虎书、鲸鱼书。

但今日咱们的要点不是底层的编译原理,而是期望一同评论 Android 编译需求处理的问题是什么,现在又遇到了哪些应战,以及国内外大厂又给出了什么样的处理计划。

Android 编译的基础常识

不管是微信的编译优化,仍是 Tinker 项目,都触及比较多的编译相关常识,因而我在 Android 编译方面研讨颇多,经历也比较丰富。Android 的编译构建流程最重要的包含代码、资源以及 Native Library 三部分,整个流程可以参阅官方文档的构建流程图。

Android 编译必须得说的那些事儿

Gradle是 Android 官方的编译东西,它也是 GitHub 上的一个开源项目。从 Gradle 的更新日志可以正常的看到,当时这个项目还更新得十分频频,根本上每一两个月都会有新的版别。关于 Gradle,我感觉最苦楚的仍是 Gradle Plugin 的编写,还在于 Gradle 在这方面没有完善的文档,因而一般都只能靠看源码或许断点调试的办法。最近我地点的公司就准备用Gradle搞一个途径打包东西,关于项意图打包和构建进程,也是深有领会。

可是编译真实太重要了,每个公司的状况又各不相同,有必要强行造一套自己的“轮子”。现已开源的项目有 Facebook 的Buck以及 Google 的Bazel。

为何需求自己“造轮子”呢?首要有下面几个原因:

  • 一致编译东西。Facebook、Google 都有专门的团队担任编译作业,他们期望内部的一切项目都运用同一套构建东西,这儿包含 Android、Java、iOS、Go、C++ 等。编译东西的一致优化,一切项目都会获益;
  • 代码安排办理架构。Facebook 和 Google 的代码办理有一个十分特别的当地,便是整个公司的一切项目都放到同一个库房里边。因而整个库房十分巨大,所以他们也不会运用 Git。现在 Google 运用的是Piper,Facebook 是依据HG修正的,也是一种依据分布式的文件体系;
  • 极致的功用寻求。Buck 和 Bazel 的功用确实比 Gradle 更好,内部包含它们的各种编译优化。可是它们或多或少都有一些定制的滋味,例如对 Maven、JCenter 这样的外部依靠支撑的也不是太好。

Android 编译必须得说的那些事儿

“程序员最怨恨写文档,还有他人不写文档”,所以它们的文档十分少的,假如想做二次定制开发会感到很苦楚。假如你想把编译东西切换到 Buck 和 Bazel,需求下很大的决计,并且还需求细心考虑和其他上下游项意图协作。当然即便咱们不去直接运用,它们内部的优化思路也十分值得咱们学习和参阅。

Gradle、Buck、Bazel 都是以更快的编译速度、更强壮的代码优化为方针,咱们下面一块儿来看看它们做了哪些尽力。

编译速度

回想一下咱们的 Android 开发生计,在编译这件作业上面终究浪费了多少时刻和生命。正如前面我所说,编译速度对团队功率很重要。

关于编译速度,咱们最关怀的或许仍是编译 Debug 包的速度,尤其是增量编译(incremental build)的速度,咱们咱们都期望有时机可以做到愈加速速的调试。正如下图所示,咱们每次代码验证都要经过编译和装置两个进程。

Android 编译必须得说的那些事儿

此处,咱们从编译时刻和装置时刻两个纬度来看Android的编译速度。

  • 编译时刻。把 Java 或许 Kotlin 代码编译为“.class“文件,然后经过 dx 编译为 Dex 文件。关于增量编译,咱们咱们都期望编译尽或许少的代码和资源,最抱负状况是只编译改动的部分。可是由于代码之间的依靠,大部分状况这并不可行。这样一个时刻段咱们只能退而求其次,期望编译更少的模块。Android Plugin 3.0及今后的版别运用 Implementation 代替 Compile,正是为了优化依靠联系;
  • 装置时刻。咱们要先经过签名校验,校验成功后会有一大堆的文件复制作业,例如 APK 文件、Library 文件、Dex 文件等。之后咱们还需求编译 Odex 文件,这样的一个进程特别是在 Android 5.0 和 6.0 会十分耗时。关于增量编译,最好的优化是直接运用新的代码,无需从头装置新的 APK。

关于增量编译,我先来讲讲 Gradle 的官方计划Instant Run。在 Android Plugin 2.3 之前,它运用的 Multidex 完成。在 Android Plugin 2.3 之后,它运用 Android 5.0 新增的 Split APK 机制。

如下图所示,资源和 Manifest 都放在 Base APK 中, 在 Base APK 中代码只要 Instant Run 结构,运用的自身的代码都在 Split APK 中。

Android 编译必须得说的那些事儿

Instant Run 有三种方式,假如是热交流和温交流,咱们都无需从头装置新的 Split APK,它们的差异在于是否重启 Activity。关于冷交流,咱们该经过adb install-multiple -r -t从头装置改动的 Split APK,运用也需求重启。

虽然不管哪一种方式,咱们都不需求从头装置 Base APK。这让 Instant Run 看起来是不是很不错,可是在大型项目里边,它的功用仍然十分糟糕,底子原因是:

  • 多进程问题。“The app was restarted since it uses multiple processes”,假如运用存在多进程,热交流和温交流都不能收效。由于大部分运用都会存在多进程的状况,Instant Run 的速度也就大打折扣。
  • Split APK 装置问题。虽然 Split APK 的装置不会生成 Odex 文件,可是这儿仍然会有签名校验和文件复制(APK 装置的乒乓机制)。这个时刻需求几秒到几十秒,是不能承受的。
  • Javac 问题。在 Gradle 4.6 之前,假如项目中运用了 Annotation Processor。那不好意思,本次修正以及它依靠的模块都需求全量 javac,而这样的一个进程是十分慢的,或许会需求几十秒。这样的一个问题直到Gradle 4.7才处理,关于这样的一个问题原因的评论你可以参阅这个Issue。

你还可以看看这一个 Issue:“full rebuild if a class contains a constant”,假定修正的类中包含一个“public static final”的变量,那相同也不好意思,本次修正以及它依靠的模块都需求全量 javac。这是怎么回事呢?由于常量池是会直接把值编译到其他类中,Gradle 并不知道有哪些类或许运用了这个常量。

问询 Gradle 的作业人员,他们出给的处理计划是下面这个:

// 本来的常量界说:
public static final int MAGIC = 23

// 将常量界说替换成办法:
public static int magic() {
return 23;
}
关于大型项目来说,这肯定是不可行的。正如我在 Issue 中所写的相同,不管咱们是不是真实改到这个常量,Gradle 都会无脑的全量 javac,这样肯定是不对的。事实上,咱们你们可以终究靠比对这次代码修正,看看是否有真实改动某一个常量的值。

可是或许用过阿里的Freeline或许蘑菇街的极速编译的同学会有疑问,它们的计划为什么不会遇到 Annotation 和常量的问题?

事实上,它们的计划在大部分状况比 Instant Run 更快,那是由于献身了正确性。也便是说它们为了寻求更快的速度,直接疏忽了 Annotation 和常量改动或许带来过错的编译产品。Instant Run 作为官方计划,它优先确保的是 100% 的正确性。

当然 Google 的人也发现了 Instant Run 的种种问题,在 Android Studio 3.5 之后,关于 Android 8.0 今后的设备将会运用新的计划“Apply Changes”代替 Instant Run。现在我还没找到关于这套计划更多的材料,不过我认为应该是扔掉了 Split APK 机制。

一直以来,我心目中都有一套抱负的编译计划,这套计划装置的 Base APK 仍然仅仅一个壳 APK,真实的事务代码放到 Assets 的 ClassesN.dex 中,它的架构图如下。

Android 编译必须得说的那些事儿

  • 无需装置。仍然运用相似 Tinker 热修正的办法,每次只把修正以及依靠的类刺进到 pathclassloader 的最前方就可以,不熟悉的同学可以参阅《微信 Android 热补丁实践演进之路》中的 Qzone 计划;
  • Oatmeal。为了处理初次运转时 Assets 中 ClassesN.dex 的 Odex 耗时问题,咱们你们可以运用“装置包优化“中讲过的 ReDex 中的黑科技:Oatmeal。它可以在 100 毫秒以内生成一个彻底解说履行的 Odex 文件;
  • 封闭JIT。咱们经过在 AndroidManifest 中添加android:vmSafeMode=“true”来封闭虚拟机的 JIT 优化,这样也就不会呈现 Tinker 在Android N 混合编译遇到的问题。

关于编译速度的优化,我还有几个主张:

  • 替换编译机器。关于实力雄厚的公司,直接替换 Mac 或许其他更给力的设备作为编译机,这种办法是最简略的;
  • Build Cache。可以将大部分不常改动的项目拆离出去,并运用远端 Cache方式保存编译后的缓存;
  • 晋级 Gradle 和 SDK Build Tools。咱们该及时去晋级最新的编译东西链,享用 Google 的最新优化作用;
  • 运用 Buck。不管是 Buck 的 exopackage,仍是代码的增量编译,Buck 都愈加高效。但我前面也说过,一个大型项目假如要切换到 Buck,其实顾忌仍是比较多的。在 2014 年头微信就接入了 Buck,可是由于跟其他项目协作的问题,导致在 2015 年切换回 Gradle 计划。

相比之下,或许现在最热的 Flutter 中Hot Reload秒级编译功用会更有吸引力。

当然最近几个 Android Studio 版别,Google 也做了许多的其他优化,例如运用AAPT2代替了 AAPT 来编译 Android 资源。AAPT2 完成了资源的增量编译,它将资源的编译拆分红 Compile 和 Link 两个进程。前者资源文件以二进制方式编译 Flat 格局,后者兼并一切的文件再打包。

除了 AAPT2,Google 还引进了 d8 和 R8,下面别离是 Google 供给的一些测验数据,如下图。

Android 编译必须得说的那些事儿

Android 编译必须得说的那些事儿

那什么是 d8 和 R8 呢?除了编译速度的优化,它们还有哪些其他的作用?可以参阅下面的介绍:Android D8 和 R8

代码优化

关于 Debug 包编译,咱们更关怀速度。可是关于 Release 包来说,代码的优化愈加重要,由于咱们会愈加介意运用的功用。

下面我就别离讲讲 ProGuard、d8、R8 和 ReDex 这四种咱们或许会用到的代码优化东西。

ProGuard

在微信 Release 包 12 分钟的编译进程里,独自 ProGuard 就需求花费 8 分钟。虽然 ProGuard 真的很慢,可是根本每个项目都会运用到它。加入了 ProGuard 之后,运用的构建进程流程如下:

Android 编译必须得说的那些事儿

ProGuard 首要有混杂、裁剪、优化这三大功用,它的整个处理流程如下:

Android 编译必须得说的那些事儿

其间优化包含内联、修饰符、兼并类和办法等 30 多种,详细介绍与运用办法你可以参阅官方文档。

D8

Android Studio 3.0 推出了d8,并在 3.1 正式成为默许东西。它的作用是将“.class”文件编译为 Dex 文件,替代之前的 dx 东西。

Android 编译必须得说的那些事儿

d8 除了更快的编译速度之外,还有一个优化是削减生成的 Dex 巨细。依据 Google 的测验成果,大约会有 3%~5% 的优化。

Android 编译必须得说的那些事儿

R8

R8 在 Android Studio 3.1 中引进,它的志趣愈加高远,它的方针是替代 ProGuard 和 d8。咱们你们可以直接运用 R8 把“.class”文件变成 Dex。

Android 编译必须得说的那些事儿

一起,R8 还支撑 ProGuard 中混杂、裁剪、优化这三大功用。由于现在 R8 仍然处于试验阶段,网上的介绍材料并不多,你可以参阅下面这些材料:

ProGuard 和 R8 比照:

ProGuard and R8: a comparison of optimizers。

Jake Wharton 大神的博客最近有许多 R8 相关的文章:https://jakewharton.com/blog/。

R8 的终究意图跟 d8 相同,一个是加速编译速度,一个是更强壮的代码优化。

ReDex

假如说 R8 是未来想替代的 ProGuard 的东西,那 Facebook 的内部运用的ReDex其完成已做到了。Facebook 内部的许多项目都现已悉数切换到 ReDex,不再运用 ProGuard 了。跟 ProGuard 不同的是,它直接输入的对象是 Dex,而不是“.class”文件,也便是它是直接针对终究产品的优化,所见即所得。

在前面的文章中,我现已不止一次说到 ReDex 这个项目,由于它里边的功用真实是太强壮了,详细可以参阅专栏前面的文章《包体积优化(上):怎么削减装置包巨细?》。

Interdex:类重排和文件重排、Dex 分包优化;
Oatmeal:直接生成的 Odex 文件;
StripDebugInfo:去除 Dex 中的 Debug 信息
此外,ReDex 中例如Type Erasure和去除代码中的Aceess 办法也是适当的好的功用,它们不管对包体积仍是运用的运转速度都有协助,因而我也鼓舞你去研讨和实践一下它们的用法和作用。

可是 ReDex 的文档也是万年不更新的,并且里边掺杂了一些 Facebook 内部定制的逻辑,所以它用起来确实十分不方便。现在我首要仍是直接研讨它的源码,参阅它的原理,然后再直接独自完成。

事实上,Buck 里边其实也还有许多好用的东西,可是文档里边仍然什么都没有说到,所以仍是需求“read the source code”。

Library Merge 和 Relinker

多言语拆分
分包支撑
ReDex 支撑

继续交给

Gradle、Buck、Bazel 它们代表的都是狭义上的编译,我认为广义的编译应该包含打包构建、Code Review、代码工程办理、代码扫描等流程,也便是业界最近常常提起的继续集成。

Android 编译必须得说的那些事儿

现在最常用的继续集成东西有 Jenkins、GitLab CI、Travis CI 等,GitHub 也有供给自己的继续集成服务。每个大公司都有自己的继续集成计划,例如腾讯的 RDM、阿里的摩天轮、群众点评的MCI等。

下面我来简略讲一下我对继续集成的一些经历和观点:

  • 自界说代码查看。每个公司都会有自己的编码标准,代码查看的意图在于避免不符合标准的代码提交到长途库房中。比方微信就界说了一套代码标准,并且写了专门的插件来检测。例如日志标准、不能直接运用 new Thread、new Handler 等,并且违反者将会得到必定的赏罚。自界说代码检测可以终究靠彻底自己完成或许扩展 Findbugs 插件,例如美团它们就使用 Findbugs 完成了Android 缝隙扫描东西 Code Arbiter;
  • 第三方代码查看。业界比较常用的代码扫描东西有收费的 Coverity,以及 Facebook 开源的Infer,例如空指针、多线程问题、资源走漏等许多问题都可以扫描出来。除了添加检测流程,我最大的领会是需求一起添加人员的训练。我遇到许多开发者为了处理扫描出来的问题,空指针就直接判空、多线程就直接加锁,最终或许会形成愈加严峻的问题;
  • Code Review。关于 Code Review,集成 GitLab、Phabricator 或许 Gerrit 都是不错的挑选。咱们肯定要注重 Code Review,这也是给其他人展现咱们“巨大”代码的时机。并且咱们自己应该是第一个 Code Reviewer,在给他人 Review 之前,自己先以第三者的视点审视一次代码。这样先经过自己这一关的检测,既尊重了他人的时刻,也可认为自己建立杰出的技能品牌。
    继续集成触及的流程有许多,你需求结合自己团队的现状。假如仅仅一味地去添加流程,有时候或许拔苗助长。

总结

在 Android 8.0,Google 引进了Dexlayout库完成类和办法的重排,Facebook 的 Buck 也第一时刻引进了 AAPT2。ReDex、d8、R8 其实都是相得益彰,可以正常的看到 Google 也在吸取社区的常识,但一起咱们也会从 Google 的新技能开展里寻求思路。

我在写今日的内容时还有别的一个领会,Google 为了处理 Android 编译速度的问题,花了许多的力气成果却不尽善尽美。我想说假如咱们勇于跳出体系的限制,或许才会彻底处理这样的一个问题,正如在 Flutter 上面就可以完美完成秒级编译。其实做人、干事也是如此,咱们常常会堕入部分最优解的困局,或许走进“思想怪圈”,这时假如能跳出途径依靠,从更高的维度从头考虑、审视大局,得到的领会或许会彻底不相同。