背景
昨天朋友圈被一篇文章(以下简称“coobjc介绍文章”)刷屏了:刚刚,阿里开源 iOS 协程开发框架 coobjc!。可能大部分iOS开发者都直接懵逼了:
- 什么是协程?
- 协程的作用是什么?
- 为什么要使用它?
因此笔者想给大家普及普及协程的知识,运行一下coobjc
的Example,顺便分析一下coobjc
源码。
分析
协程的维基百科在这里:协程。引用里面的解释如下:
协程是计算机程序的一类组件,推广了非抢先多任务的子程序,允许执行被挂起与被恢复。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自Simula和Modula-2语言,但也有其他语言支持。协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务、异常处理、事件循环、迭代器、无限列表和管道。 根据高德纳的说法, 马尔文·康威于1958年发明了术语coroutine并用于构建汇编程序。
对,还是一知半解。但最起码我们了解到
- 协程的英文是“coroutine”,因此我们能理解阿里的库起名为
coobjc
的含义。那么这个词又是怎么来的呢?笔者再深挖一下,协程(coroutine)顾名思义就是“协作的例程”(co-operative routines)。 - 协程是和进程或者线程有一定关系的
- 协程的历史还是比较悠久的,只是
Objective-C
不支持。笔者经过查阅,发现很多现代语言都支持协程。比如Python以及swift,甚至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
对应的方法中。 我们继续讨论上文提到的几个函数,并说明一下其作用:
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
其实就存放一些必要的数据,这些数据还包括拯救成功或者失败的情况需要的数据。
接下来说另外一个函数
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 | 创建一个协程上下文 |
以及ucontext
以及coobjc
结构体定义: