前言
Freeline最早诞生之初主要是为了支持蚂蚁聚宝的应用架构(mPaaS,插件化架构)的增量编译。
蚂蚁聚宝的Android开发团队使用Windows/Linux/Mac的均有,在高配mbp上,改一次代码并编译-安装-运行,大概需要1min+。在非SSD的Windows上,耗时则大于5min。完整地编译整个工程并安装,mbp上需要大于5min,而Windows上,甚至可以达到20min+。编译耗时严重影响了整个团队的开发效率,这也催发了Freeline原型的诞生。
在具体展开介绍之前,先来看下Freeline的开发历程以及社区中几个常见的加速构建方案的对比。
Freeline发展
- 2015年底,聚宝内部诞生IncrementalBuilder
- 2016.02 正式命名Freeline,对内发布,支持mPaaS框架
- 2016.05 阿里内部开源,支持Gradle
- 2016.08 正式对外发布
开源后主要还是在提升兼容性与持续开发新功能的阶段,靠用户的自发推广,慢慢地积累了1000+ Star,目前也已经有不少App Store排行前列的应用选择接入Freeline来改善日常开发的体验。
Freeline除了持续提高兼容性之外,也陆续支持了社区中呼声较高的retrolambda以及注解的增量编译,社区中也有第三方开发的Android Studio插件,与日常开发流程更加无缝贴合。
加速构建方案对比
先来对比一下社区中常见的几款加速编译的方案:
Instant-Run
- Pros
- Google官方支持的增量编译方案,随着Android Studio的迭代持续优化
- 相对来说更加稳定,零配置,基本无侵入性影响
- 几秒内可以完成编译,速度非常快
- Cons
- 对于可以修改的地方有局限性,具体可以参考官方文档
- 除了资源修改之外,修改Java文件会重启整个应用,从Launcher Activity重新进入,如果是在开发一个层级较深的UI页面的话,使用起来不方便
- 增量过的代码不支持debug
- 对于复杂的工程结构支持程度不高
- 不支持Kotlin
Buck/okbuck
- Pros
- Facebook出品的构建工具,支持多种语言/平台的构建
- okbuck是一个帮助gradle工程快速集成buck的工具,目前转入uber进行维护
- 多线程并发编译,充分利用缓存,近似增量编译
- 目前支持了retrolambda与注解(?)
- Cons
- 对于有历史的大型工程接入成本较高,需要较高的时间成本
- 构建过程与gradle不同,所以第一次接入可能会存在不少的问题需要解决
- 安装apk的时间耗时较久
- 不支持Kotlin
- 不支持Windows
JRebel for Android
- Pros
- 在Instant Run之前就已经存在的Android平台上的增量编译解决方案,zeroturnround有大量JVM上热部署的实践积累
- 零配置,只需安装Android Studio插件,立刻可以运行
- 相比Instant Run支持的范围广,参考链接
- 支持lambda与部分流行注解库
- 字节码层面的动态加载,理论上支持几乎所有基于JVM语言,包括Kotlin、Groovy等
- Cons
- 收费,价格较高,可以参考链接
- 只有收费版才能debug,有专门的debug工具
- crash后需要重新全量编译,单次全量编译、安装的速度非常慢
Freeline
- Pros
- 支持大多数场景的增量编译
- 支持retrolambda与注解
- 支持so动态替换
- 支持Windows/Linux/macOS
- App crash后,仍然可以通过增量编译来修复
- 大多数情况下增量编译可以在10s内完成
- Cons
- 初次接入可能存在一定的问题,需要稍微花点时间来解决
- 在简单的工程上,与其他构建方案相比,没有明显的优势
- 不支持删除带id的资源,会报错
- 不支持Kotlin
LayoutCast也是一个常用的方案,不过对多module的工程支持不足,算是一个增量编译的工具原型,通常都需要改造一下才能应用起来,因此就不加入上面的比较了。
Freeline使用(只需三步即可接入)
以下是命令行版本,以Linux开发环境为基础,Win下替换相关命令即可。
- 在根目录的
build.gradle
中添加classpath 'com.antfortune.freeline:gradle:${latest-version}'
- 在主工程(application工程)的
build.gradle
中添加apply plugin: 'com.antfortune.freeline'
./gradlew initFreeline -Pmirror
:初始化Freeline相关依赖- 日常开发:
python freeline.py
:Freeline会自动切换全量与增量编译模式python freeline.py -f
:强制进行全量编译
当然,也可以直接安装第三方插件,在Android Studio里的plugins中,搜索freeline并安装即可,就可以使用快捷键来迅速进行编译开发啦。
Freeline原理
在解析Freeline如何支持Gradle工程的增量编译前,先来回顾一下Freeline的原理。Freeline本质上是一个热补丁方案,将修过过的*.java和资源文件分别打成dex和pack,然后通过socket传输到手机上,在运行期动态加载生效。具体可以阅读Freeline - Android平台上的秒级编译方案。
类似的热补丁方案的开源实现有Nuwa,以及未开源的QQ空间的超级补丁包。蚂蚁聚宝在线上也采用类似的方案来实现热补丁,以及A/B test。
Gradle是如何构建Android工程的?
每个在Android Studio中新建的Android工程,在根目录下都会有个build.gradle文件,定义了buildscript,如下:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
其中,com.android.tools.build:gradle:2.2.0
就是Google官方提供的Gradle plugin,专门用来处理Android工程的构建流程,插件里声明了许多我们经常可以在Gradle Console中看到的task。对于Gradle Task来说,他通常都会有input和output,并且每个task前后都会有依赖。整个编译流程就像工厂流水线一样,从代码源文件开始,逐渐装配,最终生成“产品”apk。我们常见的Gradle编译任务:assemble
,其英文本意就是工业上装配的意思。
Android Gradle Plugin本身也是Android开源代码的一部分,可以在线浏览源代码[需自带梯子],目前最新的版本为2.2.0,在线浏览的链接:https://android.googlesource.com/platform/tools/base/+/gradle_2.2.0
了解到以上这些定义之后,我们就可以知道要对Android的构建流程或者其产物做修改,其实就是要去hook这些构建任务,来修改他们的input或者output,从而达到我们想要的目的。
Freeline全量编译流程
Freeline定制了自己的全量和增量的编译流程。当Freeline监测到build.gradle或者AndroidManifest.xml变化了,会自动进入全量编译。
全量编译流程拆解:
- generate-file-stat:生成当前工程java文件和资源文件的修改时间与文件大小的缓存,便于后面进行对比监测文件是否修改
- read-project-info:根据build.gradle的配置,预先生成项目描述文件,缓存在
~/.freeline/cache
- gradle-full-build-with-freeline:执行gradle命令对工程进行编译
- clean-all-cache:清除之前freeline编译遗留的所有缓存文件
- install-apk:安装生成的apk到设备上
- build-base-res:使用FreelineAapt打出基础资源包
- generate-pro-info:生成工程的依赖信息并缓存
- append-file-stat:检查是否有新增的module,如果有的话将新增的module的文件状态添加到generate-file-stat生成的缓存文件中
如何绕过verify校验?
Freeline在代码增量上采用了DexPathList植入dex的方案,已有不少文章有过相关介绍。如何绕过校验防止出现运行期crash有两种方案,一种是编译期植入代码,另一种是hook绕过。第二种方案的实现可以参考这篇文章QFix探索之路——手Q热补丁轻量级方案,Github上也有开源的实现QFix。Freeline目前采用的是第一种方案,在编译期植入另外一个dex的类。
上面已经讲到Gradle的构建流程是由一个个的task按照依赖顺序进行执行的,因此我们只要能够找到相应的task入口去hook所有javac编译生成的class文件与jar文件,并对其做出相应的修改,就可以做到运行期绕过校验。
在Android Gradle Plugin 1.5.0以前,根据是否开启multiDex,插入的task会有所变化,如图所示:
在1.5版本以后,因为引入了Transform API的概念,所以task也有了较大的变化。不仅如此,minSdkVersion是否是5.0以前的,也会影响构建流程的task,原因是Google允许开发者在开发时,通过productFlavor设置最低sdk版本为21,以此来减少编译时间(主要是减少merge dex消耗的时间),具体可以参考这个链接:https://developer.android.com/studio/build/multidex.html#dev-build
因而,在1.5版本以后,Freeline会如图这样影响构建流程:
注意,以上流程为不开启混淆的情况。Freeline目前只支持debug buildType,并且不支持混淆。
对task进行hook之后,我们植入的hackClassesBeforeDex会对每个拿到的class或者jar通过ASM做代码注入。ASM是一个通用的Java字节码操作与分析框架。它可以直接以二进制的形式,直接修改现用的class文件。
Freeline在每个类(不继承Application)的构造函数注入了如下一段代码,其中ClassVerifier这个类来自一个独立的dex。
题外话,其实利用ASM还可以做非常多的额外的工作,包括各种编译期的代码生成,ASM的原理也让它不需要生成新的java文件再重新编译,而是直接修改已经存在的class文件。
当然,也有很多人会问不熟悉字节码的话,是不是学习的曲线会很大?其实不然,ASM已经提供给你现成的工具jar包,你可以利用这个工具,直接从class文件dump出ASM的代码,官方也提供了相应的问题说明:http://asm.ow2.org/doc/faq.html#Q10
通过命令行工具,我们可以将生成上述Java代码对应的ASM生成代码。
依赖查找
Freeline的增量编译过程中,需要添加完整的依赖路径,这里的依赖就包括了Jar依赖以及资源路径依赖。同样,我们从构建任务入手。
Jar依赖
上文我们提到了Freeline会去hook编译流程,植入代码,实际上在那个步骤中我们就会拿到所有的Jar文件,只要将他们都存入List中,在编译流程结束后存入文件缓存即可,可以轻松地解决Jar依赖查找的问题。
资源依赖
通过查找源码,我们会发现在每个module的构建任务中,会有一个mergeResources的任务。实际上Android在每个module的编译流程中,会将module的资源与module依赖的aar的资源,合并到一起,并打出新的aar或者最终的apk。因此,我们也可以通过hook这个mergeResources的任务,拿到所有的资源依赖路径。
这里也有一种特殊情况需要处理,如果module没有添加任何依赖,那么这个module是不会存在mergeResources这个任务的。但是这个module同时也有可能存在一些编译期生成的资源,比如RenderScript会在编译期生成raw资源,存在build/generated/res/rs
这个路径,要注意对这种case进行处理,以免后面出现编译错误。
APT增量编译
Freeline开源后被问及最多的问题是:什么时候能够支持ButterKnife/AndroidAnnotation呢?
使用各类注解库的时候,通常都会需要依赖android-apt
这个Gradle插件。android-apt
会在编译期对javac
编译过程加入相关的APT参数,使得在Gradle的构建过程中能够动态生成代码并加入编译流程。因此,Freeline需要做的就是在全量编译流程中,去获取到APT参数,然后加入到javac的增量编译过程中即可。
android-apt
的源码开放在bitbucket上:https://bitbucket.org/hvisser/android-apt 。核心代码不过百行,主要的处理逻辑在于hook编译流程以及APT参数的拼接。
根据android-apt
的逻辑,Freeline可以从javac
编译任务中提取相关的APT参数,并保存到配置文件中。然后在增量编译的过程中,在javac过程里植入相应的参数即可。
注:在Android Gradle 2.2+开始,Android官方的Gradle插件终于提供了APT支持了。具体可以参考这篇博客。Freeline目前只支持了android-apt
插件,后续也会加入对官方APT插件的支持。
retrolambda的增量编译
跟解决APT的增量编译一样,我们也首先来翻下retrolambda的Gradle插件源码。retrolambda的原理是将JDK8编译出来的代码,翻译到低版本的Java字节码,使得开发者可以使用lambda表达式写出能够在Android设备上运行的代码。实际上我们不需要理解具体的工作原理,只需要弄清楚其Gradle插件的执行流程即可。
跟踪一下插件代码的执行流程,我们发现最后进入了RetrolambdaExec.groovy
这个类,实际上还是构造了一个可执行命令,并在javac编译流程
结束后执行,如图。
因此,我们要做的其实也非常简单,在Freeline的增量编译流程中插入一个retrolambda
的任务即可,模仿其构造参数的方法,实现一个python版本的简易插件,具体代码不再展开。
注:Freeline目前还不支持启用jack进行编译。
TODO
Freeline本质上是一个hack方案,所以还是会存在各种潜在的兼容性问题。所以,Freeline接下来还是会持续解决这些兼容性问题。
开源至今,Freeline已有来自BAT、新美大等各大公司的几十款产品接入,从大家的反馈来看,还是非常显著地提高了Android工程师们的开发效率的~
最后,欢迎感兴趣的团队接入使用,如果你也喜欢Freeline的话,欢迎给我们的项目加个star:https://github.com/alibaba/freeline