在iOS平台实现新的异步解决方案async,await

async,await是ES7提出的异步解决方案,对比回调链和Promise.then链的异步编程模式,基于async,await可以同步风格编写异步代码,程序逻辑清晰明了.
如顺序读取三个文件:

读取文件本身是异步操作,而在要求顺序读取的前提下,基于callback实现将造成很深的回调嵌套:

基于Promise.then链需要将逻辑分散在过多的代码块:

对比可见aync,await模式的优雅与简洁。接触完毕后,深感如果在iOS项目中也能像JS这般编写异步代码也是极好。经过研究发现要在iOS平台实现这些特性其实并不是很困难,因此本文主旨便是描述async,await在iOS平台的一次实现过程,并给出了一个成果项目.

暂时继续讨论JavaScript

1.生成器与迭代器

要明白async,await的机制及运用,需从生成器与迭代器逐步说起.在ES6中,生成器是一个函数,和普通函数的区别是:

(1)生成器函数function关键字后多了个*:

(2)生成器函数内可以yield语法多次返回值:

(3)直接调用生成器函数得到的是一个迭代器,通过迭代器的next方法控制生成器的执行:

每一次next调用将得到结果result, result对象包含两个属性:valuedone. value表示此次迭代得到的结果值,done表示是否迭代结束.
比如:

第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 }

(4)通过迭代器可向生成器内部传值

创建迭代器并开始如下迭代过程:

第1次迭代,生成器开始执行,到达第一个yield语句时,返回value = want age, done = false给迭代器, 并中断。

第2次迭代,给next传参28, 生成器从上次中断的地方恢复执行,并将28作为苏醒后yield的内部返回值赋给age; 然后生成器继续执行,再次遇到yield,返回value = want name, done = false给迭代器, 并中断。

第3次迭代,给next传参’LiLei’, 生成器从上次中断的地方恢复执行,并将’LiLei’作为苏醒后yield的内部返回值赋给name; 然后生成器继续执行,打印log:

然后到达函数尾,彻底结束生成器,并返回value = undefined, done = true给迭代器。

可见通过迭代器可与生成器“互相交换数据”,生成器通过yield返回数据A给迭代器并中断,而通过迭代器又可以把数据B传给生成器 并 让yied语句苏醒后以B作为右值. 这个特性是下一步”改进异步编程”的重要基础.

至此已基本了解了生成器与迭代器的语法与运用,总结起来:

###### 生成器是一个函数,直接调用得到其对应的迭代器,用以控制生成器的逐步执行;
###### 生成器内部通过yield语法向迭代器返回值,而且可以多次返回,并多次恢复执行,有别于传统函数”返回便消亡”的特点;
###### 可以通过迭代器向生成器内部传值,传入的值将作为本次生成器yield语句苏醒后的右值.

2.通过生成器与迭代器改进异步编程

回想本文开头提到的读取文件例子,如果以callback模式编写:

基于前面起到的”通过迭代器与生成器交换数据”的特性,拓展出新思路:

(1)把读取文件这个动作封装为一个异步操作,通过callback输出结果:err和data.

(2)把read3Files改变为生成器,内部通过yield返回异步操作给执行器(执行器第3步描述).

(3)执行器通过迭代器接收read3Files返回的异步操作,拿到异步操作后,发起该异步操作,得到结果后再其“交换”给生成器read3Files内的yield.

即:

而read3Files改进为:

此时已经把callback模式改进为同步模式。

暂且把传给执行器的生成器函数叫做”异步函数”,执行过程总结起来就是:

异步函数但凡遇到异步操作,就通过yield交给执行器; 执行器但凡拿到异步操作,就发起该操作,拿到实际结果后再将其交换给异步函数。那么在异步函数内,就可以同步风格编写异步代码,因为有了执行器在背后运作,异步函数内的yield就具有了“你给我异步操作,我还你实际结果”的能力.

Promoise同样可作为异步操作:

在执行器中新增识别Promise的代码

到此已经成功把异步编程化为同步风格,但或许有个疑问:这个例子倒是化异步为同步风格了,但是那个执行器executor看起来好大一坨,并不优雅.实际上执行器当然是复用的,不用每次都实现执行器.

3.async,await语法糖

到了ES7,async,await终于出来.async与await是上述执行器,生成器模式的语法糖,运用async,await,再也不需要每次都定义生成器作为异步函数,然后显式传给执行器,只要简单在函数定义前增加async,表示这是一个异步函数,内部将用await来等待异步结果:

如读取文件例子:

然后直接调用即可:

async表示该函数内部包含异步操作,需要把它交给内置执行器;

await表示等待异步操作的实际结果。

至此,JS下async/await的来龙去脉已基本描述完毕.

回到iOS

光描述JS生成器,迭代器,async,await就花了大量篇幅,因为在iOS上将以它们的JS特性为目标,最终实现OC版的迭代器,生成器,async,await.

1.类型定义

暂时无需在意怎么实现,既然是以前面描述的特性为目标,则可以根据其特性先做如下定义:

先定义yield如下:

yield接受一个对象value作为返回给迭代器的值,同时返回一个迭代器设置的新值或者原本值value.

每次迭代的Result:

value表示迭代的结果,为yield返回的对象,或者nil. done指示是否迭代结束.

根据前面描述的生成器特性,那么在OC里,生成器首先应该是一个C函数/OC方法/block,且内部通过调用yield来返回结果给迭代器:

实际上不论是OC方法,还是block,底层调用时都与调用C函数无异.

所以只要实现了C函数版生成器,其实现机制将也无缝适用于OC方法,block.

迭代器定义:

迭代器的创建无法做到像JS一样直接调用生成器即可创建,需要显式创建:

然后就可以像JS一样调用next来进行迭代:

2.实现生成器与迭代器

根据需求,yield调用会中断当前执行流,并期望将来能够从中断处继续恢复执行,那么必定要在触发中断时保存现场,包括:

(1)当前指令地址

(2)当前寄存器信息,包括当前栈帧栈顶

而且 中断后到恢复的这段时间内,应当确保yield以及生成器generator的栈帧不会被销毁.

而恢复执行的过程是保存现场的逆过程,即恢复相关寄存器,并跳转到保存的指令地址处继续执行.

上述过程描述起来看似简单,但是如果要自己写汇编代码去保存与恢复现场,并适配各种平台,要保证稳定性还是很难的,好在有C标准库提供的现成利器:setjmp/longjmp。

setjmp/longjmp可以实现跨函数的远程跳转,对比goto只能实现函数内跳转,setjmp/longjmp实现远程跳转基于的就是保存现场与恢复现场的机制,非常符合此处的需求.

实现思路

根据前面对生成器,迭代器的定义及需求推敲整理出如下的实现思路:

(1) 迭代器通过next方法与生成器进行交互时,在next方法内部会将控制流切换到生成器,生成器通过调用yield设置传给迭代器的返回值,并将执行流切换回到next方法。

(2) 切回next方法后,拿到这个值,正常返回给调用者。

(3) 为了确保next方法返回后,生成器的执行栈不被销毁,因此生成器方法的执行需要在一个不被释放的新栈上进行。

(4) 虽然next主要通过恢复现场方式切入生成器,但是首次还是需要通过函数调用方式来进入生成器,通过中介wrapper调用生成器的方式,可以检测到生成器执行结束的事件,然后wrapper再切回 next 方法,并设置done为YES,迭代结束.

整个流程图解如下:

乍一看好大一坨,但是只要跟着箭头流程走,思路将很快理清。

根据此思路,为迭代器新增属性如下:

为生成器分配新栈,正如前面所述,在迭代器和生成器的生命周期中,next方法的每次迭代是要正常返回的,如果直接在next自己的调用栈上调用wrapper,wrapper再调用生成器,那么next返回后,生成器就算保护了寄存器现场,它的栈帧也被破坏了,再次恢复执行将产生无法预料的结果.

实现next方法:

如果没有中介wrapper,那么迭代器返回将会造成崩溃,因为迭代器的运行栈和生成器是分开的,如果生成器内部执行return语句,返回后的栈空间将是未定义的,很有可能造成非法内存访问而崩溃.中介wrapper很好地解决了这个问题:

通过中介wrapper调用方式进入生成器,生成器最终返回后将正确返回到wrapper末尾继续执行,而wrapper也就知道,此时生成器结束了,因此以longjmp方式恢复next的现场,并设置恢复值为JMP_DONE,next被恢复后拿到这个值就知道生成器执行结束,迭代该结束了.

yield的实现就更加简单,保存当前现场,将value值传递给迭代器对象,然后恢复迭代器next方法即可,而当后续从next恢复yield的现场后,yield再取迭代器设置的新值返回给生成器内部,如此达到生成器与迭代器的数据交换:

这里的IteratorStack是一个线程本地存储的栈,栈顶永远是当前线程正在活动的迭代器,具体实现可以参考后边给出的结果项目。

至此已经实现了c函数版本的生成器,简单改变即可扩展到OC方法,block.首先是迭代器需要支持新的初始化方法:

wrapper支持新的生成器调用方式:

3.通过生成器与迭代器改进异步编程

正如前面描述的JS下的改进方法,现在可以用实现的生成器与迭代器来改进iOS的异步编程,且思路一模一样.

首先定义异步操作为如下闭包:

跟JS下的定义一样,这种闭包内部可进行任何异步调用,最终以callback输出error和value即可.

同时PromiseKit提供的AnyPromise也可以作为异步操作.

iOS 版本readFile:

执行器executor:

有了执行器executor,那么顺去读取文件的例子在iOS下可如下实现:

4.更好听的名字: async,await

将上一步实现的的执行器executor改名为async, 新增await函数:

其实await本质上就是yield.那么读取文件的例子就写成:

至此便在iOS平台实现了async,await,且通过async,await可以化异步编程为同步风格.单靠短短字面描述无法面面俱到,比如setjmp,longjmp的原理及使用,函数调用过程与栈的联系,如有生疏要额外研究. 本文旨在描述在iOS平台上的一次对async,await的实现历程,可以通过下面的项目查看完整实现代码.

成果项目

RJIterator https://github.com/renjinkui2719/RJIterator是我根据本文描述的思路实现的迭代器,生成器,yield,async,await的完整项目,欢迎交流与探讨.

本文转自renjinkui2719


丶伊眸冷

静水流深,沧笙踏歌;三生阴晴圆缺,一朝悲欢离合。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

我不是机器人*