阿里开源 iOS 协程开发框架 coobjc源码分析

栏目: IOS · 发布时间: 6年前

内容简介:昨天朋友圈被一篇文章(以下简称“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 源码。 发现目录结构如下:

阿里开源 iOS 协程开发框架 coobjc源码分析
从目录结构看还是比较清晰的,根据 coobjc 介绍文章中提到的, coobjc

不但提供了基础的异步操作还提供了基于UIKit的封装。目录中

  • cokit 及其子目录提供的是基于UIKit层的 coobjc 封装
  • coobjc 目录是 coobjcObjective-C 版实现的源代码
  • coswift 目录是 coobjcSwift 版实现的源代码
  • Example 下有两个目录,一个是 Objective-C 的实现,一个是 Swift 版的实现的Demo

我们先分析一下 coobjcBaseExample 工程: 打开项目, pod update 一下即可运行,运行结果如下:

阿里开源 iOS 协程开发框架 coobjc源码分析

可以看到是个简单的列表页。

Tips 打开podfile可以发现里面有库 coobjc 以外,还有 SpectaExpecta 以及 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];
复制代码

这一行是做了网络请求,但是我们再点击进入类 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

阿里开源 iOS 协程开发框架 coobjc源码分析

查看该文件,发现了这么几个函数:

  • _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)
复制代码

该函数是设置当前的上下文为 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 创建一个协程上下文

以及 ucontext 以及 coobjc 结构体定义:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Understanding Machine Learning

Understanding Machine Learning

Shai Shalev-Shwartz、Shai Ben-David / Cambridge University Press / 2014 / USD 48.51

Machine learning is one of the fastest growing areas of computer science, with far-reaching applications. The aim of this textbook is to introduce machine learning, and the algorithmic paradigms it of......一起来看看 《Understanding Machine Learning》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具