深入理解Flutter多線程

米米狗 2019-05-22 10:08:50 2325
本文來自 劉小壯 ,作者 米米狗

該文章屬于<簡書 — 劉小壯>原創,轉載請注明:

<簡書 — 劉小壯> https://www.jianshu.com/p/54da18ed1a9e


Flutter默認是單線程任務處理的,如果不開啟新的線程,任務默認在主線程中處理。

事件隊列

和iOS應用很像,在Dart的線程中也存在事件循環和消息隊列的概念,但在Dart中線程叫做isolate。應用程序啟動后,開始執行main函數并運行main isolate。

每個isolate包含一個事件循環以及兩個事件隊列,event loop事件循環,以及event queue和microtask queue事件隊列,event和microtask隊列有點類似iOS的source0和source1。

  • event queue:負責處理I/O事件、繪制事件、手勢事件、接收其他isolate消息等外部事件。

  • microtask queue:可以自己向isolate內部添加事件,事件的優先級比event queue高。

image.png

事件隊列

這兩個隊列也是有優先級的,當isolate開始執行后,會先處理microtask的事件,當microtask隊列中沒有事件后,才會處理event隊列中的事件,并按照這個順序反復執行。但需要注意的是,當執行microtask事件時,會阻塞event隊列的事件執行,這樣就會導致渲染、手勢響應等event事件響應延時。為了保證渲染和手勢響應,應該盡量將耗時操作放在event隊列中。

async、await

在異步調用中有三個關鍵詞,async、await、Future,其中async和await需要一起使用。在Dart中可以通過async和await進行異步操作,async表示開啟一個異步操作,也可以返回一個Future結果。如果沒有返回值,則默認返回一個返回值為null的Future。

async、await本質上就是Dart對異步操作的一個語法糖,可以減少異步調用的嵌套調用,并且由async修飾后返回一個Future,外界可以以鏈式調用的方式調用。這個語法是JS的ES7標準中推出的,Dart的設計和JS相同。

下面封裝了一個網絡請求的異步操作,并且將請求后的Response類型的Future返回給外界,外界可以通過await調用這個請求,并獲取返回數據。從代碼中可以看到,即便直接返回一個字符串,Dart也會對其進行包裝并成為一個Future。

Future<Response> dataReqeust() async {
    String requestURL = 'https://jsonplaceholder.typicode.com/posts';
    Client client = Client();
    Future<Response> response = client.get(requestURL);
    return response;
}

Future<String> loadData() async {
    Response response = await dataReqeust();
    return response.body;
}

在代碼示例中,執行到loadData方法時,會同步進入方法內部進行執行,當執行到await時就會停止async內部的執行,從而繼續執行外面的代碼。當await有返回后,會繼續從await的位置繼續執行。所以await的操作,不會影響后面代碼的執行。

下面是一個代碼示例,通過async開啟一個異步操作,通過await等待請求或其他操作的執行,并接收返回值。當數據發生改變時,調用setState方法并更新數據源,Flutter會更新對應的Widget節點視圖。

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];
  
  @override
  void initState() {
    super.initState();
    loadData();
  }
  
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

Future

Future就是延時操作的一個封裝,可以將異步任務封裝為Future對象?;袢〉紽uture對象后,最簡單的方法就是用await修飾,并等待返回結果繼續向下執行。正如上面async、await中講到的,使用await修飾時需要配合async一起使用。

在Dart中,和時間相關的操作基本都和Future有關,例如延時操作、異步操作等。下面是一個很簡單的延時操作,通過Future的delayed方法實現。

loadData() {
    // DateTime.now(),獲取當前時間
    DateTime now = DateTime.now();
    print('request begin $now');
    Future.delayed(Duration(seconds: 1), (){
      now = DateTime.now();
      print('request response $now');
    });
}

Dart還支持對Future的鏈式調用,通過追加一個或多個then方法來實現,這個特性非常實用。例如一個延時操作完成后,會調用then方法,并且可以傳遞一個參數給then。調用方式是鏈式調用,也就代表可以進行很多層的處理。這有點類似于iOS的RAC框架,鏈式調用進行信號處理。

Future.delayed(Duration(seconds: 1), (){
  int age = 18;
  return age;
}).then((onValue){
  onValue++;
  print('age $onValue');
});

協程

如果想要了解async、await的原理,就要先了解協程的概念,async、await本質上就是協程的一種語法糖。協程,也叫作coroutine,是一種比線程更小的單元。如果從單元大小來說,基本可以理解為進程->線程->協程。

任務調度

在弄懂協程之前,首先要明白并發和并行的概念,并發指的是由系統來管理多個IO的切換,并交由CPU去處理。并行指的是多核CPU在同一時間里執行多個任務。

并發的實現由非阻塞操作+事件通知來完成,事件通知也叫做“中斷”。操作過程分為兩種,一種是CPU對IO進行操作,在操作完成后發起中斷告訴IO操作完成。另一種是IO發起中斷,告訴CPU可以進行操作。

線程本質上也是依賴于中斷來進行調度的,線程還有一種叫做“阻塞式中斷”,就是在執行IO操作時將線程阻塞,等待執行完成后再繼續執行。但線程的消耗是很大的,并不適合大量并發操作的處理,而通過單線程并發可以進行大量并發操作。當多核CPU出現后,單個線程就無法很好的利用多核CPU的優勢了,所以又引入了線程池的概念,通過線程池來管理大量線程。

協程

在程序執行過程中,離開當前的調用位置有兩種方式,繼續調用其他函數和return返回離開當前函數。但是執行return時,當前函數在調用棧中的局部變量、形參等狀態則會被銷毀。

協程分為無線協程和有線協程,無線協程在離開當前調用位置時,會將當前變量放在堆區,當再次回到當前位置時,還會繼續從堆區中獲取到變量。所以,一般在執行當前函數時就會將變量直接分配到堆區,而async、await就屬于無線協程的一種。有線協程則會將變量繼續保存在棧區,在回到指針指向的離開位置時,會繼續從棧中取出調用。

async、await原理

以async、await為例,協程在執行時,執行到async則表示進入一個協程,會同步執行async的代碼塊。async的代碼塊本質上也相當于一個函數,并且有自己的上下文環境。當執行到await時,則表示有任務需要等待,CPU則去調度執行其他IO,也就是后面的代碼或其他協程代碼。過一段時間CPU就會輪訓一次,看某個協程是否任務已經處理完成,有返回結果可以被繼續執行,如果可以被繼續執行的話,則會沿著上次離開時指針指向的位置繼續執行,也就是await標志的位置。

由于并沒有開啟新的線程,只是進行IO中斷改變CPU調度,所以網絡請求這樣的異步操作可以使用async、await,但如果是執行大量耗時同步操作的話,應該使用isolate開辟新的線程去執行。

如果用協程和iOS的dispatch_async進行對比,可以發現二者是比較相似的。從結構定義來看,協程需要將當前await的代碼塊相關的變量進行存儲,dispatch_async也可以通過block來實現臨時變量的存儲能力。

我之前還在想一個問題,蘋果為什么不引入協程的特性呢?后來想了一下,await和dispatch_async都可以簡單理解為異步操作,OC的線程是基于Runloop實現的,Dart本質上也是有事件循環的,而且二者都有自己的事件隊列,只是隊列數量和分類不同。

我覺得當執行到await時,保存當前的上下文,并將當前位置標記為待處理任務,用一個指針指向當前位置,并將待處理任務放入當前isolate的隊列中。在每個事件循環時都去詢問這個任務,如果需要進行處理,就恢復上下文進行任務處理。

Promise

這里想提一下JS里的Promise語法,在iOS中會出現很多if判斷或者其他的嵌套調用,而Promise可以把之前橫向的嵌套調用,改成縱向鏈式調用。如果能把Promise引入到OC里,可以讓代碼看起來更簡潔,直觀。

isolate

isolate是Dart平臺對線程的實現方案,但和普通Thread不同的是,isolate擁有獨立的內存,isolate由線程和獨立內存構成。正是由于isolate線程之間的內存不共享,所以isolate線程之間并不存在資源搶奪的問題,所以也不需要鎖。

通過isolate可以很好的利用多核CPU,來進行大量耗時任務的處理。isolate線程之間的通信主要通過port來進行,這個port消息傳遞的過程是異步的。通過Dart源碼也可以看出,實例化一個isolate的過程包括,實例化isolate結構體、在堆中分配線程內存、配置port等過程。

isolate看起來其實和進程比較相似,之前請教阿里架構師宗心問題時,宗心也說過“isolate的整體模型我自己的理解其實更像進程,而async、await更像是線程”。如果對比一下isolate和進程的定義,會發現確實isolate很像是進程。

代碼示例

下面是一個isolate的例子,例子中新創建了一個isolate,并且綁定了一個方法進行網絡請求和數據解析的處理,并通過port將處理好的數據返回給調用方。

loadData() async {
    // 通過spawn新建一個isolate,并綁定靜態方法
    ReceivePort receivePort =ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);
    
    // 獲取新isolate的監聽port
    SendPort sendPort = await receivePort.first;
    // 調用sendReceive自定義方法
    List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
    print('dataList $dataList');
}

// isolate的綁定方法
static dataLoader(SendPort sendPort) async{
    // 創建監聽port,并將sendPort傳給外界用來調用
    ReceivePort receivePort =ReceivePort();
    sendPort.send(receivePort.sendPort);
    
    // 監聽外界調用
    await for (var msg in receivePort) {
      String requestURL =msg[0];
      SendPort callbackPort =msg[1];
    
      Client client = Client();
      Response response = await client.get(requestURL);
      List dataList = json.decode(response.body);
      // 回調返回值給調用者
      callbackPort.send(dataList);
    }    
}

// 創建自己的監聽port,并且向新isolate發送消息
Future sendReceive(SendPort sendPort, String url) {
    ReceivePort receivePort =ReceivePort();
    sendPort.send([url, receivePort.sendPort]);
    // 接收到返回值,返回給調用者
    return receivePort.first;
}

isolate和iOS中的線程還不太一樣,isolate的線程更偏底層。當生成一個isolate后,其內存是各自獨立的,相互之間并不能進行訪問。但isolate提供了基于port的消息機制,通過建立通信雙方的sendPort和receiveport,進行相互的消息傳遞,在Dart中叫做消息傳遞。

從上面例子中可以看出,在進行isolate消息傳遞的過程中,本質上就是進行port的傳遞。將port傳遞給其他isolate,其他isolate通過port拿到sendPort,向調用方發送消息來進行相互的消息傳遞。

Embedder

正如其名,Embedder是一個嵌入層,將Flutter嵌入到各個平臺上。Embedder負責范圍包括原生平臺插件、線程管理、事件循環等。

image.png

Flutter System Overriew

Embedder中存在四個Runner,四個Runner分別如下。其中每個Flutter Engine各自對應一個UI Runner、GPU Runner、IO Runner,但所有Engine共享一個Platform Runner。

image.png

Embedder

Runner和isolate并不是一碼事,彼此相互獨立。以iOS平臺為例,Runner的實現就是CFRunLoop,以一個事件循環的方式不斷處理任務。并且Runner不只處理Engine的任務,還有Native Plugin帶來的原生平臺的任務。而isolate則由Dart VM進行管理,和原生平臺線程并無關系。

Platform Runner

Platform Runner和iOS平臺的Main Thread非常相似,在Flutter中除耗時操作外,所有任務都應該放在Platform中,Flutter中的很多API并不是線程安全的,放在其他線程中可能會導致一些bug。

但例如IO之類的耗時操作,應該放在其他線程中完成,否則會影響Platform的正常執行,甚至于被watchdog干掉。但需要注意的是,由于Embedder Runner的機制,Platform被阻塞后并不會導致頁面卡頓。

不只是Flutter Engine的代碼在Platform中執行,Native Plugin的任務也會派發到Platform中執行。實際上,在原生側的代碼運行在Platform Runner中,而Flutter側的代碼運行在Root Isolate中,如果在Platform中執行耗時代碼,則會卡原生平臺的主線程。

UI Runner

UI Runner負責為Flutter Engine執行Root Isolate的代碼,除此之外,也處理來自Native Plugin的任務。Root Isolate為了處理自身事件,綁定了很多函數方法。程序啟動時,Flutter Engine會為Root綁定UI Runner的處理函數,使Root Isolate具備提交渲染幀的能力。

當Root Isolate向Engine提交一次渲染幀時,Engine會等待下次vsync,當下次vsync到來時,由Root Isolate對Widgets進行布局操作,并生成頁面的顯示信息的描述,并將信息交給Engine去處理。

由于對widgets進行layout并生成layer tree是UI Runner進行的,如果在UI Runner中進行大量耗時處理,會影響頁面的顯示,所以應該將耗時操作交給其他isolate處理,例如來自Native Plugin的事件。

image.png

Rendering Pipeline.jpg


GPU Runner

GPU Runner并不直接負責渲染操作,其負責GPU相關的管理和調度。當layer tree信息到來時,GPU Runner將其提交給指定的渲染平臺,渲染平臺是Skia配置的,不同平臺可能有不同的實現。

GPU Runner相對比較獨立,除了Embedder外其他線程均不可向其提交渲染信息。

image.png

Graphics Pipeline

IO Runner

一些GPU Runner中比較耗時的操作,就放在IO Runner中進行處理,例如圖片讀取、解壓、渲染等操作。但是只有GPU Runner才能對GPU提交渲染信息,為了保證IO Runner也具備這個能力,所以IO Runner會引用GPU Runner的context,這樣就具備向GPU提交渲染信息的能力。

11选五胆拖投注图表 六人牛牛什么方法 幸运飞艇一期稳赚10元 中巨奖的征兆 pk10一天稳赚几百块 pk10投注软件免费版 重庆时时五星全天计划 足球计算器胜平负玩法 竞彩足球比分即时比分 百人炸金花怎么制作 最热门捕鱼游戏排行榜 看四张牌抢庄牛牛技巧 兰斯 麻将胡牌牌型图解 抢庄斗牛看牌 欢聚棋牌代理