[Flutter必備]-Dart中的非同步與檔案操作全面解析

張風捷特烈發表於2019-07-24

前面在Flutter之旅:Dart語法掃尾-包訪問-泛型--異常-非同步-mixin中向大家說過:
會有一篇專門介紹Dart中非同步的文章,現在如約而至,我將用精緻的圖文加上生動的例子向你闡述
各位,下面一起來看看吧。


1.同步

1.1:同步的演示

程式同步是按順序執行:一個任務執行完才能進入下一個任務,
就像下面的程式碼,掃地用了15分鐘,然後才能燒水,必須等水開了才能洗衣服。

[Flutter必備]-Dart中的非同步與檔案操作全面解析

main() {
  print("A任務: 掃地 15min");
  print("B任務: 燒水 25min");
  print("C任務: 洗衣服 25min");
}
複製程式碼

1.2:同步的劣勢

如果把一個人看作勞動力,那麼這樣執行會減少勞動力的利用率。
對於殘酷的剝削者而言,這樣的工作方式顯然是不能讓他滿意的:
完全可以先燒水,開火之後去掃地,掃完地倒垃圾,然後再洗衣服,
等到水開了,停下洗衣服的動作,衝完水再去洗衣服,這才是剝削者的思路

[Flutter必備]-Dart中的非同步與檔案操作全面解析


1.3:關於非同步

CPU就是那個勞動力,而程式設計師就是殘酷的剝削者。為了讓它能賣命的工作,就產生了非同步
當我們需要連線網路,讀取檔案,資料庫操作等耗時操作,就像在等水燒開
你肯定不想一個勞動力傻傻站那等水開吧,所以你要告訴它,現在去洗衣服,水開了再來沖水
於是就涉及到了一個問題,我怎麼知道誰燒開了呢?這是發生在未來的不確定時間點的事件
於是需要搞點東西來標識一下,就像水開了會嗚嗚響,不然的話,一直洗衣服,還不燒乾了?


2、從讀取檔案開始看非同步

2.1:關於Future物件

在讀取檔案的時候,通過File物件的readXXX方法,你會驚奇的發現:
沒有Sync字尾的方法名都是一個Future物件,它表明該操作返回的是一個未來的物件
在未來的物件,現在當然還拿不到,那怎麼用呢?可以看到Future有一個than方法

[Flutter必備]-Dart中的非同步與檔案操作全面解析

---->[sky_engine/lib/async/future.dart:601]----
Future<R> then<R>(FutureOr<R> onValue(T value), {Function onError});
複製程式碼

該方法上註釋如下: then方法用來註冊將來完成時要呼叫的回撥。
當這個future使用一個值完成時,將該值在[onValue]中回撥。
如果這個future已經完成,那麼回撥將不會立即呼叫,而是將在稍後的微任務中排程。
另外可以看到一個可選引數onError,當執行錯誤時會進行錯誤回撥


2.2:使用Future非同步讀取檔案

既然知道than中可以傳遞一個回撥來獲取檔案內容,那就簡單了
看下圖的結果,可以感受到讀取檔案是非同步的,檔案讀取的程式碼在上,執行時在下面
說明該程式在讀取檔案這個耗時操作時,先執行後面程式碼,讀取完成後才執行than的回撥

[Flutter必備]-Dart中的非同步與檔案操作全面解析

import 'dart:io';

main() {
  var path = '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/漫感.txt';
  Future<String> futureStr = File(path).readAsString();
  futureStr.then((value){
    print(value);
  });
  print("=======看看控制檯,我是第在哪裡?======");
}
複製程式碼

[Flutter必備]-Dart中的非同步與檔案操作全面解析


2.3:使用asyncawait非同步讀取檔案

給一個方法名加上async標註,就說明該方法是非同步方法,其中可以執行非同步操作
比如非同步讀取檔案,只需要在Future物件前加上await,即可獲取未來的值。

[Flutter必備]-Dart中的非同步與檔案操作全面解析

import 'dart:io';

main() {
  readByAsync();
  print("=======看看控制檯,我是第在哪裡?======");
}

readByAsync() async{
  var path = '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/漫感.txt';
  var result = await File(path).readAsString();
  print(result);
}
複製程式碼

[Flutter必備]-Dart中的非同步與檔案操作全面解析


2.4:同步讀取檔案

同步讀取就像等著燒開水,完成再去做別的事,讀取檔案接收才能執行下一行程式碼

[Flutter必備]-Dart中的非同步與檔案操作全面解析

main() {
  readBySync();
  print("=======看看控制檯,我是第在哪裡?======");
}

readBySync() {
  var path = '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/漫感.txt';
  var result = File(path).readAsStringSync();
  print(result);
}
複製程式碼

[Flutter必備]-Dart中的非同步與檔案操作全面解析


3.Dart中的Stream流

Stream流也不是什麼新鮮的玩意了,各大語言基本上都有流的操作,
這裡就Dart中的Stream流進行詳細的闡述。首先看Stream的幾個建立方法

factory Stream.empty() = _EmptyStream<T>//建立一個空的流
Stream.fromFuture(Future<T> future)//由一個Future物件建立
Stream.fromFutures(Iterable<Future<T>> futures)//由多個Future物件建立
Stream.fromIterable(Iterable<T> elements)//由可迭代物件建立
Stream.periodic(Duration period,[T computation(int computationCount)])//有周期的流
複製程式碼

3.1 : 最重要的一點!

我覺得Stream的認知中最重要的是區別它和列表有什麼不同,下面先親身體驗一下

  • 普通列表遍歷
var fishes = ["A", "B", "C"];
fishes.forEach((e){
  print(e);
});
print("====");
---->[列印結果]----
A
B
C
====
複製程式碼
  • 流遍歷
void byStream() {
  var fishes = ["A", "B", "C"];
  var stream =Stream.fromIterable(fishes);
  stream.forEach((e){
    print(e);
  });
  print("====");
}
---->[列印結果]----
====
A
B
C
複製程式碼

3.2:關於兩者的解釋

不知有心人是否看出兩者的區別:Stream在遍歷的時候居然是非同步的,這就是它和列表最大的不同
一個List在遍歷的那一刻,我就知道里面是什麼,有多少元素,可以怎麼操作它。
List就像後宮佳麗三千都在宮裡等你隨時操作,Stream則是後宮佳麗三千正在趕來的路上,你再急也沒辦法。
算了,換個例子,List就像魚缸,裡面盛著魚,你知道魚就在那,而且隨時可以拿出來吃了
Stream像一條小溪,你只是知道里面的魚在向你游來,在這一刻你不能撈出它們,
什麼時候游到你這裡也未知,對你而言它們都是你未來的財富。

[Flutter必備]-Dart中的非同步與檔案操作全面解析

[Flutter必備]-Dart中的非同步與檔案操作全面解析

  • 話說這樣有什麼用

現在,邪惡的我在魚遊動的過程中偷偷給A下毒,然後未來你拿到A後吃掉就傻傻的死掉
這就是Stream中的元素到達目的地之前,都可以進行控制和操作,我黑你幾條魚你也不知道。

[Flutter必備]-Dart中的非同步與檔案操作全面解析


3.3:訂閱:listen

也就是站在前面的你,在等待著魚過來。說明你訂閱了這個流中的元素。
在風平浪靜,沒人下毒的情況下,未來你一定能拿到河裡向你游來的這三條魚。

var fishes = ["A", "B", "C"];
var stream =Stream.fromIterable(fishes);
stream.listen((fish)=>print("拿到了$fish"));

---->[列印結果]----
拿到了A
拿到了B
拿到了C
複製程式碼
  • 訂閱的回撥
var fishes = ["A", "B", "C"];
var stream = Stream.fromIterable(fishes);
stream.listen((fish) => print("拿到了$fish"),
    onDone: () => print("已全部拿到"),//完成回撥
    onError: () => print("產生錯誤"),//錯誤回撥
    cancelOnError: false);//錯誤時是否取消訂閱
複製程式碼

3.4:訂閱的取消

一旦訂閱取消成功,onDone不會回撥,即使你已經拿到了最後一條魚
下面就說明你在拿到B後,你就取消訂閱,走人

var fishes = ["A", "B", "C"];
var stream = Stream.fromIterable(fishes);
var you = stream.listen(null);//你訂閱了這條小溪
you.onData((fish){//宣告魚到達你那裡你的行為
  print("拿到了$fish");
  if(fish=="B"){//拿到B後,你就取消訂閱,走人
    you.cancel();
  }
});
you.onError((e)=>print("產生錯誤$e"));
you.onDone(()=>print('已全部拿到'));
複製程式碼

3.5:Stream流中的元素新增

裡面就只有三條魚,你感覺很不爽,這時善良的管理員說,我現在就給你加
StreamController中有一個stream物件,可以通過它進行流的操作
由於是非同步的,可以在訂閱後繼續新增,也是不影響你對資料的獲取
就像你訂閱之後,管理員將魚放在水裡,魚也會游到你的面前。

[Flutter必備]-Dart中的非同步與檔案操作全面解析

StreamController controller = StreamController();
controller.add("A");
controller.add("B");
controller.add("C");

