增量编译简介
增量编译是相对全量编译而言的。所谓增量编译,是指当源程序的局部发生变更后进重新编译的工作只限于修改的部分及与之相关部分的内容,而不需要对全部代码进行编译。增量编译对软件开发,尤其是在调试期,可以大大缩短编译时间, 提高编译效率。
而全量编译指的是,当用户源程序被局部修改后重新编译代码会涉及全部源代码,并不只限于局部修改及其相关部分。换句话说,无论修改了什么,全量编译都将进行一次全新的完整的编译,并不基于上一次的编译基础。
一般来说,在软件开发中,全量编译用于版本的构建与发布,比较耗费时间和资源。而处于调试阶段的程序,一般都采用增量编译,这样对于问题的定位和解决都比省时省力。在Android开发中,随着工程代码量膨胀,编译耗时也越来越长,拖慢了开发效率,因此Android官方推出了Instant Run和Apply Changes等增量更新的方案。
Instant Run简介
Instant Run是Android Studio 2.0版本推出的一个增量编译功能,使用Instant Run功能时,需要在build.gradle 文件中将 minSdkVersion 设置为 15 或以上时,并且为另外获得最佳性能,可以将 minSdkVersion 设置为 21 或更高。
之前在Android Studio 3.0版本,gradle为2.14.1的版本中做过一个测试,编译一个简单的Demo项目从之前的10秒降低到大概2、3秒。默认情况下,Instant Run是关闭的,如果要开启Instant Run,可以在Settings中打开Instant Run,如需所示。
关于Instant Run的一些原理方面的内容,可以参考我之前的文章介绍:深入理解Android Instant Run运行机制。
Apply Changes
在Android Studio 3.5及其以上版本,官方提供了Apply Changes,使用Apply Changes时,需要满足以下两个条件:
- apk必须是debug包;
- Android 8.0及以上的手机上运行
当我们使用Android Studio运行项目后,会在菜单栏看见3个按钮,分别用来控制应用重启,如下图所示。
如上图所示,从左到右的按钮分别表示【Run】、【Apply Changes 】和【Apply Code Changes】。
- Run:将部署所有的变化并重启应用 。
- Apply Changes: 将尝试应用变化的资源和代码,并仅重启Activity而不需要重启整个应用。
- Apply Code Changes :将尝试在不重启操作的情况下应用变化的代码,如果只有代码修改,可以使用此按钮来使代码生效。
不过,由于Apply Changes仅支持在Android 8.0 或者更高版本的手机上运行,并且实际操作时在工程中带来的提速效果也不明显。
Freeline
除了官方的方案外,阿里巴巴客户端团队还基于动态替换研发了一款针对Android平台的增量编译工具,它可以充分利用缓存文件,在几秒钟内迅速地对代码的改动进行编译并部署到设备上,有效地减少了日常开发中的大量重新编译与安装的耗时。
性能方面:内部采用了类似Facebook的开源工具buck的多工程多任务并发思想:端口扫描,代码扫描,并发编译, 并发dx,并发merge dex等策略,在多核机器上有明显加速效果,另外在class及dex,resources层面作了相应缓存策略,做到真正增量开发,另外引入并优化 buck的部分加速组件dx,DexMerger,资源编译方面,深入改造了Aapt资源编译流程,当资源发生改变时候,秒级完成增量包编译,其中增量包 仅含最小的变更集合(10Kb~数百Kb内),后期也被运用到线上进行资源/代码动态替换。相比目前instant- run,buck,layoutcast等方案快数倍速度。
不过,Freeline同样存在着一些不可忽视的问题。首先是不支持Kotlin,这在Kotlin已经被谷歌官宣为Android开发首选语言的今天,是比较致命的。另外,不支持删除带id的资源,否则可能导致资源编译流程出错。
另外一个潜在的问题是,为了确保编译速度,Freeline是牺牲了一部分正确性的。例如,在改动公有静态常量的时候,只会编译对应的类文件,而引用到该常量的其他类,并不会参与编译的。由于常量内联优化的存在,就可能导致这些类在运行时,使用的仍然是旧的值,进而出现改动不生效的问题。
Android 编译打包流程
对于Android是如何从源码到安装包的过程,可以参考Android官方给的一幅图,主要会经历编译、链接和签名等操作。
对于编译阶段,首先是收集工程中的所有资源文件进行编译,得到资源包以及资源索引类。随后资源索引类会跟随工程的所有源代码文件一起被编译为字节码文件,并且字节码文件还需要被进一步编译为Dex文件,这样才能被Android虚拟机所识别。
Android的编译打包会分为以下几个阶段:
- R文件的生成:R文件记录了每个资源的ID,之后要参与到java的编译过程,R文件是由aapt(Android Asset Package Tool)生成。
- Java(Kotlin)源代码:我们知道有时app开发中会跨进程通信,这时可以通过aidl的方式定义接口,aidl工具可以根据aidl文件生成对应的java文件。之后R文件、aidl相关java文件、src中的java文件通过编译生成 .class文件。
- dex生成:编译后的.class会又由dex工具打包成dex文件,其中,Android增量打包工具freeline中用到了Buck中提取的dex工具,freeline给出的数据是比原生的dex工具快了40%
资源文件编译
- aapt(Android Asset Package Tool):aapt工具对app中的资源文件进行打包和归档。
增量编译原理
Android增量编译分为代码增量和资源增量,Android早期的Instant Run方案在资源上并不是增量的,而是把整个应用的资源打成资源包,推送至手机的,因此效率极低。
代码编译
谷歌在支持multidex之后(即典型的65535问题),Android打包后会存在多个dex文件,运行时加载类时,会从一个dexList依次查找,找到则返回,利用这个原理可以把增量的代码打包成dex文件,插入到dexList的前边,这样就可以完成类的替换。
关于代码的增量编译需要考虑两个主要的问题,即获取改动文件并进行编译、对依赖的代码进行编译。关于代码的增量编译,可以参考QQ音乐的增量编译方案:QQ音乐Android编译提速之路
资源编译
资源编译与代码增量是类似的,即先收集被改动的资源,然后进行编译。Android的资源编译主要使用的是aapt或者aapt2。一般来说现在都是使用aapt2来进行资源的打包编译,因为aapt工具是不支持单个资源编译的。
aapt2(Android 资源打包工具)是Android Studio 和 Android Gradle 插件使用它来编译和打包应用的资源构建工具。aapt2 会解析资源、为资源编制索引,并将资源编译为针对 Android 平台进行过优化的二进制格式。
使用aapt2进行资源打包编译时,分为编译(compile)与链接(link) 两步,在编译阶段,负责将单个或者多个资源编译为二进制文件;链接阶段,则负责合并所有二进制文件再打包。
关于资源的增量编译,可以参考QQ音乐的增量编译方案:QQ音乐Android编译提速之路
有赞 Android 编译方案Savitar
当项目经过多次迭代之后,就会遇到各种各样的问题,而编译慢是每个成熟 Android 团队都无法回避的问题。在之前有赞零售 Android 团队的技术分享中,整个Android项目有 25 个业务模块,拥有 45W+ 行源代码(Java + Kotlin)以及多个构建 Flavor。小伙伴在进行需求开发时,平均的增量编译构建时间达到了两分钟,再加上一些 Gradle 配置与APK安装过程,基本上验证一行代码的修改需要近三分钟(MacBook Pro 13-inch, 2016, i5-8G),这样的情况大大降低了团队的开发效率。
在 Savitar 诞生之前,我们尝试了社区中一些成熟的解决方案,如 BUCK、Freeline、InstantRun 等知名框架。不过调研下来,都或多或少的存在一些问题。
比如FaceBook的BUCK框架,自身有强大的构建系统,通过增量构建缓存机制,可以有效提升编译的速度,但是其使用和配置过于复杂,对于工程的入侵比较大,且对于一些 Databinding、 Kotlin 等 Android 的特性支持还有欠缺。
其次是阿里巴巴开源的Freeline ,Freeline以其极快的部署速度出名,但对我们来说致命缺点是不支持 Kotlin。
InstantRun 是 Google 推荐的加速方式,拥有最全面的支持性,但由于我们是多进程的工程,并且 InstantRun 在编译时的一些准备 Task 也会消耗一些时间,在实践过程中发现加速并不明显。
方案实现
Savitar 是有赞 Android 团队增量编译提效方案,它能够有效减少模块修改编译时间,包含配套 IDE 插件,使用方便,具有如下一些显著的特点。
- 支持Java、Kotlin的增量编译
- 支持layout、values、assets、images等资源文件的增量编译
- 提供GUI界面插件
- 基于 Git调试、多分支管理,可以根据实际情况更换分支
结构设计
如图所示,Savitar 整体分成四个部分:
- GUI 插件部分:面向使用者的 GUI 界面,内部包含了可运行 Jar(以下简称 Runner)的自动更新、各种检查任务、编译脚本调用执行
- Runner 部分:一个 Jar 包,包含 Savitar 核心逻辑代码,完成修改获取、脚本生成、编译执行等任务
- 工程支持部分:一个 Gradle 插件,完成对工程信息的获取和产物加载代码的插入
- 外部依赖部分:完成整个流程所需要的外部依赖程序
下图演示了Savitar从代码修改到完成修改产物加载运行的完整过程。
可以发现,从代码修改到完成修改产物主要经历了以下几个步骤:
- 获取改动信息:获取代码和资源修改,是整个过程的前提
- 获取工程信息:获取当前工程的依赖信息,目录信息和 Git 信息,为后续编译做准备
- 编译生成产物:进行代码、资源编译,生成 Dex 产物和 Apk 产物
- 重启加载产物:完成对编译产物的加载运行,完成整个加速过程
关于Savitar是如何从获取改动信息到完成加载,可以参考Android 增量编译提效方案Savitar的详细介绍。
如何使用
为了方便开发者使用Savitar实现增量更新,Savitar 开发了一款 IDE 插件,只需要一键触发就可以完成整个编译打包流程。首先,打开Android Studio ,然后依次选择 【Preference】 -> 【Plugin】 搜索Savitar并安装,如下图所示。
常见文件解答
是否支持,如何支持Kotlinx ?
如下所示,有下面一段代码:
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonCpu.setOnClickListener {
... //点击事件
}
}
}
对于上面的代码,想必使用过 Kotlin 的 Android 同学并不会陌生,利用 Kotlinx 特性,可以在 .kt 代码中使用 Xml 中定义过组件Id直接获取 View 实例进行操作,极大减少 UI 开发成本。
但是上面代码中的 import 并不是一个普通的形式,这样的语法如果直接使用标准 kotlinc 进行编译,会出现找不到 import 错误。此时需要借助到 Kotlin 编译器插件,在 Kotlin 编译时传入 Kotlinx 对应插件的 Jar 地址和参数,就可以完成包含 Kotlinx 语法的文件编译。
sh kotlinc
-Xplugin=lib/android-extensions-compiler.jar
-P plugin:org.jetbrains.kotlin.android:package=${package_name}
-P plugin:org.jetbrains.kotlin.android:variant='${flovar};${resource_package}'
文档参考 Kotlin 编译器插件
Kotlinc 环境变量
在使用 Android Studio 开发过程中,Kotlin 编译所需的依赖包都是由 IDE 自动管理,但是 Savitar 是使用 Shell 实现,这样的情况下面就需要关心这个编译工具的问题了。我们将获取 Kotlin 编译依赖的逻辑放在 Savitar 运行环境检测逻辑中,在检测到没有依赖包的情况下会自动从内网服务器下载对应版本的库,完成 Kotlin 代码编译。