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: 任务失败,并返回错误更多可以参考 官方规范
    如上图所示,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. 怎么实现的顺序调用;

  1. 如果传递参数,参数是怎么传递的。

带着问题,来看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对象。上面的函数中,有一个dispatch<sub>barrier</sub><sub>sync</sub> ,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,也就获取到了参数个数与函数返回值这些信息。函数返回值的类型是经过编码的,具体的对照表可以参考官方文档

    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设计模式能够解决我对散落在各处的代理模式产生的代码的烦恼,也让我避免了跳进回调陷阱,就值得总结了。

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

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

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏JavaEdge

Netty源码阅读入门实战(八)-解码(更新 ing)

就像很多标准的架构模式都被各种专用框架所支持一样,常见的数据处理模式往往也是目标实现的很好的候选对象,它可以节省开发人员大量的时间和精力。 当然这也适应于本文...

1374
来自专栏FreeBuf

浅析ReDoS的原理与实践

*本文原创作者:MyKings,本文属FreeBuf原创奖励计划,未经许可禁止转载 ReDoS(Regular expression Denial of Ser...

8095
来自专栏Java帮帮-微信公众号-技术文章全总结

【选择题】Java基础测试题一(10道)

【选择题】Java基础测试题一(10道) 1.下面哪些是合法的变量名? (DEG) A.2variable //不能以数字开头 ...

4638
来自专栏我就是马云飞

LruCache源码解析

 今天我们来聊聊缓存策略相关的内容,LruCache应该说是三级缓存策略会使用到的内存缓存策略。今天我们就来扒一扒这里面的原理,同时也温故温故我们的数据结构方面...

2467
来自专栏Android开发指南

用最简单的例子说明设计模式(一)之单例模式、工厂模式、装饰模式、外观模式

4545
来自专栏mini188

java中的锁

java中有哪些锁 这个问题在我看了一遍<java并发编程>后尽然无法回答,说明自己对于锁的概念了解的不够。于是再次翻看了一下书里的内容,突然有点打开脑门的感觉...

4759
来自专栏大内老A

[ASP.NET MVC]通过对HtmlHelper扩展简化“列表控件”的绑定

在众多表单元素中,有一类<select>元素用于绑定一组预定义列表。传统的ASP.NET Web Form中,它对应着一组重要的控件类型,即ListContro...

2015
来自专栏喔家ArchiSelf

从构造函数看线程安全

线程是编程中常用而且强大的手段,在使用过程中,我们经常面对的就是线程安全问题了。对于Java中常见的数据结构而言,一般的,ArrayList是非线程安全的,Ve...

1132
来自专栏QQ音乐技术团队的专栏

iOS 中的 Promise 设计模式

无论是代理模式,还是闭包,在处理单一任务的时候,都出色的完成了任务。可是当两种模式要相互配合,一起完成一系列任务,并且每个任务之间还要共享信息,相互衔接,雇主就...

2150
来自专栏FreeBuf

小白都能看懂的JSON反序列化远程命令执行

前言 Fastjson是一个由阿里巴巴维护的一个json库。它采用一种“假定有序快速匹配”的算法,是号称Java中最快的json库。Fastjson接口简单易...

5024

扫码关注云+社区

领取腾讯云代金券