Flutter 基礎知識查漏補缺
Hot reload原理
熱過載分為這幾個步驟
- 掃描專案改動:檢查是否有新增,刪除或者改動,直到找到上次編譯後發生改變的dart程式碼
- 增量編譯:找到改變的dart程式碼,將其轉化為增量支援動態編譯的dart kernel
- 推送更新:熱過載模組將增量更新的程式碼通過HTTP埠傳送到在虛擬機器上的Dart VM
- 程式碼合併:Dart Vm收到增量的dart kernel程式碼,將其與原有的dart vm程式碼合併,並載入新的dart kernel程式碼
- widget重建:在確認dart vm資源載入成功後,flutter會將Ui執行緒重置,通知flutter framework重建widget
Hot reload是debug下的JIT(Just In Time)動態編譯模式,dart程式碼會被編譯成可以在dart VM上的Dart kernel中間程式碼,而Dart kernel程式碼是支援動態更新的。
JIT由於包括了很多debug工具和中間層程式碼,所以效能沒有AOT(Ahead Of Time)編譯模式好,但是AOT編譯需要花費大量時間,適合release版本,JIT雖然效能沒有那麼好,但是支援動態編譯,所以適合debug模式
var、dynamic、final、const的區別
- var在建立時會推斷變數的型別,並且確定之後無法再更改,dynamic是動態型別,可以隨時更改型別,如下
//A value of type 'int' can't be assigned
to a variable of type 'String'.
var a = 'String';
a = 1;
//成功執行
dynamic b = 'String';
b = 12;
- final是執行時常量,型別會在執行時確定,且只能賦值一次,const是編譯時常量,型別在編譯時確定,無法被賦值
//The final variable 'b' can only
be set once.
final b = '';
b = '';
//Constant variables can't be
assigned a value.
const a = '';
a = '';
??和??=的區別
?? 是給A賦值給B,如果A為null,就將??後面的值賦給B
??= 是如果A為空就將??=後面的值賦值給A
String? A;
String B = A ?? 'B';
A ??= 'A';
Dart中是值傳遞還是引用傳遞
基本型別值傳遞,類是引用傳遞
Widget 、 Element 、Render Object三者之間的聯絡
簡單說一說,後面再繼續總結
- Widget是“描述一個UI元素的配置資訊”,並不是最終繪製在螢幕上的顯示元素,所謂的配置資訊就是Widget接收的引數,比如一個Text的配置資訊就是文字內容,對齊方式,文字樣式等。Widget有一些方法,比較重要的是@immutable,代表不可變的(final),因為Flutter中如果發生屬性改變機會重新構建Widget,會建立新的Widget例項替代老的Widget例項,所以Widget中的變數如果可變的話就沒有任何意義。還有canUpdate方法,其作用是是否用新的Widget物件去更新舊UI樹上Element物件的配置,如果新老Widget的runtimeType和key相同會返回true。Widget還有一個createElement方法來建立Element物件。
- 當新建一個Widget時,會建立一個Element物件,Element樹的節點都繼承自Element。
- 然後根據Element樹生成Render樹,也就是渲染樹,渲染樹的節點都繼承自RenderObject。
- 根據渲染樹生成Layer樹,Layer樹的節點都繼承自Layer類。
- 而真正的繪製,渲染邏輯都是由渲染樹完成。Element可以說是Widget和RenderObject的粘合劑。是Widget在整個UI樹中的例項,UI樹是由一個個獨立的Element節點構成。Widget ==> Element ==> RenderObject。Element樹根據Widget樹生成,Render樹又依賴於Element樹。我們一般不會直接操縱Element,Flutter框架已經將Widget樹對映到Element樹上,極大的降低複雜度,提高開發效率。
- Widget和Element是一一對應的,但Element並不會和RenderObject一一對應。只有需要渲染的Widget才有對應的RenderObject節點。Layer樹以後再總結。
總結
- 總結一下就是,Widget是我們通過程式碼建立的UI配置資訊,Flutter框架通過遍歷Widget樹來建立Element樹,Element樹又根據需要渲染的Widget來建立renderObject樹進行繪製和渲染等邏輯的操作。Widget負責管理配置資訊,render負責渲染,Element是一箇中間層,相當於一個大管家。當Widget配置資訊改變時,通過比較Widget和老Widget的key和runtimeType來確定Element和renderObject是否需要重建,不需要重建的直接更新Element的屬性就可以了,這樣可以以最小的開銷來更新renderObject,從而達到在Widget不斷變化時達到高效能的渲染。這裡面的知識點太多了,以後再來慢慢深究。
有關extends 、 implements 、 mixins
- 書寫順序是extends(繼承) ==> with(混入) ==> implements(實現)
abstract class
abstract class C {
///這是一個抽象方法因為沒有實現
c();
///這是一個抽象getter
int get type;
///這個方法不會被強制重寫因為他有實現
cc() {}
}
///繼承自抽象類的子類必須重寫父類的抽象方法
class D extends C {
@override
c() {}
@override
int get type => 1;
}
implements
- implements是對介面的實現,當一個類implements另一個類時,會被強制重寫其父類的方法。
class A {
void a() {}
}
class B implements A {
@override
void a() {
print('b a');
}
}
mixins
- mixins可以被關聯到另外一個class,為了重用程式碼但是又不用繼承,需要用with關鍵字
- 一個類可以擁有無數個mixins,一旦將mixins混入了一個類,這個類就持有所有mixins的方法
mixin Run {
void running() {}
void same() {
print('Run');
}
}
mixin Walk {
void walking() {}
void same() {
print('Walk');
}
}
mixin Talk {
void talking() {}
void same() {
print('Talk');
}
}
///現在Man擁有talk,walk,run,並且如果多個mixin有同名方法,取最後的實現
class Man with Run, Walk, Talk {}
void main() {
//列印talk
Man().same();
Man().running();
Man().walking();
Man().talking();
}
- mixins可以指定異常型別,用on關鍵字
class F {
f() {}
}
mixin E on F {}
///G類想要混入E時,本身必須是實現了F介面或者繼承於F或者繼承於實現了F的類才能混入E
class G extends F with E {
@override
f() {}
}
//實現了F的類
class Gimp implements F {
@override
f() {}
}
class Gext extends Gimp with E {}
extends
Dart中的繼承是單繼承,子類重寫父類的方法要用@override,不會強制繼承父類的方法,子類呼叫父類的方法要用super
class Parent {
work() {}
study() {}
}
class Child extends Parent {
@override
work() {
super.work();
super.study();
}
}
關於Dart單執行緒模型
Dart是單執行緒語言,所有的main函式中的程式碼都是在一個main isolate中完成的。我們一般的非同步操作,實際上是通過單執行緒非同步排程任務有優先順序完成的,也就是所謂的Future。為了保證較高的響應性,一般特別耗時的任務都會重開一個isolate來執行,執行完成之後通過isolate之間的通訊返回結果到main isolate中。
Dart事件機制
dart中有兩個任務佇列,Micotask queue和event queue,isolate中的程式碼是按順序執行的
-
首先執行main函式中的程式碼。
-
執行完main函式中程式碼後,會檢查並執行Microtask queue中的任務,通常使用scheduleMicrotask向Microtask queue新增任務。
-
最後執行Event queue佇列中的程式碼,通常使用Future向佇列中新增任務,或者使用async await方式新增。
-
總結:Main ==> Microtask queue ==> Event queue
Future的.then方法會將其中的程式碼放入Microtask佇列,在Future執行完畢後立即執行,因為Microtask佇列優先順序更高。
下面用一段程式碼來驗證執行順序
void main() {
print('main 1');
new Future(() => print('future 1'));
scheduleMicrotask(() => print('micro 1'));
new Future(() => print('future 2'));
scheduleMicrotask(() => print('micro 2'));
print('main 2');
}
//列印
main 1
main 2
micro 1
micro 2
future 1
future 2
Exited
Stream和Future
Stream和Future都是dart中用來處理非同步事件的,Future表示稍後處理一個事件。區別在於Future只能處理單個非同步事件,stream是處理一系列非同步事件流。Stream詳細在前幾篇博文中可以找到,這裡不再贅述。
需要補充的是,await可以等待當前非同步操作完成,await for就是等待當前非同步事件流(stream)完成,並可以通過yield返回每一個非同步事件的結果。
其實await並不會阻塞main函式中的程式碼,它具體的實現是,當dart執行到有await的地方時,將整個Future函式返回為一個Future物件,放入Event佇列中稍後非同步執行,而await後面的程式碼才會跟著一起執行完畢。這一切都是在main中的程式碼都執行完畢之後完成的。
await for 示例
awaitFor() async {
print('awaitFor begin');
await for (var item in Stream.fromIterable([1, 2, 3])) {
print(item);
}
print('awaitFor end');
}
//列印
awaitFor begin
1
2
3
awaitFor end
Exited
StatefulWidget的生命週期
initState
當此物件插入樹中時呼叫。
框架將為其建立的每個 [State] 物件呼叫此方法一次。
此時State物件還沒有和context繫結,通常拿來做一些初始化操作。比如事件監聽,channel初始化
didChangeDependencies
當此 [State] 物件的依賴項發生更改時呼叫。
例如,如果上一次呼叫 [build] 引用了後來更改的 [InheritedWidget],則框架將呼叫此方法以通知此物件有關更改的資訊。
此方法也會在 [initState] 之後立即呼叫。從此方法呼叫 [BuildContext.dependOnInheritedWidgetOfExactType] 是安全的。
在initState之後理解呼叫,此時已經和context繫結,可以拿來初始化一些和基於context的內容,當有依賴改變時,也會呼叫此方法通知更改資訊
didUpdateWidget
每當小元件配置更改時呼叫。
如果父小元件重新生成並請求更新樹中的此位置以顯示具有相同 [runtimeType] 和 [Widget.key] 的新小部件,則框架將更新此 [State] 物件的 [widget] 屬性以引用新小部件,然後使用以前的小部件作為引數呼叫此方法。
框架總是在呼叫[didUpdateWidget]之後呼叫[build],這意味著在[didUpdateWidget]中對[setState]的任何呼叫都是多餘的。
如果 [State] 的 [build] 方法依賴於本身可以更改狀態的物件,例如 [ChangeNotifier] 或 [Stream],或者可以訂閱以接收通知的其他物件,請確保在 [initState]、[didUpdateWidget] 和 [dispose] 中正確訂閱和取消訂閱
當父元件有改變時,此方法會呼叫,並且通過舊的小元件生成新的小元件,在呼叫此方法後會立即呼叫build方法,如果有stream或者ChangeNotifier,確保在didUpdateWidget方法中取消訂閱
build
描述此小元件所表示的使用者介面部分。
該框架在許多不同的情況下呼叫此方法。例如:
呼叫 [initState] 之後。
在呼叫 [didUpdateWidget] 之後。
在接到對 [setState] 的呼叫後。
在此 [State] 物件的依賴項發生更改(例如,先前的 [build] 更改所引用的 [InheritedWidget] 之後。
呼叫 [停用],然後將 [State] 物件重新插入到樹中的其他位置後。
構造介面方法,在一些其他時候會呼叫,比如didChangeDependencies,didUpdateWidget,setState等
deactivate
每當框架從樹中刪除此 [State] 物件時,它都會呼叫此方法。在某些情況下,框架會將 [State] 物件重新插入到樹的另一部分(例如,如果包含此 [State] 物件的子樹由於使用了 [GlobalKey] 而從樹中的一個位置移植到另一個位置)。
如果發生這種情況,框架將呼叫[啟用],以使[State]物件有機會重新獲取它在[停用]中釋放的任何資源。然後,它還將呼叫 [build],以使 [State] 物件有機會適應其在樹中的新位置。
當移出Widget tree時呼叫,如果框架將State物件再次插入Widget tree時,呼叫build方法
dispose
從樹中永久刪除此物件時呼叫。
當此 [State] 物件永遠不會再次生成時,框架將呼叫此方法。在框架呼叫 [dispose] 之後,[State] 物件被視為未裝載,並且 [mounted] 屬性為 false。此時呼叫 [setState] 是錯誤的。生命週期的此階段是終端階段:無法重新掛載已釋放的 [State] 物件。
物件被銷燬時呼叫,比如路由中的pop操作,此時可用來釋放資源,例如AnimationController,StreamController等,如果此物件由於某些延時操作導致在銷燬後呼叫setState,會丟擲異常,建議用if(mounted)判斷是否還在當前頁面
Key
Flutter中有LocalKey和GolbalKey兩種形式的key,key是用來指明widget身份的唯一識別符號
LocalKey
- ValueKey
value型別為文字,當有widget內容是恆定且不同的,可以用ValueKey來指定,不會產生混淆
- ObjectKey
如果說WIdget擁有更復雜的資料結構,比如一個使用者資訊的地址簿應用。任何單個欄位(如名字或生日)可能與另一個條目相同,但是每一個資料組合是唯一的,此時就更適合使用ObjectKey
- UniqueKey
如果集合中擁有多個相同值的Widget,或者想確保每個Widget和Widget都是不同的就可以使用UniqueKey
GlobalKeys
允許 Widget 在應用中的任何位置更改父級而不會丟失 State ,或者可以使用它們在 Widget 樹 的完全不同的部分中訪問有關另一個 Widget 的資訊。
程式碼示例
import 'package:flutter/material.dart';
class PageA extends StatefulWidget {
const PageA({Key? key}) : super(key: key);
@override
State<PageA> createState() => _PageAState();
}
class _PageAState extends State<PageA> {
//建立一個_PageBState型別的GlobalKey
final GlobalKey<_PageBState> akey = GlobalKey();
pagea() {}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageB(key: akey),
floatingActionButton: TextButton(
onPressed: () {
//可以通過此key的currentState呼叫_PageBState的pageb方法
akey.currentState!.pageb();
},
child: Text('text'),
),
);
}
}
class PageB extends StatefulWidget {
const PageB({Key? key}) : super(key: key);
@override
State<PageB> createState() => _PageBState();
}
class _PageBState extends State<PageB> {
//同理建立一個_PageAState型別的GlobalKey
final GlobalKey<_PageAState> bkey = GlobalKey();
pageb() {}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: TextButton(
onPressed: () {
//一樣可以通過此key的currentState呼叫_PageAState的pagea方法
bkey.currentState!.pagea();
},
child: Text('text'),
),
);
}
}
什麼是Navigator? MaterialApp做了什麼
Navigator是在Flutter中負責管理維護頁面堆疊的導航器。MaterialApp在需要的時候,會自動為我們建立Navigator。Navigator.of(context),會使用context來向上遍歷Element樹,找到MaterialApp提供的_NavigatorState再呼叫其push/pop方法完成導航操作。
全域性Error捕獲和處理
Flutter 框架可以捕獲執行期間的錯誤,包括構建期間、佈局期間和繪製期間。
所有 Flutter 的錯誤均會被回撥方法 FlutterError.onError 捕獲。預設情況下,會呼叫 FlutterError.dumpErrorToConsole 方法,正如方法名錶示的那樣,將錯誤轉儲到當前的裝置日誌中。當從 IDE 執行應用時,檢查器重寫了該方法,錯誤也被髮送到 IDE 的控制檯,可以在控制檯中檢查出錯的物件。
當構建期間發生錯誤時,回撥函式 ErrorWidget.builder 會被呼叫,來生成一個新的 widget,用來代替構建失敗的 widget。預設情況,debug 模式下會顯示一個紅色背景的錯誤頁面, release 模式下會展示一個灰色背景的空白頁面。
如果在呼叫堆疊上沒有 Flutter 回撥的情況下發生錯誤(這裡可以理解為FlutterError.onError僅僅可以捕獲主執行緒的錯誤,而其他非同步執行緒的錯誤則需要Zone來捕獲),它們由發生區域的 Zone 處理。 Zone 在預設情況下僅會列印錯誤,而不會執行其他任何操作。
這些回撥方法都可以被重寫,通常在 void main() 方法中重寫。
下面來看看如何處理。
捕獲Flutter錯誤
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.dumpErrorToConsole(details);
if (kReleaseMode) {
//處理線上錯誤,如統計上傳
}
};
runApp(MyApp());
}
上面我們重寫了FlutterError.onError,這樣就可以捕獲到錯誤,第一行程式碼就是將error展示到控制檯,這樣我開發時就會在控制檯很方便的看到錯誤。下面程式碼就是線上上環境下,對錯誤進一步處理,比如統計上傳。
自定義error widget
上面我們知道,構建時發生錯誤會預設展示一個錯誤頁面,但是這個頁面很不友好,我們可以自定義一個錯誤頁面。定義一個自定義的 error widget,以當 builder 構建 widget 失敗時顯示,請使用 MaterialApp.builder。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
builder: (context, child) {
Widget error = Text('rendering error');
if (child is Scaffold || child is Navigator) {
error = Scaffold(body: Center(child: error));
}
ErrorWidget.builder = (FlutterErrorDetails errorDetails) => error;
return error;
},
);
}
}
無法捕獲的錯誤
假設一個 onPressed 回撥呼叫了非同步方法,例如 MethodChannel.invokeMethod (或者其他 plugin 的方法)
如果 invokeMethod 丟擲了錯誤,它不會傳遞至 FlutterError.onError,而是直接進入 runApp 的 Zone。
如果你想捕獲這樣的錯誤,請使用 runZonedGuarded。程式碼如下
void main() {
runZonedGuarded(() {
runApp(MyApp());
}, (Object object, StackTrace stackTrace) {
//處理錯誤
});
}
請注意,如果你的應用在 runApp 中呼叫了 WidgetsFlutterBinding.ensureInitialized() 方法來進行一些初始化操作(例如 Firebase.initializeApp()),則必須在 runZonedGuarded 中呼叫 WidgetsFlutterBinding.ensureInitialized():
void main() {
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}, (Object object, StackTrace stackTrace) {
//處理錯誤
});
}
Flutter的執行緒管理模型
預設情況下,Flutter會建立一個主isolate,並且dart程式碼會預設在這個isolate中執行,必要時可以通過isolate.spawn或者solate.spawnUri來建立新的isolate(注:Flutter中不支援isolate.spawnUri),新建的isolate由Flutter統一管理。
事實上,Flutter並不會管理執行緒,執行緒的建立和管理是通過比Flutter引擎更底層的Embeder層負責的,Embeder層是將引擎移植在平臺的中間層程式碼,Flutter Engine層架構如下圖
Embeder層提供四個Task Runner,分別是platform task runner,UI task runner,GPU task runner,I/O task runner,Flutter Engine並不關心task runner執行在哪個執行緒,只關心執行緒在整個生命週期內保持穩定。