iOS 中的 Promise 设计模式

做iOS开发的同学都非常熟悉代理模式,为避免代码耦合,代理模式的委托者任务交给代理执行,代理执行完毕之后再把回调告诉委托者。委托者不关心代理是怎么执行任务的,只关心结果是成功还是失败。代理模式就像是杀手与雇主的关系一样。

但是代理模式也不完美,代理多了,雇主也管不过来了,委托在A处,收结果却要在B处。有的时候,雇主也希望能在同一个地方既可以发配任务,也可以接收结果。闭包Block就能帮雇主解决这个问题了。无论是系统的GCD,还是平时随手封装一个 UIAlertView 的block实现,都让代码的可读性有了一定的提升。

无论是代理模式,还是闭包,在处理单一任务的时候,都出色的完成了任务。可是当两种模式要相互配合,一起完成一系列任务,并且每个任务之间还要共享信息,相互衔接,雇主就要头疼了。当然可以只用一种模式来实现,代理模式就不说了,过于分散,不善于处理这种流程性的事务。那我用闭包来举一个例子:我们需要顺序执行Task A、B、C 三个任务,A、B、C依次执行,任务完成之后都使用闭包来回调并开始下一个任务。代码如下:

- (void)callbackHell
{
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
     [self doTaskA:^{
         [self doTaskB:^{
            [self doTaskC:^{
               // all task done
            }];
         }];
     }];
 });
}

上面的代码看起来挺清晰,可读性也还可。如果加上一些 ifelse 的分支判断,再加上一些参数的传递,代码不知不觉的向右延伸,最终超出了屏幕的宽度,形成一个倒金字塔的形状。写 JavaScript 的同学会说:你已经掉进了回调陷阱(CallbackHell),赶紧用Promise设计模式来跳坑吧。

Promise 设计模式的原理

Promise设计模式把每一个异步操作都封装成一个Promise对象,这个Promise对象就是这个异步操作执行完毕的结果,但是这个结果是可变的,就像薛定谔的猫,只有执行了才知道。通过这种方式,就能提前获取到结果,并处理下一步骤。

Promise 使用 then 作为关键字,回调最终结果。 then 是整个Promise设计模式的核心,必须要被实现。另外还有其它几个关键字用来表示一个Promise对象的状态:

  • pending: 任务执行中,状态可能会进入下面的fullfill或者reject二者之一
  • fufill/resolved: 任务完成了,返回结果
  • reject: 任务失败,并返回错误

更多可以参考 官方规范(https://promisesaplus.com/ ) 。

如上图所示,fullfill与reject的状态都是不可逆转的,保证了结果的唯一性。

除了 then ,一些对 Promise 的实现还有几个关键字用来扩展,让代码可读性更强:

  • catch: 任务失败,处理error
  • finally: 无论是遇到 then 还是 catch 分支,最终都会执行的回调
  • when: 多个异步任务执行完毕之后才会回调

Promise模式的实现

Promise设计模式在 iOS/MacOS 平台的最佳实践是由大名鼎鼎的homebrew的作者 Max Howell 写的一个支持iOS/MacOS 的异步编程框架 – PromiseKit , 作者的另一个广为人知的趣事是因为没有写出反转二叉树而没有拿到Google的offer。

我们先抛出对上面改良函数使用PromiseKit的实现,再看原理:

- (void)jumpOutCallbackHell
{
    [self promiseTaskA].then(^{
        return [self promiseTaskB];
    }).then(^{
        return [self promiseTaskC];
    }).then(^{
        NSLog(@"all task done");
    });
}

调试后,发现执行的结果与我们期待的一致,但是上面的代码对我来说有几个疑惑点:

  1. then 是怎么串起来的;
  2. 怎么实现的顺序调用;
  3. 如果传递参数,参数是怎么传递的。

带着问题,来看Promise的源码:

- (PMKPromise *(^)(id))then {
    return ^(id block){
        return self.thenOn(dispatch_get_main_queue(), block);
    };
}

如果对Block不是很熟悉,可能不太理解这段代码,实际上,PromiseKit灵活的使用了Block作为函数的返回值来实现链式调用。相比原来的Block嵌套模式,PromiseKit使用Block将多个 then 串联起来,解决了Callback Hell。

接着来继续看下一个问题。

- (id)resolved:(PMKResolveOnQueueBlock(^)(id result))mkresolvedCallback
       pending:(void(^)(id result, PMKPromise *next, dispatch_queue_t q, id block, void (^resolver)(id)))mkpendingCallback
{
    __block PMKResolveOnQueueBlock callBlock;
    __block id result;

    dispatch_sync(_promiseQueue, ^{
        if ((result = _result))
            return;

        callBlock = ^(dispatch_queue_t q, id block) {

            block = [block copy];

            __block PMKPromise *next = nil;

            dispatch_barrier_sync(_promiseQueue, ^{
                if ((result = _result))
                    return;

                __block PMKPromiseFulfiller resolver;
                next = [PMKPromise new:^(PMKPromiseFulfiller fulfill, PMKPromiseRejecter reject) {
                    resolver = ^(id o){
                        if (IsError(o)) reject(o); else fulfill(o);
                    };
                }];
                [_handlers addObject:^(id value){
                    mkpendingCallback(value, next, q, block, resolver);
                }];
            });

             return next ?: mkresolvedCallback(result)(q, block);
        };
    });

     return callBlock ?: 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

@interface NSMethodSignature : NSObject {
...
@property (readonly) NSUInteger numberOfArguments;
...
@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
...
@end

那么对于block,怎么获取类型签名呢?PromiseKit自己定义了一个block的结构体:

struct PMKBlockLiteral {
    void *isa; 
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct block_descriptor {
      unsigned long int reserved;       // NULL
        unsigned long int size;         // sizeof(struct Block_literal_1)
        void (*copy_helper)(void *dst, void *src);     // IFF (1<<25)
        void (*dispose_helper)(void *src);             // IFF (1<<25)
      const char *signature;                       // IFF (1<<30)
    } *descriptor;
};

熟悉block的同学都知道,flags按照bit位保存了一些block的附加信息,在 1<<30的这个bit可以找到是否有类型签名signature,剩下的就是通过flags移动指针,找到signature所在的内存空间了。找到了signature,也就获取到了参数个数与函数返回值这些信息。函数返回值的类型是经过编码的,具体的对照表可以参考官方文档(https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html )。

id pmk_safely_call_block(id frock, id result) {
    NSMethodSignature *sig = NSMethodSignatureForBlock(frock);
    const NSUInteger nargs = sig.numberOfArguments;
    const char rtype = sig.methodReturnType[0];
    type (^block)(id, id, id) = frock; 
    return [result class] == [PMKArray class] 
               ? 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 { json in

}.catch { error in
    //…
}

还有很多的扩展与关键字的支持,这里都不再展开。

而对于我来说,Promise设计模式能够解决我对散落在各处的代理模式产生的代码的烦恼,也让我避免了跳进回调陷阱,就值得总结了。

内容转载自腾讯课堂 Coding 学院

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏程序员互动联盟

【答疑释惑】标准C语言如何操作文件?

C语言中操作文件功能都用ANSI C提供的一组标准库函数来实现。文件操作标准库函数有如下: fprintf:往文件中写格式化数据 fscanf:格式化读取文件中...

3359
来自专栏IT技术精选文摘

来自1000多个项目的10大JavaScript错误浅析

出于可读性方面的考虑,每个错误的描述经过精简。 1.Uncaught TypeError: Cannot read property 如果你是一名JavaScr...

3478
来自专栏大内老A

.NET Core采用的全新配置系统[3]: “Options模式”下的配置是如何绑定为Options对象

配置的原子结构就是单纯的键值对,并且键和值都是字符串,但是在真正的项目开发中我们一般不会单纯地以键值对的形式来使用配置。值得推荐的做法就是采用《.NET Cor...

17710
来自专栏帅小子的日常

spirng底层实现原理

3418
来自专栏PHP在线

了解这些PHP小技巧吗?

1. $_POST并非是HTTP POST过来的数据, 如json格式的数据就没法接受,这是因为由于历史原因,php只能解析Content-Type为 appl...

2585
来自专栏从流域到海域

《笨办法学Python》 第20课手记

《笨办法学Python》 第20课手记 本节课讲函数与文件,内容比较简单,但请注意常见问题解答,你应该记住那些内容。 指针表示存储地址。 原代码如下: from...

1906
来自专栏软件开发 -- 分享 互助 成长

C++ STL之迭代器注意事项

1、两个迭代器组成的区间是前闭后开的 2、如果迭代器的有效性,如果迭代器所指向的元素已经被删除,那么迭代器会失效 http://blog.csdn.net/hs...

1745
来自专栏cnblogs

Javascript的内存泄漏分析

     作为程序员(更高大尚的称谓:研软件研发)的我们,无论是用Javascript,还是.net, java语言,肯定都遇到过内存泄漏的问题。只不过他们都有...

882
来自专栏大前端_Web

详解ES7的async及webpack配置async

版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://blog.csdn.net/wkyseo/articl...

1272
来自专栏SpringBoot

java文件夹复制到指定目录

461

扫码关注云+社区