深入理解Flutter多線程

閃念基因 發佈 2023-02-12T14:16:15.916209+00:00

從18年底Flutter1.0正式版發布以來,在國內出現了爆發式增長,包括閒魚、抖音、今日頭條在內的大多數公司,相繼接入了Flutter,通過Flutter進行跨平台開發。


導讀

從18年底Flutter1.0正式版發布以來,在國內出現了爆發式增長,包括閒魚、抖音、今日頭條在內的大多數公司,相繼接入了Flutter,通過Flutter進行跨平台開發。


在使用Flutter的過程中,我們也需要對其一些特性進行研究和掌握,如果只是調用API的話並不能領悟到Flutter的精髓。


本篇文章詳細講解了Flutter的多線程,由於筆者是做iOS開發的,並且將Flutter的多線程和IOS的GCD進行了對比,以幫助大家理解Flutter多線程。



事件隊列

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


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


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

  • 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


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

5 return response;
6}
7
8Future<String> loadData() async {
9 Response response = await dataReqeust();
10 return response.body;
11}


<向右滑動查看完整代碼>


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


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


1class _SampleAppPageState extends State<SampleAppPage> {

2 List widgets = [];
3
4 @override
5 void initState() {
6 super.initState();
7 loadData();
8 }
9
10 loadData() async {
11 String dataURL = "https://jsonplaceholder.typicode.com/posts";
12 http.Response response = await http.get(dataURL);
13 setState(() {
14 widgets = json.decode(response.body);
15 });
16 }
17}

<向右滑動查看完整代碼>



Future

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


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


1loadData() {
2 // DateTime.now(),獲取當前時間
3 DateTime now = DateTime.now();
4 print('request begin $now');
5 Future.delayed(Duration(seconds: 1), (){
6 now = DateTime.now();
7 print('request response $now');
8 });
9}


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


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



協程

如果想要了解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線程之間並不存在資源搶奪的問題,所以也不需要鎖。


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


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


代碼示例

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


1loadData() async {
2 // 通過spawn新建一個Isolute,並綁定靜態方法
3 ReceivePort receivePort =ReceivePort();
4 await Isolate.spawn(dataLoader, receivePort.sendPort);
5
6 // 獲取新Isolute的監聽port
7 SendPort sendPort = await receivePort.first;
8 // 調用sendReceive自定義方法
9 List dataList = await sendReceive(sendPort, 'https://jsonplaceholder.typicode.com/posts');
10 print('dataList $dataList');
11}
12
13// isolute的綁定方法
14static dataLoader(SendPort sendPort) async{
15 // 創建監聽port,並將sendPort傳給外界用來調用
16 ReceivePort receivePort =ReceivePort();
17 sendPort.send(receivePort.sendPort);
18
19 // 監聽外界調用
20 await for (var msg in receivePort) {
21 String requestURL =msg[0];
22 SendPort callbackPort =msg[1];
23
24 Client client = Client();
25 Response response = await client.get(requestURL);
26 List dataList = json.decode(response.body);
27 // 回調返回值給調用者
28 callbackPort.send(dataList);
29 }
30}
31
32// 創建自己的監聽port,並且向新isolute發送消息
33Future sendReceive(SendPort sendPort, String url) {
34 ReceivePort receivePort =ReceivePort();
35 sendPort.send([url, receivePort.sendPort]);
36 // 接收到返回值,返回給調用者
37 return receivePort.first;
38}

<向右滑動查看完整代碼>


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


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



Embedder

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


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


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


Platform Runner

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


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


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


UI Runner

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


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


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


GPU Runner

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


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


IO Runner

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


作者:劉小壯

來源:微信公眾號:搜狐技術產品

出處:https://mp.weixin.qq.com/s/0dhV1FG0W7L45sCN49yLnQ

關鍵字: