Android 秒级编译工具 Freeline 新特性支持!

2,598 阅读13分钟
原文链接: yq.aliyun.com

前言

Freeline最早诞生之初主要是为了支持蚂蚁聚宝的应用架构(mPaaS,插件化架构)的增量编译。

蚂蚁聚宝的Android开发团队使用Windows/Linux/Mac的均有,在高配mbp上,改一次代码并编译-安装-运行,大概需要1min+。在非SSD的Windows上,耗时则大于5min。完整地编译整个工程并安装,mbp上需要大于5min,而Windows上,甚至可以达到20min+。编译耗时严重影响了整个团队的开发效率,这也催发了Freeline原型的诞生。

之前在云栖社区发布的Freeline - Android平台上的秒级编译方案主要讲的是Freeline底层的技术原理,不过完稿时间较早(16年2月份),主要都是针对mPaaS架构的内容,与在Github上开源的版本有较多不同,这里主要讲下Freeline是如何支持Gradle工程的增量编译的。

在具体展开介绍之前,先来看下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,在线浏览的链接:android.googlesource.com/platform/to…

了解到以上这些定义之后,我们就可以知道要对Android的构建流程或者其产物做修改,其实就是要去hook这些构建任务,来修改他们的input或者output,从而达到我们想要的目的。


Freeline全量编译流程

Freeline定制了自己的全量和增量的编译流程。当Freeline监测到build.gradle或者AndroidManifest.xml变化了,会自动进入全量编译。

下图是Freeline的全量编译流程图:

全量编译流程拆解:

  • 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消耗的时间),具体可以参考这个链接:developer.android.com/studio/buil…

因而,在1.5版本以后,Freeline会如图这样影响构建流程:

注意,以上流程为不开启混淆的情况。Freeline目前只支持debug buildType,并且不支持混淆。

对task进行hook之后,我们植入的hackClassesBeforeDex会对每个拿到的class或者jar通过ASM做代码注入。ASM是一个通用的Java字节码操作与分析框架。它可以直接以二进制的形式,直接修改现用的class文件。

Freeline在每个类(不继承Application)的构造函数注入了如下一段代码,其中ClassVerifier这个类来自一个独立的dex。

通过ASM的API,可以很简单地实现类的修改:

题外话,其实利用ASM还可以做非常多的额外的工作,包括各种编译期的代码生成,ASM的原理也让它不需要生成新的java文件再重新编译,而是直接修改已经存在的class文件。

当然,也有很多人会问不熟悉字节码的话,是不是学习的曲线会很大?其实不然,ASM已经提供给你现成的工具jar包,你可以利用这个工具,直接从class文件dump出ASM的代码,官方也提供了相应的问题说明:asm.ow2.org/doc/faq.htm…

举个简单的例子:

通过命令行工具,我们可以将生成上述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上:bitbucket.org/hvisser/and… 。核心代码不过百行,主要的处理逻辑在于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:github.com/alibaba/fre…