FlutterBoost浅析

1,726 阅读7分钟

1.FlutterBoost是什么

FlutterBoost是一个Flutter插件,它可以轻松地为现有原生应用程序提供Flutter混合集成方案。 FlutterBoost帮开发者处理页面的映射和跳转,而开发者只需关心页面的名字和参数即可。FlutterBoost 的通道的封装使得 Native 调用 Flutter 、Flutter 调用 Native 的开发更加简便,同时解决了flutter官方提供的混合方案中的很多问题,比如原生和Flutter页面叠加跳转由于Flutter Engine重复创建而导致内存暴增的问题、Flutter应用中全局变量在各独立页面不能共享的问题、iOS平台内存泄露的问题。

Flutter端调用:

FlutterBoost.singleton.open(
                  'flutter://flutterPage',
                  urlParams: <String, dynamic>{
                    'query': <String, dynamic>{'aaa': 'bbb'}
                  },
                );

Native端调用(iOS):

[FlutterBoostPlugin open:@"native://nativePage" urlParams:@{kPageCallBackId:@"MycallbackId#1"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) {
        NSLog(@"call me when page finished, and your result is:%@", result);
    } completion:^(BOOL f) {
        NSLog(@"page is opened");
    }];

2.FlutterBoost版本更新进程

1.0到2.0的主要变动

本次最核心的变化就是页面管理方案升级。新版(0.1.5 - 1.x.x)不再维护单一的FlutterViewController(或FlutterView),而是和原生一样每次有新页面请求时就直接打开新的ViewController或者FlutterView,和管理原生的页面一样。自然也不需要像老版本那样通过实现截图功能来记录上个页面的显示内容。

pageRendering1.0 如上图,升级前全局只有一个FlutterViewAdapter(其实是FlutterViewController),负责FlutterView渲染子View并将其内嵌在每个FLBFlutterViewContainer(继承自UIViewController)中,每次新的FLBFlutterViewContainer拉起时就需要复杂的detach和attach操作来转移唯一的FlutterView,同时进行截图缓存。 升级后,不再需要内嵌的FlutterViewAdapter和Screenshot缓存列表:

其底层实现也变的更加简单,不再需要在detach页面的时候截图,下图是前后两个方案在页面pop和push过程中的对比。

bottomImpl

其他变动还有内存占用优化、页面生命周期管理能力提升等。

2.0到3.0的主要变动

不侵入引擎,兼容Flutter的各种版本,Flutter sdk的升级不需要再升级FlutterBoost 不区分Androidx和Support分支。 简化架构和接口,和FlutterBoost2.0比,代码减少了一半。 双端统一,包括接口和设计上的统一。 支持打开Flutter页面,不再打开容器场景。

flutter3.0

3.FlutterBoost实现机制(1.17.1版本)

Boost本身作为一个Flutter插件,他的核心逻辑分为三部分,分别是Dart侧的使用API和Andorid、iOS端上的容器支持。

Native端 (iOS)

Native层的核心就是引擎复用处理。首先需要实现一个平台路由类PlatformRouterImp,并注册给FlutterBoost插件的单例,启动FlutterBoost,并得到对应的FlutterEngine,有了这个engine,我们也可以做一些和engine相关的处理操作,比如定义一些Native和Flutter通信的FlutterMethodChannel。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    PlatformRouterImp *router = [PlatformRouterImp new];
    [FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router
                                                        onStart:^(FlutterEngine *engine) {
                                                          // do some stuff
    }];
    //...
    UINavigationController *rvc = [[UINavigationController alloc] initWithRootViewController:tabVC];
    router.navigationController = rvc;
}
  

PlatformRouterImp实现了FLBPlatform协议定义的方法,开发者可以自己定义打开、关闭页面的操作。在三翼鸟app中,PlatformRouterImp的实现在vdn的组件中。

例如打开一个页面的操作:

- (void)open:(NSString *)name
   urlParams:(NSDictionary *)params
        exts:(NSDictionary *)exts
  completion:(void (^)(BOOL))completion
{
    if ([name isEqualToString:@"native"]) {//模拟打开native页面
        [self openNativeVC:name urlParams:params exts:exts];
        //至此,通过FlutterBoost打开一个原生页面就完成了。
        return;
    }
    
    BOOL animated = [exts[@"animated"] boolValue];
    FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
    [vc setName:name params:params];
    //FLBFlutterViewContainer继承自FlutterViewController,最终基类是UIViewController,所以可以直接入栈到Native的导航栏里
    [self.navigationController pushViewController:vc animated:animated];
    if(completion) completion(YES);
}

