HTTPDNS 在 iOS 中的实践

“未找到主机名”,这是很常见的错误。出现这个错误,按理来说,应该也是正常的。但郁闷的是,常常别的应用能正常使用,偏就是自己开发的应用不行,这实在令人头痛。

原因

目前很多 APP 会用 HTTP/HTTPS 来进行网络交互。APP 会用域名访问接口,正常情况下,如果设备网络畅通的情况下,都是能正常访问到服务器的。但是 DNS 劫持、UDP 不稳定等,导致经常出现域名无法解析的情况,自然也就无法正常请求了。

解决方案

找到了问题所在,自然也就是解决方法了。
1、替换系统的域名解析方案;目前 iOS 和 安卓 都是无法替换的,自然行不通了。
2、用 IP 访问;即不采用域名的形式,直接用 IP 访问,自然也是不存在域名解析的问题了。

直接用完全不用域名,只用 IP 访问也是不可能的。现在大家一般都将服务器托管在各大服务商处,说不定哪天将换到另一台服务器,新用了另一个 IP 呢;再说现在很多资源为了能尽快访问到,都会采用 CDN,固定 IP 就更不太可能了。所以好的办法就是我们手动将 host 替换成 IP。如:http://api.example.com/path 替换成:http://124.12.42.xx/path,如果是在 IPv6 的环境下则是:http://[2002:0:0:0:0:0:7c0c:xxx]/path

域名解析

国内提供域名解析 API 接口的,有 DNSPod,示例如下:

可以看到,解析域名很简单,其中 ttl=1 是表示要求在响应结果中携带解析结果的缓存时间,上例中是 17。很多服务往往会部署在多台服务器上,所以它会根据源 IP 返回的是一个合适的 IP 数组。但目前(2016/12/17)它好像还不支持 HTTPS。

拿到 IP 后,就能以合适的形式替换掉 host 了:

这个示例的核心点在于 asyncParseIPAsURLFormat。它需要考虑网络环境、缓存等问题,最主要还是热开关功能(万一突然出现 IP 不能访问的情况,不至于应用都用不了了),所以涉及的东西还是比较多的。

现在国内有很多厂商为 DNSPod 开发了 SDK,比如 阿里、七牛(开源)等。不想自己写的,不妨使用这些 SDK。

注意事项

HTTP 请求头中的 host 字段

HTTP 标准中规定,服务器会将请求头中的 host 字段的值作为请求的域名。咱们使用 IP 替换 URL 中的 host 进行访问,此时网络库会将 IP 当作 host,服务器就会解析异常了。为了解决这个问题,咱们可以手动设置请求中的 host 字段:

可能的特殊字段要求

其它一些协议也会识别 host 字段,如 websocket 协议。还有的协议会对其它字段作同样的要求,我们自己在开发中遇到过要设置的字段有:

  • refer
  • originHost

当然这根服务器的配置相关,如果你有遇到访问服务器被拒的情况,请与服务器的小伙伴沟通。

无法以 URLRequest 访问的情况

有很多第三方服务,要求只能以 URL 的形式访问,比如很多直播 SDK。只能以 URL 的形式访问,自然也就无法重写 host 字段了。所以我们在最开始的时候(2016年6月)使用了其中一家能以 IP 访问的服务商,但现在(2016年12月)他们也要求不能再以 IP 访问了。

在这种情况下,可以在 URL 的后面加上一个参数用来指定 host。当然,这需要服务器的配合。

COOKIE 字段

有部分网络库支持自动管理 COOKIE,而且用的还是 URL 中的 host,所以 COOKIE 管理也就悲剧了。在使用 HTTPDNS 时,还是关闭 COOKIE 自动管理吧。

代理

在代理情况下,代理服务器会将 URL 中的 host 发送给服务器,服务器也就无法处理了。所以在代理情况下还是关了好。

HTTPS

HTTPS 要分两种情况:
1、单 IP 单域名;很多小应用只在服务器上部署了一个 HTTPS 服务,就是这种情况。
2、单 IP 多域名;这种在 CDN 下常见,一个服务器部署了多个 HTTPS 服务。

1、单 IP 单域名

HTTPS 请求首先会跟 SSL/TLS 握手。步骤如下:
1、客户端将其支持的算法列表和用于产生密钥的随机数等参数发给服务器;
2、服务器选择合适的算法,下发公钥证书和用于产生密钥的随机数给客户端;
3、客户端检验服务器证书,然后用服务器的公钥生成一个随机密码串,发给服务器;
4、客户端与服务器根据以上信息独立计算加密和 MAC 密钥,并相互交换;
5、基于密钥通信。

其中与 HTTPDNS 有关的在第 3 步的 检验服务器证书。客户端会检查证书的 domain 域和扩展域,只有包含了请求的 host,才会验证通过。HTTPDNS 用 IP 替换了 host,会导致出现 domain 不匹配的情况,是无法通过的。

解决起来也简单,只需在验证时,传入真实的 host 即可:

如果觉得麻烦,也可以 swizzle URLProtectionSpace 类的 host 属性的 get 方法,让它返回真实 host。这样就可以无需对 AFNetworking 或 SDWebImage 等网络框架过多的配置了。

2、单 IP 多域名

多域名往往意味着多证书(证书有几种类型,有的可以带有通配符,所以不一定)。上面提到,在 SSL/TLS 握手的第二步中,服务器会下发公钥证书。如果用 IP 直接访问服务器,那么服务器怎么知道下发哪个证书合适呢?所以结果往往是不确定的。

我们用的服务器是 nginx 1.4.7,在多域名情况下,它返回的是第一个域名所对应的证书。有一次我们后台开发同学自已在线上环境下配置了一次,其结果就是我们客户端的 https 验证全部失败,造成线上应用无法使用的事故(影响大概10来分钟)。

针对这种情况,阿里的解决方案是 hook SSL 的连接过程,用 Socket 进行真正的请求。我们的做法是:多域名的情况下不用 HTTPDNS -_-!。CDN 下使用 HTTPS 的话,往往就是这种情况。

阿里的方案:地址

总结

我们自己的应用 HTTPDNS 上线一年,极大的改善了 “找不到主机” 的问题。虽然中间有些小坑,不过对于收效来说还是值得的。

ByteCountFormatter 简介

ByteCountFormatter

  介绍一个 Foundation 中不常用的小工具类:ByteCountFormatter。这个类很简单,它就是将字节数格式化成适合的描述(KBMBGB 等),还是很方便的。

  示例如下:

  果然还是很方便的,居然还有 10001024 的不同方式。

  作为一个 iOS/OS X 开发,看到这个类的名字,会感觉有点熟悉吧,有没有想到这个呢:DateFormatterDateFormatter 常被咱们用来格式化日期。其实它们都继承于 Formatter

Formatter

  NSFormatter 是一个抽象类,就是用来格式化数据的。系统中已经有很多它的子类了:

ByteCountFormatter 格式化比特

  这个就不举例了
  

DateFormatter 格式化时间

DateComponentsFormatter 格式化时间组件

DateIntervalFormatter 格式化时间间隔

EnergyFormatter 格式化物理能量数据(焦耳、卡路里等)

LengthFormatter 格式化物理长度(cm、m、km 等)

MassFormatter 格式化物理质量(g、kg 等)

NumberFormatter 极为丰富的数字格式化类(值得一看)

PersonNameComponentsFormatter 人名信息格式化类

自己的格式化类

  这么多格式化类,基本能满足一般的需求,且它们大都支持本地化的,所以都非常实用。如果这些格式化类都不能满足你的需求,咱们还可以定义自己的格式类。

  要实现自己的格式化类,其实很简单,只需要至少覆盖下面两个方法:

  要注意的是,因为一般格式化都是非常耗时的操作,所以在一个对象格式化后,需要将结果字符串与该对象关联起来,免得下回再次解析。

  

  

iOS 利用 Autolayout 实现 view 间隔自动调整

标签: Autolayout


1、需求

不知道大家是否常有这样的需求:一个界面中,有多个 view,每个 view 的大小由其内容决定。当一个 view 有内容时,下一个 view 与它之间会一个间隔。如果没有内容的话,下一个 view 就会紧挨着它。如下图所示:

example1
[图1]

图1 中,四个 label 的大小是自适应的,且每个 label 相隔 10px。这种情况下,视图看起来是很正常的。但如果其中某些 label 没有文字呢?看下图:

example2
[图2] label2.text = nil

example3
[图3] label2.text = nil; label3.text= nil;

图2 是 label3.textnil,中间有一个明显过大的间隔,这是由于 label3 的高度虽然为 0,但由于它与 label2 的间隔为 10px,而 label4 与它的间隔又是 10px,所以造成了图中 label2label4 的过大的间隔。

图3 则更为惨不忍睹。

这时,咱们的需求就出来了:labellabel 之间的间隔能随着它们自身内容的变化而变化。当有文本时,间隔存在;当没有文本时,则紧挨在一起。当然,这里的间隔希望不仅是垂直的,水平方向也应该是一样的。

2、解决方案

来看看各种解决方法。

2.1 动态更新约束

这是最直观、容易想到的办法,就是在 label2 等内容有变化时,去调整相关的间隔约束:更改 constants优先级 等。

这种方法存在的问题是维护成本太高,这里只有四个 label ,但是要维护这种间隔约束关系主就已经很累了。所以这种方法是比较初级的,不灵活。

2.2 自定义 -(CGSize)intrinsicContentSize

视图的 内容 在 auto layout 中,其与约束是 同样重要的。视图有一个方法:– (CGSize)intrinsicContentSize,用来返回展示完整视力内容的最小 size

比如 UILabel 就是根据它的 textattributedTextpreferredMaxLayoutWidth 等来计算出它的内容 size

当视图内容改变时,可以调用 – (void)invalidateIntrinsicContentSize 方法来让 Auto Layout 在下次布局时重新计算。

咱们可以将间隔当作 内容 的一部分,将其计算在内:

可以看到,这种方法在一定程度上可以解决间隔问题,但它有很大的不足:它将间隔 侵入 到内容中;需要 包装 目标视图,这个代价却实在有点大,虽然利用 继承 可以一部分 包装 问题,但类似于这里的 UILabel,由于它内容的绘制方法(文字垂直居中),继承 是无法做到 间隔 的。不过在自定义视图时,如果就间隔考虑进去的话,问题倒是不大。

所以,这种方案适用于自定义视图中,对系统定义的视图帮助有限。

2.3 利用对齐矩形(alignment rect)

你可能会直观的认为 Auto Layout 中,约束是使用 frame 来确定视图的大小和位置的,但实际上,它使用的是 对齐矩形(alignment rect) 这个几何元素。不过在大多数情况下,framealignment rect 是相等的,所以你这么理解也没什么不对。

系统有 frame 不用,为啥要用 alignment rect 呢?

有时候,咱们在创建复杂的视图时,可能会添加各种装饰元素,如阴影、外边框、角标等等。但考虑到开发这样的视图所需时间成本,或者为了避免离屏渲染等原因,会找设计师直接切相应的成品图给咱们。如下图:

alignment rect
(图片来源:iOS Auto Layout Demystified)

上图中,(a) 是咱们拿到的图,(c) 是这个图的 frame。显然,咱们在布局的时候,不想将阴影和角标考虑进去(视图的 center 和 底边、右边都发生了偏移),而是只考虑中间的核心部分,如图 (b) 中框出的矩形所示。

对齐矩形就是用来处理这种情况的。

UIView 提供了方法,由 frame 得到 alignment rect

它得可逆,也就是说得能从 alignment rect 反过来得到 frame

考虑到每次重写这两个方法比较烦,系统也提供了一个简便方法,由 inset 来指定 framealigment rect 的关系:

回到间隔问题。咱们可以将间隔当作上面提到的装饰,让 UILabelalignment rectframe 多个 10 point 间隔就好了:

不过让人感觉迷惑是的,在 iOS 中,frameForAlignmentRect:alignmentRectForFrame: 重写之后,并没有起到预期的作用,OS X 中倒是正常。所以在 iOS 中,还是使用 alignmentRectInsets 的好。对于这个现象,还希望有了解的同学帮忙解释一下。

当然,每次都得要继承才能使用对齐矩形,毕竟不太方便,也许 关联对象method swizzled 组合起来是个可行方案:

3 总结

对齐矩形可是个好玩意呢~

参考:
1、Apple Developer
2、Advanced Auto Layout Toolbox
3、iOS Auto Layout Demystified

HTTPS 初解

标签(空格分隔): https 基础


  在进行 HTTP 通信时,信息可能会监听、服务器或客户端身份伪装等安全问题。HTTPS 则能有效解决这些问题。这里就简单了解下 HTTPS。

1、HTTP 存在的问题

  HTTP 日常使用极为广泛的协议,它很优秀且方便,但还是存在一些问题,如:
– 明文通信,内容可以直接被窃听
– 无法验证报文的完整性,可能被篡改
– 通信方身份不验证,可能遇到假的客户端或服务器

1.1 明文通信,内容可以直接被窃听

  HTTP 不会对请求和响应的内容进行加密,报文直接使用明文发送。报文在服务器与客户端流转中间,会经过若干个结点,这些结点中随时都可能会有窃听行为。

  因为通信一定会经过中间很多个结点,所以就算是报文经过了加密,也一样会被窃听到,不过是窃听到加密后的内容。要窃听相同段上的通信还是很简单的,比如可以使用常用的抓包工具 Wireshark。

  那如何保护信息的安全呢?最常用的就是加密技术了。加密方式可以根据加密对象分以下几种:

通信加密

  HTTP 协议基于 TCP/IP 协议族,它没有加密机制。但可以通过 SSL(Secure Socket Layer,安全套接层)建立安全的通信线路,再进行 HTTP 通信,这种与 SSL 结合使用的称为 HTTPS(HTTP Secure,超文本传安全协议)。

内容加密

  还可以对通信内容本身加密。HTTP 协议中没有加密机制,但可以对其传输的内容进行加密,也就是对报文内容进行加密。这种加密方式要求客户端对 HTTP 报文进行加密处理后再发送给服务器端,服务器端拿到加密后的报文再进行解密。这种加密方式不同于 SSL 将整个通信线路进行加密,所以它还是有被篡改的风险的。

1.2 无法验证报文的完整性,可能被篡改

接收到的内容可能被做假

  HTTP 协议是无法证明通信报文的完整性的。因此请求或响应在途中随时可能被篡改而不自知,也就是说,没有任何办法确认,发出的请求/响应和接收到的请求/响应是前后相同的。

  比如浏览器从某个网站上下载一个文件,它是无法确定下载的文件和服务器上有些话的文件是同一个文件的。文件在传输过程中被掉包了也是不知道的。

  这种请求或响应在传输途中,被拦截、篡改的攻击就是中间人攻击。

  某运营商就经常干这种事,打开百度、网易什么的,中间直接来个大大的广告,你懂的…

防止篡改

  也有一些 HTTP 协议确定报文完整性的方法,不过这些方法很不方便,也不太可靠。用得最多的就是 MD5 等散列值校验的方法。很多文件下载服务的网站都会提供相应文件的 MD5 散列值,一来得用户亲自去动手校验(中国估计只有 0.1% 不到的用户懂得怎么做吧),二来如果网站提供的 MD5 值也被改写的话呢?所以这种方法不方便也不可靠。

1.3 通信方身份不验证,可能遇到假的客户端或服务器

  在进行 HTTP 通信时,请求和响应方都不会对通信方进行身份确认。这也会给人以可乘之机。

任何人都可以发起、响应请求

  在 HTTP 通信时,由于服务器不确认请求发起方的身份,所以任何设备都可以发起请求,服务器会对每一个接收到的请求进行响应(当然,服务器可以限制 IP 地址和端口号)。由于服务器会响应所有接收到的请求,所以有人就利用这一点,给服务器发起海量的无意义的请求,造成服务器无法响应正式的请求,这就是 Dos 攻击(Denial Of Service,拒绝服务攻击)。

  由于客户端也不会验证服务器是否真实,所以遇到来自假的服务器的响应时,客户端也不知道,只能交由人来判断。钓鱼网站就是利用了这一点。

查明对方的证书

  HTTP 协议无法确认通信方,而 SSL 则是可以的。SSL 不仅提供了加密处理,还提供了叫做“证书”的手段,用于确定通信方的身份。

  证书是由值得信任的第三方机构颁发(已获得社会认可的企业或组织机构)的,用以证明服务器和客户端的身份。而且伪造证书从目前的技术来看,是一件极为难的事情,所以证书往往可以确定通信方的身份。

  以客户端访问网页为例。客户端在开始通信之前,先向第三机机构确认 Web 网站服务器的证书的有效性,再得到其确认后,再开始与服务器进行通信。

HTTPS

2.1 HTTP + 加密 + 认证 + 完整性保护 = HTTPS

  HTTPS 也就是 HTTP 加上加密处理、认证以及完整性保护。HTTP 协议在通信过程如果没有使用加密的明文,比如在 Web 页面输入手机号,如果这条通信线路再窃听,那么手机号就被别人知道了。

  另外,在 HTTP 中,服务器和客户端都无法确认通信方,说不定就是正在和某个骗子通信呢。还得考虑报文在通信过程中有没有被篡改。

  为了解决上面这些问题,可以在 HTTP 上加入加密处理和认证机制。这种添加了加密及认证机制的 HTTP 就叫做 HTTPS(HTTP Secure)。

  使用 HTTPS 通信时,用的是 https://,而不是 http://。另外,当浏览器访问 HTTPS 的 Web 网站时,浏览器地址栏会出现一个带锁的标记。如下面就是 Chrome 的样式:
  
  HTTPS 带锁样式

2.2 HTTPS 是 HTTP 披了层 SSL 的外壳

  HTTPS 并非是应用层的新协议,而是 HTTP 通信接口部分用 SSL 协议代替而已。

  本来,HTTP 是直接和 TCP 通信。在 HTTPS 中,它先和 SSL 通信,SSL 再和 TCP 通信。所以说 HTTPS 是披了层 SSL 外壳的 HTTP。

  SSL 是独立于 HTTP 的协议,所以其他类似于 HTTP 的应用层 SMTP 等协议都可以配合 SSL 协议使用,也可以给它们增强安全性。

2.3 SSL 密钥

加密

  数据在传输过程中,很容易被窃听。加密就是保护数据安全的措施。一般是利用技术手段把数据变成乱码(加密)传送,到达目的地后,再利用对应的技术手段还原数据(解密)。

  加密包含算法和密钥两个元素。算法将要加密的数据与密钥(一窜数字)相结合,产生不可理解的密文。由此可见,密钥与算法同样重要。

  对数据加密技术可以分为两类:私人密钥加密(对称密钥加密)和公开密钥加密(非对称密钥加密)。

  SSL 采用了 公开密钥加密(Public-key cryptography)的加密处理方式。

  现在的加密方法中,加密算法都是公开的,网上都有各种算法原理解析的内容。加密算法虽然是公开的,算法用到的密钥却是保密的,以此来保持加密方法的安全性。

  加密和解密都会用到密钥。有了密钥就可以解密了,如果密钥被攻击者获得,加密也就没有意义了。

私人密钥

  私人密钥加密就是加密和解密用的是同一个密钥,这也称为对称密钥加密。

  私人密钥加密存在这样一个问题,必须要将密钥发送给对方。但如何才能安全地转交密钥呢?如果在互联网上转交密钥时,被监听了,那加密也就没有意义了。也就是说,只要在互联网上发送密钥,就有被窃听的风险,但如果不发送,对方怎么解密呢?再说,如果密钥能够安全发送,那数据不也应该能安全送达么!

公开密钥加密

  公开密钥加密方式能很好地解决私人密钥加密的困难。

  公开密钥加密方式有两把密钥。一把叫做私有密钥(private key),另一把叫做公开密钥(public key)。私有密钥是一方保管,而公开密钥则谁都可以获得。

  这种方式是需要发送密文的一方先获得对方的公开密钥,使用已知的算法进行加密处理。对方收到被加密的信息后,再使用自己的私有密钥进行解密。

  这种加密方式有意思是的加密算法的神奇,经过这个公开的算法加密后的密文,即使知道公开密钥,也是无法对密文还原的。要想对密文进行解决,必须要有私钥才行。所以公开密钥加密是非常安全的,即使窃听到密文和公开密钥,却还是无法进行解密。

公开密钥加密算法用的一般是 RSA 算法(这可能是目前最重要的算法了)。这个算法由3个小伙子在1977年提出,它的主要原理是:将两个大素数相乘很简单,但想要这个乘积进行因式分解极其困难,因此可以将乘积公开作为公开密钥。不过随着目前的分布式计算和量子计算机的快速发展,说不定在将来也许能破解这个算法了。

混合加密

  公开密钥加密很安全,但与私人密钥加密相比,由于公开密钥加密的算法复杂性,导致它的加密和解密处理速度都比私人密钥加密慢很多,效率很低。所以可以充分利用它们各自的优势,结合起来。

  先用公开密钥加密,交换私人密钥加密会用的密钥,之后的通信交换则使用私人密钥方式。这就是混合加密。

1) 使用公开密钥加密方式安全地交换在稍后的私人密钥加密中要使用的密钥
2)确保交换的密钥是安全的前提下,使用私人密钥加密方式进行通信

2.4 公开密钥证书

  其实,公开密钥加密方式还存在一个很大的问题:它无法证明公开密钥本身是真实的公开密钥。比如,打算跟银行的服务器建立公开密钥加密方式的通信时,怎么证明收到的公开密钥就是该服务器的密钥呢?毕竟,要调包公开密钥是极为简单的。

  这时,数字证书认证机构(CA,Certificated Authority)就出场了。

  数字证书认证机构是具有权威性、公正性的机构。它的业务流程是:首先,服务器的开发者向数字证书认证机构提出公开密钥(服务器公开密钥)的申请。数字证书认证机构在核实申请者的身份之后,会用自己的公开密钥(数字签名公开密钥)对申请的公开密钥做数字签名,再将 服务器公开密钥、数字签名以及申请者身份等信息放入公钥证书。

  服务器则将这份由数字证书认证机构颁发的公钥证书发送给客户端,以进行公开密钥加密方式通信。公钥证书也可做数字证书或简称为为证书。证书就相当于是服务器的身份证。

  客户端接到证书后,使用 数字签名公开密钥 对数字签名进行验证,当验证通过时,也就证明了:1、真实有效的数字证书认证机构。2、真实有效的服务器公开密钥。然后就可能与服务器安全通信了。

  其实这里还是有一个问题的。那就是如何将 数字签名公开密钥 安全地转给客户端?难道再去另一个认证机制那确认(现在是真有的)?无疑,安全转交是一件困难的事。因此,常用的认证机关的公开密钥会被很多浏览器内置在里面。

EV SSL(增强型)证书

  证书作用这一是证明服务器是否规范,另一个作用是可以确认服务器背后的企业是否真实。具有这种特性的证书就是 EV SSL (Extended Validation SSL Certificate)证书。

  EV SSL 证书是基于国际标准的严格身份验证颁发的证书。通过认证的网站能获得更高的认可度。

  EV SSL 证书在视觉上最大的特色在于激活浏览器的地址栏的背景色是绿色。而且在地址栏中显示了 SSL 证书中记录的组织名称。

  这个机制原本是为了防止用户被钓鱼攻击的,但效果如何还真不知道,目前来看,很多用户根本不清楚这是啥玩意儿。

  EV SSL

客户端证书

  客户端也是可以有证书的,让服务器知道,正在通信的客户端是可信任的。

  相比之下,客户端证书存在几个问题。第一是证书的获取和安装。想要获取证书时,还得用户自行安装,关键是客户端证书还是要花钱买的。另外,安装这件事本身就有很多年长的人不太会。

  所以,客户端证书只在安全性要求级高的特殊情况下都会用。比如网上专业银行,在登录时,不仅要用户名和密码,还要用个U盾。

  客户端的另一个问题是,客户端证书只能证明客户端的存在,却不能证明是用户本人在操作。也许是还证书都被盗了呢?

认证机构被黑

  SSL 的安全建立在认证机构绝对可靠这一前提之下。但认证机构也有成功被黑的时候。在 2011年7月10号,荷兰的一家名叫 DigiNotar(同年9月20号宣布破产) 的认证机构被黑窝入侵,还颁布了 google.com、twitter.com 等网站的伪造证书,走到8月28号才被伊朗发现。这一事件从根本上撼动了 SSL 的可信度。

  因为伪造证书上有正规认证机构的数字签名,所以浏览器会判定该证书是正常的。当伪造的证书被用于服务器伪装时,用户根本无法察觉。

  虽然微软、苹果、Opera 等浏览器厂商积极采取了措施,不过生效却是要一段时间的,这段时间内造成的用户的损失有多大就不知道了。

OpenSSL

  OpenSSL 是用 C 写的一套 SSL 和 TLS 开源实现。这也就意味着人人都可以基于这个构建属于自己的认证机构,然后给自己的颁发服务器证书。不过然并卵,其证书不可在互联网上作为证书使用。

  这种自认证机构给自己颁发的证书,叫做自签名证书。自己给自己作证,自然是算不得数的。所以浏览器在访问这种服务器时,会显示“无法确认连接安全性”等警告消息。

  OpenSSL 在2014年4月,被爆出一个内存溢出引出的 BUG,骇客利用这点能拿到服务器很多信息,其中就包括私钥,也就使得 HTTPS 形同虚设。当时全世界大概有一百万左右的服务器有受到此漏洞的影响。由于 OpenSSL 举足轻重的作用,再加上足够致命的问题,使得这个 BUG 被形容为“互联网心脏出血”。这是近年来互联网最严重的安全事件。

HTTPS 速度慢

  HTTPS 使用 SSL 通信,所以它的处理速度会比 HTTP 要慢。

  一是通信慢。它和 HTTP 相比,网络负载会变慢 2 到 100倍。除去和 TCP 连接、发送 HTTP 请求及响应外,还必须进行 SSL 通信,因此整体上处理通信量会不可避免的增加。

  二是 SSL 必须进行加密处理。在服务器和客户端都需要进行加密和解密的去处处理。所以它比 HTTP 会更多地消耗服务器和客户端的硬件资源。

一直使用 HTTPS 呢?

  由于 HTTPS 会消耗更多的资源,再者还要花钱购买证书,所以目前 HTTPS 一般只会用到敏感信息上。

  不过虽然目前的硬件的快速发展,HTTPS 的资源消耗问题将不再是什么太大问题了。另外,随着移动互联网的快速发展,用户信息泄露情况日益严重,也是非常有必要采用 HTTPS 来进行通信的。Apple 就在 iOS9 中鼓励应用积极使用 HTTPS。

结束

  本文从理论上,粗略描述了 HTTPS 的来龙去脉,希望对你有所帮助。

参考

1、https://en.wikipedia.org/wiki/HTTP
2、https://en.wikipedia.org/wiki/Transport_Layer_Security
3、https://en.wikipedia.org/wiki/HTTPS
4、https://en.wikipedia.org/wiki/RSA_(cryptosystem)
5、《图解HTTP》
  

OC 自动生成分类属性方法

objective-c-元编程实践-分类动态属性

标签(空格分隔): Objective-C runtime iOS 分类 category


  分类属性方法自动生成编码全过程。

背景

  分类,在 iOS 开发中,是常常需要用到的。在分类里添加属性也是常有的事,但分类中无法添加实例变量,编译器也无法为提供分类中属性的 gettersetter 方法了。一般而言,需要手动来实现这两个方法,如果只是用来存储变量的话,关联对象很容易做到这一点:

  这是很常见的实现方式。

  要是再给这个分类多加几个属性,也就得再多加几个这样的 gettersetter 方法,无法也就是方法名字、关联参数不一样罢了,这可真是个体力活呀!要是能像普通类属性那样就好了,自动给生成这两个方法,想想就爽。

  要想做到自动生成这两个方法,可以从两个方面入手:
  1、编码期
  2、运行期

  编码期。在写代码的时候要做到自动生成方法,可以写一个 XCode 插件,一按某些快捷键,相应代码就自动生成了,这有点类似于 Eclipse。插件的讨论不在本文范围内。

  运行期。在编码阶段只需少量代码,具体的方法则在运行期动态生成。本文研究怎么在运行期动态生成这些所需要的方法。本文最终生成的代码在:https://github.com/NathanLi/iOSCategoryPropertyDynamicSupport

需求

  简单点说,就是在运行时能生成分类中属性相应的 getter setter 方法,也就是模仿类中普通 @property 定义的属性。这样的需求太泛,咱们先来细化一下:

  1、只生成分类中的,我想要的 gettersetter方法体;
  2、属性类型:支持基本数据类型、对象、结构体,且自动存取;
  3、支持 @property 定义中的 assignstrongcopyweak
  4、支持 @property 中的自定义的方法名;
  5、支持 KVC
  6、支持 KVO
  7、本条不是需求,而是简单的设定:不支持原子即 atomic,只支持 nonatomic。

实现

1、确定要动态生成方法的属性

  这里根据属性的名字来确定是否需要动态生成方法,就以 nl_ 为前辍就好了:

  由于是在分类中,且没有定义相应的方法,所以会有警告:

  在分类实现里加个 @dynamic 就好了:

