1. 云栖社区>
  2. 博客列表>
  3. 正文

Freeline - Android平台上的秒级编译方案

弦影 2016-08-15 09:41:27 浏览59007 评论61

编程语言 android 前端与交互设计 架构 阿里技术协会 蚂蚁聚宝 freeline

摘要: Freeline是蚂蚁金服旗下一站式理财平台蚂蚁聚宝团队在Android平台上的量身定做的一个基于动态替换的编译方案,稳定性方面:完善的基线对齐,进程级别异常隔离机制。性能方面:内部采用了类似Facebook的开源工具buck的多工程多任务并发思想, 并对代码及资源编译流程做了深入的性能优化。

Freeline

技术揭秘

Freeline是什么?

Freeline是蚂蚁金服旗下一站式理财平台蚂蚁聚宝团队15年10月在Android平台上的量身定做的一个基于动态替换的编译方案,5月阿里集团内部开源,稳定性方面:完善的基线对齐,进程级别异常隔离机制。性能方面:内部采用了类似Facebook的开源工具buck的多工程多任务并发思想:端口扫描,代码扫描,并发编译,并发dx,并发merge dex等策略,在多核机器上有明显加速效果,另外在class及dex,resources层面作了相应缓存策略,做到真正增量开发,另外引入并优化buck的部分加速组件dx,DexMerger,资源编译方面,深入改造了Aapt资源编译流程,当资源发生改变时候,秒级完成增量包编译,其中增量包仅含最小的变更集合(10Kb~数百Kb内),后期也被运用到线上进行资源/代码动态替换。相比目前instant-run,buck,layoutcast等方案快数倍速度。


有何优势?

1.真增量,构建过程快且增量包体积小,极大提升更改代码部署到手机速度,较Android studio2.0及 LayoutCast快3~5倍

2.跨平台Linux,mac,windows

3.全版本覆盖 2.x ~ 6.x版本均支持

4.部署流程简化,更改代码后,构建过程中,与手机建立了tcp长连接,一行命令即可完成增量部署,毋需到各自子bundle所在的目录构建完成后再进入portal/launcher进行打包再安装到手机的过程

5.事务支持,在开发过程引入的异常不会破坏工作空间

6.无缝支持mPass,解决了类似maven各个节点需merge合并等与常规开发流程不一致的问题

7.进程级别异常隔离,开发体验持续稳定


谁在用?

目前 Freeline 在阿里内部稳定支撑蚂蚁聚宝,高德地图等ANDROID技术团队日常开发,兼容mPaas/gradle架构

开源地址:
https://github.com/alibaba/freeline


技术内幕:方案对比

先看看传统的Android打包流程:


                                      4c1a0d0bbf2efd47af05db3f26d20044624891a0

单线程沿着流水式的任务从上到下进行打包构建,其中,aapt会执行2次,第一次是生成R.java,参与javac编译,第二次是对res里面的资源文件进行编译,最后APKBuilder会把DEX文件与编译好的资源文件及DEX文件进行打包成APK,签名并安装至手机。整个流程下来,没有任何缓存,没有并发,也没有增量,每次构建都是一个全新的过程,所以每次构建时间也比较恒定,代码量,资源量越多,构建时间越慢。



下面对业内3个比较主流的增量构建方案进行对比分析:

LayoutCast

增量的思想源自于LayoutCast,与LayoutCast不同的是,Freeline把连接设备与各个工程间扫描及构建增量包任务仿照Buck的思路进行拆解,而LayoutCast是传统流水式任务构建,性能可能会被中间某个环节的耗时被拖慢,没有充分利用到多核优势。另外在资源变化后,LayoutCast选用的方案与Android Studio2.0 instant-run思路一致 ,把整个应用的资源打成资源包,推送至手机,若资源包大小较大,该过程耗费的时间相当可观,而资源包的大小直接影响了后面通过tcp传输的耗时,从资源变更的角度的来说,LayoutCast并实现没有真正意义上的增量。

另外,LayoutCast通是过运行期手机端反射R Class field的方式来生成ids.xml及public.xml,用于保证增量包的资源id与全量包的资源id一致,该方案存在几个缺陷:

1.通过手机端运行期反射,R有上万个field,效率相当低下,Galaxy note4上耗费近1s
2.存在致命缺陷,举个例子,app声明的attrs.xml若存在一个定义如下:

 <attr name="ptrMode">
       <flag name="pullFromStart" value="0x1" />
        <flag name="pullFromEnd" value="0x2" />
   </attr>

其中 ”pullFromStart“,”pullFromEnd” 对应的id值如下:

    public static final class id {
        public static final int pullFromEnd=0x56050005;
        public static final int pullFromStart=0x56050004;
   }

实际上上面2个枚举常量生成的id的type类型是“id”,若生成ids.xml及public.xml时候,不排除这些枚举id,最终的结果就是aapt给每个资源分配id时候,发生数组越界,aapt程序coredump掉,无法构建出资源包,而手机端运行期反射时候,仅知道”pullFromStart“,”pullFromEnd”为id类型,不足以知道其对应的是枚举常量,故仅仅通过运行期反射,上述致命缺陷无法解决。
再有,由于没有缓存机制,LayoutCast编译速度会随着修改文件的增加越来越慢。最后,由于代码增量使用的是dex插入系统dexlist最前位置的方式,在4.x的机器上面系统安全校验不通过,所以LayoutCast并不支持5.0以下的手机。


BUCK

下面说说BUCK,先看一幅其官方的构建过程图

175f4f1bc9d2ab40c7f03fc1b7e4e413dd6205ba

BUCK把原来单流水线任务以工程为单位拆分成多个可并发执行的子任务节点,梳理好各个节点前后的依赖关系,整理出有向拓扑图,通过多条线程并发把各个子任务节点构建出来,充分利用多核优势。

