前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >iOS 中的 Promise 设计模式

iOS 中的 Promise 设计模式

原创
作者头像
QQ音乐技术团队
修改于 2017-10-16 03:09:41
修改于 2017-10-16 03:09:41
1.5K00
代码可运行
举报
运行总次数:0
代码可运行

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (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的实现,再看原理:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (void)jumpOutCallbackHell
{
    [self promiseTaskA].then(^{
        return [self promiseTaskB];
    }).then(^{
        return [self promiseTaskC];
    }).then(^{
        NSLog(@"all task done");
    });
}

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

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (PMKPromise *(^)(id))then {
    return ^(id block){
        return self.thenOn(dispatch_get_main_queue(), block);
    };
}

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

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
- (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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@interface NSMethodSignature : NSObject {
...
@property (readonly) NSUInteger numberOfArguments;
...
@property (readonly) const char *methodReturnType NS_RETURNS_INNER_POINTER;
...
@end

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 )。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 :

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
URLSession.GET("http://example.com").asDictionary().then { json in

}.catch { error in
    //…
}

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

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

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

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?
很简单呀,因为我做了实验和看了 TCP 协议栈的内核源码,发现要增大这两个队列长度,不是简简单单增大某一个参数就可以的。
帅地
2020/06/03
4.6K0
TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?
如何正确查看线上半/全连接队列溢出情况?
在《深入解析常见三次握手异常》 这一文中,我们讨论到如果发生连接队列溢出而丢包的话,会导致连接耗时会上涨很多。那如何判断一台服务器当前是否有半/全连接队列溢出丢包发生呢?
开发内功修炼
2022/03/24
1.8K0
TCP SYN flood洪水攻击原理和防御破解
于是出现了对于握手过程进行的攻击。攻击者发送大量的SYN包,服务器回应(SYN+ACK)包,但是攻击者不回应ACK包,这样的话,服务器不知道(SYN+ACK)是否发送成功,默认情况下会重试5次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者如果处于公网,可以伪造IP的话,对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。
sunsky
2020/08/20
4.2K0
TCP SYN flood洪水攻击原理和防御破解
再聊 TCP backlog
关于三次握手,还有很多细节之前的文章没有详细介绍,这篇文章我们以 backlog 参数来深入研究一下建连的过程。通过阅读这篇文章,你会了解到下面这些知识:
挖坑的张师傅
2022/05/13
1.1K0
再聊 TCP backlog
TCP Listen backlog
TCP有限状态机 TCP服务 创建TCP服务的四个基本步骤: socket – 创建socket套接字。 bind – 绑定要监听的IP地址。 listen – 开始监听客户端连接请求。 accept
linjinhe
2018/06/06
1.7K0
linux 内核参数tcp_max_syn_backlog对应的队列最小长度
内核参数net.ipv4.tcp_max_syn_backlog定义了处于SYN_RECV的TCP最大连接数,当处于SYN_RECV状态的TCP连接数超过tcp_max_syn_backlog后,会丢弃后续的SYN报文。
charlieroro
2020/03/24
4K0
性能分析之TCP全连接队列占满问题分析及优化过程
在对一个挡板系统进行测试时,遇到一个由于TCP全连接队列被占满而影响系统性能的问题,这里记录下如何进行分析及解决的。
高楼Zee
2019/12/24
4.9K0
TCP SYNCookie 机制
简单来说,服务端收到客户端的SYN包之后,将连接放到半连接队列中,当服务端再次收到客户端的ACK包之后,会将连接从半连接队列移到全连接队列中,这样服务端的程序调用accept()方法的时候,就可以从全连接队列中获取到连接了.
书唐瑞
2022/11/14
5830
TCP SYNCookie 机制
认识 SYN Flood 攻击
SYN Flood 是互联网上最原始、最经典的 DDoS(Distributed Denial of Service)攻击之一。
恋喵大鲤鱼
2024/02/02
4630
认识 SYN Flood 攻击
关于TCP overflowed、全连接、半连接队列
最近遇到多台CVM中客户端访问服务器端超时的异常,当时查看了netstat -as信息,凭经验判断可能是tcp overflowed导致的。网卡队列满了,可能会造成子机网络包重传现象
elontian田凌翔
2019/11/27
7.7K0
关于TCP overflowed、全连接、半连接队列
TCP 三次握手,给我长脸了噢
之前有个小伙伴在技术交流群里咨询过一个问题,我当时还给提供了点排查思路,是个典型的八股文转实战分析的案例,我觉得挺有意思,趁着中午休息简单整理出来和大家分享下,有不严谨的地方欢迎大家指出。
程序员小富
2023/03/16
8010
TCP 三次握手,给我长脸了噢
linux中TCP三次握手与四次挥手介绍及调优
TCP是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息,如ip地址、端口号等。
没有故事的陈师傅
2021/08/13
8910
再见了TCP(性能优化)
客户端在建立连接时会首先发送SYN报文,但是假设此时你没有收到服务端SYN+ACK的响应报文,客户端此时会重传SYN报文,此时你需要根据实际情况来调整SYN报文的重传次数,以便客户端能够及时得到反馈。
shysh95
2021/12/21
9390
再见了TCP(性能优化)
CIA安全模型-配置Linux描述网络安全CIA模型之可用性案例
数据可用性是一种以使用者为中心的设计概念,易用性设计的重点在于让产品的设计能够符合使用者的习惯与需求。以互联网网站的设计为例,希望让使用者在浏览的过程中不会产生压力或感到挫折,并能让使用者在使用网站功能时,能用最少的努力发挥最大的效能。基于这个原因,任何有违信息的“可用性”都算是违反信息安全的规定。因此,世上不少国家,不论是美国还是中国都有要求保持信息可以不受规限地流通的运动举行。
用户7881870
2021/05/17
1.1K0
记一次惊心的网站 TCP 队列问题排查经历
来源:高效运维 ID:greatops 问题描述 监控系统发现电商网站主页及其它页面间歇性的无法访问; 查看安全防护和网络流量、应用系统负载均正常; 系统重启后,能够暂时解决,但持续一段时间后间歇性问题再次出现。 此时问题已影响到整个网站的正常业务,我那个心惊呀,最主要是报警系统没有任何报警,服务运行一切正常,瞬时背上的汗已经出来了。但还是要静心,来仔细寻找蛛丝马迹,来一步一步找问题。 问题初步判断 检查dev 和 网卡设备层,是否有error和drop ,分析在硬件和系统层是否异常 ----- 命令
小小科
2018/06/20
7000
优化内核参数提高服务器的并发处理能力
PS:在服务器硬件资源额定有限的情况下,最大的压榨服务器的性能,提高服务器的并发处理能力,是很多运维技术人员思考的问题。要提高Linux系统下的负载能力,可以使用nginx等原生并发处理能力就很强的web服务器,如果使用Apache的可以启用其Worker模式,来提高其并发处理能力。除此之外,在考虑节省成本的情况下,可以修改Linux的内核相关TCP参数,来最大的提高服务器性能。当然,最基础的提高负载问题,还是升级服务器硬件了,这是最根本的。 Linux系统下,TCP连接断开后,会以TIME_WAIT状态保
一夕如环
2018/04/03
1.5K0
全连接队列和半连接队列
大家对于 TCP 的三次握手应该都比较熟悉了,对于服务端,收到 SYN 包后该怎么处理,收到 Establish 之后又该怎么处理,或者说这些连接放在哪里,其实这也是之前面试问过的问题
opencode
2022/12/26
7440
全连接队列和半连接队列
一文带你搞定TCP连接队列
服务器收到客户端SYN数据包后,Linux内核会把该连接存储到半连接队列中,并响应SYN+ACK报文给客户端。
shysh95
2021/12/21
1.3K0
一文带你搞定TCP连接队列
五分钟带你读懂 TCP全连接队列(图文并茂)
今天有个小伙伴跑过来告诉我有个奇怪的问题需要协助下,问题确实也很奇怪。客户端调用RT比较高并伴随着间歇性异常Connection reset出现,而服务端CPU 、线程栈等看起来貌似都很正常,而且服务端的RT很短。
鲁大猿
2020/09/18
3.2K0
Kubernetes 疑难杂症排查分享:神秘的溢出与丢包
图片下载走的 k8s ingress,这个 ingress 路径对应后端 service 是一个代理静态图片文件的 nginx deployment,这个 deployment 只有一个副本,静态文件存储在 nfs 上,nginx 通过挂载 nfs 来读取静态文件来提供图片下载服务,所以调用链是:client --> k8s ingress --> nginx --> nfs。
imroc
2020/01/13
3.1K1
推荐阅读
相关推荐
TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档