Flutter Boost 混合開發框架初探

xiangzhihong發表於2021-04-01

一、Flutter Boost簡介

眾所周知,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官方也沒有提供好的解決方案。除了記憶體消耗嚴重外,多引擎模式還會帶來如下一些問題。

  • 冗餘資源問題。多引擎模式下每個引擎的Isolate是相互獨立的,雖然在邏輯上這並沒有什麼壞處,但是每個引擎底層都維護了一套圖片快取等比較消耗記憶體的物件,因此裝置的記憶體消耗是非常嚴重的。
  • 外掛註冊問題。在Flutter外掛中,訊息傳遞需要依賴Messenger,而Messenger是由FlutterViewController去實現的。如果一個應用中同時存在多個FlutterViewController,那麼外掛的註冊和通訊將會變得混亂且難以維護。
  • Flutter元件和原生頁面的差異化問題。通常,Flutter頁面是由元件構成的,原生頁面則是由ViewController或者Activity構成的。邏輯上來說,我們希望消除Flutter頁面與原生頁面的差異,否則在進行頁面埋點和其它一些操作時增加一些額外的工作量。
  • 增加頁面通訊的複雜度。如果所有的Dart程式碼都執行在同一個引擎例項中,那麼它們會共享同一個Isolate,可以用統一的框架完成元件之間的通訊,但是如果存在多個引擎例項會讓Isolate的管理變得更加複雜。

如果不解決多引擎問題,那麼混合專案的導航棧如下圖所示。 在這裡插入圖片描述 目前,對於原生工程混編Flutter工程出現的多引擎模式問題,國內主要有兩種解決方案,一種是位元組跳動的修改Flutter Engine原始碼方案,另一種是閒魚開源的FlutterBoost。由於位元組跳動的混合開發的方案沒有開源,所以現在能使用的就剩下FlutterBoost方案。

FlutterBoost是閒魚技術團隊開發的一個可複用頁面的外掛,旨在把Flutter容器做成類似於瀏覽器的載入方案。為此,閒魚技術團隊為希望FlutterBoost能完成如下的基本功能:

  • 可複用的通用型混合開發方案。
  • 支援更加複雜的混合模式,比如支援Tab切換的場景。
  • 無侵入性方案,使用時不再依賴修改Flutter的方案。
  • 支援對頁面生命週期進行統一的管理。
  • 具有統一明確的設計概念。

並且,最近Flutter Boost升級了3.0版本,並帶來了如下的一些更新:

  • 不侵入引擎,相容Flutter的各種版本,Flutter sdk的升級不需要再升級FlutterBoost,極大降低升級成本。
  • 不區分Androidx和Support分支。
  • 簡化架構和介面,和FlutterBoost2.0比,程式碼減少了一半。
  • 雙端統一,包括介面和設計上的統一。
  • 支援開啟Flutter頁面,不再開啟容器場景。
  • 頁面生命週期變化通知更方便業務使用。
  • 解決了2.0中的遺留問題,例如,Fragment接入困難、頁面關閉後不能傳遞資料、dispose不執行,記憶體佔用過高等。

二、Flutter Boost整合

在原生專案中整合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外掛拉取到本地。

2.1 Android整合

使用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頁面,如下圖所示效果。 在這裡插入圖片描述

2.2 iOS整合

和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 Boost架構

對於混合工程來說,原生端和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

四、FlutterBoost3.0更新

4.1 不入侵引擎

為了解決官方引擎複用引起的問題,FlutterBoost2.0拷貝了Flutter引擎Embedding層的一些程式碼進行改造,這使得後期的升級成本極高。而FlutterBoost3.0採用繼承的方式擴充套件FlutterActivity/FlutterFragment等元件的能力,並且通過在適當時機給Dart側傳送appIsResumed訊息解決引擎複用時生命週期事件錯亂導致的頁面卡死問題,並且,FlutterBoost 3.0 也相容最新的官方釋出的 Flutter 2.0。

4.2 不區分Androidx和Support分支

FlutterBoost2.0通過自己實現FlutterActivityAndFragmentDelegate.Host介面來擴充套件FlutterActivity和FlutterFragment的能力,而getLifecycle是必須實現的介面,這就導致對androidx的依賴。這也是為什麼FlutterBoostView的實現沒有被放入FlutterBoost3.0外掛中的原因。而FlutterBoost3.0通過繼承的方式擴充套件FlutterActivity/FlutterFragment的能力的額外收益就是,可以做到不依賴androidx。

4.3 雙端設計統一,介面統一

很多Flutter開發者只會一端,只會Android 或者只會IOS,但他需要接入雙端,所以雙端統一能降低他的 學習成本和接入成本。FlutterBoost3.0,在設計上 Android和IOS都做了對齊,特別介面上做到了引數級的對齊。

4.4 支援 【開啟flutter頁面不再開啟容器】 場景

在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}),
)
複製程式碼

4.5 生命週期的精準通知

在FlutterBoost2.0上,每個頁面都會收到頁面生命週期通知,而FlutterBoost3.0只會通知頁面可見性實際發生了變化的頁面,介面也更符合flutter的設計。

4.6 其他Issue

除了上面的一些特性外,Flutter Boost 3.0版本還解決了如下一些問題:

  • 頁面關閉後引數的傳遞,之前只有iOS支援,android不支援,目前在dart側實現,Ios 和Android 都支援。
  • 解決了Android 狀態列字型和顏色問題。
  • 解決了頁面回退willpopscope不起作用問題。
  • 解決了不在棧頂的頁面也收到生命週期回撥的問題
  • 解決了多次setState耗效能問題。
  • 提供了Framgent 多種接入方式的Demo,方便tab 場景的接入。
  • 生命週期的回撥程式碼,可以使用者程式碼裡面with的方式接入,使用更簡單。
  • 全面簡化了,接入成本,包括 dart側,android側和ios
  • 豐富了demo,包含了基本場景,方便使用者接入 和測試迴歸

相關文章