FlutterBoost管理混合栈iOS实践

4,546 阅读7分钟

官方混合方案多引擎的弊端

  1. 官方方案在Native和Flutter页面交叉跳转时于Flutter Engine数量会线性增加导致内存暴增(这里指图片缓存等比较消耗内存的对象)。
  2. 多个FlutterViewController,插件的注册和通信将会变得混乱难以维护,消息的传递的源头和目标也变得不可控。
  3. Flutter页面和Native页面差异化,一些统一操作增加复杂度。
  4. Flutter应用中全局变量在各独立页面不能共享的问题。
  5. iOS平台内存泄露的问题。

官方目前就共享同一个引擎做混合开发没有很好的支持。

混合栈管理问题

混合开发(RN,Flutter,Native)导致的混合栈的管理一直是个比较烦的问题,iOS端的表现主要包括对导航栈的一些特殊处理(增删改之类),之前做RN和Native混合开发就遇见过类似的问题。本身Native有自己的一套导航栈,RN自己也有一套Navigation的管理(我们这里使用社区维护的React Navigation),每次Native打开RN是打开一个新的VC,但RN页面通过React Navigation打开一个或多个RN页面时实际上是在同一个VC中,这就导致RN和Native交叉跳转多次后混合栈变得混乱,Native并不知道栈里面实际有多少个页面,想要直接返回到这些交叉页面的某一个页面(可能是Native或者RN)也变得困难,所以我们在处理RN和Native混合栈跳转实践过程添加了很多特殊处理,包括什么时候使用新开VC的push,什么时候使用不新开VC的push,以及手势返回什么时候使用Native响应,什么时候使用RN响应,跨多个页面pop如何计算要pop几个等一系列问题,让开发和维护都变得复杂。

当然Flutter和Native混合开发也要面对类似的问题,再加上之前的RN,导航栈的管理就更加棘手了。

FlutterBoost

官方介绍:

The philosophy of FlutterBoost is to use Flutter as easy as using a WebView

趟过坑的大厂(阿里巴巴-闲鱼技术)很明显已经走在了前面,开源了名为FlutterBoost的插件。它采用共享引擎的模式实现,解决的多引擎的很多弊端。统一管理Flutter页面映射和跳转,让pushpop的导航栈操作和Native保持一致,在我们在开发过程中就无须关心Native和Flutter导航栈交叉跳转所带来的各种问题。

目前FlutterBoost支持稳定版本的flutter v1.9.1-hotfixes,最新的1.12正在适配中。

集成

  1. 在Flutter模块的pubspec.yaml添加依赖:
flutter_boost:
    git:
        url: 'https://github.com/alibaba/flutter_boost.git'
        ref: '0.1.63'

  1. Flutter模块下执行flutter pub get更新依赖资源。
  2. 在Native工程下执行pod install将Flutter相关产物依赖带到Native。

这里需要注意每次更改pubspec.yaml文件重新做flutter pub get后,都要做pod install重新依赖产物。不然产物有变更而Native没有同步更新导致报错运行不起来。

在Flutter模块中使用

  1. runApp()函数传入的RootWidget中注册所有的Flutter页面。
  @override
  void initState() {
    super.initState();
    FlutterBoost.singleton.registerPageBuilders({
      'account/about': (pageName, params, _) => AboutWidget(),
      'account/feedback': (pageName, params, _) => FeedbackWidget(),
    });
  }
  1. RootWidgetbuild方法中初始化FlutterBoost
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        builder: FlutterBoost.init(),
  }
  1. 在Flutter模块中打开和关闭页面
  FlutterBoost.singleton.open('account/feedback');
  FlutterBoost.singleton.open('native', urlParams: {'id': '123456'});
  FlutterBoost.singleton.open('native', urlParams: {'id': '123456'}, exts: {});
  FlutterBoost.singleton.close('account/feedback', result: {}, exts: {});
  FlutterBoost.singleton.closeCurrent(result: {}, exts: {});

根据页面需求调用方法即可,还有一些更细节的用法如Native和Flutter间回传值等,有需要的可以去FlutterBoost的example里面了解。

在Native中使用

使用之前我们先了解两个类和一个协议:

  • FlutterBoostPlugin(共享引擎的管理,打开和关闭页面)
  • FLBFlutterViewContainer(在官方的FlutterViewController上做了一层封装)
  • FLBPlatform(实现此协议后统一处理FlutterBoostPlugin开关页面的回调)

具体使用:

  1. 工程启动时候做FlutterBoostPlugin的初始化
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    PlatformRouterImp *router = [PlatformRouterImp new];
    [FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router
                                                        onStart:^(FlutterEngine *engine) {
                                                            
                                                        }];
}

其中PlatformRouterImp主要是实现FLBPlatform协议,让使用FlutterBoostPlugin打开和关闭页面时能够统一处理。

@protocol FLBPlatform;
@interface PlatformRouterImp : NSObject<FLBPlatform>
@property (nonatomic,strong) UINavigationController *navigationController;
@end
@implementation PlatformRouterImp

- (void)open:(NSString *)name urlParams:(NSDictionary *)params exts:(NSDictionary *)exts completion:(void (^)(BOOL))completion {
    BOOL animated = [exts[@"animated"] boolValue];
    MyFlutterViewController *vc = [[MyFlutterViewController alloc] init];
    [vc setName:name params:params];
    [self.navigationController pushViewController:vc animated:animated];

- (void)present:(NSString *)name urlParams:(NSDictionary *)params exts:(NSDictionary *)exts completion:(void (^)(BOOL))completion {
    BOOL animated = [exts[@"animated"] boolValue];
    MyFlutterViewController *vc = [[MyFlutterViewController alloc] init];
    [vc setName:name params:params];
    [self.navigationController presentViewController:vc animated:animated completion:^{
        if (completion) completion(YES);
    }];
}

- (void)close:(NSString *)uid result:(NSDictionary *)result exts:(NSDictionary *)exts completion:(void (^)(BOOL))completion {
    BOOL animated = [exts[@"animated"] boolValue];
    MyFlutterViewController *vc = (id)self.navigationController.presentedViewController;
    if ([vc isKindOfClass:MyFlutterViewController.class] && [vc.uniqueIDString isEqual: uid]) {
        [vc dismissViewControllerAnimated:animated completion:^{}];
    } else {
        [self.navigationController popViewControllerAnimated:animated];
    }
}

@end

之后每次使用FlutterBoostPlugin打开和关闭页面都会回调给实现了FLBPlatform协议的PlatformRouterImp

[FlutterBoostPlugin open:@"account/Feedback" urlParams:nil exts:nil onPageFinished:^(NSDictionary *params) {
    
} completion:^(BOOL isComplete) {
    
}];

MyFlutterViewController继承自FLBFlutterViewContainer,目前主要用来处理Native页面和Flutter页面交叉跳转时Native导航条是否展示的问题,混合开发虽然Flutter页面外层也是一个VC,但我们并不希望导航条UI样式也使用Native的,Flutter页面应该有自己的导航条样式和逻辑,这样也更利于日后的维护和拓展。

混合踩坑

  1. Native页面手势返回到Flutter页面会跳一下(只有真机会出现模拟器没问题)。

这个问题本来最初一直以为是FlutterBoost的问题,定位了好久才发现锅在Native。我们Native工程每次跳到下一个页面会把上一个页面的截图保存在内存中,然后在每次手势返回时展示此截图。问题就出在截图的代码。

使用renderInContext方法截图

UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
UIGraphicsBeginImageContextWithOptions(window.bounds.size, window.opaque, 0);
[appdelegate.window.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();

使用drawViewHierarchyInRect方法截图

UIWindow *window = [[[UIApplication sharedApplication] delegate] window];
UIGraphicsBeginImageContextWithOptions(window.bounds.size, window.opaque, 0);
[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:NO];
UIImage *snapshotImage=UIGraphicsGetImageFromCurrentImageContext();
  • renderInContext是对viewlayer渲染到当前的上下文中。
  • drawViewHierarchyInRect是对view进行一个快照,然后将快照渲染到当前的上下文中。

在Flutter页面使用renderInContext这种方式截图会导致截图不完整,只截取了一部分(每次手势返回会先展示不完整的截图再展示页面,所以会有跳动的感觉),具体原因猜测跟Flutter的渲染方式有关系,若有明白的大神还望指点~

  1. Flutter页面的ListView滚动很卡顿

这个问题最初出现在模拟器,发现也有很多人遇见类似卡顿的问题,说Debug模式的Flutter性能不高,有卡顿也正常,当时还在想布局这么简单的List也能卡成这样,叹气~,然后果断真机跑一下,What?还有同样的问题,定位良久发现是手势处理的锅。

这个问题是由于为了响应Native的手势返回在处理手势时将RRDFlutterViewController的手势给拦截了,导致ListView的滑动刚被触发就取消了,这就造成了类似很卡的效果。

解决方式将触摸事件传递给FlutterView。

gestureRecognizer.cancelsTouchesInView = NO;

总结

FlutterBoost初步使用感觉还是不错的,帮助我们解决了很多官方的坑,阿里巴巴-闲鱼技术也确实投入了不少人力在支持,目前来看是跟着Flutter的稳定版本不断做更新的。

后续有坑和总结还会在本文中作补充。

友情提示:如果你也在用FlutterBoost,遇见问题多去看看官方Demo和issue,相信大部分都会有觉解方案。

相关链接

FlutterBoost

如何用 Flutter 实现混合开发?闲鱼公开源代码实例

Flutter 和 iOS 之间的 Battle:手势交互听谁的?