时间:2021-11-14 10:25:01 | 栏目:iOS代码 | 点击:次
众所周知,Flutter是一个由C++实现的Flutter Engine和由Dart实现的Framework组成的跨平台技术框架。其中,Flutter Engine负责线程管理、Dart VM状态管理以及Dart代码加载等工作,而Dart代码所实现的Framework则负责上层业务开发,如Flutter提供的组件等概念就是Framework的范畴。
随着Flutter的发展,国内越来越多的App开始接入Flutter。为了降低风险,大部分App采用渐进式方式引入Flutter,在App里选几个页面用Flutter来编写,但都碰到了相同的问题,在原生页面和Flutter页面共存的情况下,如何管理路由,以及原生页面与Flutter页面之间的切换和通信都是混合开发中需要解决的问题。然而,官方没有提供明确的解决方案,只是在混合开发时,官方建议开发者,应该使用同一个引擎支持多窗口绘制的能力,至少在逻辑上做到FlutterViewController是共享同一个引擎里面的资源。换句话说,官方希望所有的绘制窗口共享同一个主Isolate,而不是出现多个主Isolate的情况。不过,对于现在已经出现的多引擎模式问题,Flutter官方也没有提供好的解决方案。除了内存消耗严重外,多引擎模式还会带来如下一些问题。
如果不解决多引擎问题,那么混合项目的导航栈如下图所示。
目前,对于原生工程混编Flutter工程出现的多引擎模式问题,国内主要有两种解决方案,一种是字节跳动的修改Flutter Engine源码方案,另一种是闲鱼开源的FlutterBoost。由于字节跳动的混合开发的方案没有开源,所以现在能使用的就剩下FlutterBoost方案。
FlutterBoost是闲鱼技术团队开发的一个可复用页面的插件,旨在把Flutter容器做成类似于浏览器的加载方案。为此,闲鱼技术团队为希望FlutterBoost能完成如下的基本功能:
并且,最近Flutter Boost升级了3.0版本,并带来了如下的一些更新:
在原生项目中集成Flutter Boost只需要将Flutter Boost看成是一个插件工程即可。和其他Flutter插件的集成方式一样,使用FlutterBoost之前需要先添加依赖。使用Android Studio打开混合工程的Flutter工程,在pubspec.yaml中添加FlutterBoost依赖插件,如下所示。
flutter_boost: git: url: 'https://github.com/alibaba/flutter_boost.git' ref: 'v3.0-hotfixes'
需要说明的是,此处的所依赖的FlutterBoost的版本与Flutter的版本是对应的,如果不对应使用过程中会出现版本不匹配的错误。然后,使用flutter packages get命令将FlutterBoost插件拉取到本地。
使用Android Studio打开新建的原生Android工程,在原生Android工程的settings.gradle文件中添加如下代码。
setBinding(new Binding([gradle: this])) evaluate(new File( settingsDir.parentFile, 'flutter_library/.android/include_flutter.groovy'))
然后,打开原生Android工程app目录下的build.gradle文件,继续添加如下依赖脚本。
dependencies { implementation project(':flutter_boost') implementation project(':flutter') }
重新编译构建原生Android工程,如果没有任何错误则说明Android成功了集成FlutterBoost。使用Flutter Boost 之前,需要先执行初始化。打开原生Android工程,新建一个继承FlutterApplication的Application,然后在onCreate()方法中初始化FlutterBoost,代码如下。
public class MyApplication extends FlutterApplication { @Override public void onCreate() { super.onCreate(); FlutterBoost.instance().setup(this, new FlutterBoostDelegate() { @Override public void pushNativeRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new Intent(FlutterBoost.instance().currentActivity(), NativePageActivity.class); FlutterBoost.instance().currentActivity().startActivity(intent); } @Override public void pushFlutterRoute(String pageName, HashMap<String, String> arguments) { Intent intent = new FlutterBoostActivity.CachedEngineIntentBuilder(FlutterBoostActivity.class, FlutterBoost.ENGINE_ID) .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.opaque) .destroyEngineWithActivity(false) .url(pageName) .urlParams(arguments) .build(FlutterBoost.instance().currentActivity()); FlutterBoost.instance().currentActivity().startActivity(intent); } },engine->{ engine.getPlugins(); } ); } }
然后,打开原生Android工程下的AndroidManifest.xml文件,将Application替换成自定义的MyApplication,如下所示。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.idlefish.flutterboost.example"> <application android:name="com.idlefish.flutterboost.example.MyApplication" android:label="flutter_boost_example" android:icon="@mipmap/ic_launcher"> <activity android:name="com.idlefish.flutterboost.containers.FlutterBoostActivity" android:theme="@style/Theme.AppCompat" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" > <meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/launch_background"/> </activity> <meta-data android:name="flutterEmbedding" android:value="2"> </meta-data> </application> </manifest>
由于Flutter Boost 是以插件的方式集成到原生Android项目的,所以我们可以在Native 打开和关闭Flutter模块的页面。
FlutterBoost.instance().open("flutterPage",params); FlutterBoost.instance().close("uniqueId");
而Flutter Dart的使用如下。首先,我们可以在main.dart文件的程序入口main()方法中进行初始化。
void main() { runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { static Map<String, FlutterBoostRouteFactory> routerMap = { '/': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => Container()); }, 'embedded': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => EmbeddedFirstRouteWidget()); }, 'presentFlutterPage': (settings, uniqueId) { return PageRouteBuilder<dynamic>( settings: settings, pageBuilder: (_, __, ___) => FlutterRouteWidget( params: settings.arguments, uniqueId: uniqueId, )); }}; Route<dynamic> routeFactory(RouteSettings settings, String uniqueId) { FlutterBoostRouteFactory func =routerMap[settings.name]; if (func == null) { return null; } return func(settings, uniqueId); } @override void initState() { super.initState(); } @override Widget build(BuildContext context) { return FlutterBoostApp( routeFactory ); }
当然,还可以监听页面的生命周期,如下所示。
class SimpleWidget extends StatefulWidget { final Map params; final String messages; final String uniqueId; const SimpleWidget(this.uniqueId, this.params, this.messages); @override _SimpleWidgetState createState() => _SimpleWidgetState(); } class _SimpleWidgetState extends State<SimpleWidget> with PageVisibilityObserver { static const String _kTag = 'xlog'; @override void didChangeDependencies() { super.didChangeDependencies(); print('$_kTag#didChangeDependencies, ${widget.uniqueId}, $this'); } @override void initState() { super.initState(); PageVisibilityBinding.instance.addObserver(this, ModalRoute.of(context)); print('$_kTag#initState, ${widget.uniqueId}, $this'); } @override void dispose() { PageVisibilityBinding.instance.removeObserver(this); print('$_kTag#dispose, ${widget.uniqueId}, $this'); super.dispose(); } @override void onForeground() { print('$_kTag#onForeground, ${widget.uniqueId}, $this'); } @override void onBackground() { print('$_kTag#onBackground, ${widget.uniqueId}, $this'); } @override void onAppear(ChangeReason reason) { print('$_kTag#onAppear, ${widget.uniqueId}, $reason, $this'); } void onDisappear(ChangeReason reason) { print('$_kTag#onDisappear, ${widget.uniqueId}, $reason, $this'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('tab_example'), ), body: SingleChildScrollView( physics: BouncingScrollPhysics(), child: Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: const EdgeInsets.only(top: 80.0), child: Text( widget.messages, style: TextStyle(fontSize: 28.0, color: Colors.blue), ), alignment: AlignmentDirectional.center, ), Container( margin: const EdgeInsets.only(top: 32.0), child: Text( widget.uniqueId, style: TextStyle(fontSize: 22.0, color: Colors.red), ), alignment: AlignmentDirectional.center, ), InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(30.0), color: Colors.yellow, child: Text( 'open flutter page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ) Container( height: 300, width: 200, child: Text( '', style: TextStyle(fontSize: 22.0, color: Colors.black), ), ) ], ))), ); } }
然后,运行项目,就可以从原生页面跳转到Flutter页面,如下图所示效果。
和Android的集成步骤一样,使用Xcode打开原生iOS工程,然后在iOS的AppDelegate文件中初始化Flutter Boost ,如下所示。
@interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { MyFlutterBoostDelegate* delegate=[[MyFlutterBoostDelegate alloc ] init]; [[FlutterBoost instance] setup:application delegate:delegate callback:^(FlutterEngine *engine) { } ]; return YES; } @end
下面是自定义的FlutterBoostDelegate的代码,如下所示。
@interface MyFlutterBoostDelegate : NSObject<FlutterBoostDelegate> @property (nonatomic,strong) UINavigationController *navigationController; @end @implementation MyFlutterBoostDelegate - (void) pushNativeRoute:(FBCommonParams*) params{ BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; UIViewControllerDemo *nvc = [[UIViewControllerDemo alloc] initWithNibName:@"UIViewControllerDemo" bundle:[NSBundle mainBundle]]; if(present){ [self.navigationController presentViewController:nvc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:nvc animated:animated]; } } - (void) pushFlutterRoute:(FBCommonParams*)params { FlutterEngine* engine = [[FlutterBoost instance ] getEngine]; engine.viewController = nil; FBFlutterViewContainer *vc = FBFlutterViewContainer.new ; [vc setName:params.pageName params:params.arguments]; BOOL animated = [params.arguments[@"animated"] boolValue]; BOOL present= [params.arguments[@"present"] boolValue]; if(present){ [self.navigationController presentViewController:vc animated:animated completion:^{ }]; }else{ [self.navigationController pushViewController:vc animated:animated]; } } - (void) popRoute:(FBCommonParams*)params result:(NSDictionary *)result{ FBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController; if([vc isKindOfClass:FBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: params.uniqueId]){ [vc dismissViewControllerAnimated:YES completion:^{}]; }else{ [self.navigationController popViewControllerAnimated:YES]; } } @end
如果要在原生iOS代码中打开或关闭Flutter页面,可以使用下面的方式。
[[FlutterBoost instance] open:@"flutterPage" arguments:@{@"animated":@(YES)} ]; [[FlutterBoost instance] open:@"secondStateful" arguments:@{@"present":@(YES)}];
对于混合工程来说,原生端和Flutter端对于页面的定义是不一样的。对于原生端而言,页面通常指的是一个ViewController或者Activity,而对于Flutter来说,页面通常指的是Flutter组件。FlutterBoost框架所要做的就是统一混合工程中页面的概念,或者说弱化Flutter组件对应容器页面的概念。换句话说,当有一个原生页面存在的时候,FlutteBoost就能保证一定有一个对应的Flutter的容器页面存在。
FlutterBoost框架其实就是由原生容器通过消息驱动Flutter页面容器,从而达到原生容器与Flutter容器同步的目的,而Flutter渲染的内容是由原生容器去驱动的,下面是Flutter Boost 给的一个Flutter Boost 的架构示意图。
可以看到,Flutter Boost插件分为平台和Dart两端,中间通过Message Channel连接。平台侧提供了Flutter引擎的配置和管理、Native容器的创建/销毁、页面可见性变化通知,以及Flutter页面的打开/关闭接口等。而Dart侧除了提供类似原生Navigator的页面导航接口的能力外,还负责Flutter页面的路由管理。
总的来说,正是基于共享同一个引擎的方案,使得FlutterBoost框架有效的解决了多引擎的问题。简单来说,FlutterBoost在Dart端引入了容器的概念,当存在多个Flutter页面时,FlutterBoost不需要再用栈的结构去维护现有页面,而是使用扁平化键值对映射的形式去维护当前所有的页面,并且每个页面拥有一个唯一的id
为了解决官方引擎复用引起的问题,FlutterBoost2.0拷贝了Flutter引擎Embedding层的一些代码进行改造,这使得后期的升级成本极高。而FlutterBoost3.0采用继承的方式扩展FlutterActivity/FlutterFragment等组件的能力,并且通过在适当时机给Dart侧发送appIsResumed消息解决引擎复用时生命周期事件错乱导致的页面卡死问题,并且,FlutterBoost 3.0 也兼容最新的官方发布的 Flutter 2.0。
FlutterBoost2.0通过自己实现FlutterActivityAndFragmentDelegate.Host接口来扩展FlutterActivity和FlutterFragment的能力,而getLifecycle是必须实现的接口,这就导致对androidx的依赖。这也是为什么FlutterBoostView的实现没有被放入FlutterBoost3.0插件中的原因。而FlutterBoost3.0通过继承的方式扩展FlutterActivity/FlutterFragment的能力的额外收益就是,可以做到不依赖androidx。
很多Flutter开发者只会一端,只会Android 或者只会IOS,但他需要接入双端,所以双端统一能降低他的 学习成本和接入成本。FlutterBoost3.0,在设计上 Android和IOS都做了对齐,特别接口上做到了参数级的对齐。
在Flutter模块内部,Flutter 页面跳转Flutter 页面是可以不需要再打开Flutter容器的,不打开容器,能节省内存开销。在FlutterBoost3.0上,打开容器和不打开容器的区别表现在用户接口上仅仅是withContainer参数是否为true就好。
InkWell( child: Container( color: Colors.yellow, child: Text( '打开外部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", arguments: <String, String>{'from': widget.uniqueId}), ), InkWell( child: Container( color: Colors.yellow, child: Text( '打开内部路由', style: TextStyle(fontSize: 22.0, color: Colors.black), )), onTap: () => BoostNavigator.of().push("flutterPage", withContainer: true, arguments: <String, String>{'from': widget.uniqueId}), )
在FlutterBoost2.0上,每个页面都会收到页面生命周期通知,而FlutterBoost3.0只会通知页面可见性实际发生了变化的页面,接口也更符合flutter的设计。
除了上面的一些特性外,Flutter Boost 3.0版本还解决了如下一些问题: