阿里開源 iOS 協程開發框架 coobjc源碼分析

原文地址:kyson.cn/index.php/a…php

背景

昨天朋友圈被一篇文章(如下簡稱「coobjc介紹文章」)刷屏了:剛剛,阿里開源 iOS 協程開發框架 coobjc!。可能大部分iOS開發者都直接懵逼了:html

  • 什麼是協程?
  • 協程的做用是什麼?
  • 爲何要使用它?

所以筆者想給你們普及普及協程的知識,運行一下coobjc的Example,順便分析一下coobjc源碼。git

分析

協程的維基百科在這裏:協程。引用裏面的解釋以下:github

協程是計算機程序的一類組件,推廣了非搶先多任務的子程序,容許執行被掛起與被恢復。相對子例程而言,協程更爲通常和靈活,但在實踐中使用沒有子例程那樣普遍。協程源自Simula和Modula-2語言,但也有其餘語言支持。協程更適合於用來實現彼此熟悉的程序組件,如合做式多任務、異常處理、事件循環、迭代器、無限列表和管道。 根據高德納的說法, 馬爾文·康威於1958年發明了術語coroutine並用於構建彙編程序。shell

對,仍是隻知其一;不知其二。但最起碼咱們瞭解到編程

  • 協程的英文是「coroutine」,所以咱們能理解阿里的庫起名爲coobjc的含義。那麼這個詞又是怎麼來的呢?筆者再深挖一下,協程(coroutine)顧名思義就是「協做的例程」(co-operative routines)。
  • 協程是和進程或者線程有必定關係的
  • 協程的歷史仍是比較悠久的,只是Objective-C不支持。筆者通過查閱,發現不少現代語言都支持協程。好比Python以及swift,甚至C語言也是支持協程的。

協程的做用其實在coobjc介紹文章中有說起,是爲了優化iOS中的異步操做。解決了以下問題:json

  • "嵌套地獄"
  • 錯誤處理複雜和冗長
  • 容易忘記調用 completion handler
  • 條件執行變得很困難
  • 從互相獨立的調用中組合返回結果變得極其困難
  • 在錯誤的線程中繼續執行
  • 難以定位緣由的多線程崩潰
  • 鎖和信號量濫用帶來的卡頓、卡死

聽起來是有點強大,最明顯的好處是能夠簡化代碼;而且在coobjc介紹文章也說道,性能也有所保障:當線程的數量級大於1000以上時,coobjc的優點就會很是明顯。爲了證實文章的結論,咱們就來運行一下coobjc源碼好了。 這裏下載coobjc源碼。 發現目錄結構以下:ubuntu

coobjc目錄結構
從目錄結構看仍是比較清晰的,根據 coobjc介紹文章中提到的, coobjc不但提供了基礎的異步操做還提供了基於UIKit的封裝。目錄中

  • cokit 及其子目錄提供的是基於UIKit層的coobjc封裝
  • coobjc目錄是coobjcObjective-C版實現的源代碼
  • coswift目錄是coobjcSwift版實現的源代碼
  • Example 下有兩個目錄,一個是Objective-C的實現,一個是Swift版的實現的Demo

咱們先分析一下coobjcBaseExample工程: 打開項目,pod update一下便可運行,運行結果以下: swift

運行結果
能夠看到是個簡單的列表頁。

Tips 打開podfile能夠發現裏面有庫coobjc之外,還有SpectaExpecta以及OCMock。這三個庫這裏很少作介紹了,你們只須要知道這是用於單元測試的。api

咱們先看一下這個列表的實現邏輯是什麼樣的。咱們不難定位到頁面位於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];
複製代碼

這一行是作了網絡請求,可是咱們再點擊進入類DataServicerequestJSONWithURL方法的實現的時候,發現已經看不懂了:

- (id)requestJSONWithURL:(NSString*)url CO_ASYNC{
    SURE_ASYNC
    return await([self.jsonActor sendMessage:url]);
}
複製代碼

好吧。既然看不懂了,咱們就從頭開始學習,協程的含義以及使用。繼而對coobjc源碼進行分析。

協程入門

coobjc介紹文章中有提到

  • 第一種:利用glibcucontext組件(雲風的庫)。
  • 第二種:使用匯編代碼來切換上下文(實現C協程),原理同ucontext
  • 第三種:利用C語言語法switch-case的奇淫技巧來實現(Protothreads)。
  • 第四種:利用了 C 語言的 setjmplongjmp
  • 第五種:利用編譯器支持語法糖。

通過篩選最終選擇了第二種。那咱們來一個個分析,爲何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,其實就是模擬的上面例子中的setcontextgetcontext等函數。爲了證實筆者的猜測,筆者打開了coobjc源碼庫,發現裏面的惟一的彙編文件coroutine_context.s

coroutine_context文件
查看該文件,發現了這麼幾個函數:

  • _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);
複製代碼

接下來講另一個函數

int  setcontext(const ucontext_t *cut)
複製代碼

該函數是設置當前的上下文爲cutsetcontext的上下文cut應該經過getcontext或者makecontext取得,若是調用成功則不返回。若是上下文是經過調用getcontext()取得,程序會繼續執行這個調用。若是上下文是經過調用makecontext取得,程序會調用makecontext函數的第二個參數指向的函數,若是func函數返回,則恢復makecontext第一個參數指向的上下文第一個參數指向的上下文context_t中指向的uc_link.若是uc_link爲NULL,則線程退出。

咱們畫個表類比一下ucontextcoobjc的函數:

ucontext coobjc 含義
setcontext coroutine_setcontext 設置協程上下文
getcontext coroutine_getcontext 獲取協程上下文
makecontext coroutine_create 建立一個協程上下文

這麼一來,咱們以前的程序能夠改寫成以下:

#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」。

深刻協程

(1)目錄分析

目錄結構
上圖是 coobjc的目錄結構,其中

  • core目錄提供了核心的協程函數
  • api目錄是coobjc基於Objective-C的封裝
  • csp,目錄從庫libtask引入,提供了一些鏈式操做
  • objc提供了coobjc對象聲明週期管理的一些類 下面的文章,筆者會先從核心的core目錄開始研究,後面的你們理解起來也就不復雜了。

(2)協程的構成

上面咱們只簡單的介紹了coobjc,也瞭解到coobjc基本都是參考了ucontext。那下面的例子中,筆者儘量先介紹ucontext,而後再應用到coobjc對應的方法中。 咱們繼續討論上文提到的幾個函數,並說明一下其做用:

int  getcontext(ucontext_t *uctp)
複製代碼

這個方法是,獲取當前上下文,並將上下文設置到uctp中,uctp是個上下文結構體,其定義以下:

_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的數據結構,其內部的幾個屬性介紹一下: 噹噹前上下文(如使用makecontext建立的上下文)運行終止時系統會恢復uc_link指向的上下文;uc_sigmask爲該上下文中的阻塞信號集合;uc_stack爲該上下文中使用的棧;uc_mcontext保存的上下文的特定機器表示,包括調用線程的特定寄存器等。其實還蠻好理解的,ucontext其實就存放一些必要的數據,這些數據還包括拯救成功或者失敗的狀況須要的數據。

相比較而言,coobjc的定義和ucontext有必定區別:

/**
     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;
複製代碼

代表其是一個鏈表結構。 既然是鏈表,那麼就會有添加元素,以及刪除某個元素的方法,果真咱們在coroutine.m中發現了對應的鏈表操做方法:

// 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是爲了標識鏈表的頭尾節點:

/**
 Define the linked list of scheduler's queue. */ struct coroutine_list { coroutine_t *head; coroutine_t *tail; }; typedef struct coroutine_list coroutine_list_t; 複製代碼

爲了管理全部的協程狀態,還設置了一個調度器:

/**
 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中包含了主協程(多是即將設置數據的協程,或者即將使用的協程);running_coroutine是當前正在運行的協程。

(3)協程的操做

協程擁有和線程同樣相似的操做,例如建立,啓動,出讓控制權,恢復,以及死亡。對應的,咱們在coroutine.h看到了以下的幾個函數聲明:

//關閉一個協程若是它已經死亡
void coroutine_close_ifdead(coroutine_t *co);
//添加協程到調度器,而且馬上啓動
void coroutine_resume(coroutine_t *co);
//添加協程到調度器
void coroutine_add(coroutine_t *co);
//出讓控制權
void coroutine_yield(coroutine_t *co);
複製代碼

爲了更好的控制各個操做中的數據,coobjc還提供瞭如下兩個方法:

void coroutine_setuserdata(coroutine_t *co, void *userdata, coroutine_func userdata_dispose);
void *coroutine_getuserdata(coroutine_t *co);
複製代碼

至此,coobjc的核心代碼都分析完成了。

(4)協程的Objective-C層面的封裝

咱們再次回到文章開頭的例子- (void)requestMovies方法的實現中,第一步就是調用一個co_launch()的方法,這個方法最終會調用到

+ (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);
    }];
}
複製代碼

這兩個方法。其實代碼已經很容易理解了,第一個方法是建立一個協程,第二個是啓動。 最後咱們在說一下文章開頭提到的await方法,其實最終就交給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;
}
複製代碼

全部的操做雖然丟到了同一個線程中,但其實最終是經過chan來調度了。關於chan就不在本文討論範圍了,後面若是有時間,筆者會再進行對chan的分析。

總結

本文介紹了協程的概念,經過對比ucontext以及coobjc來講明協程的用法,並分析了coobjc的源代碼,但願對你們有所幫助。

廣告

爲了和你們更好的交流,小人建立了一個微信羣。掃描本人二維碼便可拉你們入羣,入羣請備註【iOS】:

微信

擴展閱讀

iOS單元測試:Specta + Expecta + OCMock + OHHTTPStubs + KIF

我所理解的ucontext族函數

一個「蠅量級」 C 語言協程庫

協程(Coroutine)並非真正的多線程

ucontext-人人均可以實現的簡單協程庫

相關文章
相關標籤/搜索