Flutter實戰之非同步相關

JulyYu發表於2020-04-02

前言

執行緒和非同步

平常開發中我們經常會用到非同步操作比如網路請求、IO等耗時操作,其目的是避免阻塞主執行緒導致卡頓以及後續事件消費。

通常情況下像Java和C++中處理這些耗時操作是通過開闢新執行緒,在子執行緒中處理耗時操作最後將結果返回給主執行緒。而像Dart實現非同步操作則是通過單執行緒+事件迴圈形式,每個事件在事件佇列中排隊執行並不阻塞執行緒執行最終結果以回撥方式返回。所以我們要知道不是多執行緒不代表不能非同步操作,兩者沒有必然關聯。

阻塞呼叫

阻塞呼叫就是“一心一意”完成每一件事情,比如早晨起床你先刷牙再洗臉最後吃飯,洗臉必須等刷完牙才能執行,吃飯也必須等待洗臉完成後再執行。

費阻塞呼叫

非阻塞呼叫則就是“三心二意”完成每件事,比如你做一道番茄牛腩面,可以先把牛腩焯水煮同時你可以去處理番茄並把麵條下鍋,然後牛腩焯水差不多再將牛腩盛出。每件事情可以同時進行,不一定要等待某件事情完成後再去做其他事,只需要關注每件事情的結果再做其他處理。

Isolates和事件迴圈

Dart雖然是單執行緒語言但支援非同步操作,因為Dart採用的是單執行緒+事件迴圈的工作模式處理非同步操作。

Isolates

Isolates是所有Dart程式碼執行的地方,在機器中擁有一小塊空間佔有私有記憶體塊單執行緒執行事件迴圈。不同於Java或是C++多執行緒共享同一記憶體,Dart每個Isolates都只有一個事件執行緒處理事務並相互隔離。

一般情況下一個Dart應用只有存在一個Isolates,當然你可以自己建立新Isolates去處理一些耗時操作,但兩個Isolates之間還是相互隔離無法直接訪問對方的任何資源。Isolates之間唯一互動方式是通過來回傳遞訊息主要也是因為Dart是單執行緒事件迴圈也不存線上程鎖等情況,若執行緒處在非繁忙情況就表示當前事件未發生變化。

事件迴圈

事件迴圈是Dart單執行緒實現非同步操作的關鍵。實際上是執行在一個永不阻塞單執行緒處理一個事件迴圈,程式會不停的從事件佇列中取出事件進行處理直到事件佇列清空為止。

Flutter實戰之非同步相關

Future

Future可以理解為盒子,在沒有開啟之前你是拿不到也看不到裡面會是什麼東西。當你需要知道里面是什麼時,執行then才會知道最終裡面放著啥。 如下所示為Future非同步操作延遲兩秒返回數值2的完整過程。

Future.delayed(Duration(seconds: 2), () {
  return 2;
}).then((result) {
   //正常執行返回值為 2
}).catchError((error) {
  //若發生異常情況跑出異常
}).whenComplete(() {
 //不管執行成功還是跑出異常最終都會執行到這裡
});
複製程式碼

另外像Future.value(12)同樣可以理解為“盒子”,其中存放的值我們無法直接獲取它,只能非同步執行也就是“拆盒子”的方式得到結果。

final myFuture = Future.value(12);
myFuture.then((value){
   _addLog("Hello World value $value");
 });
複製程式碼

async/await

在執行Future非同步操作屬於“非阻塞”,如下程式碼中執行非同步操作不會阻塞end日誌列印。

() {
  _removeLog();
  _addLog("start");
  Future.delayed(Duration(seconds: 2), () => 'Large Latte')
      .then((result) {
    _addLog(result);
  });
  _addLog("end");
}
複製程式碼

日誌列印如下

start
end
Large Latte
複製程式碼

同時Futrue非同步請求可採用await“等待”返回非同步結果,函式後面需要加上async表示為非同步形式,另外await寫法很好避免了地獄回撥發生。例如在做非同步操作後獲取結果拿去做另一個非同步操作,若非同步操作鏈路很長就發生套娃現象這樣的程式碼可讀性大大減弱,善用await線性執行方式會很香的。同時await非同步操作函式中程式碼執行過程中會出現“阻塞”,如下程式碼中在獲取last之後才執行end日誌,不過這隻對函式內部是“阻塞”而對於外部呼叫者來說該函式本身還是非同步操作。

前面的“等待”和“阻塞”是帶引號,採用await和async代表整個函式是以非同步形式執行並不會導致全域性程式碼執行過程中等待該函式,要注意await和async還是非同步操作。

() async {
    _removeLog();
    _addLog("start");
    String last = await Future.delayed(
        Duration(seconds: 2), () => 'Large Latte');
    _addLog(last);
    _addLog("end");
  },
)
複製程式碼

日誌列印如下

start
Large Latte
end
複製程式碼

需要注意await是呼叫,所以在處理異常情況和回撥方式有些不同,需要在外層巢狀try/catch避免Future非同步操作“阻塞”導致後續程式碼無法執行。

try {
  String last =
      await Future.delayed(Duration(seconds: 2), () {
    throw "throw error";
  }).whenComplete(() {
   
  });
} catch (e) {
  _addLog("catch $e");
}
複製程式碼

wait

等待多個非同步請求集合結果,例如下面程式碼中兩個非同步函式執行等待兩秒後統一將結果集合返回。最終結果返回需要等待最後一個非同步操作執行完後統一返回。

Future.wait([
  Future.delayed(Duration(seconds: 1), () => 1),
  Future.delayed(Duration(seconds: 2), () => 2),
]).then((values) {
  _addLog("values ${values[0]} ${values[1]}");
});
複製程式碼

delayed

Future的延遲操作中採用了Timer計時器,等待計時器結束後返回結果值。

factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
  _Future<T> result = new _Future<T>();
  new Timer(duration, () {
    if (computation == null) {
      result._complete(null);
    } else {
      try {
        result._complete(computation());
      } catch (e, s) {
        _completeWithErrorCallback(result, e, s);
      }
    }
  });
  return result;
}
複製程式碼

Stream

Stream和Future同樣是非同步操作,不同於Future非同步操作單個值形式,Stream代表著非同步運算元據流。不同於await/async非同步形式,Stream資料流準備以async*/yield形式展示。

Stream<int> countStream(int to) async* {
    for (int i = 1; i <= to; i++) {
      yield i;
    }
 }
() {
  countStream(10).listen((data) {
     ///結果返回
     throw "Error";
   }, onDone: () {
     ///相當於finally
   }, onError: () {
     ///丟擲異常的地方
   });
} 
複製程式碼

預設Stream只支援單個訂閱監聽,若需要支援多個訂閱監聽需要通過asBroadcastStream轉變為廣播訂閱。

() {
  final stream = countStream(10).asBroadcastStream();
  stream.listen(
    (data) => _addLog("Stream1 $data"),
  );
  stream.listen(
    (data) => _addLog("Stream2 $data"),
  );
},

複製程式碼

StreamController

StreamController是建立屬於你自己的Streams的高階用法。通過StreamController你可以很好的控制資料流進出操作。

/// 建立一個StreamController物件
class NumberCreator {
  ///初始化物件中Stream資料流
  NumberCreator() {
    for (int i = 0; i < 10; i++) {
      _controller.sink.add(_count);
      _count++;
    }
  }

  var _count = 1;
  ///建立StreamController流控制器
  StreamController<int> _controller = StreamController<int>();

  Stream<int> get stream => _controller.stream;
}
///(1)建立流控制器
numberCreator = NumberCreator();
///(2)建立流監聽監聽
subscriptionListen =
    numberCreator.stream.listen((value) {
});
///(3)向流控制器輸入資料
numberCreator._controller.add(100);
///暫停監聽
subscriptionListen.pause();
///重啟監聽
subscriptionListen.resume();
///移除流監聽
subscriptionListen.cancel();
複製程式碼
  • 監聽啟動後監聽器可獲取StreamController的資料流
  • StreamController可隨時push新資料
  • 監聽暫停後監聽器不會獲取StreamController資料流,同時StreamController新增資料將暫存
  • 但監聽器重新resume之後可獲取StreamController暫存未消費資料流

StreamBuilder

若是在UI元件直接使用StreamBuilder可以快速實現根據資料流展示不同UI內容。StreamBuilder元件內部設定內部監聽訂閱外部stream資料流。

void _subscribe() {
    if (widget.stream != null) {
      _subscription = widget.stream.listen((T data) {
        setState(() {
          _summary = widget.afterData(_summary, data);
        });
      }, onError: (Object error) {
        setState(() {
          _summary = widget.afterError(_summary, error);
        });
      }, onDone: () {
        setState(() {
          _summary = widget.afterDone(_summary);
        });
      });
      _summary = widget.afterConnected(_summary);
    }
 }
複製程式碼

通過stream傳遞資料在builder回撥獲取到snapshot資料在UI上做展示。

StreamBuilder<int>(
  builder: (context, data) {
    return Text(data.data.toString());
  },
  initialData: 10000,
  stream: numberCreator?.stream,
),
複製程式碼

?完整程式碼看這裡? ?完整程式碼看這裡?

參考

相關文章