扣丁書屋

iPad大屏&Flutter多引擎適配之路(詳細)

背景

在電商場景中iPad的大屏擁有比普通手機相比更大的屏幕,對于購物體驗而言,如能充分利用好iPad的大屏體驗,無疑提高用戶購買體驗,但一直以來在混合棧應用特別是Flutter混合棧中,在iPad大屏適配和Flutter多引擎適配都是個老大難問題。本文會介紹閑魚在這iPad適配中的各個疑難點。

分屏模式

華為,oppo等廠商折疊屏的方案。界面會在展開和折疊時展示不同的視圖樣式。oppo稱為平行視窗,華為稱之為平行視界。蘋果雖未推出折疊屏,但在WWDC2019推出了也為iPad的大屏體驗的解決方案multi window模式。開發者可以根據自己的需求對進行定制。

同一款App在不同設備上保持一致操作邏輯,總是讓使用者感受愉悅的用戶體驗。那么,如何讓iPad版本擁有折疊屏一樣的操作邏輯,讓Pad豎屏等同于折疊屏折疊狀態,橫屏時等同于折疊屏展開左右分屏狀態,iPad的大屏適配工作,應運而生。

分屏模式邏輯

雖然各個折疊屏廠商對于視圖的展示方案各自不同,總體而言共分成兩種展示邏輯。

一種是常用于電商軟件,左右分屏商品對比展示邏輯。這里我稱為雙屏比價模式

第二種是用于類似iPad設置界面,左固定,右打開。這里因為保留了常見的棧導航欄邏輯,這里我稱為雙屏導航模式

下面以連續打開四個視圖(ViewController/Activity)為例子。對比一下普通手機設備,折疊屏設備,以及iPad設備在兩種模式下的差異。

雙屏比價模式

最新視圖在最右屏,次新視圖在最左屏。這種場景適合瀏覽多個商品寶貝進行比對。

iPad版本橫豎屏邏輯

豎屏

豎屏時和普通頁面棧展示邏輯相同。新界面push時,覆蓋舊界面。

橫屏

橫屏時左右分屏,棧模式:左邊永遠是次最新,右邊永遠是最新界面

push邏輯動畫

pop邏輯

分屏導航模式

左屏固定不變,最新、次新視圖堆疊在右屏。這種場景適合左邊是列表頁,在右屏打開多個商品寶貝。其目的是讓最新的界面在都在右邊打開

豎屏

界面:全屏 棧模式:新界面push時,覆蓋舊界面。

橫屏時左右分屏,棧模式:左邊永遠是次最新,右邊永遠是最新界面

push邏輯動畫

pop邏輯

需求

  1. 1. 支持左右分屏
  2. 2. 保留基于棧(UINavigationController)的展示邏輯
  3. 3. 業務改造成本越小越好

技術方案

閑魚內部核心業務都使用flutter進行搭建,涉及由集團中臺提供基礎業務均是原生ViewController,再有部分業務使用H5進行搭建。這三大部分的業務都需要進行兼容改造

1. UINavigationController改造

基于UINavigationController的ContainerViewController永遠都把新的ViewController覆蓋老的ViewController。無法做到上述說的左右分屏。我的做法是基于UINavigationController創建子類,重寫push/pop的ViewCotroller整個排版邏輯。這樣讓整個應用原來的push/pop邏輯不用修改。只需要在iPad使用不一樣的新類NavigationControllerForiPad就能完美的遷移。

自定義ContainerViewCotroller

iOS中專門用于控制ViewController的控制類都統稱為ContainerViewCotroller。如 UINavigationController, UITabBarController, and UIPageViewController

最主要的工作是重寫一個ContainerViewCotroller,自定定義新ViewCotroller被push進來、舊ViewCotroller被pop移除后,后如何排版,以及其中動畫如何展示等問題。

以push新ViewCotroller為例子


-(void)pushViewController:(UIViewController*)newVC animated:(BOOL)animated{
    UIViewController* oldVC = 獲得最倒數第一個ViewController
    [self pushOldViewController:oldVC newViewController: newVC animated: animated]
}

- (void)pushOldViewController:(UIViewController*)oldVC
            newViewController:(UIViewController*)newVC
                    animated:(BOOL)animated {
       ...
        [oldVC beginAppearanceTransition:NO animated:animated];
        //1.將新的Viewcontroller.view加入到根viewcontroller.view
        WrapperView* newWrapperView =
            [self appendWrapperViewWithViewController:newVC
                                         wrapperFrame:[self newViewControllerBeginFrame]
                                               toView:self.view
                                             animated:animated];
        newVC.view.frame = [self childViewFrame];
        newWrapperView.delegate = self;
       //2. 把新的Viewcontroller添加為子Viewcontroller
        [self addChildViewController:newVC];
       //3. 進場動畫
        [UIView animateWithDuration:0.35
            animations:^{
              newWrapperView.frame = [super newViewControllerEndFrame];
              ;
            }
            completion:^(BOOL finished) {
             //4. 進場動畫結束
              [oldVC endAppearanceTransition];
              [newVC didMoveToParentViewController:self];
            }];
}

退場

- (nullable UIViewController*)popViewControllerAnimated:(BOOL)animated {
    //1.移除倒數第一個Viewcontroller
     [lastViewController willMoveToParentViewController:nil];
     [lastViewController beginAppearanceTransition:NO animated:animated];
     //2.倒數第二個Viewcontroller即將顯示
    [secondToLastViewController beginAppearanceTransition:YES animated:animated];
     //3.退場動畫
    [UIView animateWithDuration:0.35
            animations:^{
              lastWrapper.frame = [self newViewStartFrame];
              secondToLastWrapper.frame = [self rightViewFrame];
            }
            completion:^(BOOL finished) {
             //4.移除舊Viewcontroller.view
              [lastWrapper removeFromSuperview];
              [lastViewController endAppearanceTransition];
              [lastViewController removeFromParentViewController];
              // 5.倒數第二個ViewController顯示
              [secondToLastViewController endAppearanceTransition];
    }];
}

這部分功能很核心的工作是,在進場/退場后,依次調用相關函數,這些函數Viewcontroller的生命周期事件至關重要。

更詳細接口文檔:https://developer.apple.com/documentation/uikit/view_controllers/creating_a_custom_container_view_controller

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html

2.原生界面改造

在橫豎屏切換時,會導致ViewController.view 觸發重繪。使用原生進行布局的頁面需要保持所有的view都是相對布局。這里推薦使用autolayout方式,這樣既一套代碼能完美的兼容iPhone/iPad

具體的工作有:

  • ? 棄用[UIScreen main].bound.size ,改為 ViewController.view.size作為當前布局寬高 僅僅支持單屏(iPhone)時,[UIScreen main].bound.size = ViewController.view.size 但在iPad多屏橫屏時 [UIScreen main].bound.size ≠o ViewController.view.size
  • ? 將絕對布局改為相對布局,即使用autolayout
  • ? 感知橫豎屏的切換事件

3. H5界面適配

H5本身已經有非常好的適配不同屏幕大小的特性,但可能在一些特殊的場景上不排除因為歷史原因專為iOS適配hardcode

  • ? 感知橫豎屏的切換事件

4. Flutter業務技術改造

界面技術改造

flutter業務因為原生就本身支持不同屏幕大小的適配。FlutterViewController.view會在界面重排時重新觸發界面的重繪。從dart層布局層面代碼無需特別調整。

橫豎屏的切換時有不同的展示邏輯,正常監聽didChangeMetrics即可

  @override
  void didChangeMetrics() {
    setState(() { _lastSize = WidgetsBinding.instance.window.physicalSize; });
  }

引擎層技術改造

這是本次iPad適配中的重頭戲。因為閑魚中大部分核心基礎業務都是基于Flutter進行開發。讓多個Flutter界面順暢運行在iPad,繞不過的問題。

先回到閑魚的終端路由架構。

閑魚路由系統是基于flutter_boost進行搭建。而flutter_boost的原理則是多個界面共享同一個引擎,這一實現的有幾個好處

  • ? 優化內存,同一時刻Flutter引擎只提供給最前的視圖即可
  • ? 多個Flutter視圖因為運行在同一個Dart Isolate的緣故,數據能互相訪問(單例等)
  • ? 綁定到Isolate的Flutter插件、messagechannel都只有一份

但正是這些“優點”,同一時刻只能存在一個Flutter引擎,導致兩個Flutter視圖左右無法同屏。

那是否可以給每個視圖(ViewController/Activity)創建一個flutter引擎?這種方案也出現如下問題

業務問題

  • ? 每個Flutter引擎會創建兩份原生plugin實例,如原生plugin的實現是基于單例則會出現紊亂
  • ? 創建多個Flutter引擎增加非常多的性能開銷

性能問題

如不復用Flutter引擎,除此外還有其他場景也會導致內存激增

  • ? 混合棧中存在多個flutter界面
  • ? 原生tab中存在多個Flutter界面
  • ? 多個Flutter views同時存在,比如列表中每個cell都是Flutterview

那官方Flutter引擎是否類的方案?

Flutter官方在2020年也推出了輕量級多引擎的技術方案: http://flutter.dev/go/multiple-engines

輕量級多引擎方案使用的場景

  • ? 混合棧應用中多個flutter界面
  • ? 原生tab中存在多個Flutter界面
  • ? 多個Flutter views同時存在,比如列表中每個cell都是Flutterview

方案的初衷是在底層對某些如下的資源/類進行共享:

  1. 1. Threads Host(線程)
  2. 2. Skia Contexts (Skia繪制上下文)
  3. 3. Graphics Contexts (圖像繪制上下文本)
  4. 4. Image Caches (圖片緩存)
  5. 5. Isolate Groups (Dart的isolate)
  6. 6. Fonts (字體)

這個方案中需要注意點是,兩個引擎中的isolate是不共享的。

這種方案正因為各個界面/view中的因為不共享isolate的緣故。引擎之間的變量是無法共享的。隨之綁定到isolate的對象也存在有多份。

為能和原來閑魚的整個邏輯兼容,使業務平滑無感遷移,多引擎共享isolate方案勢在必行。

基于共享isolate的Flutter多引擎方案

修改后C++側代碼

引擎A和引擎B有各自的 shell , Engine ,Window 對象實例,且不同的引擎使用application_id 來標記。 他們之間共享isolate。

Application

這里我們稱創建出來的多個引擎稱為不同Application,每個引擎用不同ApplicationId標識。

不同Application使用自己的渲染管線,這樣就達到了不同引擎的渲染流程既互不影響。 但有因為運行在同一個isolate下,業務代碼自己的單例、數據等能互相訪問。

c++層支持后,在調用dart側的入口函數時,帶上application_id,傳遞到Dart側的入口main函數

bool DartIsolate::InvokeEntryPointInSharedIsolate(
   std::unique_ptr<PlatformConfiguration> platform_configuration,
   std::optional<std::string> library_name,
   std::optional<std::string> entrypoint,
   const std::vector<std::string>& args) {
 tonic::DartState::Scope scope(this);
 int64_t application_id = platform_configuration->application_id();

....

 if (!InvokeMainEntrypoint(user_entrypoint_function, entrypoint_args,
                           application_id)) {
   return false;
 }

 return true;
}

dart代碼單例問題

在原來Fltuter引擎framework層中存在多個單例如window,Bindings,PlatformDispatcher,如何解決不同Application訪問自己的單例?

使用Application.current獲取當前application后再訪問具體單例對象。以window為例

SingletonFlutterWindow get window => Application.current.get(
 SingletonFlutterWindow,
 () => SingletonFlutterWindow._(0, PlatformDispatcher.instance)
);

再dart層的渲染計算后,最終還會將渲染樹數據調用回到的c層。在回調到c函數時,也需要帶上applicationId

// platform_configuration.cc
void Render(Dart_NativeArguments args) {
   UIDartState::ThrowIfUIOperationsProhibited();
   Dart_Handle exception = nullptr;
  int64_t application_id =
      tonic::DartConverter<int>::FromArguments(args, 1, exception);
   Scene* scene =
     tonic::DartConverter<Scene*>::FromArguments(args, 2, exception);
   if (exception) {
     Dart_ThrowException(exception);
     return;
   }
  UIDartState::Current()
      ->platform_configuration(application_id)
      ->client()
      ->Render(scene);
 }

自此從flutter的Dart函數入口,到dart函數內部調用渲染流程,最后調用回到c++層,都有applicatinId去標識不同的引擎。

這樣就做到多個引擎之間既能渲染相互隔離,但內部又能訪問的結果。

總結

最后看下完成后的效果視頻截圖

橫豎屏切換

分屏導航模式

分屏比價模式

Flutter多引擎特性除了能應用到iPad場景外,還能應用到Android折疊屏場景。目前這部分的工作也在有序過程中。自定義NavigationViewController以及Flutter引擎的修改工作,也會在性能穩定后開源到社區共建。


https://mp.weixin.qq.com/s/lgW6nOzz3dyA_smxRD-9Pw

最多閱讀

如何有效定位Flutter內存問題? 2年以前  |  12971次閱讀
Flutter的手勢GestureDetector分析詳解 3年以前  |  9239次閱讀
Flutter插件詳解及其發布插件 3年以前  |  8156次閱讀
在Flutter中添加資源和圖片 4年以前  |  6312次閱讀
發布Flutter開發的iOS程序 4年以前  |  5482次閱讀
Flutter 狀態管理指南之 Provider 3年以前  |  5436次閱讀
Flutter for Web詳細介紹 3年以前  |  5236次閱讀
在Flutter中發起HTTP網絡請求 4年以前  |  4981次閱讀
使用Inspector檢查用戶界面 4年以前  |  4852次閱讀
Flutter路由詳解 3年以前  |  4427次閱讀
Flutter Widget框架概述 4年以前  |  3953次閱讀
為Flutter應用程序添加交互 4年以前  |  3918次閱讀
JSON和序列化 4年以前  |  3806次閱讀
推薦5個Flutter重磅開源項目! 2年以前  |  3725次閱讀
Flutter框架概覽 4年以前  |  3659次閱讀
處理文本輸入 4年以前  |  3575次閱讀
使用自定義字體 4年以前  |  3542次閱讀

手機掃碼閱讀
18禁止午夜福利体验区,人与动人物xxxx毛片人与狍,色男人窝网站聚色窝
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>