likes
comments
collection
share

FlutterBoost3.0 原理解析

作者站长头像
站长
· 阅读数 35

背景

随着Flutter的发展,越来越多的App开始使用Flutter。然后具有一定规模的App会依赖自己维护的基础库,那么使用Flutter重新开发App就会有较高的成本和风险,所以大部分Native App采用渐进式方式引入Flutter,在原有工程的基础上嵌入flutter的能力,由此产生了原生页面和Flutter页面共存的的情况,如何管理路由?官方并没有提供很好的解决方案,于是闲鱼推出了Flutter_Boost。

单引擎&多引擎模式

FlutterBoost 是采用单Engine的方案,所以在介绍原理之前,先搞清楚闲鱼为什么采用单engine的方式。

在一个进程里面最多只会初始化一个Dart VM。然而一个进程可以有多个Flutter Engine,多个Engine实例共享同一个Dart VM,一个Dart VM 可以对应多个isolate。

比如在iOS上面每初始化一个FlutterViewController就会有一个引擎随之初始化,也就意味着会有新的线程去跑Dart代码。如果你启动多个引擎实例,只是不同Engine实例加载的代码跑在各自独立的Isolate。

官方在Flutter 2.0 提供了FlutterEngineGroup, 采用多Engine方案,每个页面是一个Engine,或者一个页面内包含多个Engine,每个Engine对应一个Isolate,内存不共享。从FlutterEngineGroup生成的FlutterEngine ,内存只增加180k。因为它对常用资源进行共享(例如 GPU 上下文、字体度量和隔离线程的快照),加快首次渲染的速度、降低延迟并降低内存占用。

例如以下导航操作,完全可以使用官方提供的方案,每个flutter 页面对应一个FlutterViewController/Activity, 而且方便管理。

Flutter Page1 -> Flutter Page2 -> Native Page1 -> Flutter Page3

多引擎模式存在的问题

  1. 加载资源的冗余,多引擎模式下,不同的engine维护各自的图片/文件缓存,对于资源的重复占用内存,加大了内存压力
  2. 插件注册以及同原生通信混乱,flutter通过message 与原生实现消息传递,多引擎会造成插件注册以及插件和原生的channel 造成错落
  3. 增加了页面之间的传参的复杂,多引擎方案会产生多个isolate,会导致页面传参会更加复杂。

综合多方面考虑,闲鱼采用了单引擎的的方案

发展

Flutter_Boost 作为开源的混合栈方案 经过不断的迭代优化,被越来越多的App 采用,由0.X版本 升级到到 最新的 4.2版本,适配了null-safe、flutter 最新版本,解决了 黑/白屏、生命周期不一致、crash、freeze 等问题,支持不同场景下使用。

目前我们使用的flutter_boost 版本 release v3.0-null-safety-release.2, 依据此版本对boost 做分析。

架构

FlutterBoost插件分为平台和Dart两端,中间通过Message Channel连接。平台侧提供了Flutter引擎的配置和管理、Native容器的创建/销毁、页面可见性变化通知,以及Flutter页面的打开/关闭接口等。而Dart侧除了提供类似原生Navigator的页面导航接口的能力外,还负责Flutter页面的路由管理。

FlutterBoost3.0 原理解析

原生侧

flutter_boost:引擎的启动,初始化,监听App 生命周期(切换前后台)

FlutterBoostPlugin:负责boost flugin 注册,message 消息的注册以及处理 , 管理container页面生命周期

FBFlutterContainerManager: 使用Map管理容器

FBFlutterViewContainer:flutterView 容器,将页面生命周期通过FlutterBoostPlugin 传递给dart侧

messages:提供 与flutter端 接受/发送 消息 的 API

Dart侧

boost_navigator,提供 push pop 等API 管理混合栈,提供PageInfo

flutter_boost_app: 设置注册路由,container的管理,App的构建入口

boost_container:是原生container的抽象类,提供widget 容纳page以及 管理page 页面

container_overlay:管理contaier的pages, 通过可管理的栈 实现pages 层级管理,与contaier 一一对应

boost_flutter_binding:hook flutter page的生命周期

boost_lifecycle_binding:响应原生侧container生命周期

messages:提供与Native端接受/发送 消息 FlutterRouterApi/NativeRouterApi

从boost架构图大概能了解dart侧 和native侧 构成,dart和native侧通过消息传递, dart将push pop 等操作传递给native,native将的contaienr生命周期传递给dart,dart产生响应。两端相互协作实现了页面管理的能力。

流程梳理&源码分析

1 boost启动流程

  1. 首先初始化flutter engine
  2. 通过[engine runWithEntrypoint],创建isolate,运行包含main()函数的dart 程序
  3. 根据配置项预热引擎
  4. 注册所有plugin和消息channel,并获取到boost插件实力传入delegate
  5. 监听App切换前后台

- (void)setup:(UIApplication*)application delegate:(id<FlutterBoostDelegate>)delegate callback:(void (^)(FlutterEngine *engine))callback options:(FlutterBoostSetupOptions*)options{

    if([delegate respondsToSelector: @selector(engine)]){
        self.engine = delegate.engine;
    }else{
        self.engine = [[FlutterEngine alloc ] initWithName:@"io.flutter" project:options.dartObject];
    }

    //从options中获取参数
    NSString* initialRoute = options.initalRoute;
    NSString* dartEntrypointFunctionName = options.dartEntryPoint;

    void(^engineRun)(void) = ^(void) {

        [self.engine runWithEntrypoint:dartEntrypointFunctionName  initialRoute : initialRoute];

        //根据配置提前预热引擎,配置默认预热引擎
        if(options.warmUpEngine){
            [self warmUpEngine];
        }

        Class clazz = NSClassFromString(@"GeneratedPluginRegistrant");
        SEL selector = NSSelectorFromString(@"registerWithRegistry:");
        if (clazz && selector && self.engine) {
            if ([clazz respondsToSelector:selector]) {
                ((void (*)(id, SEL, NSObject<FlutterPluginRegistry>*registry))[clazz methodForSelector:selector])(clazz, selector, self.engine);
            }
        }

        self.plugin= [FlutterBoostPlugin getPlugin:self.engine];
        self.plugin.delegate=delegate;
    
        if(callback){
            callback(self.engine);
        }
    };

    if ([NSThread isMainThread]){
        engineRun();
    }else{
        dispatch_async(dispatch_get_main_queue(), ^{
            engineRun();
        });
    }

    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotificationobject:nil];

    [[NSNotificationCenter defaultCenter] addObserver:self selector: @selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotificationobject:nil]; 

}

2 Container 创建与周期同步

  1. App 启动后会创建FBFlutterViewContainer
  2. 将创建的contaier 以key-value的形式存储在containerManager
  3. Flutter 引擎将flutter页面attach到contaier,viewWillAppear时会执行pushRoute并同步contaier的生命周期到dart侧
- (void)viewWillAppear:(BOOL)animated
{
   [FB_PLUGIN containerWillAppear:self];
   ...
}

- (void)viewDidDisappear:(BOOL)animated
{
   ...
   [FB_PLUGIN containerDisappeared:self];

}

- (void)containerWillAppear:(id<FBFlutterContainer>)vc {
    ....
    //显示执行push操作
    [self.flutterApi pushRoute: params completion:^(NSError * e) {
    
    }];

    [self.containerManager activeContainer:vc forUniqueId:vc.uniqueIDString];

}

// attach 
- (void)attatchFlutterEngine

{
    if(ENGINE.viewController != self){

        ENGINE.viewController=self;
    }
}
  1. 最终dart响应pushRoute消息并执行FlutterBoostApp#pushContainer,生成container 以及page 压入栈顶 对应生成overlay entry overlayState.insert(entry) 显示flutter 页面
void pushContainer(String? pageName,
      {String? uniqueId,
      bool isFromHost = false,
      Map<String, dynamic>? arguments}) {
      ......
      final container = _createContainer(pageInfo); // 生成container 和 page
      final previousContainer = topContainer;
      containers.add(container);
      BoostLifecycleBinding.instance
          .containerDidPush(container, previousContainer);
      // Add a new overlay entry with this container
      refreshOnPush(container);

}

BoostContainer({this.key, required this.pageInfo}) {

   _pages.add(BoostPage.create(pageInfo));

}

// 页面显示

overlayState.insert(entry);
    

3 boost dart侧 启动流程

1 FlutterBoostApp 初始化

Native boost 启动时会触发dart main(), 执行 FlutterBoostApp 构建时会传入appBuilder 和routeFactory, routeFactory 是需要跳转的flutter 页面信息,通过此配置判断页面是否为flutter页面,FlutterBoostApp 构建会初始化页面管理,routerAPI, 拦截器,message 注册等

FlutterBoostApp(

  routeFactory,

  appBuilder: appBuilder,

  initialRoute: "initialRoute",

 );

2 双侧路由栈页面管理

