FlutterBoost管理混合棧iOS實踐

Inlight發表於2019-12-22

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

  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:手勢互動聽誰的?

相關文章