深入理解Flutter多線程


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高。

這兩個隊列也是有優先級的,當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負責范圍包括原生平台插件、線程管理、事件循環等。

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

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的事件。

廣州品牌設計公司https://www.houdianzi.com PPT模板下載大全https://redbox.wode007.com

GPU Runner

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

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

 

IO Runner

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM