在 2019 年,Flutter 推出了多个正式版本,支持的终端愈来愈多,使用的项目也愈来愈多。Flutter 正在经历从小范围尝鲜到大面积应用的过程,愈来愈多的研发团队加入到 Flutter 的学习热潮中,京东做为互联网大厂之一也积极参与了 Flutter 的跨端方案研究。本文将介绍京东在 Flutter 上的应用方案和相关优化成果。java
其实京东很早就开始研究并实践跨端的开发解决方案,最先使用的是Hybrid App的技术方案,从2015年低开始逐步转向RN技术栈,目前应该是业内RN技术平台应用最普遍、配套设施比较完善的公司之一。从2018年中开始,咱们也关注到了Flutter技术,最吸引咱们的特性是高性能和兼容性。这两点也是目前RN技术相对不足的地方。高性能指的是复杂场景和交互下的渲染性能,兼容性指的是不一样终端平台上的布局和体验的一致性,这点在碎片化严重的android平台上尤为重要。android
随着2018年末Google正式发布了Flutter预览版本,京东内部也愈来愈多的研发团队有用Flutter进行开发业务的诉求。咱们正式启动研发并内部发布了JDFlutter引擎。在官方Flutter引擎之上,咱们作了额外的优化和功能扩展:ios
目前京东商城、京东视频、京东到家、京东物流、7Fresh等APP都有业务采用JDFlutter进行开发。git
JDFlutter总体的框架结构,主要包含:基础框架、组件、工具三部分,如图所示:github
JDFlutter基础框架分为三层架构,包含JDFlutter基础层,通用业务层,业务层。算法
JDFlutter为业务研发团队提供了全流程的开发解决方案:shell
Flutter和原生混合开发有两种状况,其一,开发Flutter业务的同窗,须要和原生作交互,所以须要有Flutter和原生的混合编译环境;其二,使用原生SDK开发业务的同窗,须要和Flutter业务一块儿集成打包,此时需对Flutter透明,以减小对Flutter编译环境的依赖,而且,只依赖原生编译环境便可,此时咱们将Flutter编译成aar依赖,放入原生项目中便可。接下来,咱们将重点介绍Android和iOS的混合编译环境配置。json
建立一个flutter module小程序
flutter create -t module --org com.example my_flutter
复制代码
在原生根项目的settings.gradle加入以下配置信息xcode
// MyApp/settings.gradle
include ':app' // assumed existing content
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'my_flutter/.android/include_flutter.groovy' // new
))
复制代码
在原生App模块中加入flutter依赖
dependencies {
implementation project(':flutter')
}
复制代码
这样就能够原生项目一块儿编译了。
具体能够参照官方文档:github.com/flutter/flu…
这样的方式虽能够知足混编需求,但还不是特别方便,开发完项目后,还须要去Android Studio项目中进行编译,比较麻烦,因此咱们也能够把Flutter项目settings.gradle改造,在Flutter开发环境下直接运行包含原生代码的混合项目,改造方式以下
// MyApp/settings.gradle
//projectName 原生模块名称
//projectPath 原生项目路径
include ":$projectName"
project(":$projectName").projectDir = new File("$projectPath")
复制代码
这样改造以后便可在Flutter IDE中直接编译Flutter混合工程,并进行调试,也能够运行futter run来启动Flutter混合工程,不过在配置的时候,须要注意Flutter中 gradle编译环境和原生编译环境的一致性,若是不一致可能会致使编译错误。
建立flutter module
flutter create -t module my_flutter
复制代码
进入iOS工程目录,初始化pod环境(若是项目工程已经使用Cocoapods,跳过此步骤)
pod init
复制代码
编辑Podfile文件
#在Podfile文件添加的新代码
flutter_application_path = '/{flutter module目录}/my_flutter'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
复制代码
安装pod
pod install
复制代码
打开工程(***.xcworkspace) 配置build phase,为编译Dart 代码添加编译选项
打开iOS项目,选中项目的Build Phases选项,点击左上角+号按钮,选择New Run Script Phase,将下面的shell脚本添加到输入框中:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
复制代码
Flutter开发中使用的组件,通常公司内部会采用共享的方式,以免重复开发,而Flutter组件共享,即须要使用pub仓库。因为公司内部的业务组件不适合上传到pub官方仓库,所以,须要搭建私服仓库,以解决各个业务研发团队,对Flutter组件共享须要。
感兴趣的同窗能够研究下官方pub仓库的源码 pub.dartlang.org/,其对Google Cloud 环境有很大的依赖 , 也能够基于https://github.com/kahnsen/pub_server来搭建一个简易版本的私服仓库,以知足上传和下载功能,pub协议相对比较简单,咱们能够在源码增长协议接口来实现更多功能。
运行pub_server
~ $ git clone https://github.com/dart-lang/pub_server.git
~ $ cd pub_server
~/pub_server $ pub get
...
~/pub_server $ dart example/example.dart -d /tmp/package-db
Listening on http://localhost:8080
To make the pub client use this repository configure your shell via:
$ export PUB_HOSTED_URL=http://localhost:8080
复制代码
发布一个Flutter组件须要修改 pubspec.yaml,增长如下内容
name: hello_plugin //plugin名称
description: A new Flutter plugin. //介绍
version: 0.0.1//版本号
author: xxx <xxx@xxx.com>//做者和邮箱
homepage: https://localhost:8080 //组件的介绍页面
publish_to: http://localhost:8080//仓库上传地址
复制代码
上传时可使用以下命令检查代码错误,并显示出上传的目录结构
pub publish --dry-run
复制代码
若是有不想上传的文件,能够在根目录增长一个.gitignore文件来忽略以下
/build
复制代码
Flutter组件的依赖配置,在项目的pubspec.yaml中dependencies:下增长以下信息
dependencies:
hello_plugin:
hosted:
name: hello_plugin
url: http://localhost:8080
version: 0.0.2
复制代码
这样能够在公司内部实现Flutter组件共享,若是不想搭建本身的pub仓库,也能够采用git依赖,配置以下
dependencies:
hello_plugin:
git:
url: git://github.com/hello_plugin.git //git地址
ref: dev-branch //分支
复制代码
在Flutter IDE中编译代码调试会很方便,直接点击debug按钮便可进行代码调试,若是是混合工程在Android studio或者xcode中运行的工程,则没办法这么作,但也能够实现调试:
将要调试的App安装到手机中(安装debug版本),链接电脑,执行以下命令,同步Flutter代码到设备的宿主App中
$ cd flutterProjectPath/
$ flutter attach
复制代码
执行完命令后会进行等待设备链接状态,而后打开宿主App,进入Flutter页面,看到以下信息提示则表示同步成功
zbdeMacBook-Pro:example zb$ flutter attach
Waiting for a connection from Flutter on MI 5X...
Done.
Syncing files to device MI 5X... 1.2s
🔥 To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on MI 5X is available at: http://127.0.0.1:54422/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".
复制代码
打开http://127.0.0.1:54422能够查看调试信息,若有代码改动能够按r来实时同步界面,若是改动没有实时生效能够按R从新启动Flutter应用。
大部分跨端框架,诸如React Native / Weex / H5等,基本都能作到随时进行热修复,并随时上线,用于及时修复突发的在线问题,架构很是灵活。Flutter因其AOT的设计,预想会很难达到这种灵活度,但技术上仍具备必定的可行性,正如咱们在以前的Flutter介绍文章中提到的,按照先有的API设计,是能够支持热修复的,但仅限于Android。官方最新的架构上已经支持了热修复架构,你们能够更新到1.2.1版本查看,可是官方的功能还比较弱,没法作到版本控制和回滚的灵活性,因此JDFlutter并无采用。
咱们能够首先一块儿看一下Google官方热修复方案的设计原理:
Flutter1.2.1版本引入了Dynamic Patch
Flutter页面启动时是如何加载这些代码的呢?那就要从Flutter的初始化提及了,在页面启动前须要调用FlutterMain.startInitialization来作初始化:
能够看到该初始化是要求在主线程完成的,另外主要完成了如下三点:
配置了一些环境数据,好比各个核心包的路径,主要是提供给其余一些模块全局调用
检查asset下Flutter包的完整性,主要是上面介绍的一些核心包,一旦缺乏核心的一些库,就会直接抛异常。开发过程当中咱们常常由于配置致使有些文件没有打包进去,而后会直接crash,就是在这里触发的,具体代码以下:
解压部分asset下的资源到data分区,如下是一些片断的代码,那为何要解压呢?放在asset下也是能够经过assetManager读取的。这里google应该是从性能角度要求解压的,由于频繁的使用assetManager读取asset是很容易形成多线程阻塞的,一旦阻塞了将会致使整个Flutter业务所有没法渲染,因此须要解压一些核心的资源库,而不是解压了全部的资源(例如图片就没有解压)
从代码来看,先增长要解压的核心库的目录,而后启动task从asset中解压库到data分区对应app数据下的app_flutter目录,如下是解压后的目录结构:
其中res_timestamp 文件用于标记一些时间戳,算法比较固定,根据客户端的安装时间及app的version code生成,也就是说当用户打开Flutter页面后这个值就是固定的,若是有任何修改引擎会默认有变化,删除现有app_flutter的包,从新解压
上面是对Flutter程序加载的分析,最终Flutter页面显示是须要呈如今原生组件Flutter View中的,这个组件会和底层Flutter Native View 进行绑定,并最终运行上面说到的data分区的Dart代码来渲染UI。若是使用的是Flutter Activity,则默认Flutter View是全屏显示,如须要定制页面,须要本身设计Activity
了解了这些,其实热修复方案已经呼之欲出,替换原有解压后的app_flutter包,杀进程,而后从新加载Flutter页面便可。这里咱们能够作个简单的实验:
采用adb命令push一些修改过的并编译的dart代码到app_flutter目录:
先打开Flutter页面,默认会加载asset下的包,并解压到data分区
修改一个Flutter工程,并编译代码,最终在工程目录my_flutter/.android/Flutter/build/intermediates/flutter/release中看到打包生成的文件
这么文件目录中只有flutter_assets目录和isolate_snapshot_data文件是包含业务代码和图片的,其余部分基本不会变化,因此咱们这里要替换的目录也就是这两个,你们可使用adb push 命令将资源文件push到对应的data分区来作个实验
adb push my_flutter/.android/Flutter/build/intermediates/flutter/release/isolate_snapshot_data /data/data/app包名 /app_flutter
复制代码
上面这个实验,验证了方案基本是可行的,但这里只是简单替换,实际使用中替换仍是有不少问题的。那Google官方是如何设计的呢?
Flutter SDK 1.2.1中,Google提供了ResourceUpdater,用来作包的检查和下载解压。升级步骤以下:
在页面初始化时,检查固定的下载更新目录有没有业务升级包,从代码来看,必须在manifest中打开该功能,设置DynamicPatching
从逻辑上来看,只有在页面onResume或者App从新开启的时候会下载升级包,总体下载是经过http请求完成的,总体实现代码你们能够参考ResourceUpdater中DownloadTask的实现部分,这里就不细说了。
每次init的时候都会触发检查data分区的app_flutter包,若是不存在就会从aaset目录解压出来,而升级包的替换就是在这步完成的,按照逻辑会优先检查升级目录有没有包存在,若是存在则优先从升级目录解压,若是不存在仍是从asset目录解压;
固然在检查到有升级包时,会对升级包的一些配置作校验,主要是manifest.json文件,里面会包含buildNumber/baselineChecksum字段,同时也会对"isolate_snapshot_data", "isolate_snapshot_instr", "flutter_assets/isolate_snapshot_data"等文件作CRC32校验
升级后的版本时间戳是从配置的manifest.json文件中读取patchNumber和文件下载时间肯定的,完成文件覆盖后会从新生成。
如下是升级包的大概路径以下
文章上部分介绍了怎么打开升级patch的功能,因升级涉及到服务端,那Google是怎么作到关联到服务器的呢?其实原理比较简单,须要配置客户端的manifest文件的meta属性,增长PatchServerURL,也就是咱们服务的地址,以及下载模式PatchDownloadMode和加载模式PatchInstallMode,默认是ON_NEXT_RESTART(下次初始化时)
JDFlutter的总体实现原理,其实和Google是同样的,目前来看不修改引擎的前提下,只有这种方案最简单,可是咱们没有使用Google的这套升级架构,默认关闭了patch功能,并框架以外实现了替换包和加载的逻辑,优势是总体兼容性更强、更灵活。
将来,JDFlutter会继续在热修复方面进行探索和验证,以知足京东业务的快速发展须要。而针对目前的方案,咱们思考了以下的优化点:
Google Flutter是很是出色的跨端开发技术,如今已经取得了长足的发展。社区生态和框架成熟度也正在快速追赶RN。相信不久的未来,Flutter+RN必定会成为跨端开发平台的绝代双骄。
京东 ARES 跨端团队做为京东技术与数据中台的多端技术平台团队,聚焦于跨端开发技术框架和平台搭建,包括但不限于 RN、Flutter、小程序等技术栈。目前已经普遍应用于京东商城、京东金融、京东到家、京东拼购等京东系核心 App 内,帮助业务团队低成本、快速开发本身的业务,以应对市场的瞬息万变之势。