本文是
RJIterator
作者@rkui
对RJIterator
实现的详细总结。分析了ES7
中async/await
的实现原理,并依此详细说明了在iOS
中实现async/await
的思路。具体的实现细节有很多值得借鉴的地方,欢迎大家一起讨论,也一起来完善这个优秀的作品。
知识小集是一个团队公众号,每周都会有 原创 文章分享,我们的文章都会在公众号首发。欢迎关注查看更多内容。
async/await
是
ES7
提出的异步解决方案。对比回调链和
Promise.then
链的异步编程模式,基于
async/await
我们可以以同步风格编写异步代码,程序逻辑清晰明了。
如顺序读取三个文件:
function readFile(name) {
return new Promise((resolve, reject) => {
//异步读取文件
fs.readFile(name, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
async function read3Files() {
try {
//读取第1个文件
let data1 = await readFile('file1.txt');
//读取第2个文件
let data2 = await readFile('file2.txt');
//读取第3个文件
let data3 = await readFile('file3x.txt');
//3个文件读取完毕
} catch (error) {
//读取出错
}
}
读取文件本身是异步操作,而在要求顺序读取的前提下,基于
callback
实现将造成很深的回调嵌套:
function readFile(name, callback) {
//异步读取文件
fs.readFile(name, (err, data) => {
callback(err, data);
});
}
function read3Files() {
//读取第1个文件
readFile('file1.txt', (err, data) => {
//读取第2个文件
readFile('file2.txt', (err, data) => {
//读取第3个文件
readFile('file3.txt', (err, data) => {
//3个文件读取完毕
});
});
});
}
基于
Promise.then
链需要将逻辑分散在过多的代码块:
function readFile(name) {
return new Promise((resolve, reject) => {
//异步读取文件
fs.readFile(name, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
function read3Files() {
//读取第1个文件
readFile('file1.txt')
.then(data => {
//读取第2个文件
return readFile('file2.txt');
})
.then(data => {
//读取第3个文件
return readFile('file3.txt');
})
.then(data => {
//3个文件读取完毕
})
.catch(error => {
//读取出错
});
}
对比可见,
aync/await
模式的优雅与简洁。接触完毕后,深感如果在
iOS
项目中也能像
JS
这般编写异步代码也是极好。经过研究发现要在
iOS
平台实现这些特性其实并不是很困难,因此本文主旨便是描述
async/await
在
iOS
平台的一次实现过程,并给出了一个成果项目。
暂时继续讨论JavaScript
生成器与迭代器
要明白
async/await
的机制及运用,需从生成器与迭代器逐步说起。在
ES6
中,生成器是一个函数,和普通函数的有以下几个区别:
-
生成器函数
function
关键字后多了个*
:
function *numbers() {}
-
生成器函数内可以
yield
语法多次返回值
function *numbers() {
yield 1;
yield 2;
yield 3;
}
-
直接调用生成器函数得到的是一个迭代器,通过迭代器的
next
方法控制生成器的执行:
let iterator = numbers();
let result = iterator.next();
每一次
next
调用将得到结果
result
,
result
对象包含两个属性:
value
和
done
。
value
表示此次迭代得到的结果值,
done
表示是否迭代结束。比如:
function *numbers() {
yield 1;
yield 2;
yield 3;
}
let iterator = numbers();
//第1次迭代
let result = iterator.next();
console.log(result);
//输出 => { value: 1, done: false }
//第2次迭代
result = iterator.next();
console.log(result);
//输出 => { value: 2, done: false }
//第3次迭代
result = iterator.next();
console.log(result);
//输出 => { value: 3, done: false }
//第4次迭代
result = iterator.next();
console.log(result);
//输出 => { value: undefined, done: true }
第 1 次调用
next
,生成器
numbers
开始执行。执行到第一个
yield
语句时,
numbers
将中断,并将结果值
1
返回给迭代器。由于
numbers
并没有执行完,所以
done
为
false
。
第 2 次调用
next
,生成器
numbers
从上次中断的位置恢复执行,继续执行到下一个
yield
语句时,
numbers
再次中断,并将结果值
2
返回给迭代器,由于
numbers
并没有执行完,所以
done
为
false
。
第 3 次调用
next
,生成器
numbers
从上次中断的位置恢复执行,继续执行到下一个
yield
语句时,
numbers
再次中断,并将结果值
3
返回给迭代器,由于
numbers
并没有执行完,所以
done
为
false
。
第 4 次调用
next
,生成器
numbers
从上次中断的位置恢复执行,此时已是函数尾,
numbers
将直接
return
,由于
numbers
已经执行完成,所以
done
为
true
。由于
numbers
并没有显式地返回任何值,因此此次迭代
value
为
undefined
.
到此迭代结束,此后通过此迭代器的
next
方法,都将得到相同的结果
{ value: undefined, done: true }
。
- 通过迭代器可向生成器内部传值
function *hello() {
let age = yield 'want age';
let name = yield 'want name';
console.log(`Hello, my age: ${age}, name:${name}`);
}
let iterator = hello();
创建迭代器并开始如下迭代过程:
-
第 1 次迭代,生成器开始执行,到达第一个
yield
语句时,返回value = want age, done = false
给迭代器, 并中断。
let result = iterator.next();
console.log(result);
//输出 => { value: 'want age', done: false }
-
第 2 次迭代,给
next
传参28
,生成器从上次中断的地方恢复执行,并将28
作为苏醒后yield
的内部返回值赋给age
;然后生成器继续执行,再次遇到yield
,返回value = want name, done = false
给迭代器,并中断。
result = iterator.next(28);
console.log(result);
//输出 => { value: 'want name', done: false }
-
第 3 次迭代,给
next
传参'LiLei'
,生成器从上次中断的地方恢复执行,并将'LiLei'
作为苏醒后yield
的内部返回值赋给name
;然后生成器继续执行,打印log
。
Hello, my age: 28, name:LiLei
然后到达函数尾,彻底结束生成器,并返回
value = undefined, done = true
给迭代器。
result = iterator.next('LiLei');
console.log(result);
//输出 => { value: undefined, done: true }
可见通过迭代器可与生成器“互相交换数据”,生成器通过
yield
返回数据 A 给迭代器并中断,而通过迭代器又可以把数据 B 传给生成器并让yield
语句苏醒后以 B 作为右值。这个特性是下一步"改进异步编程"的重要基础。
至此已基本了解了生成器与迭代器的语法与运用,总结起来:
- 生成器是一个函数,直接调用得到其对应的迭代器,用以控制生成器的逐步执行;
-
生成器内部通过
yield
语法向迭代器返回值,而且可以多次返回,并多次恢复执行,有别于传统函数"返回便消亡"的特点; - 可以通过迭代器向生成器内部传值,传入的值将作为本次生成器 yield 语句苏醒后的右值;
通过生成器与迭代器改进异步编程
回想本文开头提到的读取文件例子,如果以
callback
模式编写:
function readFile(name, callback) {
//异步读取文件
fs.readFile(name, (err, data) => {
callback(err, data);
});
}
function read3Files() {
//读取第1个文件
readFile('file1.txt', (err, data) => {
//读取第2个文件
readFile('file2.txt', (err, data) => {
//读取第3个文件
readFile('file3.txt', (err, data) => {
//3个文件读取完毕
});
});
});
}
基于前面起到的" 通过迭代器与生成器交换数据 "的特性,拓展出新思路:
-
把读取文件这个动作封装为一个异步操作,通过
callback
输出结果:err
和data
; -
把
read3Files
改变为生成器,内部通过yield
返回异步操作给执行器(执行器第3步描述); -
执行器通过迭代器接收
read3Files
返回的异步操作,拿到异步操作后,发起该异步操作,得到结果后再其“交换”给生成器read3Files
内的yield
即:
function readFile(name) {
//返回一个闭包作为异步操作
return function(callback) {
fs.readFile(name, (err, data) => {
callback(err, data);
});
};
}
//执行器
function executor(generator) {
//创建迭代器
let iterator = generator();
//开始第一次迭代
let result = iterator.next();
let nextStep = function() {
//迭代还没结束
if (!result.done) {
//从生成器拿到的是一个异步操作
if (typeof result.value === "function") {
//发起异步操作
result.value((err, data) => {
if(err) {
//在生成器内部引发异常
iterator.throw(err);
}
else {
//得到结果值,传给生成器
result = iterator.next(data);
//继续下一步迭代
nextStep();
}
});
}
//从生成器拿到的是一个普通对象
else {
//什么都不做,直接传回给生成器
result = iterator.next(result.value);
//继续下一步迭代
nextStep();
}
}
};
//开始后续迭代
nextStep();
}
而
read3Files
改进为:
executor(function *() {
try {
//读取第1个文件
let data1 = yield readFile('file1.txt');
//读取第2个文件
let data2 = yield readFile('file2x.txt');
//读取第3个文件
let data3 = yield readFile('file3.txt');
} catch (e) {
//读取出错
}
});
此时已经把
callback
模式改进为同步模式。
暂且把传给执行器的生成器函数叫做"异步函数",执行过程总结起来就是:
异步函数但凡遇到异步操作,就通过
yield
交给执行器;执行器但凡拿到异步操作,就发起该操作,拿到实际结果后再将其交换给异步函数。那么在异步函数内,就可以同步风格编写异步代码,因为有了执行器在背后运作,异步函数内的
yield
就具有了“你给我异步操作,我还你实际结果”的能力。
Promoise
同样可作为异步操作:
function readFile(name) {
//返回一个Promise作为异步操作
return new Promise((resolve, reject) => {
fs.readFile(name, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
}
在执行器中新增识别
Promise
的代码:
function executor(generator) {
//创建迭代器
let iterator = generator();
//开始第一次迭代
let result = iterator.next();
let nextStep = function() {
//迭代还没结束
if (!result.done) {
if (typeof result.value === "function") {
....
}
//从生成器拿到的是一个Promise异步操作
else if (result.value instanceof Promise) {
//执行该Promise
result.value.then(data => {
//得到结果值,传给生成器
result = iterator.next(data);
//继续下一步迭代
nextStep();
}).catch(err => {
//在生成器内部引发异常
iterator.throw(err);
});
}
else {
...
}
}
};
...
}
到此已经成功把异步编程化为同步风格,但或许有个疑问:这个例子倒是化异步为同步风格了,但是那个执行器
executor
看起来好大一坨,并不优雅。实际上执行器当然是复用的,不用每次都实现执行器。
async/await语法糖
到了
ES7
,
async/await
终于出来。
async/await
是上述执行器,生成器模式的语法糖,运用
async/await
,再也不需要每次都定义生成器作为异步函数,然后显式传给执行器,只要简单在函数定义前增加
async
,表示这是一个异步函数,内部将用
await
来等待异步结果:
async function foo() {
let value = await 异步操作;
let value = await 异步操作;
let value = await 异步操作;
let value = await 异步操作;
}
如读取文件例子:
async function read3Files() {
//读取第1个文件
let data1 = await readFile('file1.txt');
//读取第2个文件
let data2 = await readFile('file2.txt');
//读取第3个文件
let data3 = await readFile('file3x.txt');
//3个文件读取完毕
}
然后直接调用即可:
read3Files();
async
表示该函数内部包含异步操作,需要把它交给内置执行器;
await
表示等待异步操作的实际结果。
至此,
JS
下
async/await
的来龙去脉已基本描述完毕。
回到iOS
光描述
JS
生成器,迭代器,
async/await
就花了大量篇幅,因为在
iOS
上将以它们的
JS
特性为目标,最终实现
OC
版的迭代器,生成器,
async/await
。
类型定义
暂时无需在意怎么实现,既然是以前面描述的特性为目标,则可以根据其特性先做如下定义:
先定义
yield
如下:
id yield(id value);
yield
接受一个对象
value
作为返回给迭代器的值,同时返回一个迭代器设置的新值或者原本值
value
。
每次迭代的
Result
:
@interface Result: NSObject
@property (nonatomic, strong, readonly) id value;
@property (nonatomic, readonly) BOOL done;
@end
value
表示迭代的结果,为
yield
返回的对象,或者
nil
。
done
指示是否迭代结束。
根据前面描述的生成器特性,那么在
OC
里,生成器首先应该是一个
C函数/OC方法/block
,且内部通过调用
yield
来返回结果给迭代器:
void generator() {
yield(value);
yield(value);
}
- (void)generator {
yield(value);
yield(value);
}
^{
yield(value);
yield(value);
}
实际上不论是
OC
方法,还是
block
,底层调用时都与调用
C
函数无异。
只是调用
block
会默认以block
结构体地址作为第一个隐含参数;调用方法会以对象自身self
,和选择器_cmd
作为前两个隐含参数
所以只要实现了
C
函数版生成器,其实现机制将也无缝适用于
OC
方法,
block
。
迭代器定义:
@interface Iterator : NSObject
{
void (*_func)(void);
}
- (id)initWithFunc:(void (*)(void))func;
- (Result *)next;
- (Result *)next:(id)value;
@end
迭代器的创建无法做到像
JS
一样直接调用生成器即可创建,需要显式创建:
void generator() {
yield(value);
yield(value);
}
Iterator *iterator = [[Iterator alloc] initWithFunc: generator];
然后就可以像
JS
一样调用
next
来进行迭代:
Result *result = iterator.next;
//迭代并传值
Result *result = [iterator next: value];
实现生成器与迭代器
根据需求,
yield
调用会中断当前执行流,并期望将来能够从中断处继续恢复执行,那么必定要在触发中断时保存现场,包括:
- 当前指令地址
- 当前寄存器信息,包括当前栈帧栈顶
而且中断后到恢复的这段时间内,应当确保
yield
以及生成器generator
的栈帧不会被销毁。
而恢复执行的过程是保存现场的逆过程,即恢复相关寄存器,并跳转到保存的指令地址处继续执行。
上述过程描述起来看似简单,但是如果要自己写汇编代码去保存与恢复现场,并适配各种平台,要保证稳定性还是很难的,好在有C标准库提供的现成利器:
setjmp/longjmp
。
setjmp/longjmp
可以实现跨函数的远程跳转,对比
goto
只能实现函数内跳转,
setjmp/longjmp
实现远程跳转基于的就是保存现场与恢复现场的机制,非常符合此处的需求。
实现思路
根据前面对生成器,迭代器的定义及需求推敲整理出如下的实现思路:
-
迭代器通过
next
方法与生成器进行交互时,在next
方法内部会将控制流切换到生成器,生成器通过调用yield
设置传给迭代器的返回值,并将执行流切换回到next
方法; -
切回
next
方法后,拿到这个值,正常返回给调用者; -
为了确保
next
方法返回后,生成器的执行栈不被销毁,因此生成器方法的执行需要在一个不被释放的新栈上进行; -
虽然
next
主要通过恢复现场方式切入生成器,但是首次还是需要通过函数调用方式来进入生成器,通过中介wrapper
调用生成器的方式,可以检测到生成器执行结束的事件,然后wrapper
再切回next
方法,并设置done
为YES
,迭代结束。
整个流程图解如下:
乍一看好大一坨,但是只要跟着箭头流程走,思路将很快理清。
根据此思路,为迭代器新增属性如下:
@interface Iterator : NSObject
{
int *_ev_leave; //迭代器在next方法内保存的现场
int *_ev_entry; //生成器通过yield保存的现场
BOOL _ev_entry_valid; //指示生成器现场是否可用
void *_stack; //为生成器新分配的栈
int _stack_size; //为生成器新分配的栈大小
void (*_func)(void);//迭代器函数指针
BOOL _done; //是否迭代结束
id _value; //生成器通过yield传回的值
}
- (id)initWithFunc:(void (*)(void))func;
- (Result *)next;
- (Result *)next:(id)value;
@end
为生成器分配新栈,正如前面所述,在迭代器和生成器的生命周期中,
next
方法的每次迭代是要正常返回的,如果直接在
next
自己的调用栈上调用
wrapper
,
wrapper
再调用生成器,那么
next
返回后,生成器就算保护了寄存器现场,它的栈帧也被破坏了,再次恢复执行将产生无法预料的结果。
//默认为生成器分配256K的执行栈
#define DEFAULT_STACK_SIZE (256 * 1024)
- (id)init {
if (self = [super init]) {
//分配一块内存作为生成器的运行栈
_stack = malloc(DEFAULT_STACK_SIZE);
memset(_stack, 0x00, DEFAULT_STACK_SIZE);
_stack_size = DEFAULT_STACK_SIZE;
//jmp_buf类型来自C标准库<setjmp.h>
_ev_leave = malloc(sizeof(jmp_buf));
memset(_ev_leave, 0x00, sizeof(jmp_buf));
_ev_entry = malloc(sizeof(jmp_buf));
memset(_ev_entry, 0x00, sizeof(jmp_buf));
}
return self;
}
实现
next
方法:
#define JMP_CONTINUE 1//生成器还可被继续迭代
#define JMP_DONE 2//生成器已经执行结束,迭代器应该结束
- (Result *)next:(id)value {
if (_done) {
//迭代器已结束,则每次调用next都返回最后一次结果
return [Result resultWithValue:_value error:_error done:_done];
}
//保存next当前环境
int leave_value = setjmp(_ev_leave);
//非恢复执行
if (leave_value == 0) {
//已经设置了生成器进入点
if (_ev_entry_valid) {
//设置传给生成器内yield的新值
if (value) {
self.value = value;
}
//直接从生成器进入点进入
longjmp(_ev_entry, JMP_CONTINUE);
}
else {
//生成器还没保存过现场,从wrapper进入生成器
//next栈会销毁,所以为wrapper启用新栈
intptr_t sp = (intptr_t)(_stack + _stack_size);
//预留安全空间,防止直接move [sp] 传参 以及msgsend向上访问堆栈
sp -= 256;
//对齐sp
sp &= ~0x07;
//直接修改栈指针sp,指向新栈
#if defined(__arm__)
asm volatile("mov sp, %0" : : "r"(sp));
#elif defined(__arm64__)
asm volatile("mov sp, %0" : : "r"(sp));
#elif defined(__i386__)
asm volatile("movl %0, %%esp" : : "r"(sp));
#elif defined(__x86_64__)
asm volatile("movq %0, %%rsp" : : "r"(sp));
#endif
//在新栈上调用wrapper,至此可以认为wrapper,以及生成器函数的运行栈和next无关
[self wrapper];
}
}
//从生成器内部恢复next
else if (leave_value == JMP_CONTINUE) {
//还可以继续迭代
}
//从生成器wrapper恢复next
else if (leave_value == JMP_DONE) {
//生成器结束,迭代完成
_done = YES;
}
return [RJResult resultWithValue:_value error:_error done:_done];
}
如果没有中介wrapper,那么迭代器返回将会造成崩溃,因为迭代器的运行栈和生成器是分开的,如果生成器内部执行return语句,返回后的栈空间将是未定义的,很有可能造成非法内存访问而崩溃.中介wrapper很好地解决了这个问题:
- (void)wrapper {
//调用生成器函数
if (_func) {
_func();
}
//从生成器返回,说明生成器完全执行结束
self.value = nil;
//恢复next
longjmp(_ev_leave, JMP_DONE);
//不会到此
assert(0);
}
通过中介
wrapper
调用方式进入生成器,生成器最终返回后将正确返回到
wrapper
末尾继续执行,而
wrapper
也就知道,此时生成器结束了,因此以
longjmp
方式恢复
next
的现场,并设置恢复值为
JMP_DONE
,
next
被恢复后拿到这个值就知道生成器执行结束,迭代该结束了。
yield
的实现就更加简单,保存当前现场,将
value
值传递给迭代器对象,然后恢复迭代器next方法即可,而当后续从
next
恢复
yield
的现场后,
yield
再取迭代器设置的新值返回给生成器内部,如此达到生成器与迭代器的数据交换:
id yield(id value) {
//获取当前线程正在获得的生成器
Iterator *iterator = [IteratorStack top];
return [iterator yield: value];
}
- (id)yield:(id)value {
//设置生成器的现场已保护标志
_ev_entry_valid = YES;
//现场保护
if (setjmp(_ev_entry) == 0) {
//现场保护完成
//给迭代器赋值
self.value = value;
//恢复迭代器next现场