一、Flutter介绍
Flutter是Google开源的移动开发框架,主打高性能、跨平台。从2019年下半年开始,集团部分业务线开始尝试使用Flutter,我们主要关注Flutter的高性能、跨平台和热更新特性。
1、高性能
高性能主要是与Hybrid、RN/Weex等动态化方案做比较。首先Flutter使用Skia作为渲染引擎,所以在UI布局时不需要像RN/Weex那样在JavaScript和Native之间通信,其次Flutter 采用Dart语言开发,在发布时使用AOT模式直接编译成机器码,执行效率高于JavaScript的解释执行。
2、跨平台
Flutter自带Skia渲染引擎和Dart运行时,极大减少了对运行平台的依赖性。真正实现一处编写多处运行,目前已经支持Android、iOS 、Fuchsia、macOS、Web(目前beta版)、Windows和Linux(官方还在开发中)。
3、热更新
虽然目前官方表示暂时不支持热更新:
This is currently not on Flutter’s roadmap, for reasons discussed in this comment:#14330 (comment)
但:
- 在Android平台,Flutter曾在1.2.1版本加入了Dynamic Patching特性来支持热更新,从2019年开始该特性不再支持。我们可以参考相关源码自行实现。另外Flutter在Android平台将业务逻辑打包成libapp.so,我们可以借助so的热更新方案实现Flutter的热更新。
- 在iOS平台,国内大厂都在积极探索Flutter的热更新方案,例如MXFlutter(QQ团队)、Kraken(淘宝)等,下文也会介绍我们在iOS平台热更新的尝试。
二、Flutter工程化实践
(一)开发模式
1、混合开发
由于我们的App已有线上运行的Native版本,所以我们采用Native和Flutter混合开发模式,并优先使用在新业务模块和频繁更新的业务模块上。工程结构如下:
- 创建独立的flutter module工程,H5团队可以直接在flutter module工程上进行开发,不用关注Native工程的环境配置(method channel需要mock调试)。并且该工程可以很方便的打包各平台产物。
- Native团队在开发阶段使用Native宿主工程引用flutter module,方便调试method channel功能,在提测和发布阶段则使用Native宿主工程引用flutter module对应的平台产物。
2、模块化开发
我们在启动Flutter项目时就考虑到公共业务模块和基础模块的封装沉淀,这些公共模块可以在不同业务线产品上实现代码复用,模块化开发也使得模块之间解耦,不同模块可以独立迭代发布。
(二)开发流程
开发流程主要分为三个阶段:开发调试、Flutter发布、Native宿主集成。其中Flutter发布环节包含Flutter Package、Flutter Plugin发布到PUB私服和Flutter各平台打包产物发布到各托管平台。比较重要的环节有:
1、打包产物合并
我们以Android平台为例讲述Flutter打包合并产物的过程:
- 使用flutter build构建aar
flutter build aar --no-debug --no-profile --target-platform android-arm
- 如上图所示构建后的产物会输出在build/host/outputs/repo下,flutter会把工程代码资源和引用到的Plugin(Native代码)分别打包成独立的本地maven库,并且版本号都是固定不变的1.0。宿主如果想集成这些产物有两种思路:
- 将所有产物maven库修正版本号后分别上传至远程maven仓库
- 合并所有产物maven库,生成一个对应flutter代码版本的maven库并上传至远程maven仓库。
- 我们使用了上述的方案二,这样宿主只需要关注一个flutter产物maven库即可,不用关注它引用的plugin库。合并打包工具的gradle脚本部分代码示例:
apply plugin:'com.kezong.fat-aar'
...
dependencies {
implementation fileTree(dir:'libs', include: ['*.jar'])
//固定引用
embed 'com.my_flutter_app:flutter_release:1.0'//第一层引用
embed 'io.flutter.plugins.sharedpreferences:shared_preferences_release:1.0'
embed 'io.flutter.plugins.webviewflutter:webview_flutter_release:1.0'
embed 'io.flutter:flutter_embedding_release:1.0.0-6158f03ef5fae3ef620264595021609ed9ea3a5c'
embed 'io.flutter:armeabi_v7a_release:1.0.0-6158f03ef5fae3ef620264595021609ed9ea3a5c'//第二层引用
api 'androidx.appcompat:appcompat:1.1.0'
api 'androidx.annotation:annotation:1.1.0'
api 'androidx.legacy:legacy-support-v4:1.0.0'
api 'androidx.webkit:webkit:1.0.0'
...
}
2、PUB私服
目前我们使用 unpub(字节跳动出品)搭建了PUB私服,也可以直接把Git私服作为PUB私服使用。
三、XFlutter开发框架
在Flutter实践的过程中我们逐渐积累完善了XFlutter框架,包括基础组件、业务组件和工具链。基础组件为所有业务组件及业务应用提供最基础的能力,公共业务组件实现了集团内不同产品线公共模块的复用,工具链则会贯穿在整个Flutter工程的实施过程中,为Flutter实施保驾护航。
四、常见问题及解决方案
(一)热更新
1、Android平台
在Android平台上我们直接对libapp.so进行热更新,主要有so的版本匹配、so的差分处理、so的下载及验证、so的加载等环节,下面我们只列举了so的加载环节的部分代码,其他环节可以参考Android开发中的Apk增量更新相关技术。
- 我们通过阅读源码发现so加载的入口源码在FlutterLoader类中:
public void ensureInitializationComplete(@NonNull Context applicationContext, @Nullable String[] args) {
//...部分代码省略if (args != null) {
Collections.addAll(shellArgs, args);//我们只需要在args中追加一条--aot-shared-library-name路径即可
}
String kernelPath = null;
shellArgs.add("--aot-shared-library-name=" + this.aotSharedLibraryName);
shellArgs.add("--aot-shared-library-name=" + applicationInfo.nativeLibraryDir + File.separator + this.aotSharedLibraryName);
shellArgs.add("--cache-dir-path=" + PathUtils.getCacheDirectory(applicationContext));
//...部分代码省略
FlutterJNI.nativeInit(applicationContext, (String[])shellArgs.toArray(new String[0]), (String)kernelPath, appStoragePath, engineCachesPath);
//...部分代码省略
}
- 设置so路径示例代码:
public void setSoPath(String soPath) {
try {
if (!TextUtils.isEmpty(soPath) && new File(soPath).exists() && new File(soPath).isFile()) {
String[] strings = new String[]{"--aot-shared-library-name=" + soPath};
FlutterMain.ensureInitializationComplete(context, strings);
}
} catch (Exception e) {
e.printStackTrace();
}
}
- 对于Flutter资源的热更新我们更多的是采用加载网络资源+本地缓存来简单实现。
2、iOS平台
在iOS平台上的热更新,我们团队目前使用Flutter Web+H5离线包的方案,主要原因有两个:一是我们的产品有一个简单的H5版本,我们希望能够使用Flutter Web来实现Web端的复用,二是目前市面上的Flutter iOS热更新方案并不是很成熟,大都摈弃了Dart语言,使得我们现有的Flutter工程无法复用。
(二)Flutter Web 兼容
上文提到我们正在尝试使用Flutter Web 来实现Web平台的代码复用及iOS平台热更新的问题,在开发过程中需要注意以下问题:
- 尽量使用Dart来实现功能而不是使用Flutter Plugin,比如RSA、AES加密算法使用纯Dart代码开发。
- 所有引用的Flutter Plugin需封装后使用,把对Web平台的适配做一层封装。
(三)崩溃监控
Flutter的崩溃监控主要分为两部分:
- Flutter Dart代码引起的崩溃,核心代码如下:
...
//1、收集Flutter framework捕获后抛出的Error
FlutterError.onError = (details) {
Zone.current.handleUncaughtError(details.exception, details.stack);
};
//2、收集Flutter app级别抛出的Error
runZoned<Future<Null>>(() async {
//在这里执行main方法中原有的代码逻辑
...
}, onError: (error, stackTrace) {
//在这里过滤error后进行打印输出或者上传监控平台
...
});
...
- Flutter engine引起的崩溃:目前我们使用bugly来监控Flutter engine的崩溃情况。
五、结束语
Flutter开发相关技术远不止这些,如Flutter的状态管理、混合开发中多容器问题、线上性能监控等,本文仅列举了公司在Flutter工程化相关的实践经验,希望对大家有所帮助。
本文来自拍码场,经授权后发布,本文观点不代表信也智慧金融研究院立场,转载请联系原作者。