Flutter完整開發實戰詳解(五、 深入探索)

戀貓de小郭發表於2018-10-15

作為系列文章的第五篇,本篇主要探索下 Flutter 中的一些有趣原理,幫助我們更好的去理解和開發。

文章彙總地址:

Flutter 完整實戰實戰系列文章專欄

Flutter 番外的世界系列文章專欄

1、Mixins

混入其中( ̄. ̄)!,是的,Flutter 使用的是 Dart 支援 Mixin ,而 Mixin 能夠更好的解決多繼承中容易出現的問題,如:方法優先順序混亂、引數衝突、類結構變得複雜化等等。

Mixin 的定義解釋起來會比較繞,我們直接程式碼從中出吧。如下程式碼所示,在 Dart 中 with 就是用於 mixins。可以看出,class G extends B with A, A2 ,在執行 G 的 a、b、c 方法後,輸出了 A2.a()、A.b() 、B.c() 。所以結論上簡單來說,就是相同方法被覆蓋了,並且 with 後面的會覆蓋前面的

class A {
  a() {
    print("A.a()");
  }

  b() {
    print("A.b()");
  }
}

class A2 {
  a() {
    print("A2.a()");
  }
}

class B {
  a() {
    print("B.a()");
  }

  b() {
    print("B.b()");
  }

  c() {
    print("B.c()");
  }
}

class G extends B with A, A2 {

}


testMixins() {
  G t = new G();
  t.a();
  t.b();
  t.c();
}

/// ***********************輸出***********************
///I/flutter (13627): A2.a()
///I/flutter (13627): A.b()
///I/flutter (13627): B.c()

複製程式碼

接下來我們繼續修改下程式碼。如下所示,我們定義了一個 Base 的抽象類,而A、A2、B 都繼承它,同時再 print 之後執行 super() 操作。

從最後的輸入我們可以看出,A、A2、B中的所有方法都被執行了,且只執行了一次,同時執行的順序也是和 with 的順序有關。如果你把下方程式碼中 class A.a() 方法的 super 去掉,那麼你將看不到 B.a()base a() 的輸出。

abstract class Base {
  a() {
    print("base a()");
  }

  b() {
    print("base b()");
  }

  c() {
    print("base c()");
  }
}

class A extends Base {
  a() {
    print("A.a()");
    super.a();
  }

  b() {
    print("A.b()");
    super.b();
  }
}

class A2 extends Base {
  a() {
    print("A2.a()");
    super.a();
  }
}

class B extends Base {
  a() {
    print("B.a()");
    super.a();
  }

  b() {
    print("B.b()");
    super.b();
  }

  c() {
    print("B.c()");
    super.c();
  }
}

class G extends B with A, A2 {

}

testMixins() {
  G t = new G();
  t.a();
  t.b();
  t.c();
}

///I/flutter (13627): A2.a()
///I/flutter (13627): A.a()
///I/flutter (13627): B.a()
///I/flutter (13627): base a()
///I/flutter (13627): A.b()
///I/flutter (13627): B.b()
///I/flutter (13627): base b()
///I/flutter (13627): B.c()
///I/flutter (13627): base c()

複製程式碼

2、WidgetsFlutterBinding

說了那麼多,那 Mixins 在 Flutter 中到底有什麼用呢?這時候我們就要看 Flutter 中的“膠水類”: WidgetsFlutterBinding

WidgetsFlutterBinding 在 Flutter啟動時runApp會被呼叫,作為App的入口,它肯定需要承擔各類的初始化以及功能配置,這種情況下,Mixins 的作用就體現出來了。

Flutter完整開發實戰詳解(五、 深入探索)

Flutter完整開發實戰詳解(五、 深入探索)

從上圖我們可以看出, WidgetsFlutterBinding 本身是並沒有什麼程式碼,主要是繼承了 BindingBase,而後通過 with 黏上去的各類 Binding,這些 Binding 也都繼承了 BindingBase

看出來了沒,這裡每個 Binding 都可以被單獨使用,也可以被“黏”到 WidgetsFlutterBinding 中使用,這樣做的效果,是不是比起一級一級繼承的結構更加清晰了?

最後我們列印下執行順序,如下圖所以,不出所料ヽ( ̄▽ ̄)ノ。

Flutter完整開發實戰詳解(五、 深入探索)

二、InheritedWidget

InheritedWidget 是一個抽象類,在 Flutter 中扮演者十分重要的角色,或者你並未直接使用過它,但是你肯定使用過和它相關的封裝。

Flutter完整開發實戰詳解(五、 深入探索)

如上圖所示,InheritedWidget 主要實現兩個方法:

  • 建立了 InheritedElement ,該 Element 屬於特殊 Element, 主要增加了將自身也新增到對映關係表 _inheritedWidgets【注1】,方便子孫 element 獲取;同時通過 notifyClients 方法來更新依賴。

  • 增加了 updateShouldNotify 方法,當方法返回 true 時,那麼依賴該 Widget 的例項就會更新。

所以我們可以簡單理解:InheritedWidget 通過 InheritedElement 實現了由下往上查詢的支援(因為自身新增到 _inheritedWidgets),同時具備更新其子孫的功能。

注1:每個 Element 都有一個 _inheritedWidgets ,它是一個 HashMap<Type, InheritedElement>,它儲存了上層節點中出現的 InheritedWidget 與其對應 element 的對映關係。

Flutter完整開發實戰詳解(五、 深入探索)

接著我們看 BuildContext,如上圖,BuildContext 其實只是介面, Element 實現了它。InheritedElementElement 的子類,所以每一個 InheritedElement 例項是一個 BuildContext 例項。同時我們日常使用中傳遞的 BuildContext 也都是一個 Element 。

所以當我們遇到需要共享 State 時,如果逐層傳遞 state 去實現共享會顯示過於麻煩,那麼瞭解了上面的 InheritedWidget 之後呢?

是否將需要共享的 State,都放在一個 InheritedWidget 中,然後在使用的 widget 中直接取用就可以呢?答案是肯定的!所以如下方這類程式碼:通常如 焦點、主題色、多語言、使用者資訊 等都屬於 App 內的全域性共享資料,他們都會通過 BuildContext(InheritedElement) 獲取。

///收起鍵盤
FocusScope.of(context).requestFocus(new FocusNode());

/// 主題色
Theme.of(context).primaryColor

/// 多語言
Localizations.of(context, GSYLocalizations)
 
/// 通過 Redux 獲取使用者資訊
StoreProvider.of(context).userInfo

/// 通過 Redux 獲取使用者資訊
StoreProvider.of(context).userInfo

/// 通過 Scope Model 獲取使用者資訊
ScopedModel.of<UserInfo>(context).userInfo

複製程式碼

綜上所述,我們從先 Theme 入手。

如下方程式碼所示,通過給 MaterialApp 設定主題資料,通過 Theme.of(context) 就可以獲取到主題資料並繫結使用。當 MaterialApp 的主題資料變化時,對應的 Widget 顏色也會發生變化,這是為什麼呢(キ`゚Д゚´)!!?

  ///新增主題
  new MaterialApp(
      theme: ThemeData.dark()
  );
  
  ///使用主題色
  new Container( color: Theme.of(context).primaryColor,
複製程式碼

通過原始碼一層層查詢,可以發現這樣的巢狀: MaterialApp -> AnimatedTheme -> Theme -> _InheritedTheme extends InheritedWidget ,所以通過 MaterialApp 作為入口,其實就是巢狀在 InheritedWidget 下。

Flutter完整開發實戰詳解(五、 深入探索)

如上圖所示,通過 Theme.of(context) 獲取到的主題資料,其實是通過 context.inheritFromWidgetOfExactType(_InheritedTheme) 去獲取的,而 Element 中實現了 BuildContextinheritFromWidgetOfExactType 方法,如下所示:

Flutter完整開發實戰詳解(五、 深入探索)

那麼,還記得上面說的 _inheritedWidgets 嗎?既然 InheritedElement 已經存在於 _inheritedWidgets 中,拿出來用就對了。

前文:InheritedWidget 內的 InheritedElement ,該 Element 屬於特殊 Element, 主要增加了將自身也新增到對映關係表 _inheritedWidgets

最後,如下圖所示,在 InheritedElement 中,notifyClients 通過 InheritedWidgetupdateShouldNotify 方法判斷是否更新,比如在 Theme_InheritedTheme 是:

bool updateShouldNotify(_InheritedTheme old) => theme.data != old.theme.data;
複製程式碼

Flutter完整開發實戰詳解(五、 深入探索)

所以本質上 Theme、Redux 、 Scope Model、Localizations 的核心都是 InheritedWidget

三、記憶體

最近閒魚技術釋出了 《Flutter之禪 記憶體優化篇》 ,文中對於 Flutter 的記憶體做了深度的探索,其中有一個很有趣的發現是:

  • Flutter 中 ImageCache 快取的是 ImageStream 物件,也就是快取的是一個非同步載入的圖片的物件。
  • 在圖片載入解碼完成之前,無法知道到底將要消耗多少記憶體。
  • 所以容易產生大量的IO操作,導致記憶體峰值過高。

圖片來自閒魚技術

如上圖所示,是圖片快取相關的流程,而目前的拮据處理是通過:

  • 在頁面不可見的時候沒必要發出多餘的圖片
  • 限制快取圖片的數量
  • 在適當的時候CG

更詳細的內容可以閱讀文章本體,這裡為什麼講到這個呢?是因為 限制快取圖片的數量 這一項。

還記得 WidgetsFlutterBinding 這個膠水類嗎?其中Mixins 了 PaintingBinding 如下圖所示,被"黏“上去的這個 binding 就是負責圖片快取

Flutter完整開發實戰詳解(五、 深入探索)

PaintingBinding 內有一個 ImageCache 物件,該物件全域性一個單例的,同時再圖片載入時的 ImageProvider 所使用,所以設定圖片快取大小如下:

//快取個數 100
PaintingBinding.instance.imageCache.maximumSize=100;
//快取大小 50m
PaintingBinding.instance.imageCache.maximumSizeBytes= 50 << 20;
複製程式碼

四、執行緒

在閒魚技術的 深入理解Flutter Platform Channel 中有講到:Flutter中有四大執行緒,Platform Task Runner 、UI Task Runner、GPU Task Runner 和 IO Task Runner。

其中 Platform Task Runner 也就是 Android 和 iOS 的主執行緒,而 UI Task Runner 就是Flutter的 UI 執行緒。

如下圖,如果做過 Flutter 中 Dart 和原生端通訊的應該知道,通過 Platform Channel 通訊的兩端就是 Platform Task RunnerUI Task Runner,這裡主要總結起來是:

  • 因為 Platform Task Runner 本來就是原生的主執行緒,所以儘量不要在 Platform 端執行耗時操作。

  • 因為Platform Channel並非是執行緒安全的,所以訊息處理結果回傳到Flutter端時,需要確保回撥函式是在Platform Thread(也就是Android和iOS的主執行緒)中執行的。

圖片來自閒魚技術

五、熱更新

逃不開的需求。

  • 1、首先我們知道 Flutter 依然是一個 iOS/Android 工程。

  • 2、Flutter通過在 BuildPhase 中新增 shell (xcode_backend.sh)來生成和嵌入App.frameworkFlutter.framework 到 IOS。

  • 3、Flutter通過 Gradle 引用 flutter.jar 和把編譯完成的二進位制檔案新增到 Android 中。

其中 Android 的編譯後二進位制檔案存在於 data/data/包名/app_flutter/flutter_assets/下。做過 Android 的應該知道,這個路徑下是可以很簡單更新的,所以你懂的  ̄ω ̄=。

⚠️注意,1.7.8 之後的版本,Android 下的 Flutter 已經編譯為純 so 檔案。

IOS?據我瞭解,貌似動態庫 framework 等引用是不能用熱更新的,除非你不需要稽核!

自此,第五篇終於結束了!(///▽///)

資源推薦

完整開源專案推薦:

Flutter完整開發實戰詳解(五、 深入探索)

相關文章