文章目录
FreeLine 框架解析
这篇文章主要是为了解析 Freeline 框架的实现原理和流程,篇幅较长,所以大家可以按照章节来阅读。
目前主流的动态编译方案对比
编译呀,坑呀。传统的做法是,修改代码,编译,从新安装 app,运行…项目越大,越让人抓狂,及时升级到了 as 2.0 构建速度明显上去了,安装和执行gradle task 的任务仍旧让你抓狂。所以,就出现了各种动态编译或者类似的做法。正常的一次run或者build,需要好几分钟乃至十几分钟,如果采用动态编译可以把时间降到30秒以内,这个是可观的。
框架 | 简单描述 | 技术实现 | 适用 |
---|---|---|---|
Buck | 由FB 公司开发的,多流水的并发增量编译框架,国内的话,有微信在使用这个框架 | 只支持Mac,对项目入侵性大,性能高 | 放弃 |
LayoutCast | 不支持修改了Activity生命周期方法,只支持 ART虚拟机(5.0 以版本),删除资源不生效 | 集成简单 | 放弃 |
InstantRun | as 2.0 以后的功能,需要升级 gradle puligins 版本才可以使用 | 更新as和gradle puligins 版本即可,支持四种更新 | 放弃 |
Free Line | 阿里的增量编译方案,16年10月份开源 | 集成简单 | 正在使用 |
layoutcast
github 地址: https://github.com/mmin18/LayoutCast
原理: 在 LayoutCast.init(context)被调用的时候,会在后台启动一个 http 服务,用于接受命令,本地Cast脚本保持着电脑和运行app的通信,Cast 通过id获取了应用的所有资源,然后编译成 public.xml 文件,这个public.xml文件其实就是代码打包成 apk 之后的所有资源清单。Cast脚本会扫描项目文件夹,去扫描 /res 和 /src 文件夹,依旧其中所有的文件依赖,如果运行 LayoutCast,会通过 aapt 将资源打包通过 http 发送到手机上,然后解压替换资源文件,调用 Activity.recreate()方法,启动activity。
不足之处:
- 由于是保留了 Activity Stack,如果是一些修改了 Activity 生命周期的代码,需要重新run。(在onSaveInstanceState()之外的方法)
- 只支持 ART 虚拟机,也就是 android 5.0 以上。
- 不支持 Clean Task。
- 扫描和引用了全部资源ID,项目过大,可能导致花费时间比较长。
- 删除和重命名资源,不能启动 layoutcast 增量更新。
- 对项目结构仍旧有要求,只能支持 project/src 和 project/src/main/java 这两种结构。
亮点:
支持 mac 和 windows,支持 android studio 和 eclipse。
Buck
Buck 是 FackBook 公司开源的一个 github 地址 https://github.com/facebook/buck 官网地址 https://buckbuild.com/
使用 Buck 为了配合其并发任务,需要更改项目结构,分成很多 module,而且 Buck 仅仅支持 mac 系统。所以,这里的话,基本可以考虑放弃使用这个了。
android studio Instant run
原理和 LayoutCast 差不多,好处就是,只需要升级 android studio 和 gradle plugins 版本就好了。 官网介绍地址 https://developer.android.com/studio/run/index.html?hl=zh-cn
**缺点: **
- android studio 版本必须和 gradle plugins 版本一致才可以。实际上从 as 2.0 开始就有 Instant run,但是我在 2.2 版本搭载 2.1.3 的 gradle plugin 发现是不能使用 Instant run的。
- 文件路径太长,导致报错,而且是某个文件路径太长,导致其他文件报错,报错信息为:请求的操作无法在使用用户映射区域打开的文件上执行。这个很难处理,单单是文件名过长,aapt 打包的是就会自动报错,提示 invalid filename,但是这里就坑了,因为在官网和google完全找不到这个报错,我应该把错误提交到了android studio 官网。
- android 5.0 以上才能使用 cold swap,也就是你增加删除,方法和增加删除类之类的大操作都是 cold swap。(其他两种为hot swap和warn swap,前者是改动布局文件,后者是改了现有方法)
- minsdkVersion 为15,如果为了支持全部特性,则需要21
阿里 freeline
阿里的 freeline 是在16年八月份开源出来的一个动态编译框架,是蚂蚁金服旗下研发和推广的。github 地址 https://github.com/alibaba/freeline,官方介绍 https://yq.aliyun.com/articles/59122?spm=5176.8091938.0.0.1Bw3mU
Freeline 部署步骤
include Freeline 插件
在 Project的 build.gradle 增加下面代码:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.antfortune.freeline:gradle:0.7.0'
}
}
使用 插件并依赖depenencies
在 app 的 build.gradle 增加下面代码:
apply plugin: 'com.antfortune.freeline'
android {
...
freeline {
hack true
}
}
初始化项目
进入项目文件夹,然后执行:
- Windows[CMD]: gradlew initFreeline
- Linux/Mac: ./gradlew initFreeline
- 国内用户,可以在后面加 “-Pmirror” 参数,使用国内镜像下载
tip:如果在下载中断的话,可以在初始化命令中加上 “-Pmirror”,例如 Windows 中是 gradlew initFreeline -Pmirror
AS 插件
安装 Freeline 插件(包含Freeline命令行插件和run插件),在setting-piugins-browse repositories 输入 freeline,安装,然后重启 as
第一次运行 freeline run,使用全量运行,在as的freeline 控制台输入: python freeline.py -f。其中 “-f” 参数表示全量包。
修改代码之后,点击插件图标或者执行命令 python freeline.py。
注意:需要安装 python 环境,而且 python 版本要在 2.7 以上。(CMD 输入 python)
freeline 注意事项
- 启动失败,必须重新安装,使用 Freeline run 是不行的,会提示 Application 没有运行的错误。
- 第一次run,必须使用 Freeline run,而且速度和直接 run 是一样慢的,不要着急。参看命令行输出,第一次 Freeline 也是执行 gradlew assemble命令的,所以会比较慢,我这边第一次run大概是230秒,
- 第一次 freeline run,必须使用全量包,也就是执行 python freeline.py -f 命令,而不是 python freeline.py,as 面板插件也是直接执行 python freeline.py,所以也不能直接点击图标去run,之后修改了代码,就可以使用增量包了。第一次没有使用全量包,也会出现application没有运行的错误。
- 修改了 Application,最好是卸载了app,然后重新全量 Freeline run 一次,避免其他未知问题。
- 需要在运行状态点击 freeline run,而且只能一次性调试一个部署了 freeline 的应用。非运行状态点击,不会帮你启动 app,但是会把修改的patch推送过去,你自己点击启动,是可以看到修改的。
- 如果报错为和手机连接失败,则查看电脑和手机是否同一网段(手机连接的wifi 是kugou,且 as 没有设置代理)
- 只允许一台手机连接 adb,现在的版本还不支持多台手机
freeline 使用总结
最早我们把 Freeline 集成到 自己的项目中去,可是当时(10月中)不支持 productFlavors 所以没办法继续下去;下面是其它项目的一些集成数据。
测试参数 | 测试数值 |
---|---|
第一次 Freeline run | 377s |
Freeline 实现原理
Freeline 借鉴了 Instant run,layoutcast,buck的思想和特点,主要采取了 buck 的多任务并发和layoutcast 增量更新的思想。下面的内容会介绍一些相关原理。
buck 多任务并发
buck 是 FackBook 的构建系统,大概 13年左右开发出来的,官方地址 https://buckbuild.com/ ,由于仅仅支持 Mac,而且对项目入侵性比较大,所以国内使用的不过,据说微信团队就是使用这套构建系统,如微信这么大的 project,构建也是在一分钟那个以内的,所以优化效果不言而喻。
为什么 Buck 能加快构建速度呢?主要从以下几点进行了优化:
- 根据有向拓扑图和构建规则拆分构建任务,并发执行已拆分任务
buck的原则是,明确一切影响输出的输入选项,利用有向拓扑图,拆分任务,当前任务执行完成之后,会向下一个依赖的任务发送通知,被依赖的任务接受到通知之后,检测自己的所有上一级任务是否完成,如果完成了就把自己放入执行线程队列里面。
那么这里的话,是如何拆分流水任务的呢?这里首先要从apk 打包说起,传统的 apk 打包流程如下:
- 打包资源文件,生成R.java文件
- 处理aidl文件,生成相应java 文件
- 编译工程源代码,生成相应class 文件
- 转换所有class文件,生成classes.dex文件
- 打包生成apk
- 对apk文件进行签名,debug 版本使用默认签名,所以这步感觉是不可见的
- 对签名后的apk文件进行对其处理,例如使用 zipalign 进行压缩处理
这里的任务是单线流水的,所以其中一个环节花费时间多,下个环节只能一直阻塞。
我们使用 gradle 打包的时候,会执行类似以下命令:
gradlew assembleRelease
如果你是直接点击 as 面板上的 run,实际上是执行了一个 task:“assembleDevelopDebug”,其中 assemble 是任务类型,DevlopDebug 是一个 buildVariant。
然而,实际上 gradle 里面发生的过程确实比较繁琐的,如下图说明:
tip:参看这个图可能比较模糊,可以下载原文件进行查看,svg 格式,如果没有安装 SVG 工具,建议用 IE 打开,chrome 不支持缩放。
上面的一些 build tools 介绍如下 表:
名称 | 功能介绍 | 路径 |
---|---|---|
aapt(android asset package tool) | android 资源打包工具 | buildTools 文件夹下对应版本的文件夹 |
aidl | 将aidl转换为 Java 语言 | buildTools 文件夹下对应版本的文件夹 |
javac | java 编译器 | |
dex | dex 工具,将class 文件转换为 davlik 可识别的 dex 二进制文件 | |
apk builder | 生成 apk 工具 | |
zipalign | 字节码对齐工具 | buildTools 文件夹下对应版本的文件夹 |
-
首先是对 资源文件的映射,这部分工作由 aapt 完成,会把全部资源在 R 文件中生成唯一一个ID,其实R.java文件创建的就是 android.R 类实例对象,其次 manifest 文件也对应着一个 java 类 android.Manifest 类。
-
Buck 缓存机制
Buck 根据输入单一原则,把输入的 hash 值作为缓存的键值,然后缓存一些 build 任务输出,如果下一次的相同 build 输入的 hash 相同,则直接使用缓存。
下面是影响缓存的一些 rule:
- 在 build 文件中定义了build rule 的相关
- 输入内容
- 构建工具,例如buck的版本
- 每一个 rule 依赖的缓存 key
tip:一些 clean task 会清空缓存,这点需要注意。
在 freeline 中,windows 系统下,在路径 C:\Users\用户名.freeline\cache 文件夹下可以看到cache,这里的话
-
避免 jar 的无效构建
在 buck 里面,如果你修改了 Java 代码,但是不会影响 可见的 api,这样的话,jar 是不会被重新编译的。
-
Rules 的自我管理
-
Buck task 只关心自己依赖的 task
-
自定义的 dex 分包工具
在下载的的 freeline.zip 中我们可以看到 freeline,有自己的 dex 分包和 merge工具,还有 自己的 appt 打包工具。具体我们可以看到,这里是代替了 android 原生的 dex 和 aapt 工具的。
freeline 根据任务的依赖关系,将不同的任务划分层级,然后让同一层级的任务并发执行,默认使用八个并发线程,这样的话就提高了某些可拆分环节的执行速度,下面是官方一张粗略的任务拆分流程图:
根据官方的介绍,对于单个工程的流水任务如下图,这里将code 和 resource 进行拆分是很合理的,无论从构建工具的角度考虑还是程序员的角度考虑这里的拆分都是有意义的。
Freeline 扫描机制
Freeline 会扫描 freeline{} 语句块设置的 SourceSet 下的代码,以及依赖的 mudule,及所有的 resource。实际上你每次运行 freeline,实际上在 项目/app module/build/freeline 的 stat_cache.json 里面记录了你当前 SouceSet 下的资源和文件,依赖的 moduel 及其依赖的资源,所有的信息。具体的格式如下:
"pure_java": {
"E:\\LocalRepository\\freeline-master\\sample\\pure_java\\src\\main\\java\\foo\\SomeFoo.java": {
"size": 196,
"mtime": 1475915358.632
}}
最外层是所属的module 或者 resource,然后里面是一组value:数组的形式,value 数值为文件的绝对路径,数组里面含有两个数值,分别是文件大小和上次修改的时间。这里的文件大小不是具体的文件大小,而是有其他的意义。
每次 freeline 运行,就会先读取这个 json 文件,然后根据 gradle.build 去扫描当前的文件夹,执行以下流程:
这里需要说明的是:在我们使用版本是 0.70 版本,如果在不同的分支代码里面新建了 manifest 文件,是不会被扫描记录的,freeline 只记录 app/src/main 下的 AndroidManifest文件,但是实际上gradle 运行的时候会把所有 Manifest 文件进行合并。
tip:对于代码文件,如果为 productFlavors 设置了特定的 SourceSet,则会继承 main 里面的代码,并且也会使用SourceSet 规定文件夹下的代码,但是不允许有相同命名的类,这点是 Gradle 的规范。
Freeline TCP 连接
当增量包生成的时候,Freeline会通过 http://127.0.0.1:端口号 (访问手机端口),建立一个 tcp 长连接,然后在成功发送完文件的时候,释放 tcp 链接。这里的端口号是范围选择的,具体的解释参看下文。
freeline 使用 python 脚本来向客户端推送增量包,我们在 freeline_core 文件夹下的 sync_client.py 下可以看到以下代码:
def connect_device(self):
self.debug('start to connect device...')
self._port = self.scan_device_port()
if self._port == 0:
for i in range(1, 25):
need_protection = i <= 1
self.wake_up(need_protection=need_protection)
self._port = self.scan_device_port()
if self._port != 0:
break
time.sleep(0.2)
self.debug('try to connect device {} times...'.format(i))
if self._port == 0:
self.check_device_connection()
self.check_installation()
message = 'Freeline server in app {} not found. Please make sure your application is properly running in ' \
'your device.'.format(self._config['package'])
self.debug(message)
from exceptions import NoDeviceFoundException
raise NoDeviceFoundException(message, NO_DEVICE_FOUND_MESSAGE)
self.debug('find device port: {}'.format(self._port))
这段代码大概描述了如何连接手机设备,然后我们在看一下 scan_device_port() 方法里面具体的连接代码:
for i in range(0, 10):
cexec([self._adb, 'forward', 'tcp:{}'.format(41128 + i), 'tcp:{}'.format(41128 + i)], callback=None)
url = 'http://127.0.0.1:{}/checkSync?sync={}&uuid={}'.format(41128 + i, sync_value, uuid)
result, err, code = curl(url)
从上面两段代码可以总结出 TCP 连接机制,如下图所示:
建立连接之后,freeline 会把增量的 dex 包发送过去,dex 增量包的缓存在 app/build/freeline/arsc_cache.dat,每次构建增量包,会删除旧的增量包。
那么如何发送增量包的呢?看下面的代码:
result, err, code = curl(url, body=fp.read())
// curl 方法关键代码如下
result = urllib2.urlopen(url, data=body).read().decode('utf-8').strip()
这里通过 urllib2 网络标准库,发送 request 到手机,其中的 request body,就是增量包,然后响应手机的接收信息(接收成功或者接受失败)。
科普
简单说一下 127.0.0.1这个IP地址,我们一般认为这个是本机的 IP 地址,实际上这个地址是分配给 loopback 接口的,loopback 接口是一种特殊的网络接口(或者说是虚拟网卡),用于本机各个应用之间的网络交互。Windows 上看不到这个接口及其相关信息,在 Linux 上这个接口叫做 lo。所以其实这样理解本机地址和网卡的关系,一般的电脑存在以太网卡,无线网卡,还有上面说的 loopback 虚拟网卡,所以实际上本机地址有三个,就是三个网卡的实际 IP 地址,这样的话,你就可以理解了。
自定义的 AAPT 工具
android build tools 自带的 AAPT 工具主要负责两个工作,将资源文件映射生成 R.java 文件和将xml资源文件(除了png类型的图片)生成二进制xml文件。其中非asssets 资源都会自动生成一个 ID 保存在 R.java 文件中,同时生成一个 resource.asrc 文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。
由于 freeline 的 aapt 工具没有开源,所以比较难发现里面做了什么优化,但从工具角度来看,android 24.0.2 build tools 下 aapt 才 1 M多大小,但是 freeline 的aapt 多达15M,freeline 团队肯定做了不少优化。