阅读 36

FlutterBoost 实现原理与源码分析

跨平台开发框架集成到现有项目,通常会遇到混合页面管理问题,原生有一套自己导航栈,Flutter/React Native 也有一套自己的导航栈,原生与 Flutter/RN 互相跳转时,导航栈管理将变得十分困难。FlutterBoost 是闲鱼开源的一套混合页面管理库,它可以轻松地为现有原生应用程序提供Flutter混合集成方案。

在集成跨平台开发框架时,混合页面导航栈是一个绕不过去的坎,网上这方面的资料和代码质量参差不齐,FlutterBoost 是一个难得的有文档有源码的高水平开源方案,不管是 Flutter 还是 React Native,都有借鉴学习意义,今天我们一起来探索分析它的实现原理。

实现原理

从几个问题开始,大概介绍 FlutterBoost 的实现原理。

1,混合页面中,原生、Flutter 页面如何统一管理 现有的原生项目集成 Flutter 时,面临着原生页面和 Flutter 页面两套路由,同时管理多个路由比较困难。将 Flutter 页面当成原生页面来处理,使用原生路由来统一管理混合页面,这样导航栈管理、页面跳转就比较容易了。

FlutterBoost 提供了原生容器,放置 Flutter 页面,一个原生容器容器对应一个 Flutter 页面,类似于原生页面嵌入 webview。所以不管是原生页面还是 Flutter 页面,都是统一用原生路由来管理导航栈。

现有项目一般有自己定制的路由库,Flutter 页面跳转也使用这套路由库,通过桥接类,开放路由库的 api 给 Flutter 侧调用。

2,原生侧提供什么功能,做了什么 原生侧提供一个原生容器,它用来承载 Flutter 页面,它们是一一对应的,iOS 上为 ViewController,安卓上为 Activity。原生容器的生命周期方法通过桥接发送到 Dart 侧,这样 Flutter 侧与原生侧实现了同步,共同创建、共同销毁。

当有多个原生容器时,无法区分彼此,因此为每个 Flutter 容器分配一个独一无二的 id,标识自己。同时分配 name、param 属性,页面跳转时作为标志。

3,桥接类有什么方法 原生侧与 Dart 侧之间传递信息,通过桥接类实现。如页面跳转、页面生命周期方法传递等。

桥接类提供了双向发送消息的方法,从原生侧发消息给 Dart 侧,如原生容器的生命周期方法。从 Dart 侧发消息给原生侧,如 Dart 侧调用跳转方法。

4,dart 侧做了什么 注册 Flutter 页面,每个页面分配一个 name,当需要打开页面时,根据 name 找到这个页面进行显示。

Flutter 页面跳转原生页或 Flutter 页,都是通过原生路由实现,借助桥接方法,执行路由操作。

5,混合页面相互跳转时,如何传递回调参数? Boost 的实现思路如下:从页面 A 跳转到页面 B 时,传递一个回调,当 B 页面销毁或关闭时,执行这个回调。这个回调保存在原生侧,以键值对的方式存储,key 是回调id,字符串类型,可以自己设置,默认分配一个独一的值。value 就是回调本身了。

当调用 open 方法跳转新页面时,先将回调存到 callbackCache 字典中,接收到从 dart 侧传来的容器已初始化完成消息后,把回调取出放到 _pageResultCallbacks 字典中,这里 key 变成了页面 id。页面将销毁或页面关闭时,根据页面 id 从 _pageResultCallbacks 中找到对应的回调并执行。

从原生页跳转到 Flutter 页面,回调是在原生侧创建的,从 Flutter 页跳转到原生页面,回调是通过桥接传递过来。

源码分析

页面注册

我们从 Flutter 页面注册的入口开始分析。Flutter 页面都注册一个 map 中,key 是页面的 name,不能重复,value 是页面 builder。跳转某个页面时根据传入的 name 找到对应 builder 并构建页面。下面是 demo 中的页面注册代码

    FlutterBoost.singleton.registerPageBuilders({
      'embeded': (pageName, params, _)=>EmbededFirstRouteWidget(),
      'first': (pageName, params, _) => FirstRouteWidget(),
      'second': (pageName, params, _) => SecondRouteWidget(),
      'tab': (pageName, params, _) => TabRouteWidget(),
      'platformView': (pageName, params, _) => PlatformRouteWidget(),
      'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),
      ///可以在native层通过 getContainerParams 来传递参数
      'flutterPage': (pageName, params, _) {
        print("flutterPage params:$params");

        return FlutterRouteWidget(params:params);
      },
    });
复制代码

FlutterBoost 是个单例类,只会创建一个实例,它内部又持有 ContainerCoordinator,同样是个单例类,负责接收原生传递过来的消息。页面注册的 map 最终存放在 ContainerCoordinator 中的 _pageBuilders。

//ContainerCoordinator
  final Map<String, PageBuilder> _pageBuilders = <String, PageBuilder>{};

  void registerPageBuilders(Map<String, PageBuilder> builders) {
    if (builders?.isNotEmpty == true) {
      _pageBuilders.addAll(builders);
    }
  }
复制代码

根据传入的 name,在 _pageBuilders 中找到对应的 pageBuilder 并实例化。

BoostContainerSettings 类等待原生侧传来的页面消息,收到后执行页面初始化。

final BoostContainerSettings routeSettings = BoostContainerSettings(
        uniqueId: pageId,
        name: name,
        params: params,
        builder: (BuildContext ctx) {
          //Try to build a page using keyed builder.
          if (_pageBuilders[name] != null) {
            page = _pageBuilders[name](name, params, pageId);
          }

          //Build a page using default builder.
          if (page == null && _defaultPageBuilder != null) {
            page = _defaultPageBuilder(name, params, pageId);
          }
         return page;
        });
复制代码

页面跳转

从 Flutter 页面跳转到另一页面,可能是原生页面也可能是 Flutter 页面,使用方法如下

FlutterBoost.singleton.open("second");
复制代码

url 是页面名,urlParams 是页面参数,exts 是额外参数,如是否启用跳转动画等。这三个参数包装到 properties 中,借助桥接将消息传递到原生侧。

返回值是一个回调,页面销毁或关闭时在原生侧执行。

  Future<Map<dynamic,dynamic>> open(String url,{Map<dynamic,dynamic> urlParams,Map<dynamic,dynamic> exts}){

    Map<dynamic, dynamic> properties = new Map<dynamic, dynamic>();
    properties["url"] = url;
    properties["urlParams"] = urlParams;
    properties["exts"] = exts;
    return channel.invokeMethod<Map<dynamic,dynamic>>(
        'openPage', properties);
  }
复制代码

原生侧接收到跳转消息后,从消息体中解析出参数,然后转发到 FLBFlutterApplication 类。FLBFlutterApplication 负责协调回调、引擎、原生容器等。

  - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
...
else if([@"openPage" isEqualToString:call.method]){
        NSDictionary *args = [FLBCollectionHelper deepCopyNSDictionary:call.arguments
                                                                filter:^bool(id  _Nonnull value) {
                                                                    return ![value isKindOfClass:NSNull.class];
                                                                }];
        NSDictionary *exts = args[@"exts"];
        NSString *uid = args[@"uniqueId"];
        NSDictionary *resultData = args[@"result"];
        [[FlutterBoostPlugin sharedInstance].application open:url
                                                    urlParams:urlParams
                                                         exts:exts
                                                        onPageFinished:result
                                                   completion:^(BOOL r) {}];
    }
...
}
复制代码

这个方法将回调保存起来,当 Flutter 页面关闭或销毁时,调用这个回调。platform 是需要 app 实现的协议,app 可能自己定制一套路由管理库,那在 platform 中就用定制的跳转方法,若没有单独定制,可以使用系统提供的跳转方法。最终跳转到原生容器页面 FLBFlutterViewContainer

