<file_sep><!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>深入理解 KVO | 张不坏的博客</title>
    <meta name="description" content="Just For Fun">
    <link rel="icon" href="/favicon.ico">
    
    <link rel="preload" href="/assets/css/0.styles.37bfab3d.css" as="style"><link rel="preload" href="/assets/js/app.50bca889.js" as="script"><link rel="preload" href="/assets/js/5.081d1701.js" as="script"><link rel="preload" href="/assets/js/107.5af47fd0.js" as="script"><link rel="prefetch" href="/assets/js/10.7bb33f06.js"><link rel="prefetch" href="/assets/js/100.014ff06c.js"><link rel="prefetch" href="/assets/js/101.28f11de0.js"><link rel="prefetch" href="/assets/js/102.be9d2e87.js"><link rel="prefetch" href="/assets/js/103.a1210d81.js"><link rel="prefetch" href="/assets/js/104.7101a956.js"><link rel="prefetch" href="/assets/js/105.833e6f80.js"><link rel="prefetch" href="/assets/js/106.978e1fc0.js"><link rel="prefetch" href="/assets/js/108.efc3ce89.js"><link rel="prefetch" href="/assets/js/109.a69d6b5a.js"><link rel="prefetch" href="/assets/js/11.2de4afc9.js"><link rel="prefetch" href="/assets/js/110.30984b63.js"><link rel="prefetch" href="/assets/js/12.bfc099bd.js"><link rel="prefetch" href="/assets/js/13.20253e0d.js"><link rel="prefetch" href="/assets/js/14.67131b1c.js"><link rel="prefetch" href="/assets/js/15.1af26cbd.js"><link rel="prefetch" href="/assets/js/16.4b261ee0.js"><link rel="prefetch" href="/assets/js/17.1216332f.js"><link rel="prefetch" href="/assets/js/18.c0159773.js"><link rel="prefetch" href="/assets/js/19.0f007f87.js"><link rel="prefetch" href="/assets/js/2.b4633a05.js"><link rel="prefetch" href="/assets/js/20.4b295001.js"><link rel="prefetch" href="/assets/js/21.0c46767c.js"><link rel="prefetch" href="/assets/js/22.a5e065ea.js"><link rel="prefetch" href="/assets/js/23.f43a6a7e.js"><link rel="prefetch" href="/assets/js/24.245f4f15.js"><link rel="prefetch" href="/assets/js/25.618f74a1.js"><link rel="prefetch" href="/assets/js/26.274a606b.js"><link rel="prefetch" href="/assets/js/27.c2d8fe18.js"><link rel="prefetch" href="/assets/js/28.5c522d2a.js"><link rel="prefetch" href="/assets/js/29.c90fdb1a.js"><link rel="prefetch" href="/assets/js/3.9babd8f1.js"><link rel="prefetch" href="/assets/js/30.1ccbdebc.js"><link rel="prefetch" href="/assets/js/31.acf3eca6.js"><link rel="prefetch" href="/assets/js/32.ccfdc859.js"><link rel="prefetch" href="/assets/js/33.9b262756.js"><link rel="prefetch" href="/assets/js/34.c59a4044.js"><link rel="prefetch" href="/assets/js/35.2b10fefb.js"><link rel="prefetch" href="/assets/js/36.2daeeb7b.js"><link rel="prefetch" href="/assets/js/37.d649866c.js"><link rel="prefetch" href="/assets/js/38.aba1ac95.js"><link rel="prefetch" href="/assets/js/39.58a95fd1.js"><link rel="prefetch" href="/assets/js/4.a7413ce2.js"><link rel="prefetch" href="/assets/js/40.8ef4d374.js"><link rel="prefetch" href="/assets/js/41.5799de7a.js"><link rel="prefetch" href="/assets/js/42.b7ee7489.js"><link rel="prefetch" href="/assets/js/43.28a65d64.js"><link rel="prefetch" href="/assets/js/44.90f92ea2.js"><link rel="prefetch" href="/assets/js/45.30b683fd.js"><link rel="prefetch" href="/assets/js/46.f57ccc19.js"><link rel="prefetch" href="/assets/js/47.7a82bd74.js"><link rel="prefetch" href="/assets/js/48.72503020.js"><link rel="prefetch" href="/assets/js/49.3a4ba077.js"><link rel="prefetch" href="/assets/js/50.0c3297f3.js"><link rel="prefetch" href="/assets/js/51.e9ba9363.js"><link rel="prefetch" href="/assets/js/52.473ee9ff.js"><link rel="prefetch" href="/assets/js/53.166d6e7a.js"><link rel="prefetch" href="/assets/js/54.78af3662.js"><link rel="prefetch" href="/assets/js/55.f0d54751.js"><link rel="prefetch" href="/assets/js/56.5de81531.js"><link rel="prefetch" href="/assets/js/57.6e18322f.js"><link rel="prefetch" href="/assets/js/58.1fccc879.js"><link rel="prefetch" href="/assets/js/59.773775e1.js"><link rel="prefetch" href="/assets/js/6.0c9cc532.js"><link rel="prefetch" href="/assets/js/60.0d665185.js"><link rel="prefetch" href="/assets/js/61.d9ae36dc.js"><link rel="prefetch" href="/assets/js/62.fb5e3b65.js"><link rel="prefetch" href="/assets/js/63.5ace8fda.js"><link rel="prefetch" href="/assets/js/64.d44fb0af.js"><link rel="prefetch" href="/assets/js/65.ed8fe56f.js"><link rel="prefetch" href="/assets/js/66.809078da.js"><link rel="prefetch" href="/assets/js/67.2489499e.js"><link rel="prefetch" href="/assets/js/68.e3ee952d.js"><link rel="prefetch" href="/assets/js/69.071411f8.js"><link rel="prefetch" href="/assets/js/7.8188415c.js"><link rel="prefetch" href="/assets/js/70.be8269cf.js"><link rel="prefetch" href="/assets/js/71.a320347a.js"><link rel="prefetch" href="/assets/js/72.f4fda48b.js"><link rel="prefetch" href="/assets/js/73.0f9f9284.js"><link rel="prefetch" href="/assets/js/74.b4028d07.js"><link rel="prefetch" href="/assets/js/75.6d63415f.js"><link rel="prefetch" href="/assets/js/76.d5b4df24.js"><link rel="prefetch" href="/assets/js/77.62b794e1.js"><link rel="prefetch" href="/assets/js/78.63e767ab.js"><link rel="prefetch" href="/assets/js/79.45056905.js"><link rel="prefetch" href="/assets/js/8.20d7cb0f.js"><link rel="prefetch" href="/assets/js/80.e06c5521.js"><link rel="prefetch" href="/assets/js/81.bc82bd01.js"><link rel="prefetch" href="/assets/js/82.4aeb6081.js"><link rel="prefetch" href="/assets/js/83.3ed6146f.js"><link rel="prefetch" href="/assets/js/84.f2aff9f4.js"><link rel="prefetch" href="/assets/js/85.2b8f4e50.js"><link rel="prefetch" href="/assets/js/86.27aea1da.js"><link rel="prefetch" href="/assets/js/87.7f5dc71e.js"><link rel="prefetch" href="/assets/js/88.9ca6511c.js"><link rel="prefetch" href="/assets/js/89.e8f54ad1.js"><link rel="prefetch" href="/assets/js/9.ee6c43f7.js"><link rel="prefetch" href="/assets/js/90.9abac718.js"><link rel="prefetch" href="/assets/js/91.9d8f5f36.js"><link rel="prefetch" href="/assets/js/92.2277b907.js"><link rel="prefetch" href="/assets/js/93.efca2f57.js"><link rel="prefetch" href="/assets/js/94.e9cc0386.js"><link rel="prefetch" href="/assets/js/95.fa3326f7.js"><link rel="prefetch" href="/assets/js/96.82bafc57.js"><link rel="prefetch" href="/assets/js/97.da22d13e.js"><link rel="prefetch" href="/assets/js/98.d745e5ec.js"><link rel="prefetch" href="/assets/js/99.79a6f693.js">
    <link rel="stylesheet" href="/assets/css/0.styles.37bfab3d.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><div class="navbar"><div class="navbar-content"><div class="slogan"><NAME></div> <div class="links"><span class="link-item"><a href="/">首页</a></span> <span class="link-item"><a href="/category/iOS/">iOS</a></span> <span class="link-item"><a href="/category/other/">其他</a></span></div></div></div> <div class="content-header"><div class="post-title">深入理解 KVO</div> <div class="post-info">2015-04-29 • iOS</div></div> <div class="content content__default"><p>程序设计语言中有各种各样的设计模式（pattern）和与此对应的反设计模式（anti-pattern），譬如 singleton、factory、observer、MVC 等等。对于基于 Objective-C 的 iOS 开发而言，有些设计模式几乎已经成为开发环境的一部分，譬如 MVC，自打我们设计第一个页面开始就已经开始与之打交道了；KVO，即 Key-Value Observing（根据我的理解它属于 observer 设计模式）也一样，只是它已经成为 Objective-C 事实标准了，作为一个 iOS 开发者，必须对它有相当的了解。</p> <p>之前对 KVO 的了解仅限于使用层面，没有去想过它是如何实现的，更没有想过它会存在一些坑；甚至在刚接触它时，会尽可能创造机会使用它，譬如监听<code>UITextField#text</code>值的变化；但近几天接触了 Objective-C 的 Runtime 相关的知识，从 Runtime 层面了解到了 KVO 的实现原理（即 KVO 的消息转发机制），也通过阅读各位大神的博客了解到了它的坑。</p> <p>本文首先分析 KVO 和 Runtime 的关系，阐述 KVO 的实现原理；然后结合大神们的博客整理 KVO 存在的坑以及避免掉坑的正确使用姿势。</p> <h2 id="kvo-和-runtime"><a href="#kvo-和-runtime" class="header-anchor">#</a> KVO 和 Runtime</h2> <p>关于 KVO，即 Key-Value Observing，官方文档《Key-Value Observing Programming Guide》里的介绍比较简短明了：</p> <blockquote><p>Key-value observing is a mechanism that allows objects to be notified of changes to specified properties of other objects.</p></blockquote> <p><strong>KVO 的实现</strong></p> <p>KVO 的实现也依赖于 Objective-C 的 Runtime，官方文档《Key-Value Observing Programming Guide》中在《<a href="https://developer.apple.com/library/prerelease/content/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOImplementation.html" target="_blank" rel="noopener noreferrer">Key-Value Observing Implementation Details<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>》部分简单提到它的实现：</p> <blockquote><p>Automatic key-value observing is implemented using a technique called isa-swizzling.
The isa pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.</p></blockquote> <p>简单概述下 KVO 的实现：
当你观察一个对象（称该对象为<strong>被观察对象</strong>）时，一个新的类会动态被创建。这个类继承自<strong>被观察对象</strong>所对应类的，并重写该被观察属性的 setter 方法；针对 setter 方法的重写无非是在赋值语句前后加上相应的通知（或曰方法调用）；最后，把被观察对象的<code>isa</code>指针（<code>isa</code>指针告诉 Runtime 系统这个对象的类是什么）指向这个新创建的中间类，对象就神奇变成了新创建类的实例。</p> <p>根据文档的描述，虽然被观察对象的<code>isa</code>指针被修改了，但是调用其<code>class</code>方法得到的类信息仍然是它之前所继承类的类信息，而不是这个新创建类的类信息。</p> <p>补充：下面对<code>isa</code>指针和类方法<code>class</code>作以更多的说明。</p> <p><code>isa</code>指针和类方法<code>class</code>的返回值都是<code>Class</code>类型，如下：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token keyword">@interface</span> NSObject <span class="token operator">&lt;</span>NSObject<span class="token operator">&gt;</span> <span class="token punctuation">{</span>
    Class isa  OBJC_ISA_AVAILABILITY<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
    
<span class="token operator">+</span> <span class="token punctuation">(</span>Class<span class="token punctuation">)</span>class<span class="token punctuation">;</span>
</code></pre></div><p>根据我的理解，一般情况下，<code>isa</code>指针和<code>class</code>方法返回值都是一样的；但 KVO 底层实现时，动态创建的类只是重写了被观察属性的 setter 方法，并未重写类方法<code>class</code>，因此向被观察者发送<code>class</code>消息实际上仍然调用的是被观察者原先类的类方法<code>+(Class)class</code>，得到的类型信息当然是原先类的类信息，根据我的猜测，<code>isKindOfClass:</code>和<code>isMemberOfClass:</code>与<code>class</code>方法紧密相关。</p> <p>国外的大神 <NAME> 早在 2009 年就做了关于 KVO 的实现细节的探究，更多详细参考<a href="https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html" target="_blank" rel="noopener noreferrer">这里<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>。</p> <h2 id="kvo-的槽点"><a href="#kvo-的槽点" class="header-anchor">#</a> KVO 的槽点</h2> <p>AFNetworking 作者 <NAME> 在<a href="http://nshipster.com/key-value-observing/" target="_blank" rel="noopener noreferrer">Key-Value Observing<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>中说：</p> <blockquote><p>Ask anyone who's been around the NSBlock a few times: Key-Value Observing has the worst API in all of Cocoa.</p></blockquote> <p>另一位不认识的大神在<a href="http://khanlou.com/2013/12/kvo-considered-harmful/" target="_blank" rel="noopener noreferrer">KVO Considered Harmful<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>中也写道：</p> <blockquote><p>KVO, or key-value observing, is a pattern that Cocoa provides for us for subscribing to changes to the properties of other objects. It’s hands down the most poorly designed API in all of Cocoa, and even when implemented perfectly, it’s still an incredibly dangerous tool to use, reserved only for when no other technique will suffice.</p></blockquote> <p>总之，两位大神都认为 KVO 的 API 非常差劲！</p> <p>其中<a href="http://khanlou.com/2013/12/kvo-considered-harmful/" target="_blank" rel="noopener noreferrer">KVO Considered Harmful<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>中对 KVO 的槽点有比较详细的阐述，这一部分内容就取材于此。</p> <p>为了更好说明这些槽点，假设一个应用场景：<code>ZWTableViewController</code>继承自<code>UITableViewController</code>，它现在需要做一件事情，即监测<code>self.tableView.contentSize</code>的变化，现采用典型的方式（即 KVO）处理这么个需求。</p> <p><strong>所有的 observe 处理都放在一个方法里</strong></p> <p>实现上述「监测<code>self.tableView.contentSize</code>的变化」的需求，最基本处理方式是：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token comment">// register observer</span>
<span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>viewDidLoad <span class="token punctuation">{</span>
    <span class="token punctuation">[</span><span class="token keyword">super</span> viewDidLoad<span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">[</span>_tableView addObserver<span class="token punctuation">:</span><span class="token keyword">self</span> forKeyPath<span class="token punctuation">:</span><span class="token string">@&quot;contentSize&quot;</span> options<span class="token punctuation">:</span><span class="token number">0</span> context<span class="token punctuation">:</span><span class="token constant">NULL</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token comment">/* ... */</span>
<span class="token punctuation">}</span>
    
<span class="token comment">// 处理observe</span>
<span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>observeValueForKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath
                      ofObject<span class="token punctuation">:</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>object
                        change<span class="token punctuation">:</span><span class="token punctuation">(</span>NSDictionary <span class="token operator">*</span><span class="token punctuation">)</span>change
                       context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context <span class="token punctuation">{</span>
    
    <span class="token punctuation">[</span><span class="token keyword">self</span> configureView<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div><p>但考虑到<code>observeValueForKeyPath:ofObject:change:context:</code>中可能会很多其他的 observe 事务，所以<code>observeValueForKeyPath:ofObject:change:context:</code>更好的逻辑是：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>observeValueForKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath
                      ofObject<span class="token punctuation">:</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>object
                        change<span class="token punctuation">:</span><span class="token punctuation">(</span>NSDictionary <span class="token operator">*</span><span class="token punctuation">)</span>change
                       context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context <span class="token punctuation">{</span>
    
    <span class="token keyword">if</span> <span class="token punctuation">(</span>object <span class="token operator">==</span> _tableView <span class="token operator">&amp;&amp;</span> <span class="token punctuation">[</span>keyPath isEqualToString<span class="token punctuation">:</span><span class="token string">@&quot;contentSize&quot;</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token punctuation">[</span><span class="token keyword">self</span> configureView<span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p>但如果 KVO 处理的事情种类多且繁杂，这会造成<code>observeValueForKeyPath:ofObject:change:context:</code>代码特别长，极不优雅。</p> <p><strong>严重依赖于 string</strong></p> <p>KVO 严重依赖 string，换句话说，KVO 中的 keyPath 必须是<code>NSString</code>这个事实使得编译器没办法在编译阶段将错误的 keyPath 给找出来；譬如很容易将「contentSize」写成「contentsize」；</p> <p><strong>需要自己处理 superclass 的 observe 事务</strong></p> <p>对于 Objective-C，很多时候 Runtime 系统都会自动帮助处理 superclass 的方法。譬如对于<code>dealloc</code>，假设类<code>Father</code>继承自<code>NSObject</code>，而类<code>Son</code>继承自<code>Father</code>，创建一个<code>Son</code>的实例<code>aSon</code>，在<code>aSon</code>被释放的时候，Runtime 会先调用<code>Son#dealloc</code>，之后会自动调用<code>Father#dealloc</code>，而无需在<code>Son#dealloc</code>中显式执行<code>[super dealloc];</code>。但 KVO 不会这样，所以为了保证父类（父类可能也会自己 observe 事务要处理）的 observe 事务也能被处理，上述<code>observeValueForKeyPath:ofObject:change:context:</code>代码得改成这样：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>observeValueForKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath
                      ofObject<span class="token punctuation">:</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>object
                        change<span class="token punctuation">:</span><span class="token punctuation">(</span>NSDictionary <span class="token operator">*</span><span class="token punctuation">)</span>change
                       context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context <span class="token punctuation">{</span>
    
    <span class="token keyword">if</span> <span class="token punctuation">(</span>object <span class="token operator">==</span> _tableView <span class="token operator">&amp;&amp;</span> <span class="token punctuation">[</span>keyPath isEqualToString<span class="token punctuation">:</span><span class="token string">@&quot;contentSize&quot;</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token punctuation">[</span><span class="token keyword">self</span> configureView<span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
        <span class="token punctuation">[</span><span class="token keyword">super</span> observeValueForKeyPath<span class="token punctuation">:</span>keyPath ofObject<span class="token punctuation">:</span>object change<span class="token punctuation">:</span>change context<span class="token punctuation">:</span>context<span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p><strong>多次相同的 remove observer 会导致 crash</strong></p> <p>写过 KVO 代码的人都知道，对同一个对象执行两次 remove observer 操作会导致程序 crash。</p> <p>在同一个文件中执行两次相同的 remove observer 属于粗心，比较容易 debug 出来；但是跨文件执行两次相同的 remove observer 就不是那么容易发现了。</p> <p>我们一般会在<code>dealloc</code>中进行 remove observer 操作（这也是 Apple 所推荐的）。</p> <p>譬如，假设上述的<code>ZWTableViewController</code>的父类<code>UITableViewController</code>也对<code>tableView.contentSize</code>注册了相同的监听；那么<code>UITableViewController#dealloc</code>中常常会写出如下这样的代码：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token punctuation">[</span>_tableView removeObserver<span class="token punctuation">:</span><span class="token keyword">self</span> forKeyPath<span class="token punctuation">:</span><span class="token string">@&quot;contentSize&quot;</span> context<span class="token punctuation">:</span><span class="token constant">NULL</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
</code></pre></div><p>按照一般习惯，<code>ZWTableViewController#dealloc</code>也会有相同的处理；那么当<code>ZWTableViewController</code>对象被释放时，<code>ZWTableViewController</code>的<code>dealloc</code>和其父类<code>UITableViewController</code>的<code>dealloc</code>都被调用，这样会导致相同的 remove observer 被执行两次，自然会导致 crash。</p> <p><a href="http://khanlou.com/2013/12/kvo-considered-harmful/" target="_blank" rel="noopener noreferrer">KVO Considered Harmful<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>中还有很多其他的槽点，<a href="https://www.mikeash.com/pyblog/key-value-observing-done-right.html" target="_blank" rel="noopener noreferrer">Key-Value Observing Done Right<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>也描述了一些，这里就不多说了，更多信息还是建议看原文。</p> <p>不过好在上述的槽点「严重依赖于 string」和「多次相同的 remove observer 会导致 crash」有比较好的解决方案，如下会讲到。</p> <h2 id="使用-kvo"><a href="#使用-kvo" class="header-anchor">#</a> 使用 KVO</h2> <p>这一部分将阐述 KVO 的使用方法。</p> <h3 id="订阅"><a href="#订阅" class="header-anchor">#</a> 订阅</h3> <p>KVO 中与订阅相关的 API 只有一个：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>addObserver<span class="token punctuation">:</span><span class="token punctuation">(</span>NSObject <span class="token operator">*</span><span class="token punctuation">)</span>observer
         forKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath
            options<span class="token punctuation">:</span><span class="token punctuation">(</span>NSKeyValueObservingOptions<span class="token punctuation">)</span>options
            context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context<span class="token punctuation">;</span>
</code></pre></div><p>对于这四个参数：</p> <ul><li><strong>observer</strong>: The object to register for KVO notifications. The observer must implement the key-value observing method <code>observeValueForKeyPath:ofObject:change:context:</code>.</li> <li><strong>keyPath</strong>: The key path, relative to the receiver, of the property to observe. This value must not be nil.</li> <li><strong>options</strong>: A combination of the NSKeyValueObservingOptions values that specifies what is included in observation notifications. For possible values, see <code>NSKeyValueObservingOptions</code>.</li> <li><strong>context</strong>: Arbitrary data that is passed to observer in <code>observeValueForKeyPath:ofObject:change:context:</code>.</li></ul> <p>大神们认为这个 API 丑陋的重要原因是因为后面两个参数：options 和 context。</p> <p>下面来对这两个参数进行详细介绍。</p> <p>首先是<strong>options</strong>。options 可选值是一个<code>NSKeyValueObservingOptions</code>枚举值，到目前为止，一共包括四个值，在介绍这四个值各自表示的意思之前，先得有一个概念，即 KVO 响应方法有一个<code>NSDictionary</code>类型参数 change（下面<strong>响应</strong>中可以看到），这个字典中会有一个与被监听属性相关的值，譬如被改变之前的值、新值等，<code>NSDictionary</code>中有啥值由订阅时的 options 值决定，options 可取值如下：</p> <ul><li><code>NSKeyValueObservingOptionNew</code>: 指示 change 字典中包含新属性值；</li> <li><code>NSKeyValueObservingOptionOld</code>: 指示 change 字典中包含旧属性值；</li> <li><code>NSKeyValueObservingOptionInitial</code>: 相对复杂一些，NSKeyValueObserving.h 文件中有详细说明，此处略过；</li> <li><code>NSKeyValueObservingOptionPrior</code>: 相对复杂一些，NSKeyValueObserving.h 文件中有详细说明，此处略过；</li></ul> <p>现在细想，options 这个参数也忒复杂了，难怪大神们觉得这个 API 丑陋（不过我等小民之前从未想过这个问题，=_=，没办法，Apple 是个大帝国，我只是其中一个跪舔的小屁民）。</p> <p>不过更糟心的是下面的 context 参数。</p> <p>options 信息量稍大，但其实蛮好理解的，然而对于 context，在写这篇博客之前，一直不知道 context 参数有啥用（也没在意）。</p> <p>context 作用大了去了，在上面的<strong>KVO 的槽点</strong>中提到一个槽点「多次相同的 remove observer 会导致 crash」。导致「多次调用相同的 remove observer」一个很重要的原因是我们经常在 add observer 时为 context 参数赋值<code>NULL</code>，关于如何使用 context 参数，下面会提到。</p> <h3 id="响应"><a href="#响应" class="header-anchor">#</a> 响应</h3> <p>iOS 的 UI 交互（譬如<code>UIButton</code>的一次点击）有一个非常不错的消息转发机制 -- Target-Action 模型，简单来说，为指定的 event 指定 target 和 action 处理方法。</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code>UIButton <span class="token operator">*</span>button <span class="token operator">=</span> <span class="token punctuation">[</span>UIButton new<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">[</span>button addTarget<span class="token punctuation">:</span><span class="token keyword">self</span> action<span class="token punctuation">:</span><span class="token keyword">@selector</span><span class="token punctuation">(</span>buttonDidClicked<span class="token punctuation">:</span><span class="token punctuation">)</span> forControlEvents<span class="token punctuation">:</span>UIControlEventTouchUpInside<span class="token punctuation">]</span><span class="token punctuation">;</span>
</code></pre></div><p>这种 target-action 模型逻辑非常清晰。作为对比，KVO 的响应处理就非常糟糕了，所有的响应都对应是同一个方法<code>observeValueForKeyPath:ofObject:change:context:</code>，其原型如下：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>observeValueForKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath
                      ofObject<span class="token punctuation">:</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>object
                        change<span class="token punctuation">:</span><span class="token punctuation">(</span>NSDictionary <span class="token operator">*</span><span class="token punctuation">)</span>change
                       context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context<span class="token punctuation">;</span>
</code></pre></div><p>除了<code>NSDictionary</code>类型参数 change 之外，其余几个参数都能在<code>addObserver:forKeyPath:options:context:</code>找到对应。</p> <p>change 参数上文已经讲过了，这里不多说了。下面将针对「严重依赖于 string」和「多次相同的 remove observer 会导致 crash」这两个槽点对 keyPath 和 context 参数进行阐述。</p> <p><strong>keyPath</strong>。keyPath 的类型是<code>NSString</code>，这导致了我们使用了错误的 keyPath 而不自知，譬如将<code>@&quot;contentSize&quot;</code>错误写成<code>@&quot;contentsize&quot;</code>，一个更好的方法是不直接使用<code>@&quot;xxxoo&quot;</code>，而是积极使用<code>NSStringFromSelector(SEL aSelector)</code>方法，即改<code>@&quot;contentSize&quot;</code>为<code>NSStringFromSelector(@selector(contentSize))</code>。</p> <p><strong>context</strong>。对于 context，上文已经提到一种场景：假如父类（设为<code>ClassA</code>）和子类（设为<code>ClassB</code>）都监听了同一个对象肿么办？是<code>ClassB</code>处理呢还是交给父类<code>ClassA</code>的<code>observeValueForKeyPath:ofObject:change:context:</code>处理呢？更复杂一点，如果子类的子类（设为<code>ClassC</code>）也监听了同一个对象，当<code>ClassB</code>接收到<code>ClassC</code>的<code>[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];</code>消息时又该如何处理呢？</p> <p>这么一想，KVO 的 API 还真的是设计非常糟糕。一般来说，比较靠谱的做法是自己的屁股自己擦。<code>ClassB</code>的 observe 事务在<code>ClassB</code>中处理，怎么知道是自己的事务还是<code>ClassC</code>传上来的事务呢？用 context 参数判断！</p> <p>在 add observer 时为 context 参数设置一个独一无二的值即可，在 responding 处理时对这个 context 值进行检验。如此就解决了问题，但这需要靠用户（各个层级类的程序员用户）自觉遵守。</p> <h3 id="取消订阅"><a href="#取消订阅" class="header-anchor">#</a> 取消订阅</h3> <p>和<strong>订阅</strong>以及<strong>响应</strong>不同，取消订阅有两个方法：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>removeObserver<span class="token punctuation">:</span><span class="token punctuation">(</span>NSObject <span class="token operator">*</span><span class="token punctuation">)</span>observer forKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context<span class="token punctuation">;</span>
<span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>removeObserver<span class="token punctuation">:</span><span class="token punctuation">(</span>NSObject <span class="token operator">*</span><span class="token punctuation">)</span>observer forKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath<span class="token punctuation">;</span>
</code></pre></div><p>个人觉得应该尽可能使用第一个方法，保持「订阅-响应-取消订阅」的一致性嘛，养成好习惯！</p> <p>此外，为了避免取消订阅时造成的 crash，可以把取消订阅代码放在<code>@try-@catch</code>语句中，如下是一个比较全面的的 KVO 使用示例：</p> <div class="language-objectivec extra-class"><pre class="language-objectivec"><code><span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token operator">*</span> zwContentSize <span class="token operator">=</span> <span class="token operator">&amp;</span>zwContentSize<span class="token punctuation">;</span>
    
<span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>viewDidLoad <span class="token punctuation">{</span>
    
    <span class="token punctuation">[</span><span class="token keyword">super</span> viewDidLoad<span class="token punctuation">]</span><span class="token punctuation">;</span>
    
    <span class="token comment">// 1. subscribe</span>
    <span class="token punctuation">[</span>_tableView addObserver<span class="token punctuation">:</span><span class="token keyword">self</span>
                 forKeyPath<span class="token punctuation">:</span><span class="token function">NSStringFromSelector</span><span class="token punctuation">(</span><span class="token keyword">@selector</span><span class="token punctuation">(</span>contentSize<span class="token punctuation">)</span><span class="token punctuation">)</span>
                    options<span class="token punctuation">:</span>NSKeyValueObservingOptionNew
                    context<span class="token punctuation">:</span>zwContentSize<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
    
<span class="token comment">// 2. responding</span>
<span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>observeValueForKeyPath<span class="token punctuation">:</span><span class="token punctuation">(</span>NSString <span class="token operator">*</span><span class="token punctuation">)</span>keyPath
                      ofObject<span class="token punctuation">:</span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>object
                        change<span class="token punctuation">:</span><span class="token punctuation">(</span>NSDictionary <span class="token operator">*</span><span class="token punctuation">)</span>change
                       context<span class="token punctuation">:</span><span class="token punctuation">(</span><span class="token keyword">void</span> <span class="token operator">*</span><span class="token punctuation">)</span>context <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">(</span>context <span class="token operator">==</span> zwContentSize<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token comment">// configure view</span>
    <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
        <span class="token punctuation">[</span><span class="token keyword">super</span> observeValueForKeyPath<span class="token punctuation">:</span>keyPath ofObject<span class="token punctuation">:</span>object change<span class="token punctuation">:</span>change context<span class="token punctuation">:</span>context<span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
    
<span class="token operator">-</span> <span class="token punctuation">(</span><span class="token keyword">void</span><span class="token punctuation">)</span>dealloc <span class="token punctuation">{</span>
    <span class="token keyword">@try</span> <span class="token punctuation">{</span>
        <span class="token comment">// 3. unsubscribe</span>
        <span class="token punctuation">[</span>_tableView removeObserver<span class="token punctuation">:</span><span class="token keyword">self</span>
                        forKeyPath<span class="token punctuation">:</span><span class="token function">NSStringFromSelector</span><span class="token punctuation">(</span><span class="token keyword">@selector</span><span class="token punctuation">(</span>contentSize<span class="token punctuation">)</span><span class="token punctuation">)</span>
                           context<span class="token punctuation">:</span>zwContentSize<span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
    <span class="token keyword">@catch</span> <span class="token punctuation">(</span>NSException <span class="token operator">*</span>exception<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p>总之，KVO 很强大，但也挺坑，使用它要养成好习惯，避免入坑！</p> <h2 id="本文参考"><a href="#本文参考" class="header-anchor">#</a> 本文参考</h2> <ul><li>《Key-Value Observing Programming Guide》</li> <li><a href="http://tech.glowing.com/cn/implement-kvo/" target="_blank" rel="noopener noreferrer">如何自己动手实现 KVO<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li> <li><a href="https://www.mikeash.com/pyblog/friday-qa-2009-01-23.html" target="_blank" rel="noopener noreferrer">KVO Implementation<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li> <li><a href="https://www.mikeash.com/pyblog/friday-qa-2010-11-6-creating-classes-at-runtime-in-objective-c.html" target="_blank" rel="noopener noreferrer">Creating Classes at Runtime in Objective-C<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li> <li><a href="https://www.mikeash.com/pyblog/key-value-observing-done-right.html" target="_blank" rel="noopener noreferrer">Key-Value Observing Done Right<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li> <li><a href="http://khanlou.com/2013/12/kvo-considered-harmful/" target="_blank" rel="noopener noreferrer">KVO Considered Harmful<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li> <li><a href="http://nshipster.com/key-value-observing/" target="_blank" rel="noopener noreferrer">Key-Value Observing<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li> <li><a href="http://objccn.io/issue-7-3/" target="_blank" rel="noopener noreferrer">KVC 和 KVO<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></li></ul></div></div><div class="global-ui"></div></div>
    <script src="/assets/js/app.50bca889.js" defer></script><script src="/assets/js/5.081d1701.js" defer></script><script src="/assets/js/107.5af47fd0.js" defer></script>
  </body>
</html>
<file_sep><!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>NSError 分析 | 张不坏的博客</title>
    <meta name="description" content="我的第 1 篇博客">
    <link rel="icon" href="/favicon.ico">
    
    <link rel="preload" href="/assets/css/0.styles.37bfab3d.css" as="style"><link rel="preload" href="/assets/js/app.50bca889.js" as="script"><link rel="preload" href="/assets/js/5.081d1701.js" as="script"><link rel="preload" href="/assets/js/87.7f5dc71e.js" as="script"><link rel="prefetch" href="/assets/js/10.7bb33f06.js"><link rel="prefetch" href="/assets/js/100.014ff06c.js"><link rel="prefetch" href="/assets/js/101.28f11de0.js"><link rel="prefetch" href="/assets/js/102.be9d2e87.js"><link rel="prefetch" href="/assets/js/103.a1210d81.js"><link rel="prefetch" href="/assets/js/104.7101a956.js"><link rel="prefetch" href="/assets/js/105.833e6f80.js"><link rel="prefetch" href="/assets/js/106.978e1fc0.js"><link rel="prefetch" href="/assets/js/107.5af47fd0.js"><link rel="prefetch" href="/assets/js/108.efc3ce89.js"><link rel="prefetch" href="/assets/js/109.a69d6b5a.js"><link rel="prefetch" href="/assets/js/11.2de4afc9.js"><link rel="prefetch" href="/assets/js/110.30984b63.js"><link rel="prefetch" href="/assets/js/12.bfc099bd.js"><link rel="prefetch" href="/assets/js/13.20253e0d.js"><link rel="prefetch" href="/assets/js/14.67131b1c.js"><link rel="prefetch" href="/assets/js/15.1af26cbd.js"><link rel="prefetch" href="/assets/js/16.4b261ee0.js"><link rel="prefetch" href="/assets/js/17.1216332f.js"><link rel="prefetch" href="/assets/js/18.c0159773.js"><link rel="prefetch" href="/assets/js/19.0f007f87.js"><link rel="prefetch" href="/assets/js/2.b4633a05.js"><link rel="prefetch" href="/assets/js/20.4b295001.js"><link rel="prefetch" href="/assets/js/21.0c46767c.js"><link rel="prefetch" href="/assets/js/22.a5e065ea.js"><link rel="prefetch" href="/assets/js/23.f43a6a7e.js"><link rel="prefetch" href="/assets/js/24.245f4f15.js"><link rel="prefetch" href="/assets/js/25.618f74a1.js"><link rel="prefetch" href="/assets/js/26.274a606b.js"><link rel="prefetch" href="/assets/js/27.c2d8fe18.js"><link rel="prefetch" href="/assets/js/28.5c522d2a.js"><link rel="prefetch" href="/assets/js/29.c90fdb1a.js"><link rel="prefetch" href="/assets/js/3.9babd8f1.js"><link rel="prefetch" href="/assets/js/30.1ccbdebc.js"><link rel="prefetch" href="/assets/js/31.acf3eca6.js"><link rel="prefetch" href="/assets/js/32.ccfdc859.js"><link rel="prefetch" href="/assets/js/33.9b262756.js"><link rel="prefetch" href="/assets/js/34.c59a4044.js"><link rel="prefetch" href="/assets/js/35.2b10fefb.js"><link rel="prefetch" href="/assets/js/36.2daeeb7b.js"><link rel="prefetch" href="/assets/js/37.d649866c.js"><link rel="prefetch" href="/assets/js/38.aba1ac95.js"><link rel="prefetch" href="/assets/js/39.58a95fd1.js"><link rel="prefetch" href="/assets/js/4.a7413ce2.js"><link rel="prefetch" href="/assets/js/40.8ef4d374.js"><link rel="prefetch" href="/assets/js/41.5799de7a.js"><link rel="prefetch" href="/assets/js/42.b7ee7489.js"><link rel="prefetch" href="/assets/js/43.28a65d64.js"><link rel="prefetch" href="/assets/js/44.90f92ea2.js"><link rel="prefetch" href="/assets/js/45.30b683fd.js"><link rel="prefetch" href="/assets/js/46.f57ccc19.js"><link rel="prefetch" href="/assets/js/47.7a82bd74.js"><link rel="prefetch" href="/assets/js/48.72503020.js"><link rel="prefetch" href="/assets/js/49.3a4ba077.js"><link rel="prefetch" href="/assets/js/50.0c3297f3.js"><link rel="prefetch" href="/assets/js/51.e9ba9363.js"><link rel="prefetch" href="/assets/js/52.473ee9ff.js"><link rel="prefetch" href="/assets/js/53.166d6e7a.js"><link rel="prefetch" href="/assets/js/54.78af3662.js"><link rel="prefetch" href="/assets/js/55.f0d54751.js"><link rel="prefetch" href="/assets/js/56.5de81531.js"><link rel="prefetch" href="/assets/js/57.6e18322f.js"><link rel="prefetch" href="/assets/js/58.1fccc879.js"><link rel="prefetch" href="/assets/js/59.773775e1.js"><link rel="prefetch" href="/assets/js/6.0c9cc532.js"><link rel="prefetch" href="/assets/js/60.0d665185.js"><link rel="prefetch" href="/assets/js/61.d9ae36dc.js"><link rel="prefetch" href="/assets/js/62.fb5e3b65.js"><link rel="prefetch" href="/assets/js/63.5ace8fda.js"><link rel="prefetch" href="/assets/js/64.d44fb0af.js"><link rel="prefetch" href="/assets/js/65.ed8fe56f.js"><link rel="prefetch" href="/assets/js/66.809078da.js"><link rel="prefetch" href="/assets/js/67.2489499e.js"><link rel="prefetch" href="/assets/js/68.e3ee952d.js"><link rel="prefetch" href="/assets/js/69.071411f8.js"><link rel="prefetch" href="/assets/js/7.8188415c.js"><link rel="prefetch" href="/assets/js/70.be8269cf.js"><link rel="prefetch" href="/assets/js/71.a320347a.js"><link rel="prefetch" href="/assets/js/72.f4fda48b.js"><link rel="prefetch" href="/assets/js/73.0f9f9284.js"><link rel="prefetch" href="/assets/js/74.b4028d07.js"><link rel="prefetch" href="/assets/js/75.6d63415f.js"><link rel="prefetch" href="/assets/js/76.d5b4df24.js"><link rel="prefetch" href="/assets/js/77.62b794e1.js"><link rel="prefetch" href="/assets/js/78.63e767ab.js"><link rel="prefetch" href="/assets/js/79.45056905.js"><link rel="prefetch" href="/assets/js/8.20d7cb0f.js"><link rel="prefetch" href="/assets/js/80.e06c5521.js"><link rel="prefetch" href="/assets/js/81.bc82bd01.js"><link rel="prefetch" href="/assets/js/82.4aeb6081.js"><link rel="prefetch" href="/assets/js/83.3ed6146f.js"><link rel="prefetch" href="/assets/js/84.f2aff9f4.js"><link rel="prefetch" href="/assets/js/85.2b8f4e50.js"><link rel="prefetch" href="/assets/js/86.27aea1da.js"><link rel="prefetch" href="/assets/js/88.9ca6511c.js"><link rel="prefetch" href="/assets/js/89.e8f54ad1.js"><link rel="prefetch" href="/assets/js/9.ee6c43f7.js"><link rel="prefetch" href="/assets/js/90.9abac718.js"><link rel="prefetch" href="/assets/js/91.9d8f5f36.js"><link rel="prefetch" href="/assets/js/92.2277b907.js"><link rel="prefetch" href="/assets/js/93.efca2f57.js"><link rel="prefetch" href="/assets/js/94.e9cc0386.js"><link rel="prefetch" href="/assets/js/95.fa3326f7.js"><link rel="prefetch" href="/assets/js/96.82bafc57.js"><link rel="prefetch" href="/assets/js/97.da22d13e.js"><link rel="prefetch" href="/assets/js/98.d745e5ec.js"><link rel="prefetch" href="/assets/js/99.79a6f693.js">
    <link rel="stylesheet" href="/assets/css/0.styles.37bfab3d.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><div class="navbar"><div class="navbar-content"><div class="slogan"><NAME></div> <div class="links"><span class="link-item"><a href="/">首页</a></span> <span class="link-item"><a href="/category/iOS/">iOS</a></span> <span class="link-item"><a href="/category/other/">其他</a></span></div></div></div> <div class="content-header"><div class="post-title">NSError 分析</div> <div class="post-info">2015-01-19 • iOS</div></div> <div class="content content__default"><h2 id="写在前面"><a href="#写在前面" class="header-anchor">#</a> 写在前面</h2> <p>在 iOS 开发中，<code>NSError</code>的使用非常常见，使用也比较简单，也正因为简单，所以对这一部分知识不甚注重。但是近期在做 app 底层网络封装时发现了一些问题。我使用的网络框架是 AFNetworking，AFNetworking 常常会返回一些错误信息，有时需要将这些错误信息告诉用户，通常做法是将<code>NSError#localizedDescription</code>以弹框的形式原原本本展现给用户（譬如“网络不畅”之类的），但是这样非常不友好，一是这些信息往往是英文，二是这些信息过于冗长，不够简洁。所以自然想到了对这些 error 信息进行包装。这就迫使我不得不去了解更多关于 NSError 相关的信息，本文着重叙述 Error Domain 和 Error Code。</p> <h2 id="error-domain"><a href="#error-domain" class="header-anchor">#</a> Error Domain</h2> <p>首先，error domain 是一个字符串。因为历史原因，在 OS X 中将 errors 分为不同的 domains。譬如，对于 Carbon 框架的 error，归于 OSStatus domain（<code>NSOSStatusErrorDomain</code>），对于 POSIX error，归于<code>NSPOSIXErrorDomain</code>，而对于我们的 iOS 开发，一般使用<code>NSCocoaErrorDomain</code>。NSError.h 定义了四个 domain，如下：</p> <div class="language-c extra-class"><pre class="language-c"><code><span class="token comment">// Predefined domain for errors from most AppKit and Foundation APIs.</span>
FOUNDATION_EXPORT NSString <span class="token operator">*</span><span class="token keyword">const</span> NSCocoaErrorDomain<span class="token punctuation">;</span>
    
<span class="token comment">// Other predefined domains; value of &quot;code&quot; will correspond to preexisting values in these domains.</span>
FOUNDATION_EXPORT NSString <span class="token operator">*</span><span class="token keyword">const</span> NSPOSIXErrorDomain<span class="token punctuation">;</span>
FOUNDATION_EXPORT NSString <span class="token operator">*</span><span class="token keyword">const</span> NSOSStatusErrorDomain<span class="token punctuation">;</span>
FOUNDATION_EXPORT NSString <span class="token operator">*</span><span class="token keyword">const</span> NSMachErrorDomain<span class="token punctuation">;</span>
</code></pre></div><p>除了上述的四个 domain 之外，不同的 framework 甚至一些 classes group（相关的几个 classes）也定义了自己的 domain，譬如对于 Web Kit framework，定义了<code>WebKitErrorDomain</code>，而更常见的，URL 相关的 classes 定义了<code>NSURLErrorDomain</code>。</p> <p>Domains 非常有用，特别当程序非常复杂庞大时，官方文档是这么说的：</p> <blockquote><p>Domains serve several useful purposes. They give Cocoa programs a way to identify the OS X subsystem that is detecting an error. They also help to prevent collisions between error codes from different subsystems with the same numeric value. In addition, domains allow for a causal relationship between error codes based on the layering of subsystems; for example, an error in the NSOSStatusErrorDomain may have an underlying error in the NSMachErrorDomain.</p></blockquote> <p>用户也可以为自己的 framework 或者 app 定义自己的 domain，官方推荐的 domain 命名规则是：
<code>com.company.framework_or_app.ErrorDomain</code>。</p> <h2 id="error-code"><a href="#error-code" class="header-anchor">#</a> Error Code</h2> <p>Error Code 的类型是 signed integer。Error Code 指定了特殊的错误。这个信息对于程序开发来说极为有用。比如访问 URL 资源 timeout 错误对应的是<code>NSURLErrorTimedOut</code>（<code>-1001</code>）。</p> <p>那么如何知道各个 error code 对应什么样的值呢？iOS 开发中常用的 error code 所对应的头文件如下：</p> <ul><li>Foundation/FoundationErrors.h - Generic Foundation error codes</li> <li>CoreData/CoreDataErrors.h - Core Data error codes</li> <li>Foundation/NSURLError.h - URL error codes</li></ul> <p>以 Foundation/NSURLError.h 为例，其中的 URLError Code 值从<code>NSURLErrorDataLengthExceedsMaximum</code>到<code>NSURLErrorCancelled</code>，二者分别对应<code>-1103</code>和<code>-999</code>。如果对所有网络 error 笼统处理，这两个值可以为我所用。</p> <h2 id="the-user-info-dictionary"><a href="#the-user-info-dictionary" class="header-anchor">#</a> The User Info Dictionary</h2> <p>Every NSError object has a “user info” dictionary to hold error information beyond domain and code.</p> <p>User info 可以包含很多自定义信息，最常用的或许是 localized error information。访问 localized error information 有两种方式，其一是访问 NSError 的 localizedDescription 属性，其二是访问<code>NSError#userInfo</code>的<code>NSLocalizedDescriptionKey</code>域。</p> <p>关于 user info dictionary，比较常见，这里不多讲了，更多内容参考《Error Handling Programming Guide》。</p></div></div><div class="global-ui"></div></div>
    <script src="/assets/js/app.50bca889.js" defer></script><script src="/assets/js/5.081d1701.js" defer></script><script src="/assets/js/87.7f5dc71e.js" defer></script>
  </body>
</html>
<file_sep><!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Cookie &amp; Session | 张不坏的博客</title>
    <meta name="description" content="Just For Fun">
    <link rel="icon" href="/favicon.ico">
    
    <link rel="preload" href="/assets/css/0.styles.37bfab3d.css" as="style"><link rel="preload" href="/assets/js/app.50bca889.js" as="script"><link rel="preload" href="/assets/js/5.081d1701.js" as="script"><link rel="preload" href="/assets/js/57.6e18322f.js" as="script"><link rel="preload" href="/assets/js/4.a7413ce2.js" as="script"><link rel="prefetch" href="/assets/js/10.7bb33f06.js"><link rel="prefetch" href="/assets/js/100.014ff06c.js"><link rel="prefetch" href="/assets/js/101.28f11de0.js"><link rel="prefetch" href="/assets/js/102.be9d2e87.js"><link rel="prefetch" href="/assets/js/103.a1210d81.js"><link rel="prefetch" href="/assets/js/104.7101a956.js"><link rel="prefetch" href="/assets/js/105.833e6f80.js"><link rel="prefetch" href="/assets/js/106.978e1fc0.js"><link rel="prefetch" href="/assets/js/107.5af47fd0.js"><link rel="prefetch" href="/assets/js/108.efc3ce89.js"><link rel="prefetch" href="/assets/js/109.a69d6b5a.js"><link rel="prefetch" href="/assets/js/11.2de4afc9.js"><link rel="prefetch" href="/assets/js/110.30984b63.js"><link rel="prefetch" href="/assets/js/12.bfc099bd.js"><link rel="prefetch" href="/assets/js/13.20253e0d.js"><link rel="prefetch" href="/assets/js/14.67131b1c.js"><link rel="prefetch" href="/assets/js/15.1af26cbd.js"><link rel="prefetch" href="/assets/js/16.4b261ee0.js"><link rel="prefetch" href="/assets/js/17.1216332f.js"><link rel="prefetch" href="/assets/js/18.c0159773.js"><link rel="prefetch" href="/assets/js/19.0f007f87.js"><link rel="prefetch" href="/assets/js/2.b4633a05.js"><link rel="prefetch" href="/assets/js/20.4b295001.js"><link rel="prefetch" href="/assets/js/21.0c46767c.js"><link rel="prefetch" href="/assets/js/22.a5e065ea.js"><link rel="prefetch" href="/assets/js/23.f43a6a7e.js"><link rel="prefetch" href="/assets/js/24.245f4f15.js"><link rel="prefetch" href="/assets/js/25.618f74a1.js"><link rel="prefetch" href="/assets/js/26.274a606b.js"><link rel="prefetch" href="/assets/js/27.c2d8fe18.js"><link rel="prefetch" href="/assets/js/28.5c522d2a.js"><link rel="prefetch" href="/assets/js/29.c90fdb1a.js"><link rel="prefetch" href="/assets/js/3.9babd8f1.js"><link rel="prefetch" href="/assets/js/30.1ccbdebc.js"><link rel="prefetch" href="/assets/js/31.acf3eca6.js"><link rel="prefetch" href="/assets/js/32.ccfdc859.js"><link rel="prefetch" href="/assets/js/33.9b262756.js"><link rel="prefetch" href="/assets/js/34.c59a4044.js"><link rel="prefetch" href="/assets/js/35.2b10fefb.js"><link rel="prefetch" href="/assets/js/36.2daeeb7b.js"><link rel="prefetch" href="/assets/js/37.d649866c.js"><link rel="prefetch" href="/assets/js/38.aba1ac95.js"><link rel="prefetch" href="/assets/js/39.58a95fd1.js"><link rel="prefetch" href="/assets/js/40.8ef4d374.js"><link rel="prefetch" href="/assets/js/41.5799de7a.js"><link rel="prefetch" href="/assets/js/42.b7ee7489.js"><link rel="prefetch" href="/assets/js/43.28a65d64.js"><link rel="prefetch" href="/assets/js/44.90f92ea2.js"><link rel="prefetch" href="/assets/js/45.30b683fd.js"><link rel="prefetch" href="/assets/js/46.f57ccc19.js"><link rel="prefetch" href="/assets/js/47.7a82bd74.js"><link rel="prefetch" href="/assets/js/48.72503020.js"><link rel="prefetch" href="/assets/js/49.3a4ba077.js"><link rel="prefetch" href="/assets/js/50.0c3297f3.js"><link rel="prefetch" href="/assets/js/51.e9ba9363.js"><link rel="prefetch" href="/assets/js/52.473ee9ff.js"><link rel="prefetch" href="/assets/js/53.166d6e7a.js"><link rel="prefetch" href="/assets/js/54.78af3662.js"><link rel="prefetch" href="/assets/js/55.f0d54751.js"><link rel="prefetch" href="/assets/js/56.5de81531.js"><link rel="prefetch" href="/assets/js/58.1fccc879.js"><link rel="prefetch" href="/assets/js/59.773775e1.js"><link rel="prefetch" href="/assets/js/6.0c9cc532.js"><link rel="prefetch" href="/assets/js/60.0d665185.js"><link rel="prefetch" href="/assets/js/61.d9ae36dc.js"><link rel="prefetch" href="/assets/js/62.fb5e3b65.js"><link rel="prefetch" href="/assets/js/63.5ace8fda.js"><link rel="prefetch" href="/assets/js/64.d44fb0af.js"><link rel="prefetch" href="/assets/js/65.ed8fe56f.js"><link rel="prefetch" href="/assets/js/66.809078da.js"><link rel="prefetch" href="/assets/js/67.2489499e.js"><link rel="prefetch" href="/assets/js/68.e3ee952d.js"><link rel="prefetch" href="/assets/js/69.071411f8.js"><link rel="prefetch" href="/assets/js/7.8188415c.js"><link rel="prefetch" href="/assets/js/70.be8269cf.js"><link rel="prefetch" href="/assets/js/71.a320347a.js"><link rel="prefetch" href="/assets/js/72.f4fda48b.js"><link rel="prefetch" href="/assets/js/73.0f9f9284.js"><link rel="prefetch" href="/assets/js/74.b4028d07.js"><link rel="prefetch" href="/assets/js/75.6d63415f.js"><link rel="prefetch" href="/assets/js/76.d5b4df24.js"><link rel="prefetch" href="/assets/js/77.62b794e1.js"><link rel="prefetch" href="/assets/js/78.63e767ab.js"><link rel="prefetch" href="/assets/js/79.45056905.js"><link rel="prefetch" href="/assets/js/8.20d7cb0f.js"><link rel="prefetch" href="/assets/js/80.e06c5521.js"><link rel="prefetch" href="/assets/js/81.bc82bd01.js"><link rel="prefetch" href="/assets/js/82.4aeb6081.js"><link rel="prefetch" href="/assets/js/83.3ed6146f.js"><link rel="prefetch" href="/assets/js/84.f2aff9f4.js"><link rel="prefetch" href="/assets/js/85.2b8f4e50.js"><link rel="prefetch" href="/assets/js/86.27aea1da.js"><link rel="prefetch" href="/assets/js/87.7f5dc71e.js"><link rel="prefetch" href="/assets/js/88.9ca6511c.js"><link rel="prefetch" href="/assets/js/89.e8f54ad1.js"><link rel="prefetch" href="/assets/js/9.ee6c43f7.js"><link rel="prefetch" href="/assets/js/90.9abac718.js"><link rel="prefetch" href="/assets/js/91.9d8f5f36.js"><link rel="prefetch" href="/assets/js/92.2277b907.js"><link rel="prefetch" href="/assets/js/93.efca2f57.js"><link rel="prefetch" href="/assets/js/94.e9cc0386.js"><link rel="prefetch" href="/assets/js/95.fa3326f7.js"><link rel="prefetch" href="/assets/js/96.82bafc57.js"><link rel="prefetch" href="/assets/js/97.da22d13e.js"><link rel="prefetch" href="/assets/js/98.d745e5ec.js"><link rel="prefetch" href="/assets/js/99.79a6f693.js">
    <link rel="stylesheet" href="/assets/css/0.styles.37bfab3d.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><div class="navbar"><div class="navbar-content"><div class="slogan"><NAME></div> <div class="links"><span class="link-item"><a href="/">首页</a></span> <span class="link-item"><a href="/category/iOS/">iOS</a></span> <span class="link-item"><a href="/category/other/">其他</a></span></div></div></div> <div class="content-header"><div class="post-title">Cookie &amp; Session</div> <div class="post-info">2018-05-20</div></div> <div class="content content__default"><p>最近写 Web 服务，过程中梳理了一下 Cookie 和 Session 的知识，完成度勉强可以，于是以博客的方式进行展示。</p> <p>HTTP 请求都是无状态的，但 Web 应用通常都需要记录一些状态信息，譬如知道发起请求的人是谁。Cookie 和 Session 正是用来弥补用户状态追踪的两种机制；它们的区别是啥呢？博文<a href="https://justsee.iteye.com/blog/1570652" target="_blank" rel="noopener noreferrer">Session 机制详解<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>里举了几个非常精彩的例子，直接摘抄如下：</p> <div class="custom-block tip"><p class="custom-block-title">🌰</p> <p>让我们用几个例子来描述一下 cookie 和 session 机制之间的区别与联系。笔者曾经常去的一家咖啡店有喝5杯咖啡免费赠一杯咖啡的优惠，然而一次性消费5杯咖啡的机会微乎其微，这时就需要某种方式来纪录某位顾客的消费数量。想象一下其实也无外乎下面的几种方案：</p> <p>1、该店的店员很厉害，能记住每位顾客的消费数量，只要顾客一走进咖啡店，店员就知道该怎么对待了。这种做法就是协议本身支持状态。</p> <p>2、发给顾客一张卡片，上面记录着消费的数量，一般还有个有效期限。每次消费时，如果顾客出示这张卡片，则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。</p> <p>3、发给顾客一张会员卡，除了卡号之外什么信息也不纪录，每次消费时，如果顾客出示该卡片，则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。</p> <p>由于HTTP协议是无状态的，而出于种种考虑也不希望使之成为有状态的，因此，后面两种方案就成为现实的选择。具体来说cookie机制采用的是在客户端保持状态的方案，而session机制采用的是在服务器端保持状态的方案。同时我们也看到，由于采用服务器端保持状态的方案在客户端也需要保存一个标识，所以session机制可能需要借助于cookie机制来达到保存标识的目的，但实际上它还有其他选择。</p></div> <h1 id="cookie"><a href="#cookie" class="header-anchor">#</a> Cookie</h1> <h2 id="相关首部"><a href="#相关首部" class="header-anchor">#</a> 相关首部</h2> <p>站在 HTTP 协议的角度来看，HTTP 协议中有两个与 Cookie 直接相关首部：</p> <ul><li>Set-Cookie，由 response 携带，指示 client 存储 cookie 到本地</li> <li>Cookie，由 request 携带，将本地 cookie 传给 server</li></ul> <p>Server 发送 response 时，通过 Set-Cookie 首部，告诉客户端（譬如浏览器）将 cookie 内容给存储起来。该字段包括如下常用属性用来指导「客户端如何存储」：</p> <ul><li>Domain：cookie 适用的域名，若不指定，则默认为创建 cookie 的服务器的域名</li> <li>Path：cookie 适用的 path</li> <li>Expires：到期时间，若不指定，则浏览器关闭时即删除</li> <li>Secure：指示是否仅当在 HTTPS 链接上才传输</li> <li>HttpOnly：加以限制，使得 cookie 不能被 JavaScript 脚本访问（意味着不能 JS 不能通过<code>document.cookie</code>的方式访问 cookie）</li></ul> <p>这几个常用属性都不难理解，其中 Domain 和 Path 限制了 cookie 的使用范围，浏览器在发起 request 的时候，会根据这两个属性将符合使用范围的 cookie 发给 server；Secure 指示是否在 HTTPS 环境下才能传输。</p> <p>事实上，Cookie 还有更多其他属性，只是不怎么常用；之前的博客<a href="/post/http-draft.html">HTTP 碎片信息</a>对 Cookie 的发展史和字段详情有更详细的描述。</p> <p>下图是使用 Charles 抓包的一个 Set-Cookie 信息样例：</p> <div class="custom-image-wrapper" data-v-339c7bd5><img src="/image/charles-set-cookie.png" srcset="/image/charles-set-cookie.png 2x" data-v-339c7bd5></div> <p>客户端在发起 request 请求时，会根据 Domain 和 Path 将符合规则的本地 Cookie 内容给找出来发送给 server，内容存储在 Cookie 首部中，如下图：</p> <div class="custom-image-wrapper" data-v-339c7bd5><img src="/image/charles-cookie.png" srcset="/image/charles-cookie.png 2x" data-v-339c7bd5></div> <h2 id="持久-cookie-会话-cookie"><a href="#持久-cookie-会话-cookie" class="header-anchor">#</a> 持久 Cookie &amp; 会话 Cookie</h2> <p>从存活时长来看，Cookie 通常被笼统划分为两种类型：会话 cookie、持久 cookie。前者是一种临时 cookie，会话结束（一般是浏览器被关闭）时，cookie 就被删除了。持久 cookie 的生存时间更长一些，它们存储在硬盘上，应用程序退出时，仍然会保留它们。</p> <p>简单来说，当设置了 Discard，或者没有设置 Expires 或 Max-Age，就意味着这个 cookie 是一个会话 cookie。</p> <h1 id="session"><a href="#session" class="header-anchor">#</a> Session</h1> <p>关于 Cookie 和 Session 的对比，一种常见的说法是：</p> <ul><li>Cookie：在客户端保存用户状态</li> <li>Session：在服务端保存用户状态</li></ul> <p>在我看来，第一句没问题，第二句不是很准确。为啥不准确呢？Cookie 是一种事实上的标准，但 Session 不是，它只是一种比较常见的概念，没有所谓的实现标准；换句话说，不同的服务框架，Session 的实现机制可能是不一样的，它可能被保存在服务端，也可能保存在客户端。</p> <h2 id="session-的实现机制"><a href="#session-的实现机制" class="header-anchor">#</a> Session 的实现机制</h2> <p>这一部分讲述 Session 的常见实现机制。据我所知，从存储的角度看，session 的常见实现机制有两种。</p> <p>第一种姿势：session 信息存储在服务端。这种实现机制中，服务端负责生成和管理 session 信息，每个 session 会有一个 session id，server 返回 response 给 client 时会让后者以 cookie 的形式存储 session id，后续发起 request 请求时，都会在 HTTP 报文的 Cookie 首部中携带 session id，server 根据 id 找到对应的 session 信息。</p> <p>第二种姿势：session 信息存储在客户端。这种实现机制中，服务端负责生成 session 信息，但不存储，在 response 中，通过 Set-Cookie 首部让 client 把 session 信息（一般都进行了加密）存在 cookie 中，后续 client 发起 request 请求时，server 从 Cookie 首部取出 session 信息进行解密读取，以及进行各种操作，然后再通过 Set-Cookie 回写到 client。</p> <p>这两种姿势有何优劣？如下谈谈个人的理解。</p> <p>从客户端角度看，第一种姿势 client 承担的任务更轻一些，毕竟只需管理 session id，第二种姿势负责管理的信息更重一些：整个 session 信息，虽然可能是加密压缩的。当 session 信息比较大时，负载无疑更重。</p> <p>第一种姿势更常见，好处貌似更明显；为啥还有第二种姿势存在呢？</p> <p>对于轻量级的应用，以线程或协程为粒度处理 request 的服务框架（譬如 Java、Go 生态），一般都采用第一种。而笔者实践过的 Egg.js 框架，提供的默认 session 实现机制是第二种。我认为很重要的原因是 Node 无法提供线程服务，为了提高服务端响应速度，通常开启多个 node 进程，进程之间共享数据却是个麻烦，把 session 数据放在客户端能规避这个麻烦，服务端基本上无需在管理 session 上费心，因为 session 已然是 request 的一部分。</p> <p>然而，对于大型分布式应用，一般都采用第一种姿势，在分布式场景下，管理 session 是一个重要的议题，而 Redis 在其中扮演者重要角色，本文就不展开描述了。</p></div></div><div class="global-ui"></div></div>
    <script src="/assets/js/app.50bca889.js" defer></script><script src="/assets/js/5.081d1701.js" defer></script><script src="/assets/js/57.6e18322f.js" defer></script><script src="/assets/js/4.a7413ce2.js" defer></script>
  </body>
</html>
<file_sep><!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>HTTPS 碎片信息 | 张不坏的博客</title>
    <meta name="description" content="Just For Fun">
    <link rel="icon" href="/favicon.ico">
    
    <link rel="preload" href="/assets/css/0.styles.37bfab3d.css" as="style"><link rel="preload" href="/assets/js/app.50bca889.js" as="script"><link rel="preload" href="/assets/js/5.081d1701.js" as="script"><link rel="preload" href="/assets/js/72.f4fda48b.js" as="script"><link rel="preload" href="/assets/js/4.a7413ce2.js" as="script"><link rel="prefetch" href="/assets/js/10.7bb33f06.js"><link rel="prefetch" href="/assets/js/100.014ff06c.js"><link rel="prefetch" href="/assets/js/101.28f11de0.js"><link rel="prefetch" href="/assets/js/102.be9d2e87.js"><link rel="prefetch" href="/assets/js/103.a1210d81.js"><link rel="prefetch" href="/assets/js/104.7101a956.js"><link rel="prefetch" href="/assets/js/105.833e6f80.js"><link rel="prefetch" href="/assets/js/106.978e1fc0.js"><link rel="prefetch" href="/assets/js/107.5af47fd0.js"><link rel="prefetch" href="/assets/js/108.efc3ce89.js"><link rel="prefetch" href="/assets/js/109.a69d6b5a.js"><link rel="prefetch" href="/assets/js/11.2de4afc9.js"><link rel="prefetch" href="/assets/js/110.30984b63.js"><link rel="prefetch" href="/assets/js/12.bfc099bd.js"><link rel="prefetch" href="/assets/js/13.20253e0d.js"><link rel="prefetch" href="/assets/js/14.67131b1c.js"><link rel="prefetch" href="/assets/js/15.1af26cbd.js"><link rel="prefetch" href="/assets/js/16.4b261ee0.js"><link rel="prefetch" href="/assets/js/17.1216332f.js"><link rel="prefetch" href="/assets/js/18.c0159773.js"><link rel="prefetch" href="/assets/js/19.0f007f87.js"><link rel="prefetch" href="/assets/js/2.b4633a05.js"><link rel="prefetch" href="/assets/js/20.4b295001.js"><link rel="prefetch" href="/assets/js/21.0c46767c.js"><link rel="prefetch" href="/assets/js/22.a5e065ea.js"><link rel="prefetch" href="/assets/js/23.f43a6a7e.js"><link rel="prefetch" href="/assets/js/24.245f4f15.js"><link rel="prefetch" href="/assets/js/25.618f74a1.js"><link rel="prefetch" href="/assets/js/26.274a606b.js"><link rel="prefetch" href="/assets/js/27.c2d8fe18.js"><link rel="prefetch" href="/assets/js/28.5c522d2a.js"><link rel="prefetch" href="/assets/js/29.c90fdb1a.js"><link rel="prefetch" href="/assets/js/3.9babd8f1.js"><link rel="prefetch" href="/assets/js/30.1ccbdebc.js"><link rel="prefetch" href="/assets/js/31.acf3eca6.js"><link rel="prefetch" href="/assets/js/32.ccfdc859.js"><link rel="prefetch" href="/assets/js/33.9b262756.js"><link rel="prefetch" href="/assets/js/34.c59a4044.js"><link rel="prefetch" href="/assets/js/35.2b10fefb.js"><link rel="prefetch" href="/assets/js/36.2daeeb7b.js"><link rel="prefetch" href="/assets/js/37.d649866c.js"><link rel="prefetch" href="/assets/js/38.aba1ac95.js"><link rel="prefetch" href="/assets/js/39.58a95fd1.js"><link rel="prefetch" href="/assets/js/40.8ef4d374.js"><link rel="prefetch" href="/assets/js/41.5799de7a.js"><link rel="prefetch" href="/assets/js/42.b7ee7489.js"><link rel="prefetch" href="/assets/js/43.28a65d64.js"><link rel="prefetch" href="/assets/js/44.90f92ea2.js"><link rel="prefetch" href="/assets/js/45.30b683fd.js"><link rel="prefetch" href="/assets/js/46.f57ccc19.js"><link rel="prefetch" href="/assets/js/47.7a82bd74.js"><link rel="prefetch" href="/assets/js/48.72503020.js"><link rel="prefetch" href="/assets/js/49.3a4ba077.js"><link rel="prefetch" href="/assets/js/50.0c3297f3.js"><link rel="prefetch" href="/assets/js/51.e9ba9363.js"><link rel="prefetch" href="/assets/js/52.473ee9ff.js"><link rel="prefetch" href="/assets/js/53.166d6e7a.js"><link rel="prefetch" href="/assets/js/54.78af3662.js"><link rel="prefetch" href="/assets/js/55.f0d54751.js"><link rel="prefetch" href="/assets/js/56.5de81531.js"><link rel="prefetch" href="/assets/js/57.6e18322f.js"><link rel="prefetch" href="/assets/js/58.1fccc879.js"><link rel="prefetch" href="/assets/js/59.773775e1.js"><link rel="prefetch" href="/assets/js/6.0c9cc532.js"><link rel="prefetch" href="/assets/js/60.0d665185.js"><link rel="prefetch" href="/assets/js/61.d9ae36dc.js"><link rel="prefetch" href="/assets/js/62.fb5e3b65.js"><link rel="prefetch" href="/assets/js/63.5ace8fda.js"><link rel="prefetch" href="/assets/js/64.d44fb0af.js"><link rel="prefetch" href="/assets/js/65.ed8fe56f.js"><link rel="prefetch" href="/assets/js/66.809078da.js"><link rel="prefetch" href="/assets/js/67.2489499e.js"><link rel="prefetch" href="/assets/js/68.e3ee952d.js"><link rel="prefetch" href="/assets/js/69.071411f8.js"><link rel="prefetch" href="/assets/js/7.8188415c.js"><link rel="prefetch" href="/assets/js/70.be8269cf.js"><link rel="prefetch" href="/assets/js/71.a320347a.js"><link rel="prefetch" href="/assets/js/73.0f9f9284.js"><link rel="prefetch" href="/assets/js/74.b4028d07.js"><link rel="prefetch" href="/assets/js/75.6d63415f.js"><link rel="prefetch" href="/assets/js/76.d5b4df24.js"><link rel="prefetch" href="/assets/js/77.62b794e1.js"><link rel="prefetch" href="/assets/js/78.63e767ab.js"><link rel="prefetch" href="/assets/js/79.45056905.js"><link rel="prefetch" href="/assets/js/8.20d7cb0f.js"><link rel="prefetch" href="/assets/js/80.e06c5521.js"><link rel="prefetch" href="/assets/js/81.bc82bd01.js"><link rel="prefetch" href="/assets/js/82.4aeb6081.js"><link rel="prefetch" href="/assets/js/83.3ed6146f.js"><link rel="prefetch" href="/assets/js/84.f2aff9f4.js"><link rel="prefetch" href="/assets/js/85.2b8f4e50.js"><link rel="prefetch" href="/assets/js/86.27aea1da.js"><link rel="prefetch" href="/assets/js/87.7f5dc71e.js"><link rel="prefetch" href="/assets/js/88.9ca6511c.js"><link rel="prefetch" href="/assets/js/89.e8f54ad1.js"><link rel="prefetch" href="/assets/js/9.ee6c43f7.js"><link rel="prefetch" href="/assets/js/90.9abac718.js"><link rel="prefetch" href="/assets/js/91.9d8f5f36.js"><link rel="prefetch" href="/assets/js/92.2277b907.js"><link rel="prefetch" href="/assets/js/93.efca2f57.js"><link rel="prefetch" href="/assets/js/94.e9cc0386.js"><link rel="prefetch" href="/assets/js/95.fa3326f7.js"><link rel="prefetch" href="/assets/js/96.82bafc57.js"><link rel="prefetch" href="/assets/js/97.da22d13e.js"><link rel="prefetch" href="/assets/js/98.d745e5ec.js"><link rel="prefetch" href="/assets/js/99.79a6f693.js">
    <link rel="stylesheet" href="/assets/css/0.styles.37bfab3d.css">
  </head>
  <body>
    <div id="app" data-server-rendered="true"><div class="theme-container"><div class="navbar"><div class="navbar-content"><div class="slogan"><NAME></div> <div class="links"><span class="link-item"><a href="/">首页</a></span> <span class="link-item"><a href="/category/iOS/">iOS</a></span> <span class="link-item"><a href="/category/other/">其他</a></span></div></div></div> <div class="content-header"><div class="post-title">HTTPS 碎片信息</div> <div class="post-info">2017-02-26</div></div> <div class="content content__default"><p>本文是我了解 HTTPS 过程中记录的一些碎片信息。</p> <h2 id="基本概念"><a href="#基本概念" class="header-anchor">#</a> 基本概念</h2> <ul><li>对称密钥加密系统：编/解码使用相同密钥的算法</li> <li>不对称密钥加密系统：编/解码使用不同密钥的算法</li> <li>公开密钥加密系统：一种能够使百万计算机便携地发送机密报文的系统</li> <li>数字签名：用来验证报文未被伪造或篡改的校验和</li> <li>数字证书：由一个可信的组织验证和签发的识别信息</li></ul> <h3 id="对称密钥加密技术"><a href="#对称密钥加密技术" class="header-anchor">#</a> 对称密钥加密技术</h3> <p>保持密钥的机密状态是很重要的。在很多情况下，编/解码算法都是众所周知的，密钥是唯一保密的东西。</p> <p>好的加密算法会迫使攻击者尝遍每一个可能的密钥，才能破解代码。</p> <p>可用密钥值的数量取决于密钥的位数，以及可能的密钥中有多少位是有效的。有些加密技术中只有部分密钥是有效的。</p> <p><strong>枚举攻击</strong>：用暴力去尝试所有的密钥值称为枚举攻击（enumeration attack）。</p> <p>对称密钥加密技术的痛点之一：发送者和接收者在互相对话之前，一定要有一个共享的保密密钥。如果有 N 个节点，每个节点都要和其他所有 N-1 个节点进行安全对话，总共大概会有 N*N 个保密密钥，这几乎无法管理。</p> <p>常见的对称加密算法有 DES、3DES、AES、Blowfish、IDEA、RC5、RC6。</p> <h3 id="公开密钥加密技术"><a href="#公开密钥加密技术" class="header-anchor">#</a> 公开密钥加密技术</h3> <p>公开密钥加密技术没有为每对主机使用单独的加密/解密密钥，而是使用了两个非对称密钥：一个用来对主机报文编码，另一个用来对主机报文解码。</p> <p>编码密钥是众所周知的，但是解码密钥只能为报文的目标主机所持有。</p> <p>P.S: 公开密钥加密技术似乎只适合多对一通信。</p> <p>公开密钥加密一般都是非对称的，即加密密钥和解密密钥不同。共同挑战是，要确保即便有人拥有了下面线索，也无法计算出保密的私有密钥：</p> <ul><li>公开密钥（是公有的，所有人都可以获得）</li> <li>一小片拦截下来的密文（可通过对网络的嗅探获取）</li> <li>一条报文与之相关的密文（对任意一段文本运行加密器就可以得到）</li></ul> <p><strong>RSA</strong></p> <p>RSA 是一个满足了所有上述条件的公开密钥加密系统，破解 RSA 系统的私密钥匙的困难程度相当于对一个极大的数进行质因数分解，这是计算机科学中最困难的问题之一。</p> <p>关于更多 RSA 的内容，参考：<a href="http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html" target="_blank" rel="noopener noreferrer">RSA 算法原理（一）<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>、<a href="http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html" target="_blank" rel="noopener noreferrer">RSA 算法原理（二）<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>。</p> <h3 id="数字签名"><a href="#数字签名" class="header-anchor">#</a> 数字签名</h3> <p>数字签名是附加在报文上的特殊加密校验码，即所谓的校验和。</p> <p>数字签名算法的输入是啥呢？报文消息？No！数字签名算法对消息的长度和格式是有要求的，要求数据满足一定的条件。所以不能直接使用报文消息作为输入。一般策略是是对报文消息先进行哈希，得到固定长度的哈希值，然后在对哈希值进行签名。如下图：</p> <div class="custom-image-wrapper" data-v-339c7bd5><img src="/image/QQ20170227-2@2x.png" srcset="/image/QQ20170227-2@2x.png 2x" data-v-339c7bd5></div> <ul><li>节点 A 将变长报文进行哈希，生成定长的消息摘要</li> <li>节点 A 对摘要应用了一个签名函数，该函数会将用户的私有密钥作为参数，输出校验和</li> <li>签名被附着在报文的末尾，一并发送给节点 B</li> <li>接收端 B 对签名进行逆向处理（使用公开密钥），将得到的消息摘要与自己计算得到的消息摘要进行对比，进而判断报文是否被篡改</li></ul> <p>显然，签名一般是在非对称公开加密技术基础上建立的，签名作者持有私钥，签名接收者持有公钥。</p> <p>综上，签名的作用有两点：</p> <ul><li>证明是作者编写了这条报文，因为只有作者持有的密钥才能生成校验和</li> <li>签名可以防止报文被篡改，攻击者若拦截报文，然后对之进行修改，然而校验和无法匹配，这会在接收者那里被识破</li></ul> <p>P.S: 消息摘要是哈希算法通过哈希处理得到的，哈希算法的安全性从很大程度上决定了数字签名的安全性，所谓哈希算法的安全性，在这里指的是，如果消息报文被篡改了，那么篡改后的摘要和篡改前的摘要必须不同。最近 Google 宣布实现了对著名的哈希算法 SHA-1 的碰撞，详见<a href="https://www.zhihu.com/question/56234281/answer/148349930" target="_blank" rel="noopener noreferrer">这里<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>。</p> <h3 id="数字证书"><a href="#数字证书" class="header-anchor">#</a> 数字证书</h3> <p>数字证书是由证书颁发机构签发的，像身份证一样，不易伪造。包括如下内容：</p> <ul><li>证书序列号</li> <li>证书签名算法</li> <li>证书颁发者</li> <li>有效期</li> <li>公开密钥</li> <li>证书签发机构的数字签名</li> <li>等等</li></ul> <p>P.S: 特别需要注意的是 <em>证书签发机构的数字签名</em>，浏览器收到某个 Web 服务器的证书时，会对签名颁发机构的签名进行检查，一般来说，浏览器事先就已经预安装了很多签名颁发机构的证书（含有公开密钥），因此可以对签名进行验证。如果浏览器对签名颁发机构一无所知，浏览器就无法确定是否应该信任该签名颁发机构，它通常会向用户显示一个对话框，看看用户是否相信这个签名发布者，这种签名发布者往往是本地的 IT 部门或者软件厂商。</p> <p>P.S: 大多数证书都遵循 X.509 v3 标准。</p> <p>基于 X.509 证书的签名有好几种，包括：</p> <ul><li>Web 服务器证书</li> <li>客户端电子邮件证书</li> <li>软件代码签名证书</li> <li>证书颁发机构证书</li></ul> <h2 id="http-的缺点"><a href="#http-的缺点" class="header-anchor">#</a> HTTP 的缺点</h2> <p>HTTP 主要有三大不足：</p> <ul><li>通信使用明文（不加密），内容可能会被窃听</li> <li>不验证通信方的身份，有可能遭到伪装</li> <li>无法证明报文的完整性，报文可能被篡改</li></ul> <p>或者参考阮一峰大神的博客<a href="http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html" target="_blank" rel="noopener noreferrer">SSL/TLS 协议运行机制<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>的概述的说法，简称为三大风险：</p> <ul><li>窃听风险（eavesdropping）</li> <li>冒充风险（pretending）</li> <li>篡改风险（tampering）</li></ul> <p>本文的目的是就是详细阐述这三大问题，以便以后更好地理解 HTTPS。</p> <h3 id="窃听风险与加密"><a href="#窃听风险与加密" class="header-anchor">#</a> 窃听风险与加密</h3> <p>一个重要的事实是：TCP/IP 是可能被窃听的网络。TCP/IP 协议簇的工作机制，通信内容在所有的通信线路上都有可能遭到窥视：</p> <div class="custom-image-wrapper" data-v-339c7bd5><img src="/image/QQ20170121-0@2x.png" srcset="/image/QQ20170121-0@2x.png 2x" data-v-339c7bd5></div> <p>所谓互联网，是由能联通到全世界的网络设备组成的，无论哪里的 server 和 client 通信，此线路上的网络设备（包括电缆、计算机、路由器、交换机等）都不可能是个人的私有物，因此根本无法避免某个环节的报文会被窥探。</p> <p>窃听报文并非难事，只要收集互联网上流动的数据包即可，然后对它们进行解析。</p> <h3 id="通过加密防止报文被窃听"><a href="#通过加密防止报文被窃听" class="header-anchor">#</a> 通过加密防止报文被窃听</h3> <p>在应对报文被窃听的对策中，最为普遍的做法是加密技术，加密并不是个陌生的概念，两个重要问题：被加密的对象是啥？采用哪种加密方式？</p> <p><strong>对内容进行加密</strong></p> <p>对内容本身进行加密是最容易想到的，加密后的内容，即便报文被捕获，没有解密秘钥，拿到的也是一堆乱码。此处的 <em>内容</em> 指的是啥？众所周知，HTTP message 有三大组成部分：起始行、首部、报文主体。对内容加密的这种方式，被加密对象是哪一种呢？</p> <p>一般来说，不会对起始行和首部进行加密。为啥呢？根据我的理解，一方面是意义不大，另一方面首部和起始行信息不光是给接收端看的，也是给沿路中的各种代理看的，加密后的首部和起始行叫人家怎么看？因此，加密对象一般是报文主体或者部分报文主体（敏感内容）。</p> <p>诚然，为了做到有效的内容加密，前提是要求 client 和 server 同时具备加密和解密机制。</p> <p><strong>内容加密的相关算法</strong></p> <p>另一个问题，对内容加密的方式中，一般采用何种加密呢？加密算法太多了，这里就不展开了...</p> <p><strong>对内容加密的不足</strong></p> <p>只要加密算法足够安全，并且密钥被保护得当，对内容加密能够避免报文主体的敏感内容被监听，但是仍然无法避免内容被篡改。</p> <p>这个也容易理解，在传输过程中，窃听者虽然读不懂报文的内容的意义，但是仍然可以修改报文内容，譬如把报文的主体内容给清掉，进而对接收者进行误导。</p> <p><strong>对信道加密</strong></p> <p>相对于内容加密，<em>信道加密</em> 这个概念比较生僻一些，内容是比较容易理解的，那么，何为 <em>信道</em> 呢？</p> <p>我们知道，网络协议是一层层的，HTTP 建立在 TCP 的基础上，TCP 建立在 IP 的基础上。对于 HTTP 协议来说，所谓信道，其实是 TCP 层（类似，TCP 报文的信道是 IP 层），如果 TCP 层的通信是安全的，那么我们谈论 HTTP 加密似乎就没啥意义了。因此，所谓信道加密，是指 HTTP 所依赖的更底层协议被加密。</p> <p>这里所指的信道加密和 SSL/TLS 有关，搁在后面再讲。</p> <p>P.S: 对 <em>信道</em> 和 <em>信道加密</em> 的阐述，只是我的理解，可能是错的。</p> <h3 id="冒充风险与身份认证"><a href="#冒充风险与身份认证" class="header-anchor">#</a> 冒充风险与身份认证</h3> <p>另一个重要的事实是：任何人都可以发起请求。</p> <p>HTTP 协议并没有要求对通信方进行确认，server 只要接收到 request，就会给出一个相应的 response，因此会存在如下隐患：</p> <ul><li>无法保证 request 发送到了目标 server，有可能接收者是伪装的 server</li> <li>无法保证 response 是被正确的 client 接收，有可能接收者是伪装的 client</li> <li>无法确定正在通信的对方是否具备访问权限，因为某些 Web 服务器上保存着重要的信息，只想发给具备某种特定权限的 client</li> <li>无法避免无意义的 request 涌入，既无法阻止海量请求下的 DoS 攻击（Denial of Service，拒绝服务攻击）</li></ul> <p><strong>通过证书验证身份</strong></p> <p>身份认证可以解决这个问题，SSL/TLS 提供了这种服务，详见之后的博客。</p> <h3 id="篡改风险与完整性保护"><a href="#篡改风险与完整性保护" class="header-anchor">#</a> 篡改风险与完整性保护</h3> <p>上文已经提到，通过内容加密方式，可以避免报文敏感信息被窃听，但是无法避免报文被篡改。譬如，request 或 response 在传输途中，遭受攻击者拦截并篡改内容，这种攻击行为被称为中间人攻击（Man-In-The-Middle attack，MITM attack）。</p> <p>什么是完整性保护呢？所谓完整性并不是指报文没有受损，而是指报文的准确度；换句话说，如果无法证明报文的完整性，那么意味着无法判断报文信息是否准确。</p> <p>与窃听风险、冒充风险不同，篡改风险的应对措施比较多，根据《HTTP 权威指南》的说法，至少包括：基本认证、摘要认证，以及数字签名。</p> <p>在 HTTP 应用中，一种常见的策略是使用散列值校验，简单来说，发送方使用散列值算法（最常见的有 MD5、SHA-1，又被称为 <em>摘要算法</em> ），对消息进行哈希，得到固定长度的哈希值（是谓 <em>摘要</em> ），将该哈希值与消息一起传给对方，对方在收到 message 后，也使用约定的散列值算法对报文进行哈希，再与发送方的哈希值进行对比，如果不一致，则说明报文已被篡改，不安全。</p> <p>然而，这是会存在问题的，一方面是 MD5、SHA-1 不再安全，另一方面摘要本身也是可以被篡改的。</p> <p>更好的做法是，对摘要本身也进行保护，这个过程一般被称为数字签名，上文已经对数字签名有所阐述，这里就不再赘述。</p> <h2 id="初步了解-ssl-tls"><a href="#初步了解-ssl-tls" class="header-anchor">#</a> 初步了解 SSL/TLS</h2> <p>HTTPS（Hypertext Transfer Protocol Secure）被称为 <em>HTTP over TLS</em> 或 <em>HTTP over SSL</em> 或 <em>HTTP Secure</em> 。它并不是应用层的一种新协议，只是 HTTP 通信接口部分用 SSL 或 TLS 协议代替而已。通常，HTTP 直接和 TCP 通信，当使用 SSL/TLS 时，则演变为先和 SSL/TLS 通信，再由 SSL/TLS 和 TCP 通信，简单来说，HTTPS 在 HTTP 和 TCP 之间隔了一层 SSL/TLS（通常被称为安全层）。</p> <p>上文谈到了 HTTP 存在的三个重大毛病，SSL/TLS 解决了三个毛病的哪一个呢？答案是：所有。</p> <p>因此，此处引用《图解 HTTP》对 HTTPS 的总结：</p> <blockquote><p>HTTPS = HTTP + 加密 + 认证 + 完整性保护。</p></blockquote> <p>简单来说，建立在 SSL/TLS 协议之上，HTTP 具备加密、认证和完整性保护这些特点。本文旨在理清 SSL/TLS 是如何实现加密、认证和完整性保护的。</p> <p>首先一个必要的任务是搞清楚 SSL 和 TLS 的关系。SSL 是网景公司推出的，3.0 版本后，被 IETF 标准化，写入 RFC，是谓 TLS。下表是对它们的说明：</p> <table><thead><tr><th style="text-align:left;">版本</th> <th style="text-align:left;">发布时间</th> <th style="text-align:left;">RFC</th> <th style="text-align:left;">说明</th></tr></thead> <tbody><tr><td style="text-align:left;">SSL 1.0</td> <td style="text-align:left;">无</td> <td style="text-align:left;">无</td> <td style="text-align:left;">从未公开过，因为存在严重的安全漏洞</td></tr> <tr><td style="text-align:left;">SSL 2.0</td> <td style="text-align:left;">1995 年 2 月</td> <td style="text-align:left;">无</td> <td style="text-align:left;">因为存在数个严重的安全漏洞而被摈弃</td></tr> <tr><td style="text-align:left;">SSL 3.0</td> <td style="text-align:left;">1996 年</td> <td style="text-align:left;">无（后来作为历史文献被添加到 RFC 中，即<a href="https://tools.ietf.org/html/rfc6101" target="_blank" rel="noopener noreferrer">RFC6101<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>）</td> <td style="text-align:left;">2014 年 10 月，Google 发现在 SSL 3.0 中发现设计缺陷，建议禁用此一协议，后续 Microsoft、Mozilla 跟进</td></tr> <tr><td style="text-align:left;">TLS 1.0</td> <td style="text-align:left;">1999 年 1 月</td> <td style="text-align:left;"><a href="https://tools.ietf.org/html/rfc2246" target="_blank" rel="noopener noreferrer">RFC2246<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></td> <td style="text-align:left;">从技术上讲，TLS 1.0 与 SSL 3.0 的差异非常微小</td></tr> <tr><td style="text-align:left;">TLS 1.1</td> <td style="text-align:left;">2006 年 4 月</td> <td style="text-align:left;"><a href="https://tools.ietf.org/html/rfc4346" target="_blank" rel="noopener noreferrer">RFC4346<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></td> <td style="text-align:left;"></td></tr> <tr><td style="text-align:left;">TLS 1.2</td> <td style="text-align:left;">2008 年 8 月</td> <td style="text-align:left;"><a href="https://tools.ietf.org/html/rfc5246" target="_blank" rel="noopener noreferrer">RFC5246<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></td> <td style="text-align:left;">现在主流</td></tr> <tr><td style="text-align:left;">TLS 1.3</td> <td style="text-align:left;">草案阶段</td> <td style="text-align:left;"></td> <td style="text-align:left;"></td></tr></tbody></table> <p>可见，SSL 已是历史了；但是大家仍然习惯使用术语 SSL 指代 SSL 或 TLS。</p> <p>对于 SSL/TLS，最重要的内容莫过于其握手过程的分析，以后专门开辟博客阐述吧。</p> <h2 id="各种劫持与攻击"><a href="#各种劫持与攻击" class="header-anchor">#</a> 各种劫持与攻击</h2> <p>经常看到或者听到一些与安全相关的名词，譬如 DNS 劫持、HTTP 劫持、DDoS 攻击等，这一部分旨在对这些名词进行简单概述。</p> <h3 id="http-报文劫持"><a href="#http-报文劫持" class="header-anchor">#</a> HTTP 报文劫持</h3> <p>所谓 HTTP 报文劫持，指的是 HTTP 报文在 server 和 client 的传输过程中被修改的现象，这是运营商（ISP）比较喜欢做的事情，凑不要脸！HTTP 劫持主要发生在 Web 网页中，故而又被称为网页注入。简单来说，运营商常常在 server 发往 client 的 HTTP 报文中插入一段代码，这段代码通常与广告信息有关。相当于你让某个商店邮寄一包东西到你家，你收到的包裹里却有运营商附加的一坨屎。</p> <p><strong>HTTPS 能解决 HTTP 报文劫持的问题吗？</strong></p> <p>显然可以，SSL 的完整性保护确保了这件事情不会发生。</p> <h3 id="dns-劫持"><a href="#dns-劫持" class="header-anchor">#</a> DNS 劫持</h3> <p>DNS（域名系统）的作用是把网络地址对应到能够识别的 IP 地址，以便设备能够进一步通信，传递网址和内容等。DNS 劫持的实质是把 client 的域名劫持指向到非正常的 IP 地址。</p> <p>P.S: DNS 劫持是流量劫持的一种，又称 <em>域名劫持</em> ，或称 <em>DNS 钓鱼攻击</em> 。</p> <p>P.P.S: 除了 DNS 劫持，流量劫持还包括哪些呢？</p> <p><strong>HTTPS 能解决 DNS 劫持的问题吗？</strong></p> <p>答案是一般可以，但没有什么事情是绝对的。Server 与 client 建立 HTTPS 通信的前提是 server 持有受信任的合法证书；换句话说，如果被劫持到的目标 server 搞到一张假的受信任的证书，又或者它自己签发一个证书，并且 client 信任了该证书，那么 DNS 攻击依然存在。</p> <p>总之，HTTPS 下的 DNS 攻击门槛非常高，要么搞定 CA 机构，要么让用户信任自签发的证书。从另外一个角度来看，作为用户，我们不应该轻易相信别人自签发的证书，根据我的理解，这一点在网页端的问题要比移动客户端严重得多。</p> <p>P.S: <a href="https://www.zhihu.com/question/22795329" target="_blank" rel="noopener noreferrer">知乎<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>的这个话题下有些比较好的说明。</p> <p>P.S: DNS 劫持也常常发生在路由器中，譬如被人诟病的小米路由器。</p> <h3 id="运营商-路由器缓存造成的劫持"><a href="#运营商-路由器缓存造成的劫持" class="header-anchor">#</a> 运营商/路由器缓存造成的劫持</h3> <p>上述的 HTTP 报文劫持和 DNS 劫持都被统称为网络劫持，除了这两种，还有一种常见的劫持：运营商或者路由器无脑的缓存。</p> <p>点评大神周辉我司内部分享时举了一个相关例子：</p> <div class="custom-image-wrapper" data-v-339c7bd5><img src="/image/tree-pics@2x.png" srcset="/image/tree-pics@2x.png 2x" data-v-339c7bd5></div> <p>如上图所示，不同用户居然获取到同一个用户的个人信息，这几个用户连接的都是上海地铁上的花生 wifi，最后定位到的原因是花生 wifi 服务商对 HTTP response 进行了缓存，缓存的 key 自然是 HTTP request，但是在对 request 处理时居然将参数给去掉了...</p> <h3 id="ddos-攻击"><a href="#ddos-攻击" class="header-anchor">#</a> DDoS 攻击</h3> <p>这种攻击比较简单了，一般情况下的 SSL/TLS 通信模式是单向认证，并不能解决 DDoS 问题...</p> <h2 id="https-与实践"><a href="#https-与实践" class="header-anchor">#</a> HTTPS 与实践</h2> <p>本文记录我的一些 HTTPS 实践经历或体会。</p> <p>P.S: 然而，到目前为止，其实没啥近距离的 HTTPS 实践机会...</p> <h3 id="ats"><a href="#ats" class="header-anchor">#</a> ATS</h3> <p>ATS 并不等价于<em>所有 App 的 HTTP API 都得使用 HTTPS</em> ，它比后者要求得更多，<a href="https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW57" target="_blank" rel="noopener noreferrer">这里<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a>有详细说明。简单来说：</p> <ul><li>默认情况下，server 的 TLS 版本得是 1.2+</li> <li>对此加密算法得是 AES-128 或者 AES-256 级别的</li> <li>等等</li></ul> <p>处理业务时，遇到一个 case：外网 https://api.guanaitong.com 的 web 页面在我们 app 内不能访问，经查，发现是 server 的 TLS 版本过低（TLS 1.0），给他们的 server 反馈后，已解决...可惜当时没留下截图，一个难得的 case。</p> <p><strong>如何知道 server 支持的 TLS 的版本呢？</strong></p> <p>可以使用 openssl 工具，譬如<code>openssl s_client -connect api.guanaitong.com:443 -tls1_2</code>，参考自：<a href="https://serverfault.com/questions/638691/how-can-i-verify-if-tls-1-2-is-supported-on-a-remote-web-server-from-the-rhel-ce" target="_blank" rel="noopener noreferrer">How can I verify if TLS 1.2 is supported on a remote Web server from the RHEL/CentOS shell?<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" x="0px" y="0px" viewBox="0 0 100 100" width="15" height="15" class="icon outbound"><path fill="currentColor" d="M18.8,85.1h56l0,0c2.2,0,4-1.8,4-4v-32h-8v28h-48v-48h28v-8h-32l0,0c-2.2,0-4,1.8-4,4v56C14.8,83.3,16.6,85.1,18.8,85.1z"></path> <polygon fill="currentColor" points="45.7,48.7 51.3,54.3 77.2,28.5 77.2,37.2 85.2,37.2 85.2,14.9 62.8,14.9 62.8,22.9 71.5,22.9"></polygon></svg></a></p> <p>P.S: macOS 默认已安装 openssl 工具，但是版本比较老（低于 v1.0），使用<code>-tls1_2</code>选项会有问题，需要升级...</p> <p><strong>如何知道 server 对 ATS 的支持情况呢？</strong></p> <p>使用另一个工具 nscurl，譬如：</p> <div class="language-sh extra-class"><pre class="language-sh"><code>nscurl --ats-diagnostics --verbose https://api.guanaitong.com
</code></pre></div></div></div><div class="global-ui"></div></div>
    <script src="/assets/js/app.50bca889.js" defer></script><script src="/assets/js/5.081d1701.js" defer></script><script src="/assets/js/72.f4fda48b.js" defer></script><script src="/assets/js/4.a7413ce2.js" defer></script>
  </body>
</html>
<file_sep>(window.webpackJsonp=window.webpackJsonp||[]).push([[110],{6:function(n,w,o){}}]);