内容简介:背景Flutter是Google推出的跨平台、高性能开发框架,使用Skia作为渲染引擎,不使用平台控件,保证Android和iOS上UI一致性。使用Flutter开发,Android、iOS使用一套Dart代码,可以节省开发成本。通常具有一定规模的App都有一套成熟通用的基础库,而且依赖公司体系内的很多基础库。使用Flutter重新开发时间和实现成本都很高。所以在Native App中嵌入Flutter功能的混合开发模式是应用Flutter技术的稳健型改造方式。
导语
本文主要介绍将 Flutter 应用到已有 Native 项目中混合开发遇到的问题及解决过程,以及 Flutter 应用开发框架的探索与实践。
背景
Flutter是Google推出的跨平台、高性能开发框架,使用Skia作为渲染引擎,不使用平台控件,保证Android和iOS上UI一致性。使用Flutter开发,Android、iOS使用一套Dart代码,可以节省开发成本。
通常具有一定规模的App都有一套成熟通用的基础库,而且依赖公司体系内的很多基础库。使用Flutter重新开发时间和实现成本都很高。所以在Native App中嵌入Flutter功能的混合开发模式是应用Flutter技术的稳健型改造方式。
公寓PMS是一款给公寓管家提供房源管理的APP,前期功能已使用Native开发上线。我们在该项目中使用了Flutter开发,需要实现以下功能:将Flutter集成到已有Native项目中;实现Flutter与Native页面混合管理;实现Flutter与Native通信,复用已有Native资源;实现Dart侧代码开发框架。
Flutter引擎介绍
1. Flutter架构
首先看下Flutter架构图:
图1
Flutter的架构主要分为3层:Framework,Engine,Embedder。
Framework使用dart实现,包括Material Design和Cupertino风格的Widgets,文本/图片/按钮等基础Widgets、渲染、动画、手势等。
Engine使用C++实现,主要包括:Skia,Dart和Text。Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。
Embedder是一个嵌入层,是将Flutter引擎移植到各个平台的中间层代码,主要包括渲染Surface设置,线程设置,以及插件等。
2.Flutter线程模型
Flutter Engine自己不创建管理线程。Flutter Engine线程的创建和管理是由Embedder负责的。Flutter Engine要求Embedder提供四个Task Runner。尽管Flutter Engine不在乎Runner具体跑在哪个线程,但是它需要线程配置在整一个生命周期里面保持稳定。也就是说一个Runner最好始终保持在同一线程运行。这四个主要 的Task Runner包括:
Platform Task Runner
Flutter Engine的主Task Runner,运行Platform Task Runner的线程可以理解为是主线程。类似于Android Main Thread或者iOS的Main Thread。
UI Task Runner Thread(Dart Runner)
UI Task Runner被Flutter Engine用于执行Dart root isolate代码。
GPU Task Runner
GPU Task Runner被用于执行设备GPU的相关调用。
IO Task Runner
IO Runner的主要功能是从图片存储(比如磁盘)中读取压缩的图片格式,将图片数据进行处理为GPU Runner 的渲染做好准备。
前面我们提到Engine Runner的线程可以按照实际情况进行配置,各个平台目前有自己的实现策略。Android和iOS平台上面每一个Engine实例启动的时候会为UI,GPU,IO Runner各自创建一个新的线程。所有Engine实例共享同一个Platform Runner和线程。
Flutter官方默认混合方案
多引擎模式
在混合方案中解决的主要问题是如何去处理交替出现的Flutter和Native页面。Flutter官方给出了一个Keep It Simple的方案:对于连续的Flutter页面(Widget)只需要在当前FlutterActivity打开即可,对于间隔的Flutter页面初始化新的引擎。页面示意如下图所示:
图2
这个方案的好处就是简单易懂,容易使用,但是存在比较严重的问题。如果Native页面与Flutter页面交替出现,Flutter Engine的数量会线性增加,多引擎模式会造成以下问题:
-
内存问题。多引擎模式下每个引擎之间的Isolate是相互独立的,所以每一个引擎底层都维护了图片缓存等比较消耗内存的对象。
-
冗余资源问题。通过前文可以知道,引擎在Android和iOS的实现中,每一个Flutter实例会新启动三个线程(IO,GPU和UI),从而带来了额外的资源使用。
-
页面间通信复杂。每一个Flutter页面在一个隔离的isolate中,页面间通信将会变得非常复杂。
-
插件的注册问题。插件依赖Messenger传递消息,而Messenger由FlutterNativeView实现。多引擎方式使得插件的注册和通信将会变得混乱且难以维护。
综上,由于多引擎混合方案存在比较多的问题,所以项目中没有采用此方案。
Flutter Boost实现方案
通过调研发现,阿里闲鱼推出了Flutter Boost解决方案,该方案采用的是多个Flutter页面共享引擎的实现方式,示意图如下所示:
图3
所有的Flutter页面共享一个Flutter实例(FlutterView),这种方式能够有效避免多引擎方式带来的各种问题,但是单例的实现也使页面的管理变得更加复杂。为此Flutter Boost提供了一套完整的解决方案。
下面看下Flutter Boost的整体架构图:
图4
方案实现分为Native部分与Dart部分:
Native部分概念
-
Container:Native容器,Fragment(Android),ViewController(iOS)
-
Container Manager:Native容器管理器
-
Messaging:基于Message Channel的消息通道
Dart部分概念
-
Container:Flutter Widget的容器,Flutter Navigator
-
Container Manager:Flutter 容器管理器
-
Coordinator: 协调器,接受Messaging消息,负责调用Container Manager的状态管理。
Native容器与Flutter容器(Navigator)是一一对应的,生命周期也是同步的。当一个Native容器被创建的时候,Flutter对应的容器也被创建,它们通过相同的唯一id关联起来。当Native的容器被销毁的时候,Flutter的容器也被销毁。Flutter容器的状态是跟随Native容器,这也就是Native驱动。由Manager统一管理切换当前在屏幕上展示的容器。
性能对比
下图对官方默认多引擎混合方案和Flutter Boost方案进行了性能对比:
图5 默认多引擎方式页面内存图
图6 Flutter Boost页面内存图
从上述对比图可以看出,当连续打开多个Flutter页面时,默认多引擎方式页面的内存呈线性增长,而Flutter Boost页面内存保持在一个比较稳定的范围。所以我们的项目中选用了Flutter Boost方案。
公寓PMS进入Flutter Boost
1.Dart工程部分
在Dart工程的pubspec.yaml中引入Flutter Boost:
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '0.0.410'
然后运行flutter packages get获取Flutter Boost代码到本地。
2. Native工程部分(Android)
(1)在setting.gradle中依赖Flutter工程:
setBinding(new Binding([gradle: this, mainModuleName: 'ApartmentClient']))
evaluate(new File(
settingsDir.parentFile,
'flutter_apartment/.android/include_flutter.groovy'
))
(2)在build.gradle中引入Flutter Boost的Native工程:
implementation project(':flutter')
implementation project(':flutter_boost')
至此就把Flutter Boost接入到公寓PMS工程里面了,但是要使用Flutter Boost,还需要以下工作要完成。
-
设计Flutter跳转协议,接入跳转框架
Flutter Boost框架没有集成ARouter等路由跳转框架。所以我们需要结合自己的业务特点设计跳转协议。仿照WubaRN的设计思想,我们需要在Native端有一个Flutter通用载体页,所有的路由跳转都经由Native侧跳转中心。跳转框架我们用的是58JumpCenterLib,[h1]跳转协议如下所示:
wbapartment://jump/house/flutter?params={"container_name":"personalCenter","show_guide":true}
“flutter”:Native侧载体页页面类型
“params”:跳转协议参数,其中“container_name”是固定参数,标识Dart侧的具体显示页面(Navigator);“params”里面的所有参数都经由MessageChannel透传到Dart侧。
最后需要处理一下Dart侧传过来的跳转协议,代码如下:
private void initFlutterBoost() {
FlutterBoostPlugin.init(new IPlatform() {
......
/**
* 当Dart侧打开一个本地页面,将会回调这个方法,页面参数拼接在url中
* @param context
* @param url
* @param requestCode
* @return
*/
@Override
public boolean startActivity(Context context, String url, int requestCode) {
return PageTransferManager.jump(context, url);
}
});
}
-
完善Native侧Flutter载体页
由于在公寓PMS APP中,我们需要在首页TAB页中嵌入Flutter页面,还需要支持跳转协议的单独展示页面。所以我们的做法是基于Fragment进行封装,单独页面使用FragmentActivity/Fragment的方式。
通过完成以上工作,就可以在公寓PMS项目中使用Flutter Boost框架了。
Flutter Boost的缺点及改进
Flutter Boost是从应用层出发,直接复用FlutterView从而共享Flutter Engine。Native侧实现时,需要共享FlutterView,不同Activity/ViewController切换时,需要将FlutterView从前页面的Activity/ViewController移除,然后添加到当前页面的Activity/ViewController。这个过程在Android上能够明显的感觉到页面的闪动。Flutter 1.12的发布完美的解决了这个问题,Flutter 1.12支持将Flutter Engine通过id缓存起来,然后启动页面时,可以指定使用缓存中的Engine,从而彻底解决了混合开发共享引擎的问题。页面间使用缓存引擎方案,需要将Native侧页面和Dart侧页面一一对应。可以使用Message Channel通信,结合路由跳转中心,由Native页面驱动即可。
混合开发中遇到的问题
1. Dart侧网络请求问题
在公寓PMS项目中,Dart侧网络请求使用的是开源框架dio。但是开发过程中遇到问题,登录信息、设备版本等信息是Native侧实现的,Dart侧的网络请求header没法直接获取这些信息。解决办法是通过Message Channel将Native侧的header信息共享给Dart侧。
Native侧实现:
new MethodChannel(getBoostFlutterView(), METHOD_CHANNEL).setMethodCallHandler(
new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
if (call.method.equals("getHeader")) {
IHeadersIntegration commonHeaderUtils = CommonHeaderUtils.getInstance(MainTabActivity.sRef.get());
Map<String, String> headerMap = commonHeaderUtils.generateParamMap(MainTabActivity.sRef.get());
result.success(JsonUtils.hashMapToJson(headerMap));
} else {
result.notImplemented();
}
}
});
Dart侧实现:
Map<String, dynamic> headers;
try {
final String headerString = await platform.invokeMethod('getHeader');
headers = jsonDecode(headerString);
} on PlatformException catch (e) {}
Map<String, String> params = new Map();
Response response = await dio.get(
dataUrl,
queryParameters: params,
options: Options(headers: headers),
);
2. 复用Native的资源图片问题
Flutter默认将所有的图片资源文件打包到assets目录下,但是我们并不是用Flutter全新开发的项目,图片资源放在Native侧的drawable目录下,即使是全新的Flutter页面也会有很多图片复用已有的Native侧图片,所以在assets目录下新增图片资源并不合适。但是Flutter官方并没有提供直接调用drawable目录下的图片资源的途径。
通过调研,可以通过以下方式实现Native侧的图片共享:
Message Channel方式
Dart侧通过Message Channel将资源文件名传递到Native侧;Native侧将对应名称的drawable以二进制格式传递到Dart侧;Dart侧接收到二进制格式图片后进行渲染。
Native侧代码:
BasicMessageChannel<Object> messageChannel = new BasicMessageChannel<>(getFlutterView(),
"getPic", StandardMessageCodec.INSTANCE);
messageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() {
@Override
public void onMessage(Object o, BasicMessageChannel.Reply<Object> reply) { reply.reply(drawableToByte(getResources().getDrawable(getResId(o.toString()))));
}
});
Dart侧代码:
const _messageChannel =
const BasicMessageChannel<Object>("getPic", StandardMessageCodec());
Future<Uint8List> getNativeImage(String name) async {
Uint8List result = await _messageChannel.send(name);
return result;
}
通过以上步骤,就可以将Android Native侧drawable目录下侧资源图片共享给Dart侧控件使用,从而避免了重复引入资源。
Dart侧开发框架使用
在使用Dart开发需求之初,为了快速实现功能,还有对Flutter特性不熟悉,我们没有使用开发框架,功能就是代码的堆砌。但是,随着使用页面的增多,发现项目中业务代码耦合严重,代码可维护性很差。为此,我们进行了相关调研,发现闲鱼开源了一款Flutter应用框架——Fish-Redux。
1. Fish-Redux介绍
Fish-Redux是一个基于Redux数据管理的组装式Flutter应用框架,特别适用于构建中大型的复杂应用。它的最大特点是配置式组装,它会非常干净,易编写、易维护、易协作。
下面看下Fish-Redux架构图:
图7
架构主要分为3层,自下向上依次为:
Redux
Redux是一个用来做[可预测][集中式][易调试][灵活性]的数据管理的框架。所有对数据的增删改查等操作都由Redux来集中负责。
Fish-Redux通过Redux做集中化的可观察的数据状态管理。Fish-Redux在Flutter中对传统的Redux做了改良。一个组件需要定义一个数据(Struct)和一个Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,解决了【集中】和【分治】之间的矛盾,同时对Reducer的手动层层Combine变成由框架自动完成,简化了使用Redux的困难。
Component
Component是对局部的展示和功能的封装。基于Redux的原则,Fish-Redux对功能细分为修改数据的功能(Reducer)和非修改数据的功能(Effect)。组件是对视图的分治,也是对数据的分治。通过逐层分治,将复杂的页面和数据切分为相互独立的小模块,有利于团队内的协作开发。
Adapter
Adapter也是对局部的展示和功能的封装。它是Component实现上的一种变化,优化了Flutter在使用ListView场景下的性能问题。
综上所述,Fish-Redux不仅实现了Flutter页面的状态管理,更是一套完整的Flutter应用开发框架。下面介绍一下公寓PMS是如何使用Fish-Redux进行开发的。
2. Fish-Redux在公寓PMS的应用
Fish-Redux的接入非常简单,只需在Flutter项目中pubspec.yaml的dependencies模块设置fish-redux及依赖版本,然后运行flutter packages get即可。
下面以公寓PMS中个人中心页面介绍:
下图是个人中心页面,
图8
该页面使用Flutter ListView控件实现,主要由6个item,5种item组合而成。
下面是个人中心的Page代码:
class PersonalCenterPage
extends Page<PersonalCenterPageState, Map<dynamic, dynamic>> {
PersonalCenterPage(): super(
initState: initState,
effect: buildEffect(),
view: buildView,
dependencies: Dependencies<PersonalCenterPageState>(
adapter: NoneConn<PersonalCenterPageState>() +
PersonalCenterListAdapter()),
);
}
PersonalCenterPage由State,Effect,View,Adapter组成。其中,State定义了页面的数据及状态信息;Effect定义了在页面生命周期开始时,调用网络请求api获取页面数据;View定义了页面具体的UI,包括ListView,Loading图,TitleBar等;Adapter里面定义了列表包含的Component等。下面着重看下Adapter实现:
class PersonalCenterListAdapter
extends DynamicFlowAdapter<PersonalCenterPageState> {
PersonalCenterListAdapter(): super(
pool: <String, Component<Object>>{
NORMAL_ITEM: NormalItemComponent(),
USER_INFO_ITEM: UserItemComponent(),
LOGOUT_ITEM: LogoutItemComponent(),
TODO_ITEM: TodoItemComponent(),
CONTACT_ITEM: ContactItemComponent(),
},
connector: _HouseListConnector(),
reducer: buildReducer(),
);
}
在PageCenterListAdapter中,pool中注册了列表中所包含的Component及类型;connector是连接器,负责将网络请求返回的数据转化成Component渲染时需要的数据;reducer里定义了修改页面数据的行为,当网络请求成功后,会调用该action触发页面渲染。
最后看下Component实现,以UserItemComponent为例:
class UserItemComponent extends Component<UserItemState> {
UserItemComponent() : super(
view: buildView
);
}
其中,UserItemState是该模块渲染所需数据,view则是该模块UI逻辑。
下面看下该页面整体的代码结构图:
图9
从上图可以看出,使用Fish-Redux开发会使代码结构非常清晰,尤其是当页面逻辑复杂的时候。Fish-Redux使Flutter开发变得简单,只要按照方法的要求传入对应的参数即可,实现了面向方法编程。
该实现中,将页面分解成Page->Adapter->Component的结构。当列表页中新增样式,只需要开发对应的Component并注册到Adapter中的pool即可。由于模块拆分到粒度比较细的业务单元,该页面中实现的Component也可以复用到别的页面中,避免重复开发。
由于Fish-Redux中包含了Redux的功能,使得开发过程中的状态传递变得非常简单,只需注册Action,在接收Action的地方设置响应逻辑,在触发的地方调用dispatch(Action action)方法即可。
此外,Fish-Redux的好处是将逻辑与视图隔离开,view只负责具体的页面渲染;而逻辑通过Effect和Reducer实现。所以有这样的公式Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。这不仅很好的实现了代码的解耦,也为以后实现UI代码自动生成,开发人员只开发业务逻辑代码的开发模式提供了可能。
借鉴Flutter中面向函数编程,可插拔的页面组件化思想,我们目前正在对Native项目58APP租房页面进行重构,以实现代码结构的统一,不同页面组件间的复用,并且页面可以根据Server返回数据灵活组装。
总结
本文介绍了Flutter混合开发中遇到的问题及解决办法,以及开发中应用Fish-Redux的实践。Flutter混合开发,主要的问题是共享Flutter引擎的实现。Flutter-Boost提供了共享FlutterView的实现方式。我们引入了Flutter-Boost,开发了Native侧载体页,设计了通用Flutter跳转协议,结合58跳转中心解决了Flutter混合开发的问题。在业务开发过程中,随着开发的深入和业务逻辑的复杂,通过调研,使用了Fish-Redux进行了代码的重构,对复杂业务进行了细粒度的拆分,对逻辑和试图进行隔离,优化了代码结构。
参考文献
1、Flutter中文网,https://flutterchina.club/
2、深入理解Flutter引擎线程模式,https://mp.weixin.qq.com/s/hZ5PUvPpMlEYBAJggGnJsw
3、已开源|码上用它开始Flutter混合开发——FlutterBoost,https://mp.weixin.qq.com/s/v-wwruadJntX1n-YuMPC7g
4、Fish-Redux介绍文档,https://github.com/alibaba/fish-redux/tree/master/doc
作者简介
万兵 :58同城房产技术部-Android开发工程师。主要负责58和安居客APP租房和商业地产业务的开发和维护工作。
live
沙龙活动直播
2020年58技术沙龙活动在线直播第一弹——《大数据平台建设实践与探讨》系列第三期已准备就绪,欢迎你强势围观!
详情:mag_right:请戳:point_up_2:图片查看,3 月7日本周六19:00,我们不见不散。
阅读推荐
2.基于无监督学习的语义不畅低质文本识别与应用如何撑起58同城海量数据?
3.基于git submodule的增量项目源码自动化管理探索实践
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。