在”阿里开源项目最佳实践“上,蚂蚁金服客户端开发工程师黄咏分享了Freeline整个的开源历程和变化,他从不同的角度讲述了Freeline整个技术底层的原理,以及编译加速方案的对比,并分享了Freeline整个开源以来的收获和体会。
以下内容根据现场分享和幻灯片整理而成。
Freeline是非常快速的编译工具,其诞生主要为了迎合现在工程的需要,实现更好的动态化。Freeline最早诞生之初主要是为了支持蚂蚁聚宝的应用架构(mPaaS,插件化架构)的增量编译,它于2016年8月在Alibaba Github上开源,目前为止已经累计了3244个Star,目前是Alibaba Github下排行前十的开源项目,已有上千款应用接入使用Freeline,可能是东半球用户最多的第三方编译工具。
为什么选择Freeline?
目前,市面上类似Freeline的工具有很多,如Google官方的Instant Run、Facebook-buck、JRebel for Android等,那么Freeline和这些工具相比究竟有什么优势呢?下面来一探究竟。
Instant Run
首先介绍一下Google官方的Instant Run,其优点是Android Studio随身携带,下载之后,默认打开全部功能,零配置,相对其他方案最为稳定,基本无侵入性影响。Instant Run可以在1s或2s之内完成所有的编译效果。它的缺点是:首先对增量编译的支持有局限性,无法使所有的代码修改都支持增量编译,尤其是跨模块修改。比如现在一些大型的Android工程,不会是单个模块,而是数十个以上,如果工程师跨越了2个或3个Module来修改,同时进行编译,一般来说Instant Run只进行全量编译,因为目前不支持这样的修改情况;其次,修改Java文件会对整个Application进行重启,比如说要修改一个页面,可能是4-5个层级,进入一个较深的层级后,使用Instant Run就有可能使APP回到最初始的页面,又要重新花4-5个步骤,进入到深层级;另外,Instant Run在ASM植入时,植入一段自己的代码,当增量修改之后,无法进行Debug,带来调试困难;Instant Run无法支持复杂的工程结构(主要指非常复杂,十几个模块以上的工程),也不支持Kotlin与Jack。
Facebook–Buck/Uber-Okbuck
Facebook-Buck是Facebook出品的,其内部统一使用的构建系统。Facebook整个后端的服务端工程包括iOS工程或安卓工程,基本上都统一由Buck作为构建系统来统一构建,但使用Buck要做一些额外的系统配置,这对于几乎没有接触过Buck的工程师来说,门槛相对较高。Okbuck是一个帮助Gradle工程快速集成Buck的开源工具,目前由Uber维护。Buck一个核心的出发点是多线程开发编译,充分利用缓存,Buck通过对一个模块拆分,拆分成各种小Module,近似达到增量编译效果、加速效果。而且其效果非常好,一般十几秒就可以完成编译。并且,Buck也已经支持目前非常流行的Retrolambda表达式的插件。
当然,Buck也有其局限性。对于有历史包袱的大型工程接入,想要接触Buck,需要较高的成本。构建过程与Gradle不同,即使是在Okbuck帮助的情况下,很多Gradle的东西都需要工程师做专门的适配,才能在Buck上面使用,有较高的学习曲线(国内基本没有太多相关资料),也无法迅速用上社区最新的技术。而且,Buck需要重新安装APK,在安卓7.0以前,一个30M左右的APK安装时间甚至可以达到30s以上,虽然编译过程可能就十几秒,但加上整个安装过程,包括重新进入页面的过程,可能需要1分钟的时间等待编译重新开始与调试。Buck不支持Windows。一些国内的工程师会有所顾忌,没有办法迅速接入;它不支持Kotlin,这也是一些使用新技术公司所担忧的。
JRebel for Android
JRebel是一家来自国外开发公司Zeroround的开发工具,是比Instant Run还早的增量编译工具。Zeroround公司最早做JVM热部署,支持Spring以及SSH框架,有大量的实践积累,做了很多优化与性能。如果要找一个Instant Run的替代品,JRebel是一个很好的选择。它是一个几乎零配置的工程,只需要安装一个插件,即可立刻运行,Zeroround公司在这方面花了非常大的精力,支持Retrolambda与大量流行的Android开发组件库。原理为字节码层面的动态加载,所以它理论上支持Kotlin、Groovy等各种基于JVM的语言。
该工具同样有一些缺陷,最大的问题是收费,且价格不菲,不过现在可以免费试用一年。且它不支持DataBinding,只有收费版才能Debug,而且由于热部署功能以及基于字节码层面的动态加载,导致没有办法使用IDE集成的Debug插件,必须使用JRebel for Android单独提供的插件进行Debug功能。还有,Crash后需要重新全量编译,单次全量编译、安装的速度非常慢。它不支持Jack。
Freeline
那么Freeline与这几种工具相比,具体有哪些优势呢?
目前来说Freeline已经支持了绝大多数场景的增量编译和Java文件的修改,这一点比Instant Run范围广。目前来说支持Retrolambda与APT,部分支持DataBinding,因为本身开发不是使用DataBinding,所以只能说在自测的范围内已经支持了DataBinding绝大部分的Feature。但是可能支持的不全,且它也处于持续的迭代中,这也是在后期迭代时注意的一些地方;另一个优点是支持SO文件的动态替换,尤其是一些地图应用或出行应用,对于整个开发调试会非常有用,这也是Freeline相当于目前几个开发工具不一样的地方;同时,Freeline的增量资源是真正的增量资源,大小可以达到几十K,甚至几百K ,I/O很小,如果仔细看Instant Run打包的资源包,整个I/O传输会使用很长时间。比如说一个十几M的资源包,在不同的USB环境下,因为Freeline真正做到资源包达到了增量的级别,所以会使传输速度达到7秒或8秒,整个增量传输非常快。它支持Windows/Linux/Mac,从而覆盖了更广泛的用户,做的比JRebel好的一点就是Appcrash后,仍然能够进行增量编译来修复,基本上不需要重新安装,因此,重新把代码修改好,运行Freeline插件,就可以立刻完成整个增量的修复。
Freeline也存在不足的地方,因为在做适配时,更多针对复杂的、十几个模块以上的工程,所以对于单Module工程,基本上没有优势,不如直接就使用Instant Run。其次是Hack方案的本质导致了存在无法避免的兼容性问题。另外,它不支持删除带ID的资源,所以资源ID删除时,可能需要重新运行Freeline进行全量编译。它也不支持Kotlin与Jack。
Freeline的原理
Freeline的本质是基于Gradle构建系统上的一套Hack解决方案。去年是热替换爆发的年度,而Freeline其实是热替换方案在编译时的实际运用,实现实时热替换,即实现了整个增量编译的效果。Hack方案本身就存在兼容性问题,从理念上,它充分利用多核性能,多级缓存,尽可能减少不必要的编译步骤。从上图可简单分析Freeline过程,除了主进程,还会单独运行Freeline进程,还有Socket长链,通过Freeline进程与主进程间的通信来完成整个Patch过程,实现热替换,这也是其运行模型。
Freeline全量编译
上图是Freeline整个全量编译的流程。从图中可以看出,中间有一个过程是gradle-full-build-with-freeline,Freeline在整个Gardle全量编译的过程中植入Hack,在Hack过程中收集了Module之间的依赖关系,包括依赖文件存放在哪里,整个过程结束之后,还会进行build-base-res过程。如果只是看Gradle的Resource编译过程,Gradle会将所有的Resource进行Merge,这就会导致在Resource编译的时候,没有办法将Compile出来的Resource资源进一步加工编译,所以Freeline在此基础上,进行build-base-res,进行全量资源包的构建,为后面的增量资源做准备。通过该图可以清楚看到相应的并发及依赖关系。
Freeline增量编译
接下来看Freeline增量编译过程。从右图可以看出,中间的流水线下来之后,整个并发过程在多模块编译,能够做到最小粒度的编译单元。如果对比Facebook的Buck,会发现Buck 的编译,整个构建单元基于Module,这就意味着如果整个工程的Module没有做好,Buck不会达到速度提升十几倍的编译效果。所以如果不使用Freeline而使用Buck,就会对整个工程的架构模式进行深度拆分,拆分成多个比较细小的模块,Freeline尽可能Dex merge。Dx过程非常耗时,Gradle在原本的编译过程中,所有的Module编译跟Class编译是合并的过程,为了进行更好的加速效果,省掉了很多不必要的步骤,将所有的资源编译统一,单独做一个统一的增量资源编译。像整个DataBinding,基于新的增量过程,生成新的Java文件。在资源编译过程中,动态修改整个R文件,让新增的资源ID能够进入最新的ID,传递给后面编译过程中的Java Module,之后再做一个Merge-dex,来实现增量编译,最终完成Sync同步,实现资源编译。
整个过程中也有一些缺陷,如果依靠全量生成Classmap,就没有办法在增量过程中实现。因为在增量过程中,拿不到全量的Class依赖,所以这是Freeline不能支持的一个地方。如果要让路由组件支持Freeline编译,需要对路由资源进行单独的拓展开发,让Freeline作为调用,才能实现整个Freeline的拓展实现。
未来规划
接下来谈一下Freeline未来的规划进展。首先关注Android Gradle Plugin的最新动态:
去年Google推出了Android Gradle Plugin2.2.0版本时说,如果将之前版本中的Instant Run关闭的话,那这个版本的Instant Run是值得打开的。因为这个版本对其做了非常大的优化和改进,包括对多Module工程的支持,得到了比较高的提升,但编译速度不是特别理想。从上表的对比可以看出,针对于100个Module的工程进行的测试,2.2.0版本在Configuration阶段接近2min多,仅仅是改变一个Java文件也需要这么久,如果使用Freeline没有那么大的耗时。因为如果没有工程的变更,Configuration等过程可能就全部跳过了。Google官方推出的2.3版本,能够缩小到9s,2.5版本里面号称可以做到2.5s的Configuration时间,而且是6.4s之内完成一行Java文件的变更,立刻生效,这是非常大的性能改进,所以这也是Freeline未来继续跟进关注的技术进展,来看是否可以回馈到Freeline的开发过程当中,实现更好的开发过程。
Freeline同时也会跟进Google的AAPT2的进展。整个看LSP源码会发现,Google很多源码其实是采用了AAPT2编译,AAPT2真正做到了类似于C++中的模块化编译,单元编译,它将每个Module自带的资源编译成一个单独的Resource包,最终将所有编译出来的Resource包通过一个Link命令,编译成最终的APK。这其实预示着,如果接触了AAPT2的工程,对于有上百个Module的工程,如果只有一个Module变更,只需要使用AAPT2编译单一的Module,然后对这100个Module使用link命令,就可以实现几乎是一个增量的效果。因为编译的耗时过程只进行一次就可以达到新的资源包,这是目前AAPT2整个技术的实现。AAPT2在整个6.0或7.0的源码里面,做了一个很大的变化。我们目前已经实现了使用AAPT2打包资源包的功能,之后也会看整个AAPT2是否值得我们去接入实现。
Freeline后期也会对最近比较流行的Kotlin、Jack等做跟进,希望未来可以对这两个工具做更好的支持,社区开发等很多新的项目都在使用Kotlin,包括阿里内网。Jack也是Google前几年推出的。希望Freeline在未来的迭代中能够支持这两个编译工具。
如何集成?
现在Freeline的集成非常简单,直接搜索Freeline,就会有一个插件,一键安装后就会自动配置集成整个环境。如果工程比较复杂,有几十个Module,需要特殊配置,也可以到官网上参考文档。Freeline以后也会继续向一键集成与无缝集成发展。
Freeline的开源历程
最后介绍一下Freeline的整个开源历程:
(1)Freeline始于2015年10月,最初是一个3000多行的Python文件,仅支持基于Maven构建的mPssS框架工程,最长可达30分钟的编译时间,后来缩短为10s增量编译,工程效能达到质的飞跃。
(2)后来在2016年5月进行重构,能支Gradle构建的Android工程,并在阿里集团内部开源。紧接着在2016年8月实现开源,当天登上了Github Trending总榜。
(3)在2016年12月,Freeline又实现了发展,获得多个技术公众号的推荐与转载,连续一个月出现在Github Java Trending榜单上,社区内开始有多篇在讨论Freeline的技术文章。我们得到了APT、Retrolambda与DataBinding的支持,并进行持续的兼容性提升,推出了IDE插件,不断满足社区的需求。