ios中你不知的promise设计模式,教你如何正确避免跳进回调陷阱。
作者 | johnssong,移动客户端开发工程师
本文由“135编辑器”提供技术支持
做iOS开发的同学都非常熟悉代理模式,为避免代码耦合,代理模式的委托者任务交给代理执行,代理执行完毕之后再把回调告诉委托者。委托者不关心代理是怎么执行任务的,只关心结果是成功还是失败。代理模式就像是杀手与雇主的关系一样。
但是代理模式也不完美,代理多了,雇主也管不过来了,委托在A处,收结果却要在B处。有的时候,雇主也希望能在同一个地方既可以发配任务,也可以接收结果。闭包Block就能帮雇主解决这个问题了。无论是系统的GCD,还是平时随手封装一个UIAlertView的block实现,都让代码的可读性有了一定的提升。
无论是代理模式,还是闭包,在处理单一任务的时候,都出色的完成了任务。可是当两种模式要相互配合,一起完成一系列任务,并且每个任务之间还要共享信息,相互衔接,雇主就要头疼了。当然可以只用一种模式来实现,代理模式就不说了,过于分散,不善于处理这种流程性的事务。那我用闭包来举一个例子:我们需要顺序执行Task A、B、C 三个任务,A、B、C依次执行,任务完成之后都使用闭包来回调并开始下一个任务。代码如下:
上面的代码看起来挺清晰,可读性也还可以。如果加上一些ifelse的分支判断,再加上一些参数的传递,代码不知不觉的向右延伸,最终超出了屏幕的宽度,形成一个倒金字塔的形状。
写JavaScript 的同学会说:“你已经掉进了回调陷阱(CallbackHell),赶紧用Promise设计模式来跳坑吧。”
Promise 设计模式的原理
Promise设计模式把每一个异步操作都封装成一个Promise对象,这个Promise对象就是这个异步操作执行完毕的结果,但是这个结果是可变的,就像薛定谔的猫,只有执行了才知道。通过这种方式,就能提前获取到结果,并处理下一步骤。
Promise使用then作为关键字,回调最终结果。then是整个Promise设计模式的核心,必须要被实现。另外还有其它几个关键字用来表示一个Promise对象的状态:
pending:任务执行中,状态可能进入下面的fullfill或reject两者之一
ufill/resolved:任务完成了,返回结果
reject:任务失败,并返回错误
如上图所示,fullfill与reject的状态都是不可逆转的,保证了结果的唯一性。
除了then,一些对Promeise的实现还有几个关键字用来扩展,让代码可读性更强:
catch:任务失败,处理error
finally:无论是遇到then还是catch分支最终都会执行回调
when:多个异步任务执行完毕之后才会回调
Promise模式的实现
Promise设计模式在IOS/MacOS平台的最佳实践是由大名鼎鼎的homebrew的作者 Max Howell 写的一个支持iOS/MacOS 的异步编程框架 –PromiseKit, 作者的另一个广为人知的趣事是因为没有写出反转二叉树而没有拿到Google的offer。
我们先抛出对上面改良函数使用PromiseKit的实现,再看原理:
调试后,发现执行的结果与我们期待的一致,但是上面的代码对我来说有几个疑惑点:
then 是怎么串起来的;
怎么实现的顺序调用;
如果传递参数,参数是怎么传递的。
带着问题,来看Promise的源码:
如果对block不是很熟悉,可能不太理解这段代码,实际上,PromiseKit灵活的使用了block作为函数的返回值来实现链式调用。相比原来的block嵌套模式,PromiseKit使用block将多个then串联起来,解决了callback hell。
接着来继续看下一个问题。
- (id)resolved:(PMKResolveOnQueueBlock(^)(idresult))mkresolvedCallback pending:(void(^)(idresult, PMKPromise *next,dispatch_queue_tq,idblock,void(^resolver)(id)))mkpendingCallback{ __block PMKResolveOnQueueBlock callBlock; __blockidresult;
dispatch_sync(_promiseQueue, ^{
if((result = _result))
return; callBlock = ^(dispatch_queue_tq,idblock) { block = [blockcopy]; __block PMKPromise *next =nil; dispatch_barrier_sync(_promiseQueue, ^{
if((result = _result))
return; __block PMKPromiseFulfiller resolver; next = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) { resolver = ^(ido){
if(IsError(o)) reject(o);elsefulfill(o); }; }]; [_handlers addObject:^(idvalue){ mkpendingCallback(value, next, q, block, resolver); }]; });
returnnext ?: mkresolvedCallback(result)(q, block); }; });
returncallBlock ?: mkresolvedCallback(result);}
代码有点长,不过也可以理解。这个方法是上面的thenon调用的,接受两个参数,第一个参数是一个resolve的block,第二个参数是一个pending的block。
一个Promise在执行完毕之后,无论状态是变成resolve还是pending,都通过这个方法,执行对应的 then,并返回一个Promise对象。在上面的函数中,有一个是dispatchbarriersync ,其中barrier是栅栏的意思,一般来说如果我们有多个异步任务,但是希望他们按照一定的顺序执行,就可以使用这个方法。
在这里PromiseKit通过barrier实现了then的依次调用。在这个barrier方法内部,一个是会去看当前是否已经有下一个要执行的Promise,如果没有就生成一个新的,另一个把对应的pending 放到handler队列,依次执行。
参数传递
这里需要思考的另外一个问题是,既然多个任务之间有依次调用的关系,那么这样的一种任务流之间如何互相通信呢?PromiseKit用了一个比较有趣的办法来实现相邻Promise对象的参数传递。
在万物皆消息的OC语言内部,每一个方法,包括Block在内都是有类型签名的。这个类型签名对象是NSMethodSignature。
@interfaceNSMethodSignature:NSObject{...
@property(readonly)NSUIntegernumberOfArguments;...
@property(readonly)constchar*methodReturnTypeNS_RETURNS_INNER_POINTER;...
@end
那么对于block,怎么获取类型签名呢? PromiseKit自己定义了一个block的结构体:
structPMKBlockLiteral {
void*isa;intflags;
intreserved;
void(*invoke)(void*, ...);
structblock_descriptor {
unsignedlongintreserved;// NULLunsignedlongintsize;// sizeof(struct Block_literal_1)void(*copy_helper)(void*dst,void*src);// IFF (1<<25)void(*dispose_helper)(void*src);// IFF (1<<25)constchar*signature;// IFF (1<<30)} *descriptor;};
熟悉block的同学都知道,flags按照bit位保存了一些block的附加信息,在1<<30的这个bit可以找到是否有类型签名signature,剩下的就是通过flags移动指针,找到signature所在的内存空间了。找到了signature,也就获取到了参数个数与函数返回值这些信息,函数返回值的类型是经过编码的。
idpmk_safely_call_block(idfrock,idresult) {
NSMethodSignature*sig =NSMethodSignatureForBlock(frock);
constNSUIntegernargs = sig.numberOfArguments;
constcharrtype = sig.methodReturnType[0]; type (^block)(id,id,id) = frock;return[resultclass] == [PMKArrayclass] ? block(result[0], result[1], result[2]) : block(result,nil,nil); }
有了函数签名,就能知道block的信息了。上面只截取了部分代码,简单来说,PromiseKit通过动态的获取block的参数个数与返回类型来决定block的调用。一般来说,fullfill(id)在调用的时候最多只支持传递一个参数,在必要的时候,PromiseKit把这些参数放在一个数组里面,这个数组就是PMKArray,当检测到这个参数是一个数组的时候,就依次取出数组内的元素作为参数传递,从而支持了多个参数的传递。
总结
至此, 对PromiseKit的一些解释也就结束了,PromiseKit有OC的1.0版本,也有支持了swift的3.0版本。如果你非常享受这样的书写方式,可以接入很多扩展的版本,可以写出看起来优雅又舒服的代码,比如NSURLSession:
URLSession.GET("http://example.com").asDictionary().then{ jsonin
}.catch{ errorin//…
}
还有很多的扩展与关键字的支持,这里也不再展开了。
而对于我来说,Promise设计模式能够解决我对散落在各处的代理模式产生的代码烦恼,也让我避免了跳进回调陷阱,就值得总结了。
— END —
温馨提醒:
1、微信端搜索课程
在“腾讯课堂”官方微信里,回复你想学习的内容,即可快速找到你期待的课程哦!
2、学习方式
【电脑端】
*可通过登录ke.qq.com进入学习;
*可通过windows PC版QQ客户端面板上的课堂入口进入学习。
【移动端】
*下载APP “腾讯课堂” 即可进入学习;
*关注微信公众号或者手Q公众号“腾讯课堂”,进入学习。
(注:微信和QQ的课程报名信息独立,登录时请选择对应的登录方式)
网友评论