有道詞典Flutter架構與應用

有道技術團隊發表於2021-09-01

首圖.gif

聯絡我們有道技術團隊助手:ydtech01 / 郵箱ydtech@rd.netease.com

在 18 年 Flutter 釋出正式版 1.0 版本以來,有道 Luna 團隊保持持續的關注,在不少業務上進行大量的嘗試,Flutter 本身統一 Skia 引擎帶來的跨平臺特性和一致的體驗,AOT 下高效能,JIT 下熱過載帶來提高開發效率等特性,都讓人們保持極大的熱情和持續的投入,其生態社群也在快速增長。

從實際表現上來看,整個技術棧設計很好。上層 Flutter Framework 引入 Widget/LayerTree 等概念自己實現了介面描述框架,下層Flutter Engine 把 LayerTree 用 OpenGL 渲染成使用者介面。

長期來看,用 Flutter 來替代 Native ,實現雙端程式碼統一,節約人力開發,也是我們持續探索的方向。

一、前言

1.1 詞典業務嘗試

我們使用Flutter在有道詞典今年的6、7月份上線了單詞本和聽力模考業務,現在是flutter 1.12版本以下是業務展示

圖1.jpg 單詞本

圖2清晰版.jpg

聽力模考

我們在較為獨立的新業務上進行大膽嘗試,新技術難免會有問題,但是還是要勇於嘗試。

二、Flutter基礎簡介

2.1 Dart

  • Dart單執行緒模型

Dart 和 JavaScript 都是單執行緒模型,執行機制很相似,Dart 官方提供了一張執行原理圖:

圖3.png

Dart 在單執行緒中是以訊息迴圈機制來執行的,其中包含兩個任務佇列,一個是“微任務佇列” microtask queue,另一個叫做“事件佇列”** event queue**。從圖中可以發現,微任務佇列的執行優先順序高於事件佇列。

其中event queue:負責處理I/O事件、繪製事件、手勢事件、接收其他isolate訊息等外部事件;microtask queue:可以自己向isolate內部新增事件,事件的優先順序比event queue高。

事件佇列模型過程

  1. 先檢查 MicroTask 佇列是否為空,非空則先執行 MicroTask 佇列中的MicroTask
  2. 一個 MicroTask 執行完後,檢查有沒有下一個 MicroTask ,直到 MicroTask 佇列為空,才去執行 Event 佇列
  3. 在 Evnet 佇列取出一個事件處理完後,再次返回第一步,去檢查 MicroTask 佇列是否為空

在事件迴圈中,當某個任務發生異常並沒有被捕獲時,程式並不會退出,而直接導致的結果是當前任務的後續程式碼就不會被執行了,也就是說一個任務中的異常是不會影響其它任務執行的。

異常捕獲上傳至統計崩潰平臺也是應用這個模型,後面會講到。

2.2 Flutter Widget

介紹完 Dart,我們再看下 Flutter Widget。在 Flutter 中一切皆為 Widget,通過使用 Widget 可以實現頁面整體佈局、文字展示、圖片展示、手勢操作、事件響應等。

圖4.png

2.2.1 StatelessWidget

StatelessWidget 是一個沒有狀態的 widget ——沒有要管理的內部狀態。它通過構建一系列其他小部件來更加具體地描述使用者介面,從而描述使用者介面的一部分。當我們的頁面不依賴 Widget 物件本身中的配置資訊以及BuildContext 時,就可以用到無狀態元件。例如當我們只需要顯示一段文字時。實際上 Icon、Divider、Dialog、Text 等都是 StatelessWidget 的子類。

2.2.2 StatefulWidget

StatefulWidget 是可變狀態的 widget。使用 setState 方法管理 StatefulWidget 的狀態的改變。呼叫setState 通知 Flutter 框架某個狀態發生了變化,Flutter 會重新執行 build 方法,應用程式變可以顯示最新的狀態。

狀態是在構建 widget 的時候,widget 可以同步讀取的資訊,而這些狀態會發生變化。要確保在狀態改變的時候即使通知 widget 進行動態更改,就需要用到 StatefulWidget。例如一個計數器,我們點選按鈕就要讓數字加一。在 Flutter 中,Checkbox、FadeImage 等都是有狀態元件。

StatefulWidget的生命週期大致可分為三個階段

  • 初始化:插入渲染樹,這一階段涉及的生命週期函式主要有 createState、initState、didChangeDependencies 和 build。
  • 執行中:在渲染樹中存在,這一階段涉及的生命週期函式主要有 didUpdateWidge t和build。
  • 銷燬:從渲染樹中移除,此階段涉及的生命週期函式主要有 deactivate 和 dispose。

具體的宣告週期呼叫過程如下:

圖5.png

2.3 StatefulWidget 和 StatelessWidget 的實用場景

在 Flutter 中,元件和頁面資料變化是通過 State 驅動的,對於有互動的頁面或元件可以繼承 StatefulWidget,靜態元件或頁面可以繼承 StatelessWidget。StatelessWidget 沒有內部狀態,Icon、 IconButton 和 Text 都是無狀態 widget, 他們都是 StatelessWidget 的子類。StatefulWidget 是動態的. 使用者可以和其互動或者可以隨時間改變 (也許是資料改變導致的UI更新)。Checkbox、 Radio、Slider, InkWell、Form、TextField 都是 StatefulWidget, 他們都是 StatefulWidget 的子類。

 

使用 StatefulWidget 還是 StatelessWidget 的判斷依據

  • 如果使用者與widget互動,widget 會發生變化,那麼它就是有狀態的.
  • widget 的狀態(state)是一些可以更改的值, 如一個 slider 滑動條的當前值或 checkbox 是否被選中.
  • widget的狀態儲存在一個State物件中, 它和widget的佈局顯示分離。
  • 當widget狀態改變時, 呼叫 setState(), 告訴框架去重繪widget.

三、混合開發 - 整體框架

開發之初我們考慮兩個問題:

  • 場景1:a,b 兩個業務線,都要在 flutter 工程裡面開發業務?
  • 場景2:M app 有 flutter 工程,這個時候我們 N app 裡的 flutter 工程要嵌入到 M app 裡我們怎麼辦?

起初我們希望生成多個產物進行嵌入,通過 Flutter 的線下會議探討發現這個思路是比較後期的事情,但是也得到了另一個思路將我們的業務進行 “下沉” ,下沉到同一個工程裡面進行業務區分,引入元件化的概念進行實踐;

3.1 支援多團隊開發 

Flutter 工程中,通常有以下4種工程型別,下面分別簡單概述下:

1. Flutter Application:標準的 Flutter App 工程,包含標準的 Dart 層與 Native 平臺層

2. Flutter Module:Flutter 元件工程,僅包含 Dart 層實現,Native 平臺層子工程為通過 Flutter 自動生成的隱藏工程

3. Flutter Plugin:Flutter 平臺外掛工程,包含 Dart 層與 Native 平臺層的實現

4. Flutter Package:Flutter 純 Dart 外掛工程,僅包含 Dart 層的實現,往往定義一些公共 Widget

Flutter 工程之間的依賴管理是通過 Pub 來管理的,依賴的產物是直接原始碼依賴,這種依賴方式和 IOS 中的Pod有點像,都可以進行依賴庫版本號的區間限定與 Git 遠端依賴 path 等,其中具體宣告依賴是在 pubspec.yaml 檔案中,其中的依賴編寫是基於 YAML 語法, YAML 是一個專門用來編寫檔案配置的語言,下面是依賴示例:

vibration:
  git:
   url: https://github.com/YoudaoMobile/flutter_vibration.git
   ref: 'task/youdao'
複製程式碼
flutter_jsbridge_builder:
  path: ../../Common/flutter_jsbridge_builder
複製程式碼

所以,通過 Flutter Plugin / Flutter Package + Pub 達到解耦的目的

圖6.jpg

Flutter Plugin / Flutter Package為模組開發,原則上我們將工程分為殼工程,業務元件,基礎元件;依賴關係為殼工程->業務元件->基礎元件,不能依賴倒置,同層之間不能相互引用。

 

3.2 基礎元件沉澱

