在 2019 年,Flutter 推出了多个正式版本,支持的终端越来越多,使用的项目也越来越多。Flutter 正在经历从小范围尝鲜到大面积应用的过程,越来越多的研发团队加入到 Flutter 的学习热潮中,京东作为互联网大厂之一也积极参与了 Flutter 的跨端方案研究。本文将介绍京东在 Flutter 上的应用方案和相关优化成果。
为什么考虑Flutter技术方案
京东在Flutter的实践
Flutter工程改造: 对Flutter开发环境和dart代码管理进行优化,可以无缝集成到现有APP中并支持自动化dart编译打包,便于开发和调试。
路由及多页面管理: 对原生页面和flutter页面实现了集中路由管理,可以双向传参、跳转并且进行了共享内存优化。
扩展UI组件库: 官方支持的Material和Cupertino样式不能满足需求,我们内部实现了自定义样式的组件库。
原生能力扩展: 对官方原生能力进行了扩展,封装了包括网络、登陆、埋点等等基础能力的打通并提供了50+原生扩展API。
Android端动态化支持: 在Android端实现了动态化支持,可以线上热更新业务。iOS端暂不支持动态化。
JDFlutter框架设计
基础框架
基础层:提供了Flutter的基础组件支持,包括组件管理,状态管理等;基础层完全独立,对业务没有依赖。
通用业务层:提供了通用型业务组件支持,例如登录组件,支付组件等;通用业务层依赖于基础层。
业务层:即具体业务逻辑实现层,根据业务需要进行不同组件的组合,实现业务页面的快速开发。
核心组件
组件管理:组件之间通过标准的协议接口进行通信,降低组件耦合,便于维护及组件升级;
状态管理:实现数据和界面分离,统一状态管理,以数据的变化来驱动界面的改变,更有利于数据的持久化和保存,同时也有利于UI组件的复用;
Hybrid Router:主要解决Flutter和Native之间交叉跳转的问题,减少内存开销,共享同一个Flutter Engine。
工具介绍
编译发布:优化Flutter原有的编译逻辑,管理依赖Flutter原生依赖关联,打包Flutter和原生代码,实现自动化构建发布。
资源管理:管理图片资源,将资源转换成Flutter类,便于资源的读取操作,类似Andorid的R类;
模版代码生成:减少Flutter的代码编写,自动生成Flutter 组件的框架模板代码,提升代码编写效率;
JSON转换:将JSON数据转换成Flutter code,并提供json转Flutter对象的API,减少动手编写Flutter code及解析。
JDFlutter业务开发实践
配置混合工程
Android平台配置
flutter create -t module --org com.example my_flutter
// 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
))
dependencies {
implementation project(':flutter')
}
// MyApp/settings.gradle
//projectName 原生模块名称
//projectPath 原生项目路径
include ":$projectName"
project(":$projectName").projectDir = new File("$projectPath")
iOS平台配置
flutter create -t module my_flutter
pod init
#在Podfile文件添加的新代码
flutter_application_path = '/{flutter module目录}/my_flutter'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
pod install
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
~ $ 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
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
/build
dependencies:
hello_plugin:
hosted:
name: hello_plugin
url: http://localhost:8080
version: 0.0.2
dependencies:
hello_plugin:
git:
url: git://github.com/hello_plugin.git //git地址
ref: dev-branch //分支
Flutter业务的开发与调试
$ cd flutterProjectPath/
$ flutter attach
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".
JDFlutter热更新实践
Flutter App的包结构
1、icudtl.dat
2、isolate_snapshot_data
3、isolate_snapshot_instr
Flutter包的初始化流程
配置了一些环境数据,比如各个核心包的路径,主要是提供给其他一些模块全局调用
检查 asset 下 Flutter 包的完整性,主要是上面介绍的一些核心包,一旦缺少核心的一些库,就会直接抛异常。开发过程中我们经常因为配置导致有些文件没有打包进去,然后会直接 crash,就是在这里触发的,具体代码如下:
解压部分 asset 下的资源到 data 分区,以下是一些片段的代码,那为什么要解压呢?放在 asset 下也是可以通过 assetManager 读取的。这里 google 应该是从性能角度要求解压的,因为频繁的使用 assetManager 读取 asset 是很容易造成多线程阻塞的,一旦阻塞了将会导致整个 Flutter 业务全部无法渲染,所以需要解压一些核心的资源库,而不是解压了所有的资源 (例如图片就没有解压)
运行原理
上面是对Flutter程序加载的分析,最终Flutter页面显示是需要呈现在原生组件Flutter View中的,这个组件会和底层Flutter Native View 进行绑定,并最终运行上面说到的data分区的Dart代码来渲染UI。如果使用的是Flutter Activity,则默认Flutter View是全屏显示,如需要定制页面,需要自己设计Activity。
热修复实验
先打开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
关闭 Flutter 页面,在 Task 中杀掉进程,回来后重新打开 Flutter 页面,就能看到改动的效果,图片资源是存放在 flutter_asset 目录的,将图片放到这个目录,同样能更新图片
上面这个实验,验证了方案基本是可行的,但这里只是简单替换,实际使用中替换还是有很多问题的。那 Google 官方是如何设计的呢?
Google热修复设计
热修复步骤
在页面初始化时,检查固定的下载更新目录有没有业务升级包,从代码来看,必须在manifest中打开该功能,设置DynamicPatching
每次 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 和文件下载时间确定的,完成文件覆盖后会重新生成。
以下是升级包的大概路径如下:
如何配置服务器
整体流程
存在的缺陷
过于定制化,全部在引擎完成,很难适配一些特殊的需求定制;
不支持现在比较主流的升级流程,诸如灰度和白名单等功能;
版本号的维度不好控制,同时不能做版本回滚等操作。
JDFlutter如何实现热修复
实现原理
热修复规划
升级后及时更新页面:现有方案(包括标准google升级方案)没有办法做到下载业务包或者替换业务包后及时刷新页面,需要restart进程后重新开启才能刷新页面。未来我们会优化引擎,通过释放底层资源并重新加载,来完成随时刷新页面的功能。
未来展望
点击了解京东云“移动跨端开发解决方案”