原文地址:kyson.cn/index.php/a…php
昨天朋友圈被一篇文章(如下简称“coobjc介绍文章”)刷屏了:刚刚,阿里开源 iOS 协程开发框架 coobjc!。可能大部分iOS开发者都直接懵逼了:html
所以笔者想给你们普及普及协程的知识,运行一下coobjc
的Example,顺便分析一下coobjc
源码。git
协程的维基百科在这里:协程。引用里面的解释以下:github
协程是计算机程序的一类组件,推广了非抢先多任务的子程序,容许执行被挂起与被恢复。相对子例程而言,协程更为通常和灵活,但在实践中使用没有子例程那样普遍。协程源自Simula和Modula-2语言,但也有其余语言支持。协程更适合于用来实现彼此熟悉的程序组件,如合做式多任务、异常处理、事件循环、迭代器、无限列表和管道。 根据高德纳的说法, 马尔文·康威于1958年发明了术语coroutine并用于构建汇编程序。shell
对,仍是只知其一;不知其二。但最起码咱们了解到编程
coobjc
的含义。那么这个词又是怎么来的呢?笔者再深挖一下,协程(coroutine)顾名思义就是“协做的例程”(co-operative routines)。Objective-C
不支持。笔者通过查阅,发现不少现代语言都支持协程。好比Python以及swift,甚至C语言也是支持协程的。协程的做用其实在coobjc
介绍文章中有说起,是为了优化iOS
中的异步操做。解决了以下问题:json
听起来是有点强大,最明显的好处是能够简化代码;而且在coobjc介绍文章也说道,性能也有所保障:当线程的数量级大于1000以上时,coobjc
的优点就会很是明显。为了证实文章的结论,咱们就来运行一下coobjc
源码好了。 这里下载coobjc
源码。 发现目录结构以下:ubuntu
coobjc
介绍文章中提到的,
coobjc
不但提供了基础的异步操做还提供了基于UIKit的封装。目录中
cokit
及其子目录提供的是基于UIKit层的coobjc
封装coobjc
目录是coobjc
的Objective-C
版实现的源代码coswift
目录是coobjc
的Swift
版实现的源代码Example
下有两个目录,一个是Objective-C
的实现,一个是Swift
版的实现的Demo咱们先分析一下coobjcBaseExample
工程: 打开项目,pod update
一下便可运行,运行结果以下: swift
Tips 打开podfile能够发现里面有库
coobjc
之外,还有Specta
、Expecta
以及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];
复制代码
这一行是作了网络请求,可是咱们再点击进入类DataService
看requestJSONWithURL
方法的实现的时候,发现已经看不懂了:
- (id)requestJSONWithURL:(NSString*)url CO_ASYNC{ SURE_ASYNC return await([self.jsonActor sendMessage:url]); } 复制代码
好吧。既然看不懂了,咱们就从头开始学习,协程的含义以及使用。继而对coobjc
源码进行分析。
coobjc
介绍文章中有提到
glibc
的 ucontext
组件(云风的库)。ucontext
。switch-case
的奇淫技巧来实现(Protothreads)。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_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)
复制代码
该函数是设置当前的上下文为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 | 建立一个协程上下文 |
这么一来,咱们以前的程序能够改写成以下:
#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
的目录结构,其中
core
目录提供了核心的协程函数api
目录是coobjc
基于Objective-C
的封装csp
,目录从库libtask引入,提供了一些链式操做objc
提供了coobjc
对象声明周期管理的一些类 下面的文章,笔者会先从核心的core
目录开始研究,后面的你们理解起来也就不复杂了。上面咱们只简单的介绍了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
是当前正在运行的协程。
协程拥有和线程同样相似的操做,例如建立,启动,出让控制权,恢复,以及死亡。对应的,咱们在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
的核心代码都分析完成了。
咱们再次回到文章开头的例子- (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】: