<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.moecoder.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.moecoder.com/" rel="alternate" type="text/html" /><updated>2024-11-30T18:42:33+08:00</updated><id>https://blog.moecoder.com/feed.xml</id><title type="html">Wasteland</title><subtitle>夜切同学的拖更现场 
</subtitle><author><name>Yang Chao</name></author><entry><title type="html">Apple Silicon 电脑上的 VPN 与 Surge 共存方案</title><link href="https://blog.moecoder.com/openconnect-with-surge.html" rel="alternate" type="text/html" title="Apple Silicon 电脑上的 VPN 与 Surge 共存方案" /><published>2022-05-22T00:31:00+08:00</published><updated>2022-05-22T00:31:00+08:00</updated><id>https://blog.moecoder.com/openconnect-with-surge</id><content type="html" xml:base="https://blog.moecoder.com/openconnect-with-surge.html"><![CDATA[<p>时隔两年的又一次长期居家办公，不免又要长时间通过 VPN 访问公司内网，我个人的需求很简单，只有一条：</p>

<ul>
  <li>电脑上需要开启 Surge 无障碍上网，但同时对于公司内网域名要走 VPN，两者互不影响</li>
</ul>

<p>之前参考 <a href="https://blog.indigo.codes/2020/04/24/home-network-deployment/">这篇博客</a> 已经完成了整体的配置，具体思路就是使用 Surge 把公司内网流量转发到 Docker 容器，容器内部开启 OpenConnect 连 VPN。但是最近家里购置了一台 Mac Studio，Apple Silicon 什么都好，就是在一些特殊场景下兼容性堪忧，网络配置花了很久，所以在这里做个记录。</p>

<h2 id="surge-规则配置">Surge 规则配置</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Proxy]
🚇 VPN = snell, localhost, 8388, psk=123, obfs=http, version=3

[Proxy Group]
💼 公司内网 = select, DIRECT, 🚇 VPN

[Rule]
AND,((DOMAIN-KEYWORD,内网域名), (NOT,((PROCESS-NAME,com.docker.vpnkit)))),💼 公司内网
</code></pre></div></div>

<p>这里有几个细节点：</p>
<ul>
  <li>对于公司内网域名，可以根据情况手动选择是直连还是走 VPN，这样在家里或者公司都可以复用同一套配置</li>
  <li>规则这里一定要排除掉 com.docker.vpnkit 进程的请求，否则在 Surge 的增强模式下会出现死循环的情况</li>
</ul>

<h2 id="本地-vpn-服务配置">本地 VPN 服务配置</h2>

<p>参考上面的博客，按照 <a href="https://hub.docker.com/r/dianqk/openconnect-snell">openconnect-snell</a> 里的说明起一个 Docker 实例就可以了——至少曾经可以这样。但是要想在 M1 上把实例跑起来，必须要重新发布一版 arm64 的镜像才行，具体来说就是要把原来的 Dockerfile 中引用的资源全部替换成 arm64 的，改动点见 <a href="https://github.com/Yeatse/openconnect-snell/commit/d9c6bed909c45fd3746ceebba4558b5be5276960">diff</a>。</p>

<p>这里有一个坑点，就是 snell-server 依赖了 glibc，但是网络上根本找不到适配 arm64 的 alpine linux 的较新的 glibc 发布包（定语这么多找不到也正常），最终只能借助 <a href="https://github.com/sgerrand/docker-glibc-builder.git">docker-glibc-builder</a> 自己在本机编译了一份，然后手动拷贝到镜像中。另外就是要解决各种系统库找不到的问题，alpine 下报错信息少得可怜，解决这个问题花了好几个晚上。</p>

<p>总而言之，镜像发布之后，开启 VPN 就比较方便了，新建一个名字叫 <code class="language-plaintext highlighter-rouge">runvpn.sh</code> 的脚本，内容如下：</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--privileged</span> <span class="nt">-p</span> 8388:8388 <span class="nt">-p</span> 8388:8388/udp <span class="nt">-e</span> <span class="nv">PSK</span><span class="o">=</span><span class="s2">"123"</span> <span class="nt">-e</span> <span class="nv">OC_HOST</span><span class="o">=</span><span class="s2">"..."</span> <span class="nt">-e</span> <span class="nv">OC_AUTH_GROUP</span><span class="o">=</span><span class="s2">"common-vpn"</span> <span class="nt">-e</span> <span class="nv">OC_PASSWD</span><span class="o">=</span><span class="s2">"..."</span> <span class="nt">-e</span> <span class="nv">OC_AUTH_CODE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="nt">-e</span> <span class="nv">OC_USER</span><span class="o">=</span><span class="s2">"..."</span> <span class="nt">--name</span><span class="o">=</span>openconnect-snell yeatse/openconnect-snell
</code></pre></div></div>

<p>使用时只需要执行以下命令即可：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sh ./runvpn.sh &lt;动态验证码&gt;
</code></pre></div></div>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="Tools" /><summary type="html"><![CDATA[时隔两年的又一次长期居家办公，不免又要长时间通过 VPN 访问公司内网，我个人的需求很简单，只有一条：]]></summary></entry><entry><title type="html">React Native 一处诡异 crash: RCTFont SIGABRT</title><link href="https://blog.moecoder.com/react-native-crash-rctfont-sigabrt.html" rel="alternate" type="text/html" title="React Native 一处诡异 crash: RCTFont SIGABRT" /><published>2017-09-29T01:10:29+08:00</published><updated>2017-09-29T01:10:29+08:00</updated><id>https://blog.moecoder.com/react-native-crash-rctfont-sigabrt</id><content type="html" xml:base="https://blog.moecoder.com/react-native-crash-rctfont-sigabrt.html"><![CDATA[<p>目前在做的一个项目迁移到 React Native 已经一年多了，也意味着踩了一年的坑，感觉光填上各种奇怪的坑都会让自己的水平提升不少。最近解决了一个占比接近 10% 的崩溃，在这里记录一下。</p>

<!-- more -->

<p><img src="/assets/images/2017/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-07-31%20%E4%B8%8B%E5%8D%889.38.38.png" alt="屏幕快照" /></p>

<p>崩溃的方法是
<code class="language-plaintext highlighter-rouge">+[RCTFont updateFont:withFamily:size:weight:style:variant:scaleMultiplier:]</code>，在这个位置会抛出 <code class="language-plaintext highlighter-rouge">mutex lock failed: Invalid argument</code> 的异常。</p>

<p>查了下崩溃栈，大概长这样：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Thread 24 Crashed:
0   libsystem_kernel.dylib              __pthread_kill + 8
1   libsystem_c.dylib                   abort + 140
2   libc++abi.dylib                     __cxa_bad_cast + 0
3   libc++abi.dylib                     default_terminate_handler() + 280
4   libobjc.A.dylib                     _objc_terminate() + 140
5   libc++abi.dylib                     std::__terminate(void (*)()) + 16
6   libc++abi.dylib                     __cxxabiv1::exception_cleanup_func(_Unwind_Reason_Code, _Unwind_Exception*) + 0
7   libc++.1.dylib                      std::__1::__throw_system_error(int, char const*) + 88
8   bee                                 +[RCTFont updateFont:withFamily:size:weight:style:variant:scaleMultiplier:] + 780
9   bee                                 -[RCTShadowText _attributedStringWithFontFamily:fontSize:fontWeight:fontStyle:letterSpacing:useBackgroundColor:foregroundColor:backgroundColor:opacity:] + 580
10  bee                                 -[RCTShadowText attributedString] + 192
11  bee                                 -[RCTShadowText recomputeText] + 28
12  bee                                 -[RCTTextManager uiBlockToAmendWithShadowViewRegistry:] + 612
13  bee                                 -[RCTComponentData uiBlockToAmendWithShadowViewRegistry:] + 96
14  bee                                 -[RCTUIManager _layoutAndMount] + 220
15  bee                                 __36-[RCTBatchedBridge batchDidComplete]_block_invoke + 52
16  libdispatch.dylib                   _dispatch_call_block_and_release + 24
17  libdispatch.dylib                   _dispatch_client_callout + 16
18  libdispatch.dylib                   _dispatch_queue_serial_drain + 928
19  libdispatch.dylib                   _dispatch_queue_invoke + 884
20  libdispatch.dylib                   _dispatch_root_queue_drain + 540
21  libdispatch.dylib                   _dispatch_worker_thread3 + 124
22  libsystem_pthread.dylib             _pthread_wqthread + 1096
23  libsystem_pthread.dylib             start_wqthread + 4
</code></pre></div></div>

<p>从崩溃栈上可以看出来是 RN 库 RCTFont 模块出的问题，除此之外再也找不到其他信息，放 google 搜了一圈，只能找到别人提的同样的问题 – <a href="https://github.com/facebook/react-native/issues/13588">RCTFont SIGABRT crash</a>、<a href="https://github.com/facebook/react-native/issues/14526">App crashes for “mutex lock failed: Invalid argument”</a>，却没人提出解决方法，看来只能自己解了。</p>

<p>首先把可执行文件拉到 <a href="https://www.hopperapp.com">Hopper</a> 里，定位到崩溃处，看一下对应的汇编指令：</p>

<p><img src="/assets/images/2017/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-08-01%20%E4%B8%8B%E5%8D%883.03.38.png" alt="屏幕快照 2017-08-01 下午3.03.38" /></p>

<p>可以得知应用在 RCTFont 内部使用 std::mutex 加锁的时候抛出了异常，对应于 <a href="https://github.com/facebook/react-native/blob/6ce42441ec98bb8543e8eff8849ce50e076ce520/React/Views/RCTFont.mm#L103">RCTFont.mm 第 103 行</a>：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
    <span class="n">std</span><span class="o">::</span><span class="n">lock_guard</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">mutex</span><span class="o">&gt;</span> <span class="n">lock</span><span class="p">(</span><span class="n">fontCacheMutex</span><span class="p">);</span> <span class="c1">///&lt; 在这里挂掉了</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">fontCache</span><span class="p">)</span> <span class="p">{</span>
      <span class="n">fontCache</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSCache</span> <span class="nf">new</span><span class="p">];</span>
    <span class="p">}</span>
    <span class="n">font</span> <span class="o">=</span> <span class="p">[</span><span class="n">fontCache</span> <span class="nf">objectForKey</span><span class="p">:</span><span class="n">cacheKey</span><span class="p">];</span>
<span class="p">}</span> 
</code></pre></div></div>

<p>对比收集到的各种崩溃样本，可以总结出以下几处共同点：</p>

<ul>
  <li>主线程都有 <code class="language-plaintext highlighter-rouge">handleApplicationDeactivationWithScene</code> → <code class="language-plaintext highlighter-rouge">+[_UIAlertManager hideAlertsForTermination]</code> → <code class="language-plaintext highlighter-rouge">exit</code> 的调用；</li>
  <li>crash 都发生在后台；</li>
  <li>都在 <code class="language-plaintext highlighter-rouge">mutex::lock</code> 的时候抛出了异常。</li>
</ul>

<p>给 <code class="language-plaintext highlighter-rouge">handleApplicationDeactivationWithScene</code> 等方法下个断点，发现只有在用户手动 kill 掉 app 时这些方法才会被调用，猜测这个时候系统可能正在做一些清理工作，这时候如果有其他线程调用了 <code class="language-plaintext highlighter-rouge">mutex::lock</code> 可能就会导致异常。</p>

<p>假设上面的猜测为真，那么解决问题的关键是让应用进程结束时不调用 <code class="language-plaintext highlighter-rouge">mutex::lock</code> 方法。查看 React Native 源码，crash 处的代码只被一个方法调用，即上面提到的 <code class="language-plaintext highlighter-rouge">+[RCTFont updateFont:withFamily:size:weight:style:variant:scaleMultiplier:]</code></p>

<p><img src="/assets/images/2017/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-08-09%20%E4%B8%8A%E5%8D%8811.45.36.png" alt="屏幕快照 2017-08-09 上午11.45.36" /></p>

<p>这个方法的调用者有多个，但最终都走到了 React Native 模块的各个属性的设置方法里，在 React Native 线程里，由 <code class="language-plaintext highlighter-rouge">RCTBatchedBridge</code> → <code class="language-plaintext highlighter-rouge">RCTJSCExecutor</code> 驱动</p>

<p><img src="/assets/images/2017/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-08-23%20%E4%B8%8B%E5%8D%8812.25.22.png" alt="屏幕快照 2017-08-23 下午12.25.22" /></p>

<p>注意到 js 每次调用时都会检查 <code class="language-plaintext highlighter-rouge">_valid</code> 属性，所以只需要在进程结束时把 <code class="language-plaintext highlighter-rouge">_valid</code> 置为 false，crash 处的代码就不会被执行。<code class="language-plaintext highlighter-rouge">RCTBatchedBridge</code> 和它的包装类 <code class="language-plaintext highlighter-rouge">RCTBridge</code> 刚好暴露出了 <code class="language-plaintext highlighter-rouge">invalidate</code> 方法，可以把 <code class="language-plaintext highlighter-rouge">_valid</code> 置为 false：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// -[RCTBridge invalidate]</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">invalidate</span>
<span class="p">{</span>
  <span class="n">RCTBridge</span> <span class="o">*</span><span class="n">batchedBridge</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">batchedBridge</span><span class="p">;</span>
  <span class="n">self</span><span class="p">.</span><span class="n">batchedBridge</span> <span class="o">=</span> <span class="nb">nil</span><span class="p">;</span>
 
  <span class="k">if</span> <span class="p">(</span><span class="n">batchedBridge</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">RCTExecuteOnMainQueue</span><span class="p">(</span><span class="o">^</span><span class="p">{</span>
      <span class="p">[</span><span class="n">batchedBridge</span> <span class="nf">invalidate</span><span class="p">];</span>
    <span class="p">});</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// -[RCTBatchedBridge invalidate]</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">invalidate</span>
<span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">_valid</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span><span class="p">;</span>
  <span class="p">}</span>
 
  <span class="n">_loading</span> <span class="o">=</span> <span class="nb">NO</span><span class="p">;</span>
  <span class="n">_valid</span> <span class="o">=</span> <span class="nb">NO</span><span class="p">;</span>
 
  <span class="c1">// Invalidate modules</span>
  <span class="k">for</span> <span class="p">(</span><span class="n">RCTModuleData</span> <span class="o">*</span><span class="n">moduleData</span> <span class="k">in</span> <span class="n">_moduleDataByID</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">id</span><span class="o">&lt;</span><span class="n">RCTBridgeModule</span><span class="o">&gt;</span> <span class="n">instance</span> <span class="o">=</span> <span class="n">moduleData</span><span class="p">.</span><span class="n">instance</span><span class="p">;</span>
    <span class="p">[</span><span class="n">instance</span> <span class="nf">invalidate</span><span class="p">];</span>
    <span class="p">[</span><span class="n">moduleData</span> <span class="nf">invalidate</span><span class="p">];</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>用户杀掉 app 时，系统会调用 App Delegate 的 <code class="language-plaintext highlighter-rouge">applicationWillTerminate</code> 方法，所以我们需要在这里调用一下 <code class="language-plaintext highlighter-rouge">-[RCTBridge invalidate]</code>，使 <code class="language-plaintext highlighter-rouge">RCTBridge</code> 失效，这样就不会再触发导致 crash 的代码了。</p>

<p>但问题是，<code class="language-plaintext highlighter-rouge">-[RCTBridge invalidate]</code> 方法是异步的，<code class="language-plaintext highlighter-rouge">applicationWillTerminate</code> 一返回马上就进入 <code class="language-plaintext highlighter-rouge">exit</code> 函数，这时候程序还来不及干掉 <code class="language-plaintext highlighter-rouge">RCTBridge</code>，crash 处的代码还是会执行。所以这里还需要借用 runloop 让 <code class="language-plaintext highlighter-rouge">applicationWillTerminate</code> 卡一会儿，直到 <code class="language-plaintext highlighter-rouge">RCTBridge</code> 完全停止：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">applicationWillTerminate</span><span class="p">:(</span><span class="n">UIApplication</span> <span class="o">*</span><span class="p">)</span><span class="nv">application</span> <span class="p">{</span>
    <span class="n">RCTBridge</span> <span class="o">*</span><span class="n">batchedBridge</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span><span class="p">.</span><span class="n">bridge</span> <span class="nf">valueForKey</span><span class="p">:</span><span class="s">@"batchedBridge"</span><span class="p">];</span>
    <span class="p">[</span><span class="n">self</span><span class="p">.</span><span class="n">bridge</span> <span class="nf">invalidate</span><span class="p">];</span>
    
    <span class="n">NSRunLoop</span> <span class="o">*</span><span class="n">runLoop</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSRunLoop</span> <span class="nf">currentRunLoop</span><span class="p">];</span>
    <span class="n">NSArray</span><span class="o">&lt;</span><span class="n">NSRunLoopMode</span><span class="o">&gt;</span> <span class="o">*</span><span class="n">allModes</span> <span class="o">=</span> <span class="n">CFBridgingRelease</span><span class="p">(</span><span class="n">CFRunLoopCopyAllModes</span><span class="p">(</span><span class="n">runLoop</span><span class="p">.</span><span class="n">getCFRunLoop</span><span class="p">));</span>
    <span class="k">while</span> <span class="p">(</span><span class="n">batchedBridge</span><span class="p">.</span><span class="n">moduleClasses</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="n">NSRunLoopMode</span> <span class="n">mode</span> <span class="k">in</span> <span class="n">allModes</span><span class="p">)</span> <span class="p">{</span>
            <span class="p">[</span><span class="n">runLoop</span> <span class="nf">runMode</span><span class="p">:</span><span class="n">mode</span> <span class="nf">beforeDate</span><span class="p">:[</span><span class="n">NSDate</span> <span class="nf">dateWithTimeIntervalSinceNow</span><span class="p">:</span><span class="mi">0</span><span class="p">.</span><span class="mi">1</span><span class="p">]];</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这里用到了几处 trick：</p>

<ul>
  <li>在这里阻塞 <code class="language-plaintext highlighter-rouge">applicationWillTerminate</code> 要用 runloop，而不是简单的 sleep，原因是 invalidate 方法内部向主线程分发了一些事要做（见 <a href="https://github.com/facebook/react-native/blob/6ce42441ec98bb8543e8eff8849ce50e076ce520/React/Base/RCTBatchedBridge.mm#L714-L735">RCTBatchedBridge.mm 源码</a>），需要主线程有处理事件的能力；</li>
  <li>invalidate 方法最后一步是置空 <code class="language-plaintext highlighter-rouge">RCTBatchedBridge</code> 的 <code class="language-plaintext highlighter-rouge">moduleClasses</code> 属性，所以可以通过它是否为空来确定 RCTBridge 完全停止的时机。 batchedBridge 是私有属性所以需要 kvc 来拿到。</li>
</ul>

<p>加入工程发版之后，这个 crash 就消失了，撒花。</p>

<p><img src="/assets/images/2017/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7%202017-08-23%20%E4%B8%8B%E5%8D%881.05.44.png" alt="屏幕快照 2017-08-23 下午1.05.44" /></p>

<hr />

<h2 id="one-more-thing">One more thing</h2>

<p>为什么主线程调用了 <code class="language-plaintext highlighter-rouge">exit</code> 之后，其他线程调用 <code class="language-plaintext highlighter-rouge">mutex::lock</code> 方法时会抛异常？</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">NSCache</span> <span class="o">*</span><span class="n">fontCache</span><span class="p">;</span>
<span class="k">static</span> <span class="n">std</span><span class="o">::</span><span class="n">mutex</span> <span class="n">fontCacheMutex</span><span class="p">;</span>
 
<span class="n">NSString</span> <span class="o">*</span><span class="n">cacheKey</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSString</span> <span class="nf">stringWithFormat</span><span class="p">:</span><span class="s">@"%.1f/%.2f"</span><span class="p">,</span> <span class="n">size</span><span class="p">,</span> <span class="n">weight</span><span class="p">];</span>
<span class="n">UIFont</span> <span class="o">*</span><span class="n">font</span><span class="p">;</span>
<span class="p">{</span>
  <span class="n">std</span><span class="o">::</span><span class="n">lock_guard</span><span class="o">&lt;</span><span class="n">std</span><span class="o">::</span><span class="n">mutex</span><span class="o">&gt;</span> <span class="n">lock</span><span class="p">(</span><span class="n">fontCacheMutex</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">fontCache</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">fontCache</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSCache</span> <span class="nf">new</span><span class="p">];</span>
  <span class="p">}</span>
  <span class="n">font</span> <span class="o">=</span> <span class="p">[</span><span class="n">fontCache</span> <span class="nf">objectForKey</span><span class="p">:</span><span class="n">cacheKey</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>回过来看崩溃位置代码，第 2 行声明了一个局部变量 fontCacheMutex，通过汇编指令可以看出，它在创建的时候通过 <code class="language-plaintext highlighter-rouge">__cxa_atexit</code> 方法注册了一个销毁函数：</p>

<p><img src="/assets/images/2017/DX-20170823@2x.png" alt="DX-20170823@2x" /></p>

<p>主线程调用 <code class="language-plaintext highlighter-rouge">exit</code> 方法时，会通过 <code class="language-plaintext highlighter-rouge">__cxa_finalize</code> 逐个调用之前注册的销毁函数（参考 <a href="https://opensource.apple.com/source/Libc/Libc-1158.50.2/stdlib/FreeBSD/atexit.c.auto.html">atexit.c 源码</a>），这个静态变量 fontCacheMutex 随之销毁，之后再调用这个销毁过的 mutex 对象的方法自然会 crash 了。</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><category term="React Native" /><summary type="html"><![CDATA[目前在做的一个项目迁移到 React Native 已经一年多了，也意味着踩了一年的坑，感觉光填上各种奇怪的坑都会让自己的水平提升不少。最近解决了一个占比接近 10% 的崩溃，在这里记录一下。]]></summary></entry><entry><title type="html">让 WKWebView 支持 NSURLProtocol</title><link href="https://blog.moecoder.com/support-nsurlprotocol-in-wkwebview.html" rel="alternate" type="text/html" title="让 WKWebView 支持 NSURLProtocol" /><published>2016-10-26T23:53:25+08:00</published><updated>2016-10-26T23:53:25+08:00</updated><id>https://blog.moecoder.com/support-nsurlprotocol-in-wkwebview</id><content type="html" xml:base="https://blog.moecoder.com/support-nsurlprotocol-in-wkwebview.html"><![CDATA[<p>最近把公司的项目从 UIWebView 迁移到了 WKWebView，因为之前大体上还是遵从了 Apple 的 API 没有过度地去 hack，而且 <a href="https://github.com/marcuswestin/WebViewJavascriptBridge">WebViewJavascriptBridge</a> 也同样支持 WKWebView，所以迁移过程没有想象中那么痛苦，只要把 UIWebViewDelegate 的方法改成 WKUIDelegate 和 WKNavigationDelegate 对应方法就好了。</p>

<!-- more -->

<p>但是在 WKWebView 已经出现了三年的今天，UIWebView 还没有被标记为 deprecated，我想 Apple 一定也和很多开发者一样，觉得 WKWebView 还没有完善到能完全替代 UIWebView 的程度。比如其中一个痛点——对请求拦截的支持，正常情况下，按照下面的方式注册一个 NSURLProtocol 子类，就可以对 app 内所有的网络请求进行 <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">MitM</a> 了：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">NSURLProtocol</span> <span class="nf">registerClass</span><span class="p">:[</span><span class="n">AwesomeURLProtocol</span> <span class="nf">class</span><span class="p">]];</span>
</code></pre></div></div>

<p>但 WKWebView 中的请求却完全不遵从这一规则，除了一开始会调用一下 <code class="language-plaintext highlighter-rouge">+ [NSURLProtocol canInitWithRequest:]</code> 方法，之后的整个请求流程似乎就与 NSURLProtocol 完全无关了。关于这一点，网络上文章一般都解释说 WKWebView 的请求是在单独的进程里，所以不走 NSURLProtocol。</p>

<p>既然 WKWebView 不走 NSURLProtocol，那为什么还要在一开始调一下 <code class="language-plaintext highlighter-rouge">canInitWithRequest:</code> 呢？更令我好奇的是从 WebKit.framework dump 出的头文件能看出，有几个类（<a href="https://github.com/JaviSoto/iOS10-Runtime-Headers/blob/master/Frameworks/WebKit.framework/WKCustomProtocol.h">WKCustomProtocol</a>、<a href="https://github.com/JaviSoto/iOS10-Runtime-Headers/blob/master/Frameworks/WebKit.framework/WKCustomProtocolLoader.h">WKCustomProtocolLoader</a>）明显与 NSURLProtocol 有关，说明 WKWebView 很可能是支持 NSURLProtocol 的，只是出于某种原因没开放而已，于是我决定翻 WebKit 的<a href="https://github.com/WebKit/webkit">源码</a>一探究竟。</p>

<h2 id="wkbrowsingcontextcontroller">WKBrowsingContextController</h2>

<p>翻 WebKit 源码的过程就不细说了，光从 GitHub 上拉源码到本地就花了我几个 G 的 ss 流量……总之翻到最后，我在一项单元测试 <a href="https://github.com/WebKit/webkit/blob/master/Tools/TestWebKitAPI/cocoa/TestProtocol.mm">TestProtocol.mm</a> 中看到了 NSURLProtocol 熟悉的身影：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">registerWithScheme</span><span class="p">:(</span><span class="n">NSString</span> <span class="o">*</span><span class="p">)</span><span class="nv">scheme</span>
<span class="p">{</span>
    <span class="n">testScheme</span> <span class="o">=</span> <span class="p">[</span><span class="n">scheme</span> <span class="nf">retain</span><span class="p">];</span>
    <span class="p">[</span><span class="n">NSURLProtocol</span> <span class="nf">registerClass</span><span class="p">:[</span><span class="n">self</span> <span class="nf">class</span><span class="p">]];</span>
<span class="cp">#if WK_API_ENABLED
</span>    <span class="p">[</span><span class="n">WKBrowsingContextController</span> <span class="nf">registerSchemeForCustomProtocol</span><span class="p">:</span><span class="n">testScheme</span><span class="p">];</span>
<span class="cp">#endif
</span><span class="p">}</span>
</code></pre></div></div>

<p>从 <code class="language-plaintext highlighter-rouge">registerSchemeForCustomProtocol:</code> 这个方法名来猜测，它的作用的应该是注册一个自定义的 scheme，这样对于 WebKit 进程的所有网络请求，都会先检查是否有匹配的 scheme，有的话再走主进程的 NSURLProtocol 这一套流程，猜测这么做可能是为了保证效率 (NSURLRequest 的 HTTPBody 属性在 WKWebView 中被忽略了应该也出于这个原因)，毕竟 IPC 代价挺高的。后续翻 <code class="language-plaintext highlighter-rouge">WebKit::CustomProtocolManager</code> 和 <code class="language-plaintext highlighter-rouge">WebKit::WebProcessPool</code> 等相关源码也印证了这个猜想。</p>

<p>看上去没什么问题，于是按照 TestCase 里的例子尝试了一下：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Class</span> <span class="n">cls</span> <span class="o">=</span> <span class="n">NSClassFromString</span><span class="p">(</span><span class="s">@"WKBrowsingContextController"</span><span class="p">);</span>
<span class="n">SEL</span> <span class="n">sel</span> <span class="o">=</span> <span class="n">NSSelectorFromString</span><span class="p">(</span><span class="s">@"registerSchemeForCustomProtocol:"</span><span class="p">);</span>
<span class="k">if</span> <span class="p">([(</span><span class="n">id</span><span class="p">)</span><span class="n">cls</span> <span class="nf">respondsToSelector</span><span class="p">:</span><span class="n">sel</span><span class="p">])</span> <span class="p">{</span>
    <span class="c1">// 把 http 和 https 请求交给 NSURLProtocol 处理</span>
    <span class="p">[(</span><span class="n">id</span><span class="p">)</span><span class="n">cls</span> <span class="nf">performSelector</span><span class="p">:</span><span class="n">sel</span> <span class="nf">withObject</span><span class="p">:</span><span class="s">@"http"</span><span class="p">];</span>
    <span class="p">[(</span><span class="n">id</span><span class="p">)</span><span class="n">cls</span> <span class="nf">performSelector</span><span class="p">:</span><span class="n">sel</span> <span class="nf">withObject</span><span class="p">:</span><span class="s">@"https"</span><span class="p">];</span>
<span class="p">}</span>

<span class="c1">// 这下 AwesomeURLProtocol 就可以用啦</span>
<span class="p">[</span><span class="n">NSURLProtocol</span> <span class="nf">registerClass</span><span class="p">:[</span><span class="n">AwesomeURLProtocol</span> <span class="nf">class</span><span class="p">]];</span>
</code></pre></div></div>

<p>现在 WKWebView 中的所有请求都可以被 NSURLProtocol 修改了：
<img src="/assets/images/2016/14774810372171.jpg" alt="14774810372171-w375" /></p>

<h2 id="关于私有-api">关于私有 API</h2>

<p>按照 @sunnyxx 的<a href="http://blog.sunnyxx.com/2015/06/07/fullscreen-pop-gesture/">总结</a>，Apple 检查私有 API 的使用，大概会采取下面几种手段：</p>

<ul>
  <li>是否 link 了私有 framework 或者公开 framework 中的私有符号，这可以防止开发者把私有 header 都 dump 出来供程序直接调用。</li>
  <li>同上，使用@selector(_private_sel)加上-performSelector:的方式直接调用私有 API。</li>
  <li>扫描所有符号，查看是否有继承自私有类，重载私有方法，方法名是否有重合。</li>
  <li>扫描所有string，看字符串常量段是否出现和私有 API 对应的。</li>
</ul>

<p>而本文所介绍的方法，一共有两个地方使用了私有 API：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Class</span> <span class="n">cls</span> <span class="o">=</span> <span class="n">NSClassFromString</span><span class="p">(</span><span class="s">@"WKBrowsingContextController"</span><span class="p">);</span>
<span class="n">SEL</span> <span class="n">sel</span> <span class="o">=</span> <span class="n">NSSelectorFromString</span><span class="p">(</span><span class="s">@"registerSchemeForCustomProtocol:"</span><span class="p">);</span>
</code></pre></div></div>

<p>这两个地方都是通过反射的方式拿到了私有的 class/selector，对应上面的第四条。其中第二行那个还好说，因为 <code class="language-plaintext highlighter-rouge">registerSchemeForCustomProtocol</code> 这个名词看上去相当普通，如果把这种字符串也禁掉了的话会误伤一大票开发者，所以有风险的主要是 <code class="language-plaintext highlighter-rouge">WKBrowsingContextController</code> 这个字符串，要前缀有前缀，要 camel case 有 camel case，再跟私有 class 名撞车的话就跟可能被拒了。</p>

<p>那么怎样绕过这个字符串呢？查询 <a href="https://github.com/JaviSoto/iOS10-Runtime-Headers/blob/master/Frameworks/WebKit.framework/WKWebView.h">WKWebView.h</a> 可以看到，有个方法 <code class="language-plaintext highlighter-rouge">- browsingContextController</code> 的方法名跟 <code class="language-plaintext highlighter-rouge">WKBrowsingContextController</code> 长得很像，通过 KVC 取出来（没错，KVC 不但可以取 property 取 ivar，还可以取无入参 selector 的返回值）发现它就是 <code class="language-plaintext highlighter-rouge">WKBrowsingContextController</code> 的一个实例，这样一来这个私有类就可以通过 KVC 的方式来得到了：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Class</span> <span class="n">cls</span> <span class="o">=</span> <span class="p">[[[</span><span class="n">WKWebView</span> <span class="nf">new</span><span class="p">]</span> <span class="nf">valueForKey</span><span class="p">:</span><span class="s">@"browsingContextController"</span><span class="p">]</span> <span class="nf">class</span><span class="p">];</span>
</code></pre></div></div>

<p>比起粗暴地 <code class="language-plaintext highlighter-rouge">NSClassFromString</code>，使用 <code class="language-plaintext highlighter-rouge">valueForKey</code> 的方法安全了许多。当然，如果还有什么要担心的话，这些字符串也可以不明着写出来，只要运行时算出来就行，比如用 base64 编码啊，图片资源里藏一段啊，甚至通过服务器下发……既然到了这个程度，苹果的静态扫描就很难再 hold 住了。</p>

<p>使用私有 API 的另一风险是兼容性问题，比如上面的 <code class="language-plaintext highlighter-rouge">browsingContextController</code> 就只能在 iOS 8.4 以后才能用，反注册 scheme 的方法 <code class="language-plaintext highlighter-rouge">unregisterSchemeForCustomProtocol:</code> 也是在 iOS 8.4 以后才被添加进来的，要支持 iOS 8.0 ~ 8.3 机型的话，只能通过动态生成字符串的方式拿到 WKBrowsingContextController，而且还不能反注册，不过这些问题都不大。至于向后兼容，这个也不用太担心，因为 iOS 发布新版本之前都会有开发者预览版的，那个时候再测一下也不迟。对于本文的例子来说，如果将来哪个 iOS 版本移除了这个 API，那很可能是因为官方提供了完整的解决方案，到那时候自然也不需要本文介绍的方法了。</p>

<p>最后，我写了一个 Demo 放到了 GitHub 上，支持 iOS 8.4+，代码经测试已通过 App Store 审核：
https://github.com/yeatse/NSURLProtocol-WebKitSupport</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[最近把公司的项目从 UIWebView 迁移到了 WKWebView，因为之前大体上还是遵从了 Apple 的 API 没有过度地去 hack，而且 WebViewJavascriptBridge 也同样支持 WKWebView，所以迁移过程没有想象中那么痛苦，只要把 UIWebViewDelegate 的方法改成 WKUIDelegate 和 WKNavigationDelegate 对应方法就好了。]]></summary></entry><entry><title type="html">UIWebView 与 3D Touch 的自定义交互</title><link href="https://blog.moecoder.com/using-3d-touch-with-uiwebview.html" rel="alternate" type="text/html" title="UIWebView 与 3D Touch 的自定义交互" /><published>2016-10-08T20:50:40+08:00</published><updated>2016-10-08T20:50:40+08:00</updated><id>https://blog.moecoder.com/using-3d-touch-with-uiwebview</id><content type="html" xml:base="https://blog.moecoder.com/using-3d-touch-with-uiwebview.html"><![CDATA[<h2 id="0x00-3d-touch-api-处理-uiwebview-的局限">0x00 3D Touch API 处理 UIWebView 的局限</h2>

<p>从 iOS 9 开始，UIKit 新增了 3D Touch 相关接口，如果使用苹果推荐的 storyboard 搭建 UI，勾选了 <code class="language-plaintext highlighter-rouge">Preview &amp; Commit Segues</code> 选项之后就可以零代码实现系统级的 3D Touch 效果；用代码实现也很简单，只要实现 <code class="language-plaintext highlighter-rouge">UIViewControllerPreviewingDelegate</code> 协议，然后调用 <code class="language-plaintext highlighter-rouge">- [UIViewController registerForPreviewingWithDelegate:sourceView:]</code> 方法，就可以对任意 UIView 进行 Peek 和 Pop 操作了。</p>

<p>实际上，尽管 <code class="language-plaintext highlighter-rouge">- [UIViewController registerForPreviewingWithDelegate:sourceView:]</code> 方法的第二个参数接受的是任意的 UIView，但在 UIWebView 和 WKWebView 上按压的操作却是没有效果的。虽然苹果针对这两个 WebView 提供了 <code class="language-plaintext highlighter-rouge">allowsLinkPreview</code> 属性做了特殊处理，但这也仅仅是调用了 Safari 打开链接，实际应用中经常要针对某些特殊的链接进行应用内跳转，这是 <code class="language-plaintext highlighter-rouge">allowsLinkPreview</code> 无论如何也完成不了的。</p>

<!-- more -->

<p>那么为什么在其他 View 上都正常的 3D Touch 在 WebView 上却无效了呢？以 UIWebView 为例，在实际应用中可以发现，如果把 WebView 的 <code class="language-plaintext highlighter-rouge">userInteractionEnabled</code> 值设为 NO，或者按压空白位置，<code class="language-plaintext highlighter-rouge">UIViewControllerPreviewingDelegate</code> 中的方法还是可以正常回调的，所以原因很可能是 UIWebView 处理链接点击事件的手势与 3D Touch 的手势发生了冲突。要让 UIWebView 和其他 View 一样支持 Peek &amp; Pop，就要完成以下三个步骤：</p>

<ol>
  <li>取出 UIWebView 中处理点击事件的 gesture recognizer；</li>
  <li>取出 UIWebView 中处理 3D Touch 的 gesture recognizer；</li>
  <li>对 <code class="language-plaintext highlighter-rouge">1</code> 中的每一个手势监听器，调用 <code class="language-plaintext highlighter-rouge">- [UIGestureRecognizer requireGestureRecognizerToFail:]</code> 方法，保证 UIKit 优先处理 3D Touch 事件。</li>
</ol>

<h2 id="0x01-uiwebview-的点击与-3d-touch-手势的冲突解决">0x01 UIWebView 的点击与 3D Touch 手势的冲突解决</h2>

<p>通过断点等方法可以得出，UIWebView 的内部视图层级结构和继承关系是这样的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+--- UIWebView → UIView
|   |
|   +--- _UIWebViewScrollView → UIWebScrollView → UIScrollView → UIView
|       |
|       +--- UIWebBrowserView → UIWebDocumentView → UIWebTiledView → UIView
|           |
|           (...)
</code></pre></div></div>

<p>UIWebBrowserView 是显示 WebView 内部元素的容器，处理链接点击事件也应该由它来做，于是在 Xcode 中打个断点偷窥一下成员变量，果然在它的父类 UIWebDocumentView 里看到了一组与 gesture 有关的私有成员：</p>

<p><img src="/assets/images/2016/14759362518041.jpg" alt="" /></p>

<p>看这些单词的意思很明显了，与 3D Touch 冲突的手势可能是 <code class="language-plaintext highlighter-rouge">_singleTapGestureRecognizer</code>、<code class="language-plaintext highlighter-rouge">highlightLongPressGestureRecognizer</code>、<code class="language-plaintext highlighter-rouge">_longPressGestureRecognizer</code>。在 runtime 面前一切私有成员都是纸老虎，通过 KVC 把这三个手势取出来：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">UIView</span><span class="o">*</span> <span class="n">browserView</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span> <span class="nf">valueForKeyPath</span><span class="p">:</span><span class="s">@"internal.browserView"</span><span class="p">];</span> <span class="c1">// internal.browserView 是一个能获取内部 UIWebBrowserView 的私有 keyPath</span>
<span class="n">UIGestureRecognizer</span><span class="o">*</span> <span class="n">singleTapGesture</span> <span class="o">=</span> <span class="p">[</span><span class="n">browserView</span> <span class="nf">valueForKey</span><span class="p">:</span><span class="s">@"singleTapGestureRecognizer"</span><span class="p">];</span>
<span class="n">UIGestureRecognizer</span><span class="o">*</span> <span class="n">longPressGesture</span> <span class="o">=</span> <span class="p">[</span><span class="n">browserView</span> <span class="nf">valueForKey</span><span class="p">:</span><span class="s">@"longPressGestureRecognizer"</span><span class="p">];</span>
<span class="n">UIGestureRecognizer</span><span class="o">*</span> <span class="n">highlightLongPressGesture</span> <span class="o">=</span> <span class="p">[</span><span class="n">browserView</span> <span class="nf">valueForKey</span><span class="p">:</span><span class="s">@"highlightLongPressGestureRecognizer"</span><span class="p">];</span>
</code></pre></div></div>

<p>同样地，检查调用 <code class="language-plaintext highlighter-rouge">registerForPreviewingWithDelegate:sourceView:</code> 前后 UIWebView 的 gesture 变化，发现注册了 Previewing Delegate 之后，UIWebView 的父类 <code class="language-plaintext highlighter-rouge">UIView</code> 多出了三个手势监听器：</p>

<p><img src="/assets/images/2016/14759368882844.jpg" alt="" /></p>

<p>那么这三个监听器自然也就与 3D Touch 相关了。接下来调用 <code class="language-plaintext highlighter-rouge">requireGestureRecognizerToFail:</code> 为上边取出的手势添加依赖：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="n">UIGestureRecognizer</span><span class="o">*</span> <span class="n">gesture</span> <span class="k">in</span> <span class="n">self</span><span class="p">.</span><span class="n">webView</span><span class="p">.</span><span class="n">gestureRecognizers</span><span class="p">)</span> <span class="p">{</span>
    <span class="p">[</span><span class="n">singleTapGesture</span> <span class="nf">requireGestureRecognizerToFail</span><span class="p">:</span><span class="n">gesture</span><span class="p">];</span>
    <span class="p">[</span><span class="n">longPressGesture</span> <span class="nf">requireGestureRecognizerToFail</span><span class="p">:</span><span class="n">gesture</span><span class="p">];</span>
    <span class="p">[</span><span class="n">highlightLongPressGesture</span> <span class="nf">requireGestureRecognizerToFail</span><span class="p">:</span><span class="n">gesture</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这样一来 UIWebView 就可以和普通的 UIView 一样使用 <code class="language-plaintext highlighter-rouge">UIViewControllerPreviewingDelegate</code> 进行 Peek 和 Pop 了：</p>

<p><img src="/assets/images/2016/20161009-screenshot.gif" alt="screenshot" /></p>

<h2 id="0x02-3d-touch-过程中监听器的状态变化">0x02 3D Touch 过程中监听器的状态变化</h2>

<p>按上面的方法我们虽然可以正常使用 Peek 和 Pop，但是正常的链接点击却也受到了影响。当<code class="language-plaintext highlighter-rouge">长按</code>链接并松手之后，即使没有<code class="language-plaintext highlighter-rouge">重按</code>调出 Peek 界面，链接点击事件也依然没有触发，这跟 UIButton 和 UITableViewCell 等行为不一样，所以还需要进一步的处理。</p>

<p>在上面得到的三个 3D Touch 相关手势当中，仅仅根据类名很难猜出它们的具体作用。于是祭出 KVO，进行操作的同时监听它们的 state 变化，得到结果如下：</p>

<h4 id="单击">单击</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 1
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 2
_UIPreviewGestureRecognizer state changed to 5
_UIRevealGestureRecognizer state changed to 5
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 4
</code></pre></div></div>

<h4 id="长按后松手">长按后松手</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 1
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 2
_UIRevealGestureRecognizer state changed to 1
_UIPreviewGestureRecognizer state changed to 1
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 4
_UIRevealGestureRecognizer state changed to 3
_UIPreviewGestureRecognizer state changed to 3
</code></pre></div></div>

<h4 id="重按-peek">重按 (Peek)</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 1
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 2
_UIRevealGestureRecognizer state changed to 1
_UIPreviewGestureRecognizer state changed to 1
_UIRevealGestureRecognizer state changed to 2
_UIPreviewGestureRecognizer state changed to 2
</code></pre></div></div>

<h4 id="重按后松手">重按后松手</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_UIRevealGestureRecognizer state changed to 3
_UIPreviewGestureRecognizer state changed to 3
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 4
</code></pre></div></div>

<h4 id="peek-之后-pop">Peek 之后 Pop</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>_UIRevealGestureRecognizer state changed to 4
_UIPreviewInteractionTouchObservingGestureRecognizer state changed to 4
_UIPreviewGestureRecognizer state changed to 4
</code></pre></div></div>

<p>可以看出，对于 <code class="language-plaintext highlighter-rouge">_UIPreviewGestureRecognizer</code> 而言（<code class="language-plaintext highlighter-rouge">_UIRevealGestureRecognizer</code> 同样），除了单击的时候 state 值会变成 5 (<code class="language-plaintext highlighter-rouge">UIGestureRecognizerStateFailed</code>) 以外，其余情况都成功触发了按压手势，从而导致链接点击的手势启动失败。这也很容易理解，因为长按也算是重按的一种，力度轻了点而已，通过断点也可以看出 <code class="language-plaintext highlighter-rouge">_UIPreviewGestureRecognizer</code> 和 <code class="language-plaintext highlighter-rouge">_UIRevealGestureRecognizer</code> 本身就是继承自 <code class="language-plaintext highlighter-rouge">UILongPressGestureRecognizer</code> 的。</p>

<p>对比<code class="language-plaintext highlighter-rouge">长按</code>和<code class="language-plaintext highlighter-rouge">重按</code>的状态变化可以发现，<code class="language-plaintext highlighter-rouge">重按</code>的时候，<code class="language-plaintext highlighter-rouge">_UIPreviewGestureRecognizer</code>的 state 值会变成 2 (<code class="language-plaintext highlighter-rouge">UIGestureRecognizerStateChanged</code>)，这就是区别<code class="language-plaintext highlighter-rouge">长按</code>和<code class="language-plaintext highlighter-rouge">重按</code>的突破口。那么接下来要做的就是，在 <code class="language-plaintext highlighter-rouge">_UIPreviewGestureRecognizer</code> 的 state 值变成 3 (<code class="language-plaintext highlighter-rouge">UIGestureRecognizerStateEnded</code>)的时候，检查之前是否有“重按”过，如果没有重按过（state 从 1 直接变成 3），那么就手动触发 <code class="language-plaintext highlighter-rouge">_singleTapGestureRecognizer</code> 手势，模拟对 UIWebView 的点击事件。</p>

<h2 id="0x03-用代码触发-uiwebview-的点击事件">0x03 用代码触发 UIWebView 的点击事件</h2>

<p>那么问题又来了，如何通过代码来触发一个手势事件呢？</p>

<p>一个自然的想法是构造一个 UITouch 丢给系统，不过构造这个相当麻烦，<a href="http://www.cocoawithlove.com/2008/10/synthesizing-touch-event-on-iphone.html">一篇 2008 年的文章</a>详细地说明了整个步骤。实际上我们模拟手势的目的只是为了调用对应的 selector 而已，如果能拿到对应的 selector 的话，直接调用 selector 就好了。</p>

<p>通过断点检查 <code class="language-plaintext highlighter-rouge">_singleTapGestureRecognizer</code> 的成员变量，可以看到里面 <code class="language-plaintext highlighter-rouge">_targets</code> 那一项很有意思：</p>

<p><img src="/assets/images/2016/14759403483679.jpg" alt="" /></p>

<p>很明显这个单击事件最终是要调用 UIWebBrowserView 的 <code class="language-plaintext highlighter-rouge">_singleTapRecognized:</code> 方法的，所以我们可以通过 <code class="language-plaintext highlighter-rouge">performSelector:withObject:</code> 绕过 gesture recognizer 直接调用它。</p>

<p>最后一个问题是 <code class="language-plaintext highlighter-rouge">_singleTapRecognized:</code> 的参数问题，直接传 <code class="language-plaintext highlighter-rouge">_singleTapGestureRecognizer</code> 本身会被认为点击了左上角，跟传 nil 效果一样。怀疑 Apple 使用了某个私有方法或变量来确定点击的坐标，于是传一个 NSObject 进去试试，系统弹出这个错误：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-[NSObject location]: unrecognized selector sent to instance 0x6000000162e0
</code></pre></div></div>

<p>查看 dump 出的 <a href="https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/UIKit.framework/UITapGestureRecognizer.h">UITapGestureRecognizer.h</a> 文件可以知道，location 是 UITapGestureRecognizer 私有的一个计算型属性，返回类型是 CGPoint。猜想 <code class="language-plaintext highlighter-rouge">_singleTapRecognized:</code> 就是通过它来确定到底点了哪里的，那么我们只要构造出一个有 location 属性的对象，把 location 存进去再交给这个方法就可以了：</p>

<div class="language-objc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@interface</span> <span class="nc">LocationWrapper</span> <span class="p">:</span> <span class="nc">NSObject</span>

<span class="k">@property</span> <span class="p">(</span><span class="n">nonatomic</span><span class="p">)</span> <span class="n">CGPoint</span> <span class="n">location</span><span class="p">;</span>

<span class="k">@end</span>

<span class="k">@implementation</span> <span class="nc">LocationWrapper</span>

<span class="k">@end</span>

<span class="c1">// ...</span>

<span class="n">LocationWrapper</span><span class="o">*</span> <span class="n">wrapper</span> <span class="o">=</span> <span class="p">[</span><span class="n">LocationWrapper</span> <span class="nf">new</span><span class="p">];</span>
<span class="n">wrapper</span><span class="p">.</span><span class="n">location</span> <span class="o">=</span> <span class="p">[</span><span class="n">singleTapGesture</span> <span class="nf">locationInView</span><span class="p">:</span><span class="n">singleTapGesture</span><span class="p">.</span><span class="n">view</span><span class="p">];</span>
<span class="p">[</span><span class="n">browserView</span> <span class="nf">performSelector</span><span class="p">:</span><span class="n">NSSelectorFromString</span><span class="p">(</span><span class="s">@"_singleTapRecognized"</span><span class="p">)</span> <span class="nf">withObject</span><span class="p">:</span><span class="n">wrapper</span><span class="p">];</span>
</code></pre></div></div>

<h2 id="0x04-talk-is-cheap-">0x04 Talk is cheap …</h2>

<p>按上面的思路，UIWebView 与 Peek &amp; Pop 的冲突就可以比较完美地解决了。我把它封装成了一个 category，通过 method swizzling 的方式尽量简化使用成本，只要把 UIWebView+PeekingSupport.h 和 UIWebView+PeekingSupport.m 拖到工程里，其他什么都不用做，就可以像正常 UIView 一样在使用 UIWebView 上使用 3D Touch 了。项目地址在这里：
https://github.com/yeatse/UIWebView-PeekingSupport</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[0x00 3D Touch API 处理 UIWebView 的局限]]></summary></entry><entry><title type="html">View Controller 转场实现机制分析</title><link href="https://blog.moecoder.com/view-controller-transition-implementation.html" rel="alternate" type="text/html" title="View Controller 转场实现机制分析" /><published>2016-09-10T12:33:51+08:00</published><updated>2016-09-10T12:33:51+08:00</updated><id>https://blog.moecoder.com/view-controller-transition-implementation</id><content type="html" xml:base="https://blog.moecoder.com/view-controller-transition-implementation.html"><![CDATA[<p>众所周知，iOS 的 View Controller 的转场效果本质上是基于“当前视图消失和下一视图出现”所进行的动画。如果是自己实现的 UIViewControllerAnimatedTransitioning 协议，那么此动画就由 <code class="language-plaintext highlighter-rouge">- animateTransition:</code> 方法来提供；如果没有实现此协议则此动画由系统提供。以结构如下图的 Navigation Controller 为例：</p>

<p><img src="/assets/images/2016/14734732613835.jpg" alt="Navigation Controller 结构" /></p>

<!-- more -->

<p>在页面 B → A 切换的过程中，应用的视图层级结构如下图所示：
<img src="/assets/images/2016/14734073322458.jpg" alt="Navigation Controller B → A 切换" /></p>

<p>最终运行的转场动画，不论是系统默认的平移动画还是通过 UIViewControllerAnimatedTransitioning 来实现的动画，都作用在 View Controller A &amp; B 共同的父视图 UIViewControllerWrapperView 内部，而这个共同的父视图即是 <code class="language-plaintext highlighter-rouge">- [UIViewControllerContextTransitioning containerView]</code> 所返回的那个容器 View。</p>

<hr />

<p>下面再说说可交互式动画的实现机制。</p>

<p>一般情况下，实现可交互式动画需要先实现 UIViewControllerInteractiveTransitioning 协议，通过<code class="language-plaintext highlighter-rouge">- startInteractiveTransition:</code> 来启动转场，然后不断调用 <code class="language-plaintext highlighter-rouge">- updateInteractiveTransition:</code> 更新转场进度，最后调用 <code class="language-plaintext highlighter-rouge">-  finishInteractiveTransition</code> 或 <code class="language-plaintext highlighter-rouge">- cancelInteractiveTransition</code> 来完成或取消转场。</p>

<p><code class="language-plaintext highlighter-rouge">- updateInteractiveTransition:</code> 方法接受一个表示<code class="language-plaintext highlighter-rouge">完成百分比</code>的参数，显然这个百分比是用来更新转场动画 CAAnimation 的进度的，但是 Core Animation 框架并没有提供直接改变动画进度的接口，所以一直以来我都以为苹果利用它的特权调用了某些私有方法来完成这件事。直到有一天，我在一个视图中通过 <code class="language-plaintext highlighter-rouge">- [CALayer convertTime:fromLayer:]</code> 来定期检查当前 layer 的相对时间：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CFTimeInterval</span> <span class="n">time</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span><span class="p">.</span><span class="n">layer</span> <span class="nf">convertTime</span><span class="p">:</span><span class="n">CACurrentMediaTime</span><span class="p">()</span> <span class="nf">fromLayer</span><span class="p">:</span><span class="nb">nil</span><span class="p">];</span>
<span class="n">NSLog</span><span class="p">(</span><span class="s">@"Current layer time: %@"</span><span class="p">,</span> <span class="err">@</span><span class="p">(</span><span class="n">time</span><span class="p">));</span>
</code></pre></div></div>

<p>在通过左滑手势操作当前页滑动返回时，打出的 Log 是这样的：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Current layer time: 261910.875040995
Current layer time: 261911.377889105
Current layer time: 0.02057165431515606
Current layer time: 0.05607890069196765
Current layer time: 0.1110305915132237
Current layer time: 0.2074074031074266
Current layer time: 0.2620772946859903
Current layer time: 0.3263285024154589
Current layer time: 261917.375051694
Current layer time: 261917.879273495
</code></pre></div></div>

<p>当前 layer 的时间竟然变成了返回动画的相对时间！根据 Apple 文档说明，这个时间会被 CAMediaTiming 协议的属性所影响（如 speed），并且一个 layer 的时间发生改变，则此 layer 层级树中的子孙 layer 的时间全部发生同样的改变。所以我决定找出导致 layer 的 <code class="language-plaintext highlighter-rouge">speed</code> 值改变的元凶：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="n">UIView</span><span class="o">*</span> <span class="n">view</span> <span class="o">=</span> <span class="n">self</span><span class="p">;</span> <span class="n">view</span><span class="p">;</span> <span class="n">view</span> <span class="o">=</span> <span class="n">view</span><span class="p">.</span><span class="n">superview</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">view</span><span class="p">.</span><span class="n">layer</span><span class="p">.</span><span class="n">speed</span> <span class="o">!=</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">NSLog</span><span class="p">(</span><span class="s">@"Speed changed in the layer of view: %@, speed: %@, time offset: %@"</span><span class="p">,</span> <span class="n">view</span><span class="p">.</span><span class="n">class</span><span class="p">,</span> <span class="err">@</span><span class="p">(</span><span class="n">view</span><span class="p">.</span><span class="n">layer</span><span class="p">.</span><span class="n">speed</span><span class="p">),</span> <span class="err">@</span><span class="p">(</span><span class="n">view</span><span class="p">.</span><span class="n">layer</span><span class="p">.</span><span class="n">timeOffset</span><span class="p">));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>输出：</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Speed changed in the layer of view: UIViewControllerWrapperView, speed: 0, time offset: 0.1144122469252434
Speed changed in the layer of view: UIViewControllerWrapperView, speed: 0, time offset: 0.1479468599033816
Speed changed in the layer of view: UIViewControllerWrapperView, speed: 0, time offset: 0.172463768115942
Speed changed in the layer of view: UIViewControllerWrapperView, speed: 0, time offset: 0.2020531400966183
</code></pre></div></div>

<p>于是可交互动画的实现机制就变得明朗了：</p>

<ol>
  <li>调用 <code class="language-plaintext highlighter-rouge">- startInteractiveTransition:</code> 方法，实际上是将 UIViewControllerWrapperView 的 <code class="language-plaintext highlighter-rouge">layer.speed</code> 值改成 0 以暂停动画；</li>
  <li>调用 <code class="language-plaintext highlighter-rouge">- updateInteractiveTransition:</code> 方法，实际上是通过 <code class="language-plaintext highlighter-rouge">duration</code> 和百分比换算出一个合适的 <code class="language-plaintext highlighter-rouge">timeOffset</code>，更新到 UIViewControllerWrapperView 的 layer 上面，以模拟动画进度更新的效果；</li>
  <li>调用 <code class="language-plaintext highlighter-rouge">- finishInteractiveTransition</code>，则是将 <code class="language-plaintext highlighter-rouge">layer.speed</code> 值改回 1，让 Core Animation 继续完成剩下的动画。</li>
</ol>

<p>这个 UIViewControllerWrapperView 就是上面图中那个共同的父 View，在不同的实现中，它的类名可能会有变化，但都是 <code class="language-plaintext highlighter-rouge">- [UIViewControllerContextTransitioning containerView]</code> 返回的那个容器 View。由于父 layer 的动画时间改变是会影响到所有子 layer 的，所以有时候即使不用 <code class="language-plaintext highlighter-rouge">transitionCoordinator</code>，一些看上去与转场过程毫无关联的动画依然会受滑动返回手势的影响：</p>

<p><img src="/assets/images/2016/20160910-screenshot.gif" alt="screenshot" /></p>

<p>最后一个问题，是调用 <code class="language-plaintext highlighter-rouge">- cancelInteractiveTransition</code> 之后，系统会自动将动画逆转，以回退到切换之前的状态，这个又是如何实现的呢？</p>

<p><a href="http://blog.devtang.com/2016/03/13/iOS-transition-guide/">网上一些文章</a> 介绍了通过一帧帧调整 timeOffset 值来模拟逆转动画的效果，还用了 CADisplayLink 来同步屏幕刷新率，讲道理我是不信苹果会用这么蠢的办法的。通过一系列断点和 Log 调试发现，实际上苹果做的事非常巧妙：将所有 CAAnimation 的 <code class="language-plaintext highlighter-rouge">autoreverses</code> 属性设定为 <code class="language-plaintext highlighter-rouge">YES</code>，然后将 <code class="language-plaintext highlighter-rouge">beginTime</code> 设定为一个在 <code class="language-plaintext highlighter-rouge">(- 2 * duration, - duration)</code> 区间的一个合适的值，再用 CAAnimationGroup 包裹起来，以保证 CAAnimation 只运行想要的那部分动画。这样一来由于 <code class="language-plaintext highlighter-rouge">autoreverses</code> 的作用，Core Animation 层自己就会将动画回放了。</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[众所周知，iOS 的 View Controller 的转场效果本质上是基于“当前视图消失和下一视图出现”所进行的动画。如果是自己实现的 UIViewControllerAnimatedTransitioning 协议，那么此动画就由 - animateTransition: 方法来提供；如果没有实现此协议则此动画由系统提供。以结构如下图的 Navigation Controller 为例：]]></summary></entry><entry><title type="html">在 iOS APP 崩溃时弹出友好提示框</title><link href="https://blog.moecoder.com/show-a-friendly-alert-after-app-crashes.html" rel="alternate" type="text/html" title="在 iOS APP 崩溃时弹出友好提示框" /><published>2016-07-30T14:57:57+08:00</published><updated>2016-07-30T14:57:57+08:00</updated><id>https://blog.moecoder.com/show-a-friendly-alert-after-app-crashes</id><content type="html" xml:base="https://blog.moecoder.com/show-a-friendly-alert-after-app-crashes.html"><![CDATA[<p>昨天补了 iOS RunLoop 相关的基础知识，在一部<a href="http://v.youku.com/v_show/id_XODgxODkzODI0.html">讨论 RunLoop 实现细节的视频</a>的最后面，@sunnyxx 讲到了一个很有意思的黑科技————“让 App 在 Crash 的时候回光返照”，内容大致如下：</p>

<blockquote>
  <div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//取当前 run loop</span>
<span class="n">CFRunLoopRef</span> <span class="n">runLoop</span> <span class="o">=</span> <span class="n">CFRunLoopGetCurrent</span><span class="p">();</span>
<span class="c1">//取 run loop 所有运行的 mode</span>
<span class="n">NSArray</span> <span class="o">*</span><span class="n">allModes</span> <span class="o">=</span> <span class="n">CFBridgingRelease</span><span class="p">(</span><span class="n">CFRunLoopCopyAllModes</span><span class="p">(</span><span class="n">runLoop</span><span class="p">));</span>
<span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="n">NSString</span> <span class="o">*</span><span class="n">mode</span> <span class="k">in</span> <span class="n">allModes</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">//在每个 mode 中轮流运行至少 0.001 秒</span>
        <span class="n">CFRunLoopRunInMode</span><span class="p">((</span><span class="n">CFStringRef</span><span class="p">)</span><span class="n">mode</span><span class="p">,</span> <span class="mi">0</span><span class="p">.</span><span class="mo">001</span><span class="p">,</span> <span class="nb">false</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>  </div>
  <p>对于因为接收到 crash 的 signal 而挂掉的程序，可以在接收到 crash 的信号之后重新起一个 run loop 然后跑起来。但是这个并不能保证 app 能像原来一样能正常运行，只能是利用它来在奄奄一息的状态下弹出一些友好的错误信息。</p>
</blockquote>

<!-- more -->

<p>自己写了个 Demo 测试了一下，首先随便触发一个 unrecognized selector 错误：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">UIView</span><span class="o">*</span> <span class="n">view</span> <span class="o">=</span> <span class="p">(</span><span class="n">id</span><span class="p">)[</span><span class="n">NSObject</span> <span class="nf">new</span><span class="p">];</span>
<span class="n">view</span><span class="p">.</span><span class="n">hidden</span> <span class="o">=</span> <span class="nb">YES</span><span class="p">;</span>
</code></pre></div></div>

<p>捕获到的崩溃栈如下图：
<img src="/assets/images/2016/14698640741585.jpg" alt="" /></p>

<p>可以看到，RunLoop 在 Source0 中处理点击事件，调用了未定义的 selector 之后经过一系列消息转发，最后调用 objc_exception_throw 抛出了异常。</p>

<p>Foundation 提供的 NSSetUncaughtExceptionHandler 方法可以截获到这个异常。通过它设置一个回调函数，在里面展示一个 UIAlertViewController，再按照上面的方式手动启动一个 RunLoop 来监听手势事件，用 Swift 实现如下：</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">NSSetUncaughtExceptionHandler</span> <span class="p">{</span> <span class="p">(</span><span class="n">exception</span><span class="p">)</span> <span class="k">in</span>
    <span class="k">var</span> <span class="nv">shouldRun</span> <span class="o">=</span> <span class="kc">true</span>
    
    <span class="k">let</span> <span class="nv">runLoop</span> <span class="o">=</span> <span class="kt">CFRunLoopGetCurrent</span><span class="p">()</span>
    
    <span class="k">let</span> <span class="nv">alertCtrl</span> <span class="o">=</span> <span class="kt">UIAlertController</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"Oops"</span><span class="p">,</span> <span class="nv">message</span><span class="p">:</span> <span class="s">"Your app crashed! OAO"</span><span class="p">,</span> <span class="nv">preferredStyle</span><span class="p">:</span> <span class="o">.</span><span class="kt">Alert</span><span class="p">)</span>
    <span class="n">alertCtrl</span><span class="o">.</span><span class="nf">addAction</span><span class="p">(</span><span class="kt">UIAlertAction</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="s">"OK"</span><span class="p">,</span> <span class="nv">style</span><span class="p">:</span> <span class="o">.</span><span class="kt">Default</span><span class="p">,</span> <span class="nv">handler</span><span class="p">:</span> <span class="p">{</span> <span class="p">(</span><span class="n">_</span><span class="p">)</span> <span class="k">in</span>
        <span class="n">shouldRun</span> <span class="o">=</span> <span class="kc">false</span>
    <span class="p">}))</span>
    
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">rootViewController</span> <span class="o">=</span> <span class="kt">UIApplication</span><span class="o">.</span><span class="nf">sharedApplication</span><span class="p">()</span><span class="o">.</span><span class="n">keyWindow</span><span class="p">?</span><span class="o">.</span><span class="n">rootViewController</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="n">rootViewController</span><span class="o">.</span><span class="nf">presentViewController</span><span class="p">(</span><span class="n">alertCtrl</span><span class="p">,</span> <span class="nv">animated</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="nv">completion</span><span class="p">:</span> <span class="kc">nil</span><span class="p">)</span>
    
    <span class="k">let</span> <span class="nv">allModesAO</span> <span class="o">=</span> <span class="kt">CFRunLoopCopyAllModes</span><span class="p">(</span><span class="n">runLoop</span><span class="p">)</span> <span class="k">as</span> <span class="p">[</span><span class="kt">AnyObject</span><span class="p">]</span>
    <span class="k">guard</span> <span class="k">let</span> <span class="nv">allModes</span> <span class="o">=</span> <span class="n">allModesAO</span> <span class="k">as?</span> <span class="p">[</span><span class="kt">CFStringRef</span><span class="p">]</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span>
    <span class="p">}</span>
    
    <span class="k">while</span> <span class="p">(</span><span class="n">shouldRun</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">mode</span> <span class="k">in</span> <span class="n">allModes</span> <span class="p">{</span>
            <span class="kt">CFRunLoopRunInMode</span><span class="p">(</span><span class="n">mode</span><span class="p">,</span> <span class="mf">0.001</span><span class="p">,</span> <span class="kc">false</span><span class="p">)</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这样就可以在程序 Crash 之前弹出一个提示框了。</p>

<p>一个需要注意的地方是，如果程序使用了第三方 SDK 做崩溃收集的话，很可能由于第三方 SDK 也注册了 UncaughtExceptionHandler 导致自己注册的函数被覆盖掉。另外由于注册的回调<a href="https://github.com/opensource-apple/objc4/blob/cd5e62a5597ea7a31dccef089317abb3a661c154/runtime/objc-exception.mm#L673-L682">只对 Foundation 对象的异常有效</a>，所以这个方法只对 NSException 起作用，对于 BAD_ACCESS、std::terminate() 等非 Foundation 异常还是没有办法的。至于用 @throw 还是 c++ 的 throw 这个倒是影响不大，亲自尝试了之后发现两者都是抛 NSException 有效，抛其他对象 (包括非 NSException 的 NSObject)无效的。</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[昨天补了 iOS RunLoop 相关的基础知识，在一部讨论 RunLoop 实现细节的视频的最后面，@sunnyxx 讲到了一个很有意思的黑科技————“让 App 在 Crash 的时候回光返照”，内容大致如下：]]></summary></entry><entry><title type="html">检测 iOS 项目中的内存泄漏</title><link href="https://blog.moecoder.com/find-memory-leaks-in-ios-project.html" rel="alternate" type="text/html" title="检测 iOS 项目中的内存泄漏" /><published>2016-07-19T21:36:50+08:00</published><updated>2016-07-19T21:36:50+08:00</updated><id>https://blog.moecoder.com/find-memory-leaks-in-ios-project</id><content type="html" xml:base="https://blog.moecoder.com/find-memory-leaks-in-ios-project.html"><![CDATA[<p>一般来说，在 ARC 环境下，只要在使用 delegate、NSTimer、block 的时候注意一下不要出现循环引用，那么 Objective-C 对象的内存泄漏问题就可以轻松避免。</p>

<p>但是在实际项目中，一些错误的结构设计可能会导致难以发现的泄漏问题，比如像 <code class="language-plaintext highlighter-rouge">A -&gt; B -&gt; C -&gt; ... -&gt; A</code> 这种长环的循环引用，或者一个实例被一个 单例 持有，在 review 的时候可能会漏掉这些问题，这时就需要流程化的方式来检测了。</p>

<!-- more -->

<p>一个很方便的检测方法是重写 dealloc 方法：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">dealloc</span> <span class="p">{</span>
    <span class="n">NSLog</span><span class="p">(</span><span class="s">@"%s"</span><span class="p">,</span> <span class="n">__func__</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>只要目标对象有 dealloc 的 log 输出，就表示这里没有出现循环引用问题。</p>

<p>对于拿不到源文件的类，也可以通过类似的方法来实现：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// DeallocationObserver.h</span>
<span class="cp">#import &lt;Foundation/Foundation.h&gt;
</span>
<span class="k">@interface</span> <span class="nc">DeallocationObserver</span> <span class="p">:</span> <span class="nc">NSObject</span>

<span class="k">+</span> <span class="p">(</span><span class="n">instancetype</span><span class="p">)</span><span class="nf">attachObserverToObject</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">object</span><span class="p">;</span>

<span class="k">@end</span>



<span class="c1">// DeallocationObserver.m</span>
<span class="cp">#import "DeallocationObserver.h"
#import &lt;objc/runtime.h&gt;
</span>
<span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">ObserverTag</span><span class="p">;</span>

<span class="k">@interface</span> <span class="nc">DeallocationObserver</span> <span class="p">()</span>

<span class="k">-</span> <span class="p">(</span><span class="n">instancetype</span><span class="p">)</span><span class="nf">initWithParent</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">parent</span><span class="p">;</span>

<span class="k">@property</span> <span class="p">(</span><span class="n">nonatomic</span><span class="p">,</span> <span class="n">copy</span><span class="p">)</span> <span class="kt">void</span><span class="p">(</span><span class="o">^</span><span class="n">deallocationBlock</span><span class="p">)();</span>

<span class="k">@end</span>

<span class="k">@implementation</span> <span class="nc">DeallocationObserver</span>

<span class="k">+</span> <span class="p">(</span><span class="n">instancetype</span><span class="p">)</span><span class="nf">attachObserverToObject</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">object</span> <span class="p">{</span>
    <span class="k">return</span> <span class="p">[[</span><span class="n">self</span> <span class="nf">alloc</span><span class="p">]</span> <span class="nf">initWithParent</span><span class="p">:</span><span class="n">object</span><span class="p">];</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="n">instancetype</span><span class="p">)</span><span class="nf">initWithParent</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">parent</span> <span class="p">{</span>
    <span class="n">self</span> <span class="o">=</span> <span class="p">[</span><span class="n">super</span> <span class="nf">init</span><span class="p">];</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">self</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">NSString</span><span class="o">*</span> <span class="n">deallocMsg</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSString</span> <span class="nf">stringWithFormat</span><span class="p">:</span><span class="s">@"deallocated: %@"</span><span class="p">,</span> <span class="n">parent</span><span class="p">];</span>
        <span class="n">self</span><span class="p">.</span><span class="n">deallocationBlock</span> <span class="o">=</span> <span class="o">^</span><span class="p">{</span>
            <span class="n">NSLog</span><span class="p">(</span><span class="s">@"%@"</span><span class="p">,</span> <span class="n">deallocMsg</span><span class="p">);</span>
        <span class="p">};</span>
        <span class="n">objc_setAssociatedObject</span><span class="p">(</span><span class="n">parent</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ObserverTag</span><span class="p">,</span> <span class="n">self</span><span class="p">,</span> <span class="n">OBJC_ASSOCIATION_RETAIN_NONATOMIC</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">self</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">dealloc</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">deallocationBlock</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">self</span><span class="p">.</span><span class="n">deallocationBlock</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">@end</span>


<span class="c1">// Usage:</span>
<span class="n">NSObject</span><span class="o">*</span> <span class="n">testObj</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSObject</span> <span class="nf">new</span><span class="p">];</span>
<span class="p">[</span><span class="n">DeallocationObserver</span> <span class="nf">attachObserverToObject</span><span class="p">:</span><span class="n">testObj</span><span class="p">];</span>
<span class="n">testObj</span> <span class="o">=</span> <span class="nb">nil</span><span class="p">;</span>  <span class="c1">// Output - deallocated: &lt;NSObject: 0x7fce1a412c10&gt;</span>

</code></pre></div></div>

<p>因为 NSObject 对象在 dealloc 的时候也会把 objc_setAssociatedObject 关联的对象也一并 release 掉，通过监听 DeallocationObserver 的销毁时机，我们就可以检测到目标对象的销毁事件了。</p>

<p>由于 ARC 只对 NSObject 有效，所以对于 Core Foundation、Core Graphics 等非 NSObject 对象，就需要苹果提供的 Instruments 来检测内存泄漏问题了。</p>

<p>按照 Instruments 的<a href="https://developer.apple.com/library/ios/documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/FindingLeakedMemory.html">官方文档</a>中的步骤，测试一下这段代码：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">testMemoryLeak</span> <span class="p">{</span>
    <span class="n">CFMutableDataRef</span> <span class="n">data</span> <span class="o">=</span> <span class="n">CFDataCreateMutable</span><span class="p">(</span><span class="n">kCFAllocatorDefault</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
    <span class="n">CGDataConsumerRef</span> <span class="n">consumer</span> <span class="o">=</span> <span class="n">CGDataConsumerCreateWithCFData</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>打开 Instruments - Leaks，选择目标设备和应用，然后点击🔴按钮，时间线面板就开始记录当前内存的使用情况：
<img src="/assets/images/2016/QQ20160719-1@2x.png" alt="QQ20160719-1@2x" /></p>

<p>可以看出，图中 28 s 的位置出现了内存泄漏，泄漏点刚好在 testMemoryLeak 方法上。</p>

<p>修改 Details 栏的 Leaks 选项，切换到 Call Tree，<kbd>⌘ + 2</kbd> 键切换到 Display Settings，然后勾选右边设置栏中的 Invert Call Tree 和 Hide System Libraries 选项，可以看到泄漏点具体的调用栈：
<img src="/assets/images/2016/QQ20160720-0@2x.png" alt="QQ20160720-0@2x" /></p>

<p>双击其中一个方法，Instruments 还会把出错的具体代码标识出来：
<img src="/assets/images/2016/QQ20160720-1@2x.png" alt="QQ20160720-1@2x" /></p>

<p>问题果然出现在 CFDataCreateMutable 和 CGDataConsumerCreateWithCFData 上，根据 Core Foundation 中 <a href="https://developer.apple.com/library/ios/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html#//apple_ref/doc/uid/20001148-SW3">关于方法命名的约定</a>，含有 <code class="language-plaintext highlighter-rouge">Copy</code> 和 <code class="language-plaintext highlighter-rouge">Create</code> 的方法返回的对象需要调用 CFRelease 来释放，Core Graphics / Core Text 也一样，所以需要在 testMemoryLeak 方法中加入这两行，以解决这里的内存泄漏问题：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="n">CGDataConsumerRelease</span><span class="p">(</span><span class="n">consumer</span><span class="p">);</span>
    <span class="n">CFRelease</span><span class="p">(</span><span class="n">data</span><span class="p">);</span>
</code></pre></div></div>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[一般来说，在 ARC 环境下，只要在使用 delegate、NSTimer、block 的时候注意一下不要出现循环引用，那么 Objective-C 对象的内存泄漏问题就可以轻松避免。]]></summary></entry><entry><title type="html">给 Objective-C 中的 Protocol 加上默认的实现</title><link href="https://blog.moecoder.com/default-implementation-for-oc-protocol.html" rel="alternate" type="text/html" title="给 Objective-C 中的 Protocol 加上默认的实现" /><published>2016-06-20T22:04:44+08:00</published><updated>2016-06-20T22:04:44+08:00</updated><id>https://blog.moecoder.com/default-implementation-for-oc-protocol</id><content type="html" xml:base="https://blog.moecoder.com/default-implementation-for-oc-protocol.html"><![CDATA[<h2 id="0x01-abstract-class">0x01 Abstract Class</h2>

<p>Java、C++ 等 OOP 语言有一个<code class="language-plaintext highlighter-rouge">抽象类</code>的概念，即一个类实现了部分方法，另一部分的方法必须由继承它的子类来实现。Objective-C 在设计上没有这个概念，转而提供了用途类似的 <code class="language-plaintext highlighter-rouge">协议</code>，除了不能给方法加默认实现以外，与抽象类的用法大体相同。但是在实际项目中，让一个协议实现一些共通的方法还是很有必要的，比如很多类都遵守了某一个协议，而这个协议中某一个方法的实现大体上都一样的时候，在每一个子类内部都 copy 一份同样的代码就不太合适了。</p>

<!-- more -->

<p>一种规避 copy 的做法是把它的实现抽离到全局方法中，比如下面的协议：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@protocol</span> <span class="nc">MyProtocol</span> <span class="o">&lt;</span><span class="n">NSObject</span><span class="o">&gt;</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method1</span><span class="p">;</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method2</span><span class="p">;</span>

<span class="k">@end</span>
</code></pre></div></div>

<p>如果所有子类的 <code class="language-plaintext highlighter-rouge">method2</code> 的实现都差不多，就可以将它抽到一个全局方法(或者一个单例类的方法)中：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">MyProtocolMethod2</span><span class="p">(</span><span class="n">id</span><span class="o">&lt;</span><span class="n">MyProtocol</span><span class="o">&gt;</span> <span class="n">instance</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Do with myprotocol...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>另一种办法是抛弃 <code class="language-plaintext highlighter-rouge">@protocol</code>，直接使用 <code class="language-plaintext highlighter-rouge">@interface</code>，然后使用文档说明的方式<code class="language-plaintext highlighter-rouge">约定</code>它是一个抽象类：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MyBaseClass.h</span>
<span class="k">@interface</span> <span class="nc">MyBaseClass</span> <span class="p">:</span> <span class="nc">NSObject</span>

<span class="c1">/// 这个方法必须由子类重写</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method1</span><span class="p">;</span>

<span class="c1">/// 这个方法可以被子类重写</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method2</span><span class="p">;</span>

<span class="k">@end</span>


<span class="c1">// MyBaseClass.m</span>
<span class="k">@implementation</span> <span class="nc">MyBaseClass</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method1</span> <span class="p">{</span>
    <span class="c1">// 如果没有重写就报错...</span>
    <span class="n">NSAssert</span><span class="p">(</span><span class="n">method_getImplementation</span><span class="p">(</span><span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">class</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">))</span> <span class="o">!=</span>
             <span class="n">method_getImplementation</span><span class="p">(</span><span class="n">class_getInstanceMethod</span><span class="p">([</span><span class="n">MyBaseClass</span> <span class="nf">class</span><span class="p">],</span> <span class="n">_cmd</span><span class="p">)),</span>
             <span class="s">@"method1 must be overriden!"</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method2</span> <span class="p">{</span>
    <span class="c1">// A default implementation...</span>
<span class="p">}</span>

<span class="k">@end</span>
</code></pre></div></div>

<p>以上两个方法都可以达成目的，但都有一些缺陷：前一种方法把 MyProtocol 相关的代码放到了全局环境中，不优雅；后一种方法在编译阶段没有提示，需要由开发人员仔细阅读文档才能避免误用。<a href="http://stackoverflow.com/questions/4330656/how-do-i-provide-a-default-implementation-for-an-objective-c-protocol">StackOverflow 的一篇答案</a>还提供了另一个方案：在每一个子类的 <code class="language-plaintext highlighter-rouge">+initialize</code> 方法中通过 <code class="language-plaintext highlighter-rouge">class_addMethod</code> 把协议的默认实现加到方法列表当中，但这样也略显繁琐。</p>

<h2 id="0x02-extconcreteprotocol">0x02 EXTConcreteProtocol</h2>

<p>一个第三方库 <a href="https://github.com/jspahrsummers/libextobjc">libextobjc</a> 通过 <code class="language-plaintext highlighter-rouge">EXTConcreteProtocol</code> 神奇地实现了这个功能，使用方法与原生协议类似：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MyProtocol.h</span>
<span class="k">@protocol</span> <span class="nc">MyProtocol</span> <span class="o">&lt;</span><span class="n">NSObject</span><span class="o">&gt;</span>
<span class="err">@required</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method1</span><span class="p">;</span>

<span class="err">@concrete</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method2</span><span class="p">;</span>

<span class="k">@end</span>


<span class="c1">// MyProtocol.m</span>
<span class="err">@concreteprotocol</span><span class="p">(</span><span class="n">MyProtocol</span><span class="p">)</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method1</span> <span class="p">{}</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">method2</span> <span class="p">{</span>
    <span class="c1">// A default implementation</span>
<span class="p">}</span>

<span class="k">@end</span>
</code></pre></div></div>

<p>这样声明以后，对于任何遵守 MyProtocol 协议的类，如果没有重写 <code class="language-plaintext highlighter-rouge">method2</code> 方法，都会有一个在 MyProtocol.m 中声明的默认实现。</p>

<p>这个库为什么这么吊，<code class="language-plaintext highlighter-rouge">@concrete</code> 和 <code class="language-plaintext highlighter-rouge">@concreteprotocol</code> 到底做了什么。其实 <code class="language-plaintext highlighter-rouge">concrete</code> 只是 <code class="language-plaintext highlighter-rouge">optional</code> 的别名，为了提示调用者就算不重写这个方法也一定会有的，重点还是在 <code class="language-plaintext highlighter-rouge">concreteprotocol</code> 宏上。</p>

<p>查看 EXTConcreteProtocol 源码可以知道，<code class="language-plaintext highlighter-rouge">@concreteprotocol(MyProtocol)</code> 这一行通过宏定义的方式生成了这样的一个包装类：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@interface</span> <span class="nc">MyProtocol_ProtocolMethodContainer</span> <span class="p">:</span> <span class="nc">NSObject</span> <span class="o">&lt;</span><span class="n">MyProtocol</span><span class="o">&gt;</span>
<span class="k">@end</span>

<span class="k">@implementation</span> <span class="nc">MyProtocol_ProtocolMethodContainer</span>

<span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">ext_addConcreteProtocol</span><span class="p">(</span><span class="n">objc_getProtocol</span><span class="p">(</span><span class="s">"MyProtocol"</span><span class="p">),</span> <span class="n">self</span><span class="p">))</span>
        <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"ERROR: Could not load concrete protocol %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"MyProtocol"</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">__attribute__</span><span class="p">((</span><span class="n">constructor</span><span class="p">))</span>
<span class="k">static</span> <span class="kt">void</span> <span class="nf">ext_MyProtocol_inject</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">ext_loadConcreteProtocol</span><span class="p">(</span><span class="n">objc_getProtocol</span><span class="p">(</span><span class="s">"MyProtocol"</span><span class="p">));</span>
<span class="p">}</span>

<span class="k">@end</span>
</code></pre></div></div>

<p>其中 <code class="language-plaintext highlighter-rouge">ext_addConcreteProtocol</code> 在 <code class="language-plaintext highlighter-rouge">load</code> 方法中被调用，它的作用是把将要对 MyProtocol 进行的注入操作缓存到一个全局列表中，除此之外还有一些边界条件的判断和加锁什么的。</p>

<p><code class="language-plaintext highlighter-rouge">__attribute__((constructor))</code> 是<a href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html"> GCC 的一个编译器指令</a>（其实是 Clang 的指令，但我翻遍了 Clang 的官方文档并没有找到关于 constructor 的描述- -），被它标记的函数会在整个 Objective-C runtime 初始化完毕之后，在 <code class="language-plaintext highlighter-rouge">main()</code> 函数之前被调用。这时 <code class="language-plaintext highlighter-rouge">ext_loadConcreteProtocol</code> 函数会遍历 runtime 中所有的 Class，对其中每一个遵从 MyProtocol 协议的 Class 进行缓存过的注入操作：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">metaclass</span><span class="p">,</span> <span class="n">selector</span><span class="p">))</span> <span class="p">{</span>
    <span class="c1">// it does exist, so don't overwrite it</span>
    <span class="k">continue</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// add this class method to the metaclass in question</span>
<span class="n">IMP</span> <span class="n">imp</span> <span class="o">=</span> <span class="n">method_getImplementation</span><span class="p">(</span><span class="n">method</span><span class="p">);</span>
<span class="k">const</span> <span class="kt">char</span> <span class="o">*</span><span class="n">types</span> <span class="o">=</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">method</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">class_addMethod</span><span class="p">(</span><span class="n">metaclass</span><span class="p">,</span> <span class="n">selector</span><span class="p">,</span> <span class="n">imp</span><span class="p">,</span> <span class="n">types</span><span class="p">))</span> <span class="p">{</span>
    <span class="n">fprintf</span><span class="p">(</span><span class="n">stderr</span><span class="p">,</span> <span class="s">"ERROR: Could not implement class method +%s from concrete protocol %s on class %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
    <span class="n">sel_getName</span><span class="p">(</span><span class="n">selector</span><span class="p">),</span> <span class="n">protocol_getName</span><span class="p">(</span><span class="n">protocol</span><span class="p">),</span> <span class="n">class_getName</span><span class="p">(</span><span class="n">class</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>虽然调用层级很复杂，但最终还是调用了 <code class="language-plaintext highlighter-rouge">class_addMethod</code> 方法给 Class 自动加上了默认的实现，原理跟上面的 StackOverflow 给的答案是一样的。</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[0x01 Abstract Class]]></summary></entry><entry><title type="html">扩展 UIButton 和 UICollectionViewCell 的响应区域</title><link href="https://blog.moecoder.com/expand-hit-area-for-uibutton-and-uicollectionviewcell.html" rel="alternate" type="text/html" title="扩展 UIButton 和 UICollectionViewCell 的响应区域" /><published>2016-06-13T20:56:29+08:00</published><updated>2016-06-13T20:56:29+08:00</updated><id>https://blog.moecoder.com/expand-hit-area-for-uibutton-and-uicollectionviewcell</id><content type="html" xml:base="https://blog.moecoder.com/expand-hit-area-for-uibutton-and-uicollectionviewcell.html"><![CDATA[<h2 id="0x01-前言">0x01 前言</h2>

<p>问题由一个项目需求引起。设计MM给的图大概像这样：</p>

<p><img src="/assets/images/2016/snapshot.png" alt="snapshot" /></p>

<p>如上图所示，列表的内容由服务器传回，且用户可编辑，显然这样的界面应该用 <code class="language-plaintext highlighter-rouge">UICollectionView</code> 来搭建。实现关闭按钮则需要在 <code class="language-plaintext highlighter-rouge">UICollectionViewCell</code> 的右上角添加一个 <code class="language-plaintext highlighter-rouge">UIButton</code>，并且要将 Cell 的 <code class="language-plaintext highlighter-rouge">clipsToBounds</code> 属性设置为 <code class="language-plaintext highlighter-rouge">NO</code> 以避免按钮被切掉一部分。</p>

<p>界面搭建好了，但默认状态下 UIButton 的点击响应范围跟它的显示区域一样小，导致这个按钮很难被点到，因此首先要解决的是这个按钮的热区扩展问题。</p>

<!-- more -->

<h2 id="0x02-uibutton-的热区扩展">0x02 UIButton 的热区扩展</h2>
<p>网络上介绍 UIButton 响应区域扩展的文章有很多，但大多数都是继承 UIButton 然后重写 <code class="language-plaintext highlighter-rouge">pointInside:withEvent:</code> 方法。实际上这种子类继承的方法在很多场景下是不合适的，因为这意味着每次用到这个功能都要将系统 UIButton 换成自己的子类。一个更符合 <em>Objective-C Style</em> 的方法是增加一个 UIButton 的 <code class="language-plaintext highlighter-rouge">Category</code>，然后在<strong>不影响控件默认的行为</strong>的情况下在 Category 里做文章。</p>

<p>新建一个 UIButton+ExpandHitArea 分类，像这样在头文件中添加一个 <code class="language-plaintext highlighter-rouge">hitTestEdgeInsets</code> 属性，用来配置热区扩展(根据语意实际上是缩小)的范围：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@interface</span> <span class="nc">UIButton</span> <span class="p">(</span><span class="nl">ExpandHitArea</span><span class="p">)</span>

<span class="k">@property</span> <span class="p">(</span><span class="n">nonatomic</span><span class="p">)</span> <span class="n">UIEdgeInsets</span> <span class="n">hitTestEdgeInsets</span><span class="p">;</span>

<span class="k">@end</span>
</code></pre></div></div>

<p>在 .m 文件中，我们将 <code class="language-plaintext highlighter-rouge">pointInside:withEvent:</code> 替换成修改过的方法：先将 <code class="language-plaintext highlighter-rouge">hitTestEdgeInsets</code> 的值与 <code class="language-plaintext highlighter-rouge">UIEdgeInsetsZero</code> 相比较，如果不等，则调用我们自己的扩展热区的逻辑；如果相等，则调用系统默认的 <code class="language-plaintext highlighter-rouge">pointInside:withEvent:</code> 方法，就像什么事也没发生过一样。最终代码如下：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@implementation</span> <span class="nc">UIButton</span> <span class="p">(</span><span class="nl">ExpandHitArea</span><span class="p">)</span>

<span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span> <span class="p">{</span>
    <span class="k">static</span> <span class="n">dispatch_once_t</span> <span class="n">onceToken</span><span class="p">;</span>
    <span class="n">dispatch_once</span><span class="p">(</span><span class="o">&amp;</span><span class="n">onceToken</span><span class="p">,</span> <span class="o">^</span><span class="p">{</span>
        <span class="n">YTSwizzleMethod</span><span class="p">([</span><span class="n">self</span> <span class="nf">class</span><span class="p">],</span> <span class="k">@selector</span><span class="p">(</span><span class="n">pointInside</span><span class="o">:</span><span class="n">withEvent</span><span class="o">:</span><span class="p">),</span> <span class="k">@selector</span><span class="p">(</span><span class="n">yt_pointInside</span><span class="o">:</span><span class="n">withEvent</span><span class="o">:</span><span class="p">));</span>
    <span class="p">});</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="n">UIEdgeInsets</span><span class="p">)</span><span class="n">hitTestEdgeInsets</span> <span class="p">{</span>
    <span class="n">NSValue</span><span class="o">*</span> <span class="n">value</span> <span class="o">=</span> <span class="n">objc_getAssociatedObject</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">);</span>
    <span class="n">UIEdgeInsets</span> <span class="n">insets</span> <span class="o">=</span> <span class="n">UIEdgeInsetsZero</span><span class="p">;</span>
    <span class="p">[</span><span class="n">value</span> <span class="nf">getValue</span><span class="p">:</span><span class="o">&amp;</span><span class="n">insets</span><span class="p">];</span>
    <span class="k">return</span> <span class="n">insets</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">setHitTestEdgeInsets</span><span class="p">:(</span><span class="n">UIEdgeInsets</span><span class="p">)</span><span class="nv">hitTestEdgeInsets</span> <span class="p">{</span>
    <span class="n">NSValue</span><span class="o">*</span> <span class="n">value</span> <span class="o">=</span> <span class="p">[</span><span class="n">NSValue</span> <span class="nf">value</span><span class="p">:</span><span class="o">&amp;</span><span class="n">hitTestEdgeInsets</span> <span class="nf">withObjCType</span><span class="p">:</span><span class="k">@encode</span><span class="p">(</span><span class="n">UIEdgeInsets</span><span class="p">)];</span>
    <span class="n">objc_setAssociatedObject</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">hitTestEdgeInsets</span><span class="p">),</span> <span class="n">value</span><span class="p">,</span> <span class="n">OBJC_ASSOCIATION_RETAIN_NONATOMIC</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="n">BOOL</span><span class="p">)</span><span class="nf">yt_pointInside</span><span class="p">:(</span><span class="n">CGPoint</span><span class="p">)</span><span class="nv">point</span> <span class="nf">withEvent</span><span class="p">:(</span><span class="n">UIEvent</span><span class="o">*</span><span class="p">)</span><span class="nv">event</span> <span class="p">{</span>
    <span class="n">UIEdgeInsets</span> <span class="n">insets</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">hitTestEdgeInsets</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">UIEdgeInsetsEqualToEdgeInsets</span><span class="p">(</span><span class="n">insets</span><span class="p">,</span> <span class="n">UIEdgeInsetsZero</span><span class="p">))</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">[</span><span class="n">self</span> <span class="nf">yt_pointInside</span><span class="p">:</span><span class="n">point</span> <span class="nf">withEvent</span><span class="p">:</span><span class="n">event</span><span class="p">];</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">CGRect</span> <span class="n">hitBounds</span> <span class="o">=</span> <span class="n">UIEdgeInsetsInsetRect</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">bounds</span><span class="p">,</span> <span class="n">insets</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">CGRectContainsPoint</span><span class="p">(</span><span class="n">hitBounds</span><span class="p">,</span> <span class="n">point</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">@end</span>
</code></pre></div></div>

<p>其中在 <code class="language-plaintext highlighter-rouge">load</code> 方法里的 <code class="language-plaintext highlighter-rouge">YTSwizzleMethod</code> 是 <a href="http://nshipster.com/method-swizzling/">Method Swizzling</a> 的具体实现，因为这段代码经常被用到所以我把它抽了出来，以下是函数的内容：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">YTSwizzleMethod</span><span class="p">(</span><span class="n">Class</span> <span class="n">cls</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">originalSelector</span><span class="p">,</span> <span class="n">SEL</span> <span class="n">swizzledSelector</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">Method</span> <span class="n">originalMethod</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">originalSelector</span><span class="p">);</span>
    <span class="n">Method</span> <span class="n">swizzledMethod</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">swizzledSelector</span><span class="p">);</span>
    
    <span class="n">BOOL</span> <span class="n">didAddMethod</span> <span class="o">=</span> <span class="n">class_addMethod</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">originalSelector</span><span class="p">,</span> <span class="n">method_getImplementation</span><span class="p">(</span><span class="n">swizzledMethod</span><span class="p">),</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">swizzledMethod</span><span class="p">));</span>
    
    <span class="k">if</span> <span class="p">(</span><span class="n">didAddMethod</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">class_replaceMethod</span><span class="p">(</span><span class="n">cls</span><span class="p">,</span> <span class="n">swizzledSelector</span><span class="p">,</span> <span class="n">method_getImplementation</span><span class="p">(</span><span class="n">originalMethod</span><span class="p">),</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">originalMethod</span><span class="p">));</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">method_exchangeImplementations</span><span class="p">(</span><span class="n">originalMethod</span><span class="p">,</span> <span class="n">swizzledMethod</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>在工程中引入这个 Category 之后，我们对任意一个 UIButton 设置它的 hitTestEdgeInsets 属性，都可以将它的点击响应区域扩大或缩小。在本文的例子里，扩展 12 pt 差不多是个合适的值：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">_closeButton</span><span class="p">.</span><span class="n">hitTestEdgeInsets</span> <span class="o">=</span> <span class="n">UIEdgeInsetsMake</span><span class="p">(</span><span class="o">-</span><span class="mi">12</span><span class="p">,</span> <span class="o">-</span><span class="mi">12</span><span class="p">,</span> <span class="o">-</span><span class="mi">12</span><span class="p">,</span> <span class="o">-</span><span class="mi">12</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="0x03-uicollectionviewcell-的响应区域修正">0x03 UICollectionViewCell 的响应区域修正</h2>

<p><img src="/assets/images/2016/snapshot2.png" alt="snapshot" /></p>

<p>如图所示，按照上一段的步骤配置好以后，按钮的响应区域理应变成图中整块高亮的部分，但实际运行后真正的响应区域只有左下角的 A 部分。这是因为 UIKit 在检测点击响应区域时，首先询问的是父控件的 <code class="language-plaintext highlighter-rouge">pointInside:withEvent:</code> 方法，如果返回 <code class="language-plaintext highlighter-rouge">NO</code>，那么 UIKit 就认为点击区域在整个控件范围之外，不会继续遍历子控件，因此 Cell 的响应区域也需要跟随按钮一起扩展。除此之外我们还需要屏蔽掉 Cell 本身的事件响应，以防下一个 Cell 覆盖掉了上一个 Button 扩展后的热区 (图中 B 区域)。所以重写 UICollectionViewCell 的两个相关方法，内容如下：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">-</span> <span class="p">(</span><span class="n">UIView</span> <span class="o">*</span><span class="p">)</span><span class="nf">hitTest</span><span class="p">:(</span><span class="n">CGPoint</span><span class="p">)</span><span class="nv">point</span> <span class="nf">withEvent</span><span class="p">:(</span><span class="n">UIEvent</span> <span class="o">*</span><span class="p">)</span><span class="nv">event</span> <span class="p">{</span>
    <span class="n">CGPoint</span> <span class="n">pointInButton</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span> <span class="nf">convertPoint</span><span class="p">:</span><span class="n">point</span> <span class="nf">toView</span><span class="p">:</span><span class="n">_closeButton</span><span class="p">];</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">_closeButton</span> <span class="nf">pointInside</span><span class="p">:</span><span class="n">pointInButton</span> <span class="nf">withEvent</span><span class="p">:</span><span class="n">event</span><span class="p">]</span> <span class="p">?</span> <span class="n">_closeButton</span> <span class="p">:</span> <span class="nb">nil</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="n">BOOL</span><span class="p">)</span><span class="nf">pointInside</span><span class="p">:(</span><span class="n">CGPoint</span><span class="p">)</span><span class="nv">point</span> <span class="nf">withEvent</span><span class="p">:(</span><span class="n">UIEvent</span> <span class="o">*</span><span class="p">)</span><span class="nv">event</span> <span class="p">{</span>
    <span class="n">CGPoint</span> <span class="n">pointInButton</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span> <span class="nf">convertPoint</span><span class="p">:</span><span class="n">point</span> <span class="nf">toView</span><span class="p">:</span><span class="n">_closeButton</span><span class="p">];</span>
    <span class="k">return</span> <span class="p">[</span><span class="n">_closeButton</span> <span class="nf">pointInside</span><span class="p">:</span><span class="n">pointInButton</span> <span class="nf">withEvent</span><span class="p">:</span><span class="n">event</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[0x01 前言]]></summary></entry><entry><title type="html">使用 Objective-C Runtime 解决 unrecognized selector 错误</title><link href="https://blog.moecoder.com/fix-unrecognized-selector-error-using-runtime.html" rel="alternate" type="text/html" title="使用 Objective-C Runtime 解决 unrecognized selector 错误" /><published>2016-04-25T23:41:08+08:00</published><updated>2016-04-25T23:41:08+08:00</updated><id>https://blog.moecoder.com/fix-unrecognized-selector-error-using-runtime</id><content type="html" xml:base="https://blog.moecoder.com/fix-unrecognized-selector-error-using-runtime.html"><![CDATA[<h2 id="0x01-前言">0x01 前言</h2>
<p><code class="language-plaintext highlighter-rouge">NSOperation</code> 类有一个属性 <code class="language-plaintext highlighter-rouge">name</code>，用以标记一个 NSOperation 对象。苹果提供这个属性的本意是为了调试方便，但实际上通过它我们还可以简便地实现一些业务需求，比如加入 <code class="language-plaintext highlighter-rouge">NSOperationQueue</code> 前检查去重和排序什么的。</p>

<p>但很可惜，这个属性是 <code class="language-plaintext highlighter-rouge">NS_AVAILABLE(10_10, 8_0)</code> 的，换言之如果在 iOS 7 以下的系统上使用这个属性的话，控制台会打印这样一行错误：</p>

<blockquote>
  <p>unrecognized selector sent to instance</p>
</blockquote>

<p>如果项目需要兼容 iOS 7 系统的话，我们就需要寻找一种方法，在 iOS 7 上也能方便地标记一个 NSOperation 。</p>

<!-- more -->

<h2 id="0x02-associated-objects">0x02 Associated Objects</h2>
<p>扩展一个已有的 Objective-C 类一般有两种方法： <code class="language-plaintext highlighter-rouge">Subclass</code> 和 <code class="language-plaintext highlighter-rouge">Category</code> 。这里我们选择 Category ，这样可以在扩展 NSOperation 的同时也扩展 NSBlockOperation 等它的子类。</p>

<p>使用 OC Runtime 的两个函数 <code class="language-plaintext highlighter-rouge">objc_getAssociatedObject</code> 和 <code class="language-plaintext highlighter-rouge">objc_setAssociatedObject</code> ，可以方便地在 Category 中给一个类增加属性。代码大概像这样：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">-</span> <span class="p">(</span><span class="n">NSString</span><span class="o">*</span><span class="p">)</span><span class="n">xxx_name</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">objc_getAssociatedObject</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">_cmd</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">xxx_setName</span><span class="p">:(</span><span class="n">NSString</span><span class="o">*</span><span class="p">)</span><span class="nv">name</span> <span class="p">{</span>
    <span class="n">objc_setAssociatedObject</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">xxx_name</span><span class="p">),</span> <span class="n">name</span><span class="p">,</span> <span class="n">OBJC_ASSOCIATION_COPY_NONATOMIC</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>在头文件里声明 <code class="language-plaintext highlighter-rouge">xxx_name</code> 属性以后，在需要调用 <code class="language-plaintext highlighter-rouge">name</code> 的地方都改成 <code class="language-plaintext highlighter-rouge">xxx_name</code>，就可以完美兼容 iOS 7 以上的所有机型了。</p>

<h2 id="0x03-imp-sel-method">0x03 IMP, SEL, Method</h2>
<p>以上方法虽然实现了功能，但实际上我们抛弃了苹果提供的接口，这实在跟标题的<strong>优雅</strong>沾不上边。所以还需要继续使用 OC Runtime 的黑魔法，来尝试实现『安全地在低版本上调用高版本才有的API，同时完全不影响高版本API的功能』这个目的。</p>

<p>OC Runtime 有三个基础类型<code class="language-plaintext highlighter-rouge">IMP</code>，<code class="language-plaintext highlighter-rouge">SEL</code>和<code class="language-plaintext highlighter-rouge">Method</code>，它们的内容如下表：</p>

<table>
  <thead>
    <tr>
      <th>名词</th>
      <th>定义</th>
      <th>说明</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Selector</td>
      <td>typedef struct objc_selector *SEL</td>
      <td>表示一个 OC 对象方法的方法名</td>
    </tr>
    <tr>
      <td>Implementation</td>
      <td>typedef id (*IMP)(id, SEL, …)</td>
      <td>实际上是一个函数指针，指向了 Selector 对应的方法的具体实现</td>
    </tr>
    <tr>
      <td>Method</td>
      <td>typedef struct objc_method *Method</td>
      <td>封装了从 <code class="language-plaintext highlighter-rouge">Selector</code> 到 <code class="language-plaintext highlighter-rouge">Implementation</code> 的映射关系</td>
    </tr>
  </tbody>
</table>

<p>三者之间的联系，可以用<a href="http://nshipster.com/method-swizzling/">NShipster</a>的一段话来总结：</p>

<blockquote>
  <p>A class (Class) maintains a dispatch table to resolve messages sent at runtime; each entry in the table is a method (Method), which keys a particular name, the selector (SEL), to an implementation (IMP), which is a pointer to an underlying C function.</p>
</blockquote>

<p>在 NSOperation 的 Category 中，我们尝试调用与之相关的 OC Runtime 函数来实现以上目的：</p>

<div class="language-objectivec highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">+</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">load</span>
<span class="p">{</span>
    <span class="k">static</span> <span class="n">dispatch_once_t</span> <span class="n">onceToken</span><span class="p">;</span>
    <span class="n">dispatch_once</span><span class="p">(</span><span class="o">&amp;</span><span class="n">onceToken</span><span class="p">,</span> <span class="o">^</span><span class="p">{</span>
        <span class="n">Class</span> <span class="n">class</span> <span class="o">=</span> <span class="p">[</span><span class="n">self</span> <span class="nf">class</span><span class="p">];</span>
        
        <span class="n">SEL</span> <span class="n">nameSEL</span> <span class="o">=</span> <span class="k">@selector</span><span class="p">(</span><span class="n">name</span><span class="p">);</span>
        <span class="n">SEL</span> <span class="n">setNameSEL</span> <span class="o">=</span> <span class="k">@selector</span><span class="p">(</span><span class="n">setName</span><span class="o">:</span><span class="p">);</span>
        
        <span class="n">Method</span> <span class="n">nameMethod</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">nameSEL</span><span class="p">);</span>
        <span class="n">Method</span> <span class="n">setNameMethod</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">setNameSEL</span><span class="p">);</span>
        
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">nameMethod</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">SEL</span> <span class="n">xxxNameSEL</span> <span class="o">=</span> <span class="k">@selector</span><span class="p">(</span><span class="n">xxx_name</span><span class="p">);</span>
            <span class="n">Method</span> <span class="n">xxxNameMethod</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">xxxNameSEL</span><span class="p">);</span>
            <span class="n">class_addMethod</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">nameSEL</span><span class="p">,</span> <span class="n">method_getImplementation</span><span class="p">(</span><span class="n">xxxNameMethod</span><span class="p">),</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">xxxNameMethod</span><span class="p">));</span>
        <span class="p">}</span>
        
        <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">setNameMethod</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">SEL</span> <span class="n">xxxSetNameSEL</span> <span class="o">=</span> <span class="k">@selector</span><span class="p">(</span><span class="n">xxx_setName</span><span class="o">:</span><span class="p">);</span>
            <span class="n">Method</span> <span class="n">xxxSetNameMethod</span> <span class="o">=</span> <span class="n">class_getInstanceMethod</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">xxxSetNameSEL</span><span class="p">);</span>
            <span class="n">class_addMethod</span><span class="p">(</span><span class="n">class</span><span class="p">,</span> <span class="n">setNameSEL</span><span class="p">,</span> <span class="n">method_getImplementation</span><span class="p">(</span><span class="n">xxxSetNameMethod</span><span class="p">),</span> <span class="n">method_getTypeEncoding</span><span class="p">(</span><span class="n">xxxSetNameMethod</span><span class="p">));</span>
        <span class="p">}</span>
    <span class="p">});</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="n">NSString</span><span class="o">*</span><span class="p">)</span><span class="n">xxx_name</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">objc_getAssociatedObject</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">xxx_name</span><span class="p">));</span>
<span class="p">}</span>

<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">xxx_setName</span><span class="p">:(</span><span class="n">NSString</span><span class="o">*</span><span class="p">)</span><span class="nv">name</span>
<span class="p">{</span>
    <span class="n">objc_setAssociatedObject</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="k">@selector</span><span class="p">(</span><span class="n">xxx_name</span><span class="p">),</span> <span class="n">name</span><span class="p">,</span> <span class="n">OBJC_ASSOCIATION_COPY_NONATOMIC</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>在以上代码中，<code class="language-plaintext highlighter-rouge">+ (void)load</code> 函数调用的时候，我们通过运行时的操作，来实现以下两个步骤：</p>

<ol>
  <li>如果 NSOperation 本身有 name 属性，则什么也不做；</li>
  <li>如果 NSOperation 没有 name 属性，则在运行时动态添加 <code class="language-plaintext highlighter-rouge">name</code> 和 <code class="language-plaintext highlighter-rouge">setName:</code> 方法，使用我们自己的实现。</li>
</ol>

<p>把这个 Category 加入工程以后，我们就可以安全地在 iOS 7 以上使用 NSOperation 的 name 属性了，好像它原生支持了低版本的 iOS 一样。</p>

<h2 id="0x04-总结">0x04 总结</h2>
<p>本文演示了通过 OC Runtime 来优雅地为 <code class="language-plaintext highlighter-rouge">NSOperation</code> 的 <code class="language-plaintext highlighter-rouge">name</code> 属性增加了 iOS 7 以下的支持。实际上不止是 <code class="language-plaintext highlighter-rouge">NSOperation</code>，通过这个方法，很多高版本 iOS 新增的 API（比如 <code class="language-plaintext highlighter-rouge">[NSString containsString:]</code> 等）都可以用同样的方法移植到低版本系统上，只需要我们自己模拟实现相应的功能，然后通过 Category 提供给相应的 Selector 就可以了。</p>]]></content><author><name>Yang Chao</name></author><category term="笔记" /><category term="iOS" /><summary type="html"><![CDATA[0x01 前言 NSOperation 类有一个属性 name，用以标记一个 NSOperation 对象。苹果提供这个属性的本意是为了调试方便，但实际上通过它我们还可以简便地实现一些业务需求，比如加入 NSOperationQueue 前检查去重和排序什么的。]]></summary></entry></feed>