作為系列文章的第五篇,本篇主要探索下 Flutter 中的一些有趣原理,幫助我們更好的去理解和開發(fā)。
文章匯總地址:
1、Mixins
混入其中( ̄. ̄)!,是的,F(xiàn)lutter 使用的是 Dart 支持 Mixin ,而 Mixin 能夠更好的解決多繼承中容易出現(xiàn)的問題,如:方法優(yōu)先順序混亂、參數(shù)沖突、類結(jié)構(gòu)變得復(fù)雜化等等。
Mixin 的定義解釋起來會比較繞,我們直接代碼從中出吧。如下代碼所示,在 Dart 中 with 就是用于 mixins。可以看出,class G extends B with A, A2 ,在執(zhí)行 G 的 a、b、c 方法后,輸出了 A2.a()、A.b() 、B.c() 。所以結(jié)論上簡單來說,就是相同方法被覆蓋了,并且 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()
接下來我們繼續(xù)修改下代碼。如下所示,我們定義了一個 Base 的抽象類,而A、A2、B 都繼承它,同時再 print 之后執(zhí)行 super() 操作。
從最后的輸入我們可以看出,A、A2、B中的所有方法都被執(zhí)行了,且只執(zhí)行了一次,同時執(zhí)行的順序也是和 with 的順序有關(guān)。如果你把下方代碼中 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會被調(diào)用,作為App的入口,它肯定需要承擔(dān)各類的初始化以及功能配置,這種情況下,Mixins 的作用就體現(xiàn)出來了。
從上圖我們可以看出, WidgetsFlutterBinding 本身是并沒有什么代碼,主要是繼承了 BindingBase,而后通過 with 黏上去的各類 Binding,這些 Binding 也都繼承了 BindingBase。
看出來了沒,這里每個 Binding 都可以被單獨使用,也可以被“黏”到 WidgetsFlutterBinding 中使用,這樣做的效果,是不是比起一級一級繼承的結(jié)構(gòu)更加清晰了?
最后我們打印下執(zhí)行順序,如下圖所以,不出所料ヽ( ̄▽ ̄)?。
二、InheritedWidget
InheritedWidget 是一個抽象類,在 Flutter 中扮演者十分重要的角色,或者你并未直接使用過它,但是你肯定使用過和它相關(guān)的封裝。
如上圖所示,InheritedWidget 主要實現(xiàn)兩個方法:
創(chuàng)建了
InheritedElement,該 Element 屬于特殊 Element, 主要增加了將自身也添加到映射關(guān)系表_inheritedWidgets【注1】,方便子孫 element 獲??;同時通過notifyClients方法來更新依賴。增加了
updateShouldNotify方法,當(dāng)方法返回 true 時,那么依賴該 Widget 的實例就會更新。
所以我們可以簡單理解:InheritedWidget 通過 InheritedElement 實現(xiàn)了由下往上查找的支持(因為自身添加到 _inheritedWidgets),同時具備更新其子孫的功能。
注1:每個 Element 都有一個
_inheritedWidgets,它是一個HashMap<Type, InheritedElement>,它保存了上層節(jié)點中出現(xiàn)的 InheritedWidget 與其對應(yīng) element 的映射關(guān)系。
接著我們看 BuildContext,如上圖,BuildContext 其實只是接口, Element 實現(xiàn)了它。InheritedElement 是 Element 的子類,所以每一個 InheritedElement 實例是一個 BuildContext 實例。同時我們?nèi)粘J褂弥袀鬟f的 BuildContext 也都是一個 Element 。
所以當(dāng)我們遇到需要共享 State 時,如果逐層傳遞 state 去實現(xiàn)共享會顯示過于麻煩,那么了解了上面的 InheritedWidget 之后呢?
是否將需要共享的 State,都放在一個 InheritedWidget 中,然后在使用的 widget 中直接取用就可以呢?答案是肯定的!所以如下方這類代碼:通常如 焦點、主題色、多語言、用戶信息 等都屬于 App 內(nèi)的全局共享數(shù)據(jù),他們都會通過 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 設(shè)置主題數(shù)據(jù),通過 Theme.of(context) 就可以獲取到主題數(shù)據(jù)并綁定使用。當(dāng) MaterialApp 的主題數(shù)據(jù)變化時,對應(yīng)的 Widget 顏色也會發(fā)生變化,這是為什么呢(?`?Д?′)!!?
///添加主題
new MaterialApp(
theme: ThemeData.dark()
);
///使用主題色
new Container( color: Theme.of(context).primaryColor,
通過源碼一層層查找,可以發(fā)現(xiàn)這樣的嵌套: MaterialApp -> AnimatedTheme -> Theme -> _InheritedTheme extends InheritedWidget ,所以通過 MaterialApp 作為入口,其實就是嵌套在 InheritedWidget 下。
如上圖所示,通過 Theme.of(context) 獲取到的主題數(shù)據(jù),其實是通過 context.inheritFromWidgetOfExactType(_InheritedTheme) 去獲取的,而 Element 中實現(xiàn)了 BuildContext 的 inheritFromWidgetOfExactType 方法,如下所示:
那么,還記得上面說的 _inheritedWidgets 嗎?既然 InheritedElement 已經(jīng)存在于 _inheritedWidgets 中,拿出來用就對了。
前文:InheritedWidget 內(nèi)的
InheritedElement,該 Element 屬于特殊 Element, 主要增加了將自身也添加到映射關(guān)系表 _inheritedWidgets
最后,如下圖所示,在 InheritedElement 中,notifyClients 通過 InheritedWidget 的 updateShouldNotify 方法判斷是否更新,比如在 Theme的 _InheritedTheme 是:
bool updateShouldNotify(_InheritedTheme old) => theme.data != old.theme.data;
所以本質(zhì)上 Theme、Redux 、 Scope Model、Localizations 的核心都是
InheritedWidget。
三、內(nèi)存
最近閑魚技術(shù)發(fā)布了 《Flutter之禪 內(nèi)存優(yōu)化篇》 ,文中對于 Flutter 的內(nèi)存做了深度的探索,其中有一個很有趣的發(fā)現(xiàn)是:
- Flutter 中 ImageCache 緩存的是 ImageStream 對象,也就是緩存的是一個異步加載的圖片的對象。
- 在圖片加載解碼完成之前,無法知道到底將要消耗多少內(nèi)存。
- 所以容易產(chǎn)生大量的IO操作,導(dǎo)致內(nèi)存峰值過高。
如上圖所示,是圖片緩存相關(guān)的流程,而目前的拮據(jù)處理是通過:
- 在頁面不可見的時候沒必要發(fā)出多余的圖片
- 限制緩存圖片的數(shù)量
- 在適當(dāng)?shù)臅r候CG
更詳細(xì)的內(nèi)容可以閱讀文章本體,這里為什么講到這個呢?是因為 限制緩存圖片的數(shù)量 這一項。
還記得 WidgetsFlutterBinding 這個膠水類嗎?其中Mixins 了 PaintingBinding 如下圖所示,被"黏“上去的這個 binding 就是負(fù)責(zé)圖片緩存
在 PaintingBinding 內(nèi)有一個 ImageCache 對象,該對象全局一個單例的,同時再圖片加載時的 ImageProvider 所使用,所以設(shè)置圖片緩存大小如下:
//緩存?zhèn)€數(shù) 100
PaintingBinding.instance.imageCache.maximumSize=100;
//緩存大小 50m
PaintingBinding.instance.imageCache.maximumSizeBytes= 50 << 20;
四、線程
在閑魚技術(shù)的 深入理解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 和原生端通信的應(yīng)該知道,通過 Platform Channel 通信的兩端就是 Platform Task Runner 和 UI Task Runner,這里主要總結(jié)起來是:
因為 Platform Task Runner 本來就是原生的主線程,所以盡量不要在 Platform 端執(zhí)行耗時操作。
因為Platform Channel并非是線程安全的,所以消息處理結(jié)果回傳到Flutter端時,需要確?;卣{(diào)函數(shù)是在Platform Thread(也就是Android和iOS的主線程)中執(zhí)行的。
五、熱更新
逃不開的需求。
1、首先我們知道 Flutter 依然是一個 iOS/Android 工程。
2、Flutter通過在 BuildPhase 中添加 shell (xcode_backend.sh)來生成和嵌入App.framework 和 Flutter.framework 到 IOS。
3、Flutter通過 Gradle 引用 flutter.jar 和把編譯完成的二進(jìn)制文件添加到 Android 中。
其中 Android 的編譯后二進(jìn)制文件存在于 data/data/包名/app_flutter/flutter_assets/下。做過 Android 的應(yīng)該知道,這個路徑下是可以很簡單更新的,所以你懂的  ̄ω ̄=。
??注意,1.7.8 之后的版本,Android 下的 Flutter 已經(jīng)編譯為純 so 文件。
IOS?據(jù)我了解,貌似動態(tài)庫 framework 等引用是不能用熱更新的,除非你不需要審核!
自此,第五篇終于結(jié)束了!(///▽///)
資源推薦
- Github : https://github.com/CarGuo/
- 開源 Flutter 完整項目:https://github.com/CarGuo/GSYGithubAppFlutter
- 開源 Flutter 多案例學(xué)習(xí)型項目: https://github.com/CarGuo/GSYFlutterDemo
- 開源 Fluttre 實戰(zhàn)電子書項目:https://github.com/CarGuo/GSYFlutterBook