Flutter 中的非同步程式設計總結

Flutter程式設計開發發表於2019-09-12

Flutter 中的非同步程式設計總結

一、 Dart 中的事件迴圈模型

Dart 是一種單執行緒模型執行語言,其執行原理如下圖所示:

Flutter 中的非同步程式設計總結

Dart 在單執行緒中是以訊息迴圈機制來執行的,其中包含兩個任務佇列,一個是“微任務佇列” microtask queue,另一個叫做“事件佇列” event queue。 從圖中可以發現,微任務佇列的執行優先順序高於事件佇列。

現在我們來介紹一下Dart執行緒執行過程,如上圖中所示,入口函式 main() 執行完後,訊息迴圈機制便啟動了。 首先會按照先進先出的順序逐個執行微任務佇列中的任務,當所有微任務佇列執行完後便開始執行事件佇列中的任務,事件任務執行完畢後再去執行微任務, 如此迴圈往復,生生不息。

在Dart中,所有的外部事件任務都在事件佇列中,如IO、計時器、點選、以及繪製事件等,而微任務通常來源於Dart內部,並且微任務非常少, 之所以如此,是因為微任務佇列優先順序高,如果微任務太多,執行時間總和就越久,事件佇列任務的延遲也就越久, 對於GUI應用來說最直觀的表現就是比較卡,所以必須得保證微任務佇列不會太長。

在事件迴圈中,當某個任務發生異常並沒有被捕獲時,程式並不會退出,而直接導致的結果是當前任務的後續程式碼就不會被執行了, 也就是說一個任務中的異常是不會影響其它任務執行的。

我們可以看出,將任務加入到MicroTask中可以被儘快執行,但也需要注意,當事件迴圈在處理MicroTask佇列時,Event佇列會被卡住,應用程式無法處理滑鼠單擊、I/O訊息等等事件。 同時,當事件循壞出現異常時,dart 中也可以通過 try/catch/finally 來捕獲異常。

二、任務排程

基於上述理論,看一下任務排程

2.1 新增到 MicroTask 任務佇列 兩種方法:

import  'dart:async';
void  myTask(){
  print("this is my task");
}
void  main() {
  /// 1. 使用 scheduleMicrotask 方法新增
  scheduleMicrotask(myTask);
  /// 2. 使用Future物件新增
  new  Future.microtask(myTask);
}
複製程式碼

兩個 task 都會被執行,控制檯輸出:

this is my task
this is my task
複製程式碼

2.2 新增到 Event 佇列。

import  'dart:async';

void  myTask(){
  print("this is my event task");
}

void  main() {
  new  Future(myTask);
}
複製程式碼

控制檯輸出:

this is my event task
複製程式碼

示例:

import  'dart:async';

void  main() {
  print("main start");

  new  Future((){
    print("this is my task");
  });

  new  Future.microtask((){
    print("this is microtask");
  });

  print("main stop");
}

複製程式碼

控制檯輸出:

main start
main stop
this is microtask
this is my task
複製程式碼

可以看到,程式碼的執行順序並不是按照我們的編寫順序來的,將任務新增到佇列並不等於立刻執行,它們是非同步執行的,當前main方法中的程式碼執行完之後,才會去執行佇列中的任務,且MicroTask佇列執行在Event佇列之前。

2.3、延時任務

new  Future.delayed(new  Duration(seconds:1),(){
    print('task delayed');
});

複製程式碼

使用 Future.delayed 可以使用延時任務,但是延時任務不一定準

import  'dart:async';
import  'dart:io';

void  main() {
  var now = DateTime.now();

  print("程式執行開始時間:" + now.toString());

  new Future.delayed(new  Duration(seconds:1),(){
    now = DateTime.now();
    print('延時任務執行時間:' + now.toString());
  });

  new Future((){
    // 模擬耗時5秒
    sleep(Duration(seconds:5));
    now = DateTime.now();
    print("5s 延時任務執行時間:" + now.toString());
  });

  now = DateTime.now();
  print("程式結束執行時間:" + now.toString());
}

複製程式碼

輸出結果:

程式執行開始時間:2019-09-11 20:46:18.321738
程式結束執行時間:2019-09-11 20:46:18.329178
5s 延時任務執行時間:2019-09-11 20:46:23.330951
延時任務執行時間:2019-09-11 20:46:23.345323
複製程式碼

可以看到,5s 的延時任務,確實實在當前程式執行之後的 5s 後得到了執行,但是另一個延時任務,是在當前延時的基礎上又延時執行的, 也就是,延時任務必須等前面的耗時任務執行完,才得到執行,這將導致延時任務延時並不準。

三、Future 與 FutureBuilder

Dart類庫有非常多的返回Future或者Stream物件的函式。 這些函式被稱為非同步函式:它們只會在設定好一些耗時操作之後返回,比如像 IO操作。而不是等到這個操作完成。

Future與JavaScript中的Promise非常相似,表示一個非同步操作的最終完成(或失敗)及其結果值的表示。簡單來說,它就是用於處理非同步操作的,非同步處理成功了就執行成功的操作,非同步處理失敗了就捕獲錯誤或者停止後續操作。一個Future只會對應一個結果,要麼成功,要麼失敗。

由於本身功能較多,這裡我們只介紹其常用的API及特性。還有,請記住,Future 的所有API的返回值仍然是一個Future物件,所以可以很方便的進行鏈式呼叫。

3.1 Future 使用

建立方法: Future的幾種建立方法

Future() Future.microtask() Future.sync() Future.value() Future.delayed() Future.error()

Future 和 Future.microtask 和 Future.delayed 上面已經演示過了。 Future.sync() 表示同步方法,會被立即執行: 如:

import  'dart:async';

void  main() {
  print("main start");

  new  Future.sync((){
    print("sync task");
  });

  new  Future((){
    print("async task");
  });

  print("main stop");
}

複製程式碼

輸出:

main start
sync task
main stop
async task

複製程式碼

3.2 Future 中的回撥

當Future中的任務完成後,我們往往需要一個回撥,這個回撥立即執行,不會被新增到事件佇列。 如:

import 'dart:async';

void main() {
  print("main start");


  Future fut =new Future.value(18);
  // 使用then註冊回撥
  fut.then((res){
    print(res);
  });

  // 鏈式呼叫,可以跟多個then,註冊多個回撥
  new Future((){
    print("async task");
  }).then((res){
    print("async task complete");
  }).then((res){
    print("async task after");
  });

  print("main stop");
}

複製程式碼

輸出結果:

main start
main stop
18
async task
async task complete
async task after
複製程式碼

使用 catchError 捕獲異常:

import 'dart:async';

void main() {
  print("main start");

  Future.delayed(new Duration(seconds: 2),(){
    //return "hi world!";
    throw AssertionError("Error");
  }).then((data){
    //執行成功會走到這裡  
    print("success");
  }).catchError((e){
    //執行失敗會走到這裡
    print(e);
  });

  print("main stop");
}

複製程式碼

輸出:

main start
main stop
Assertion failed
複製程式碼

在本示例中,我們在非同步任務中丟擲了一個異常,then的回撥函式將不會被執行,取而代之的是 catchError回撥函式將被呼叫;但是,並不是只有 catchError回撥才能捕獲錯誤,then方法還有一個可選引數onError,我們也可以它來捕獲異常:

import 'dart:async';

void main() {
  print("main start2");

  Future.delayed(new Duration(seconds: 2), () {
    //return "hi world!";
    throw AssertionError("Error");
  }).then((data) {
    print("success");
  }, onError: (e) {
    print(e);
  });

  print("main stop2");
}

複製程式碼

結果:

main start2
main stop2
Assertion failed
複製程式碼

whenComplete 一定得到執行:

import 'dart:async';

void main() {
  print("main start3");
  Future.delayed(new Duration(seconds: 2),(){
    //return "hi world!";
    throw AssertionError("Error");
  }).then((data){
    //執行成功會走到這裡
    print(data);
  }).catchError((e){
    //執行失敗會走到這裡
    print(e);
  }).whenComplete((){
    //無論成功或失敗都會走到這裡
    print("this is the end...");
  });
  print("main stop3");
}

複製程式碼

結果:

main start3
main stop3
Assertion failed
this is the end...
複製程式碼

Future.wait 表示多個任務都完成之後的回撥。

import 'dart:async';

void main() {
  print("main start4");

  Future.wait([
    // 2秒後返回結果
    Future.delayed(new Duration(seconds: 2), () {
      return "hello";
    }),
    // 4秒後返回結果
    Future.delayed(new Duration(seconds: 4), () {
      return " world";
    })
  ]).then((results){
    print(results[0]+results[1]);
  }).catchError((e){
    print(e);
  });

  print("main stop4");
}

複製程式碼

結果:

main start4
main stop4
hello world
複製程式碼

3.3 FutureBuilder 的使用

很多時候我們會依賴一些非同步資料來動態更新UI,比如在開啟一個頁面時我們需要先從網際網路上獲取資料,在獲取資料的過程中我們顯式一個載入框,等獲取到資料時我們再渲染頁面;又比如我們想展示Stream(比如檔案流、網際網路資料接收流)的進度。當然,通過StatefulWidget我們完全可以實現上述這些功能。 但由於在實際開發中依賴非同步資料更新UI的這種場景非常常見,因此Flutter專門提供了FutureBuilder和StreamBuilder兩個元件來快速實現這種功能。

FutureBuilder會依賴一個Future,它會根據所依賴的Future的狀態來動態構建自身。我們看一下FutureBuilder建構函式:

FutureBuilder({
  this.future,
  this.initialData,
  @required this.builder,
})
複製程式碼
  • future:FutureBuilder依賴的Future,通常是一個非同步耗時任務。

  • initialData:初始資料,使用者設定預設資料。

  • builder:Widget構建器;該構建器會在Future執行的不同階段被多次呼叫,構建器簽名如下:

Function (BuildContext context, AsyncSnapshot snapshot)
複製程式碼

snapshot會包含當前非同步任務的狀態資訊及結果資訊 ,比如我們可以通過snapshot.connectionState獲取非同步任務的狀態資訊、通過snapshot.hasError判斷非同步任務是否有錯誤等等,完整的定義讀者可以檢視AsyncSnapshot類定義。

示例:

import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  Future<String> mockNetworkData() async {
    return Future.delayed(Duration(seconds: 2), () => "這是網路請求的資料。。。");
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: FutureBuilder<String>(
          future: mockNetworkData(),
          builder: (BuildContext context, AsyncSnapshot snapshot) {
            // 請求已結束
            if (snapshot.connectionState == ConnectionState.done) {
              if (snapshot.hasError) {
                // 請求失敗,顯示錯誤
                return Text("Error: ${snapshot.error}");
              } else {
                // 請求成功,顯示資料
                return Text("Contents: ${snapshot.data}");
              }
            } else {
              // 請求未結束,顯示loading
              return CircularProgressIndicator();
            }
          },
        ),
      ),
    // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

複製程式碼

效果:

Flutter 中的非同步程式設計總結

Flutter 中的非同步程式設計總結

四、async/await

在Dart1.9中加入了async和await關鍵字,有了這兩個關鍵字,我們可以更簡潔的編寫非同步程式碼,而不需要呼叫Future相關的API 將 async 關鍵字作為方法宣告的字尾時,具有如下意義

  • 被修飾的方法會將一個 Future 物件作為返回值
  • 該方法會同步執行其中的方法的程式碼直到第一個 await 關鍵字,然後它暫停該方法其他部分的執行;
  • 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行程式碼將立即執行。
// 匯入io庫,呼叫sleep函式
import 'dart:io';

// 模擬耗時操作,呼叫sleep函式睡眠2秒
doTask() async{
  await sleep(const Duration(seconds:2));
  return "Ok";
}

// 定義一個函式用於包裝
test() async {
  var r = await doTask();
  print(r);
}

void main(){
  print("main start");
  test();
  print("main end");
}
複製程式碼

結果:

main start
main end
Ok
複製程式碼

五、Stream 與 StreamBuilder

Stream 也是用於接收非同步事件資料,和Future 不同的是,它可以接收多個非同步操作的結果(成功或失敗)。 也就是說,在執行非同步任務時,可以通過多次觸發成功或失敗事件來傳遞結果資料或錯誤異常。 Stream 常用於會多次讀取資料的非同步任務場景,如網路內容下載、檔案讀寫等。

5.1 Stream 的使用

void main(){
  print("main start");
  Stream.fromFutures([
    // 1秒後返回結果
    Future.delayed(new Duration(seconds: 1), () {
      return "hello 1";
    }),
    // 丟擲一個異常
    Future.delayed(new Duration(seconds: 2),(){
      throw AssertionError("Error");
    }),
    // 3秒後返回結果
    Future.delayed(new Duration(seconds: 3), () {
      return "hello 3";
    })
  ]).listen((data){
    print(data);
  }, onError: (e){
    print(e.message);
  },onDone: (){

  });
  print("main end");
}

複製程式碼

結果:

main start
main end
hello 1
Error
hello 3
複製程式碼

5.1 StreamBuilder 的使用

我們知道,在Dart中Stream 也是用於接收非同步事件資料,和Future 不同的是,它可以接收多個非同步操作的結果,它常用於會多次讀取資料的非同步任務場景,如網路內容下載、檔案讀寫等。StreamBuilder正是用於配合Stream來展示流上事件(資料)變化的UI元件。 下面看一下StreamBuilder的預設建構函式:

StreamBuilder({
  Key key,
  this.initialData,
  Stream<T> stream,
  @required this.builder,
})
複製程式碼

可以看到和FutureBuilder的建構函式只有一點不同:前者需要一個future,而後者需要一個stream。

示例:

  Stream<int> counter() {
    return Stream.periodic(Duration(seconds: 1), (i) {
      return i;
    });
  }
複製程式碼
    Widget buildStream (BuildContext context) {
      return StreamBuilder<int>(
        stream: counter(), //
        //initialData: ,// a Stream<int> or null
        builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
          if (snapshot.hasError)
            return Text('Error: ${snapshot.error}');
          switch (snapshot.connectionState) {
            case ConnectionState.none:
              return Text('沒有Stream');
            case ConnectionState.waiting:
              return Text('等待資料...');
            case ConnectionState.active:
              return Text('active: ${snapshot.data}');
            case ConnectionState.done:
              return Text('Stream已關閉');
          }
          return null; // unreachable
        },
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body:Center(
        child:  buildStream(context),
      )
    // This trailing comma makes auto-formatting nicer for build methods.
    );
複製程式碼

效果:

Flutter 中的非同步程式設計總結

Flutter 中的非同步程式設計總結

六、isolate

Dart是基於單執行緒模型的語言。但是在開發當中我們經常會進行耗時操作比如網路請求,這種耗時操作會堵塞我們的程式碼,所以在Dart也有併發機制,名叫isolate。 APP的啟動入口main函式就是一個類似Android主執行緒的一個主isolate。和Java的Thread不同的是,Dart中的isolate無法共享記憶體,類似於Android中的多程式。

6.1 使用 spawnUri 建立 isolate

spawnUri方法有三個必須的引數,第一個是Uri,指定一個新Isolate程式碼檔案的路徑,第二個是引數列表,型別是List,第三個是動態訊息。 需要注意,用於執行新Isolate的程式碼檔案中,必須包含一個main函式,它是新Isolate的入口方法,該main函式中的args引數列表, 正對應spawnUri中的第二個引數。如不需要向新Isolate中傳引數,該引數可傳空List 主Isolate中的程式碼:

import 'dart:isolate';


void main() {
  print("main isolate start");
  create_isolate();
  print("main isolate stop");
}

// 建立一個新的 isolate
create_isolate() async{
  ReceivePort rp = new ReceivePort();
  ///建立傳送端
  SendPort port1 = rp.sendPort;
  ///建立新的 isolate ,並傳遞了傳送埠。
  Isolate newIsolate = await Isolate.spawnUri(new Uri(path: "./other_task.dart"), ["hello, isolate", "this is args"], port1);

  SendPort port2;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      port2 = message[1];
    }else{
      port2?.send([1,"這條資訊是 main isolate 傳送的"]);
    }
  });

  // 可以在適當的時候,呼叫以下方法殺死建立的 isolate
  // newIsolate.kill(priority: Isolate.immediate);
}

複製程式碼

在主 isolate 檔案的同級路徑下,新建一個 other_task.dart ,內容如下:

import 'dart:isolate';
import  'dart:io';


///接收到了傳送埠。
void main(args, SendPort port1) {
  print("isolate_1 start");
  print("isolate_1 args: $args");

  ReceivePort receivePort = new ReceivePort();
  SendPort port2 = receivePort.sendPort;

  receivePort.listen((message){
    print("isolate_1 message: $message");
  });

  // 將當前 isolate 中建立的SendPort傳送到主 isolate中用於通訊
  port1.send([0, port2]);
  // 模擬耗時5秒
  sleep(Duration(seconds:5));
  port1.send([1, "isolate_1 任務完成"]);
  print("isolate_1 stop");
}

複製程式碼

執行主 isolate,輸出結果:

main isolate start
main isolate stop
isolate_1 start
isolate_1 args: [hello, isolate, this is args]
main isolate message: [0, SendPort]
isolate_1 stop
main isolate message: [1, isolate_1 任務完成]
isolate_1 message: [1, 這條資訊是 main isolate 傳送的]
複製程式碼

isolate 之間的通訊時通過 ReceivePort 來完成的,ReceivePort 可以理解為一根水管,訊息的傳遞方向時固定的,把這個水管的傳送端傳送給對方, 對方就能通過這個水管傳送訊息。

6.2 通過 spawn 建立 isolate

除了使用spawnUri,更常用的是使用spawn方法來建立新的Isolate,我們通常希望將新建立的Isolate程式碼和main Isolate程式碼寫在同一個檔案, 且不希望出現兩個main函式,而是將指定的耗時函式執行在新的Isolate,這樣做有利於程式碼的組織和程式碼的複用。spawn方法有兩個必須的引數, 第一個是需要執行在新Isolate的耗時函式,第二個是動態訊息,該引數通常用於傳送主Isolate的SendPort物件。

示例:

import 'dart:isolate';
import  'dart:io';

void main() {
  print("主程式開始");
  create_isolate();
  print("主程式結束");
}

// 建立一個新的 isolate
create_isolate() async{
  ReceivePort rp = new ReceivePort();
  SendPort port1 = rp.sendPort;

  Isolate newIsolate = await Isolate.spawn(doWork, port1);

  SendPort port2;
  rp.listen((message){
    print("main isolate message: $message");
    if (message[0] == 0){
      port2 = message[1];
    }else{
      port2?.send([1,"這條資訊是 main isolate 傳送的"]);
    }
  });
}

// 處理耗時任務
void doWork(SendPort port1){
  print("new isolate start");
  ReceivePort rp2 = new ReceivePort();
  SendPort port2 = rp2.sendPort;

  rp2.listen((message){
    print("doWork message: $message");
  });

  // 將新isolate中建立的SendPort傳送到主isolate中用於通訊
  port1.send([0, port2]);
  // 模擬耗時5秒
  sleep(Duration(seconds:5));
  port1.send([1, "doWork 任務完成"]);

  print("new isolate end");
}

複製程式碼

結果:

主程式開始
主程式結束
new isolate start
main isolate message: [0, SendPort]
new isolate end
main isolate message: [1, doWork 任務完成]
doWork message: [1, 這條資訊是 main isolate 傳送的]
複製程式碼

參考:

juejin.im/post/5cdbf2… juejin.im/post/5c876e… book.flutterchina.club/chapter2/th… book.flutterchina.club/chapter7/fu… book.flutterchina.club/chapter1/da…

最後

歡迎關注「Flutter 程式設計開發」微信公眾號 。

Flutter 中的非同步程式設計總結

相關文章