三国杀武将|手游三国杀边锋版

深入理解Flutter多線程

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

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

<簡書 — 劉小壯> 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對象。獲取到Future對象后,最簡單的方法就是用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提交渲染信息的能力。

三国杀武将