本文来自张绍文老师的《Android开发高手课》,我把我认为比较好的文章整理分享给你们。html
做为一名 Android 工程师,咱们天天都会经历无数次编译。对于小项目来讲,半分钟或者1,2分钟便可编译完成,而对于大型项目来讲,每次编译可能须要花去一杯咖啡的时间。可能我讲具体的数字你会更有体会,当时我在微信团队时,全量编译 Debug 包须要 5 分钟,而编译 Release 包更是要超过 15 分钟。java
若是每次编译能够减小 1 分钟,对微信整个 Android 团队来讲就能够节约 1200 分钟(团队 40 人 × 天天编译 30 次 × 1 分钟)。因此说优化编译速度,对于提高整个团队的开发效率是很是重要的。android
那应该怎么样优化编译速度呢?微信、Google、Facebook 等国内外大厂都作了哪些努力呢?除了编译速度以外,关于编译你还须要了解哪些知识呢?git
虽然咱们天天都在编译,那到底什么是编译呢?
你能够把编译简单理解为,将高级语言转化为机器或者虚拟机所能识别的低级语言的过程。对于 Android 来讲,这个过程就是把 Java 或者 Kotlin 转变为 Android 虚拟机可以运行的Dalvik 字节码的过程。程序员
编译的整个过程会涉及词法分析、语法分析 、语义检查和代码优化等步骤。对于底层编译原理感兴趣的同窗,你能够挑战一下编译原理的三大经典巨做:龙书、虎书、鲸鱼书。github
但今天咱们的重点不是底层的编译原理,而是但愿一块儿讨论 Android 编译须要解决的问题是什么,目前又遇到了哪些挑战,以及国内外大厂又给出了什么样的解决方案。android-studio
不管是微信的编译优化,仍是 Tinker 项目,都涉及比较多的编译相关知识,所以我在 Android 编译方面研究颇多,经验也比较丰富。Android 的编译构建流程主要包括代码、资源以及 Native Library 三部分,整个流程能够参考官方文档的构建流程图。缓存
Gradle是 Android 官方的编译工具,它也是 GitHub 上的一个开源项目。从 Gradle 的更新日志能够看到,当前这个项目还更新得很是频繁,基本上每一两个月都会有新的版本。对于 Gradle,我感受最痛苦的仍是 Gradle Plugin 的编写,主要是由于 Gradle 在这方面没有完善的文档,所以通常都只能靠看源码或者断点调试的方法。最近我所在的公司就准备用Gradle搞一个渠道打包工具,对于项目的打包和构建过程,也是深有体会。微信
可是编译实在过重要了,每一个公司的状况又各不相同,必须强行造一套本身的“轮子”。已经开源的项目有 Facebook 的Buck以及 Google 的Bazel。多线程
为何要本身“造轮子”呢?主要有下面几个缘由:
“程序员最痛恨写文档,还有别人不写文档”,因此它们的文档也是比较少的,若是想作二次定制开发会感到很痛苦。若是你想把编译工具切换到 Buck 和 Bazel,须要下很大的决心,并且还须要考虑和其余上下游项目的协做。固然即便咱们不去直接使用,它们内部的优化思路也很是值得咱们学习和参考。
Gradle、Buck、Bazel 都是以更快的编译速度、更强大的代码优化为目标,咱们下面一块儿来看看它们作了哪些努力。
回想一下咱们的 Android 开发生涯,在编译这件事情上面究竟浪费了多少时间和生命。正如前面我所说,编译速度对团队效率很是重要。
关于编译速度,咱们最关心的可能仍是编译 Debug 包的速度,尤为是增量编译(incremental build)的速度,咱们但愿能够作到更加快速的调试。正以下图所示,咱们每次代码验证都要通过编译和安装两个步骤。
此处,咱们从编译时间和安装时间两个纬度来看Android的编译速度。
对于增量编译,我先来说讲 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 中。
Instant Run 有三种模式,若是是热交换和温交换,咱们都无需从新安装新的 Split APK,它们的区别在因而否重启 Activity。对于冷交换,咱们须要经过adb install-multiple -r -t
从新安装改变的 Split APK,应用也须要重启。
虽然不管哪种模式,咱们都不须要从新安装 Base APK。这让 Instant Run 看起来是否是很不错,可是在大型项目里面,它的性能依然很是糟糕,主要缘由是:
你还能够看看这一个 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:vmSafeMode=“true”
来关闭虚拟机的 JIT 优化,这样也就不会出现 Tinker 在Android N 混合编译遇到的问题。对于编译速度的优化,我还有几个建议:
相比之下,可能目前最热的 Flutter 中Hot Reload秒级编译功能会更有吸引力。
固然最近几个 Android Studio 版本,Google 也作了大量的其余优化,例如使用AAPT2替代了 AAPT 来编译 Android 资源。AAPT2 实现了资源的增量编译,它将资源的编译拆分红 Compile 和 Link 两个步骤。前者资源文件以二进制形式编译 Flat 格式,后者合并全部的文件再打包。
除了 AAPT2,Google 还引入了 d8 和 R8,下面分别是 Google 提供的一些测试数据,以下图。
那什么是 d8 和 R8 呢?除了编译速度的优化,它们还有哪些其余的做用?能够参考下面的介绍:Android D8 和 R8
对于 Debug 包编译,咱们更关心速度。可是对于 Release 包来讲,代码的优化更加剧要,由于咱们会更加在乎应用的性能。
下面我就分别讲讲 ProGuard、d八、R8 和 ReDex 这四种咱们可能会用到的代码优化工具。
在微信 Release 包 12 分钟的编译过程里,单独 ProGuard 就须要花费 8 分钟。尽管 ProGuard 真的很慢,可是基本每一个项目都会使用到它。加入了 ProGuard 以后,应用的构建过程流程以下:
ProGuard 主要有混淆、裁剪、优化这三大功能,它的整个处理流程以下:
其中优化包括内联、修饰符、合并类和方法等 30 多种,具体介绍与使用方法你能够参考官方文档。
Android Studio 3.0 推出了d8,并在 3.1 正式成为默认工具。它的做用是将“.class”文件编译为 Dex 文件,取代以前的 dx 工具。
d8 除了更快的编译速度以外,还有一个优化是减小生成的 Dex 大小。根据 Google 的测试结果,大约会有 3%~5% 的优化。
R8 在 Android Studio 3.1 中引入,它的志向更加高远,它的目标是取代 ProGuard 和 d8。咱们能够直接使用 R8 把“.class”文件变成 Dex。
同时,R8 还支持 ProGuard 中混淆、裁剪、优化这三大功能。因为目前 R8 依然处于实验阶段,网上的介绍资料并很少,你能够参考下面这些资料:
ProGuard 和 R8 对比:ProGuard and R8: a comparison of optimizers。
Jake Wharton 大神的博客最近有不少 R8 相关的文章:https://jakewharton.com/blog/。
R8 的最终目的跟 d8 同样,一个是加快编译速度,一个是更强大的代码优化。
若是说 R8 是将来想取代的 ProGuard 的工具,那 Facebook 的内部使用的ReDex其实已经作到了。Facebook 内部的不少项目都已经所有切换到 ReDex,再也不使用 ProGuard 了。跟 ProGuard 不一样的是,它直接输入的对象是 Dex,而不是“.class”文件,也就是它是直接针对最终产物的优化,所见即所得。
在前面的文章中,我已经不止一次提到 ReDex 这个项目,由于它里面的功能实在是太强大了,具体能够参考专栏前面的文章《包体积优化(上):如何减小安装包大小?》。
此外,ReDex 中例如Type Erasure和去除代码中的Aceess 方法也是很是不错的功能,它们不管对包体积仍是应用的运行速度都有帮助,所以我也鼓励你去研究和实践一下它们的用法和效果。
可是 ReDex 的文档也是万年不更新的,并且里面掺杂了一些 Facebook 内部定制的逻辑,因此它用起来的确很是不方便。目前我主要仍是直接研究它的源码,参考它的原理,而后再直接单独实现。
事实上,Buck 里面其实也还有不少好用的东西,可是文档里面依然什么都没有提到,因此仍是须要“read the source code”。
ReDex 支持
Gradle、Buck、Bazel 它们表明的都是狭义上的编译,我认为广义的编译应该包括打包构建、Code Review、代码工程管理、代码扫描等流程,也就是业界最近常常提起的持续集成。
目前最经常使用的持续集成工具备 Jenkins、GitLab CI、Travis CI 等,GitHub 也有提供本身的持续集成服务。每一个大公司都有本身的持续集成方案,例如腾讯的 RDM、阿里的摩天轮、大众点评的MCI等。
下面我来简单讲一下我对持续集成的一些经验和见解:
持续集成涉及的流程有不少,你须要结合本身团队的现状。若是只是一味地去增长流程,有时候可能拔苗助长。
在 Android 8.0,Google 引入了Dexlayout库实现类和方法的重排,Facebook 的 Buck 也第一时间引入了 AAPT2。ReDex、d八、R8 其实都是相辅相成,能够看到 Google 也在摄取社区的知识,但同时咱们也会从 Google 的新技术发展里寻求思路。
我在写今天的内容时还有另一个体会,Google 为了解决 Android 编译速度的问题,花了大量的力气结果却不尽如人意。我想说若是咱们勇于跳出系统的制约,可能才会完全解决这个问题,正如在 Flutter 上面就能够完美实现秒级编译。其实作人、作事也是如此,咱们常常会陷入局部最优解的困局,或者走进“思惟怪圈”,这时若是能跳出路径依赖,从更高的维度从新思考、审视全局,获得的体会可能会彻底不同。