阅读 66

FlutterBoost3.0 原理解析

背景

随着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页面的路由管理。

原生侧

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 } 复制代码

流程图 (2).jpg

Pop 操作流程如下

流程图 (3).jpg

流程图 (4).jpg

4 页面周期管理

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

页面生命周期

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

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