2、消息

  @dynamic 告诉编译器,这两个方法有没有实现你都不用管,就当作它们存在就行了。那么问题来了,这两个方法明明没有实现,却依然能够调用呢?

  这根消息发送机制有关。在 Objective-C中,消息不会与方法实现绑定,而是在运行时才关联起来的。

  编译器会把所有消息转换为一个函数调用:objc_msgSend。这个函数总得知道是 发送了 哪条 消息吧,所以它最少也有两个参数——消息的接收者 和 消息名(即选择子 SEL),比如下面这个方法:

  编译器就会把它变成这个样子:

  如果消息有参数的话,就会直接传这个函数,所以这个函数的参数个数不定:

  objc_msgSend 的工作就是消息的动态绑定:
  1、根据 selecotrreceiver 找到对应的函数实现(也就是函数地址)。不同的类可以有相同的方法,但是它们所对应的函数地址是不一样的。
  2、调用找到的函数,并传入相应的参数:receiverselectorarg1…。
  3、返回调用的函数的返回值。

  来看下实例:

  所对应的函数代码是这样的:

  可以看到, [self setName:@"name"],最后变成了 objc_msgSend 函数调用。这个函数最终会根据 selfsetName: 找到函数 _I_NLPerson_setName_ 并调用。被调用的函数包含三个参数,分别是调用者、SEL_cmd)和方法参数。

  正如上那个函数看到的,每个方法都有一个选择子这个参数:_cmd,所以才能这么打印方法名:

  SEL 实际上就是一个字符串:cahr *,所以咱们将 SEL 简单理解为方法名也并无不可。刚刚说到了,objc_msgSend 会根据 SEL 找到对应的函数地址,来看看它是怎么找的。

  实际上,OC 中的所有对象和类,最后都被处理为结构体。对象结构体中,会有一个 isa 指针,指向自己的类结构体。而类结构体有很多类信息,其中两个:
  1、指向 superclass 的指针。
  2、类分发表。这个表里存储了方法名 selector 与所对应的函数地址 address
  如上面的 NLPeson 类中的分发表:

selector addrss
init _I_NLPerson_init
setName: _I_NLPerson_setName_

  消息传递框架图:
  方法查找图

  当发送一个消息给一个对象时,首先会去这个对象的 isa 所指向的类结构体里的分发表中寻找 selector,如果找不到的话,objc_msgSend 会根据 superclass 指针找到下一个结构体里寻找 selector,直到 NSObject。只要它找到了 selector,就会调用相应的函数了。意思就是说,先通过消息名,找到函数地址,再调用。这就是所谓的消息运行时动态绑定。
  

3、动态增加方法

  如果给对象发送了一个未知的消息,如果这个对象无法响应或转发的话,就会调用 doesNotRecognizeSelector: 方法,这个方法会抛出 NSInvalidArgumentException 异常。如果你不想让一个让别人调用你的类的 copyinit 方法的话,可以这么做:

  但在调用这个方法之前,系统还是给了我们处理的机会。实现 resolveInstanceMethod: 方法,能动态地给实例方法和类方法添加一个实现。

  Objective-C 中的方法所对应的函数最少有两个参数:self_cmd。如下所示:

  可以用 C 函数 class_addMethod 将其作为一个方法动态加到一个类中:

4、属性元数据、类型编码(Type Encodings)

  要能动态生成属性的方法,首先得知道属性的一些基本信息:类型、方法名、是 weak 还是 strong 等。这些数据都可以在运行时获得到。要用到的技术是:类型编码(Type Encdoings)。

  类型编码是 runtime 的辅助工具。编译器会将类型用字符串来表示。可以用 @encode 得到这个字符串:

  编码表如下:
此处输入图片的描述

  这是描述类型的数据。那描述属性的呢?

  编译器会将类、分类和协议中的属性以元数据信息存起来。有一系列的 C 函数来访问这些数据。

  属性元数据是用结构体 Property 来描述的:

  可以用 class_copyPropertyListprotocol_copyPropertyList 来分别获取类(包含分类)中和协议中的属性:

  比如下面声明的这个类:

  可以这么来获取它的属性列表:

  除了一次性获得所有属性列表外,还有方法 class_getPropertyprotocol_getProperty 可以通过属性名获取单个属性:

  获取到属性结构体后,就可以拿到这个属性的名字和元信息:

  property_getAttributes 能获取到属性的很多信息,包括刚看到的类型编码、gettersetter 方法名、对应的实例变量名等等。打印所有属性的元信息例子:

  property_getAttributes 获取到的 Tf,D,N 是什么意思呢?Tf,是以 T 开头,后面的字符串 f 表示类型编码;D 表示 @dynamicN 表示 nonatomic。这些都是属性本身的信息,以 , 分割。这些字符串的规则是这样的:

Code 意义
R readonly
C copy
& assigned (retain).
N nonatomic
Gname 以 G 开头是的自定义的 Getter 方法名。(如:GcustomGetter 名字是:customGetter).
Sname 以 S 开头是的自定义的 Setter 方法名。(如:ScustoSetter: 名字是: ScustoSetter:).
D @dynamic
W __weak

 
  来看看下面这个例子,你就全理解了:
此处输入图片的描述

5、属性解析

  直接使用属性的元数据可不太好用,用一个对象来描述它会好很多。

  将属性的各项特性都存起来,想要的时候直接拿就好了,这就比 objc_property_t 好用多了。下面是初始化方法:

  可以通过 class_copyPropertyList 获取到一个类中的所有属性结构体,也就能拿到所有属性的元数据。但大部分属性咱们是不感兴趣的,只对 @dynamic 以及以 nl_ 为前辍的属性感兴趣。那就写一个分类方法,用来获取对咱们有用的所有属性数据:

  gettersetter 方法里的数据总得存储在某个地方吧,用字典来存储是比较理想的做法。就在 nl_dynamicPropertyStore 这个分类里定义:

6、自动生成 gettersetter 方法

  要用到的知识都已经介绍完了,接着就看看怎么来自动生成方法了。

  前面介绍过,当发送了一个没有实现过的消息时,我们在 resolveInstanceMethod: 方法中为其添加实现。这个方法在 NSObject 类中定义,在这里,不可能继承它来实现我们想要的功能。我们可以在 NSObject 的分类中写一个新的方法来替代原有的这个方法实现,这叫“方法调配”(method swizzling),这常常用于给原有方法增加新的功能。

方法是动态绑定的,只有在运行时经过查找 后,才知道这条消息所对应的函数。方法,也就是一个名字,加上一个与之关联的函数。所谓方法调配,也就是将两个方法各自关联的函数互相交换一行而已。比如,nameA–>funcationA(nameA 是方法名,funcationA 是关联的方法实现函数), nameB–>funcationB,经过方法调配后,nameA–>funcationB,nameB–>funcationA。那么此时 [obj nameA] 这个消息,实现上调用的是 funcationB。

  方法调配的核心函数是 method_exchangeImplementations,它就是交换两个方法的实现的,代码:

  经过调配之后,原本调用 resolveInstanceMethod 最后执行的是 nl_resolveInstanceMethod 方法体。由于是给 resolveInstanceMethod 增加新的功能,所以在自定义的方法实现了自己的逻辑后,再调用原有的实现。那接下来就将增加方法的逻辑放在这里。

  要添加方法,得先把这些方法所对应的函数定义出来。由于 gettersetter 方法的参数个数和返回值个数都是一致的,所以它们对应的函数并不与属性名相关。而且所有属性的方法都有一个共同的参数:SEL,我们可以用这个参数来对数据进行存储。这里以对象、int、CGRect类型为例:

  方法的各个实现都有了,接下来的工作根据未实现的方法名,找到对应的函数,再把这个函数加到方法中去:

class_addMethod 函数声明:BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)cls 是要添加方法的类;name是要添加方法实现的名字;imp是要添加方法对应的实现,types是用类型编码描述该方法参数的字符串,而方法的函数必定会有参数:self(对象,类型编码是@)和_cmd(选择子,类型编码是:),所以这个 type 字符串中必定包含 “@:” 子串,这个子串前的字符是这个方法的返回值,其后面的字符是该方法的其它参数。
  
  实验一把:

  完全没问题,奖励自己一把先。

7、添加 KVO 支持

  KVO 还不简单,在 setter 实现里加上 willChangeValueForKey:didChangeValueForKey: 就好了:

  再来验证一把:

  会打印出什么?

  可惜,什么也不会打印,而会崩溃:

  log 显示 __NL__object_dynamicSetterIMP 函数里的 [[self nl_dynamicPropertyDictionary] setObject:arg forKey:propertyName]; 崩溃,原因是 propertyName 等于 nilpropertyName 不是选择子所对应的属性名吗,这个属性明明存在的呀,怎么为会空呢?

  看看下面的代码:

  原因就在这里,在 addObserver:... 后,咱们这个对象所属的类就已经不是原来的那个类了,而是原来的类的子类了。系统不过重写了 -class 方法,让人看起来还是原来的类的样子。咱们之前的 nl_dynamicPropertyDescriptors 只包含了当前类的属性,显然不对。这里把父类的属性也加进去:

  再来验证一下:

  验证通过。

8、结束

  代码过多,KVC 和 weak 的支持属于细枝末节,这里就不一一介绍了,想看看完整的代码的话,这里:https://github.com/NathanLi/iOSCategoryPropertyDynamicSupport

  虽然现在 Objective-C 在 Swift 面前已经显得过时,但这 runtime 知识此时了解却也还是有些价值的。这里只是简单的介绍了一个属性相关的知识,实际上可玩的东西很多,比如 ORM (如 LKDBHelper) 等等。

iOS 中的 promise 模式

1、概述

  异步编程 App 开发中用得非常频繁,但异步请求后的操作却比较麻烦。Promise 就是解决这一问题的编程模型。其适用于 延迟(deferred) 计算和 异步(asynchronous) 计算。一个 Promise 对象代表着一个还未完成,但预期将来会完成的操作。它并非要替代 GCD 和 NSOperation,而是与它们一起合作。

2、历史

  Promise 的出现已经很久了,这个术语首页在 C++ 中出现,之后被用在 E 语言中。2009年,提出 CommonJS 的 Promises/A 规范。它能在今天变得如此引人注目,则得归功于 jQuery 框架了,随着 jQuery 1.5 版本中的 Promises 实现,越来越多的人喜欢上它了。

3、介绍

  Promise 模式,可以简单理解为延后执行。Promise,中文意思为发誓(承诺),既然都发誓了,也就一定会有某些行为的发生。Promise 对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功或失败指定处理方法。 这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的 promise 对象来替代原返回值。

  Promise对象有以下几种状态:

  • pending: 初始状态, 非 fulfilled 或 rejected.
  • fulfilled: 成功的操作.
  • rejected: 失败的操作.

  pending状态的promise对象既可转换为带着一个成功值的 fulfilled 状态,也可变为带着一个 error 信息的 rejected 状态。当状态发生转换时,promise.then 绑定的方法就会被调用。(当绑定方法时,如果 promise 对象已经处于 fulfilledrejected 状态,那么相应的方法将会被立刻调用, 所以在异步操作的完成情况和它的绑定方法之间不存在竞争条件。)

  因为 Promise.then 方法会返回 promises 对象, 所以可以链式调用,待会咱们就会看到的。
  
状态转换

4、PromiseKit 和 Bolts-iOS

Bolts-iOS

  看到这个名字,会不会想到有 Bolts-Android 的存在呢?是的,也确实存在。Bolts 是 Parse 和 Facebook 开源的库,包含 TasksApp Links protocol 两个基础组件。Tasks 即是其 Promise 实现。

PromiseKit

  这个框架是 Max Howell 大牛一个人写的。注:这哥们是 Mac 下著名软件 Homebrew 的作者,没错,传说就是这哥们因为不会写反转二叉树而没拿到 Google offer 的。

  PromiseKit 除了有 Promise 实现外,还有一套基于 Promise 的系统类扩展。

5、实例

  由于 PromiseKit 有一套使用的扩展包,所以本文就用它来举例说明了。

then

  在 OC 的异步操作中,常常会引用一些状态变量:

  视图控制器的属性应该是用来存储它自身的状态的,而不是存储一下这种临时变量。像上例这个参数 moive ,还得有一个专门的属性 willPlayMoive 来存储它,如果它只在 - (void)confirmPlayTheMoive 方法内部出现就好了。

  如果说是个同步方法的话,那就好说话了:

  这样,不需要专门的属性来保存这个变量,代码量还减了不少,还更容易理解了。

  不过可惜的是,UIAlertView 中并没有 showSynchronously 这种类似的方法。

  promises 出马咯~

  一点击 “确认”,影片就会接着播放啦~

  在 PromiseKit 中,我们用 then 方法来访问 promise value。

  PromiseKit 给 UIAlertViewUIActionSheetNSURLConnectionUIViewController 等很多常用类提供了类似的扩展,还是很方便的。

链式 promises

  在同步世界中,一切都有序地执行着:

  但到了异步代码时,这结构让人感觉跟碗热干面似的:

  话说,有个小偷,偷到了一个程序员家里,看到了一堆文件,他翻到最后一页,看到了这样的内容:

  这种右漂移的代码,看着感觉怎么样?这样的代码,写起来是爽快,不过别人看起来可就蛋疼了,而且还容易出各种 BUG。

  在 promise 中,咱们可以通过链式调用来避免这种右漂的代码:

  只要 then 方法中返回一个 promise,就可以像这样进行链式调用了。感觉清爽多了。

  异步代码往往没有同步代码具有可读性。

  要找到这最后执行的代码,还得经过好好思考一下才行,而在链式表达式中就没有这样的问题了:

  怎么样,看着有没有舒服点?更容易看懂了?

错误处理

  众所周知,异步编程中,错误的处理是比较蛋疼的一件事。如果每个 error 都处理一下的话,不仅写得烦,代码也会难看得要死。所以很多人的选择是,直接无视了它,但显然,这么做并不合适。

  来看下这种代码:

  怎么样,这还只是个小例子,就写着烦,看着也烦。

  在 promise 中,咱们可以统一处理错误:

  这里,只要任何一个地方有过 error,就会直接被最后的 catch 方法里处理,而中间的那些 then 方法也不会被执行了,有木有很方便呢?

组合 when

  咱们常有这种需求:当两个异步操作都完成后,再执行一下个。简单的,可以用一个临时变量来记录:

  呃,这多少有点淡淡的忧伤,在 promisekit 中,咱们可以通过 PMKWhenwhen 来处理这种需求:

  PMKWhen 还能通过字典来处理:

finally

  promiseKit 不仅提供了 thencatch,还弄了个 finally,顾名思义,也就是最后一定会执行了咯:

总结

  可以看到,Promise 确实能简化异步编程,好处多多呀。而且其本身本不复杂难懂,不妨试试。

  

ReactiveCocoa2 源码浅析

标签(空格分隔): ReactiveCocoa iOS Objective-C


  • 开车不需要知道离合器是怎么工作的,但如果知道离合器原理,那么车子可以开得更平稳。

  ReactiveCocoa 是一个重型的 FRP 框架,内容十分丰富,它使用了大量内建的 block,这使得其有强大的功能的同时,内部源码也比较复杂。本文研究的版本是2.4.4,小版本间的差别不是太大,无需担心此问题。 这里只探究其核心 RACSignal 源码及其相关部分。本文不会详细解释里面的代码,重点在于讨论那些核心代码是 怎么来 的。文本难免有不正确的地方,请不吝指教,非常感谢。

@protocol RACSubscriber

  信号是一个异步数据流,即一个将要发生的以时间为序的事件序列,它能发射出三种不同的东西:valueerrorcompleted。咱们能异步地捕获这些事件:监听信号,针对其发出的三种东西进行操作。“监听”信息的行为叫做 订阅(subscriber)。我们定义的操作就是观察者,这个被“监听”的信号就是被观察的主体(subject) 。其实,这正是“观察者”设计模式!
  
  RAC 针对这个订阅行为定义了一个协议:RACSubscriber。RACSubscriber 协议是与 RACSignal 打交道的唯一方式。咱们先不探究 RACSignal 的内容,而是先研究下 RACSubscriber 是怎么回事。
  
  先来看下 RACSubscriber 的定义:

1、NLSubscriber

  咱们自己来实现这个协议看看(本文自定义的类都以 “NL” 开头,以视区别):

  现在咱们这个类只关心 sendNext:sendError:sendCompleted。本类的实现只是简单的打印一些数据。那怎么来使用这个订阅者呢?RACSignal 类提供了接口来让实现了 RACSubscriber 协议的订阅者订阅信号:

  用定时器信号来试试看:

  下面是输出结果:

  
  

2、改进NLSubscriber

  现在的这个订阅者类 NLSubscriber 除了打印打东西外,啥也干不了,更别说复用了,如果针对所有的信号都写一个订阅者那也太痛苦了,甚至是不太可能的事。
  
  咱们来改进一下,做到如下几点:
  1. 实现 RACSubscriber 协议
  2. 提供与 RACSubscriber对应的可选的可配的接口。
  
  没错,这正是一个适配器!

  第2点的要求可不少,那怎么才能做到这一点呢?还好,OC 中有 block !咱们可以将 RACSubscriber 协议中的三个方法转为三个 block:

  改进目标和改进方向都有了,那咱们来看看改进后的的样子:

  现在来试试看这个改进版,还是上面那个定时器的例子:

  输出结果如下:

  输出结果没什么变化,但是订阅者的行为终于受到咱们的撑控了。再也不用为了一个信号而去实现 RACSubscriber 协议了,只需要拿出 NLSubscriber 这个适配器,再加上咱们想要的自定义的行为即可。如果对信号发出的某个事件不感兴趣,直接传个 nil 可以了,例如上面例子的 error: ,要知道, RACSubscriber 协议中的所有方法都是 @required 的。NLSubscriber 大大方便了我们的工作。
  
  那还以再改进吗?
  

3、RACSignal 类别之 Subscription

  有没有可能把 NLSubscriber 隐藏起来呢?毕竟作为一个信号的消费者,需要了解的越少就越简单,用起来也就越方便。咱们可以通过 OC 中的类别方式,给 RACSignal 加个类别(nl_Subscription),将订阅操作封装到这个信号类中。这样,对于使用这个类的客户而言,甚至不知道订阅者的存在。
  
  nl_Subscription 类别代码如下:

  在这个类别中,将信号的 next:error:completed 以及这三个事件的组合都以 block 的形式封装起来,从以上代码中可以看出,这些方法最终调用的还是 - (void)nl_subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock; 方法,而它则封装了订阅者 NLSubsciber
  
  通过这么个小小的封装,客户使用起来就极其方便了:

  输出如下:

  本例并没有采用之前的 “定时器信号”,而是自己创建的信号,当有订阅者到来时,由这个信号来决定在什么时候发送什么事件。这个例子里发送的事件的逻辑请看代码里的注释。
  
  看到这里,是不是很熟悉了?有没有想起 subscribeNext:,好吧,我就是在使用好多好多次它之后才慢慢入门的,谁让 RAC 的大部分教程里面第一个讲的就是它呢!
  
  到了这里,是不是订阅者这部分就完了呢?我相信你也注意到了,这里有几个不对劲的地方:
  
  1. 无法随时中断订阅操作。想想订阅了一个无限次的定时器信号,无法中断订阅操作的话,定时器就是永不停止的发下去。
  
  2. 订阅完成或错误时,没有统一的地方做清理、扫尾等工作。比如现在有一个上传文件的信号,当上传完成或上传错误时,你得断开与文件服务器的网络连接,还得清空内存里的文件数据。

4、Disposable

RACDisposable

  针对上述两个问题,RACDisposable 应运而生。也就是说 Disposable 有两个作用:
  
  1. 中断订阅某信号
  2. 订阅完成后,执行一些收尾任务(清理、回收等等)。

  订阅者与 Disposable 的关系:
  
  1. 当 Disposable 有“清理”过,那么订阅者就不会再接收到这个被“清理”订阅源的任何事件。举例而言,就是订阅者 subscriberX 订阅了信号 signalA 和 signalB 两个信号,其所对应的 Disposable 分别为 disposableA 和 disposableB,也就是说 subscriberX 会同时接收来自 signalA 和 signalB 的信号。当我们手动强制 “清理” disposableA 后,subscriberX 就不会再接收来自 signalA 的任何事件;而来自 signalB 的事件则不受影响。

  2. 当订阅者 subscriberX 有接收来自任何一个信号的 “error” 或 “completed” 事件时,则不会再接收任何事件了。
  
  可以这么说:Disposable 代表发生了订阅行为

  根据 Disposable 的作用和与订阅者的关系,来总结它所需要提供的接口:
  
  1. 包含清理任务的 block ;
  2. 执行清理任务的方法:- (void)dispose ;
  3. 一个用来表明是否已经 “清理” 过的布尔变量:BOOL disposed 。

  咱们为这个 Disposable 也整了一个类,如下:

  
  从这个类提供的接口来看,显然是做不到 “订阅者与 Disposable 的关系” 中的第2条的。因为这条中所描述的是一个订阅者订阅多个信号,且能手动中断订阅其中一个信号的功能,而 NLDisposable 是单个订阅关系所设计的。

RACCompoundDisposable

  那怎么组织这“多个”的关系呢?数组?Good,就是数组。OK,咱们来相像一下这个方案的初步代码。每个订阅者有一个 Disposable 数组,订阅一个一个信号,则加入一个 Disposable;当手动拆除一个订阅关系时,找到与之相关的 Disposable,发送 dispose 消息,将其从数组中移除;当订阅者不能再接收消息时(接收过 errorcompleted 消息),要 dispose 数组中所有元素,接下来再加入元素时,直接给这个要加入的元素发送 dispose 消息;在多线程环境下,每一次加入或移除或其遍历时,都得加锁。。。(好吧,我编不下去了)
  
  我** ,这么复杂,看来直接用数组来维护是不可行的了。有啥其它可行的法子没?还好,GoF 对此有个方案,叫做“组合模式”:

组合模式 允许你将对象组合成树形结构来表现 “整体/部分” 层次结构。组合能让客户以一致的方式处理个别对象以及对象组合。

  使用组合结构,我们能把相同的操作应用在组合和个别对象上。换句话说,在大多数情况下,我们可以 忽略 对象组合和个别对象之间的差别。
  
  本文毕竟不是来讲模式的,关于这个模式更多的信息,请自行 google。
  
  RAC 中这个组合类叫 RACCompoundDisposable, 咱们的叫 NLCompoundDisposable,来看看咱们这个类的代码:

RACScheduler 简介

  本文不打算研究 RACScheduler 源码,但其又是 RAC 中不可或缺的一个组件,在研究 RACSignal 的源码时不可避免地会遇到它,所以对其作下介绍还是有必要的。其实它的源码并不复杂,可自行研究。

  ReactiveCocoa 中 RACSignal 发送的所有事件的传递交给了一个特殊的框架组件——调度器,即 RACScheduler 类簇(类簇模式稍后介绍)。调度器是为了简化 同步/异步/延迟 事件传递 以及 取消预定的任务(scheduded actions) 这两种 RAC 中常见的动作而提出来的。“事件传递” 简单而言就是些 blocks,RACScheduler 所做的就是:调度这些 blocks (schedule blokcs,还是英文的意思准确些)。我们可以通过那些调度方法所返回的 RACDisposable 对象来取消那些 scheduling blocks。
  
  正如前面所说,RACScheduler 是一个类簇。咱们来看看几种具体的调度器:

RACImmediateScheduler

  这是 RAC 内部使用的私有调度器,只支持同步 scheduling。就是简单的马上执行 block。这个调试器的延迟 scheduling 是通过调用 -[NSThread sleepUntilDate:] 来阻塞当前线程来达到目的的。显然,这样一个调度器,没法取消 scheduling,所以它那些方法返回的 disposables 啥也不会做(实际上,它那些 scheduling 方法返回的是nil)。

RACQueueScheduler

  这个调度器使用 GCD 队列来 scheduling blocks。如果你对 GCD 有所了解的话,你会发现这个调度器的功能很简单,它只是在 GCD 队列 dispatching blocks 上的简单封装罢了。

RACSubscriptionScheduler

  这是另一个内部使用的私有调度器。如果当前线程有调度器(调度器可以与线程相关联起来:associated)那它就将 scheduling 转发给这个线程的调度器;否则就转发给默认的 background queue 调试器。

接口

  调试器有下面一些方法:

  scheduling block 如下:

  

5、Subscriber 和 Disposable

  前面介绍了 Disposable 的来源,现在来研究下怎么使用它。还记得吗,订阅者与信号打交道的唯一方式是 RACSignal 中的一个方法:

自定义信号所对应的类是 RACDynamicSignalRACSignal 采用的是类簇模式。除自定义信号之外还有几种其它的信号,之后会研究到。OC 中的 NSNumber 用的就是类簇模式。类簇是Foundation框架中广泛使用的设计模式。类簇将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,以这种方法来组织类可以简化一个面向对象框架的公开架构,而又不减少功能的丰富性。

  咱们来研究一下自定义信号里的这个方法的实现。这个方法实现的难处在于:“一个订阅者可以订阅多个信号,并可以手动拆除其中任何一个订阅”。针对这个问题,提出了上节讲到的 RACDisposable。也就是说,在每一次订阅时,都会返回一个与这次订阅相关的 Disposable,那怎么做到这一点呢?

  给订阅者添加一个 CompoundDisposable 类型的属性 (毕竟 CompoundDisposable 就是用来针对多个 Disposable 的统一管理而存在的),然后在每一次订阅时,都加一个 Disposable 到这个属性里,行不行?但很可惜,订阅者是一个协议 protocol RACSubscriber,而不是一个具体的类,咱们在使用到它时,都是别人实现了这个协议的类的对象,所以咱们不太可能做到说给这么一个未知的类添加一个属性。

事实上,RAC 中确实有 RACSubscriber 这么一个私有类(它是咱们第一个自定义类 NLSubscriber 的原型),咱们叫它做 class RACSubscriber。嗯,class RACSusbscriber 实现了 protocol RACSubscriber 协议:@interface RACSubscriber : NSObject <RACSubscriber>。有没有想到 class NSObjectprotocol NSObject ?虽然它们形式上确实很像,但千万别混为一谈。RAC 中的其它实现了 protocol RACSubscriber 协议的订阅者类可没有一个继承自 class RACSubscriber 的。

  咱们可以用装饰模式来解决这个问题

装饰模式。在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。

订阅者装饰器 RACPassthroughSubscriber

  在订阅者每一次订阅信号时产生一个 Disposable,并将其与此次订阅关联起来,这是通过装饰器 RACPassthroughSubscriber 来做到的。这个装饰器的功能:
  
  1. 包装真正的订阅者,使自己成为订阅者的替代者。
  
  2. 将真正的订阅者与一个订阅时产生的 Disposable 关联起来。
  
  这正是一个装饰器所应该做的。依之前的,咱们来模仿这个装饰器,新建一个咱们的装饰器:NLPassthroughSubscriber,来看下它的代码:

自定义信号 RACDynamicSignal 的订阅方法 subscribe

  咱们来看看 RACDynamicSignal 是怎么来使用 RACPassthroughSubscriber 的,这里就不自己写代码了,直接上它的代码:

  可以看到,订阅者装饰器直接伪装成真正的订阅器,传给 didSubscribe 这个 block 使用。在这个 block 中,会有一些事件发送给订阅者装饰器,而这个订阅者装饰器则根据 disposable 的状态来来决定是否转发给真正的订阅者。disposable 作为返回值,返回给外部,也就是说能够从外部来取消这个订阅了。

  从这几行代码中,我们可以看到,didSubscribe 这个 block 是处于 subscriptionScheduler 这个 scheduler 的调度中。RACSubscriptionScheduler 的调度是取决于当前所在的线程的,即 didSubscribe 可能会在不同的调度器中被执行。

  假设当前 -(RACDisposable *)subscribe:(id<RACSubscriber>)subscriber 这个方法是在异步环境下调用的,那么在 disposable 返回后,在schedule block 还没有来得及调用,此时 disposable 中包含 schedulingDisposable。如果我们此时给 disposable 发送 dispose 消息,那么 schedulingDisposable 也会被 dispose,schedule block 就不会执行了;如果是在 schedule block 执行中或执行后给 disposable 发送 dispose 消息,那么 innerDisposableschedulingDisposable 都会被 dispose。这些行为正是咱们所预期的。

6、再次改进NLSubscriber

1、didSubscribeWithDisposable

  这个 RACSubscriber 协议中声明的一个方法,在最开始的时候被我们特意给忽略,现在是时候回过头来看看它了。对于一个订阅者来说,nexterrorcompleted 三种事件分别对应协议里的三种方法,那么这个方法存在的意义是什么呢?

  从 RACSubscriber 协议中,可以看到,当一个订阅者有收到过 errorcompleted 事件后,这个订阅者就不能再接收任何事件了,换句话说,此时这个订阅者会解除所有的订阅关系,且无法再次订阅。既然要解除所有订阅,首先我得知道我订阅过哪些信号是不?而代表一个订阅行为的就是 disposable ,告诉它就传一个给它好了。所以这个方法就是告诉订阅者:你发生了订阅行为。

  那为啥要 RACCompoundDisposable 类型作为参数呢?因为有些订阅者会针对其附加一些操作,而只有这个类型的 disposable 才能动态加入一些操作。接下来我们就会看到的。

2、NLSubscriber 结合 RACDisposable

  这一次改进 NLSubscriber 的目的是让其可以终结自己的订阅能力的功能。同时实现 didSubscribeWithDisposable 方法。千言万语不如实际代码,让我们来一探究竟:

3、改进类别 nl_Subscription

  还记得么?nl_Subscription 类别中的订阅方法一旦订阅,就无法停止了,这显然有很大的问题。解决这个问题很简单,直接将 disposable 返回即可:
  

RACSignal – Operations

  本节主要研究这些操作(Operations) —— flattenMap:map:filter: ….

  终于看到你想看的东西了?好吧,我承认,上节的东西很无趣,可能压根不是你想看的东西。但如果没弄清上面的内容的话,直接研究 Operations 可是会比较吃力的哟~

  你以为咱们现在开始研究 Operations?哈哈,你又得失望了~ 咱得先看看这两个类:RACEmptySignalRACReturnSignal

1、两个 RACSignal 的特殊子类 RACEmptySignal 和 RACReturnSignal

1、RACEmptySignal

  RACEmptySignal+[RACSignal empty] 的内部实现,一个私有 RACSignal 子类。它就是一个会立即 completed 的信号。让我们来看看它的 - subscribe: 方法:

  这样一个订阅者一订阅就会 completed 信号有什么用呢?稍后揭晓。

2、RACReturnSignal

  RACReturnSignal+[RACSignal return:] 的内部实现,也是一个私有 RACSignal 子类。它会同步发送出一个值(即 next)给订阅者,然后再发送 completed 事件。 它比 RACEmptySignal 多了一点点东西,它是。直接看其实现: