Flutter 2.x 混合栈路由管理

一、背景

在Flutter开发中,除了热更新之外,Flutter 最受诟病的就是混合开发体验不好,而混合开发最重要的就是路由和组件生命周期的管理。

目前,Flutter 在跨平台方案一致性以及优秀的体验已经获得大多数开发者的一致称赞。但对于本身已有成熟的业务代码的项目来说,直接使用纯Flutter进行开发几乎是不现实的,所以更多的项目是在不改变更原有 App 业务的基础上,将 Flutter 作为子模块进行接入和开发,架构如下。
Flutter 2.x 混合栈路由管理_第1张图片

在混合开发中,就必然栈涉及到 Flutter 页面与原生页面的跳转。而在Flutter 2.0之前,官方的路由方案在多引擎下有着通信隔离、资源不共享、以及可能带来极大的内存损耗(体现在打开多个 Flutter 页面时内存异常增长)等缺陷。对于这种问题,业内出现了flutter_boost 、 mix_stack 、 flutter_thrio等混合路由方案,其原理都是采用单引擎复用方案,但这仍有不少痛点,主要体现在以下两点:

  • 混合栈路由在使用时,仍有可能会产生内存异常;
  • Flutter底层代码的修改,造成上层框架需要不断适配。
  • 二、多引擎方案

    在Flutter 2.0版本之前, Flutter 的控件渲染直接脱离了原生平台,也就是无论页面堆栈和渲染树都独立于平台运行,这固然给 Flutter 带来了较好的跨平台体验,但是也造成了在和原生平台混合时开发的一些问题。

研究过Flutter架构的童鞋都应该知道,Flutter 的技术链路是建立在 C++ 编写的 Engine 和 Dart 编写的 Framework 层组成,如下图所示。
Flutter 2.x 混合栈路由管理_第2张图片
通过上面的架构图,我们可以得到如下一些信息:

  • Flutter 的Engine层管理着 Flutter 所使用到的四个线程,关于四个线程的介绍可以参考:Flutter的线程管理模型
  • Flutter的isolate 的作用是管理Dart 层的内存和单线程控制的运行实体,并且每个 isolate 之间的内存和逻辑是隔离的,对应着 Engine 也是资源不共享的。
  • Engine依赖于原生视图组件提供渲染能力,对应着Activity/ViewController ,所以打开一个新的页面的时候就意味着会创建一个新的Engine,如果打开的页面够多,就可能会出现内存的泄露。

并且,在混合路由管理方面,虽然 Dart 层本身有提供 navigator 等路由管理方式,但当我们在原生工程中集成Flutter时,一定会出现 Native -> Flutter -> Native -> Flutter… 这种混合路由跳转的情况。

如何保存Flutter页面的状态,并且在页面回退或者跳转时恢复Flutter页面的内容是多引擎方案一个比较棘手的问题。下图演示了在Flutter多引擎方案下,由于Engine 的重复创建,带来的内存损耗的情况。

Flutter 2.x 混合栈路由管理_第3张图片
那多引擎方案在哪些场景中有优势呢?主要体现在下面两个方面:

  • 集成了 Flutter 界面的应用,其位置并不在路由栈的叶子节点上,且其可能是混合路由栈,即 Native -> Flutter -> Native -> Flutter。
  • 多个 Flutter View 同时集成在同一个页面上,且同时显示。

三、 FlutterEngineGroup方案

3.1 FlutterEngineGroup

FlutterEngineGroup是Flutter 2.0提出的一个全新的方案,FlutterEngineGroup 方案使用的多个 Engine 的混合模式,大幅减少了额外的 Flutter 引擎的内存占用。根据官方的数据,FlutterEngineGroup从 Android 上约 19MB,iOS上约13MB,降至约180kB,将固定的内存开销减少了约 99% 。

并且,从 Flutter 官方提供的例子上看,FlutterEngineGroup 的 API 十分简单,多个 Engine 实例的内部都是独立维护自己的内部导航堆栈,所以可以做到每个 Engine 对应一个独立的模块。

得益于FlutterEngineGroup 生成的 FlutterEngine 可以共享 GPU 上下文、共享渲染线程等特性 ,因此可以更快的实现引擎的初始化和更低的内存占用。下面是使用运行官方的例子打开10个页面,在Android真机上,内存、CPU的使用情况。
Flutter 2.x 混合栈路由管理_第4张图片

3.2 运行官方示例

官方提供了一个FlutterEngineGroup使用例子,可以从FlutterEngineGroup下载例子,然后按照下面的步骤来运行例子:

cd ../multiple_flutters_module
flutter pub get
cd -
open -a "Android Studio" multiple_flutters_android/

然后,在Android Studio打开并运行Android混合工程即可。

3.3 FlutterEngineGroup基本使用

从Flutter 2.0版本开始,FlutterEngine都将由 FlutterEngineGroup 去生成,生成的 FlutterEngine 可以独立应用于 FlutterActivity/FlutterViewController,甚至是 FlutterFragment。因此,使用FlutterEngineGroup之前需要先创建一个 FlutterEngineGroup对象,最好在Application的onCreate()方法中创建。

private void initFlutterEngine() {
        engineGroup = new FlutterEngineGroup(this);
        DartExecutor.DartEntrypoint dartEntrypoint = new DartExecutor.DartEntrypoint(
                FlutterInjector.instance().flutterLoader().findAppBundlePath(), "topMain"
        );
        FlutterEngine topEngine = engineGroup.createAndRunEngine(this, dartEntrypoint);
        FlutterEngineCache.getInstance().put("topMain", topEngine);
    }

上面的topMain就是是跳转Flutter 需要执行的方法。然后,在原生页面使用Intent方式跳转到Flutter主页面,跳转时注意使用withCachedEngine,可以解决跳转的白屏问题。

Intent intent =  FlutterActivity.withCachedEngine("topMain").build(getActivity());
           startActivity(intent);

然后,在Flutter模块的main.dart创建一个DartEntrypoint对象,注意方法名需要和FlutterEngineGroup创建的FlutterEngine进行对应,如下所示。

@pragma('vm:entry-point')
void topMain() => runApp(MyApp(color: Colors.green));

通过上面创建的 dartEntrypoint 和 context ,使用 FlutterEngineGroup 就可以创建出对应的 FlutterEngine ,其实在内部就是通过FlutterJNI.nativeSpawn 和原有的引擎交互,得到新的地址id。最后,利用生成的 FlutterEngine 的 binaryMessenger 来得到一个 MethodChannel 用于原生和 dart 之间的通信。

并且,通过上述流程得到的 Engine,可以直接用于渲染运行新的 Flutter UI,由于FlutterEngineGroup去管理Engine,所以我们无需担心内存问题。

到这里,你或许已经注意到,因为每个 Flutter 页面都是一个独立的 Engine ,由于 dart isolate 的设计理念,每个独立 Engine 的 Flutter 页面内存是无法共享的。如果需要共享数据,那么只能在原生层持有数据,然后注入或者传递到每个 Flutter 页面中。正如官方所说的,每个 Flutter 页面更像是一个独立 Flutter 模块

接下来,我们会介绍在在混合开发中,Flutter 2.x如何进行数据的传递等基本操作。

你可能感兴趣的