下面分别以原生界面跳转到Flutter界面以及Flutter界面跳转到其他界面为例,记录一下主要的流程。

原生界面跳转Flutter界面的流程

通过原生代码执行FlutterBoostPlugin的open方法。

[FlutterBoostPlugin open:@"first" urlParams:@{kPageCallBackId:@"MycallbackId#1"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) {
        NSLog(@"call me when page finished, and your result is:%@", result);
    } completion:^(BOOL f) {
        NSLog(@"page is opened");
    }];

FlutterBoostPlugin会调用PlatformRouterImp来打开此页面。所有Flutter页面都由FLBFlutterViewContainer来承载。

FLBFlutterViewContainer在初始化的时候,会把自己attach到FlutterEngine上。调用者可以给它传入跳转地址、参数等信息,保存在它的内部。在FLBFlutterViewContainer运行的整个生命周期中,会把不同生命节点的状态通过MessageChannel通知到flutter层。例如它即将要显示在用户屏幕上时,会执行父类的viewWillAppear方法。

- (void)viewWillAppear:(BOOL)animated
{
    //对于一个新建的FLBFlutterViewContainer,初始化时已完成attach,但是如果是一个下层页面,由于上层页面pop了,也会调用下层的viewWillAppear,这时下层页面之前已经detach了,现在重新变成顶层页面,需要重新把自己attach到FlutterEngine。
    [self attatchFlutterEngine]; 
    [BoostMessageChannel willShowPageContainer:^(NSNumber *result) {}
                                            pageName:_name
                                              params:_params
                                            uniqueId:self.uniqueIDString];
    //将第一次的flutter页面信息记录下来,回传flutter端
    [FlutterBoostPlugin sharedInstance].fPagename = _name;
    [FlutterBoostPlugin sharedInstance].fPageId = self.uniqueIDString;
    [FlutterBoostPlugin sharedInstance].fParams = _params;

    [super bridge_viewWillAppear:animated];
    [self.view setNeedsLayout];
}

BoostMessageChannel定义了一系列的方法用来保持Native和Flutter端的页面状态一致。这样,就能保证Native端显示一个FLBFlutterViewContainer的时候,FLBFlutterViewContainer上的内容能够及时显示,而当FLBFlutterViewContainer被Native端移除时,Native和flutter端的资源都能得到及时释放。

@interface BoostMessageChannel : NSObject

 + (void)onNativePageResult:(void (^)(NSNumber *))result uniqueId:(NSString *)uniqueId key:(NSString *)key resultData:(NSDictionary *)resultData params:(NSDictionary *)params;
 + (void)didShowPageContainer:(void (^)(NSNumber *))result pageName:(NSString *)pageName params:(NSDictionary *)params uniqueId:(NSString *)uniqueId;
 + (void)willShowPageContainer:(void (^)(NSNumber *))result pageName:(NSString *)pageName params:(NSDictionary *)params uniqueId:(NSString *)uniqueId;
 + (void)willDisappearPageContainer:(void (^)(NSNumber *))result pageName:(NSString *)pageName params:(NSDictionary *)params uniqueId:(NSString *)uniqueId;
 + (void)didDisappearPageContainer:(void (^)(NSNumber *))result pageName:(NSString *)pageName params:(NSDictionary *)params uniqueId:(NSString *)uniqueId;
 + (void)didInitPageContainer:(void (^)(NSNumber *))result pageName:(NSString *)pageName params:(NSDictionary *)params uniqueId:(NSString *)uniqueId;
 + (void)willDeallocPageContainer:(void (^)(NSNumber *))result pageName:(NSString *)pageName params:(NSDictionary *)params uniqueId:(NSString *)uniqueId;

Flutter界面跳转其他界面流程

由于FlutterBoost的理念就是页面跳转由原生页面来驱动,所以从Flutter界面跳转到其他界面的流程,也和上面的差不多,只不过开始的时候,多了一步,即Dart代码调用FlutterBoost的open函数时,flutter侧通过channel发送一个openPage消息到FlutterBoost的Native侧,然后再执行Native侧的open方法:

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

Flutter端

Flutter端,FlutterBoost主要有以下几个文件:

文件名功能
boost_channel.dart对flutter和Native的消息通信进行封装
boost_container.dartFlutter端的页面实现。实现了一个Navigator的子类,用于承载页面Widget
boost_page_route.dart实现了一个继承MaterialPageRoute的Route
container_coordinator.dart管理所有PageBuilder,协调同步Flutter和Native端的页面状态
container_manager.dart管理所有页面,受container_coordinator调度,完成页面的push、pop。它本身也是一个Widget,通过对外暴露State,提供了API操作。这个Widget内部管理了一个Overlay组件,从而实现了多视图栈的管理。
flutter_boost.dartFlutterBoost主类,维护FlutterBoost的单实例,并对外提供open、close等接口