- (void)open:(NSString *)url
   urlParams:(NSDictionary *)urlParams
        exts:(NSDictionary *)exts
       onPageFinished:(void (^)(NSDictionary *))resultCallback
  completion:(void (^)(BOOL))completion
{
    NSString *cid = urlParams[kPageCallBackId];
   
    if(!cid){
        static int64_t sCallbackID = 1;
        cid = @(sCallbackID).stringValue;
        sCallbackID += 2;
        NSMutableDictionary *newParams = [[NSMutableDictionary alloc]initWithDictionary:urlParams];
        [newParams setObject:cid?cid:@"__default#0__" forKey:kPageCallBackId];
        urlParams = newParams;
    }
    
    _callbackCache[cid] = resultCallback;
    if([urlParams[@"present"]respondsToSelector:@selector(boolValue)] && [urlParams[@"present"] boolValue] && [self.platform respondsToSelector:@selector(present:urlParams:exts:completion:)]){
        [self.platform present:url
                  urlParams:urlParams
                       exts:exts
                 completion:completion];
    }else{
        [self.platform open:url
                  urlParams:urlParams
                       exts:exts
                 completion:completion];
    }
}

//PlatformRouterImp
- (void)open:(NSString *)name
   urlParams:(NSDictionary *)params
        exts:(NSDictionary *)exts
  completion:(void (^)(BOOL))completion
{
    BOOL animated = [exts[@"animated"] boolValue];
    FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
    [vc setName:name params:params];
    [self.navigationController pushViewController:vc animated:animated];
    if(completion) completion(YES);
}
复制代码

原生容器 FLBFlutterViewContainer

FLBFlutterViewContainer 有三个属性 name、params、identifier,name 表示页面 url,页面跳转时根据这个属性找到对应页面,params 页面跳转时传递的参数,identifier 表示页面的唯一 id,多个原生容器同时存在时区分彼此

@interface FLBFlutterViewContainer  ()
@property (nonatomic,copy,readwrite) NSString *name;//.h

@property (nonatomic,strong,readwrite) NSDictionary *params;
@property (nonatomic,assign) long long identifier;
@end
复制代码

FLBFlutterViewContainer 初始化时,静态变量 sCounter 加一,生成唯一 id

instanceCounterIncrease 方法计算 Flutter 容器数量,数量超过1时,启动 Flutter 引擎,否则关闭 Flutter 引擎。

- (void)_setup
{
    static long long sCounter = 0;
    _identifier = sCounter++;
    [self.class instanceCounterIncrease];
}
复制代码

FLBFlutterViewContainer 的生命周期方法: viewDidAppear、viewWillDisappear、viewWillAppear、viewDidLoad 等,触发时通过桥接传递到 dart 侧, 以 viewWillAppear 为例

- (void)viewWillAppear:(BOOL)animated
{
   [BoostMessageChannel willShowPageContainer:^(NSNumber *result) {}
                                            pageName:_name
                                              params:_params
                                            uniqueId:self.uniqueIDString];
    ...
}
复制代码

viewDidAppear 额外作了一些事,把自身添加到 FLBFlutterContainerManager 中,这样就可以轻松获取容器实例数、某个容器是否在栈顶

- (void)viewDidAppear:(BOOL)animated
{
    [FLUTTER_APP addUniqueViewController:self];
    
    //Ensure flutter view is attached.
    [self attatchFlutterEngine];
    ...
}

- (void)addUniqueViewController:(id<FLBFlutterContainer>)vc
{
    return [_manager addUnique:vc];
}
复制代码

dart 侧的页面容器

dart 侧的 ContainerCoordinator 负责接收原生侧传来的消息,当接收到 willShowPageContainer 事件时,同步创建 dart 侧的页面容器。

bool _nativeContainerWillShow(String name, Map params, String pageId) {
    if (FlutterBoost.containerManager?.containsContainer(pageId) != true) {
      FlutterBoost.containerManager
          ?.pushContainer(_createContainerSettings(name, params, pageId));
    }
    ...
}
复制代码

_offstage 是个数组,表示未在栈顶的页面,_onstage 表示当前处于栈顶的页面,这里将上一个栈顶页面入到 _offstage 中,创建新的 Flutter 容器页面,然后调用 setState 方法重新绘制页面

  void pushContainer(BoostContainerSettings settings) {
     _offstage.add(_onstage);
    _onstage = BoostContainer.obtain(widget.initNavigator, settings);

    setState(() {});

    for (BoostContainerObserver observer in FlutterBoost
        .singleton.observersHolder
        .observersOf<BoostContainerObserver>()) {
      observer(ContainerOperation.Push, _onstage.settings);
    }
  }
复制代码

参考资料

Flutter FlutterBoost 码上用它开始Flutter混合开发——FlutterBoost