深刻理解Flutter多線程

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

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


封面圖

Flutter默認是單線程任務處理的,若是不開啓新的線程,任務默認在主線程中處理。github

事件隊列

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

每一個isolate包含一個事件循環以及兩個事件隊列,event loop事件循環,以及event queuemicrotask queue事件隊列,eventmicrotask隊列有點相似iOS的source0source1json

  • event queue:負責處理I/O事件、繪製事件、手勢事件、接收其餘isolate消息等外部事件。
  • microtask queue:能夠本身向isolate內部添加事件,事件的優先級比event queue高。

事件隊列

這兩個隊列也是有優先級的,當isolate開始執行後,會先處理microtask的事件,當microtask隊列中沒有事件後,纔會處理event隊列中的事件,並按照這個順序反覆執行。但須要注意的是,當執行microtask事件時,會阻塞event隊列的事件執行,這樣就會致使渲染、手勢響應等event事件響應延時。爲了保證渲染和手勢響應,應該儘可能將耗時操做放在event隊列中。安全

async、await

在異步調用中有三個關鍵詞,asyncawaitFuture,其中asyncawait須要一塊兒使用。在Dart中能夠經過asyncawait進行異步操做,async表示開啓一個異步操做,也能夠返回一個Future結果。若是沒有返回值,則默認返回一個返回值爲nullFuture網絡

asyncawait本質上就是Dart對異步操做的一個語法糖,能夠減小異步調用的嵌套調用,而且由async修飾後返回一個Future,外界能夠以鏈式調用的方式調用。這個語法是JSES7標準中推出的,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對象。獲取到Future對象後,最簡單的方法就是用await修飾,並等待返回結果繼續向下執行。正如上面async、await中講到的,使用await修飾時須要配合async一塊兒使用。

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

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');
});
複製代碼

協程

若是想要了解asyncawait的原理,就要先了解協程的概念,asyncawait本質上就是協程的一種語法糖。協程,也叫做coroutine,是一種比線程更小的單元。若是從單元大小來講,基本能夠理解爲進程->線程->協程。

任務調度

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

併發的實現由非阻塞操做+事件通知來完成,事件通知也叫作「中斷」。操做過程分爲兩種,一種是CPU對IO進行操做,在操做完成後發起中斷告訴IO操做完成。另外一種是IO發起中斷,告訴CPU能夠進行操做。

線程本質上也是依賴於中斷來進行調度的,線程還有一種叫作「阻塞式中斷」,就是在執行IO操做時將線程阻塞,等待執行完成後再繼續執行。但線程的消耗是很大的,並不適合大量併發操做的處理,而經過單線程併發能夠進行大量併發操做。當多核CPU出現後,單個線程就沒法很好的利用多核CPU的優點了,因此又引入了線程池的概念,經過線程池來管理大量線程。

協程

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

協程分爲無線協程和有線協程,無線協程在離開當前調用位置時,會將當前變量放在堆區,當再次回到當前位置時,還會繼續從堆區中獲取到變量。因此,通常在執行當前函數時就會將變量直接分配到堆區,而asyncawait就屬於無線協程的一種。有線協程則會將變量繼續保存在棧區,在回到指針指向的離開位置時,會繼續從棧中取出調用。

async、await原理

asyncawait爲例,協程在執行時,執行到async則表示進入一個協程,會同步執行async的代碼塊。async的代碼塊本質上也至關於一個函數,而且有本身的上下文環境。當執行到await時,則表示有任務須要等待,CPU則去調度執行其餘IO,也就是後面的代碼或其餘協程代碼。過一段時間CPU就會輪訓一次,看某個協程是否任務已經處理完成,有返回結果能夠被繼續執行,若是能夠被繼續執行的話,則會沿着上次離開時指針指向的位置繼續執行,也就是await標誌的位置。

因爲並無開啓新的線程,只是進行IO中斷改變CPU調度,因此網絡請求這樣的異步操做可使用asyncawait,但若是是執行大量耗時同步操做的話,應該使用isolate開闢新的線程去執行。

若是用協程和iOS的dispatch_async進行對比,能夠發現兩者是比較類似的。從結構定義來看,協程須要將當前await的代碼塊相關的變量進行存儲,dispatch_async也能夠經過block來實現臨時變量的存儲能力。

我以前還在想一個問題,蘋果爲何不引入協程的特性呢?後來想了一下,awaitdispatch_async均可以簡單理解爲異步操做,OC的線程是基於Runloop實現的,Dart本質上也是有事件循環的,並且兩者都有本身的事件隊列,只是隊列數量和分類不一樣。

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

Promise

這裏想提一下JS裏的Promise語法,在iOS中會出現不少if判斷或者其餘的嵌套調用,而Promise能夠把以前橫向的嵌套調用,改爲縱向鏈式調用。若是能把Promise引入到OC裏,可讓代碼看起來更簡潔,直觀。

isolate

isolateDart平臺對線程的實現方案,但和普通Thread不一樣的是,isolate擁有獨立的內存,isolate由線程和獨立內存構成。正是因爲isolate線程之間的內存不共享,因此isolate線程之間並不存在資源搶奪的問題,因此也不須要鎖。

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

isolate看起來其實和進程比較類似,以前請教阿里架構師宗心問題時,宗心也說過「isolate的總體模型我本身的理解其實更像進程,而asyncawait更像是線程」。若是對比一下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的消息機制,經過創建通訊雙方的sendPortreceiveport,進行相互的消息傳遞,在Dart中叫作消息傳遞。

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

Embedder

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

Flutter System Overriew

Embedder中存在四個Runner,四個Runner分別以下。其中每一個Flutter Engine各自對應一個UI RunnerGPU RunnerIO Runner,但全部Engine共享一個Platform Runner

Embedder

Runnerisolate並非一碼事,彼此相互獨立。以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 IsolateEngine提交一次渲染幀時,Engine會等待下次vsync,當下次vsync到來時,由Root IsolateWidgets進行佈局操做,並生成頁面的顯示信息的描述,並將信息交給Engine去處理。

因爲對widgets進行layout並生成layer treeUI Runner進行的,若是在UI Runner中進行大量耗時處理,會影響頁面的顯示,因此應該將耗時操做交給其餘isolate處理,例如來自Native Plugin的事件。

Rendering Pipeline.jpg

GPU Runner

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

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

Graphics Pipeline

IO Runner

一些GPU Runner中比較耗時的操做,就放在IO Runner中進行處理,例如圖片讀取、解壓、渲染等操做。可是隻有GPU Runner才能對GPU提交渲染信息,爲了保證IO Runner也具有這個能力,因此IO Runner會引用GPU Runnercontext,這樣就具有向GPU提交渲染信息的能力。


簡書因爲排版的問題,閱讀體驗並很差,佈局、圖片顯示、代碼等不少問題。因此建議到我Github上,下載Flutter編程指南 PDF合集。把全部Flutter文章總計三篇,都寫在這個PDF中,並且左側有目錄,方便閱讀。

Flutter編程指南

下載地址:Flutter編程指南 PDF 麻煩各位大佬點個贊,謝謝!😁

相關文章
相關標籤/搜索