微店的Flutter混合栈管理技术实践

前景介绍

Flutter 是谷歌开发的一款可以跨平台开发的 UI框架,它的原理接近于游戏引擎,目的在于统一Android/iOS 两端开发,Flutter页面有自己的栈,正常情况下,如果一个App完全由 Flutter构成,那么只需要一个 FlutterView 即可。

上述方案只适用于一些新构建的App,对于一些已有的App,是不可能用 flutter来重构的,成本太大,周期太长,所以这里需要实现一套 Native 页面栈和Flutter页面栈的管理方案,即混合栈

关于混合栈的管理,闲鱼出过一篇文章,但是对于它的兼容性问题和截图问题,没有采用,不过作者对闲鱼的混合栈源码做了参考,这里感谢闲鱼的源码分享。本文是在 android 的基础上讲解实现方式的,iOS 目前使用的还是截图方案,其余的原理差不多

方案探索

方案一

不进行任何处理,直接使用 FlutterActivity 来打开页面:此方法最接近原生,交替打开几个页面后会呈现出以下页面结构

每个 FlutterActivity 都有自己的 flutter 栈,此时如果用户点击了返回按钮的时候页面退出的呈现形式是正常的,但是如果App用了侧滑返回的话工作就会不正常。

侧滑结束 FlutterActivit2 会一下子结束三个 flutter widget 页面

除了上述问题外,还存在一个严重的问题:FlutterView1FlutterView2 属于两个 isolate,两者相当于两个 flutter engine 实例,在内存上隔离的,不共享

总结: 该方案有以下缺点

  • 不兼容现有的侧滑返回
  • 页面的生命周期埋点需要在 dart 层重新实现一套
  • 不同 FlutterView 之间无法共享内存(图片缓存,全局单例都不可公用)
  • 资源占用大:每次启动一个 FlutterActivity 都会启动一个新的 Flutter 实例
  • 界面切换体验有差别:Native 页面之间的切换动画和 flutter 页面之间的切换动画有差别

方案二

全局共用一个 FlutterView,每个 flutter 页面都有一个对应的 native 页面:此方案可以解决方案一中的内存共享浪费问题

此方案的大致原理如下:

关键步骤是 2 这个操作,当要打开一个新的 flutter 页面时,native 会启动一个新的 FlutterActivity,然后把当前 FlutterActivity1 中的 FlutterView 移除,并且添加到 FlutterActivity2 中。

退出页面的时候也一样,先让 FlutterView 从 FlutterActivity2 中 remove 移走,然后 add 到 FlutterActivity1 中。

你可能会想:“切换页面的时候,FlutterView 从 FlutterActiviy 移除了,显示不是会变成空白了吗?

什么都不做,的确存在上述问题,这里想把此方案实现,还需要考虑两点:

  • FlutterView 从 FlutterActivity1 移除的时候,显示的内容不会被移除
  • FlutterView 从 FlutterActivity1 移除添加到 FlutterActivity2 的之前,必须保证新的 flutter page 已经 push 到 flutter 的栈中,否则 FlutterActivity2 显示的还是 FlutterActivity1 中显示的界面

这里要实现第一点的话只能使用截图方案,在 FlutterView 移除前先保存一份当前页面的截图快照,然后移除,这样就不会出现空白的问题

方案三

全局共用一个 FlutterNativeView,每个 flutter 页面都有一个对应的 native 页面:此方案和方案二想接近,最大的区别就是复用的东西变成了 FlutterNativeView

此方案的结构图如下:

和方案二不同的是,方案三中 FlutterViewFlutterActivity 绑定在一起了,这样可以避免 FlutterView 单例化造成的 context 泄漏。

而且相比于方案二,要实现此方案只需要满足一条规则即可:

  • FlutterNativeView 从 FlutterActivity1 detach 然后 attach 到 FlutterActivity2 的之前,必须保证新的 flutter page 已经 push 到 flutter 的栈中,否则 FlutterActivity2 显示的还是 FlutterActivity1 中显示的界面

你会发现,这里不需要 FlutterNativeView 在 detach 的时候构造一份当前页面的快照然后占位显示.

因为在页面切换的时候 FlutterView 并没有从 FlutterActivity 中移除,FlutterNativeViewFlutterView detach 的时候,FlutterView 显示的内容就不会再更新了,相当于 Android 上的 onPreDraw 函数返回 false, 所以这里没必要截图保存快照。

实现

经过上述方案的探索,决定在 android 上使用第三套方案

iOS 因为有侧滑返回,无法避免截图,因为在侧滑的时候,页面不一定结束。所以我这里抛弃了 android 上侧滑返回(本来 android 的侧滑返回就很奇怪,不支持合理)

实现关键点:

  • 整个布局为多 FlutterViewFlutterNativeView 实例
  • 每一个 flutter 页面对应一个 native 的 activity,并通过一个 id 关联,做到栈同步
  • Flutter 和 Native 基于 url 的方式开管理页面
  • 禁用 flutter 自带的页面切换动画,使用 native 自带的动画来实现
  • 使用一个空白的 widget 作为 flutter 页面的栈底
  • 当打开新页面或者退出页面的时候,必须先让 FlutterNativeViewFlutterView 脱离,才可以在 flutter 栈操作页面的进退

整个页面启动跳转打大致流程图如下:

左侧是 flutter 的执行流程,右侧是 android native activity 的执行流程

页面的传参和数据返回

上述代码的设计还没有考虑页面之间的数据传递,原生 flutter 的页面数据传递是这样的:

void jumpToSettings(BuildContext context) async {
    String result = await Navitor.of(context).pushNamed("settings");
    print("page return: $result");
}
复制代码

所以在设计页面数据传递的时候向原生的看齐,如下所示:

final result = await VDRouter.instance().openUrlFromNative(
    context: context,
    routerOption: RouterOption(
        url: "native://example", args: {"message": "Open from Flutter"}));
复制代码

当 native 端的 MethodCallHandler 被调用时,有个参数是 result,只有给这个 result 设置了结果 (result.success(xxx)),上面的 await 才会有返回,顺着这个思路去实现很简单。

只要 FlutterActivity 把由当前 flutter 发起打开页面请求的 result 对象保存起来,然后调用 startActivityForResult 来启动页面,等页面结束后会回调到 onActivityResult 中,此时再通过保存的 result 对象,把结果返回给 flutter 端。

传参直接使用 intent 传参即可。

沉浸式同步的问题

每次启动一个新的 FlutterActivity 都需要和 flutter 端同步下当前状态栏的沉浸式状态,这里通过 native 主动调用 channel 来同步

// 请求更新主题色到 native 端,这里使用了一个测试接口,以后要注意
var preTheme = SystemChrome.latestStyle;
if (preTheme != null) {
    SystemChannels.platform.invokeMethod("SystemChrome.setSystemUIOverlayStyle", _toMap(preTheme));
}
复制代码

FlutterNativeView 的 detach 和 attach

FlutterNativeView 的 detach 和 attach 的时候,需要注意 FlutterActivity 的生命周期和 FlutterView 中 surface 的创建状态,保证 FlutterActivityFlutterView 的生命周期同步到 FlutterNativeView

总结

总的来说,我们微店基于上述理论实现了一个混合栈插件,没有反射 flutter sdk,没有内存泄漏,不需要截图,支持页面间的数据传递(源码后续会开放),看似简单实际实现过程中还是遇到过很多小问题的,比如页面白屏,返回键无效之类的,这些都是 native 栈和 flutter 栈不同步导致的。

后续计划

后续我们微店混合栈的问题继续跟进的问题如下:

  1. 首次打开白屏时间长
  2. 不支持 Hero 动画
  3. iOS 无法避免截图方案
  4. 无法和 Navigator.of(context).pop() 结合

其中1.的话目前没有什么好的思路,但是2.3.4.点 已经有了想法,待实现验证,敬请期待。

作者简介

qigengxin,@WeiDian,2016年加入微店,目前主要负责微店App的基础支撑开发工作。

欢迎关注微店App技术团队官方公众号

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值