导语
背景
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包括:
前面我们提到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
*/
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
沙龙活动直播
END
阅读推荐
2.基于无监督学习的语义不畅低质文本识别与应用如何撑起58同城海量数据?