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
多引擎模式存在的问题
- 加载资源的冗余,多引擎模式下,不同的engine维护各自的图片/文件缓存,对于资源的重复占用内存,加大了内存压力
- 插件注册以及同原生通信混乱,flutter通过message 与原生实现消息传递,多引擎会造成插件注册以及插件和原生的channel 造成错落
- 增加了页面之间的传参的复杂,多引擎方案会产生多个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启动流程
- 首先初始化flutter engine
- 通过[engine runWithEntrypoint],创建isolate,运行包含main()函数的dart 程序
- 根据配置项预热引擎
- 注册所有plugin和消息channel,并获取到boost插件实力传入delegate
- 监听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 创建与周期同步
- App 启动后会创建FBFlutterViewContainer
- 将创建的contaier 以key-value的形式存储在containerManager
- 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;
}
}
- 最终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
}
Pop 操作流程如下
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