該文章屬於<簡書 — 劉小壯>原創,轉載請註明:
<簡書 — 劉小壯> 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
高。
這兩個佇列也是有優先順序的,當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
的事件。
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提交渲染資訊的能力。