Flutter查漏補缺1

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運行在哪個線程,只關心線程在整個生命周期內保持穩定。

Tags: