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

5,648 阅读7分钟

前景介绍

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 页面:此方案可以解决方案一中的内存共享浪费问题

此方案的大致原理如下:

-w884

关键步骤是 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

此方案的结构图如下:

屏幕快照 2019-01-14 下午8.18.37

和方案二不同的是,方案三中 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 的侧滑返回就很奇怪,不支持合理)

实现关键点:

-w454

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

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

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 才会有返回,顺着这个思路去实现很简单。

-w722

只要 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技术团队官方公众号

微店App技术团队