阿里開源 iOS 協程開發框架 coobjc原始碼分析
昨天朋友圈被一篇文章(以下簡稱“coobjc介紹文章”)刷屏了: 剛剛,阿里開源 iOS 協程開發框架 coobjc! 。可能大部分iOS開發者都直接懵逼了:
- 什麼是協程?
- 協程的作用是什麼?
- 為什麼要使用它?
因此筆者想給大家普及普及協程的知識,執行一下 coobjc
的Example,順便分析一下 coobjc
原始碼。
分析
協程的維基百科在這裡:協程。引用裡面的解釋如下:
協程是計算機程式的一類元件,推廣了非搶先多工的子程式,允許執行被掛起與被恢復。相對子例程而言,協程更為一般和靈活,但在實踐中使用沒有子例程那樣廣泛。協程源自Simula和Modula-2語言,但也有其他語言支援。協程更適合於用來實現彼此熟悉的程式元件,如合作式多工、異常處理、事件迴圈、迭代器、無限列表和管道。 根據高德納的說法, 馬爾文·康威於1958年發明了術語coroutine並用於構建彙編程式。
對,還是一知半解。但最起碼我們瞭解到
coobjc Objective-C
協程的作用其實在 coobjc
介紹文章中有提及,是為了優化 iOS
中的非同步操作。解決了如下問題:
- "巢狀地獄"
- 錯誤處理複雜和冗長
- 容易忘記呼叫 completion handler
- 條件執行變得很困難
- 從互相獨立的呼叫中組合返回結果變得極其困難
- 在錯誤的執行緒中繼續執行
- 難以定位原因的多執行緒崩潰
- 鎖和訊號量濫用帶來的卡頓、卡死
聽起來是有點強大,最明顯的好處是可以簡化程式碼;並且在coobjc介紹文章也說道,效能也有所保障:當執行緒的數量級大於1000以上時, coobjc
的優勢就會非常明顯。為了證明文章的結論,我們就來執行一下 coobjc
原始碼好了。 這裡 下載 coobjc
原始碼。 發現目錄結構如下:

coobjc
介紹文章中提到的,
coobjc
不但提供了基礎的非同步操作還提供了基於UIKit的封裝。目錄中
-
cokit
及其子目錄提供的是基於UIKit層的coobjc
封裝 -
coobjc
目錄是coobjc
的Objective-C
版實現的原始碼 -
coswift
目錄是coobjc
的Swift
版實現的原始碼 -
Example
下有兩個目錄,一個是Objective-C
的實現,一個是Swift
版的實現的Demo
我們先分析一下 coobjcBaseExample
工程: 開啟專案, pod update
一下即可執行,執行結果如下:

可以看到是個簡單的列表頁。
Tips 開啟podfile可以發現裡面有庫 coobjc
以外,還有 Specta
、 Expecta
以及 OCMock
。這三個庫這裡不多做介紹了,大家只需要知道這是用於單元測試的。
我們先看一下這個列表的實現邏輯是什麼樣的。我們不難定位到頁面位於 KMDiscoverListViewController
中,其網路請求(這裡是電影列表)程式碼如下:
- (void)requestMovies { co_launch(^{ NSArray *dataArray = [[KMDiscoverSource discoverSource] getDiscoverList:@"1"]; [self.refreshControl endRefreshing]; if (dataArray != nil) { [self processData:dataArray]; } else { [self.networkLoadingViewController showErrorView]; } }); } 複製程式碼
這裡很容易理解程式碼
NSArray *dataArray = [[KMDiscoverSource discoverSource] getDiscoverList:@"1"]; 複製程式碼
是請求網路資料的,其實現如下:
- (NSArray*)getDiscoverList:(NSString *)pageLimit; { NSString *url = [NSString stringWithFormat:@"%@&page=%@", [self prepareUrl], pageLimit]; id json = [[DataService sharedInstance] requestJSONWithURL:url]; NSDictionary* infosDictionary = [self dictionaryFromResponseObject:json jsonPatternFile:@"KMDiscoverSourceJsonPattern.json"]; return [self processResponseObject:infosDictionary]; } 複製程式碼
以上程式碼也能猜出,
id json = [[DataService sharedInstance] requestJSONWithURL:url]; 複製程式碼
這一行是做了網路請求,但是我們再點選進入類 DataService
看 requestJSONWithURL
方法的實現的時候,發現已經看不懂了:
- (id)requestJSONWithURL:(NSString*)url CO_ASYNC{ SURE_ASYNC return await([self.jsonActor sendMessage:url]); } 複製程式碼
好吧。既然看不懂了,我們就從頭開始學習,協程的含義以及使用。繼而對 coobjc
原始碼進行分析。
協程入門
coobjc
介紹文章中有提到
- 第一種:利用
glibc
的ucontext
元件(雲風的庫)。 - 第二種:使用匯編程式碼來切換上下文(實現C協程),原理同
ucontext
。 - 第三種:利用C語言語法
switch-case
的奇淫技巧來實現(Protothreads)。 - 第四種:利用了 C 語言的
setjmp
和longjmp
。 - 第五種:利用編譯器支援語法糖。
經過篩選最終選擇了第二種。那我們來一個個分析,為什麼 coobjc
摒棄了其他的方式。 首先我們看第一種, coobjc
介紹文章中提到 ucontext
在iOS中被廢棄了,那如果不廢棄,我們如何去使用 ucontext
呢?如下的一個Demo可以解釋一下 ucontext
的用法:
#include <stdio.h> #include <ucontext.h> #include <unistd.h> int main(int argc, const char *argv[]){ ucontext_t context; getcontext(&context); puts("Hello world"); sleep(1); setcontext(&context); return 0; } 複製程式碼
注:示例程式碼來自維基百科.
儲存上述程式碼到example.c,執行編譯命令:
gcc example.c -o example 複製程式碼
想想程式執行的結果會是什麼樣?
kysonzhu@ubuntu:~$ ./example Hello world Hello world Hello world Hello world Hello world Hello world ^C kysonzhu@ubuntu:~$ 複製程式碼
上面是程式執行的部分輸出,不知道是否和你想得一樣呢?我們可以看到,程式在輸出第一個“Hello world"後並沒有退出程式,而是持續不斷的輸出“Hello world”。其實是程式通過 getcontext
先儲存了一個上下文,然後輸出“Hello world”,在通過 setcontext
恢復到 getcontext
的地方,重新執行程式碼,所以導致程式不斷的輸出“Hello world”,在我這個菜鳥的眼裡,這簡直就是一個神奇的跳轉。那麼問題來了, ucontext
到底是什麼?
這裡筆者不多做介紹了,推薦一篇文章,講的比較詳細: ucontext-人人都可以實現的簡單協程庫 這裡我們只需要知道,所謂 coobjc
介紹文章中提到的使用匯編語言模擬 ucontext
,其實就是模擬的上面例子中的 setcontext
及 getcontext
等函式。為了證明筆者的猜想,筆者打開了 coobjc
原始碼庫,發現裡面的唯一的彙編檔案 coroutine_context.s

檢視該檔案,發現了這麼幾個函式:
- _coroutine_getcontext
- _coroutine_begin
- _coroutine_setcontext
果然驗證了筆者的想法。這三個方法被暴露在檔案 coroutine_context.h
中,供後序呼叫:
extern int coroutine_getcontext (coroutine_ucontext_t *__ucp); extern int coroutine_setcontext (coroutine_ucontext_t *__ucp); extern int coroutine_begin (coroutine_ucontext_t *__ucp); 複製程式碼
這麼一來,我們之前的程式可以改寫成如下:
#import <coobjc/coroutine_context.h> int main(int argc, const char *argv[]) { coroutine_ucontext_t context; coroutine_getcontext(&context); puts("Hello world"); sleep(1); coroutine_setcontext(&context); return 0; } 複製程式碼
返回的結果仍然不變,一直列印“hello world”。
深入協程
上面我們只簡單的介紹了 coobjc
,也瞭解到 coobjc
基本都是參考了 ucontext
。那下面的例子中,筆者儘可能先介紹 ucontext
,然後再應用到 coobjc
對應的方法中。 我們繼續討論上文提到的幾個函式,並說明一下其作用:
intgetcontext(ucontext_t *uctp) 複製程式碼
這個方法是,獲取當前上下文,並將上下文設定到 uctp
中, uctp
是個上下文結構體,其定義如下:
_STRUCT_UCONTEXT { intuc_onstack; __darwin_sigset_tuc_sigmask;/* signal mask used by this context */ _STRUCT_SIGALTSTACKuc_stack;/* stack used by this context */ _STRUCT_UCONTEXT*uc_link;/* pointer to resuming context */ __darwin_size_tuc_mcsize;/* size of the machine context passed in */ _STRUCT_MCONTEXT*uc_mcontext;/* pointer to machine specific context */ #ifdef _XOPEN_SOURCE _STRUCT_MCONTEXT__mcontext_data; #endif /* _XOPEN_SOURCE */ }; /* user context */ typedef _STRUCT_UCONTEXTucontext_t;/* [???] user context */ 複製程式碼
以上是 ucontext
的資料結構,其內部的幾個屬性介紹一下: 噹噹前上下文(如使用makecontext建立的上下文)執行終止時系統會恢復 uc_link
指向的上下文; uc_sigmask
為該上下文中的阻塞訊號集合; uc_stack
為該上下文中使用的棧; uc_mcontext
儲存的上下文的特定機器表示,包括呼叫執行緒的特定暫存器等。其實還蠻好理解的, ucontext
其實就存放一些必要的資料,這些資料還包括拯救成功或者失敗的情況需要的資料。
接下來說另外一個函式
intsetcontext(const ucontext_t *cut) 複製程式碼
該函式是設定當前的上下文為 cut
, setcontext
的上下文 cut
應該通過 getcontext
或者 makecontext
取得,如果呼叫成功則不返回。如果上下文是通過呼叫 getcontext()
取得,程式會繼續執行這個呼叫。如果上下文是通過呼叫 makecontext
取得,程式會呼叫 makecontext
函式的第二個引數指向的函式,如果 func
函式返回,則恢復 makecontext
第一個引數指向的上下文第一個引數指向的上下文 context_t
中指向的 uc_link
.如果 uc_link
為NULL,則執行緒退出。
同樣的,我們畫個表類比一下 ucontext
和 coobjc
的函式:
ucontext | coobjc | 含義 |
---|---|---|
setcontext | coroutine_setcontext | 設定協程上下文 |
getcontext | coroutine_getcontext | 獲取協程上下文 |
makecontext | coroutine_create | 建立一個協程上下文 |
以及 ucontext
以及 coobjc
結構體定義: