本系列博客是本人的開(kāi)發(fā)筆記一睁。為了方便討論,本人新建了一個(gè)微信群(iOS技術(shù)討論群)佃却,想要加入的者吁,請(qǐng)?zhí)砑颖救宋⑿牛簔hujinhui207407,【加我前請(qǐng)備注:iOS 】饲帅,本人博客http://www.kyson.cn 也在不停的更新中复凳,歡迎一起討論
背景
今天朋友圈被一篇文章(以下簡(jiǎn)稱(chēng)“coobjc介紹文章”)刷屏了:剛剛,阿里開(kāi)源 iOS 協(xié)程開(kāi)發(fā)框架 coobjc灶泵!染坯。可能大部分iOS開(kāi)發(fā)者都直接懵逼了:
- 什么是協(xié)程丘逸?
- 協(xié)程的作用是什么单鹿?
- 為什么要使用它?
因此筆者想給大家普及普及協(xié)程的知識(shí)深纲,運(yùn)行一下coobjc
的Example仲锄,順便分析一下coobjc
源碼劲妙。
分析
協(xié)程的維基百科在這里:協(xié)程。引用里面的解釋如下:
協(xié)程是計(jì)算機(jī)程序的一類(lèi)組件儒喊,推廣了非搶先多任務(wù)的子程序镣奋,允許執(zhí)行被掛起與被恢復(fù)。相對(duì)子例程而言怀愧,協(xié)程更為一般和靈活侨颈,但在實(shí)踐中使用沒(méi)有子例程那樣廣泛。協(xié)程源自Simula和Modula-2語(yǔ)言芯义,但也有其他語(yǔ)言支持哈垢。協(xié)程更適合于用來(lái)實(shí)現(xiàn)彼此熟悉的程序組件,如合作式多任務(wù)扛拨、異常處理耘分、事件循環(huán)、迭代器绑警、無(wú)限列表和管道求泰。
根據(jù)高德納的說(shuō)法, 馬爾文·康威于1958年發(fā)明了術(shù)語(yǔ)coroutine并用于構(gòu)建匯編程序。
對(duì)计盒,還是一知半解渴频。但最起碼我們了解到
- 協(xié)程的英文是“coroutine”,因此我們能理解阿里的庫(kù)起名為
coobjc
的含義北启。那么這個(gè)詞又是怎么來(lái)的呢枉氮?筆者再深挖一下,協(xié)程(coroutine)顧名思義就是“協(xié)作的例程”(co-operative routines)暖庄。 - 協(xié)程是和進(jìn)程或者線程有一定關(guān)系的
- 協(xié)程的歷史還是比較悠久的聊替,只是
Objective-C
不支持。筆者經(jīng)過(guò)查閱培廓,發(fā)現(xiàn)很多現(xiàn)代語(yǔ)言都支持協(xié)程惹悄。比如Python以及swift,甚至C語(yǔ)言也是支持協(xié)程的肩钠。
協(xié)程的作用其實(shí)在coobjc
介紹文章中有提及泣港,是為了優(yōu)化iOS
中的異步操作。解決了如下問(wèn)題:
- "嵌套地獄"
- 錯(cuò)誤處理復(fù)雜和冗長(zhǎng)
- 容易忘記調(diào)用 completion handler
- 條件執(zhí)行變得很困難
- 從互相獨(dú)立的調(diào)用中組合返回結(jié)果變得極其困難
- 在錯(cuò)誤的線程中繼續(xù)執(zhí)行
- 難以定位原因的多線程崩潰
- 鎖和信號(hào)量濫用帶來(lái)的卡頓价匠、卡死
聽(tīng)起來(lái)是有點(diǎn)強(qiáng)大当纱,最明顯的好處是可以簡(jiǎn)化代碼;并且在coobjc介紹文章也說(shuō)道踩窖,性能也有所保障:當(dāng)線程的數(shù)量級(jí)大于1000以上時(shí)坡氯,coobjc
的優(yōu)勢(shì)就會(huì)非常明顯。為了證明文章的結(jié)論,我們就來(lái)運(yùn)行一下coobjc
源碼好了箫柳。
這里下載coobjc
源碼手形。
發(fā)現(xiàn)目錄結(jié)構(gòu)如下:
從目錄結(jié)構(gòu)看還是比較清晰的,根據(jù)coobjc
介紹文章中提到的悯恍,coobjc
不但提供了基礎(chǔ)的異步操作還提供了基于UIKit的封裝库糠。目錄中
-
cokit
及其子目錄提供的是基于UIKit層的coobjc
封裝 -
coobjc
目錄是coobjc
的Objective-C
版實(shí)現(xiàn)的源代碼 -
coswift
目錄是coobjc
的Swift
版實(shí)現(xiàn)的源代碼 -
Example
下有兩個(gè)目錄,一個(gè)是Objective-C
的實(shí)現(xiàn)涮毫,一個(gè)是Swift
版的實(shí)現(xiàn)的Demo
我們先分析一下coobjcBaseExample
工程:
打開(kāi)項(xiàng)目瞬欧,pod update
一下即可運(yùn)行,運(yùn)行結(jié)果如下:
可以看到是個(gè)簡(jiǎn)單的列表頁(yè)。
Tips
打開(kāi)podfile可以發(fā)現(xiàn)里面有庫(kù)coobjc
以外罢防,還有Specta
艘虎、Expecta
以及OCMock
。這三個(gè)庫(kù)這里不多做介紹了篙梢,大家只需要知道這是用于單元測(cè)試的。
我們先看一下這個(gè)列表的實(shí)現(xiàn)邏輯是什么樣的美旧。我們不難定位到頁(yè)面位于KMDiscoverListViewController
中渤滞,其網(wǎng)絡(luò)請(qǐng)求(這里是電影列表)代碼如下:
- (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"];
是請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)的,其實(shí)現(xiàn)如下:
- (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];
這一行是做了網(wǎng)絡(luò)請(qǐng)求妄呕,但是我們?cè)冱c(diǎn)擊進(jìn)入類(lèi)DataService
看requestJSONWithURL
方法的實(shí)現(xiàn)的時(shí)候,發(fā)現(xiàn)已經(jīng)看不懂了:
- (id)requestJSONWithURL:(NSString*)url CO_ASYNC{
SURE_ASYNC
return await([self.jsonActor sendMessage:url]);
}
好吧嗽测。既然看不懂了绪励,我們就從頭開(kāi)始學(xué)習(xí),協(xié)程的含義以及使用唠粥。繼而對(duì)coobjc
源碼進(jìn)行分析疏魏。
協(xié)程入門(mén)
coobjc
介紹文章中有提到
- 第一種:利用
glibc
的ucontext
組件(云風(fēng)的庫(kù))。 - 第二種:使用匯編代碼來(lái)切換上下文(實(shí)現(xiàn)C協(xié)程)晤愧,原理同
ucontext
大莫。 - 第三種:利用C語(yǔ)言語(yǔ)法
switch-case
的奇淫技巧來(lái)實(shí)現(xiàn)(Protothreads)。 - 第四種:利用了 C 語(yǔ)言的
setjmp
和longjmp
官份。 - 第五種:利用編譯器支持語(yǔ)法糖只厘。
經(jīng)過(guò)篩選最終選擇了第二種。那我們來(lái)一個(gè)個(gè)分析舅巷,為什么coobjc
摒棄了其他的方式羔味。
首先我們看第一種,coobjc
介紹文章中提到ucontext
在iOS中被廢棄了钠右,那如果不廢棄赋元,我們?nèi)绾稳ナ褂?code>ucontext呢?如下的一個(gè)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;
}
注:示例代碼來(lái)自維基百科.
保存上述代碼到example.c,執(zhí)行編譯命令:
gcc example.c -o example
想想程序運(yùn)行的結(jié)果會(huì)是什么樣?
kysonzhu@ubuntu:~$ ./example
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
^C
kysonzhu@ubuntu:~$
上面是程序執(zhí)行的部分輸出们陆,不知道是否和你想得一樣呢寒瓦?我們可以看到,程序在輸出第一個(gè)“Hello world"后并沒(méi)有退出程序坪仇,而是持續(xù)不斷的輸出“Hello world”杂腰。其實(shí)是程序通過(guò)getcontext
先保存了一個(gè)上下文,然后輸出“Hello world”,在通過(guò)setcontext
恢復(fù)到getcontext
的地方,重新執(zhí)行代碼椅文,所以導(dǎo)致程序不斷的輸出“Hello world”喂很,在我這個(gè)菜鳥(niǎo)的眼里,這簡(jiǎn)直就是一個(gè)神奇的跳轉(zhuǎn)皆刺。那么問(wèn)題來(lái)了少辣,ucontext
到底是什么?
這里筆者不多做介紹了羡蛾,推薦一篇文章漓帅,講的比較詳細(xì):ucontext-人人都可以實(shí)現(xiàn)的簡(jiǎn)單協(xié)程庫(kù)
這里我們只需要知道,所謂coobjc
介紹文章中提到的使用匯編語(yǔ)言模擬ucontext
痴怨,其實(shí)就是模擬的上面例子中的setcontext
及getcontext
等函數(shù)忙干。為了證明筆者的猜想,筆者打開(kāi)了coobjc
源碼庫(kù)浪藻,發(fā)現(xiàn)里面的唯一的匯編文件coroutine_context.s
查看該文件捐迫,發(fā)現(xiàn)了這么幾個(gè)函數(shù):
- _coroutine_getcontext
- _coroutine_begin
- _coroutine_setcontext
果然驗(yàn)證了筆者的想法。這三個(gè)方法被暴露在文件coroutine_context.h
中爱葵,供后序調(diào)用:
extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_begin (coroutine_ucontext_t *__ucp);
接下來(lái)說(shuō)另外一個(gè)函數(shù)
int setcontext(const ucontext_t *cut)
該函數(shù)是設(shè)置當(dāng)前的上下文為cut
施戴,setcontext
的上下文cut
應(yīng)該通過(guò)getcontext
或者makecontext
取得,如果調(diào)用成功則不返回萌丈。如果上下文是通過(guò)調(diào)用getcontext()
取得,程序會(huì)繼續(xù)執(zhí)行這個(gè)調(diào)用赞哗。如果上下文是通過(guò)調(diào)用makecontext
取得,程序會(huì)調(diào)用makecontext
函數(shù)的第二個(gè)參數(shù)指向的函數(shù),如果func
函數(shù)返回,則恢復(fù)makecontext
第一個(gè)參數(shù)指向的上下文第一個(gè)參數(shù)指向的上下文context_t
中指向的uc_link
.如果uc_link
為NULL,則線程退出辆雾。
我們畫(huà)個(gè)表類(lèi)比一下ucontext
和coobjc
的函數(shù):
ucontext | coobjc | 含義 |
---|---|---|
setcontext | coroutine_setcontext | 設(shè)置協(xié)程上下文 |
getcontext | coroutine_getcontext | 獲取協(xié)程上下文 |
makecontext | coroutine_create | 創(chuàng)建一個(gè)協(xié)程上下文 |
這么一來(lái)懈玻,我們之前的程序可以改寫(xiě)成如下:
#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;
}
返回的結(jié)果仍然不變,一直打印“hello world”乾颁。
深入?yún)f(xié)程
(1)目錄分析
上圖是
coobjc
的目錄結(jié)構(gòu)涂乌,其中
-
core
目錄提供了核心的協(xié)程函數(shù) -
api
目錄是coobjc
基于Objective-C
的封裝 -
csp
,目錄從庫(kù)libtask引入英岭,提供了一些鏈?zhǔn)讲僮?/li> -
objc
提供了coobjc
對(duì)象聲明周期管理的一些類(lèi)
下面的文章湾盒,筆者會(huì)先從核心的core
目錄開(kāi)始研究,后面的大家理解起來(lái)也就不復(fù)雜了诅妹。
(2)協(xié)程的構(gòu)成
上面我們只簡(jiǎn)單的介紹了coobjc
罚勾,也了解到coobjc
基本都是參考了ucontext
毅人。那下面的例子中,筆者盡可能先介紹ucontext
尖殃,然后再應(yīng)用到coobjc
對(duì)應(yīng)的方法中丈莺。
我們繼續(xù)討論上文提到的幾個(gè)函數(shù),并說(shuō)明一下其作用:
int getcontext(ucontext_t *uctp)
這個(gè)方法是送丰,獲取當(dāng)前上下文缔俄,并將上下文設(shè)置到uctp
中,uctp
是個(gè)上下文結(jié)構(gòu)體器躏,其定義如下:
_STRUCT_UCONTEXT
{
int uc_onstack;
__darwin_sigset_t uc_sigmask; /* signal mask used by this context */
_STRUCT_SIGALTSTACK uc_stack; /* stack used by this context */
_STRUCT_UCONTEXT *uc_link; /* pointer to resuming context */
__darwin_size_t uc_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_UCONTEXT ucontext_t; /* [???] user context */
以上是ucontext
的數(shù)據(jù)結(jié)構(gòu)俐载,其內(nèi)部的幾個(gè)屬性介紹一下:
當(dāng)當(dāng)前上下文(如使用makecontext創(chuàng)建的上下文)運(yùn)行終止時(shí)系統(tǒng)會(huì)恢復(fù)uc_link
指向的上下文;uc_sigmask
為該上下文中的阻塞信號(hào)集合登失;uc_stack
為該上下文中使用的棧遏佣;uc_mcontext
保存的上下文的特定機(jī)器表示,包括調(diào)用線程的特定寄存器等揽浙。其實(shí)還蠻好理解的状婶,ucontext
其實(shí)就存放一些必要的數(shù)據(jù),這些數(shù)據(jù)還包括拯救成功或者失敗的情況需要的數(shù)據(jù)馅巷。
相比較而言膛虫,coobjc
的定義和ucontext
有一定區(qū)別:
/**
The structure store coroutine's context data.
*/
struct coroutine {
coroutine_func entry; // Process entry.
void *userdata; // Userdata.
coroutine_func userdata_dispose; // Userdata's dispose action.
void *context; // Coroutine's Call stack data.
void *pre_context; // Coroutine's source process's Call stack data.
int status; // Coroutine's running status.
uint32_t stack_size; // Coroutine's stack size
void *stack_memory; // Coroutine's stack memory address.
void *stack_top; // Coroutine's stack top address.
struct coroutine_scheduler *scheduler; // The pointer to the scheduler.
int8_t is_scheduler; // The coroutine is a scheduler.
struct coroutine *prev;
struct coroutine *next;
void *autoreleasepage; // If enable autorelease, the custom autoreleasepage.
bool is_cancelled; // The coroutine is cancelled
};
typedef struct coroutine coroutine_t;
其中
struct coroutine *prev;
struct coroutine *next;
表明其是一個(gè)鏈表結(jié)構(gòu)。
既然是鏈表令杈,那么就會(huì)有添加元素走敌,以及刪除某個(gè)元素的方法碴倾,果然我們?cè)?code>coroutine.m中發(fā)現(xiàn)了對(duì)應(yīng)的鏈表操作方法:
// add routine to the queue
void scheduler_add_coroutine(coroutine_list_t *l, coroutine_t *t) {
if(l->tail) {
l->tail->next = t;
t->prev = l->tail;
} else {
l->head = t;
t->prev = nil;
}
l->tail = t;
t->next = nil;
}
// delete routine from the queue
void scheduler_delete_coroutine(coroutine_list_t *l, coroutine_t *t) {
if(t->prev) {
t->prev->next = t->next;
} else {
l->head = t->next;
}
if(t->next) {
t->next->prev = t->prev;
} else {
l->tail = t->prev;
}
}
其中coroutine_list_t
是為了標(biāo)識(shí)鏈表的頭尾節(jié)點(diǎn):
/**
Define the linked list of scheduler's queue.
*/
struct coroutine_list {
coroutine_t *head;
coroutine_t *tail;
};
typedef struct coroutine_list coroutine_list_t;
為了管理所有的協(xié)程狀態(tài)逗噩,還設(shè)置了一個(gè)調(diào)度器:
/**
Define the scheduler.
One thread own one scheduler, all coroutine run this thread shares it.
*/
struct coroutine_scheduler {
coroutine_t *main_coroutine;
coroutine_t *running_coroutine;
coroutine_list_t coroutine_queue;
};
typedef struct coroutine_scheduler coroutine_scheduler_t;
看命名就大概能猜到,main_coroutine
中包含了主協(xié)程(可能是即將設(shè)置數(shù)據(jù)的協(xié)程跌榔,或者即將使用的協(xié)程)异雁;running_coroutine
是當(dāng)前正在運(yùn)行的協(xié)程。
(3)協(xié)程的操作
協(xié)程擁有和線程一樣類(lèi)似的操作僧须,例如創(chuàng)建纲刀,啟動(dòng),出讓控制權(quán)担平,恢復(fù)示绊,以及死亡。對(duì)應(yīng)的暂论,我們?cè)?code>coroutine.h看到了如下的幾個(gè)函數(shù)聲明:
//關(guān)閉一個(gè)協(xié)程如果它已經(jīng)死亡
void coroutine_close_ifdead(coroutine_t *co);
//添加協(xié)程到調(diào)度器面褐,并且立刻啟動(dòng)
void coroutine_resume(coroutine_t *co);
//添加協(xié)程到調(diào)度器
void coroutine_add(coroutine_t *co);
//出讓控制權(quán)
void coroutine_yield(coroutine_t *co);
為了更好的控制各個(gè)操作中的數(shù)據(jù),coobjc
還提供了以下兩個(gè)方法:
void coroutine_setuserdata(coroutine_t *co, void *userdata, coroutine_func userdata_dispose);
void *coroutine_getuserdata(coroutine_t *co);
至此取胎,coobjc
的核心代碼都分析完成了展哭。
(3)協(xié)程的Objective-C層面的封裝
我們?cè)俅位氐轿恼麻_(kāi)頭的例子- (void)requestMovies
方法的實(shí)現(xiàn)中湃窍,第一步就是調(diào)用一個(gè)co_launch()
的方法,這個(gè)方法最終會(huì)調(diào)用到
+ (instancetype)coroutineWithBlock:(void(^)(void))block onQueue:(dispatch_queue_t _Nullable)queue stackSize:(NSUInteger)stackSize {
if (queue == NULL) {
queue = co_get_current_queue();
}
if (queue == NULL) {
return nil;
}
COCoroutine *coObj = [[self alloc] initWithBlock:block onQueue:queue];
coObj.queue = queue;
coroutine_t *co = coroutine_create((void (*)(void *))co_exec);
if (stackSize > 0 && stackSize < 1024*1024) { // Max 1M
co->stack_size = (uint32_t)((stackSize % 16384 > 0) ? ((stackSize/16384 + 1) * 16384) : stackSize/16384); // Align with 16kb
}
coObj.co = co;
coroutine_setuserdata(co, (__bridge_retained void *)coObj, co_obj_dispose);
return coObj;
}
- (void)resumeNow {
[self performBlockOnQueue:^{
if (self.isResume) {
return;
}
self.isResume = YES;
coroutine_resume(self.co);
}];
}
這兩個(gè)方法匪傍。其實(shí)代碼已經(jīng)很容易理解了您市,第一個(gè)方法是創(chuàng)建一個(gè)協(xié)程,第二個(gè)是啟動(dòng)役衡。
最后我們?cè)谡f(shuō)一下文章開(kāi)頭提到的await方法茵休,其實(shí)最終就交給chan
去處理了:
- (COActorCompletable *)sendMessage:(id)message {
COActorCompletable *completable = [COActorCompletable promise];
dispatch_async(self.queue, ^{
COActorMessage *actorMessage = [[COActorMessage alloc] initWithType:message completable:completable];
[self.messageChan send_nonblock:actorMessage];
});
return completable;
}
所有的操作雖然丟到了同一個(gè)線程中,但其實(shí)最終是通過(guò)chan
來(lái)調(diào)度了映挂。關(guān)于chan就不在本文討論范圍了泽篮,后面如果有時(shí)間,筆者會(huì)再進(jìn)行對(duì)chan的分析柑船。
總結(jié)
本文介紹了協(xié)程的概念帽撑,通過(guò)對(duì)比ucontext
以及coobjc
來(lái)說(shuō)明協(xié)程的用法,并分析了coobjc
的源代碼鞍时,希望對(duì)大家有所幫助亏拉。
擴(kuò)展閱讀
iOS單元測(cè)試:Specta + Expecta + OCMock + OHHTTPStubs + KIF
一個(gè)“蠅量級(jí)” C 語(yǔ)言協(xié)程庫(kù)
ucontext-人人都可以實(shí)現(xiàn)的簡(jiǎn)單協(xié)程庫(kù)
廣告
我的首款個(gè)人開(kāi)發(fā)的APP壁紙寶貝上線了,歡迎大家下載逆巍。