通過以元件化形式進行開發,通過各個團隊業務的不斷迭代,逐步沉澱出一套CommonUI的基礎Widget元件,方便其他業務和團隊擴充套件使用。

  • button工具:YDLabelButton,YDRaisedButton 

  • 字型工具:YDFontWeight,YDText,YDHtmlText

  • 遮罩工具:YDMaskView

  • loading工具:YDDefaultLoadingView,YDLoadingView

  • 圓角工具:CornerDecoration

  • 模態彈出工具:YDCupertinoModalPopupRoute

  • 點選彈窗工具:YDCoordinateTap

  • 幀序列動畫工具:YDSimpleFrameAniImage

  • YDRaisedButton,YDLabelButton是我們統一遵守有道UI準則自定義的一套button內容 YDText整合了英文螢幕取詞,以及解決中日韓同時展示在介面字型展示異常的問題 YDHtmlText我們整合了基於html標籤展示進行深層定製,來實現富文字的效果 YDSimpleFrameAniImage 幀動畫播放元件,解決了單純的圖片第一次迴圈播放會閃爍等問題的播放動畫元件 YDCupertinoModalPopupRoute 仿照新的ios模態彈出的效果,支援隨手滑動消失的互動方式 ....

  • 圖7.gif

3.3 超程式設計

我們在開發過程發現我們的bridge的內容大多數是相同的,只不過是形參,函式名不同罷了,所以我們打算引入source_gen,來生成bridge層的程式碼,這樣也帶來兩個好處,一是防止手誤,帶來的不必要的bug,二是將程式碼統一

source_gen主要提處理dart原始碼,可以通過註解生成程式碼。

大致的流程是通過 source_gen 一個 _Builder ,_Builder 需要生成器 Generator ,之後通過 Generator 去生成程式碼。

總結一下,在 Flutter 中應用註解以及生成程式碼僅需一下幾個步驟:

1.依賴

dev_dependencies:
 source_gen: ^0.9.0
複製程式碼

2.建立註解

class JSBridgeModule {
  final String moduleName;
 final List<String> enumTypeName;
 const JSBridgeModule({this.moduleName : "app", this.enumTypeName : const []});
}
 
複製程式碼

3.建立生成器

class JSBridgeImplGenerator
    extends GeneratorForAnnotation<JSBridgeModule> {
  JSBridgeImplGenerator() {}
  @override
 Iterable<String> generateForAnnotatedElement (
      Element element, ConstantReader annotation, BuildStep buildStep) {
    if (element is! ClassElement) {
      final name = element.name;
 throw InvalidGenerationSourceError('Generator cannot target `$name`.',
 return _generate(classElement, moduleName, enumTypeName: checkEnumTypeName);
 }
}
複製程式碼

4.建立Builder

Builder getJSBridgeImpGeneratorBuilder(BuilderOptions options) {
  return SharedPartBuilder();}
複製程式碼

5.編寫配置檔案

在專案根目錄建立 build.yaml 檔案,配置各項引數

builders:
  JSBridgeImpGeneratorBuilder:
    import: "package:flutter_jsbridge_builder/builder.dart"
 builder_factories: ["getJSBridgeImpGeneratorBuilder"]
    build_extensions: {".dart": ["flutter_jsbridge_builder.g.part"]}
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]
複製程式碼

這樣就為我們輸出一份模板程式碼提供了實現的可能

5.4 資源管理

眾所周知,flutter圖片等資源管理方面,還是處在手動管理階段,費時費力,所以推薦一款網易嚴選團隊開發的flr外掛,flr配合通過AndroidStudio外掛,將用於幫助Flutter開發者在修改專案資源後,可以自動為資源新增宣告到 pubspec.yaml 以及生成集中在一起的資源路徑檔案,Flutter開發者可以在程式碼中通過資源ID函式的方式應用資源。

通過建立起一個自動化的服務來監聽和管理資源變化,之後將變化的資源同步到 pubspec.yaml 和對應的資原始檔當中,也支援文字,字型資源,後續我們也和flr的團隊支援黑暗模式的計劃。

地址:github.com/Fly-Mix/flr…

3.5 異常捕獲上傳

在 flutter 簡介裡面我們介紹了 dart 的執行緒模型

事件佇列模型過程:

  1. 先檢查 MicroTask 佇列是否為空,非空則先執行 MicroTask 佇列中的 MicroTask
  2. 一個 MicroTask 執行完後,檢查有沒有下一個 MicroTask ,直到 MicroTask 佇列為空,才去執行 Event 佇列
  3. 在 Evnet 佇列取出一個事件處理完後,再次返回第一步,去檢查 MicroTask 佇列是否為空

在事件迴圈中,當某個任務發生異常並沒有被捕獲時,程式並不會退出,而直接導致的結果是當前任務的後續程式碼就不會被執行了,也就是說一個任務中的異常是不會影響其它任務執行的。

Flutter 框架為我們在很多關鍵的方法進行了異常捕獲。在發生異常時,錯誤是通過 FlutterError.reportError 方法上報的,其中 onError 是 FlutterError 的一個靜態屬性,我們重寫onError就可以捕獲異常了;但是還有一些非同步異常是需要我們通過 Zone 方法來捕獲的,整理程式碼如下:

圖8.png

四、產物介紹

4.1 介紹

1.12是個分水嶺,在這之前安卓打包方式有所不同,並且iOS官方也提供一些命令也來支援打不同的包

  • iOS產物組成

圖9.jpg

通過 flutter build ios --release 來得到產物,然後 flutter-plugins 裡面記錄各種 plugin 的位置 copy 過來,放在 .symlinks 資料夾下

app.framework:程式碼資料段+圖片 flutter.framework:engine+channel+... FlutterPluginRegistrant:原始碼,一些flutter自身的bridge podhelper.rd:通過flutter-plugins裡的bridge列表迴圈的將bridge填到pod中,在宿主工程通過pod引入

  • android產物組成 flutter1.0

圖10.png

進入 flutter 工程的 .android 資料夾執行 ./gradlew assembleRelease 就會打出一個 flutter-release.aar 的包,但是還有 path_provider,share_preference,audioplayer 等官方外掛我們也需要 copy出來,這裡我們發現 flutter 工程目錄下面有.flutter-plugins 這個檔案,這個檔案記錄著你當前 flutter所使用的官方外掛的檔案位置,我們通過 shell 讀取檔案位置,找到對應的 aar 集中到一起。

  • ** android flutter 1.12以後**

圖11.png

flutter 1.12打包執行flutter build  aar --no-debug –-no-profile來得到.

4.2 打包問題彙總

在flutter1.0版本進入android資料夾執行./gradlew assembleRelease會得到aar產物,但是此時的aar嵌入進去run起來會報錯,錯誤資訊是缺少一個.dat檔案,根據官方的issue和對原始碼的思考,討論結果是裡assets下面少一個flutter_assets檔案內容,事實上在io/flutter.jar可以看到,但是flutter還是會去assets資料夾下去找,導致嵌入Android失敗。解決辦法,從apk裡copy一份flutter_asset放到aar裡

在flutter 1.12版本官方提供aar產物命令,但是工程中引入官方庫(shared_preferences)的時候會執行命令失敗,原因是第三方會帶上macos和web的package,但是這個package不帶android檔案的內容,解決辦法:通過修改官方sdk對其android資料夾進行相容。

 

4.3 打包機問題彙總

在打包機配置完flutter環境,需要在Jenkins的節點配置將flutter path新增到PATH當中,否則flutter 命令執行失敗,以及ios 打包flutter build ios --release會因為code sign沒有許可權的問題失敗,儘量用flutter build ios --release --no-codesign來得到環境

 

五、遇到的問題

5. 1 ios端存在的問題

5.1.1 混合棧 boost出現的問題

首先感謝鹹魚團隊,提供了混合棧的一種方案,我們從flutter1.9升級到1.12過程中,遇到不少的問題和麻煩。

1.生命週期多次回撥

在1.9的版本中,ContainerLifeCycle.Appear 方法會回撥兩次,導致依賴生命週期操作重複,在 ios 這邊是在 viewdidappear 的時候會發通過 channel 發 didShowPageContainer 的訊息,呼叫 nativeContainerDidShow,然後在 TransitionBuilder 的方法再去呼叫一次.

onPageStart 然後再去呼叫 nativeContainerDidShow,就會導致兩次觸發,android 也是在 onAppear 的方法上重複上述的操作。

解決辦法就是去掉其中一個。

2.升級1.12之後,切前後臺的crash問題

這個問題版本有很多,得考慮業務場景,我們這裡是先模態出一個NavigationViewController,然後在這個NavigationViewController基礎上進行push和pop操作,然後我們在全域性提供一個回到模態之前ViewController的操作。在全域性回退的過程中,我們清掉了native的棧,然後在native的任意vc,切前後臺後crash。但是在1.9版本時並沒有發現此類問題。

crash的原因是在1.12的版本中 FlutterEngine自身加了surfaceUpdated的操作,當你整個退出後沒有正確的處理,導致FlutterEngine認為你的頁面上還存在著Flutter頁面,進行重新整理建立工作,就crash了。當然這個是我們這個業務場景總結出的crash的原因,據說還有其他版本crash問題,歡迎其他朋友補充。

解決辦法是在全域性回退的過程中迴圈呼叫close方法將棧裡的vc退出。

5.1.2 多語言顯示異常

當介面同時顯示在韓語/日語 與中文時,介面展示異常

官方issue:github.com/flutter/flu…

解決方式有三種:

  1. 增加字型 ttf ,全域性指定改字型顯示。

  2. TextStyle 屬性指定

fontFamilyFallback: ["PingFang SC", "Heiti SC"]
複製程式碼

可以封裝成一個widget

  1. 修改主題下所有 TextTheme 的 fontFamilyFallback
getThemeData() {
var themeData = ThemeData(
primarySwatch: primarySwatch
);

var result = themeData.copyWith(
textTheme: confirmTextTheme(themeData.textTheme),
accentTextTheme: confirmTextTheme(themeData.accentTextTheme),
primaryTextTheme: confirmTextTheme(themeData.primaryTextTheme),
);
return result;
}
/// 處理 ios 上,同頁面出現韓文和簡體中文,導致的顯示字型異常
confirmTextTheme(TextTheme textTheme) {
getCopyTextStyle(TextStyle textStyle) {
return textStyle.copyWith(fontFamilyFallback: ["PingFang SC", "Heiti SC"]);
}

return textTheme.copyWith(
display4: getCopyTextStyle(textTheme.display4),
display3: getCopyTextStyle(textTheme.display3),
display2: getCopyTextStyle(textTheme.display2),
display1: getCopyTextStyle(textTheme.display1),
headline: getCopyTextStyle(textTheme.headline),
title: getCopyTextStyle(textTheme.title),
subhead: getCopyTextStyle(textTheme.subhead),
body2: getCopyTextStyle(textTheme.body2),
body1: getCopyTextStyle(textTheme.body1),
caption: getCopyTextStyle(textTheme.caption),
button: getCopyTextStyle(textTheme.button),
subtitle: getCopyTextStyle(textTheme.subtitle),
overline: getCopyTextStyle(textTheme.overline),
);
}
複製程式碼

5.2 雙端存在的問題

flutter pub get失敗。Flutter 專案在引用第三庫時,在pub會選擇使用 git 引用,如:

flutter_boost:
  git: 
 url: 'https://github.com/YoudaoMobile/flutter_boost.git'
 ref: 'youdao_0.0.8'
複製程式碼

會報 pub get fail 的問題

在下載包的過程中出現問題,下次再拉包的時候,在.pub_cache內的 git 可能是空目錄,導致 flutter packages get 的時候異常。

所以你需要清除掉 .pub_cache 內的 git 的異常目錄或者執行flutter cache repair ,之後重新執行 flutter packages get 。

5.3 channel 通訊

Flutter定義了三種不同型別的 Channel,它們分別是

  • BasicMessageChannel:用於傳遞字串和半結構化的資訊。
  • MethodChannel:用於傳遞方法呼叫(method invocation)。
  • EventChannel: 用於資料流(event streams)的通訊。

其中channel有個很重要的變數codec;Codec官方定義了兩種Codec:MessageCodec和MethodCodec

其中MessageCodec有4種不同的種類:BinaryCodec;StringCodec;JSONMessageCodec;StandardMessageCodec

起初我們使用 MethodChannel 來建立通訊,但是使用過程中遇到大記憶體的傳遞耗時很長的問題,我們通過一系列的實驗和官方文件的指引,當需要傳遞大記憶體資料塊時,使用 BasicMessageChannel 以及 BinaryCodec 可以解決問題。

以下是實驗內容:

實驗機型:iphone 7 ios 13.7系統

實驗資料:開發者可以自行模擬1M-2M左右的資料進行測試,由於涉及到真實資料 這裡就不放出來了。

實驗結果:

實驗資料.png

從實驗結果上看,傳輸效率最優的是BinaryCodec,當然選擇什麼樣的code根據專案的需求來定才是最合理的

BinaryCodec>StringCodec>StandardMessageCodec>JSONMessageCodec

5.4 長列表優化

5.4.1 列表內容項長短不一

當我們實現類似上面的頁面,item 高度不一的思路肯定是類似以下的程式碼,但是當我們達到一定的數量級的情況下,發現記憶體佔用的十分嚴重,導致有些需求就實現不了,比如說支援滾動條快速定位;究其因CustomScrollView 初始化就載入了很多的 widget。

List<Widget> list = [];
for (int i = 0; i < 10000; i ++) {
  list.add(SliverToBoxAdapter(child: Container(height:30,color: Colors.red,),));
 list.add(SliverFixedExtentList(delegate: SliverChildBuilderDelegate((context,index){
    return Container(child: Text(index.toString()),);
 },childCount: 50), itemExtent: 50)
  );
}
CustomScrollView(slivers: list,);
複製程式碼

但是假如都是同樣的 itemExtent,滾動效率,記憶體表現都是良好的;因為事先告訴好高度,而不是依賴 widget自身的 layout 計算效率就高了很多, 比如說以下的程式碼:

List<Widget> list = [];

 list.add(SliverFixedExtentList(delegate: SliverChildBuilderDelegate((context,index){
    return Container(child: Text(index.toString()),);
 },childCount: 20000), itemExtent: 50)
  );
CustomScrollView(slivers: list,);
複製程式碼

如何兩者兼得呢?

我們決定自定義SliverFixedExtentList, SliverFixedExtentList返回的RenderObject是RenderSliverFixedExtentBoxAdaptor,我們將RenderSliverFixedExtentBoxAdaptor重新設計下。

RenderSliverFixedExtentBoxAdaptor原先設計的思路是通過scrolloffset除以itemExtent計算出當前的index(itemExtent是寫死的所以直接除),SliverConstraints可以拿到他的remainingCacheExtent也就是cacheExtent加上滾動可見區域,也就可以拿到lastIndex,在滾動的過程中不斷的釋放和建立。我們改寫的思路如下:

1.SliverFixedExtentList在createRenderObject和updateRenderObject的時候將每個元素的位置重新計算快取。

2.然後重寫performLayout方法,通過全域性的first和last索引拿到元素的位置和scrolloffset進行比較,得到新的first和last索引,不斷的調整。

3.將計算好的約束佈局傳入子佈局。

大致的思路就是這樣,介面層面我們設計成這個樣子,以下是呼叫示例

YDSliverFixedExtentList(
  delegate: SliverChildBuilderDelegate((context, int index) {
      if (index > wordList.length - 1){
        return Container(color: Colors.transparent,);
 }
      YDWBListBaseModel model = wordList[index];
 if (model is YDWBListHeaderModel) {
        return buildSusWidget(model.title);
 } else if (model is YDWBListItemModel){
        return buildListSingleItem(index, wordList[index], onMoveTap, onDeleteTap);
 } else{
        return Container();
 }
    },
 childCount: wordList.length + 1,
 ),
 itemHeightDelegate: (index){
    if (index > wordList.length - 1){
      return 60;
 }
    var model = wordList[index];
 if (model is YDWBListHeaderModel) {
      return kItemHeaderHeight;
 } else if (model is YDWBListItemModel) {
      return kItemHeight;
 } else {
      return 60;
 }
  },
 itemIndexDelegate: (startIndex, endIndex){
    firstIndex = startIndex;
 lastIndex = endIndex;
 },
複製程式碼

5.4.2 如何獲取當前展示列表索引

ios 開發都知道,我們的 TableView 是有代理來知道我們當前頁面展示的 Cell 的索引,但是在 Flutter 裡我們怎麼辦呢?

  • 思路1:給每個 item 加上 GlobalKey,然後放在 model 中,然後滾動的過程中利用去找迴圈遍歷model 的 GlobalKey,通過 GlobalKey 找到對應的 RenderObject,RenderObject 存在著位置座標等資訊,通過此資訊可以比較計算出 key 對應的 RenderObject 是否展示介面

程式碼如下:

double y=model.key.currentContext.findRenderObject().getTransformTo(null).getTranslation().y;
double height=model.key.currentContext.findRenderObject().paintBounds.size.height;
複製程式碼

然後找到對應繫結的model

  • 思路2:如果是實時獲取展示的索引可能上述思路不太合適,可能每次都需要在存放 model 裡的陣列去找,當然也可以在思路1的基礎上進行演算法優化,暫存當前展示的 index,來做下次起始尋找的index,減少迴圈次數。

不過,接下來介紹的是,另一種辦法,改寫 SliverChildBuilderDelegate,在SliverChildBuilderDelegate裡面的didFinishLayout裡會返回它的firstIndex和lastIndex,但是要注意此時返回的是加了cacheExtent的firstIndex,所以可能比實際展示的要小,所以可以結合思路一進行精確定位

class MySliverChildBuilderDelegate extends SliverChildBuilderDelegate {
  final int markIndex;
 MySliverChildBuilderDelegate(
      this.markIndex,
 Widget Function(BuildContext, int) builder, {
        int childCount,
 bool addAutomaticKeepAlives = true,
 bool addRepaintBoundaries = true,
 }) : super(builder,
 childCount: childCount,
 addAutomaticKeepAlives: addAutomaticKeepAlives,
 addRepaintBoundaries: addRepaintBoundaries,
 );

 @override
 void didFinishLayout(int firstIndex, int lastIndex) {
    debugPrint('pre' + 'didFinishLayout firstIndex: $firstIndex, lastIndex: $lastIndex' + "markIndex" + markIndex.toString());
 if (firstIndex == 0 && lastIndex == 0) {
      return;
 }
    YDBaseListEvent.notifyIndexChange({"firstIndex": markIndex + firstIndex, "lastIndex": markIndex + lastIndex});
 debugPrint(mark + 'didFinishLayout firstIndex: $firstIndex, lastIndex: $lastIndex' + "markIndex" + markIndex.toString());
 }
}
複製程式碼
  • 思路3: 可以參考長列表優化,將SliverFixedExtentList改寫暴露對應返回index的介面

 

六、後續計劃  

我們後續計劃升級到 flutter2.0,但是目前來看 2.0 還存在問題,如果 2.0 真的徹底的解決多引擎複用的問題,我們也會嘗試去除 boost 的管理機制,根據flutter.cn/posts/flutt…,在多個引擎複用的視訊章節,於瀟分析了多引擎複用的記憶體增長的問題,主要在

  • 執行緒
  • GPU資源
  • Skia Context
  • 字形
  • Dart Isolate

這五部分,每起一個 engine 之後就會起 3 個新的作業系統的執行緒,每個執行緒都是有成本的尤其是在ios上,在2.0版本上都合併在一起了;另一部分 GPU 資源,Skia Context 就包含了 opengl 的 context,metal context,metal buffer,shader program,skia program,為了提高使用啟動將 GPU 資源和 Skia Context的內容做共享;字形的大小都會有緩衝的,假如不加以利用的話也造成一定的浪費;dart Isolate 事實上每次建立 engine 都會重新建立一個,2.0 版本也做了一個共享。

結果也是比較客觀,優化的效果比較明顯,10 次的啟動不升反降,40M 變成了35M。有興趣的可以試下,github.com/flutter/sam…,但是對於 flutter 團隊來說 2.0 版本只是解決了記憶體問題,還存在其他的問題,主要是以下幾方面:

  • 只支援AOT,不支援debug
  • ios IOSurface解除安裝,fluttervc沒有解除但是又被覆蓋了的話,它的metal layer沒有釋放IOSurface,會影響混合棧的場景,目前的辦法是覆蓋的時候需要手動把flutterview 去掉
  • 不支援資料共享
  • 不支援記憶體共享
  • platform View 不支援
  • 只支援一個snapshot

七、結束語

我們在單詞本和聽力等模組進行 flutter 落地的探索,在前期實踐過程中,碰到了很多問題,但總體來說還處於可控的狀態;前期把各種困難都解決後,後面業務再此基礎上進行開發會順暢很多,效率會提升很多,這個也是 flutter 期望帶給我們的一次開發,多端執行。但是另一方面希望開發者們在落地過程中,更為慎重些,多多實踐,提前發現提前解決,畢竟存在處理不好的情況,還需要推動官方或者生態提供更好的解決辦法。

未來期望 flutter 以及社群在平臺一致性以及混合棧,記憶體,鍵盤,音視訊等具體問題上持續發力,我們也會進一步的探索 flutter 在業務上更多實現的可能。感謝觀看。

以上內容僅代表個人觀點,如果內容或者實驗資料存在疑問和問題,歡迎大家批評指正,一起學習,一起成長。

本內容僅代表個人觀點,不代表網易,僅供內部分享傳播,不允許以任何形式外洩,否則追究法律責任。

相關文章