BUCK建立了一套完善的依赖规则以及细化的缓存系统来缩减编译时间,其增量构建的原理,实际是以工程目录为单位进行增量构建,发生变更时候,变更的工程,以及该工程作为父节点或祖先节点的工程,均需要重新构建,构建完这些变更涉及的工程后,Buck需要重新走一次合并各工程DEX,对齐,签名,打包APK的过程,构建完毕后,还要继续走安装流程,到最后手机查看修改效果时,可能还需要几个页面的切换才能进入之前修改的页面,这些流程整个下来,耗费的时间是相当可观的,另外不支持windows,以及较强的入侵性(整个工程需要做较大的调整才能使用)均是接入BUCK的门槛,但不得不承认,若作为全量构建的角度,BUCK的确是不二的选择,背后还有强大的Facebook技术团队在维护,在Facebook内部,所有的app构建工具均为BUCK,在国内,BUCK也被微信应用为默认构建方案。


instant-run

最后是谷歌官方的增量解决方案Android Studio2.0 instant-run ,首先其基本流程与LayoutCast有点相似,但因其代码增量是通过运行期hack method实现,所以进行了instant-run后,实际App没有重新走原有该走的生命周期,导致要看到类似onCreate,onResume等生命周期方法修改后的效果,必须手动重启一次进程,另外因为不同手机指令集合的不同,instant-run还会有一定挂掉的机会,最后,因为instant-run采用hack的方式,导致debug包调试时候无法看到对应的method堆栈,不得不说,这是个巨大的弊端,最后,与LayoutCast一样,instant-run不支持5.0以下的机器。


核心思想

正因为上面几个方案各自有各自的优缺点,Freeline融合各自优点而生,核心技术思想源自于Buck,LayoutCast,并在此基础上进行一步改良,争取把增量思想做到极致。

主要有如下几点:
多任务并发,多级缓存,增量范围最小化,懒加载,基于长链接无安装式运行期动态替换,基线对齐触发机制,可调试



多任务并发

研究过Buck的同学应该清楚,Buck把原来单流水线任务以工程为单位拆分成多个可并发执行的子任务节点,梳理好各个节点前后的依赖关系,整理出有向拓扑图,通过多条线程并发把各个子任务节点构建出来,充分利用多核优势,在macbook上默认16条线程并发。
Freeline在启动时候仿照了Buck,根据工程间及任务间的依赖关系,提前计算好有向拓扑图,进行并发任务执行,默认开启8条线程(因聚宝工程数较少,没有必要开启过多线程),下面先简单介绍一下相关知识:


有向拓扑图

拓扑图是图的一种,“有向”保证了依赖关系和顺序关系,可以有多个根,
子可以有多个父


                                         d6bee667b31fc1efbe4c4f01fe526ebc6c2fede2


下面先以一张图简要说明Freeline构建期间各个工程任务工作次序:

d02e8d4d60bccb11cd4d0897d1464ec995cdf98a

整个工程角度来看,主要分成:
PC端与手机建立TCP长连接,扫描各个子工程文件变化,各个子工程的增量dex构建,增量资源包构建,合并所有工程dex,传输增量包

上图中,分叉的箭头代表任务是并发的,同一时间,不同的工程可能处于不同的构建阶段,Freeline在启动时候,会先定义好各个子工程及其子任务前后的依赖关系,每个任务的前置任务,后置任务,位于同一层级的工程会进行并发构建,默认8线程并发。


单个工程流程

上面从总体上介绍了Freeline工作的整体情况,接下来详细介绍Freeline每个子工程做的事情:先以一张图说明Freeline构建期间单个工程为单位的任务流程:


91c364a47a39bf23eb9aeb2af013ffe1bb1576c9

以app工程为例子,这里app依赖common ,构建过程有如下步骤:

1.scan扫描app工程内文件变化

2.根据扫描结果,若同时有资源及代码变化则并发运行inc-code-task 及inc-res-task,
以”inc“开头代表该任务是增量任务


inc-code-task介绍
从上到下分别为:
check-r-change
(校验R文件MD5是否发生变化),若发生变化则把新的R.java加入变更列表

begin-code-transaction:
该过程会把代码增量所必需的工作空间进行备份,若下面其中一个过程发生错误,则会把整个过程中构建的产物进行事务回滚

javac:
把扫描出来的java变更集合,进行编译,若存在dependency (上面例子为common工程)也在构建,则挂起,等待前置任务javac构建完毕后再往下执行

buck-dx:
这里实际上是把上面编译后的class文件变成dex文件,这里用“buck-”描述是因为该dx工具是从Buck中提取出来,经实测比Android原生的dx工具快40%左右

buck-smart-dex:
同上,该工具在buck工具中提取而得,目的是使上一步打出来的dex体积进一步减小,最后生成的dex则为该工程该次增量dex构建的最终结果


inc-res-task介绍

begin-res-transaction:
该过程会把资源增量所必需的工作空间进行备份,若下面其中一个过程发生错误,则会把整个过程中构建的产物进行事务回滚

merge xml:
若更改的文件在其他子工程也存在,以mPaas架构为例,存在api,biz,build,或tools,这些工程可能会存在同名的xml文件,这种情况需要对这些xml文件内对应的节点进行合并

merge ids:
若上面gen-r 阶段发现R的md5发生过变更,或更改的文件集合里面有ids.xml或public.xml,则把目标目录里面的ids.xml及public.xml与新变更的ids.xml 与public.xml进行xml节点合并

gen id files:
该过程是实现资源增量的关键,该过程会通过最后一次构建的资源包,反向生成
ids.xml及public.xml,该两个文件在构建增量资源包时候参与编译,可以使得
最后构建出来的资源包的内对于的资源ID与前一次构建的资源包保持一致,该过程原理后面篇幅会详细介绍

build-inc-res:
该过程会把上面scan过程扫描出来的资源变更集合参与作为参数传入我们自己改写过的increment版本的Aapt,该工具主要完成几个事情:

1.构建增量包,生成最终的资源包时候,仅仅包含编译后的变更资源集及“resources.arsc” 与 “AndroidManifest.xml”

2.兼容mPaas架构Base Package id 问题

3.根据ids.xml及public.xml生成保持id值与前一次构建结果里面的id值相同,若该任务有前置的资源任务(上面例子为common),则等待其前置增量资源任务先构建完毕,最后构建出来的包以“.pack”结尾


多级缓存

在代码变更方面,Freeline在各个工程的Class,dex层面加入了缓存,已经编译过的java文件,直接从增量工作空间里面的的Class pool获取,已经dx过的Class文件,会直接从dex pool中获取,最后实现的效果是,每次增量构建都是一个全新的流程,此前的修改不会参与到本次增量编译过程,不存在LayoutCast方案随着修改文件的增多越来越慢的问题。

                            6c82c6899af6bd7674ee7acb4d2269c4938c55cc

在资源变更方面,Freeline会在每次增量包构建后,把增量修改的资源文件与手机端对应的文件进行一次sync同步,每次资源增量构建范围仅仅是本次修改的集合,此前的修改均在此前的sync同步过程中同步至手机端。与代码变更一样,不需要构建此前修改的增量集合。

                   15c4627532582efc754a1e3b280fbd9ea4ae9945


加入多级缓存及多任务并发策略后数据对比



                                       51f331b63ec181ed1ec6de6017e14bc3cc5159b3


增量范围最小化

Freeline会尽可能把增量的范围缩小到单次修改对应的必须要更改的文件集合,不定期与手机端进行同步,以减少随着修改范围增大带来的性能损耗。
代码层面,运用了上面提到的多级缓存,每次仅仅编译本次修改的文件,此前修改过的文件不在本次编译范围。
资源层面,我们为了尽可能降低增量包的体积及构建成本,在aapt的基础上,拓展了一个叫IncrementAapt的工具,并把其编译成linux,mac,windows三个不同平台以做平台兼容,该工具会根据修改的资源文件,及最后一次资源构建结果,构建出对应的增量包,该增量包仅仅含变更的资源集合,且进行过7-zip压缩,大小视更改修改量而定,一般情况只有数百kb。极大程度降低打资源包及最后tcp传输的耗时。


懒加载

Freeline 把任务尽可能延后到真正需要的时候进行,例如对R文件的javac编译,若仅仅修改资源文件,即便是新增了资源文件,如:加了新的id,新的图片,layout等,触发了新的R文件与旧的R文件的id集合不一致,但此如果没有修改过java文件,则不会触发对R文件的编译,也就是如果只修改资源,没有更改过java代码的话,不管实际上应用的id集合是否已经变更,Freeline会以极小的代价构建出增量的资源包,推送至手机,直接在当前的Actvity刷新,不需要重启进程。对于新的R文件的编译,会延后到该工程有java文件更改才执行,这样也保证代码里面真正需要R文件新增的id值的时候,能找到对应的值,在没有代码更改前,进程无需重启,加快刷新效率。


可调试

Android studio instant-run 因采用的是Hack method 的方案,存在被修改的方法无法调试问题,LayoutCast构建的增量Class,在Debug调试下也存在参数值无法显示的问题,Freeline在该点上进行了处理,使得增量构建的类文件与全量构建一致,不影响日常调试。


基于长连接无安装式动态替换

无安装式动态替换与LayoutCast及Android Stduio2.0 instant-run一致,也是该两种增量构建方案的最大的优点,整个构建过程不需要重新安装app,动态替换代码及资源,省去了安装app及重启进程进入对应界面的过程。整个交互流程图见下:


                   0696345275c63612380f573baa8c2fcbc684cf8e


1.phone端会架设一个tcp socket作为服务器。

2.pc端会与手机端进行socket连接。

3.pc端与phone端会通过自定义协议进行交互,pc端会询问phone状态,比如获取手机端基线包版本,sdk版本号,当前手机是否支持资源增量,当前Activity名字等等,后续传输增量包,手机端向pc端返回增量构建结果等,整个通讯过程,均会沿用同一条长连接进行。

4.在同步完增量包后,phone端会根据当前变化是代码变化还是仅仅res变化来决定下一步操作,若仅仅res变化,则直接restart 整个Activity栈里面的Activity,若存在代码变更,则直接重启当前进程,由于Android系统Activity栈的管理,进程被杀若Activity栈还存在Activity,则在该app重启时候,会沿用原来的栈顺序重新创建这些Activity。最终的结果,重启后,界面就会出现最后显示的Activity,(这里有特殊情况,如果该Activity的launchmode设置的是singleTask,或singeInstance,则重启后除了最后的这个Activity,堆栈内的其他Activity均会被清空,这涉及到Android对Activity的管理机制问题,这里不细说,有兴趣的同学可以到自行google。)而按返回键后UI也会顺着原来栈里的Activity顺序显示。



基线对齐触发机制

Freeline会在下面情况重新构建基线包:

1.在git pull 或 一次性修改大量的文件情况下,会导致增量包体积大增,影响后期传输及手机重启后对增量包进行dexopt的速度,考虑到这种情况毕竟是少数,没必要为一次的变更影响后期的增量构建速度。

2.无法依赖增量实现的修改:修改AndroidManifest.xml,更改第三方jar引用,
依赖编译期切面,注解或其他代码预处理插件实现的功能等。

3.更换调试手机或同一调试手机安装了与开发环境不一致的安装包。

由于在重建基线包前,可能已经进行了若干次的增量构建,故在重建基线包时候,要把这些增量构建对应的module进行全量构建,以使得最新的基线包包含了所有过去的修改。整个流程如下图:(A,B, C 分别为3个不同子工程)

9e257f6c857ae448b4487261b7fcaba7fd77543f

head 作为指针,指向最新的基线包状态,base为对应初始的基线状态,在经过3次增量构建,手机端内app的状态会变成base + 3次增量的结果,上面例子里面,3次增量构建涉及A,B,C 3个Module,那么,在触发基线包对齐过程中,会对A ,B,C 按照原来的全量构建方式进行构建,与增量包构建一样,全量包的构建顺序会按照A,B,C前后的依赖关系按顺序进行,位于同层级的工程会进行并发构建,构建完毕后会重新安装至手机,在此之后,手机端内app以全量的方式包含A,B,C的修改,此前的3个增量包会在覆盖安装后第一次启动中被清除,此时基线指针head会从最初的base指向最新的base(with new A,B,C),至此,整个基线对齐就完成了,若中间发生异常,则在下次运行时候仍然会进行一次基线对齐过程,保证手机端安装上最新的全量包。

基线对齐的校验机制

上面的介绍的是基线对齐的整体思路,下面介绍一下校验部分的关键思路:

633f73544d24291b4e112714656310cd11b00a41

1.在全量包构建的时候,把当前的时间戳打包进assets目录,该值用于确保全量包的一致性。

2.每次进行增量包传输后,由手机端与PC端共同维护了一个自增长的sync id,每次传输成功后,该id会触发更新,该值用于确保开发环境的开发状态与手机端增量的开发包的状态一一对应。

3.在每次传输增量包前,手机端与pc端会基于上述两个值的生成一个验证码,并且对这个验证码进行校对,若两端的验证码不一致,则认为校验不通过,需进行基线对齐。


进程级别异常隔离:

Freeline的socket tcp server是运行是独立进程的,之所以要进行进程隔离为的是当开发增量部分传输至主进程后,导致crash的情况,防止无法进行进行增量传输,故把tcp传输部分独立到单独进程,保证传输过程持续稳定,实际上这也是遵循”轻重分离“,把刷新替换部分较重的容易导致crash的部分交由主进程执行,把建立连接,传输及基线对齐等较稳定的部分移至独立进程。


增量原理

代码增量

关于代码增量,与业内主流的通过植入Dex 到 系统DexList 实现hotpatch方案相同,关于其原理网上也有不少介绍,这里再简单的提一下:

系统查找Class,最后会到BaseDexClassLoader查找

最后调用到DexPathList

2dd6eb556693b42bfe883d5cd2b83971f95f0fb9

其中DexFile对应的为默认安装包里面的class.dex,class2.dex...等。在Google支持MultiDex后,构建工具默认会按照65536方法及LinearAlloc内存限制进行分包,一般一个大型app,会有多个dex文件存在,从上面的代码来看,对于类的查找,从dex数组,最前的位置开始找,找到对应的Class则不会继续往下找,这也给利用该特性进行增量带来了契机。
在应用启动时候,把我们准备好的增量dex通过反射注入到DexElements最前面,则整个增量部署就完成了。

                                      fe52c088bcdbcdf9981dd8e469a60f0530aab454

资源增量

资源增量是开发Freeline过程中,攻克时间最长的一块,也是Freeline相对其他构建方式,比较明显的一个特性,前面说过,LayoutCast和instant-run在资源更改后,实际上是把全量的res资源重新打包,推送至手机,进行整个资源包的更换,所以资源数量越多,大小越大,构建的时间就越长。

先说说开发一个资源增量的特性需要解决什么问题:

1.增量包资源id怎么兼容基线包资源id?

2.怎么样高效构建出仅仅包含变更集合的资源包?

3.怎么样在手机端让上面构建的增量包生效?



带着问题,我们一步步来介绍:

先解答第一个问题:1.基线包资源id与增量包资源id怎么保持一致?

1.关于资源包id向前兼容的问题,业界一般采用上一次资源包生成的public.xml 及 ids.xml参与后续资源编译解决,业界生成上述2个文件,主要有如下2个方案:

app运行期通过反射R class field生成

该方案前面已经提过,存在致命缺陷,且反射过程涉及过万个field,效率低

ApkTool 反编译资源包

该方案实际需要把所有的资源逆向导出,全部资源都需要从资源包解压逆向编译回原文件之后,生成对应的ids文件,随着资源数量,大小越多,耗费的时间就越长。



Freeline采取的思路是通过最后一次编译res过程的R.java,反向导出保留id所需要的两个文件,这个功能抽成单独的工具“id-gen-tool”,该工具会根据枚举常量生成的id的上下文特征,过滤掉枚举常量,解决掉其引起的内存越界问题。

                           9456ffa1a3c4ab64e668572efdeaf9ce95cd1458

由于整个过程仅仅需要对R.java一个文件进行分析导出,不需要解压APK以及反编译APK资源包内资源,故整个过程基本不受资源包内资源大小,数量影响,另外因为是在pc端进行,故整个过程比在手机端快90%以上。下面是数据对比:

                                 4753a200d6a79060f07b19e05824a6b50d435973

在30mb的资源数量下,id-gen-tool的速度较app反射方案快90%,较apktool反编译方案快95%以上,随着资源数越多,差距会越来越明显。


id-gen-tool细节问题

有了这两个文件,资源id的问题算是搞定了,实际上真的这么简单么?等等,把这两个文件放置到资源目录里面的values目录下,对资源进行编译,又出现意想不到的问题:

且看看在styles.xml上这个定义


   < style name="Animations.Pop">
        < item name="@android:windowEnterAnimation">@anim/pump_bottom < /item>
        < item name="@android:windowExitAnimation">@anim/disappear < /item>
    < /style>

生成的R.java对应的id是什么:


    public static final class style {
        public static final int Animations_Pop=0x1f0b002c;
        ………………….     
    }
    

也就是说,R.java内的资源命名,压根不存在“.”, 这样而通过R.java生成的id就会变成这样:


< resources>
    < public type="style" name="Animations_Pop" id="0x1f0b002c" />
    ………………………
< /resources>
    

这样,最终导致的结果是编译资源时候找不到"Animations_Pop" 这个资源而编译报错。而因为无法从R.java 变量命名来推断出原资源定义里面是" ." 还是 "-" 。这个这么看来,通过R.java反向生成id文件的办法是行不通的,但还好,aapt程序也在我们手里,只要让aapt针对这种情况进行兼容,那上面的方案就是行得通,最后,我们拓展aapt寻找资源的策略,发现找不到资源时候,会尝试把资源名称里面“-”替换成“.”,继续寻找,如此一来,上面问题也就解决了。

32164f59806278ac4261879d6f0f5be02e99f95b

最后提一点,在基线包id被固定后,新增资源就不会对原有资源id的访问造成影响,也就是说,基于这个前提下,我们就不需要保证增量包里面的资源数与基线包一致。这也解决了日常开发引入新的资源可能会引起的与基线包id不对称问题。



2.下面来介绍第二点:怎么样高效构建出仅仅包含变更集合的资源包?

关于Android资源编译,可以看看老罗的这篇博客:
http://blog.csdn.net/luoshengyang/article/details/8744683

这里把过程图片借鉴一下:

                                   db9090f2021cfd472142939721b868638571ddd5

我们把整个资源包构建可以优化的技术点在上面图片的红圈标了出来:实际上,在前一步通过资源ids.xml及public.xml生成出来,放进values目录参与编译后,即便不对未变更的layout资源及AndroidManifest.xml进行编译,最终对生成的resoucres.arsc是没有影响的。也就是在保留了资源id的情况下,只需要编译变更了的xml文件就能实现对resoucres.arsc的更新。

在前面扫描里面,我们知道了总共有哪些变更的资源文件,py会把这些资源文件相对路径截出来,作为参数’—buildIncrement’传入到incrementAapt工具里面,在编译资源的流程里面,如果非变更的资源,我们利用了最后一次资源包里面编译好的资源作为缓存,非变更的文件,我们直接让其从编译好的资源读取,整个过程不需要重新对非变更资源进行编译。(由于这块代码更改地方较多,这里就不贴出来,后面整理好后,会进行开源)


7e2f88dc6b895398a2614a005f937fb357e71063

最后打包成最终APK时:我们还修改了打包文件的流程,incrementAapt仅仅对修改的文件对应的编译后的资源进行打包:

2f740a58610324ec9de3b0ba5febb56f66ecb9ab

整个流程下来,最终构建出来的包就仅包含变更的资源集及“resoucres.arsc”与“AndroidManifest.xml”

这里解答一下:为何需要对“resoucres.arsc”与“AndroidManifest.xml”进行打包?
由于当有新增资源后,“resoucres.arsc” 是会变化的,代码里面对新增资源的引用就是通过更新”resoucres.arsc” 来实现。这里打包“AndroidManifest.xml”原因是,在sdk19之后,底层AssetsManager->addpath 过程会触发对res资源包的校验过程,没有“AndroidManifest.xml”的资源包会被认为不合法的资源包,不会被成功添加。



资源编译各步骤优化数据:

以30mb的资源为例子,下面是整个资源编译流程优化前后数据:

0a6892194893eeaccffb141f26706b6e8d3987e7


下面简单介绍一下各个步骤,详细流程可以到上面提到的老罗的博客看。

slurp up res: 收集资源。

makeFileResoucres all resource: 把上一步收集的资源添加进内存,如果有图片,则会在这一
步对图片资源进行处理。

compile value:对“value”目录下面的资源进行编译。

makeFileResoucres for color and menu :对”color“ 及”menu“目录的资源进行编译。

generate all bag attr:为“bag”类型分配资源id。

compile all xml:这一步是真正对layout,anim,animator,interpolator,transition,xml,color,menu目录所包含的资源进行编译,压平。

flatten gen resources.arsc :根据上面收集的信息生成“resources.arsc”文件。

gen r file:生成R.java。

APK Bunding:对所有编译后的资源进行打包。


可以通过数据对比看到:主要的速度提升在compile all xml(编译xml文件) ,APK Bunding(打包APK),makeFileResoucres all resource(对图片资源进行预处理)几个步骤。若通过传统方式aapt编译打包,接近3.5s,而通过上述方式,整个构建时间仅仅需要300ms。这比前者快90%,随着资源数的增多,总大小的增大,差距会越来越明显。


第三点:怎么让上面构建出来的增量包在手机端生效
经过深入AssetsManager底层的分析,我们发现,res实际上是支持以目录的形式存在,那么整个增量包生效的思路就呈现出来了,流程如下:

1.首次运行增量构建,手机端会对基线包的base.apk(在mPaas,这里是Bundle对应的资源jar路径)进行解压:

5ecddbfc8efc02c13f98b62b6280c3f5a0d79ce8

2.在这之后,resDir目录里面就包含了所有该资源包里面的文件。在此之后我们可以把
AssetsManager所对应的path指向resDir,这样一来,实际上UI对应的资源就来自于resDir目录。在进行资源更改后,在前面介绍知道,Freeline通过tcp连接把增量的资源包 inc.pack 传输进手机后,会触发一次sync,用于把增量包的修改同步到手机端。流程见下图:

64dc5e4c7731e8815cb5bd36b2a429fcd2ea7ee7

手机端会将inc.pack进行解压,然后把解压后的buffer,直接写入resDir目录里面的相对位置,整个过程仅有从压缩包提取-写入文件系统两步。最后一步,就剩下清空Resources资源对应的Cache,重新构建新的Resources并让app使用之,这一步网上已经有很多介绍,这里就不另作篇章了。对于mPaas架构而言,要使之生效,无非是找到对应的Bundle所在的Resources,清除其缓存,重新刷新一次UI。


资源增量构建数据对比:

dd5ceeb84aa9157981f961ea92a534d3dc7216123060cac722df3fd84db5209405fed26defd076da


在40mb的资源下,构建出一个资源增量包,上述方案仅仅需要600ms,其他方案要4s以上,增量包体积仅仅为300kb,而其他方案构建出的资源包为5mb。

这里提一点:让增量资源包生效的还有另外一条路径是采用系统自带的”overlay”方案。
也就是“layout”,”drawable“,”color“,”anim“,”xml“ ,”raw”,”animator”,”interpolator“,”menu“等目录类型所在的资源id对应的索引项在构建“resources.arsc ”时候设成“NO_ENTRY”,运行期生成AssetsManager时候把增量资源包的path顺序放在添加全量包的前面。利用系统查找机制来覆盖掉更改的资源。
Freeline这里没有选用上述方案的一点原因是:
1.由于系统overlay设计限制,资源无法实现新增功能,只能修改,这在日常开发中极不方便。
2.每次构建增量资源包时必须保证要把全量资源包构建以来的所有修改的资源文件都要参与编译及打包。也就说,随着资源修改量的增加,越往后,参与编译及打包的资源数量会越来越大。而采用上面提到的方案,简单的讲,就是能做到完全不受累积的修改影响,每次修改在与手机同步后,这次修改就算清除了,后续编译也无需把此前的修改的资源文件拉进来参与编译打包。这也使得Freeline在修改UI时候能持续保持稳定的性能,不受修改范围的累积的影响。


资源索引Cache

resoucre.arsc是保存Android资源id索引的索引文件,在一些大型的app,arsc的体积不小,6m~10m是比较常见的情况,Freeline在arsc进行打包前,做了一个优化策略,当资源修改不引起arsc更新时,不会把arsc打包进增量包,避免无用的打包及TCP传输,采取的策略是,入参传入上一份arsc的md5,在aapt编译流程进行打包时,对C++层arsc内存块进行提前MD5计算,发现与上一份arsc的MD5不一致才进行打包。在大型的app(资源数量较大的)情况下,(tcp传输+打包+解包)可优化降低接近3~5s的时间。


全平台覆盖背后

1.构建过程选用python + java作为构建语言,其中py负责文件扫描,调度各个
工具(dx,smart-dex,merger,increment-res-tool),与手机建立tcp连接及传输增量包等。

2.C++编写的IncrementAapt 分别编译成3个不同平台运行库的方式,实现平台兼容。

50792c863546c9e5321fee7e807326a9cf580be4

3.非Art版本手机上,代码兼容方案是使用Asm技术在编译期动态修改基线包的Class字节码,在每个类构造函数插入外部dex的引用,使之绕开dvm的对Class 的安全检验,我们称之为“hackbyte”。

这里介绍下其原理:

在安装包进行安装流程里面的dexopt步骤会对DEX文件所有的Class文件进行扫描,当Class文件内所有直接引用到的类,与该Class均在同一个DEX,那么这个类就会被打上“CLASS_ISPREVERIFIED”标签。

830117ea13646c82772d3d91884c24293ff3654f

a5b9385c8a88a5cd41a1684c4765be5ae18a11a0

而被标上“CLASS_ISPREVERIFIED”的类,dvm在运行期载入Class时候,会对其内存中对应的直接引用类进行校验,如果该类存在与直接引用类所在的dex不是同一个,则直接报“pre-verification” 错误,该类无法加载(注意:无法加载的是这个被标上“CLASS_ISPREVERIFIED”的类,非其直接引用类),也就是说,若我们通过增量包推送进去的类作为其他类的直接引用类时候,这些引用了增量包里面类的类在加载时候就可能出现校验失败。

2843d133d1900e0eb687c3d853c3583f55f81185

实际上上面这一步也是google为了防止外部DEX注入的一个安全方案,即保证运行期的Class与其直接引用类之间所在的DEX关系要与安装时候一致。

通过上面分析,只要一个类的存在直接引用类与该类不在同一个DEX,我们就可以让该类避免被贴上”CLASS_ISPREVERIFIED“的标签,接下来要做的事情只需要把自己工程的代码在编译之后,通过ASM技术动态修改Class字节码,给自己工程所有的Class植入一个来自其他DEX的类,注意这里只需要给我们自己能改的工程注入,对于第三方jar包,无需做这一步,因为依赖关系,第三方jar不会反过来引用我们的工程代码,也就不存在上面的问题。

最终,我们植入代码后,反编译出来的代码是这样子:

65db5dd55ce7433a2363b5cfe10c6e8c8072af73

之所以选择构造函数植入因为其不增加方法数,其中ClassVerifier.class来自于一个单独的DEX,该DEX只有ClassVerifier.class一个类,在app启动时候,把该DEX注入到上面提到的DexList的最前面。则在5.x以下,该方案就会生效。

最后提一点,实际上业内,该方案也被应用到hotpatch里面,国内手机QQ空间的hotpatch就是这么做的。而我们是把其应用到增量构建方案里面。

细节处理

经过研究,在art上,dexopt过程中会对final class里面的基本类型进行优化,所有对final class的static变量进行访问,都会被优化成通过offset的方式进行访问,举个例子:



final class A {
    public static int a = 1;
    public static int b = 2;
}

假设我们在开发过程中,在a前间插入了一个新的变量:public static int a0 = 0;若通过上面的方案来进行代码增量,则会出现其他类访问a得到的值是a0的值,访问b得到的是a的值。也就是所有对的static
int类型的值访问都被往下挪了一位,导致其他类在从这个被patch过的类获取到的值是不对的。最典型的就是R.java文件,里面如果用了类似buck来构建全量包,生成的R.java文件里面的field就是非final的,当我们新增了资源,即便我们通过前文提到的方式解决了资源id前后一致的问题,也无法保证新增的资源id其他类能正确访问到,甚至会导致其他类里面的通过”R.xxx.xxx“的形式访问资源id得到的值是乱掉的。

在这一点上面,freeline也进行了处理,目前采取的方案是在开发环境时候在Manifest.xml关闭android:vmSafeMode来解决。

篇幅问题,关于art上面对这个环节的优化及解决原理,后续打算以单独一篇文章来介绍,这里不细说。


数据对比

性能 :

cae88fd3c8e8d90b657a7058716703771478746f



可见:较传统的maven构建方式,在增量模式下Freeline可提升数十倍的性能,与业内主流几个先进的构建方式比, Freeline仍然有数倍的速度领先



兼容性 :

8695df10dabda4433e0b3d41b77b25620bc7464d


可见,Freeline相对于LayoutCast及AS2.0(手机端不支持Android5.0以下),Buck(pc端不支持windows)等构建方式,在 平台覆盖上更广


release后续计划

1.常见注解库支持
2.native so 动态替换
3.多设备连接支持
4.AS插件支持

加入我们

最后,在结尾打下广告:我们是蚂蚁金服一站式理财平台蚂蚁聚宝客户端团队,诚招广大有能力的Android,iOS,H5 开发同学,简历可投递至xianying.hjw@alipay.com

本文为云栖社区原创内容,未经允许不得转载,如需转载请发送邮件至yqeditor@list.alibaba-inc.com;如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:yqgroup@service.aliyun.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

用云栖社区APP,舒服~

【云栖快讯】浅析混合云和跨地域网络构建实践,分享高性能负载均衡设计,9月21日阿里云专家和你说说网络那些事儿,足不出户看直播,赶紧预约吧!  详情请点击

网友评论

1F
守护forever

使用了下,真的很快的,目前还有点小问题,希望能尽快修复

tanranran东风破啊 赞同
tanranran

请问,是哪些小问题呢?

道道禅机

我这里每次都报com.android.dex.DexException: Too many classes in --main-dex-list, main dex capacity exceeded,但是在gradle下就能编译成功,用freeline就报这个错

评论
2F
舞影凌风

minSdkVersion不能更小吗?

弦影

Freeline在0.5.4版本对minSdkVersion进行了降级,请问你是在哪个地方遇到问题呢,欢迎到https://github.com/alibaba/freeline给我们提issue,或者加入我们官方群,github上面有贴的,

另外,目前也支持通过在mannifest配置解决,解决方案如下:

android:minSdkVersion="9"
android:targetSdkVersion="21"
tools:overrideLibrary="com.antfortune.freeline"/>

tank_pei

Error:Execution failed for task ':app:processDebugManifest'.

Manifest merger failed : uses-sdk:minSdkVersion 11 cannot be smaller than version 14 declared in library [com.antfortune.freeline:runtime:0.5.4]

tank_pei

@弦影 最低支持是14不是9?

野崎

@tank_pei 我们目前只在API 14+的设备测试过,尚不支持14以下的设备哈

舞影凌风

@tank_pei 开发的时候用14,打包的时候改回9呗

弦影

@tank_pei tools:overrideLibrary="com.antfortune.freeline" 在android:minSdkVersion下面加这个即可

严振杰

@弦影 你好,在github上面没有看到官方群,能否给个地址,谢谢。

评论
3F
小谷xg

自从用了这个整个人的心情都变好了

4F
牛顿爱苹果

为什么一台设备只支持一个应用依赖Freeline呢?

野崎

socket扫描的时候多个app依赖freeline的话可能会导致冲突致使freeline基线对齐失败,这个问题之后的版本会解决

评论
5F
牛顿爱苹果

我的colors.xml有key为color_d1d1d1,value为#D1D1D1,原样复制过来这里显示不出来。
error.jpg

报错:
/app/src/main/res/values/colors.xml:3: error: Found text "

" where item tag is expected

同时,还有:
error: Public symbol color/colorAccent declared here is not defined.
error: Public symbol color/colorPrimary declared here is not defined.
error: Public symbol color/colorPrimaryDark declared here is not defined.

定义了的color值,代码中没有引用也会报错吗?

野崎

/app/src/main/res/values/colors.xml:3: error: Found text "

" where item tag is expected

错误在这一行,你这里估计有一些肉眼看不见的unicode字符,导致aapt无法识别。

gradle能编译过是因为gradle使用的是merge过后的资源。

解决的方式是清除一下那行无用的空格,然后再试试看

弦影

@牛顿爱苹果 升级到0.55,这个问题彻底解决了

牛顿爱苹果

@弦影 确实不错,升级到0.6.3了,但是老是提示我NoInstallationException occurs, a clean build will be automatically executed.我明明就装了的,这是什么原因呢?

评论
6F
wv1124

86秒快么?

maclay 赞同
7F
maclay

第一次运行非常慢,比as还要慢,第二次就非常块了,但是会报错,必崩溃。错误日志E/System: java.util.zip.ZipException: Not a zip archive

野崎

报这个错目前发现是在One Plus/CM上会出现,系统对于zip文件似乎有特殊处理。其余的系统上没有出现这个问题。

评论
8F
往之

问个问题,类的更新是直接前插吗? 那这样当修改了类结构,比如说添加字段之类的更改时,如果只动态加载更改的类,在5.x的手机上会报类不一致的错误吧

武智

上面有解释如果规避class-pre-verified的问题

往之

@武智 不是class-pre-verified的问题, 是在5.x机型上的问题。我们做hotfix时关于类加载方案与此文提到的一致,但是hotfix时我们只能改方法内容,而不能改类结构,如果要改类结构,就要把该类的父类以及所有用到该类的类都打到patch包里去,否则就会报一个类不一致的错误,具体是哪个错误我也忘了

武智

@往之 貌似在layoutcast和freeline里都没有类似的问题

往之

@武智 这个错误: Incompatible structural change detected: Structural change of XXX is hazardous (XXX.odex at compile time, XXX.dex at runtime): Direct method count off: 2 vs 1.

野崎

@往之 修改类结构是指?我们目前是无法增量编译abstract类,这个问题后面都会解决。

我是往之

@野崎 比如说 增加/减少一个类的变量/方法

评论
9F
往之

还有个问题,我们公司的项目已经拆分成多个工程,是通过aar的形式去引用,如果我一个aar升级了,freeline支持吗

武智

aar升级了会走一次clean-build

往之

@武智 也就是说速度和比较慢落,后续会考虑支持aar升级的秒编译吗

野崎

@往之 你们的aar是类似在build.gradle中定义dependency还是直接local的*.aar引用呢?目前freeline对于build.gradle的修改是直接重新全量的。不过你们aar升级的话,应该不是日常开发的常见case...

我是往之

@野崎 在build.gralde定义 dependency。我们拆分成了100多个工程,都是通过aar引用的,主工程里基本没有代码,所以升级aar版本 是我们日常开发很常见的case。 你们的项目是多工程依赖的形式?

评论
10F
游龙天下

File "D:\workspace\keruyun_calm4\freeline_core__init__.py", line 1, in
import build_commands
ImportError: No module named 'build_commands'

按步骤出现这个错误怎么处理啊,使用的python3.5

弦影

freeline目前只支持2.7+

silence杰

请问,解决了吗 我这两天想用也报这个错误

评论
11F
time_erhu

不错,速度立马快了许多,在正式打包发布的时候,把这个取消,会不会有影响?比如,新版本替换老版本安装的时候。

弦影

Freeline只更改了debug包,不会影响release包的处理

评论
12F
heiheiwanne

网络正常,不知道为啥一直报URL错误

  • What went wrong: Execution failed for task ':initFreeline'. > Server returned HTTP response code: 403 for URL: https://api.github.com/repos/alibaba/freeline/releases/latest
弦影

你好,你可以尝试“./gradlew initFreeline -Pmirror”

评论
13F
venusonrui

你好!我在工程中使用了注解工具androidannotations,修改后编译都会报错,找不到相应的注解,请问这是什么原因?谢谢!

野崎

@venusonrui 可以试试看新版本的...

评论
14F
我是往之

freeline 使用的dex工具是release-tools目录下的dx.jar 和DexMerge.jar吗? 我看了下,似乎是anroid官方的dex工具啊。freeline再哪用到了facebook的dx工具的呢?

野崎

@我是往之 就是 release-tools 下的这两个文件...从 buck 的代码里编译出来的...

评论
15F
xiejiajin

clean project再Rebuild project 之后 就会报错

Error:Execution failed for task ':app:compileAnzhiDebugJavaWithJavac'.

java.io.FileNotFoundException: E:\yydj\YYDJ3\app\src\main\libs\cyberplayer-1.11.0.jar (系统找不到指定的文件。)

16F
南门吹雪

不错, 两个问题:
1, 和 android studio 2.2 的比较有多大优势?
2, Android 7 采用的新的APK安装优化模式, 不再一次生成完整的本地art优化文件, 那么会有啥问题吗?

17F
hichen90669750

警警告告: [options] 未未与与 -source 1.7 一一起起设设置置引引导导类类路路径径
MainActivity.java:614: 错错误误: -source 1.7 中中不不支支持持 lambda 表表达达式式
private Runnable checkCity = () -> {
^
(请请使使用用 -source 8 或或更更高高版版本本以以启启用用 lambda 表表达达式式)
1 个个错错误误
1 个个警警告告

这够任性的竟然不支持lambda ,

野崎

0.7.2已经支持lambda了,可以试试看。

joychine@qq.com

我测试的 版本0.8.1 都不支持 lambda啊。。。。 编译报错

  • What went wrong: Execution failed for task ':myapplication:transformJackWithJackForDebug'. > Could not get unknown property 'classpath' for task ':myapplication:transformJackWithJackForDebug' of type com.android.build.gradle.internal.pipeline.TransformTask. 求回复,求关注。。。
野崎

@joychine@qq.com 不支持开启 jack 来编译...支持 lambda 指的是 retrolambda 这个插件...

评论
18F
hichen90669750

(XXXApplication) getApplication()).getPatchManager().startCheck();还报java.lang.ClassCastException: com.antfortune.freeline.FreelineApplication cannot be cast to com.xxx.XXXApplication,我调用XXXApplication里自己的方法都不能?

野崎

https://github.com/alibaba/freeline/issues/159

这个issue可以解决你的问题

评论
19F
ligangcc

刚装了一下,结果运行出现如下错误:

D:\AndroidStudioProjects\RoyalCarAndroid>#
'#' 不不是是内内部部或或外外部部命命令令,,也也不不是是可可运运行行的的程程序序
或或批批处处理理文文件件。。

D:\AndroidStudioProjects\RoyalCarAndroid>python freeline.py
File "freeline.py", line 39
print 'Freeline only support Python 2.7+ now. Please use the correct version of Python for freeline

环境: win10 + Python3.4 + Android Stuido 2.2.1
我的电话:181 156 00310

野崎

@ligangcc 日志有提示了呀...目前不支持 Python 3+

评论
20F
ligangcc

另外有些介绍文章说要在onCreate里面加:
freelineCore.init()

但我看官网上的介绍没提到这个,到底要不要加这个语句?

野崎

目前不支持python 3以上的版本,然后在0.7.2里,是可以不用加 FreelineCore.init(this); 了。

遗迹

sdk version 配置的SDK_VERSION = 'Google Inc.:Google APIs:21'
生成的freeline_project_description.json中
"compile_sdk_directory": "****/sdk/platforms/Google Inc.:Google APIs:21",
编译失败

何必昵称

build failed with script: gradlew.bat :app:assembleHuaweiDebug -P freelineBuild=true --
stacktrace
啊啊啊,按demo配置完渠道后就报这个错,不知道怎么解决

风成月

遇到这个问题,咋回事呢?

  • What went wrong:
    A problem occurred configuring root project 'SelfApp'.

    Could not resolve all dependencies for configuration ':classpath'.
    Could not resolve com.android.tools.build:gradle:2.2.3.
    Required by:
    :SelfApp:unspecified
    Could not resolve com.android.tools.build:gradle:2.2.3.
    > Could not get resource 'https://jcenter.bintray.com/com/android/tools/build/gradle/2.2.3/
    gradle-2.2.3.pom'.
    > Could not GET 'https://jcenter.bintray.com/com/android/tools/build/gradle/2.2.3/gradle
    -2.2.3.pom'.
    > sun.security.validator.ValidatorException: PKIX path building failed: sun.security.
    provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested
    target
    Could not resolve commons-io:commons-io:2.4.
    Required by:
    :SelfApp:unspecified > com.antfortune.freeline:gradle:0.8.4
    Could not resolve commons-io:commons-io:2.4.
    > Could not get resource 'https://jcenter.bintray.com/commons-io/commons-io/2.4/commons-io-
    2.4.pom'.
    > Could not GET 'https://jcenter.bintray.com/commons-io/commons-io/2.4/commons-io-2.4.po
    m'.
    > sun.security.validator.ValidatorException: PKIX path building failed: sun.security.
    provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested
    target

  • Try:
    Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more l
    og output.

BUILD FAILED

1154098106271496

接入时遇到这个问题:Error:Execution failed for task ':initFreeline'.

Can't get http://static.freelinebuild.com/freeline/0.8.7/all/freeline.zip to E:\android_workspace\TgnetInformation4.0\freeline.zip.tmp,怎么解决呐?

评论
关注
弦影
蚂蚁金服高级技术专家,专注客户端终端基础架构,...
1篇文章|38关注
阿里云移动APP解决方案,助力开发者轻松应对移动app中随时可能出现的用户数量的爆发式增长、复杂的移动安全挑战等... 更多>

为金融行业提供量身定制的云计算服务,具备低成本、高弹性、高可用、安全合规的特性。帮助金融客户实现从传统IT向云计... 更多>

阿里云依据网站不同的发展阶段,提供更合适的架构方案,有效降低网站的开发运维难度和整体IT成本,并保障网站的安全性... 更多>

为您提供简单高效、处理能力可弹性伸缩的计算服务,帮助您快速构建更稳定、安全的应用,提升运维效率,降低 IT 成本... 更多>
MaxCompute75折抢购

MaxCompute75折抢购