Flutter端的核心,就是多视图栈管理。对于一个使用FlutterBoost管理页面的app,dart端的页面层级关系是这样的:

flutter_container_stack

使用FluttberBoost前,开发者需要将所有flutter页面的builder注册到FlutterBoost中:

FlutterBoost.singleton.registerPageBuilders(<String, PageBuilder>{
      'embeded': (String pageName, Map<String, dynamic> params, String _) =>
          EmbeddedFirstRouteWidget(),
      'first': (String pageName, Map<String, dynamic> params, String _) => FirstRouteWidget(),
      'second': (String pageName, Map<String, dynamic> params, String _) => SecondRouteWidget(),      
    });

如果有需要,可以实现一个NavigatorObserver来监听导航的变化:

FlutterBoost.singleton.addBoostNavigatorObserver(TestBoostNavigatorObserver());

当需要打开一个flutter页面时,调用FlutterBoost的open函数:

FlutterBoost.singleton
                    .open('firstFirst')
                    .then((Map<dynamic, dynamic> value) {
                  print(
                      'call me when page is finished. did recieve FF route result $value');
});

在open函数内部,通过channel通知Native:

Future<Map<dynamic, dynamic>> open(
    String url, {
    Map<String, dynamic> urlParams,
    Map<String, dynamic> exts,
  }) {
    final Map<String, dynamic> properties = <String, dynamic>{};
    properties['url'] = url;
    properties['urlParams'] = urlParams;
    properties['exts'] = exts;
    return channel.invokeMethod<Map<dynamic, dynamic>>('openPage', properties);
  }

之后,ContainerCoordinator可能会接收到Native端的各种页面生命周期的通知,并进行处理。

Future<dynamic> _onMethodCall(MethodCall call) {
    Logger.log('onMetohdCall ${call.method}');

    final String pageName = call.arguments['pageName'] as String;
    final Map<String, dynamic> params =
        (call.arguments['params'] as Map<dynamic, dynamic>)
            ?.cast<String, dynamic>();
    final String uniqueId = call.arguments['uniqueId'] as String;

    switch (call.method) {
      case 'didInitPageContainer':
        _nativeContainerDidInit(pageName, params, uniqueId);
        break;
      case 'willShowPageContainer':
        _nativeContainerWillShow(pageName, params, uniqueId);
        break;
      case 'didShowPageContainer':
        nativeContainerDidShow(pageName, params, uniqueId);
        break;
      case 'willDisappearPageContainer':
        _nativeContainerWillDisappear(pageName, params, uniqueId);
        break;
      case 'didDisappearPageContainer':
        _nativeContainerDidDisappear(pageName, params, uniqueId);
        break;
      case 'willDeallocPageContainer':
        _nativeContainerWillDealloc(pageName, params, uniqueId);
        break;
      case 'onNativePageResult':
        break;
    }

    return Future<dynamic>(() {});
  }

Flutter页面的显示就是在_nativeContainerWillShow函数中处理的:

if (FlutterBoost.containerManager?.containsContainer(pageId) != true) {
  FlutterBoost.containerManager?.pushContainer(
    _createContainerSettings(name, params, pageId),
  );
}

pushContainer中,对加入要显示的页面信息,并进行绘制。offstage包含了所有未在栈顶的页面信息,onstage表示当前需要显示的页面。

_offstage.add(_onstage);
_onstage = BoostContainer.obtain(widget.initNavigator, settings);
setState(() {});

在setState中,会调用以下函数:

  void _refreshOverlayEntries() {
    final OverlayState overlayState = _overlayKey.currentState;

    if (overlayState == null) {
      return;
    }
    //  移除所有的_ContainerOverlayEntry
    if (_leastEntries != null && _leastEntries.isNotEmpty) {
      for (final _ContainerOverlayEntry entry in _leastEntries) {
        entry.remove();
      }
    }

    // 添加所有非顶层页面,然后添加顶层页面
    final List<BoostContainer> containers = <BoostContainer>[];
    containers.addAll(_offstage);

    assert(_onstage != null, 'Should have a least one BoostContainer');
    containers.add(_onstage);

    // 转换成_ContainerOverlayEntry的list,添加到overlayState上。
    _leastEntries = containers
        .map<_ContainerOverlayEntry>(
            (BoostContainer container) => _ContainerOverlayEntry(container))
        .toList(growable: false);
    
    overlayState.insertAll(_leastEntries);

    ///...
  }