controller.stream.listen((fish) => print("拿到了$fish"));

controller.add("D");
controller.add("E");
controller.add("F");
controller.close();
複製程式碼

3.6:邪惡的我上線

邪惡的我來了,在中游截獲一條條魚。記住這幅圖,Stream流的思想就差不多了。

[Flutter必備]-Dart中的非同步與檔案操作全面解析

StreamController controller = StreamController();
controller.add("A");
controller.add("B");
controller.add("C");
controller.stream
    .map((fish) {//每條魚都從我面前遊過
      if (fish == "C") {
        print("我已經已經對C下毒");
        return "中毒的C";
      }
      if(fish=="D"){
        print("D已經被我吃完了");
        return "D的骨頭";
      }
      return fish;
    })
    .skip(2)//扔掉前兩個
    .take(2)//最終只能拿兩個
    .listen((fish) => print("傻傻的你拿到了$fish"));
controller.add("D");
controller.add("E");
controller.add("F");
controller.close();

---->[列印結果]----
我已經已經對C下毒
傻傻的你拿到了中毒的C
D已經被我吃完了
傻傻的你拿到了D的骨頭
複製程式碼

3.7、你的朋友也來了

當魚塘里加到B魚之後,你朋友和你站在一起,也訂閱了,這時候他只能監聽到之後新增的。
使用broadcast方法可以讓一個流被多人監聽,否則異常:Stream has already been listened to.

StreamController<String> controller = StreamController<String>.broadcast();
StreamSubscription you =
    controller.stream.listen((value) => print('監聽到 $value魚游到你身邊'));
controller.sink.add("A");
controller.sink.add("B");
StreamSubscription youFriend =
    controller.stream.listen((value) => print('監聽到 $value魚游到你朋友身邊'));
controller.sink.add("C");
controller.sink.add("D");
controller.close();
複製程式碼

4.Dart的檔案系統

在Dart中檔案的頂層為FileSystemEntity抽象類,其下有三個孩子:
File介面,Directory介面,Link介面,其中三個各有一個私有類分別繼承之

[Flutter必備]-Dart中的非同步與檔案操作全面解析

資料夾類Directory
---->[構造方法]----
Directory(String path)//從路徑
Directory.fromUri(Uri uri)//從uri
Directory.fromRawPath(Uint8List path)//從原生路徑

Uri get uri;
Directory get current;
Directory get absolute;

---->[非同步操作]----
Future<Directory> create({bool recursive: false});//建立資料夾
Future<Directory> createTemp([String prefix]);//建立臨時資料夾
Future<Directory> rename(String newPath);//重新命名
Stream<FileSystemEntity> list(//遍歷
    {bool recursive: false, bool followLinks: true});
    
---->[同步操作]----
void createSync({bool recursive: false});
Directory createTempSync([String prefix]);
Directory renameSync(String newPath);
Stream<FileSystemEntity> list(
    {bool recursive: false, bool followLinks: true});
複製程式碼
var dir=Directory(path);
print(dir.path);//Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data
print(Directory.current.path);//當前專案磁碟路徑:/Volumes/coder/Project/Flutter/flutter_journey
print(dir.absolute.path);//Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data
dir.createTemp("-");//隨機建立自定義字首的一個資料夾,

dir.list(recursive: true).forEach((e){
  print(e.path);
}).then((v){
  print("遍歷完畢");
});
print("----");//驗證list方法為非同步
複製程式碼
File基本操作的API
  • 檔案操作相關
---->[非同步操作]----
Future<File> create({bool recursive: false}); //非同步建立一個檔案(是否遞迴)
Future<File> rename(String newPath);//非同步重新命名檔案
Future<File> copy(String newPath);//非同步拷貝檔案到新路徑
Future<RandomAccessFile> open({FileMode mode: FileMode.read});//非同步開啟檔案

---->[同步操作]----
void createSync({bool recursive: false});//同步建立一個檔案(是否遞迴)
File renameSync(String newPath);//同步重新命名檔案
File copySync(String newPath);//同步拷貝檔案到新路徑
RandomAccessFile openSync({FileMode mode: FileMode.read});//同步開啟檔案
複製程式碼

不知簡寫成下面的樣子大家可不可以接受,這是Future物件的鏈式呼叫
我們可以看到create返回的仍是一個Future物件,也就是說then方法的回撥值仍是File物件
你就可以繼續呼叫相應的非同步方法再進行then,再回撥,再then,是不是很有趣。

var path =
    '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data/應龍.txt';
var pathCopy =
    '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data/應龍-copy.txt';
var pathRename =
    '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data/應龍-rename.txt';
var file = File(path);
file
    .create(recursive: true)
    .then((file) => file.copy(pathCopy)
    .then((file) => file.rename(pathRename)
    .then((file)=>print("建立,拷貝,重新命名完畢"))));
複製程式碼

  • 檔案資訊相關

這一組沒什麼好說的,顧名思義,需要的時候知道有這些API就行了

---->[非同步操作]----
Future<int> length();//非同步獲取檔案大小
Future<DateTime> lastAccessed();//非同步獲取最後訪問時間
Future setLastAccessed(DateTime time);//非同步設定最後訪問時間
Future<DateTime> lastModified();//非同步獲取最後修改時間
Future setLastModified(DateTime time);//非同步設定最後修改時間

---->[同步操作]----
int lengthSync();//同步獲取檔案大小
DateTime lastAccessedSync();//同步獲取最後訪問時間
void setLastAccessedSync(DateTime time);//同步設定最後訪問時間
DateTime lastModifiedSync();//同步獲取最後修改時間
void setLastModifiedSync(DateTime time);//非同步設定最後修改時間

File get absolute;//獲取絕對檔案
String get path;//獲取路徑
Directory get parent => new Directory(parentOf(path));//獲取父檔案
複製程式碼
  • 檔案讀寫相關

檔案的讀寫可謂是重中之重

IOSink openWrite({FileMode mode: FileMode.write, Encoding encoding: utf8});
---->[非同步寫操作]----
Future<File> writeAsBytes(List<int> bytes,
    {FileMode mode: FileMode.write, bool flush: false});
Future<File> writeAsString(String contents,
    {FileMode mode: FileMode.write,Encoding encoding: utf8,bool flush: false});

---->[同步寫操作]----
void writeAsBytesSync(List<int> bytes,
    {FileMode mode: FileMode.write, bool flush: false});
void writeAsStringSync(String contents,
    {FileMode mode: FileMode.write,Encoding encoding: utf8,bool flush: false});

Stream<List<int>> openRead([int start, int end]);
---->[非同步讀操作]----
Future<List<int>> readAsBytes();
Future<String> readAsString({Encoding encoding: utf8});
Future<List<String>> readAsLines({Encoding encoding: utf8});

---->[同步讀操作]----
List<int> readAsBytesSync();
String readAsStringSync({Encoding encoding: utf8});
List<String> readAsLinesSync({Encoding encoding: utf8});
複製程式碼

檔案的讀寫
  • openWrite方法

其一,它返回了一個IOSink物件;其二,它就收模式和編碼兩個入參
這裡測試了一下,它可以自動建立檔案並寫入字元,注意它並不能自動建立資料夾

[Flutter必備]-Dart中的非同步與檔案操作全面解析

var path =
      '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data/應龍-openWrite.txt';
var file=File(path);
file.openWrite().write("應龍");
複製程式碼

其中返回的IOSink物件有幾個方法可以對不同的的型別進行寫入,比如陣列
在寫入時可以自定義分隔符

var li=["Java","Dart","Kotlin","Swift"];
file.openWrite().writeAll(li,"¥¥");

---->[結果]----
Java¥¥Dart¥¥Kotlin¥¥Swift
複製程式碼

  • 關於讀入模式

預設情況下是FileMode.write,名稱寫入都會先將原來的內容清空,除此之外,還有:

FileMode.write//開啟可讀寫檔案,會覆蓋已有檔案
FileMode.append//開啟可讀寫檔案,往後追加
FileMode.writeOnly//開啟只寫檔案,會覆蓋已有檔案
FileMode.writeOnlyAppend//開啟只寫檔案,往後追加
複製程式碼

檔案的讀操作

openRead返回一個Stream<List>物件,它和Future比較像,有一個listen回撥方法
它可以回撥多個未來的物件的序列 ,你可以測試一下,它也是非同步的
這裡回撥出的是一個List,也就是對應的位元組在碼錶中的數值集合。

var path =
      '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data/應龍-openRead.txt';
file.openRead().listen((li) => li.forEach((e) => print(String.fromCharCode(e))));
複製程式碼

可以看到openRead方法中有兩個不定引數,可以控制讀取的起止點
至於為什麼這樣做:如果一個非常大的檔案通過readAsString,那麼會一次載入到記憶體中
如果記憶體不足就會崩掉,Stream就像是細水長流,一點一點進行讀取。

var path =
      '/Volumes/coder/Project/Flutter/flutter_journey/lib/day6/data/應龍-openRead.txt';
file.openRead().listen((li) => li.forEach((e) => print(String.fromCharCode(e))));
複製程式碼

另外的一些方法,使用上都大同小異,就不贅述了。


結語

本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。

相關文章