flutter 多例項實戰

共田君發表於2019-02-21

tags: flutter flutter多例項

在混合開發中,我們使用fluter作為外掛化開發,即起一個flutterviewcontroller,這就是一個外掛,該外掛與其他模組並沒有任何互動,用的資料來源是通過method channel主動從從宿主app取得的.

具體的需求是這樣的,在第二個tab中放入一個flutter做的的視訊頁面,另外第三個tab有兩個外掛的入口,也是用flutter寫的

第二個tabflutter

兩個外掛

 [原生]  ---> [flutter]
複製程式碼

痛點問題

拿到需求第一步就想到,存在幾個問題

  1. 如何同時開啟多個外掛,或者從一個外掛開啟另一個外掛,即保持多個flutter vc並存
  2. 多個flutter啟動後如何保證記憶體

第一次嘗試 建立

於是只需要使用建立程式碼不就完了嗎

   FlutterViewController* flutterViewController = [[FlutterViewController alloc] initWithEngine:self.engine nibName:nil bundle:nil];
複製程式碼

然而事情並沒有那麼簡單 首先在tab中插入的flutterviewcontroller.view直接拿出來顯示不出來,使用延遲載入也不管用

第二次嘗試 - 解決顯示問題

經過大神指點要先使用present然後dismiss才能顯示出來

  __weak __typeof(self)weakSelf = self;
        self.ctr4.modalPresentationStyle = UIModalPresentationOverCurrentContext;
        [self presentViewController:weakSelf.ctr4 animated:NO completion:^{
            [weakSelf dismissViewControllerAnimated:NO completion:^{
                [weakSelf addChildViewController:weakSelf.ctr4];
                [weakSelf.view bringSubviewToFront:weakSelf.tabbarContainer];
            }];
        }];
複製程式碼

接下來準備新增多個flutter 然而在push過程中發現flutter的第一次顯示的介面竟然是上次tab的頁面,因為engine是同一份的,我們建立的時候會儲存一份engine。

第三次嘗試 顯示錯誤

這裡有個前提是1.0 ios flutter engine無法釋放,如果僅僅使用FlutterViewController.new的方式肯定是會有釋放的,但是官方提供了一種根據engine建立 fluttervc的方式,所以保留一份engine,或者說讓engine保留成一個單例狀態。

至此第三次嘗試失敗

但是從這一次的問題來看,flutter上面的介面並不是跟著fluttervc走的,而是跟著engine走的,fluttervc僅僅提供了一個手勢和其他事件入口,所以即使關閉了fluttervc或者delloc了,只要engine存在,圖形渲染就保留了上一次的介面,到此為止多例項的fluttervc從根本上就沒有存在的必要了。

第四次嘗試 單例項實現多vc樣式

我們知道fluttervc有個初始routername的方法,在第一次啟動的時候可以設定這個routername

- (void)setInitialRoute:(NSString*)route
複製程式碼

於是想到通過這個來設定不同的路由。 殊不知,這個方法和initWithEngine 搭配使用時,並沒有起作用,傳入到main.dart裡面的window.defaultRouteName一直是 / 根目錄符號

- (instancetype)initWithEngine:(FlutterEngine*)engine
                       nibName:(NSString*)nibNameOrNil
                        bundle:(NSBundle*)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
複製程式碼

另外設定即使可以起作用,也無法實現多路由問題。

第五次嘗試 改造setInialRoute

既然官方存在bug,那就解決吧,首先看了一圈flutter engine setInialRoute的實現,最終在shell.cc裡面,如果直接改動engine編譯有點麻煩,想到的解決方案是在main.dart裡面去原生讀取宿主路由 然後渲染對應的頁面。

    MosNativeHelper.defaultRouteName().then((name){
      setState(() {
        if(name != null){
            widget.defaultRouteName = name;
        }
      });
    });

複製程式碼

然而這是第一次讀取,後續怎麼更新新的頁面呢,這時候需要宿主主動發通知給flutter了

第六次嘗試 宿主發訊息給flutter

flutter有個eventchannel就是用於接收宿主的事件回撥的,使用方法是先註冊事件,傳送一個引數給宿主,然後監聽event,最後釋放


  // 註冊一個通知
  static const EventChannel eventChannel = const EventChannel('com.moschat.app/native_post');

  // 渲染前的操作,類似viewDidLoad
  @override
  void initState() {
    super.initState();
    // 監聽事件,同時傳送引數12345
    eventChannel.receiveBroadcastStream(12345).listen(_onEvent,onError: _onError);
    print("[flutter]進入到initState");
    widget.defaultRouteName = window.defaultRouteName;
    MosNativeHelper.defaultRouteName().then((name){
      setState(() {
        if(name != null){
          widget.defaultRouteName = name;
        }
      });
    });
  }
複製程式碼

在宿主端的程式碼

新增eventchannel代理方法

        NSString *channelName = @"com.moschat.app/native_post";
        FlutterEventChannel *evenChannal = [FlutterEventChannel eventChannelWithName:channelName binaryMessenger:flutterViewController];
        // 代理
        [evenChannal setStreamHandler:MOSFlutterEngine.sharedInstance];
複製程式碼

新增代理 FlutterStreamHandler

#pragma mark - <FlutterStreamHandler>
// // 這個onListen是Flutter端開始監聽這個channel時的回撥,第二個引數 EventSink是用來傳資料的載體。
- (FlutterError* _Nullable)onListenWithArguments:(id _Nullable)arguments
                                       eventSink:(FlutterEventSink)events {
    
    // arguments flutter給native的引數
    // 回撥給flutter, 建議使用例項指向,因為該block可以使用多次
    if (events) {
        self.eventsBlock = [events copy];
        self.eventsBlock (@"我是標題");
    }
    return nil;
}

/// flutter不再接收
- (FlutterError* _Nullable)onCancelWithArguments:(id _Nullable)arguments {
    // arguments flutter給native的引數
    return nil;
}
複製程式碼

由於eventblock可以回撥多次,可以達到宿主直接發訊息給flutter的作用。 main.dart中接收到訊息,則可以直接在flutter裡面重新整理根路由介面。

第七次嘗試 優化

在切換外掛過程中還是會有閃現前一個介面的問題,我們在pop fluttervc的時機多flutter傳送一個重新整理一個黑色介面的指令,則在下次啟動時會閃過一個黑色頁面的過程,這個時機可以看做是啟動的過程大概0.3s。 第二個點是在flutter poproute的過程中,有業務需要在中間的route就退出整個vc,此時要注意一個點是,pop vc過程中要主動一層層返回到flutter根部頁面,否則下一次看到的還是上一次的那個頁面。

結語

在多例項的實踐過程中,發現ios的engine除了記憶體問題外,還有根路由設定不成功的問題,從業務方案上使用單engine 單flutterviewcontroller 避免了這一問題,也達到了體驗和記憶體上的最佳效果。

YY Flutter技術積累相關連結


flutter多例項實戰 by共田君

一行程式碼教你解決FlutterPlatformViews記憶體洩露 by AShawn

手把手教你在Flutter專案優雅的使用ORM資料庫 by williamwen1986

flutter通用基礎庫flutter_luakit_plugin by williamwen1986

github - flutter_luakit_plugin使用例子 by williamwen1986

手把手教你編譯Flutter engine by 共田君

手把手教你解決 Flutter engine 記憶體洩漏 by 共田君

github - 編譯產物下載 修復記憶體洩漏後的flutter engine(可直接使用)by 共田君

github demo - 修復記憶體洩漏後的flutter engine by 共田君

持續更新中...

相關文章