上文的boost 架构也介绍过boost 在dart 侧使用双层路由栈来管理页面,双路由栈代码如下

  双层路由栈结构代码

  外部路由
  List<BoostContainer> get containers => _containers;
  final List<BoostContainer> _containers = <BoostContainer>[];

  内部路由
  /// A list of page in this container
  final List<BoostPage<dynamic>> _pages = <BoostPage<dynamic>>[];

_containers 会存储BoostContainer,而每一个BoostContainer 都会维护_pages 并结合overlayEntry 管理pages

3 Dart侧调用Push&Pop流程

1 当dart侧执行push操作时,withContainer == false 时,最终会生成page 并添加到contaier的pages,最终显示flutter页面, 可参考4

topContainer!.addPage(BoostPage.create(pageInfo))

2 当push 操作 withContainer == true 时候,dart侧最终会通过messageChannel 调用Native 侧Api,如果是flutter页面则会触发 pushFlutterRoute ,生成新的container 并重复走 Container 创建与周期同步的 3 和 4 步骤,将container 存储到containerManager 中,然后在dart侧创建contaier 和 page 并压入双侧栈,最终显示Flutter页面;

- (void)pushFlutterRoute:(FlutterBoostRouteOptions *)options {

    FBFlutterViewContainer *vc = FBFlutterViewContainer.new;

    [vc setName:options.pageName uniqueId:options.uniqueId params:options.arguments opaque:options.opaque];
    ...
    Push/Present VC
}

如果push Native 页面,则最终会调用一下方法,最终跳转到原生页面

- (void) pushNativeRoute:(NSString *) pageName arguments:(NSDictionary *) arguments {

    UIViewController *vc = [[UIViewController alloc] init]:
    ...
    Push/Present VC

}

FlutterBoost3.0 原理解析

Pop 操作流程如下

FlutterBoost3.0 原理解析

FlutterBoost3.0 原理解析

4 页面周期管理

每当push操作withContainer 就会生成新的FlutterViewController, engin 就会attach 到最新的FlutterViewController 上 并渲染显示出页面。Native 和 dart 侧都会存在对应的container, 如图示

FlutterBoost3.0 原理解析

页面生命周期

Native 侧生命周期 FLBViewController会同步 生命周期会同步到dart侧,当Native容器展示的时候发出onContainerShow事件通知对应的BoostContainer展示,Native容器隐藏时再发出onContainerHide事件通知对应的的BoostContainer隐藏。

Dart侧生命周期,dart侧通过 hook handleAppLifecycleStateChanged方法 从而获取flutter页面的生命周期。

另外还提供了一个changeAppLifecycleState方法,这个方法可以真正来改变上层Flutter应用的生命周期,目前这个方法的调用时机与Flutter容器个数相关,当容器大于等于1,Flutter应用的生命周期状态为resumed,而容器个数为0时,Flutter应用的生命周期状态则为paused。

这样就做到不入侵flutter的引擎的同时获取到flutter页面的生命周期, Native自己管理生命周期时间,并在适当时机给Dart侧发送appIsResumed消息解决引擎复用时生命周期事件错乱导致的页面卡死问题

mixin BoostFlutterBinding on WidgetsFlutterBinding {

  bool _appLifecycleStateLocked = true;

  @override

  void initInstances() {

    super.initInstances();
    _instance = this;
    changeAppLifecycleState(AppLifecycleState.resumed);

  }

  static BoostFlutterBinding get instance => _instance;

  static BoostFlutterBinding _instance;

  @override

  void handleAppLifecycleStateChanged(AppLifecycleState state) {

    if (_appLifecycleStateLocked) {
      return;
    }

    Logger.log('boost_flutter_binding: handleAppLifecycleStateChanged ${state.toString()}');
    super.handleAppLifecycleStateChanged(state);
  }

  void changeAppLifecycleState(AppLifecycleState state) {

    if (SchedulerBinding.instance.lifecycleState == state) {
      return;
    }

    _appLifecycleStateLocked = false;
    handleAppLifecycleStateChanged(state);
    _appLifecycleStateLocked = true;
  }
}

总结

以上总结了flutter_boost在单engine模式下的页面管理、启动和执行流程等等,大概能了解boost的原理,flutter_boost 好比将App变成一个浏览器,可以再当前页面切换不同网页,也可以新建页面切换网页,Native 和dart 侧 相互协作,形成最终的页面管理能力。

flutter_boost很多细节点值得关注,比如使用双链表保证执行时序,热更新时两端栈的同步,以及原生侧通过category暴露flutterVC的私有方法,有很多细节点值得我们学习和深究,

未经作者允许,禁止转载

如有纰漏,希望大家斧正。

转载自:https://juejin.cn/post/7170997208223842312
评论
请登录