布景

跟着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的私有办法,有许多细节点值得我们学习和深究,

未经作者允许,制止转载

如有纰漏,期望大家指正。