Flutter查漏補缺1

R1cardo發表於2022-03-27

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執行在哪個執行緒,只關心執行緒在整個生命週期內保持穩定。

相關文章