Jekyll2018-04-04T06:28:27+00:00https://danleechina.github.io/我的学习历程和大家一起分享、学习Ampire_Dansmile47777@gmail.comiOS 和 Android 开发的 React Native 入门指南2018-04-04T00:00:00+00:002018-04-04T00:00:00+00:00https://danleechina.github.io/A-Guide-of-React-Native-for-iOS-And-Android-developer<h2 id="前言">前言</h2>
<p>这一篇是给稍微有点原生(iOS 或者 Android)编程经验的人的一个系统性的 React Native 入门指南。主要总结的是我之前系统学习 React Native 的经验。</p>
<p>我在很早的时候就接触了 RN,但是刚开始那段时间基本处于一种瞎写的状态,不知道很多内在原理,导致我碰到问题各种谷歌、StackOverflow,搜代码,拷贝粘贴代码。然后又荒废了一段时间,今年开始又重拾 RN,有了之前的经验,这次是比较系统的学习了。</p>
<p>先介绍一下我的情况:我是原生 iOS 开发,Objective C 3年经验,Swift 2年经验。</p>
<h2 id="路线">路线</h2>
<p>基本路线如下:</p>
<ol>
<li>详细了解 JavaScript</li>
<li>详细了解 React</li>
<li>详细了解 React Native</li>
<li>其他(比如说 Redux)</li>
</ol>
<p>下面详细介绍。</p>
<h3 id="javascript">JavaScript</h3>
<p>关键是了解 JavaScript 的继承和原型链机制,同时包括 JavaScript 的对象模型。</p>
<p>参考资料如下:(参考资料如果包含浏览器相关的内容,可以忽略)</p>
<ol>
<li><a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript">MDN JavaScript</a></li>
<li><a href="http://es6.ruanyifeng.com/">ECMAScript 6 入门</a> 或者 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide">JavaScript 指南</a></li>
<li><a href="http://www.typescriptlang.org/docs/handbook/basic-types.html">TypeScript 英文文档</a>(<a href="https://www.tslang.cn/docs/handbook/basic-types.html">中文版文档</a>)</li>
</ol>
<p>先从 MDN 的 JavaScript 教程开始看,这是 JavaScript 的基础,有一定编程经验在初级部分可以看的快一点,但是关于 <a href="https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects">JavaScript 对象介绍</a>、<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain">继承与原型链</a>、<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Details_of_the_Object_Model">对象模型的细节</a> 要详细的看。</p>
<p>如果没有头绪,或者觉得跳着看思路跟不上,可以从头到尾细看(我就是这么看的,但是我看的比较快,花了2天时间看完)</p>
<p>看完 MDN 的教程以后就可以看 ES6 相关内容了。我看的是 ruanyifeng 的 ES6 网上教程,不过也可以看 MDN 的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide">JavaScript 指南</a>,两者可以对比的看。我是看完 ruanyifeng 的教程之后又看了 MDN 的 JavaScript 指南,由于看过 ruanyifeng 的 ES6,所以 MDN 的 JavaScript 指南就可以快一点。我觉得这里比较关键的是理解 ES6 的模块(Module),也就是这两章:<a href="http://es6.ruanyifeng.com/#docs/module">Module 的语法</a> 和 <a href="http://es6.ruanyifeng.com/#docs/module-loader">Module 的加载实现
</a>,还有就是基本的 ES6 语法。</p>
<p>在看的过程中有些内容可能不好懂,完全可以跳过,等后面写代码或者看代码碰到的时候再回过头来看,只要记住有这么一个东西即可。有些内容比较好懂,或者理解一下就懂了,这时候可以对比一下和其他语言在这方面有哪些不同和相同(比如你是 Java 程序员,学习 JavaScript,就可以对比一下 Java 的继承和 JavaScript 的继承),这样子可以加深你对 JavaScript 的理解。</p>
<p>看完这些其实 JavaScript 已经入门了,不过我要特别提一下 TypeScript。由于 JavaScript 是弱类型语言,经常代码写着写着就不知道当前函数的参数定义是什么。无论是 debug 还是阅读代码、写代码,不知道一个东西确定是什么是很痛苦的(你可以想象一下你写的一个函数,有一个参数,但是却无法确定类型是什么,所以需要时刻在脑海中告诉自己这个参数的定义是这样的,但是一不小心疏忽了,编辑器也没有提醒你这里写的有问题,那只能在运行时发生问题才知道出了问题,然后又要花一段时间去调试)。TypeScript 是微软出的一个比较流行的解决这类问题的方案,就是给 JavaScript 加上 Type (类型)。你也可以在一开始不用 TypeScript,但是等到你碰到很多上面提到的类似问题时,TypeScript 就是你的解决方案。</p>
<h3 id="react">React</h3>
<p>我刚开始学习 RN 的时候,对 React 一无所知,直接根据 React Native 的官方文档来学习。但是在越来越深入的写 React Native 相关代码的时候,我发现,了解 React 是很关键的。</p>
<p>对于喜欢立即上手的人来说,可以先不看 React 相关的内容,直接学习 RN。但是在你写了一定量的代码以后,你会碰到一些问题,比如如何复用组件,如何处理一些不是很常见的操作,如何处理数据(展示组件和容器组件)等等,这时候你就可以开始去了解 React 了。</p>
<p>了解 React 当然是官方文档:</p>
<ol>
<li><a href="https://doc.react-china.org/docs/hello-world.html">React 中文文档</a></li>
<li><a href="https://reactjs.org/docs/hello-world.html">React 英文官方文档</a></li>
</ol>
<p>上面两个内容是一样的,喜欢中文的可以看中文,喜欢看英文的可以看英文。</p>
<p>建议能够全部看完所有内容,不要忽略任何一个地方(如果是 Web 开发可以跳着看)。</p>
<p>当然其实看完这文档是不够的,我觉得有时间一定要理解一下 React 的实现原理。(我没有看过 React 源码,所以就不继续说了)</p>
<h3 id="react-native">React Native</h3>
<p>说了那么多终于到 RN 这一部分了。还是要强调一下,学习 RN 可以先不了解 JavaScript 和 React,(前提是你要有一点编程经验,如果是大神的话,请随意)喜欢立即上手的可以直接根据官方文档上手。不过要深入了解 RN,则必须要了解 React 和 JavaScript。</p>
<p>学习 RN 的时候主要就是看 <a href="https://facebook.github.io/react-native/docs/getting-started.html">官方英文文档</a>(<a href="https://reactnative.cn/docs/0.51/getting-started.html">中文版</a>),一些环境依赖等根据官方文档来操作即可。</p>
<p>RN 需要关键了解使用的部分如下:</p>
<ol>
<li>FlexBox,一个前端布局的方式,写界面的时候一定要了解这个,否则界面就写不出来</li>
<li>各种 UI 组件的使用、属性等,可以一边写代码一边学习使用</li>
<li>网络请求,大部分 app 都会有网络请求,所以了解这个也很关键。有一个框架 <a href="https://github.com/axios/axios">axios</a> 可以尝试使用,这是一个 HTTP 客户端框架。</li>
<li>如何与原生代码交互,iOS 相关文档地址 <a href="https://facebook.github.io/react-native/docs/native-modules-ios.html">iOS Native Modules</a>、<a href="https://facebook.github.io/react-native/docs/native-components-ios.html">iOS Native UI Components</a> ,Android 相关文档 <a href="https://facebook.github.io/react-native/docs/native-modules-android.html">Android Native Modules</a>、<a href="https://facebook.github.io/react-native/docs/native-components-android.html">Android Native UI Components</a></li>
<li>数据存储,使用 RealmJS 或者原生的 AsyncStorage,使用 RealmJS 的问题是,如果你的 iOS 原生项目已经使用了 RealmSwift 等的话,会导致符号冲突,无法通过编译。</li>
</ol>
<p>一些坑:</p>
<ol>
<li>建议在原生项目上添加 RN 支持,文档地址<a href="https://reactnative.cn/docs/0.51/integration-with-existing-apps.html#content">集成到现有原生应用</a>。如果使用 <code class="highlighter-rouge">create-react-native-app AwesomeProject</code> 命令来生成一个 RN 项目,会引入 Expo 这种东西,我个人不建议使用此类框架。</li>
<li>不建议使用 <code class="highlighter-rouge">NavigatorIOS</code> 这类组件,推荐 <a href="https://reactnavigation.org/">React Navigation</a>,但是这个开源组件坑也比较多,不过个人认为还是比 RN 提供的组件要好。</li>
</ol>
<h3 id="其他">其他</h3>
<p>Redux,Redux 是一个关于数据流的前端框架,也可以用在 RN 中。其他还有类似的比如 MobX(除此之外据说新版 React 有种 Context API 可以让我们告别使用 Redux,不太清楚,应该是比较新的东西)。</p>
<p>先说一下,一开始可以不使用 Redux,Redux 的官网也提示说如果不知道要不要使用 Redux,则不要使用 Redux。</p>
<p>学习自然要根据文档来:<a href="https://redux.js.org/">Redux 英文官方文档</a>(<a href="https://cn.redux.js.org/">中文版</a>)</p>
<p>理解 Redux 比较关键的是它的状态树,所有的操作最终会反应到状态树上,所有的操作也是围绕状态树展开来的。所以一开始做 app 的时候设计一个比较贴切的状态树很关键。</p>
<h3 id="进阶">进阶</h3>
<ol>
<li>打包</li>
<li>热更新</li>
<li>与原生交互</li>
<li>RN 原生实现原理</li>
</ol>
<p>参考资料有如下:</p>
<ol>
<li><a href="http://solart.cc/2017/02/22/react-native-jsbundle-patch/">React Native拆包及热更新方案</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/23715716">携程是如何做React Native优化的</a></li>
<li><a href="https://microsoft.github.io/code-push/">微软的热更新方案 CodePush</a></li>
<li><a href="https://github.com/reactnativecn/react-native-pushy">ReactNative中文网推出的代码热更新服务</a></li>
<li><a href="https://facebook.github.io/react-native/docs/native-modules-ios.html">iOS Native Modules</a></li>
<li><a href="https://facebook.github.io/react-native/docs/native-components-ios.html">iOS Native UI Components</a></li>
<li><a href="https://facebook.github.io/react-native/docs/native-modules-android.html">Android Native Modules</a></li>
<li><a href="https://facebook.github.io/react-native/docs/native-components-android.html">Android Native UI Components</a></li>
<li><a href="https://blog.cnbang.net/tech/2698/">React Native通信机制详解</a></li>
</ol>
<h2 id="开发环境">开发环境</h2>
<p>我使用 Visual Studio Code,不建议使用 Atom(很卡)。并且配置了 TypeScript 环境,配置参考 demo 地址: https://github.com/Microsoft/TypeScript-React-Native-Starter</p>
<h2 id="注意事项">注意事项</h2>
<p>个人觉得 RN 的要求其实很高,不仅要了解前端开发相关内容,还要了解 Android 和 iOS 的原生内容。一个纯 JavaScript 的 RN 项目不太现实。所以建议在原生项目上使用 RN,不要使用纯 RN 项目。这对于理解 RN 也有一定帮助。</p>
<p>不建议使用其他类似 RN 的解决方案,比如说 Weex,当然我并没有了解过 Weex。我想说的是 RN 的生态环境是很活跃的,GitHub 有很多开发者共享的开源代码,很多人也在 GitHub 和 stackoverflow 上讨论自己碰到的 RN 相关的问题。但是即使如此,RN 还是没有出 1.0 版本,而且在实际使用过程中经常要自己造轮子,也会碰到很多奇奇怪怪的问题,导致拖累开发速度,从学习角度不是问题,但是工作就不一样了。所以使用 Weex 情况可能更糟糕。</p>
<h2 id="总结">总结</h2>
<p>学习 React Native 的过程其实更像是入门 React 前端开发,区别就是代码环境从浏览器变成了移动设备。</p>
<p>在原生移动开发势微的今天,从 React Native 切入来了解前端开发也很不错。在学习的同时,结合自己已有的原生开发经验,对比 React Native 开发,更能在巩固 React Native 相关知识的同时对加深原生开发的理解。</p>Ampire_Dansmile47777@gmail.com前言CocoaAsyncSocket 实现时用到的技术2018-03-29T00:00:00+00:002018-03-29T00:00:00+00:00https://danleechina.github.io/The-tech-used-in-CocoaAsyncSocket<h2 id="前言">前言</h2>
<p>最近在阅读 <a href="https://github.com/robbiehanson/CocoaAsyncSocket">CocoaAsyncSocket</a> 的源码,整理里一下其中用到的一些技术点。</p>
<h2 id="gcd-相关">GCD 相关</h2>
<h3 id="目标队列target-queue">目标队列(Target Queue)</h3>
<h4 id="概念">概念</h4>
<p>目标队列的基本概念是:你创建的所有队列,如果没有指定其目标队列,那么它的目标队列是优先级为 <code class="highlighter-rouge">DISPATCH_QUEUE_PRIORITY_DEFAULT</code> 的全局并发队列。每次在你队列中的一个 block 开始执行的时候,GCD 会重新将其放入目标队列执行。详细介绍可以查看引用1。</p>
<h4 id="目标队列可能引起死锁">目标队列可能引起死锁</h4>
<p>这里说一下使用目标队列可能导致的死锁问题:</p>
<p>比如说有两个队列:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dispatch_queue_t queueOne;
dispatch_queue_t targetQueueOne;
queueOne.targetQueue = targetQueueOne;
</code></pre></div></div>
<p>那么所有在 queue 上的操作最终会在 targetQueue 上执行。一切看起来都很好,现在有设想下面这样的函数:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> - (BOOL)doSomething
{
__block BOOL result = NO;
dispatch_block_t block = ^{
result = [self someInternalMethodToBeRunOnlyOnQueueOne];
}
if (is_executing_on_queue(queueOne))
block();
else
dispatch_sync(queueOne, block);
return result;
}
</code></pre></div></div>
<p>如果你在 targetQueueOne 上调用这个方法会怎样?答案是死锁。这是因为 GCD 的 API 没有提供一个机制去发现队列的目标队列,所以我们不知道 queueOne 的目标队列。(死锁的原因是在当前串型队列上又同步派发了一个操作,导致两者相互等待)</p>
<h4 id="目标队列死锁问题解决">目标队列死锁问题解决</h4>
<p>一句话解释:使用 <code class="highlighter-rouge">dispatch_queue_set_specific()</code> 和 <code class="highlighter-rouge">dispatch_get_specific()</code>。</p>
<p>先给 queueOne 设置 sepcific</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IsOnQueueOneOrTargetQueueKey = &IsOnQueueOneOrTargetQueueKey;
void *nonNullUnusedPointer = (__bridge void *)self;
dispatch_queue_set_specific(queue, IsOnQueueOneOrTargetQueueKey, nonNullUnusedPointer, NULL);
</code></pre></div></div>
<p>每次要判断是否是在当前队列或者目标队列的时候如下判断:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (dispatch_get_specific(IsOnQueueOneOrTargetQueueKey)) {
block();
} else {
dispatch_sync(queueOne, block);
}
</code></pre></div></div>
<h3 id="gcd-dispatch-源">GCD Dispatch 源</h3>
<h4 id="概念-1">概念</h4>
<p>当要处理系统底层相关任务的时候,我们必须准备好要等待一段时间。当你的应用程序陷入系统内核或者其他系统层面的时候,比起在进程内部的函数调用,我们要面临切换上下文导致的巨大时间开销。结果就是,许多系统库会提供异步接口使你的代码提交一个请求给系统,然后继续自己的工作,当系统完成你的请求,在回调你的请求结果处理 block。GCD 的 dispatch source 就是这样一个基本数据类型。具体介绍可以查看官方文档,引用2。</p>
<h4 id="使用-dispatch-源来做一个定时器">使用 Dispatch 源来做一个定时器</h4>
<p>在 iOS 中想要实现一个定时器可以使用系统原生的类 <code class="highlighter-rouge">NSTimer</code>,<code class="highlighter-rouge">NSTimer</code> 的问题是它的设计模式是 target-action 模式。也就是要在当前类中定义一个方法给定时器使用。比起多定义一个方法我们更喜欢用 block。但是 <code class="highlighter-rouge">NSTimer</code> 的 API 在 iOS 11 才支持回调的方式。有很多方式来自定义 <code class="highlighter-rouge">NSTimer</code> 使用回调的方式。这里不做介绍,可以查看引用3。</p>
<p>实现一个回调的定时器的一种方式是使用 Dispatch 源,代码如下所示:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
dispatch_source_t CreateDispatchTimer(uint64_t interval,
uint64_t leeway,
dispatch_queue_t queue,
dispatch_block_t block)
{
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, queue);
if (timer)
{
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer;
}
void MyCreateTimer()
{
dispatch_source_t aTimer = CreateDispatchTimer(30ull * NSEC_PER_SEC,
1ull * NSEC_PER_SEC,
dispatch_get_main_queue(),
^{ MyPeriodicTask(); });
// Store it somewhere for later use.
if (aTimer)
{
MyStoreTimer(aTimer);
}
}
</code></pre></div></div>
<h4 id="使用-dispatch-源来处理-socket-相关操作">使用 Dispatch 源来处理 Socket 相关操作</h4>
<p>无论是服务器端还是客户端socket 都需要某种机制使得其能够和对方进行持续的通信。通常的解决办法是进行一个永久循环,遇到某种条件时再终止循环。这种方式缺陷较多,一是不够“优雅”,二是无限循环非常浪费 CPU 时间。使用 Dispatch 源来做处理会更好。</p>
<p>CocoaAsyncSocket 这个框架的核心思想之一就是使用源来处理 Socket 的读和写。下面代码来自 CocoaAsyncSocket:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue);
int socketFD = socket4FD;
dispatch_source_t acceptSource = accept4Source;
__weak GCDAsyncSocket *weakSelf = self;
dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool {
__strong GCDAsyncSocket *strongSelf = weakSelf;
if (strongSelf == nil) return_from_block;
unsigned long i = 0;
unsigned long numPendingConnections = dispatch_source_get_data(acceptSource);
while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections));
}});
dispatch_source_set_cancel_handler(accept4Source, ^{
close(socketFD);
});
dispatch_resume(accept4Source);
</code></pre></div></div>
<p>使用 <code class="highlighter-rouge">dispatch_source_create</code> 来创建源,参数 <code class="highlighter-rouge">DISPATCH_SOURCE_TYPE_READ</code> 指的是这是一个读类型的源,读事件来自与 socket4FD,事件回调发生在 socketQueue 上。</p>
<p><code class="highlighter-rouge">dispatch_source_set_event_handler</code> 指定了每次事件发生时的回调,
<code class="highlighter-rouge">dispatch_source_set_cancel_handler</code> 指定了取消一个源的操作,取消一个源可以使用 <code class="highlighter-rouge">dispatch_source_cancel</code> 完成。默认情况下源是不会开始执行的,所以要用 <code class="highlighter-rouge">dispatch_resume</code> 显示的启动一个源。</p>
<p>dispatch 源还有很多作用,比如监视一个文件夹中的文件变化等。详细介绍可以看官方文档或者引用4</p>
<h3 id="ssltls-握手">SSL/TLS 握手</h3>
<p>我们知道每一个 TCP 连接都会进行3次握手,然后才会开始通信,具体原理可以查看引用5。HTTP 连接是建立在 TCP 连接的基础之上的。HTTPS 则更进一步,其连接是建立在 SSL/TLS 连接之上的。而 SSL/TLS 连接又是建立在 TCP 3次握手之后。这里不描述 SSL/TLS 的握手过程,详细可以查看参考资料6。</p>
<p>如何在 iOS 或者 macOS 系统上实现 SSL/TLS 握手呢?先说一下为什么要自己实现 SSL/TLS 握手,毕竟我们使用 HTTPS 的时候好像并没有处理这个?</p>
<p>了解客户端证书绑定的会知道,要做客户端证书绑定我们需要实现 NSURLSessionDelegate 的 <code class="highlighter-rouge">URLSession:didReceiveChallenge:completionHandler</code> 协议。其实在这一步发生的时候,NSURLSession 就已经正在为我们进行 SSL/TLS 握手,只是由于我们实现了这个协议,所以 NSURLSession 需要我们告诉它是否要信任这次握手,默认情况下 NSURLSession 有自己的逻辑来决定是否信任。NSURLSession 为我们隐藏了很多其他握手细节。</p>
<p>当我们想自定义 SSL/TLS 的握手细节的时候就需要自定义了。(SSL/TLS 握手中每一个步骤请看资料6)。</p>
<p>iOS 这边实现 SSL/TLS 握手 有两种方式</p>
<ol>
<li>使用苹果提供的 SecureTransport 框架</li>
<li>使用 CFStream 相关接口</li>
</ol>
<p>使用 SecureTransport 的优势是可以结合 dispatch 源,并且性能高,可控性强,但是 SecureTransport 没有开源。</p>
<p>使用 CFStream 的优势是使用 dispatch source 苹果没有一种机制告诉我们当前是否要进入后台或者正在后台。CFStream 的 API 就可以,通过设置 steam 的 kCFStreamNetworkServiceType 为 kCFStreamNetworkServiceTypeVoIP 即可。</p>
<p>注意 CocoaAsyncSocket 中貌似无法用 CFSteam 来自定义证书认证。</p>
<p>具体如何使用 CFStream 请看资料 <a href="https://developer.apple.com/library/content/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECCFSOCKETSTREAM">CFSocketStream</a></p>
<p>具体如何使用 Secure Transport 请看资料 <a href="https://developer.apple.com/library/content/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECSECURETRANSPORT">Secure Transport</a></p>
<h3 id="socket">SOCKET</h3>
<p>CocoaAsyncSocket 本来就是对原生的 Socket 的封装,所以要看懂源码需要了解原生的 Socket 的写法。主要分成两个部分:</p>
<ol>
<li>服务器端 Socket</li>
<li>客户端 Socket</li>
</ol>
<p>同时 Socket 本身还有很多细节内容需要了解。建议在阅读或者使用 CocoaAsyncSocket 之前了解这方面的知识。</p>
<p>英文资料可以看: <a href="http://www.csd.uoc.gr/~hy556/material/tutorials/cs556-3rd-tutorial.pdf">Introduction to Sockets Programming in C using TCP/IP</a></p>
<p>中文资料可以看: <a href="https://github.com/xuelangZF/CS_Offer/blob/master/Network/Socket.md">Socket</a></p>
<h2 id="引用">引用</h2>
<ol>
<li><a href="https://www.jianshu.com/p/bd2609cac26b">【翻译】GCD Target Queues</a></li>
<li><a href="https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/GCDWorkQueues/GCDWorkQueues.html">Dispatch Sources 官方文档</a></li>
<li><a href="https://github.com/jivadevoe/NSTimer-Blocks">NSTimer-Blocks</a></li>
<li><a href="https://github.com/ming1016/study/wiki/%E7%BB%86%E8%AF%B4GCD%EF%BC%88Grand-Central-Dispatch%EF%BC%89%E5%A6%82%E4%BD%95%E7%94%A8">细说GCD(Grand Central Dispatch)如何用</a></li>
<li><a href="https://github.com/jawil/blog/issues/14">通俗大白话来理解TCP协议的三次握手和四次分手</a></li>
<li><a href="http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html">SSL/TLS协议运行机制的概述</a></li>
</ol>Ampire_Dansmile47777@gmail.com前言JSPatch 和 React Native 的区别2018-03-10T00:00:00+00:002018-03-10T00:00:00+00:00https://danleechina.github.io/Difference-between-JSPatch-and-React-Native<h2 id="前言">前言</h2>
<p>去年的3月份,很多苹果开发者可能都收到了苹果对于热更新的警告邮件。收到邮件的很可能代码中使用了类似 JSPatch 这样的热修复框架。</p>
<p>同时很多没有使用 JSPatch 等类似框架的开发者可能也收到了这份警告邮件,这很可能是苹果的代码检测机制的误伤。不过这也引起了很多使用 React Native 这类框架的开发者的担忧,是不是说以后也不能使用 React Native 这类框架了?</p>
<p>答案当然是可以继续使用 React Native 了。我这里会详细说明一下为什么,主要从两者的原理和能力来阐述。</p>
<h2 id="jspatch-的原理">JSPatch 的原理</h2>
<p>JSPatch 的代码非常精炼。核心文件就两个,一个是 <code class="highlighter-rouge">JSPatch.js</code>,另外一个是 <code class="highlighter-rouge">JSEngine.m</code>。实现主要依赖两个:</p>
<ol>
<li>Objective C 语言的动态属性</li>
<li>JavaScriptCore 通过 JS 调用 Native 的能力</li>
</ol>
<p>JSPatch 的核心文件也分别对应了这两者。<code class="highlighter-rouge">JSEngine.m</code> 主要是定义一套原生接口给 <code class="highlighter-rouge">JSPatch.js</code> 使用,这套原生接口使用了 OBjective C 语言的动态能力。<code class="highlighter-rouge">JSPatch.js</code> 则是定义了一套 JS 接口给开发者使用,开发者可以基于这套接口写热更新代码。<code class="highlighter-rouge">JSPatch.js</code> 定义的这套接口相当于一层 DSL。JSPatch 的 GitHub 上的文档详细介绍了这套 <a href="https://github.com/bang590/JSPatch/wiki/JSPatch-%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95">DSL JSPatch-基础用法</a></p>
<p>结合两者使得 JSPatch 的能力十分强大,可以通过 JS 任意调用原生 API。</p>
<h2 id="react-native-的能力">React Native 的能力</h2>
<p>React Native 是一个非常庞大的项目,我这边暂时还没有去详细阅读过源码。我主要讲一下 React Native 是如何和原生交互的。关于更详细的描述可能查看 <a href="http://facebook.github.io/react-native/docs/native-modules-ios.html">官方文档</a></p>
<p>React Native 主要有两种与原生代码交互的途径。官方文档将其描述成 <code class="highlighter-rouge">Native Modules</code> 和 <code class="highlighter-rouge">Native UI Components</code>。</p>
<ol>
<li><code class="highlighter-rouge">Native Modules</code>,说明如何将原生的代码(与 UI 无关)暴露给 RN 的 JS 代码</li>
<li><code class="highlighter-rouge">Native UI Components</code>,说明如何将原生的 UI 组件暴露给 RN 的 JS 代码</li>
</ol>
<p>注意,这两者都需要写原生代码,同时重新打包运行,这样暴露出来接口才能被 JS 代码调用执行。也就是说这里没有热更新。</p>
<p>与原生交互,我们比较关注</p>
<ol>
<li>JS 如何调用原生代码并取得返回值</li>
<li>原生代码如何调用 JS 代码并获得返回值</li>
</ol>
<p>对于前者,官方文档已经详细的阐述了。主要是 Callback、Promises。</p>
<p>对于后者,官方文档中说的并不详细,需要将文档中的手段结合起来使用。总的来说比较复杂。</p>
<p>总结一下 RN,本质上来说 RN 是自成一体的,我们只能在 RN 的能力范围内具备热更新能力。RN 的能力主要来自两方面:</p>
<ol>
<li>RN 的原生代码库,这套代码库使得我们可以基于此编写 JS 的 UI,进行网络请求以及其他一些原生服务,比如地理位置服务、推送等。这些方面都能进行热更新。RN 的官方文档也几乎全是关于这方面的内容</li>
<li>我们自己编写的并暴露给 RN 的原生代码,可以基于此编写相应地 JS 代码,RN 的官方文档详细的描述了这一方面的规范。在我们自己的 RN 原生模块范围内能够进行热更新</li>
</ol>
<h2 id="总结">总结</h2>
<p>通过上面的描述,我们知道了:</p>
<ol>
<li>JSPatch 很强大,可以通过 JS 任意调用原生 API。所以 JSPatch 的热更新能力是全面的。</li>
<li>React Native 也很强大,但是其强大是<strong>克制</strong>的。其提供的热更新能力基本上只是写界面,编写一些通常的业务逻辑。</li>
</ol>
<h2 id="参考资料">参考资料</h2>
<ol>
<li><a href="https://github.com/bang590/JSPatch/issues/746">JSPatch 关于苹果热更新警告邮件的 issue</a></li>
<li><a href="https://github.com/facebook/react-native/issues/13011">React Native 关于苹果热更新警告邮件的 issue</a></li>
<li><a href="http://blog.cnbang.net/internet/3374/">关于苹果警告 JSPatch 作者回复</a></li>
<li><a href="https://jspatch.com/Docs/appleFAQ">接入 JSPatch FAQ</a></li>
<li><a href="http://blog.cnbang.net/tech/2808/">JSPatch实现原理详解</a></li>
<li><a href="http://blog.cnbang.net/tech/3237/">iOS 动态更新方案对比:JSPatch vs React Native</a></li>
<li><a href="http://blog.cnbang.net/tech/2698/">React Native通信机制详解</a></li>
<li><a href="http://facebook.github.io/react-native/docs/native-modules-ios.html">Native Modules</a></li>
</ol>Ampire_Dansmile47777@gmail.com前言实现 UITableView 以及思考2017-12-29T00:00:00+00:002017-12-29T00:00:00+00:00https://danleechina.github.io/implement-UITableView-and-think<h2 id="前言">前言</h2>
<p>一年前因为 <code class="highlighter-rouge">UITableView</code> 无法满足需求,我实现了类似 <code class="highlighter-rouge">UITableView</code> 的组件, <code class="highlighter-rouge">DLTableView</code>。</p>
<p>之所以实现一个自定义的 <code class="highlighter-rouge">UITableView</code>,是因为我需要一个能无限循环滚动的 TableView。</p>
<p>通常的做法是设置 dataSource 的 <code class="highlighter-rouge">numberOfRowsInSection:</code> 方法返回尽量多的行数,然后在对 row 取余以实现看起来是无限循环滚动的特效。<code class="highlighter-rouge">UIDatePicker</code> 就是这样子实现的。</p>
<p>这种方案有个问题,如果用户一直往下滚动,还是能够滚动到底的,所以行数要设置的尽量大。另一方面太大的行数会导致内存使用暴涨。 <code class="highlighter-rouge">UIDatePicker</code> 的做法是设置一个合理的行数,在用户停止滚动的时候去校正 <code class="highlighter-rouge">contentOffset</code>,由于没有动画效果,用户无法感知,在内存和效果之间取得了平衡。但是连续往下滚的话,你会发现还是能够滚到底部,等你重新进来的时候才会重置一下 <code class="highlighter-rouge">contentOffset</code>。所以这个方案并不完美。</p>
<p>因为诸多原因最终我决定实现一个 <code class="highlighter-rouge">UITableView</code>。好处有几个:</p>
<ol>
<li>从实践中思考苹果是如何实现的,有什么难点,它的 <code class="highlighter-rouge">UITabeleView</code> 有什么可以改进之处。这些不是单纯的看几篇博客或者直接看源码就可以获得的</li>
<li>实现一个基本的 TableView 之后,可以添加诸多原生 TableView 不具备的功能,比如说循环滚动特性。项目实践可以更加灵活</li>
<li>可以基于新的自定义的 TableView 实现一个比官方 <code class="highlighter-rouge">UIPickerView</code>/<code class="highlighter-rouge">UIDatePicker</code> 更好的 <code class="highlighter-rouge">DLPickerView</code></li>
</ol>
<p>这里先预告一下,下一篇我会讲解如何基于 <code class="highlighter-rouge">DLTableView</code> 实现 <code class="highlighter-rouge">DLPickerView</code>。这个 pickerView 有一下特性:</p>
<ol>
<li>类似 <code class="highlighter-rouge">UITableView</code> 的 delegate 和 dataSource,同时可以自定义 cell,用法完全类似 <code class="highlighter-rouge">UITableView</code>,灵活性更高。</li>
<li>可以设置连续的可选区域,用户不能滚动到可选区域之外</li>
<li>可以配置循环滚动或者不循环滚动</li>
<li><code class="highlighter-rouge">DLPickerViewDelegate</code> 提供对每个 cell 基于当前位置的视图自定义</li>
</ol>
<p><a href="https://github.com/danleechina/DLTableView"><code class="highlighter-rouge">DLTableView</code></a> 的 GitHub 地址是 https://github.com/danleechina/DLTableView。</p>
<p>下面开始讲如何实现一个自定义的 TableView。</p>
<h2 id="关键点">关键点</h2>
<p>自定义 TableView 的实现有两个关键点:</p>
<ol>
<li>Cell 复用</li>
<li>找到实现的切入点</li>
</ol>
<p>Cell 复用比较好理解,因为内存是有限的,不可能无限多的生成 Cell 实例。</p>
<p>Cell 复用的实现也比较简单,给每种 Cell 类设置一个 identifier,以此为键,值为一个包含该 Cell 类的 set 集合。Cell 滚出可视域时候加到 set 里面,Cell 出现在可视域时从 set 里面获取一个,如果 set 里面没有的话则生成一个。</p>
<p>那么切入点呢?</p>
<p>首先,我们的自定义 <code class="highlighter-rouge">DLTableView</code> 是基于 <code class="highlighter-rouge">UIScrollView</code> 的,<code class="highlighter-rouge">UIScrollView</code> 提供了很好的滚动效果,虽然在使用的时候发现还是有点不太满意的地方,比如以动画的方式设置 <code class="highlighter-rouge">contentOffset</code> 时没法做到设置一个完成动画的回调,还有就是比如说对滚动的阻力做调整。</p>
<p>现在请思考一个问题,在什么时间给 <code class="highlighter-rouge">DLTableView</code> 添加一个 Cell,也就是说将 Cell 作为 TableView 的子视图或者子孙视图?以及当 Cell 从用户视线中消失时将 Cell 从 <code class="highlighter-rouge">DLTableView</code> 中移除,并加入到复用的队列中?</p>
<p>首先想到的是 <code class="highlighter-rouge">scrollViewDidScroll:</code>,在 <code class="highlighter-rouge">UIScrollView</code> 滚动的时候添加 Cell。但是 <code class="highlighter-rouge">scrollViewDidScroll</code> 是代理实现的,作为继承自 <code class="highlighter-rouge">UIScrollView</code> 的 <code class="highlighter-rouge">DLTableView</code> 本身不应该实现这个代理。而且在 <code class="highlighter-rouge">contentSize</code> 未知的时候 <code class="highlighter-rouge">UIScrollView</code> 可能根本不能滚动。</p>
<p>当然我们可以 hook 掉 <code class="highlighter-rouge">UIScrollView</code> 的 <code class="highlighter-rouge">setDelegate:</code> 方法,然后在自定义 TableView 里面实现所有 <code class="highlighter-rouge">UIScrollViewDelegate</code> 的方法,在这些方法里面再回调给使用自定义 TableView 的 delegate。天猫的 <a href="https://github.com/alibaba/LazyScrollView">LazyScrollView</a> 就是这样实现的 (不过它只复写了 <code class="highlighter-rouge">scrollViewDidScroll:</code> 方法,其他方法是通过动态转发来实现的)。这种方案的缺点是要手动设置 <code class="highlighter-rouge">contentSize</code>。</p>
<p>我这边的实现是使用 <code class="highlighter-rouge">layoutSubviews</code>。每次初始化的时候,UIView 都会调用该方法,在这个方法里面我们可以初始化最初的可见 Cell,以及 <code class="highlighter-rouge">contentSize</code>。灵感来自与苹果的官方 demo:<a href="https://developer.apple.com/library/content/samplecode/StreetScroller/Introduction/Intro.html">StreetScroller</a></p>
<p>另外,当 <code class="highlighter-rouge">UIScrollView</code> 滚动的时候,会频繁的调用该方法。这样我们就可以动态的将 Cell 加入或去掉。</p>
<blockquote>
<p>这里补充一点,有时候 <code class="highlighter-rouge">scrollViewDidScroll:</code> 的调用频率没我们想的那么多的时候,你也可以实现 <code class="highlighter-rouge">layoutSubviews</code>,<code class="highlighter-rouge">layoutSubviews</code> 的频率比 <code class="highlighter-rouge">scrollViewDidScroll:</code> 高,当然不要忘记调用 <code class="highlighter-rouge">[super layoutSubviews]</code></p>
</blockquote>
<h2 id="实现细节">实现细节</h2>
<ol>
<li>TableView 可能会有 header 或者 footer,所以在计算高度的时候每个 cell 的位置要加上 header 高度的偏移,计算 <code class="highlighter-rouge">contentSize</code> 的高度的时候要加上 footer 的高度。</li>
<li>TableView 可以不只有 row 还可以进一步区分每个 section,关键一点是每个 section 也可以有 header 和 footer,所以计算 cell 的位置的时候会有那么一点不直观,同时 section 的 header 和 footer 在 TableView 中的位置是滚动的,到顶的时候又是漂浮在 row 上面,这一点要注意。</li>
<li>UITableView 支持将某个 Cell 滚动到顶部,自定义的时候可以考虑将 Cell 滚到顶、中、底。</li>
<li>点击。一般不会在每个 Cell 里面加一个 tap gesture recognizer。我的做法是在 TableView 层面加一个 Tap gesture recognizer,这样每次识别到点击的时候,可以具体分配到某个 cell。当然还有设置点击效果(比如 cell 点击变灰)。</li>
</ol>
<h2 id="dltableview">DLTableView</h2>
<p>我这边实现了一个自定义 TabelView,<code class="highlighter-rouge">DLTableView</code>。</p>
<p>目前 <code class="highlighter-rouge">DLTableView</code> 实现的特性有:</p>
<ol>
<li>类似 <code class="highlighter-rouge">UITableViewDataSource</code> 的 <code class="highlighter-rouge">DLTableViewDataSource</code>,使用者可以实现 dataSource 来自定义行数和每一行的 cell</li>
<li>类似 <code class="highlighter-rouge">UITableViewDelegate</code> 的 <code class="highlighter-rouge">DLTableViewDelegate</code>,目前包含点击逻辑、自定义高度和某个 Cell 滚出可视区域时候的回调</li>
<li>可以循环滚动,同时内存使用不会出现暴涨(比如 <code class="highlighter-rouge">UITableView</code> 就会暴涨)</li>
<li>点击某一行可以滚动到顶、中、底</li>
</ol>
<p>你可以将 <code class="highlighter-rouge">DLTableView</code> 看做是一个简化版的 <code class="highlighter-rouge">UITableView</code>,它没有 section,只有 row,也没有实现每个行的分割线,更没有实现 <code class="highlighter-rouge">UITableView</code> 里面的左滑 cell 自定义菜单这些功能,而且目前还不支持自动计算行高。</p>
<p>不过由于 <code class="highlighter-rouge">DLTableView</code> 的实现是开源可见的,所以你可以基于此做进一步的自定义。</p>
<h2 id="更好的-tableview">更好的 TableView</h2>
<p>上面说了那么多,都是在讲如何实现一个类似 <code class="highlighter-rouge">UITableView</code> 的 TableView,主要是山寨。</p>
<p>那么如何基于山寨版的 TableView,提供一个更好的 TableView ?</p>
<p>首先什么叫做更好的 TableView ?我整理了如下:</p>
<ol>
<li>自动计算以及缓存 Cell 高度,市面上有很多相关开源代码,你可以查看。</li>
<li>异步构建视图,以及加载数据,参考 AsyncDisplayKit(Texture) 的思想。</li>
</ol>
<h2 id="引用">引用</h2>
<ol>
<li><a href="https://developer.apple.com/library/content/samplecode/StreetScroller/Introduction/Intro.html">StreetScroller</a></li>
<li><a href="http://pingguohe.net/2017/12/21/iOS-LazyScroll-implemention.html">iOS 异构滚动视图 LazyScrollView 一些实现细节的深入解读</a></li>
<li><a href="https://github.com/alibaba/LazyScrollView">LazyScrollView</a></li>
</ol>Ampire_Dansmile47777@gmail.com前言Swift 中类似 Java 的注解:Attribute2017-12-07T00:00:00+00:002017-12-07T00:00:00+00:00https://danleechina.github.io/Swift%E2%80%99s-annotation-like-Java-Attribute<h2 id="前言">前言</h2>
<p>本文将描述 Java 中使用注解(<code class="highlighter-rouge">annotation</code>)的优势及原理(但是不会介绍 Java 注解的使用和自定义,你可以网上搜索相关资料),以及类似 Java 注解的 Swift Attribute,同时还会思考如果 Swift 支持自定义 Attribute 会有什么好处。</p>
<h2 id="java-使用注解的优势">Java 使用注解的优势</h2>
<p>举例来说,Java 的 Web 框架 <code class="highlighter-rouge">SpringMVC</code>,这个框架的核心思想之一是使用 Java 的注解来简化 Web 工程配置。只需很少的代码就可以构造一个 Restful API。如下所示(代码引用来自 <a href="https://spring.io/guides/gs/rest-service/">Building a RESTful Web Service</a> 官方 demo):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">hello</span><span class="p">;</span>
<span class="n">import</span> <span class="n">java</span><span class="p">.</span><span class="n">util</span><span class="p">.</span><span class="n">concurrent</span><span class="p">.</span><span class="n">atomic</span><span class="p">.</span><span class="n">AtomicLong</span><span class="p">;</span>
<span class="n">import</span> <span class="n">org</span><span class="p">.</span><span class="n">springframework</span><span class="p">.</span><span class="n">web</span><span class="p">.</span><span class="n">bind</span><span class="p">.</span><span class="n">annotation</span><span class="p">.</span><span class="n">RequestMapping</span><span class="p">;</span>
<span class="n">import</span> <span class="n">org</span><span class="p">.</span><span class="n">springframework</span><span class="p">.</span><span class="n">web</span><span class="p">.</span><span class="n">bind</span><span class="p">.</span><span class="n">annotation</span><span class="p">.</span><span class="n">RequestParam</span><span class="p">;</span>
<span class="n">import</span> <span class="n">org</span><span class="p">.</span><span class="n">springframework</span><span class="p">.</span><span class="n">web</span><span class="p">.</span><span class="n">bind</span><span class="p">.</span><span class="n">annotation</span><span class="p">.</span><span class="n">RestController</span><span class="p">;</span>
<span class="p">@</span><span class="n">RestController</span>
<span class="k">public</span> <span class="n">class</span> <span class="n">GreetingController</span> <span class="p">{</span>
<span class="n">private</span> <span class="n">static</span> <span class="n">final</span> <span class="k">String</span> <span class="n">template</span> <span class="p">=</span> <span class="s2">"Hello, %s!"</span><span class="p">;</span>
<span class="n">private</span> <span class="n">final</span> <span class="n">AtomicLong</span> <span class="k">counter</span> <span class="p">=</span> <span class="n">new</span> <span class="n">AtomicLong</span><span class="p">();</span>
<span class="p">@</span><span class="n">RequestMapping</span><span class="p">(</span><span class="s2">"/greeting"</span><span class="p">)</span>
<span class="k">public</span> <span class="n">Greeting</span> <span class="n">greeting</span><span class="p">(@</span><span class="n">RequestParam</span><span class="p">(</span><span class="n">value</span><span class="p">=</span><span class="s2">"name"</span><span class="p">,</span> <span class="n">defaultValue</span><span class="p">=</span><span class="s2">"World"</span><span class="p">)</span> <span class="k">String</span> <span class="n">name</span><span class="p">)</span> <span class="p">{</span>
<span class="n">return</span> <span class="n">new</span> <span class="n">Greeting</span><span class="p">(</span><span class="k">counter</span><span class="p">.</span><span class="n">incrementAndGet</span><span class="p">(),</span>
<span class="k">String</span><span class="p">.</span><span class="n">format</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">name</span><span class="p">));</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>这里简单的几句代码,就完成了一个 <code class="highlighter-rouge">/greeting</code> API 的所有编码及配置(当然,这里忽然了 Model 类)。</p>
<p><code class="highlighter-rouge">SpringMVC</code> 的优势之一是避免了像 Struts2 一样要配置和维护 XML 文件。所以用户可以不在过问 XML 文件,使用纯 Java 语言来完成所有开发及配置。</p>
<h2 id="java-注解原理">Java 注解原理</h2>
<p>Java 编译器在将源码编译成字节码的同时,会处理注解符号并附加到字节码的结构中。编译出来的文件大致包含类、类的方法、类的成员变量这三种信息,对于添加了注解的元素,编译器会同时在相应的元素上添加注解信息。</p>
<p>当 JVM 开始加载字节码文件的时候,会去读取类、类的方法、类的成员变量这些信息,当发现读取的信息中包含注解信息的时候则去处理这些信息。对待注解信息的逻辑有两种,一个是类加载完成以后丢弃,一个是类加载完成以后不丢弃。当然在编译阶段编译器也可以处理注解信息,根据注解信息对用户输出编译警告或者错误,所以注解的处理逻辑事实上有三种。</p>
<p>从这里我们可以发现注解的实现实际上需要两种支持:</p>
<ol>
<li>编译器的支持。这样我们才能将注解信息加入到编译输出的文件(对 Java 来说是字节码,对 c/c++/objC 来说是 object 文件)</li>
<li>类加载支持。根据编译输出的文件结构加载类的时候,同时读取注解信息,根据注解信息进行相应的处理</li>
</ol>
<h2 id="swift-的注解-attribute">Swift 的注解: Attribute</h2>
<p>事实上很多其他语言也有类似 Java 注解的这种东西,比如 c# 和 Swift 的 Attribute。</p>
<p>Swift 的 Attribute 使用在 Swift 的开发中其实也是非常普遍的,比如下面这个 AppDelegate:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
return true
}
}
</code></pre></div></div>
<p>通过使用 <code class="highlighter-rouge">@UIApplicationMain</code> 编译器就会知道下面的 <code class="highlighter-rouge">AppDelegate</code> 这个类就是 app 启动的时候系统需要调用的 app 代理。联想一下 ObjC 中我们是这样使用 <code class="highlighter-rouge">AppDelegate</code></p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
</code></pre></div></div>
<p>所以在相应的类定义前面使用 <code class="highlighter-rouge">@UIApplicationMain</code>,我们就不需要自己告诉 iOS 系统该 app 的代理是哪个类。
并且如果你定义的 <code class="highlighter-rouge">AppDelegate</code> 没有实现 <code class="highlighter-rouge">UIApplicationDelegate</code> 的话,编译器在编译阶段就会展示一个编译错误给你,提示你应该去实现这个协议。</p>
<h2 id="swift-attribute-的使用及缺点">Swift Attribute 的使用及缺点</h2>
<p>Swift 语言本身定义了很多 Attribute 给开发人员使用,你可以查看<a href="https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Attributes.html#//apple_ref/doc/uid/TP40014097-CH35-XID_460">官方文档</a>了解使用详情。</p>
<p>网上有人也整理了一份 Swift 的所有 Attribute 以及说明,<a href="http://andelf.github.io/blog/2014/06/06/swift-attributes/">swift attributes 符号</a>,当然随着 Swift 的发展可能会有越来越多的 Attribute 加入,原有的 attribute 也可能有些变化。</p>
<p>目前 Swift Attribute 的缺点是无法支持自定义,也就是说我们无法像 Java 一样使用注解。官方提供的 Attribute 的效果非常有限。有可能随着 Swift 的发展后面会加入对自定义 Attribute 的支持,毕竟 Swift 本身是已经支持 Attribute 的。关于 Swift 支持自定义 Attribute 你可以看 <a href="https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151221/004545.html">Swift 关于自定义 Attribute 实现的讨论</a></p>
<h2 id="如果-swift-支持自定义-attribute">如果 Swift 支持自定义 Attribute</h2>
<p>让我们来想一想如果 Swift 可以支持自定义注解的话有什么好的? Medium 上面有人已经这样设想了,你可以看 <a href="https://medium.com/@azamsharp/custom-attributes-in-swift-language-5fa24030b3d0">Custom Attributes in Swift Language</a> 来了解。</p>
<p>但是对我来说,Swift 自定义 Attribute 的最大一个好处是,会出现类似 <a href="https://spring.io/"><code class="highlighter-rouge">SpringMVC</code></a> 这样的框架,用于支持 Swift 的后端开发,以及革新移动端开发方式。</p>
<p>一方面,很多人开始将 Swift 用于后端开发,类似 SpringMVC 这样的 Swift Web 框架也一定会出现。</p>
<p>另一方面,现在很多移动端开始支持 Router,也就是移动 app 里面页面的跳转是基于 URL 的,这样的开源代码很多比如 <a href="https://github.com/joeldev/JLRoutes">JLRoutes</a>,但是基于 URL 的跳转的缺陷是需要配置 URL 跳转逻辑。我们是否可以想象一下,移动端的开发以后也可以像上面我举例的 SpringMVC demo 一样,通过简单的使用注解,当一个 URL 请求来的时候新的框架能够自动帮我们解析,并跳转到该界面,省去了配置 URL 和解析 URL?</p>
<h2 id="总结">总结</h2>
<p>Java 注解的使用可以简化项目的配置,因为编译器和类加载机制可以根据注解来帮助我们完成这一部分配置,使得开发人员可以专注于语言本身。</p>
<p>另一方法 Swift 的 Attribute 作用则非常有限,但是由于 Swift 本身是支持 Attribute 的,如果未来 Swift 能支持自定义 Attribute 的话,想象空间还是很大的。</p>
<h2 id="引用">引用</h2>
<ol>
<li><a href="https://spring.io/guides/gs/rest-service/">Building a RESTful Web Service</a></li>
<li><a href="https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Attributes.html#//apple_ref/doc/uid/TP40014097-CH35-XID_460">Swfit 语言官方文档 - Attribute</a></li>
<li><a href="https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151221/004545.html">Swift 关于自定义 Attribute 实现的讨论</a></li>
<li><a href="http://blog.csdn.net/wangyangzhizhou/article/details/51698638">Java 注解机制及其原理</a></li>
<li><a href="http://www.importnew.com/10294.html">Java中的注解是如何工作的?</a></li>
<li><a href="https://kemchenj.github.io/2016-12-25-1/">深入理解 Swift 派发机制</a></li>
<li><a href="http://andelf.github.io/blog/2014/06/06/swift-attributes/">swift attributes 符号</a></li>
<li><a href="https://medium.com/@azamsharp/custom-attributes-in-swift-language-5fa24030b3d0">Custom Attributes in Swift Language</a></li>
<li><a href="https://spring.io/">SpringMVC</a></li>
<li><a href="https://github.com/joeldev/JLRoutes">JLRoutes</a></li>
</ol>Ampire_Dansmile47777@gmail.com前言iOS 生成 SecKeyRef 的正规方式2017-11-16T00:00:00+00:002017-11-16T00:00:00+00:00https://danleechina.github.io/iOS-create-SecKeyRef-official-way<h2 id="前言">前言</h2>
<p>针对 macOS 的开发,多年以前苹果就弃用了 OpenSSL,转而推荐自有框架 Security 和 CommonCrypto。当然你仍然可以使用 OpenSSL,比如说在 iOS 上使用开源库 <a href="https://github.com/x2on/OpenSSL-for-iPhone">OpenSSL for iPhone</a>。</p>
<p>苹果有一套自己的方式来生成各种密钥(对称加密、非对称加密),你可以查看苹果的 sample code <a href="https://developer.apple.com/library/content/samplecode/CryptoExercise/Introduction/Intro.html">CryptoExercise</a>,来了解如何在苹果自有平台(macOS、iOS、 tvOS 等等)上使用这一套机制。</p>
<p>OpenSSL 是被广泛使用的生成公私钥对以及各类证书等文件的方式,比如生成 PEM 或者 DER 后缀的文件(前者是 Base64 编码,或者则是 DER 编码的内容,如何包含的只是公钥或者私钥的话,本质上就没有区别)。但是在 iOS 并没有原生支持读取只包含公钥或者私钥的方法(iOS 10 之后可以使用 SecKeyCreateWithData 来生成) 。</p>
<p>在 iOS 上, SecKeyRef 对象是一个密码学角度的抽象的密钥对象(也就是说它可以代表一个公钥、私钥或者某种对称加密的密钥)。所以如何生成这样一个对象就显得格外重要,因为无论是加解密还是签名,都会需要这个对象</p>
<h2 id="原生生成公私钥对象的一种通用方式-仅限-ios-10-及以上">原生生成公私钥对象的一种通用方式 (仅限 iOS 10 及以上)</h2>
<p>苹果从 iOS 10 开始支持直接从公私钥数据来生成 SecKeyRef。步骤如下:</p>
<ol>
<li>对于 PEM 编码的数据,需要先将多余的信息给剔除,主要是头尾两行 (begin 和 end )以及去掉换行。</li>
<li>构造一个 attribute 属性字典,指定密钥算法(比如 RSA),密钥格式(公钥还是私钥),还有密钥大小</li>
<li>调用 SecKeyCreateWithData,返回一个 SecKeyRef</li>
</ol>
<p>下面是具体代码:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SecKeyRef getPrivateKeyFromPem() {
// 下面是对于 PEM 格式的密钥文件的密钥多余信息的处理,通常 DER 不需要这一步
NSString *key = @"PEM 格式的密钥文件";
NSRange spos;
NSRange epos;
spos = [key rangeOfString:@"-----BEGIN RSA PRIVATE KEY-----"];
if(spos.length > 0){
epos = [key rangeOfString:@"-----END RSA PRIVATE KEY-----"];
}else{
spos = [key rangeOfString:@"-----BEGIN PRIVATE KEY-----"];
epos = [key rangeOfString:@"-----END PRIVATE KEY-----"];
}
if(spos.location != NSNotFound && epos.location != NSNotFound){
NSUInteger s = spos.location + spos.length;
NSUInteger e = epos.location;
NSRange range = NSMakeRange(s, e-s);
key = [key substringWithRange:range];
}
key = [key stringByReplacingOccurrencesOfString:@"\r" withString:@""];
key = [key stringByReplacingOccurrencesOfString:@"\n" withString:@""];
key = [key stringByReplacingOccurrencesOfString:@"\t" withString:@""];
key = [key stringByReplacingOccurrencesOfString:@" " withString:@""];
// This will be base64 encoded, decode it.
NSData *data = base64_decode(key);
if(!data){
return nil;
}
// 设置属性字典
NSMutableDictionary *options = [NSMutableDictionary dictionary];
options[(__bridge id)kSecAttrKeyType] = (__bridge id) kSecAttrKeyTypeRSA;
options[(__bridge id)kSecAttrKeyClass] = (__bridge id) kSecAttrKeyClassPrivate;
NSNumber *size = @2048;
options[(__bridge id)kSecAttrKeySizeInBits] = size;
NSError *error = nil;
CFErrorRef ee = (__bridge CFErrorRef)error;
// 调用接口获取密钥对象
SecKeyRef ret = SecKeyCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)options, &ee);
if (error) {
return nil;
}
return ret;
}
</code></pre></div></div>
<h2 id="原生生成公私钥对象的一种通用方式-ios-9-及以前">原生生成公私钥对象的一种通用方式 (iOS 9 及以前)</h2>
<p>针对 iOS 10 以前的版本,想要获取私钥的正规途径是通过 P12(亦即 PKCS #12) 文件获取(P12 是同时包含公私钥的文件,同时需要一个对称密码来使用 p12 文件),步骤也很简单:</p>
<ol>
<li>读取 p12 文件,当然我不推荐你直接将 p12 文件放在 app bundle 中。你可以硬编码在代码中,会安全一丢丢。</li>
<li>设置参数字典,主要是设置你在导出 p12 文件时候设置的密码。</li>
<li>调用 SecPKCS12Import 导出 p12 文件包含的 item 数组</li>
<li>获取 item 数组第一个元素的字典,其中 kSecImportItemIdentity 键对应的是值也就是 SecIdentityRef 对象</li>
<li>从 SecIdentityRef 中拷出私钥对象</li>
</ol>
<p>如果要拷出公钥,稍微有点不一样:</p>
<ol>
<li>上面步骤中获得 item 数组第一个元素的字典,其中 kSecImportItemTrust 键对应的是值也就是一个 Trust 对象。</li>
<li>调用 SecTrustCopyPublicKey 获取公钥对象</li>
</ol>
<p>下面是代码解释:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NSString *resourcePath = [[NSBundle mainBundle] pathForResource:@"rsaPrivate" ofType:@"p12"];
NSData *p12Data = [NSData dataWithContentsOfFile:resourcePath];
NSMutableDictionary * options = [[NSMutableDictionary alloc] init];
SecKeyRef privateKeyRef = NULL;
id publicKey = NULL;
// 改成你设置的密码
[options setObject:@"" forKey:(id)kSecImportExportPassphrase];
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
OSStatus securityError = SecPKCS12Import((CFDataRef) p12Data, (CFDictionaryRef)options, &items);
if (securityError == noErr && CFArrayGetCount(items) > 0) {
// 获取一个 Identity 对象
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
// 获取私钥
SecIdentityRef identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
securityError = SecIdentityCopyPrivateKey(identityApp, &privateKeyRef);
if (securityError != noErr) {
privateKeyRef = NULL;
}
// 获取一个 Trust 对象
SecTrustRef trustRef = (SecTrustRef)CFDictionaryGetValue(identityDict, kSecImportItemTrust);
// 获取公钥
publicKey = (__bridge_transfer id)SecTrustCopyPublicKey(trustRef);
}
CFRelease(items);
</code></pre></div></div>
<h2 id="从证书文件读取公钥对象">从证书文件读取公钥对象</h2>
<p>从证书文件读取公钥对象步骤如下:</p>
<ol>
<li>读取证书文件生成一个 Certificate 对象(SecCertificateRef 类型)</li>
<li>从 Certificate 对象获取一个 Trust 对象 (SecTrustRef 类型)</li>
<li>从 Trust 对象拷贝出公钥 (这一步可以先根据 Trust 对象来判断证书是否可信)</li>
</ol>
<p>代码解释如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id publicKey = nil;
SecCertificateRef certificate;
SecCertificateRef certificates[1];
CFArrayRef tempCertificates = nil;
SecPolicyRef policy = nil;
SecTrustRef trust = nil;
SecTrustResultType result;
certificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateDate);
if (certificate) {
certificates[0] = certificate;
tempCertificates = CFArrayCreate(NULL, (const void **)certificates, 1, NULL);
policy = SecPolicyCreateBasicX509();
SecTrustCreateWithCertificates(tempCertificates, policy, &trust);
SecTrustEvaluate(trust, &result);
// 获得公钥对象
publicKey = (__bridge_transfer id)SecTrustCopyPublicKey(trust);
}
if (trust) {
CFRelease(trust);
}
if (policy) {
CFRelease(policy);
}
if (tempCertificates) {
CFRelease(tempCertificates);
}
if (certificate) {
CFRelease(certificate);
}
</code></pre></div></div>
<h2 id="如何从密钥文件生成-p12-和证书等">如何从密钥文件生成 P12 和证书等</h2>
<p>你可以参考这个 SO <a href="https://stackoverflow.com/questions/10579985/how-can-i-get-seckeyref-from-der-pem-file">How can I get SecKeyRef from DER/PEM file</a></p>
<h2 id="开源库">开源库</h2>
<p><a href="https://github.com/ideawu/Objective-C-RSA">Objective-C-RSA</a> 这个开源库源码解释了如何自己处理 PEM 格式密钥文件的头,但是由于解析力不够强,经常会返回一个空的密钥对象。所以必要时候可以参考一下。但是不太推荐。笔者对 ASN.1 等概念不太熟悉,这里不过多讨论了。</p>
<h2 id="使用-openssl">使用 OpenSSL</h2>
<p>使用 OpenSSL 无法生成 SecKeyRef 密钥对象,但是 OpenSSL 提供了完整的密码学各类操作支持(加密,加签,解密,验签等),所以你完全可以不需要苹果的 Security 框架。你可以参考 <a href="https://doc.open.alipay.com/docs/doc.htm?spm=a219a.7629140.0.0.ycHYn0&treeId=54&articleId=104509&docType=1">支付宝的 Demo</a>,了解如何使用该库。开源代码地址是 <a href="https://github.com/x2on/OpenSSL-for-iPhone">OpenSSL for iPhone</a></p>
<h2 id="ios-上关于加密等密码学操作的建议">iOS 上关于加密等密码学操作的建议</h2>
<p>苹果的官方文档 <a href="https://developer.apple.com/documentation/security">苹果 Security 框架文档</a> 完整的描述了如何在苹果自有平台使用 Security 框架。你可以参考它。</p>
<p>主要是理解几个对象:(文档地址 <a href="https://developer.apple.com/documentation/security/certificate_key_and_trust_services">Certificate, Key, and Trust Services</a>)</p>
<ol>
<li>certificate 对象</li>
<li>identity 对象</li>
<li>trust 对象</li>
<li>key 对象</li>
<li>policy 对象</li>
</ol>
<h2 id="引用">引用</h2>
<ol>
<li><a href="https://github.com/x2on/OpenSSL-for-iPhone">OpenSSL for iPhone</a></li>
<li><a href="https://developer.apple.com/library/content/samplecode/CryptoExercise/Introduction/Intro.html#//apple_ref/doc/uid/DTS40008019-Intro-DontLinkElementID_2">苹果 sample code CryptoExercise</a></li>
<li><a href="https://digitalleaves.com/blog/2015/08/commoncrypto-in-swift/">Swift 对称密码使用法</a></li>
<li><a href="https://digitalleaves.com/blog/2015/10/asymmetric-cryptography-in-swift/">Swift 非对称密码使用法</a></li>
<li><a href="https://digitalleaves.com/blog/2015/10/sharing-public-keys-between-ios-and-the-rest-of-the-world/">iOS 上的公钥 VS OpenSSL 上的公钥对比</a></li>
<li><a href="https://support.ssl.com/Knowledgebase/Article/View/19/0/der-vs-crt-vs-cer-vs-pem-certificates-and-how-to-convert-them">密钥文件等的区别和转换</a></li>
<li><a href="https://stackoverflow.com/questions/42689108/rsa-sha256-signing-in-ios-and-verification-on-java">iOS 上的 SHA256 with RSA VS JAVA 平台</a></li>
<li><a href="https://stackoverflow.com/questions/10579985/how-can-i-get-seckeyref-from-der-pem-file">P12、证书文件的生成</a></li>
<li><a href="https://developer.apple.com/documentation/security">苹果 Security 框架文档</a></li>
<li><a href="https://github.com/ideawu/Objective-C-RSA">Objective-C-RSA 开源库</a></li>
</ol>Ampire_Dansmile47777@gmail.com前言macOS 和 iOS 中 Nib 文件实现原理以及最佳实践2017-06-21T00:00:00+00:002017-06-21T00:00:00+00:00https://danleechina.github.io/macOS-and-iOS-nib-file-principle-and-best-practice<blockquote>
<p>译者注:本文是对 Apple 官方文档的翻译,原文地址为:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html</p>
</blockquote>
<blockquote>
<p>iOS 和 macOS 中的 xib 文件以及 storyboard 面板最终会被 Xcode 打包系统处理生成相应的 nib 文件并放置到 ipa 包中。所以可以说本文是对这两种文件底层原理的解析。同时该文档也是实践指南。</p>
</blockquote>
<h1 id="目录"><a name="table-content"></a>目录</h1>
<ul>
<li><a href="#Nib-Files">Nib 文件</a></li>
<li><a href="#Anatomy-of-a-Nib-File">解剖 Nib 文件</a>
<ul>
<li><a href="#About-Your-Interface-Objects">关于您的界面对象</a></li>
<li><a href="#About-the-File’s-Owner">关于文件的所有者</a></li>
<li><a href="#About-the-First-Responder">关于第一响应者</a></li>
<li><a href="#About-the-Top-Level-Objects">关于顶级对象</a></li>
<li><a href="#About-Image-and-Sound-Resources">关于图像和声音资源</a></li>
</ul>
</li>
<li><a href="#Nib-File-Design-Guidelines">Nib 文件设计指南</a></li>
<li><a href="#The-Nib-Object-Life-Cycle">Nib 对象生命周期</a>
<ul>
<li><a href="#The-Object-Loading-Process">对象加载过程</a></li>
</ul>
</li>
<li><a href="#Managing-the-Lifetimes-of-Objects-from-Nib-Files">管理 Nib 文件中对象的生命周期</a>
<ul>
<li><a href="#Top-level-Objects-in-OS-X-May-Need-Special-Handling">OS X 中的顶级对象可能需要特殊处理</a></li>
</ul>
</li>
<li><a href="#Action-Methods">Action Methods</a></li>
<li><a href="#Built-In-Support-For-Nib-Files">Nib 文件的内置支持</a>
<ul>
<li><a href="#The-Application-Loads-the-Main-Nib-File">应用程序加载主 Nib 文件</a></li>
<li><a href="#Each-View-Controller-Manages-its-Own-Nib-File">每个视图控制器管理其自己的 Nib 文件</a></li>
<li><a href="#Document-and-Window-Controllers-Load-Their-Associated-Nib-File">文档和窗口控制器加载相关的 Nib 文件</a></li>
</ul>
</li>
<li><a href="#Loading-Nib-Files-Programmatically">以编程方式加载 Nib 文件</a>
<ul>
<li><a href="#Loading-Nib-Files-Using-NSBundle">使用NSBundle加载Nib文件</a></li>
<li><a href="#Loading-Nib-Files-Using-UINib-and-NSNib">使用 UINib 和 NSNib 加载 Nib 文件</a></li>
<li><a href="#Replacing-Proxy-Objects-at-Load-Time">在加载时替换代理对象</a></li>
<li><a href="#Accessing-the-Contents-of-a-Nib-File">访问 Nib 文件的内容</a></li>
</ul>
</li>
<li><a href="#Connecting-Menu-Items-Across-Nib-Files">连接 Nib 文件中的菜单项</a></li>
</ul>
<h1 id="-nib-文件"><a name="Nib-Files"></a> Nib 文件</h1>
<p>在 OS X 和 iOS 创建应用程序中起着重要的作用。使用 nib 文件,您可以使用 Xcode 以图形方式创建和操作用户界面(而不是以编程方式)。因为您可以立即查看更改的结果,您可以快速尝试不同的布局和配置。您也可以稍后更改用户界面的许多方面,而无需重写任何代码。</p>
<p>对于使用 AppKit 或 UIKit 框架构建的应用程序,nib 文件具有重要意义。这两个框架都支持使用 nib 文件来布局窗口、视图和控件,并将这些项目与应用程序的事件处理代码集成在一起。Xcode 与这些框架配合使用,可帮助您将用户界面的控件连接到项目中,设置为您自定义的对象。该集成显着减少了加载 nib 文件后所需的设置量,并且在以后可以轻松更改代码和用户界面之间的关系。</p>
<blockquote>
<p>注意:虽然可以在不使用 nib 文件的情况下创建 Objective-C 应用程序,但这样做非常罕见,不推荐。 根据您的应用程序,避免 nib 文件可能需要您编程大量的框架行为才能实现与使用 nib 文件相同的结果。</p>
</blockquote>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-解剖-nib-文件"><a name="Anatomy-of-a-Nib-File"></a> 解剖 Nib 文件</h2>
<p>一个 nib 文件描述了应用程序的用户界面的视觉元素,包括窗口,视图,控件等等。 它还可以描述非可视元素,例如应用程序中管理窗口和视图的对象。 更重要的是,在 Xcode 中配置的对象最后会一一映射到 nib 文件描述的对象。 在运行时,这些描述用于在应用程序中重新创建对象及其配置。 当您在运行时加载 nib 文件时,您将获得 Xcode 文档中对象的精确副本。 nib 加载代码实例化对象,配置它们,并重新建立您在 nib 文件中创建的任何对象间的连接。</p>
<p>以下部分描述了如何组织与 AppKit 和 UIKit 框架一起使用的 nib 文件,在其中找到的对象类型以及如何有效地使用这些对象。</p>
<h3 id="-关于您的界面对象"><a name="About-Your-Interface-Objects"></a> 关于您的界面对象</h3>
<p>界面对象是添加到 nib 文件中来实现用户界面的对象。当在运行时加载一个 nib 时,界面对象是由 nib 加载代码实际实例化的对象。大多数新的 nib 文件默认情况下至少有一个界面对象,通常是窗口或菜单资源,并且可以在界面设计中添加更多的界面对象到 nib 文件。这是 nib 文件中最常见的对象类型,通常也是您为什么要使用 nib 文件的原因。</p>
<p>除了表示视觉对象,如窗口,视图,控件和菜单之外,界面对象还可以表示非视觉对象。在几乎所有情况下,添加到 nib 文件的非可视对象都是您的应用程序用于管理可视对象的额外控制器对象。虽然您可以在应用程序中创建这些对象,但将其添加到 nib 文件并将其配置在那里通常更为方便。 Xcode 提供了一个通用对象,特别地,您可以在将控制器和其他非可视对象添加到 nib 文件时使用。它还提供了通常用于管理 Cocoa 绑定的控制器对象。</p>
<h3 id="-关于文件的所有者"><a name="About-the-File’s-Owner"></a> 关于文件的所有者</h3>
<p>nib 文件中最重要的对象是 File’s Owner 对象。 与界面对象不同,File’s Owner 对象是一个占位符对象,在加载 nib 文件时不会创建。 相反,您可以在代码中创建此对象,并将其传递给 nib 加载代码。 这个对象如此重要的原因是它是应用程序代码和 nib 文件内容之间的主要链接。 更具体地说,它是负责 nib 文件内容的控制器对象。</p>
<p>在 Xcode 中,您可以在文件的所有者和 nib 文件中的其他接口对象之间创建连接。 加载 nib 文件时,nib 加载代码将使用您指定的替换对象重新创建这些连接。 这允许您的对象引用 nib 文件中的对象并自动从界面对象接收消息。</p>
<h3 id="-关于第一响应者"><a name="About-the-First-Responder"></a> 关于第一响应者</h3>
<p>在 nib 文件中,First Responder 是一个占位符对象,表示应用程序动态确定的响应链中的第一个对象。因为应用程序的响应链无法在设计时确定,所以第一响应者占位符充当需要针对应用程序的响应链的任何操作消息的备用目标。菜单项通常充当第一响应者占位符。例如,“窗口”菜单中的“最小化”菜单项隐藏应用程序中的最前面的窗口,而不仅仅是特定的窗口,“复制”菜单项应该复制当前选择,而不仅仅是单个控件或视图的选择。应用程序中的其他对象也可以成为第一响应者。</p>
<p>当您将 nib 文件加载到内存中时,您无需管理或替换第一响应者占位符对象。 AppKit 和 UIKit 框架根据应用程序的当前配置自动设置和维护第一响应者。</p>
<p>有关响应链的更多信息以及如何用于在基于 AppKit 的应用程序中分派事件,请参阅 <a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/EventOverview/EventArchitecture/EventArchitecture.html#//apple_ref/doc/uid/10000060i-CH3"><em>Event Architecture</em></a>。有关 iPhone 应用程序中的响应链和处理操作的信息,请参阅 <a href="https://developer.apple.com/library/content/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/index.html#//apple_ref/doc/uid/TP40009541"><em>Event Handling Guide for UIKit Apps</em></a>。</p>
<h3 id="-关于顶级对象"><a name="About-the-Top-Level-Objects"></a> 关于顶级对象</h3>
<p>当您的程序加载 nib 文件时,Cocoa 会重新创建 Xcode 中创建的整个对象图。该对象图包括在 nib 文件中找到的所有窗口、视图、控件、单元格、菜单和自定义对象。顶级对象是不具有父对象的这些对象的子集。顶级对象通常仅包含添加到 nib 文件中的窗口、菜单栏和自定义控制器对象。 (诸如 File’s Owner,第一响应者和应用程序的对象是占位符对象,并不被视为顶级对象。)</p>
<p>通常,您使用 File’s Owner 对象中的 outlet 来存储对 nib 文件的顶级对象的引用。如果不使用 outlet,则可以直接从 nib 加载例程中检索顶层对象。您应该始终保持一个指向这些对象的指针,因为您的应用程序负责使用完之后释放它们。有关加载时的 nib 对象行为的更多信息,请参阅 <a href="#Managing-the-Lifetimes-of-Objects-from-Nib-Files"><em>管理 Nib 文件中对象的生命周期</em></a>。</p>
<h3 id="-关于图像和声音资源"><a name="About-Image-and-Sound-Resources"></a> 关于图像和声音资源</h3>
<p>在 Xcode 中,您可以从 nib 文件的内容中引用外部图像和声音资源。一些控件和视图能够显示图像或播放声音作为默认配置的一部分。 Xcode 库提供对 Xcode 项目中的图像和声音资源的访问支持,以便您可以将 nib 文件链接到这些资源。 nib 文件不直接存储这些资源。相反,它存储资源文件的名称,以便 nib 加载代码稍后可以找到。</p>
<p>加载包含图像或声音资源引用的 nib 文件时,nib 加载代码将实际的图像或声音文件读入内存并缓存。在 OS X 中,图像和声音资源存储在命名的缓存中,以便以后可以在需要时访问它们。在 iOS 中,只有图像资源存储在命名的缓存中。要访问图像,请使用 NSImage 或 UIImage 的 <code class="highlighter-rouge">imageNamed:</code> 方法,具体取决于您的平台。要在OS X 中访问缓存的声音,请使用 NSSound 的 <code class="highlighter-rouge">soundNamed:</code> 方法。</p>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-nib-文件设计指南"><a name="Nib-File-Design-Guidelines"></a> Nib 文件设计指南</h2>
<p>创建 nib 文件时,请仔细考虑如何打算使用该文件中的对象。一个非常简单的应用程序可能将所有用户界面组件存储在单个 nib 文件中,但对于大多数应用程序,最好将组件分布在多个 nib 文件中。创建较小的 nib 文件可让您立即加载您需要的界面部分。它们还可以更容易地调试您可能遇到的任何问题,因为需要检查的地方较少。</p>
<p>创建 nib 文件时,请牢记以下准则:</p>
<ul>
<li>用懒加载设计你的 nib 文件。计划加载仅包含您需要的对象的 nib 文件。</li>
<li>在 OS X 应用程序的主要 nib 文件中,请考虑仅将应用程序菜单栏和可选应用程序委托对象存储在 nib 文件中。避免在应用程序启动后包括任何不会使用的窗口或用户界面元素。相反,将这些资源放在单独的 nib 文件中,并在启动后根据需要加载它们。</li>
<li>将重复的用户界面组件(如文档窗口)存储在单独的 nib 文件中。</li>
<li>对于仅偶尔使用的窗口或菜单,将其存储在单独的 nib 文件中。通过将其存储在单独的 nib 文件中,只有在实际使用时才将资源加载到内存中。</li>
<li>使 File’s Owener 对象成为 nib 文件外的任何单一联系人。请参阅 <a href="#Accessing-the-Contents-of-a-Nib-File"><em>访问 Nib 文件的内容</em></a></li>
</ul>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="--nib-对象生命周期"><a name="The-Nib-Object-Life-Cycle"></a> Nib 对象生命周期</h2>
<p>当 nib 文件加载到内存中时,nib 加载代码需要几个步骤来确保 nib 文件中的对象被正确创建和初始化。 了解这些步骤可以帮助您编写更好的控制器代码来管理用户界面。</p>
<h3 id="-对象加载过程"><a name="The-Object-Loading-Process"></a> 对象加载过程</h3>
<p>当您使用 NSNib 或 NSBundle 的方法加载和实例化 nib 文件中的对象时,底层的
nib 加载代码执行以下操作:</p>
<ol>
<li>它将 nib 文件和任何引用的资源文件的内容加载到内存中:
<ul>
<li>整个 nib 对象图的原始数据被加载到内存中,但没有解档。</li>
<li>nib 文件相关联的任何自定义图像资源都将被加载并添加到 Cocoa 图像缓存中。请参阅<a href="#About-Image-and-Sound-Resources"><em>关于图像和声音资源</em></a>。</li>
<li>nib 文件相关联的任何自定义声音资源都将被加载并添加到 Cocoa 声音缓存;请参阅 <a href="#About-Image-and-Sound-Resources"><em>关于图像和声音资源</em></a>。</li>
</ul>
</li>
<li>
<p>它解档 nib 对象图数据并实例化对象。如何初始化每个新对象取决于对象的类型及其在归档中的编码方式。 nib 加载代码使用以下规则(按顺序)来确定要使用的初始化方法。</p>
<ul>
<li>
<p>默认情况下,对象会收到一个 <code class="highlighter-rouge">initWithCoder:</code> 消息。</p>
<ul>
<li>
<p>在 OS X 中,标准对象列表包括系统提供的视图,单元格,菜单和视图控制器,并且在默认的 Xcode 库中可用。它还包括使用自定义插件添加到库的任何第三方对象。即使您更改了这样一个对象的类,Xcode 将标准对象编码到 nib 文件中,然后在对象被解档时通知归档器使用改自定义类。</p>
</li>
<li>
<p>在 iOS 中,使用 <code class="highlighter-rouge">initWithCoder:</code> 方法初始化符合 NSCoding 协议的任何对象。这包括 UIView 和 UIViewController 的所有子类,无论它们是默认的 Xcode 库或定义的自定义类的一部分。</p>
</li>
</ul>
</li>
<li>
<p>OS X 中自定义视图会收到一条 <code class="highlighter-rouge">initWithFrame:</code> 消息。</p>
<ul>
<li>
<p>自定义视图是 NSView 的子类,Xcode 没有可用的实现。通常,这些是您在应用程序中定义并用于提供自定义可视内容的视图。自定义视图不包括作为默认库或集成第三方插件的一部分的标准系统视图(如 NSSlider)。</p>
</li>
<li>
<p>当它遇到自定义视图时,Xcode 将一个特殊的 NSCustomView 对象编码到你的 nib 文件中。自定义视图对象包括构建您指定的真实视图子类所需的信息。在加载时,NSCustomView 对象将一个 <code class="highlighter-rouge">alloc</code> 和 <code class="highlighter-rouge">initWithFrame:</code> 消息发送到真实的视图类,然后交换自己生成的视图对象。净效果是真正的视图对象处理在 nib 加载过程中的后续交互。</p>
</li>
<li>
<p>iOS 中的自定义视图不会使用 <code class="highlighter-rouge">initWithFrame:</code>方法进行初始化。</p>
</li>
</ul>
</li>
<li>
<p>除了上述步骤中描述的以外的自定义对象接收 <code class="highlighter-rouge">init</code> 初始消息。</p>
</li>
</ul>
</li>
<li>
<p>重新建立 nib 文件中对象之间的所有连接(action、outlet 和绑定)。 这包括与 File’s Owner 和其他占位符对象的连接。 建立连接的方法因平台而异:</p>
<ul>
<li>Outlet 连接
<ul>
<li>在 OS X 中,nib 加载代码首先尝试使用对象自己的方法重新连接 outlet。对于每个 outlet ,Cocoa 查找一个形式为 setOutletName 的方法,如果存在这样的方法,则调用它。如果找不到这样的方法,Cocoa 会在对象中搜索具有相应 outlet 名称的实例变量,并尝试直接设置值。如果找不到实例变量,则不会创建任何连接。
设置 outlet 还会为任何注册的观察者生成键值观察(KVO)通知。这些通知可能在所有对象间连接重新建立之前发生,并且在调用任何对象的 <code class="highlighter-rouge">awakeFromNib</code> 方法之前肯定会发生这些通知。</li>
<li>在 iOS 中,nib 加载代码使用 <code class="highlighter-rouge">setValue:forKey:</code>方法重新连接每个 outlet。该方法同样寻找适当的访问器方法,并且在失败时使用其他方式。有关此方法如何设置值的更多信息,请参阅其在 <a href="https://developer.apple.com/documentation/foundation/object_runtime/nskeyvaluecoding"><em>NSKeyValueCoding Protocol Reference</em></a> 的描述。</li>
<li>在 iOS 中设置 outlet 还会为任何注册的观察者生成 KVO 通知。这些通知可能在所有对象间连接重新建立之前发生,并且在调用任何对象的 <code class="highlighter-rouge">awakeFromNib</code> 方法之前肯定会发生这些通知。</li>
</ul>
</li>
<li>动作连接
<ul>
<li>在 OS X 中,nib 加载代码使用源对象的 <code class="highlighter-rouge">setTarget:</code> 和 <code class="highlighter-rouge">setAction:</code>方法来建立与目标对象的连接。如果目标对象没有响应 action 方法,则不会创建任何连接。如果目标对象为空,则该动作由响应链处理。</li>
<li>在 iOS 中,nib 加载代码使用 UIControl 对象的 <code class="highlighter-rouge">addTarget:action:forControlEvents:</code> 方法来配置操作。如果目标为空,则动作由响应链处理。</li>
</ul>
</li>
<li>绑定
<ul>
<li>在 OS X 中,Cocoa 使用源对象的 <code class="highlighter-rouge">bind:toObject:withKeyPath:options:</code> 方法来创建它与其目标对象之间的连接。</li>
<li>iOS 中不支持绑定。</li>
</ul>
</li>
</ul>
</li>
<li>将一个 <code class="highlighter-rouge">awakeFromNib</code> 消息发送到 nib 文件中定义匹配选择器的相应对象:
<ul>
<li>在 OS X 中,该消息被发送到定义该方法的任何界面对象。它也被发送到 File’s Owner 和定义的任何占位符对象。</li>
<li>在 iOS 中,此消息仅发送到由 nib 加载代码实例化的界面对象。它不发送到 File’s Owner、第一响应者或任何其他占位符对象。</li>
</ul>
</li>
<li>显示在 nib 文件中启用了“Visible at launch time”属性的任何窗口。</li>
</ol>
<p>nib 加载代码调用对象的 awakeFromNib 方法的顺序是不能保证的。在 OS X 中,Cocoa尝试最后调用 File‘Owner 的 awakeFromNib 方法,但不能保证该行为。如果您需要在加载时进一步配置 nib 文件中的对象,则最适合的时间是在您的 nib 加载调用返回后。这时,所有的对象都被创建,初始化并准备好使用。</p>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-管理-nib-文件中对象的生命周期"><a name="Managing-the-Lifetimes-of-Objects-from-Nib-Files"></a> 管理 Nib 文件中对象的生命周期</h2>
<p>每次您要求 NSBundle 或 NSNib 类加载 nib 文件时,底层代码会创建该文件中的对象的新副本并将其返回给您。(nib 加载代码不会从以前的加载尝试中回收 nib 文件对象。)您需要确保在必要时保持新的对象图,并在完成之后将其取消。通常需要对顶级对象的强引用以确保它们不被释放。您不需要强引用对象图中较低的对象,因为它们由父母结点拥有,您应该尽可能减少引用循环的风险。</p>
<p>从实际的角度来看,iOS 和 OS X 的 outlet 应该被定义为声明的属性。 Outlets 通常应该是 weak 的,除了 nib 文件对应的 File’s Owner 、和 nib 文件拥有的顶级对象(或 iOS 的故事板场景中)应该是强的。因此,您创建的 outlet 通常应该是 weak 的,因为:</p>
<ul>
<li>例如,您创建的从视图控制器 view 或窗口控制器窗口的子视图的 outlet 是不暗示所有权的对象之间的任意引用。</li>
<li>强大的 outlet 经常由框架类指定(例如,UIViewController 的 view outlet 或 NSWindowController 的窗口 outlet)。</li>
</ul>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@property</span> <span class="p">(</span><span class="n">weak</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">MyView</span> <span class="o">*</span><span class="n">viewContainerSubview</span><span class="p">;</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">strong</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">MyOtherClass</span> <span class="o">*</span><span class="n">topLevelObject</span><span class="p">;</span>
</code></pre></div></div>
<blockquote>
<p>注意:在 OS X 中,并非所有类都支持弱引用 - 请参阅 <a href="https://developer.apple.com/library/content/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011226"><em>Transitioning to ARC Release Notes</em></a>。 在不能指定的情况下,您应该使用 <code class="highlighter-rouge">assign</code> :</p>
</blockquote>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">@property</span> <span class="p">(</span><span class="n">assign</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">NSTextView</span> <span class="o">*</span><span class="n">textView</span><span class="p">;</span>
</code></pre></div></div>
<p>Outlet 通常被认为是定义类别的私有; 除非有理由公开揭露属性,否则隐藏属性声明类扩展名。 例如:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MyClass.h
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span> <span class="p">:</span> <span class="nc">MySuperclass</span>
<span class="k">@end</span>
<span class="c1">// MyClass.m
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span> <span class="p">()</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">weak</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">MyView</span> <span class="o">*</span><span class="n">viewContainerSubview</span><span class="p">;</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">strong</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">MyOtherClass</span> <span class="o">*</span><span class="n">topLevelObject</span><span class="p">;</span>
<span class="k">@end</span>
</code></pre></div></div>
<p>这些模式扩展到从容器视图到其子视图的引用,您必须考虑对象图的内部一致性。 例如,在表视图单元格的情况下,特定子视图的 outlet 通常是 weak。 如果表视图包含图像视图和文本视图,那么这些视图仍然有效,只要它们是表视图单元本身的子视图。</p>
<p>当 outlet 被认为拥有参考对象时,outlet 应变为 strong:</p>
<ul>
<li>如前所述,通常情况下,top level 的对象在 nib 文件中经常被认为是由 File’s Owner 拥有的。</li>
<li>在某些情况下,您可能需要一个来自 nib 文件的对象存在其原始容器之外。 例如,您可能有一个视图的 outlet,可以临时从其初始视图层次结构中删除,因此必须独立进行维护。</li>
</ul>
<p>您希望被子类化的类(特别是抽象类)公开暴露 outlet,以便它们可以被子类(例如UIViewController 的视图 outlet)适当地使用。 如果使用者需要访问属性,则 outlet 也可能会暴露出来;.例如,表视图单元格可能会暴露子视图。 在后一种情况下,可以适合公开一个以私有定义可读的只读公共 outlet ,例如:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MyClass.h
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span> <span class="p">:</span> <span class="nc">UITableViewCell</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">weak</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">MyType</span> <span class="o">*</span><span class="n">outletName</span><span class="p">;</span>
<span class="k">@end</span>
<span class="c1">// MyClass.m
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span> <span class="p">()</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">weak</span><span class="p">,</span> <span class="n">readwrite</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">MyType</span> <span class="o">*</span><span class="n">outletName</span><span class="p">;</span>
<span class="k">@end</span>
</code></pre></div></div>
<h3 id="-os-x-中的顶级对象可能需要特殊处理"><a name="Top-level-Objects-in-OS-X-May-Need-Special-Handling"></a> OS X 中的顶级对象可能需要特殊处理</h3>
<p>由于历史原因,在 OS X 中,将创建一个 nib 文件的顶级对象,并附加一个引用计数。应用开发框架提供了几个功能,可帮助确保正确释放 nib 对象:</p>
<ul>
<li>
<p>NSWindow 对象(包括面板)有一个 isReleasedWhenClosed 属性,如果设置为YES,则会在窗口关闭时指示窗口释放自身(以及其视图层次结构中的所有相关对象)。在 nib 文件中,您可以通过 Xcode inspector 的“Attribute”窗格中的“Release when closed ”复选框来设置此选项。</p>
</li>
<li>
<p>如果 nib 文件的 File’s Owner 是 NSWindowController 对象(在基于文档的应用程序中的文档 nib 中的默认值,请记住 NSDocument 管理 NSWindowController 的一个实例)或 NSViewController 对象,它会自动处理其管理的窗口。</p>
</li>
</ul>
<p>如果 File’s Owner 不是 NSWindowController 或 NSViewController 的实例,那么您需要自己递减顶级对象的引用计数。您必须将顶级对象的引用转换为 Core Foundation 类型并使用 CFRelease。 (如果不希望有所有顶级对象的 outlet ,可以使用 NSNib 类的 <code class="highlighter-rouge">instantiateNibWithOwner:topLevelObjects:</code>方法来获取一个 nib文件顶层对象的数组。)</p>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-action-methods"><a name="Action-Methods"></a> Action Methods</h2>
<p>一般来说,action method(参见 OS X 中的 Target-Action 或 iOS 中的 Target-Action)是通常由 nib 文件中另一个对象调用的方法。 Action method 使用类型限定符 IBAction(void类型的替代),将声明的方法标记为一个 action method,以便Xcode 知道它。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@interface</span> <span class="nc">MyClass</span>
<span class="k">-</span> <span class="p">(</span><span class="n">IBAction</span><span class="p">)</span><span class="nf">myActionMethod</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">sender</span><span class="p">;</span>
<span class="k">@end</span>
</code></pre></div></div>
<p>您可以选择将 action method 视为您的类私有的,因此不会在 public @interface 中声明它们。 (因为 Xcode 解析实现文件,所以不需要在头文件中声明它们。)</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MyClass.h
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span>
<span class="k">@end</span>
<span class="c1">// MyClass.m
</span>
<span class="k">@implementation</span> <span class="nc">MyClass</span>
<span class="k">-</span> <span class="p">(</span><span class="n">IBAction</span><span class="p">)</span><span class="nf">myActionMethod</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">sender</span> <span class="p">{</span>
<span class="c1">// Implementation.
</span><span class="p">}</span>
<span class="k">@end</span>
</code></pre></div></div>
<p>您通常不应以编程方式调用 action method。 如果您的类需要执行与 action method相关联的工作,那么您应该将实现应用到另一种由 action mehtod 调用的方法中。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MyClass.h
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span>
<span class="k">@end</span>
<span class="c1">// MyClass.m
</span>
<span class="k">@interface</span> <span class="nc">MyClass</span> <span class="p">(</span><span class="nl">PrivateMethods</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">doSomething</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">doWorkThatRequiresMeToDoSomething</span><span class="p">;</span>
<span class="k">@end</span>
<span class="k">@implementation</span> <span class="nc">MyClass</span>
<span class="k">-</span> <span class="p">(</span><span class="n">IBAction</span><span class="p">)</span><span class="nf">myActionMethod</span><span class="p">:(</span><span class="n">id</span><span class="p">)</span><span class="nv">sender</span> <span class="p">{</span>
<span class="p">[</span><span class="n">self</span> <span class="nf">doSomething</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">doSomething</span> <span class="p">{</span>
<span class="c1">// Implementation.
</span><span class="p">}</span>
<span class="o">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">doWorkThatRequiresMeToDoSomething</span> <span class="p">{</span>
<span class="c1">// Pre-processing.
</span> <span class="p">[</span><span class="n">self</span> <span class="nf">doSomething</span><span class="p">];</span>
<span class="c1">// Post-processing.
</span><span class="p">}</span>
<span class="k">@end</span>
</code></pre></div></div>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-nib-文件的内置支持"><a name="Built-In-Support-For-Nib-Files"></a> Nib 文件的内置支持</h2>
<p>AppKit 和 UIKit 框架都提供了一定量的自动化行为来加载和管理应用程序中的 nib 文件。 这两个框架都提供了用于加载应用程序主要 nib 文件的基础设施。 此外,AppKit 框架提供了通过 NSDocument 和 NSWindowController 类加载其他 nib 文件的支持。 以下部分介绍了 nib 文件的内置支持,如何利用它们,以及在自己的应用程序中修改该支持的方法。</p>
<h3 id="-应用程序加载主-nib-文件"><a name="The-Application-Loads-the-Main-Nib-File"></a> 应用程序加载主 Nib 文件</h3>
<p>大多数 Xcode 项目模板都已预先为应用程序配置了一个主要的 nib 文件。所有您需要做的是修改默认 nib 文件并构建您的应用程序。在启动时,应用程序的默认配置数据告诉应用程序对象找到该 nib 文件,以便加载它。在基于 AppKit 和 UIKit 的应用程序中,此配置数据位于应用程序的 Info.plist 文件中。首次加载应用程序时,应用程序启动代码默认会在 Info.plist 文件中查找 NSMainNibFile 键。如果找到它,它会在应用程序包中查找一个 nib 文件,其名称(带或不带文件扩展名)与该键的值匹配,然后加载它。</p>
<h3 id="-每个视图控制器管理其自己的-nib-文件"><a name="Each-View-Controller-Manages-its-Own-Nib-File"></a> 每个视图控制器管理其自己的 Nib 文件</h3>
<p>UIViewController(iOS)和NSViewController(OS X)类支持自动加载其关联的 nib文件。如果在创建视图控制器时指定 nib 文件,则当您尝试访问视图控制器的视图时,该 nib 文件会自动加载。视图控制器和 nib 文件对象之间的任何连接都将自动创建,在 iOS 中,当视图最终加载并显示在屏幕上时,UIViewController 对象还会收到其他通知。为了更好地管理内存,UIViewController 类还可以在低内存条件下处理卸载其 nib 文件(如适用)。</p>
<p>有关如何使用 UIViewController 类及其配置方式的更多信息,请参阅 <a href="https://developer.apple.com/library/content/featuredarticles/ViewControllerPGforiPhoneOS/index.html#//apple_ref/doc/uid/TP40007457"><em>View Controller Programming Guide for iOS</em></a>。</p>
<h3 id="-文档和窗口控制器加载相关的-nib-文件"><a name="Document-and-Window-Controllers-Load-Their-Associated-Nib-File"></a> 文档和窗口控制器加载相关的 Nib 文件</h3>
<p>在 AppKit 框架中,NSDocument 类与默认窗口控制器一起加载包含文档窗口的 nib 文件。 NSDocument的 windowNibName 方法是一种方便的方法,您可以使用它来指定包含相应文档窗口的 nib 文件。创建新文档时,文档对象将您指定的 nib 文件名传递给默认的窗口控制器对象,该对象加载并管理 nib 文件的内容。如果您使用Xcode 提供的标准模板,您唯一需要做的是将文档窗口的内容添加到 nib 文件中。</p>
<p>NSWindowController 类还提供自动支持加载 nib 文件。如果以编程方式创建自定义窗口控件,则可以选择使用 NSWindow 对象或 nib 文件的名称初始化它们。如果选择后一个选项,NSWindowController 类会在客户端首次尝试访问窗口时自动加载指定的 nib 文件。之后,窗口控制器将窗口保持在内存中。即使窗口的“Release when closed”属性被设置,它也不会从 nib 文件重新加载它。</p>
<blockquote>
<p>重要:当使用 NSWindowController 或 NSDocument 自动加载窗口时,重要的是您的 nib 文件配置正确。 这两个类都包括一个窗口 outlet,您必须连接到您要管理的窗口。 如果不将此 outlet 连接到窗口对象,则 nib 文件已加载,但文档或窗口控制器不显示窗口。 有关 Cocoa 文档体系结构的更多信息,请参阅 <a href="https://developer.apple.com/library/content/documentation/DataManagement/Conceptual/DocBasedAppProgrammingGuideForOSX/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011179"><em>Document-Based App Programming Guide for Mac</em></a> 。</p>
</blockquote>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-以编程方式加载-nib-文件"><a name="Loading-Nib-Files-Programmatically"></a> 以编程方式加载 Nib 文件</h2>
<p>OS X 和 iOS 都提供了将 nib 文件加载到应用程序中的便利方法。 AppKit 和 UIKit 框架都在 NSBundle 类上定义了支持加载 nib 文件的附加方法。 此外,AppKit 框架还提供了 NSNib 类,它提供与 NSBundle 类似的 nib 加载行为,但提供了在特定情况下可能有用的一些其他优点。</p>
<p>在设计应用程序时,请确保手动加载的任何 nib 文件都以简化加载过程的方式进行配置。 为 File’s Owner 选择一个适当的对象并保持你的 nib 文件很小可以大大提高它们的易用性和内存效率。 有关配置 nib 文件的更多提示,请参阅 <a href="#Nib-File-Design-Guidelines"><em>Nib 文件设计指南</em></a>。</p>
<h3 id="-使用nsbundle加载nib文件"><a name="Loading-Nib-Files-Using-NSBundle"></a> 使用NSBundle加载Nib文件</h3>
<p>AppKit 和 UIKit 框架在 NSBundle 类(使用 Objective-C 类别)上定义了其他方法来支持加载 nib 文件资源。 两平台之间使用方法的语义与方法的语法不同。 在 AppKit 中,通常有更多的选项可以访问 bundle,因此还有更多的方法可以从这些 bundle 加载 nib 文件。 在 UIKit 中,应用程序只能从 main bundle 装载 nib 文件,因此需要较少的选项。 两种平台上可用的方法如下:</p>
<ul>
<li>AppKit:
<ul>
<li><code class="highlighter-rouge">loadNibNamed:owner:</code> 类方法</li>
<li><code class="highlighter-rouge">loadNibFile:externalNameTable:withZone:</code> 类方法</li>
<li><code class="highlighter-rouge">loadNibFile:externalNameTable:withZone:</code> 对象方法</li>
</ul>
</li>
<li>UIKit
<ul>
<li><code class="highlighter-rouge">loadNibNamed:owner:options:</code> instance方法</li>
</ul>
</li>
</ul>
<p>每当加载 nib 文件时,都应该始终指定一个对象作为该 nib 文件的 File’s Owner。File’s Owner 的作用很重要的。它是运行代码和即将在内存中创建的新对象之间的主要接口。所有的 nib 加载方法提供了一种方法来直接指定 Files’ Owner,或者作为选项字典中的参数。</p>
<p>AppKit 和 UIKit 框架处理 nib 加载的方式之间的语义差异之一就是顶层的 nib 对象返回到应用程序的方式。在 AppKit 框架中,您必须使用 <code class="highlighter-rouge">loadNibFile:externalNameTable:withZone:methods</code> 显式请求它们。在 UIKit 中,<code class="highlighter-rouge">loadNibNamed:owner:options:</code> 方法直接返回这些对象的数组。在这两种情况下避免担心顶层对象的最简单的方法是将它们存储在 File’s Owner 的 outlets 中(请参阅 <a href="#Managing-the-Lifetimes-of-Objects-from-Nib-Files"><em>管理 Nib 文件中对象的生命周期</em></a>)。</p>
<p>清单1-1 显示了一个简单的例子,说明如何使用基于 AppKit 的应用程序中的 NSBundle 类加载 nib 文件。 <code class="highlighter-rouge">loadNibNamed:owner:</code>方法返回后,可以使用任何引用 nib 文件对象的 outlet。换句话说,整个 nib 加载过程发生在该单个调用的限制内。 AppKit 框架中的 nib 加载方法返回一个布尔值,以指示加载操作是否成功。</p>
<p>清单 1-1 Loading a nib file from the current bundle</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (BOOL)loadMyNibFile
{
// The myNib file must be in the bundle that defines self's class.
if (![NSBundle loadNibNamed:@"myNib" owner:self])
{
NSLog(@"Warning! Could not load myNib file.\n");
return NO;
}
return YES;
</code></pre></div></div>
<p>清单 1-2 显示了如何在基于 UIKit 的应用程序中加载 nib 文件的示例。 在这种情况下,该方法将检查返回的数组以查看 nib 对象是否已成功加载。 (每个 nib 文件应至少有一个表示 nib 文件内容的顶级对象。)此示例显示了当文件所有者对象不包含占位符对象时的简单情况。 有关如何指定其他占位符对象的示例,请参阅<a href="#Replacing-Proxy-Objects-at-Load-Time"><em>在加载时替换代理对象</em></a>。</p>
<p>清单 1-2 Loading a nib in an iPhone application</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (BOOL)loadMyNibFile
{
NSArray* topLevelObjs = nil;
topLevelObjs = [[NSBundle mainBundle] loadNibNamed:@"myNib" owner:self options:nil];
if (topLevelObjs == nil)
{
NSLog(@"Error! Could not load myNib file.\n");
return NO;
}
return YES;
}
</code></pre></div></div>
<blockquote>
<p>注意:如果您正在开发适用于 iOS 的通用应用程序,则可以使用特定于设备的命名约定自动为底层设备加载正确的 nib 文件。 有关如何命名 nib 文件的更多信息,请参阅<a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/Introduction/Introduction.html#//apple_ref/doc/uid/10000051i-CH1-SW2"><em>iOS Supports Device-Specific Resources</em></a> 。</p>
</blockquote>
<h3 id="-获取-nib-文件的顶级对象"><a name="Getting-a-Nib-File’s-Top-Level-Objects"></a> 获取 Nib 文件的顶级对象</h3>
<p>获取 nib 文件的顶级对象的最简单方法是在 File’s Owner 对象中定义 outlets 以及用于访问这些对象的 setter 方法(或属性)。 此方法可确保顶层对象由对象保留,并始终对其进行引用。</p>
<p>清单 1-3 显示了使用 outlet 保留 nib 文件唯一顶级对象的简化 Cocoa 类的接口和实现。 在这种情况下,nib 文件中唯一的顶级对象是 NSWindow对象。 因为 Cocoa 中的顶级对象的初始引用计数为1,因此会包含一个额外的释放消息。 这很好,因为在发出调用的时候,该属性已被保留在窗口中。 您不会希望以这种方式在 iPhone 应用程序中释放顶级对象。</p>
<p>清单 1-3 Using outlets to get the top-level objects</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Class interface.
</span><span class="k">@interface</span> <span class="nc">MyController</span> <span class="p">:</span> <span class="nc">NSObject</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">loadMyWindow</span><span class="p">;</span>
<span class="k">@end</span>
<span class="c1">// Private class extension.
</span><span class="k">@interface</span> <span class="nc">MyController</span> <span class="p">()</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">strong</span><span class="p">)</span> <span class="n">IBOutlet</span> <span class="n">NSWindow</span> <span class="o">*</span><span class="n">window</span><span class="p">;</span>
<span class="k">@end</span>
<span class="c1">// Class implementation
</span><span class="k">@implementation</span> <span class="nc">MyController</span>
<span class="k">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="n">loadMyWindow</span> <span class="p">{</span>
<span class="p">[</span><span class="n">NSBundle</span> <span class="nf">loadNibNamed</span><span class="p">:</span><span class="s">@"myNib"</span> <span class="nf">owner</span><span class="p">:</span><span class="n">self</span><span class="p">];</span>
<span class="c1">// The window starts off with a retain count of 1
</span> <span class="c1">// and is then retained by the property, so add an extra release.
</span> <span class="n">NSWindow</span> <span class="o">*</span><span class="n">window</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">window</span><span class="p">;</span>
<span class="n">CFRelease</span><span class="p">(</span><span class="n">__bridge</span> <span class="n">window</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">@end</span>
</code></pre></div></div>
<p>如果您不想使用 outlet 来存储对 nib 文件的顶级对象的引用,则必须在代码中手动检索这些对象。 获取顶级对象的技术因目标平台而异。 在 OS X 中,您必须明确要求对象,而在 iOS 中,它们将自动返回给您。</p>
<p>清单 1-4 显示了在 OS X 中获取 nib 文件的顶级对象的过程。此方法将可变数组放入 nameTable 字典中,并将其与 NSNibTopLevelObjects 关键字相关联。 nib 加载代码查找此数组对象,如果存在,将顶级对象放在其中。 因为每个对象在添加到数组之前以保留计数为1开始,所以简单地释放数组也不足以释放数组中的对象。 因此,此方法向每个对象发送发送消息,以确保数组是唯一持有对它们的引用的实体。</p>
<p>清单 1-4 Getting the top-level objects from a nib file at runtime</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (NSArray*)loadMyNibFile
{
NSBundle* aBundle = [NSBundle mainBundle];
NSMutableArray* topLevelObjs = [NSMutableArray array];
NSDictionary* nameTable = [NSDictionary dictionaryWithObjectsAndKeys:
self, NSNibOwner,
topLevelObjs, NSNibTopLevelObjects,
nil];
if (![aBundle loadNibFile:@"myNib" externalNameTable:nameTable withZone:nil])
{
NSLog(@"Warning! Could not load myNib file.\n");
return nil;
}
// Release the objects so that they are just owned by the array.
[topLevelObjs makeObjectsPerformSelector:@selector(release)];
return topLevelObjs;
}
</code></pre></div></div>
<p>获取 iPhone 应用程序中的顶级对象要简单得多,如清单 1-2 所示。在 UIKit 框架中,NSBundle 的 loadNibNamed:owner:options: 方法自动返回一个包含顶级对象的数组。另外,在返回数组之前,对对象的保留计数进行调整,以便不需要向每个对象发送额外的释放消息。返回的数组是对象的唯一所有者。</p>
<h3 id="-使用-uinib-和-nsnib-加载-nib-文件"><a name="Loading-Nib-Files-Using-UINib-and-NSNib"></a> 使用 UINib 和 NSNib 加载 Nib 文件</h3>
<p>在要创建 nib 文件内容的多个副本的情况下,UINib(iOS)和 NSNib(OS X)类提供更好的性能。正常的 nib 加载过程涉及从磁盘读取 nib 文件,然后实例化其包含的对象。然而,使用 UINib 和 NSNib 类,从磁盘读取 nib 文件只要一次,并将内容存储在内存中。因为它们在内存中,创建连续的对象集需要更少的时间,因为它不需要访问磁盘。</p>
<p>使用 UINib 和 NSNib 类始终是一个两步的过程。首先,您创建一个类的实例,并使用 nib 文件的位置信息进行初始化。其次,您将实例化 nib 文件的内容以将对象加载到内存中。每次实例化 nib 文件时,都需要指定一个不同的 File’s Owner 对象并接收一组新的顶级对象。</p>
<p>清单 1-5 显示了使用 OS X 中的 NSNib 类加载 nib 文件的内容的一种方法。由instantiateNibWithOwner :topLevelObjects:方法返回给您的数组已经自动释放。如果您打算使用该数组任何一段时间,您应该复制一份。</p>
<p>清单 1-5 Loading a nib file using NSNib</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (NSArray*)loadMyNibFile
{
NSNib* aNib = [[NSNib alloc] initWithNibNamed:@"MyPanel" bundle:nil];
NSArray* topLevelObjs = nil;
if (![aNib instantiateNibWithOwner:self topLevelObjects:&topLevelObjs])
{
NSLog(@"Warning! Could not load nib file.\n");
return nil;
}
// Release the raw nib data.
[aNib release];
// Release the top-level objects so that they are just owned by the array.
[topLevelObjs makeObjectsPerformSelector:@selector(release)];
// Do not autorelease topLevelObjs.
return topLevelObjs;
}
</code></pre></div></div>
<h3 id="-在加载时替换代理对象"><a name="Replacing-Proxy-Objects-at-Load-Time"></a> 在加载时替换代理对象</h3>
<p>在 iOS 中,可以创建除 File’s Owner 之外的包括占位符对象的 nib 文件。 代理对象表示在 nib 文件外部创建但与 nib 文件内容有某种连接的对象。 代理通常用于支持 iPhone 应用程序中的导航控制器。 当使用导航控制器时,您通常将文件的所有者对象连接到某些常见对象(如应用程序委托)。 因此,代理对象表示导航控制器对象层次结构中已经加载到内存中的部分,因为它们是以编程方式创建的(也可以从不同的 nib 文件加载)。</p>
<blockquote>
<p>注意:OS X nib 文件不支持自定义占位符对象(File’s Owner 除外)。</p>
</blockquote>
<p>您添加到 nib 文件的每个占位符对象必须具有唯一的名称。要为对象分配名称,请选择 Xcode 中的对象并打开 insepector 窗口。insepector 的“Attributes”窗格包含一个“Name”字段,用于指定占位符对象的名称。您分配的名称应描述对象的行为或类型,但实际上它可以是任何您想要的。</p>
<p>当您准备加载包含占位符对象的 nib 文件时,必须在调用 <code class="highlighter-rouge">loadNibNamed:owner:options:method</code> 时指定任何代理的替换对象。此方法的options 参数接受附加信息的字典。您可以使用此字典传递有关占位符对象的信息。字典必须包含 UINibExternalObjects 键,其值是另一个包含每个占位符替换的名称和对象的字典。</p>
<p>清单 1-6 显示了一个 applicationDidFinishLaunching: 方法的示例版本,用于手动加载应用程序的主 nib 文件。因为应用程序的委托对象是由 UIApplicationMain 函数创建的,所以该方法在主 nib 文件中使用占位符(名称为“AppDelegate”)来表示该对象。代理字典存储占位符对象信息,并且选项字典包含该字典。</p>
<p>清单 1-6 Replacing placeholder objects in a nib file</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)applicationDidFinishLaunching:(UIApplication *)application
{
NSArray* topLevelObjs = nil;
NSDictionary* proxies = [NSDictionary dictionaryWithObject:self forKey:@"AppDelegate"];
NSDictionary* options = [NSDictionary dictionaryWithObject:proxies forKey:UINibExternalObjects];
topLevelObjs = [[NSBundle mainBundle] loadNibNamed:@"Main" owner:self options:options];
if ([topLevelObjs count] == 0)
{
NSLog(@"Warning! Could not load myNib file.\n");
return;
}
// Show window
[window makeKeyAndVisible];
}
</code></pre></div></div>
<p>有关 <code class="highlighter-rouge">loadNibNamed:owner:options:</code> 方法中选项字典的更多信息,请参阅 <em>NSBundle UIKit Additions Reference</em>。</p>
<h3 id="-访问-nib-文件的内容"><a name="Accessing-the-Contents-of-a-Nib-File"></a> 访问 Nib 文件的内容</h3>
<p>成功加载 nib 文件后,其内容就可以立即使用。如果您在 File’s Owner 中配置了 outlet 来指向 nib 文件中的对象,那么现在可以使用这些 outlet。如果您没有使用任何 outlet 配置 File’s Owner ,则应确保以某种方式获取对顶级对象的引用,以便稍后释放它们。</p>
<p>因为 outlets 在加载 nib 文件时填充实际对象,所以随后可以像您以编程方式创建的任何其他对象一样使用 outlet。例如,如果您有一个指向窗口的 outlet,则可以将窗口发送一个 <code class="highlighter-rouge">makeKeyAndOrderFront:</code> 消息,以在用户屏幕上显示该消息。完成使用
nib 文件中的对象后,您必须像任何其他对象一样释放它们。</p>
<blockquote>
<p>重要提示:使用这些对象后,您将负责释放加载的任何 nib 文件的顶级对象。不这样做是许多应用程序中内存泄漏的原因。释放顶级对象后,将 nib 文件中指向对象的任何 outlet 都清除为 nil (weak 属性的不需要手动处理),这是一个好主意。您应该清除与所有 nib 文件对象相关联的 outlet,而不仅仅是顶级对象。</p>
</blockquote>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h2 id="-连接-nib-文件中的菜单项"><a name="Connecting-Menu-Items-Across-Nib-Files"></a> 连接 Nib 文件中的菜单项</h2>
<p>OS X 应用程序菜单栏中的项目通常需要与许多不同的对象进行交互,包括应用程序的文档和窗口。问题是许多这些对象不能(或不应该)直接从主 nib 文件访问。主 nib 文件的 File’s Owner 始终设置为 NSApplication 类的一个实例。虽然您可能能够在主 nib 文件中实例化一些自定义对象,但这样做是不切实际或不必要的。在文档对象的情况下,直接连接到特定文档对象是不可能的,因为文档对象的数量可以动态地更改,甚至可以为 nil。</p>
<p>大多数菜单项将动作消息发送到以下之一:</p>
<ul>
<li>一个总是处理命令的固定对象</li>
<li>动态对象,如文档或窗口</li>
</ul>
<p>消息传递给固定对象是一个相对简单的过程,通常最好通过应用程序委托来处理。应用程序委托对象在运行应用程序时协助 NSApplication 对象,并且是少数几个包括在主 nib 文件的。如果菜单项是指应用程序级命令,则可以直接在应用程序委托中实现该命令,或者只需让代理将消息转发到应用程序其他位置的相应对象。</p>
<p>如果您有一个菜单项作用于最前面的窗口的内容,则需要将菜单项链接到第一个响应者占位符对象。如果与菜单项相关联的操作方法特定于您的一个对象(而不是由Cocoa 定义),则必须在创建连接之前将该操作添加到第一响应程序。</p>
<p>创建连接后,您需要在自定义类中实现操作方法。该对象还应实现validateMenuItem: 方法,以在适当的时间启用菜单项。有关响应链如何处理命令的更多信息,请参阅 <a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/EventOverview/Introduction/Introduction.html#//apple_ref/doc/uid/10000060i"><em>Cocoa Event Handling Guide</em></a>。</p>
<p><a href="#table-content">↑目录</a></p>
<hr />Ampire_Dansmile47777@gmail.com译者注:本文是对 Apple 官方文档的翻译,原文地址为:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html使用 libclang 实现 iOS 代码中的明文加密2017-05-22T00:00:00+00:002017-05-22T00:00:00+00:00https://danleechina.github.io/use-libclang-in-xcode<h1 id="前言">前言</h1>
<p>之前写过一个小工具 <a href="https://github.com/danleechina/mixplaintext">MixPlainText</a>,可以将 Xcode 工程代码中所有的明文加密。原理是使用正则表达式来提取代码中所有的明文。现在介绍一种新的更加完善、更有拓展性的方法。用到的工具主要是 <code class="highlighter-rouge">libclang</code>。</p>
<p>这里我已经将所有代码、配置上传到 GitHub 了,所以有兴趣也可以直接下载代码查看,地址 <a href="https://github.com/danleechina/UsingLibClang">UsingLibClang</a></p>
<h1 id="配置-xcode-工程">配置 Xcode 工程</h1>
<p>由于我是用 C 语言来调用 <code class="highlighter-rouge">libclang</code> 的。所以这里介绍一下怎么配置 Xcode 工程来使用这个库。当然你也可以用 Python 来使用这个库,具体方式可以看这个文章 <a href="http://eli.thegreenplace.net/2011/07/03/parsing-c-in-python-with-clang">Parsing C++ in Python with Clang</a></p>
<ol>
<li>去 <a href="http://llvm.org/svn/llvm-project/cfe/trunk/include/clang-c/">http://llvm.org/svn/llvm-project/cfe/trunk/include/clang-c/</a> 下载所有头文件。</li>
<li>工程中添加上面下载的头文件</li>
<li>点击工程配置中的 ‘Build Phases’,打开 ‘Link Binary With Libraries’,点击 ‘+’ 号,然后选择 ‘Add other…’。</li>
<li>使用快捷键 ⌘⇧G 打开 <code class="highlighter-rouge">/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libclang.dylib</code>,将 Xcode 自带的 libclang.dylib 添加到环境中。</li>
<li>转到 ‘Build Setting’。</li>
<li>
<p>添加一个新的 ‘Runpath search paths’:‘<code class="highlighter-rouge">$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib</code>’</p>
<p>这里的 <code class="highlighter-rouge">$(DEVELOPER_DIR)</code> 指代 <code class="highlighter-rouge">/Applications/Xcode.app/Contents/Developer</code></p>
</li>
<li>
<p>添加一个新的 ‘header search paths’:<code class="highlighter-rouge">$(SRCROOT)/path_to_clang_header_file</code></p>
<p>注意这里的 <code class="highlighter-rouge">$(SRCROOT)</code> 指代工程所在的目录。后面的具体路径就是上面下载的头文件的位置。</p>
</li>
<li>添加新的 ‘Library Search Paths’:<code class="highlighter-rouge"> $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib</code></li>
<li>将 <code class="highlighter-rouge">Enable Modules (C and Objective-C)</code> 设置为 NO</li>
</ol>
<h1 id="使用-libclang-api">使用 <code class="highlighter-rouge">libclang</code> API</h1>
<p>代码如下,必要地方有注释说明:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
#import <Foundation/Foundation.h>
#include <cstdio>
#include <string>
#include <cstdlib>
// libclang 公开 API 均在这里
#include "clang-c/Index.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 一个编译单元,通常是一个文件
CXTranslationUnit tu;
// 一个 index 可以包含多个编译单元
CXIndex index = clang_createIndex(1, 1);
// 需要分析混淆的 Objective C 源代码文件
const char *filePath = "/path_to_OC_code/HomeViewController.m";
//
tu = clang_parseTranslationUnit(index, filePath, NULL, 0, nullptr, 0, 0);
if (!tu) {
printf("Couldn't create translation unit");
return 1;
}
// 根 cursor
CXCursor rootCursor = clang_getTranslationUnitCursor(tu);
// 一个个经过词法分析以后得到的 token
CXToken *tokens;
unsigned int numTokens;
CXCursor *cursors = 0;
CXSourceRange range = clang_getCursorExtent(rootCursor);
// 获取所有的 token
clang_tokenize(tu, range, &tokens, &numTokens);
cursors = (CXCursor *)malloc(numTokens * sizeof(CXCursor));
// 获取每个 token 对应的 cursor
clang_annotateTokens(tu, tokens, numTokens, cursors);
// 遍历 token
for(int i=0; i < numTokens; i++) {
CXToken token = tokens[i];
CXCursor cursor = cursors[i];
CXString tokenSpelling = clang_getTokenSpelling(tu, token);
CXString cursorSpelling = clang_getCursorSpelling(cursor);
const char *tokenName = clang_getCString(tokenSpelling);
if (CXToken_Literal == clang_getTokenKind(token) // 是明文的 token
&& CXCursor_PreprocessingDirective != cursor.kind // 排除预编译 token
&& strlen(tokenName) >= 2 // 排除所有非字符串 token
&& tokenName[0] == '\"'
&& tokenName[strlen(tokenName) - 1] == '\"' ) {
// Do some replacing.
NSData *content = [[NSFileManager defaultManager] contentsAtPath:[NSString stringWithUTF8String:filePath]];
NSString *contentString = [[NSString alloc] initWithData:content encoding:NSUTF8StringEncoding];
// stringToBeResplaced 是需要被混淆的代码
NSString *stringToBeResplaced = [NSString stringWithUTF8String:tokenName];
// stringToBePutted 是经过混淆的代码
NSString *stringToBePutted = @"\"Hello\"";
contentString = [contentString stringByReplacingOccurrencesOfString:stringToBeResplaced withString:stringToBePutted];
// 将经过混淆的代码写回原文件
[contentString writeToFile:[NSString stringWithUTF8String:filePath] atomically:YES encoding:NSUTF8StringEncoding error:nil];
printf("\t%d\t%s\n", cursor.kind, tokenName);
}
// 释放内存
clang_disposeString(tokenSpelling);
clang_disposeString(cursorSpelling);
}
// 释放内存
clang_disposeTokens(tu, tokens, numTokens);
clang_disposeIndex(index);
clang_disposeTranslationUnit(tu);
free(cursors);
}
return 0;
}
</code></pre></div></div>
<h1 id="使用方式">使用方式</h1>
<p>建议将上述代码编译生成的二进制可执行文件,放到您需要混淆的工程里面。添加一个新的 ‘Run path’,调用这个二进制文件处理每个你想要处理的源代码。</p>
<h1 id="参考资料">参考资料</h1>
<ol>
<li><a href="http://eli.thegreenplace.net/2011/07/03/parsing-c-in-python-with-clang">Parsing C++ in Python with Clang</a></li>
<li><a href="https://github.com/burnflare/libclang-experiments">libclang experiments</a></li>
<li><a href="http://llvm.org/devmtg/2010-11/">Apple libclang video</a></li>
<li><a href="https://clang.llvm.org/doxygen/group__CINDEX.html">liblcang API</a></li>
</ol>Ampire_Dansmile47777@gmail.com前言一个实现极为简单的约束框架2017-05-02T00:00:00+00:002017-05-02T00:00:00+00:00https://danleechina.github.io/a-very-simple-encapsulation-of-autolayout<h1 id="目录"><a name="table-content"></a>目录</h1>
<ul>
<li><a href="#reason-for-this">起因</a></li>
<li><a href="#result-for-this">结果</a></li>
<li><a href="#how-to-encapsulate-autolayout">如何封装 <code class="highlighter-rouge">autolayout</code></a>
<ul>
<li><a href="#problem-about-original-api">使用原生约束 API 的问题</a></li>
<li><a href="#using-masonry">使用 <code class="highlighter-rouge">Masonry</code></a></li>
<li><a href="#using-dlsimpleautolayout">使用 <code class="highlighter-rouge">DLSimpleAutolayout</code></a></li>
<li><a href="#how-to-implement-DLSimpleAutolayout"><code class="highlighter-rouge">DLSimpleAutolayout</code> 实现原理</a>
<ul>
<li><a href="#principle-autolayout">基本原理</a></li>
<li><a href="#principle-DLSimpleViewAttribute"><code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现之 <code class="highlighter-rouge">DLSimpleViewAttribute</code></a></li>
<li><a href="#principle-NSArray-category"><code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现之 <code class="highlighter-rouge">NSArray</code> 类别</a></li>
<li><a href="#principle-UIView-category"><code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现之 <code class="highlighter-rouge">UIView</code> 类别</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="-起因"><a name="reason-for-this"></a> 起因</h1>
<p>开发 iOS SDK 经常需要注意引入的开源代码或者定义的类名和方法名,可能会和用户代码冲突。所以一般的做法是类名,类别方法名加前缀。</p>
<p>最近在开发 SDK 的时候需要写一点界面,所以在思考用什么来布局。考虑使用 <code class="highlighter-rouge">Masonry</code>。但是引入 <code class="highlighter-rouge">Masonry</code> 需要对 <code class="highlighter-rouge">Masonry</code> 里面所有类、类别方法加前缀,成本太大,引入错误风险太高。同时 SDK 里面的界面通常比较简单,数量也比较少,引入一个 <code class="highlighter-rouge">Masonry</code> 反而会增大编译出来的二进制文件大小,不太值得。所以我就在思考如何设计一个代码精简的,类似 <code class="highlighter-rouge">Masonry</code> 的约束框架呢?</p>
<p><a href="#table-content">↑目录</a></p>
<h1 id="-结果"><a name="result-for-this"></a> 结果</h1>
<p>在阅读了 <code class="highlighter-rouge">Masonry</code> 的代码之后,我决定仿照它来重新写一个精简的约束框架,这里的精简是代码量极少。所以就有了后面的 <code class="highlighter-rouge">DLSimpleAutolayout</code>。这个约束框架代码总量大约只有300行。但是使用方式基本和 <code class="highlighter-rouge">Masonry</code> 类似,下面是 <code class="highlighter-rouge">DLSimpleAutolayout</code> 写出来的布局代码样式。</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
@[
view1.dl_top.equalTo(superview).offset(padding.top),
view1.dl_left.equalTo(superview).offset(padding.left),
view1.dl_bottom.equalTo(superview).offset(-padding.bottom),
view1.dl_right.equalTo(superview).offset(-padding.right),
].dl_constraint_install();
</code></pre></div></div>
<p><a href="#table-content">↑目录</a></p>
<hr />
<h1 id="-如何封装-autolayout"><a name="how-to-encapsulate-autolayout"></a> 如何封装 <code class="highlighter-rouge">autolayout</code></h1>
<p>下面我会循序渐进地讲解为什么,以及如何封装 <code class="highlighter-rouge">autolayout</code></p>
<h2 id="-使用原生约束-api-的问题"><a name="problem-about-original-api"></a> 使用原生约束 API 的问题</h2>
<p>在 iOS 上面使用原生约束写代码是极为繁琐的一件事。比如下面这样的原生约束代码(来自 <a href="https://github.com/SnapKit/Masonry/blob/master/README.md">Masonry README.md</a>):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UIView *superview = self.view;
UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[superview addConstraints:@[
//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-padding.bottom],
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],
]];
</code></pre></div></div>
<p>问题出在苹果提供的那个特别长的函数。当你的视图多的时候,像上面这样的代码就会越来越庞大,可读性也越来越低。</p>
<p><a href="#table-content">↑目录</a></p>
<h2 id="-使用-masonry"><a name="using-masonry"></a> 使用 <code class="highlighter-rouge">Masonry</code></h2>
<p>那么如何更加优雅的使用 <code class="highlighter-rouge">autolayout</code> 呢?绝大多数的 iOS 开发如果使用 <code class="highlighter-rouge">autolayout</code> 来布局视图的话,肯定或多或少的听说过 <a href="https://github.com/SnapKit/Masonry"><code class="highlighter-rouge">Masonry</code></a> (如果是 <code class="highlighter-rouge">Swift</code> 版本的话,官方推荐 <a href="https://github.com/SnapKit/SnapKit"><code class="highlighter-rouge">SnapKit</code></a>)。</p>
<p>利用 <code class="highlighter-rouge">Masonry</code>,我们可以将上面约束代码写成下面这样(来自 <a href="https://github.com/SnapKit/Masonry/blob/master/README.md">Masonry README.md</a>):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
make.left.equalTo(superview.mas_left).with.offset(padding.left);
make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];
</code></pre></div></div>
<p><code class="highlighter-rouge">Masonry</code> 会帮助我们将约束添加到适当的视图上(比如自身或者和另一个视图的公共祖先),同时也会帮助我们设置相应视图的 <code class="highlighter-rouge">translatesAutoresizingMaskIntoConstraints</code> 为 <code class="highlighter-rouge">NO</code>。</p>
<p><a href="#table-content">↑目录</a></p>
<h2 id="-使用-dlsimpleautolayout"><a name="using-dlsimpleautolayout"></a> 使用 <code class="highlighter-rouge">DLSimpleAutolayout</code></h2>
<p><a href="https://github.com/danleechina/DLSimpleAutolayout"><code class="highlighter-rouge">DLSimpleAutolayout</code></a> 是一个使用方式类似 <code class="highlighter-rouge">Masonry</code> 的约束框架,下面是 <code class="highlighter-rouge">DLSimpleAutolayout</code> 的约束代码(等价于上面的 <code class="highlighter-rouge">Masonry</code> 代码):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
@[
view1.dl_top.equalTo(superview).offset(padding.top),
view1.dl_left.equalTo(superview).offset(padding.left),
view1.dl_bottom.equalTo(superview).offset(-padding.bottom),
view1.dl_right.equalTo(superview).offset(-padding.right),
].dl_constraint_install();
</code></pre></div></div>
<p>是不是看起来一摸一样?!!</p>
<p>但是 <code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现极为简单,只有一个头文件和一个实现文件,合计代码行数只有300行左右。</p>
<p><a href="#table-content">↑目录</a></p>
<h2 id="-dlsimpleautolayout-实现原理"><a name="how-to-implement-DLSimpleAutolayout"></a> <code class="highlighter-rouge">DLSimpleAutolayout</code> 实现原理</h2>
<h3 id="-基本原理"><a name="principle-autolayout"></a> 基本原理</h3>
<p>首先你需要了解布局的<a href="https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html#//apple_ref/doc/uid/TP40010853-CH9-SW1">基本原理</a></p>
<p>简单的说每一个相等关系的约束最后会被 Apple 处理成一条类似下面的等式:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>view1.left = 2.0 * view2.left + 8.0
</code></pre></div></div>
<p>上面这条公式的意思是 <code class="highlighter-rouge">view1</code> 的 <code class="highlighter-rouge">left</code> 约束等于 <code class="highlighter-rouge">view2</code> 的 <code class="highlighter-rouge">left</code> 值乘以 2 加 8。这里的 2 叫 multiplier,8 叫 constant,view1 叫 first item,view2 叫 second item,也就是分别对应</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;
</code></pre></div></div>
<p>这个原生 API 的几个参数。无论是 <code class="highlighter-rouge">DLSimpleAutolayout</code> 还是 <code class="highlighter-rouge">Masonry</code> 的实现,基本上都是对这个 API 的封装,同时添加了一些其他代码,方便使用者使用约束。</p>
<p><a href="#table-content">↑目录</a></p>
<h3 id="-dlsimpleautolayout-的实现之-dlsimpleviewattribute"><a name="principle-DLSimpleViewAttribute"></a> <code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现之 <code class="highlighter-rouge">DLSimpleViewAttribute</code></h3>
<p><code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现其实参考了 <code class="highlighter-rouge">Masonry</code>,比如 <code class="highlighter-rouge">DLSimpleViewAttribute</code> 类似与 <code class="highlighter-rouge">MASViewAttribute</code>。<code class="highlighter-rouge">DLSimpleViewAttribute</code> 类型接口如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">@interface</span> <span class="nc">DLSimpleViewAttribute</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="p">(</span><span class="o">^</span><span class="n">equalTo</span><span class="p">)(</span><span class="n">id</span> <span class="n">view</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="p">(</span><span class="o">^</span><span class="n">lessThanOrEqualTo</span><span class="p">)(</span><span class="n">id</span> <span class="n">view</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="p">(</span><span class="o">^</span><span class="n">greaterThanOrEqualTo</span><span class="p">)(</span><span class="n">id</span> <span class="n">view</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="p">(</span><span class="o">^</span><span class="n">multipliedBy</span><span class="p">)(</span><span class="n">CGFloat</span> <span class="n">multipier</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="p">(</span><span class="o">^</span><span class="n">offset</span><span class="p">)(</span><span class="n">CGFloat</span> <span class="n">offset</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">NSLayoutConstraint</span> <span class="o">*</span><span class="p">(</span><span class="o">^</span><span class="n">install</span><span class="p">)();</span>
<span class="k">@end</span>
</code></pre></div></div>
<p>也就是对应约束的3种大小关系,同时可以对 constant 进行 offset 操作(对应设置 constant),对 second item 有 multipliedBy 操作(对应设置 multiplier),最后添加约束的时候需要手动 <code class="highlighter-rouge">install()</code> 。比如下面这样:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
view1.dl_top.equalTo(superview).offset(padding.top).install();
view1.dl_left.equalTo(superview).offset(padding.left).install();
view1.dl_bottom.equalTo(superview).offset(-padding.bottom).install();
view1.dl_right.equalTo(superview).offset(-padding.right).install();
</code></pre></div></div>
<p>当调用 <code class="highlighter-rouge">install()</code> 的时候,<code class="highlighter-rouge">DLSimpleAutolayout</code> 会根据当前配置的信息,来生成一个约束,同时将约束添加到自身或者是和 second item 的最近的公共视图上。并设置自身的 <code class="highlighter-rouge">translatesAutoresizingMaskIntoConstraints</code> 为 <code class="highlighter-rouge">NO</code>。代码如下所示:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (NSLayoutConstraint * (^)())install {
return ^id(){
// 设置为 NO
((UIView *)self.firstItem).translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutConstraint *constaraint = [NSLayoutConstraint constraintWithItem:self.firstItem
attribute:self.firstAttribute
relatedBy:self.relation
toItem:self.secondItem
attribute:self.secondAttribute
multiplier:self.multiplier
constant:self.constant];
if (self.secondItem == nil) {
[self.firstItem addConstraint:constaraint];
} else {
// 寻找最近的公共祖先
UIView *closestCommonSuperview = [self dl_ClosestCommonSuperview:self.firstItem view2:self.secondItem];
[closestCommonSuperview addConstraint:constaraint];
}
return constaraint;
};
}
</code></pre></div></div>
<p><a href="#table-content">↑目录</a></p>
<h3 id="-dlsimpleautolayout-的实现之-nsarray-类别"><a name="principle-NSArray-category"></a> <code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现之 NSArray 类别</h3>
<p>考虑到方便在每个属性后面自动使用 <code class="highlighter-rouge">install()</code>,我对 NSArray 添加了一个类别,封装了这个操作。所以无需在每个 <code class="highlighter-rouge">DLSimpleViewAttribute</code> 对象后调用 <code class="highlighter-rouge">install()</code>,只需要将这些对象放到一个 NSArray 里面,对这个 array 调用 <code class="highlighter-rouge">dl_constraint_install()</code> 即可。这种类似语法糖的写法也方便了使用者更好的组织、调整自己的布局代码。</p>
<p>该 NSArray 类别公开接口如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">@interface</span> <span class="nc">NSArray</span> <span class="p">(</span><span class="nl">DLSimpleAutoLayout</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">NSArray</span><span class="o"><</span><span class="n">NSLayoutConstraint</span> <span class="o">*></span> <span class="o">*</span> <span class="p">(</span><span class="o">^</span><span class="n">dl_constraint_install</span><span class="p">)();</span>
<span class="k">@end</span>
</code></pre></div></div>
<p>写出来的代码类似下面这样:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
@[
view1.dl_top.equalTo(superview).offset(padding.top),
view1.dl_left.equalTo(superview).offset(padding.left),
view1.dl_bottom.equalTo(superview).offset(-padding.bottom),
view1.dl_right.equalTo(superview).offset(-padding.right),
].dl_constraint_install();
</code></pre></div></div>
<p><a href="#table-content">↑目录</a></p>
<h4 id="-dlsimpleautolayout-的实现之-uiview-类别"><a name="principle-UIView-category"></a> <code class="highlighter-rouge">DLSimpleAutolayout</code> 的实现之 <code class="highlighter-rouge">UIView</code> 类别</h4>
<p><code class="highlighter-rouge">DLSimpleAutolayout</code> 为 <code class="highlighter-rouge">UIView</code> 添加了类别,就是类似 <code class="highlighter-rouge">dl_xxx</code> 这样的只读属性。
并且每个 view 返回的 <code class="highlighter-rouge">dl_xxx</code> 都是一个 <code class="highlighter-rouge">DLSimpleViewAttribute</code> 类型。该类别的接口如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="k">@interface</span> <span class="nc">UIView</span> <span class="p">(</span><span class="nl">DLSimpleAutoLayout</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_left</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_top</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_right</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_bottom</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_leading</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_trailing</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_width</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_height</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_centerX</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_centerY</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_baseline</span><span class="p">;</span>
<span class="cp">#if TARGET_OS_IPHONE || TARGET_OS_TV
</span>
<span class="k">@property</span> <span class="p">(</span><span class="n">nonatomic</span><span class="p">,</span> <span class="n">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_leftMargin</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_rightMargin</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_topMargin</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_bottomMargin</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_leadingMargin</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_trailingMargin</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_centerXWithinMargins</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">strong</span><span class="p">,</span> <span class="n">readonly</span><span class="p">)</span> <span class="n">DLSimpleViewAttribute</span> <span class="o">*</span><span class="n">dl_centerYWithinMargins</span><span class="p">;</span>
<span class="cp">#endif
</span><span class="k">@end</span>
</code></pre></div></div>
<p><a href="#table-content">↑目录</a></p>
<p>到此为止,<code class="highlighter-rouge">DLSimpleAutolayout</code> 基本上就实现了。具体实现的话可以看代码 <a href="https://github.com/danleechina/DLSimpleAutolayout/tree/master/DLSimpleAutolayoutDemo/DLSimpleAutolayout"><code class="highlighter-rouge">DLSimpleAutolayout</code></a></p>
<hr />Ampire_Dansmile47777@gmail.com目录UIWebView 和 WKWebView2017-03-30T00:00:00+00:002017-03-30T00:00:00+00:00https://danleechina.github.io/things-to-know-about-WKWebView-UIWebView<p>公司上一个版本 x.x.x 的 app 发布之后,OOM(Out of Memory) free seesion 的值直线下降。目前维持在 73% 左右。一直在思考为什么这
个值下降的这么严重。直到发现这篇文章,<a href="(https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/)">资料1</a>。意识到是 UIWebView 的内存泄漏导致的。</p>
<p>先说一下为什么之前 UIWebView 内存泄漏但是 fabric 显示的 OOM free session 值却并不低。</p>
<p>iOS 8 设备有两个奇怪的 bug</p>
<ol>
<li><a href="https://bugs.webkit.org/show_bug.cgi?id=139654">JavaScriptCore bug</a></li>
<li><a href="http://stackoverflow.com/questions/12757237/websafeforwarder-forwardinvocation-crashes">UIWebView bug</a></li>
</ol>
<p>第一个 bug 已经在 iOS 9 上面解决了(在 iOS 8 设备上,如果您使用了 documentView.webView.mainFrame.javaScriptContext 获取了网页的 JSContext,通过频繁的进入和退出网页界面,这个 crash 是毕现的)。但是第二 bug iOS 10 也还存在。</p>
<p>关于这两个 bug 网上的资料都不多。从搜到的仅有的资料来看,后者很可能和 UIWebView 的内存泄漏有关。至少从 crash 堆栈来看,OC 运行时找不到转发的类。</p>
<p>所以我的想法就很简单了。直接干涉 UIWebView delegate 的方法转发。基于 NSProxy 可以很好的实现一个代理 delegate。具体实现可以看这个 <a href="http://petersteinberger.com/blog/2013/smart-proxy-delegation/">Smart Proxy Delegation</a>,或者有兴趣可以直接看 AsyncDisplay 和 YYKit 的源代码,里面都有具体的实现,AsyncDisplay 的源代码中注释很丰富,强烈推荐。</p>
<p>换了这种方式之后,这两个 bug 导致的 crash 就再也没出现了(意外的也修复好了在 iOS 8 上的 JavaScriptCore crash)。</p>
<p>但是 OOM free session 值却开始直线下滑了,到现在稳定在 73% 左右。联想到之前这两个 crash 占比达到 80% 左右,我猜想之前的 crash 都转换到了 OOM 上了。</p>
<p>由于 UIWebView 内存泄漏实在太过严重,我决定切换到 WKWebView。</p>
<h1 id="如何兼容-ios-7">如何兼容 iOS 7</h1>
<p>切换到 WKWebView 遇到的第一个问题是怎么同时也支持 iOS 7,这是我们的产品经理要求的。</p>
<p>先说一点题外话,苹果设备默认是支持老版本的系统安装老版本的 app 的。也就是说如果您的 app 之前是支持 iOS 7 的,但是下一个版本只支持最低 iOS 8了,那么一个 iOS 7 的用户去 App Store 下载您的 app 的时候,苹果会自动弹窗提醒用户将会安装该 app 的支持 iOS 7的老版本。</p>
<p>也就是说只支持最低 iOS 8设备并不会让你损失 iOS 7 的用户,如果您之前是支持 iOS 7的话。</p>
<p>但是万一真的想支持 iOS 7同时又能实现新的 iOS 特性怎么办?两个步骤:</p>
<ol>
<li>在项目配置的 build phases 中添加你需要的 framework(比如 WebKit.framework),如果该 framework 不支持您的最低 iOS 版本要求,设置 framework 的 status 为 weak</li>
<li>
<p>当你需要用到该 framework 的类的时候(比如 WKWebView),首先在实现文件中导入 framework 的主头文件</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> #import <WebKit/WebKit.h>
</code></pre></div> </div>
<p>当你要生成 WKWebView 对象的时候,先判断类是否存在,再决定是用 WKWebView 还是 UIWebView</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> id webView = nil;
if([WKWebView class]) {
webView = [WKWebView new];
} else {
webView = [UIWebView new];
}
</code></pre></div> </div>
<p>同时在实现文件中实现 WKWebView 和 UIWebView 的 delegate。实际运行的时候,运行时会调用正确的 delegate 方法(WKWebView 或者 UIWebView)。</p>
</li>
</ol>
<h1 id="如何使用-webview">如何使用 WebView</h1>
<p>这个比较简单,我说一下一些有用的实现方式:</p>
<ol>
<li>KVO 监听 <code class="highlighter-rouge">estimatedProgress</code> 来设置进度条的值</li>
<li>KVO 监听 <code class="highlighter-rouge">title</code> 来设置比如说导航栏标题</li>
<li>通常设置 webView(无论是 UIWebView 还是 WKWebView) 的 delegate 的时候我会用 proxy 进行封装,见资料2</li>
<li>在 UIWebView 里面,我会是用 JavaScriptCore 来让页面的 JS 调用 OC 本地方法</li>
<li>
<p>在 WKWebView 里面,我会用 WKWebViewConfiguration 添加一个脚本处理对象给 WKUserContentController,页面 JS 可以通过调用 <code class="highlighter-rouge">window.webkit.messageHandlers.<your handler name>.postMessage({data: data, id: handle});</code>将数据传给客户端本地。代码如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> WKWebViewConfiguration *config = [WKWebViewConfiguration new];
config.userContentController = [WKUserContentController new];
[config.userContentController addScriptMessageHandler:<Your Handler Object> name:@"<your handler Name>"];
WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0,0,0,0) configuration:config];
</code></pre></div> </div>
<p>然后在<Your Handler="" Object="">上实现 WKScriptMessageHandler ,处理 js 传过来的数据。参见资料3</Your></p>
</li>
</ol>
<h1 id="处理-cookie">处理 Cookie</h1>
<p>UIWebView 的 cookie 和 NSHTTPCookieStorage 的单例对象是互通的。所以 UIWebView 的 cookie 基本上处理比较简单。NSHTTPCookieStorage 的单例对象会自动将 cookie 添加到相应的 UIWebView 的 request 上。每次加载完页面以后 UIWebView 上生成的 cookie 也会自动保存到 NSHTTPCookieStorage 单例对象。</p>
<p>WKWebView 的 cookie 处理起来就很麻烦了。为了将 NSHTTPCookieStorage 单例对象的 cookie 设置到 WKWebView,需要在 loadRequest 的时候将 cookie 设置到 request 的 http 头上。代码如下:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- (void)loadRequest:(NSURLRequest *)req {
NSMutableURLRequest *request = req.mutableCopy;
NSString *urlString = request.URL.absoluteString;
if (urlString && [urlString rangeOfString:@"www.xxx.com"].location != NSNotFound) {
NSHTTPCookie *cookie = // specific cookie to be set;
NSArray* cookies = [NSArray arrayWithObjects: cookie, nil];
NSDictionary * headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
[request setAllHTTPHeaderFields:headers];
}
[(WKWebView *)self.wkWebView loadRequest:request];
}
</code></pre></div></div>
<p>如果有 AJAX 的 request 的话需要设置脚本(原理就是将本地 cookie 通过 js 设置到页面的 document.cookie):</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc]
initWithSource: @"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
// again, use stringWithFormat: in the above line to inject your values programmatically
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
</code></pre></div></div>
<p>由于采用了新的实现机制,WKWebView 获取的 cookie 不会被设置到 NSHTTPCookieStorage 单例对象上。如果想要获取 WKWebView 上面的 cookie,同样可以是用 document.cookie。但是这种方式无法获取到 httpOnly 的 cookie。为了获取到所有的 WKWebView 上的 cookie。可以使用 NSURLProtocol,在 NSURLProtocol 得到调用的时候去取所有的相关 cookie。但是 WKWebView 默认并不走 NSURLProtocol,需要使用私有 API:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> Class cls = NSClassFromString(@"WKBrowsingContextController");
SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
if ([(id)cls respondsToSelector:sel]) {
// 把 http 和 https 请求交给 NSURLProtocol 处理
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[(id)cls performSelector:sel withObject:@"http"];
[(id)cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
}
[NSURLProtocol registerClass:[CustomURLProtocol class]];
</code></pre></div></div>
<p>原理请看<a href="https://blog.yeatse.com/2016/10/26/support-nsurlprotocol-in-wkwebview/">让 WKWebView 支持 NSURLProtocol</a></p>
<h1 id="参考资料">参考资料</h1>
<ol>
<li><a href="https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/">OOM Crash 分析</a></li>
<li><a href="http://petersteinberger.com/blog/2013/smart-proxy-delegation/">Smart Proxy Delegation</a></li>
<li><a href="http://stackoverflow.com/questions/29249132/wkwebview-complex-communication-between-javascript-native-code">在 WKWebView 上 js 传递数据给 iOS </a></li>
<li><a href="http://stackoverflow.com/questions/26573137/can-i-set-the-cookies-to-be-used-by-a-wkwebview">Set cookie to WKWebView</a></li>
<li><a href="https://blog.yeatse.com/2016/10/26/support-nsurlprotocol-in-wkwebview/">让 WKWebView 支持 NSURLProtocol</a></li>
</ol>Ampire_Dansmile47777@gmail.com公司上一个版本 x.x.x 的 app 发布之后,OOM(Out of Memory) free seesion 的值直线下降。目前维持在 73% 左右。一直在思考为什么这 个值下降的这么严重。直到发现这篇文章,资料1。意识到是 UIWebView 的内存泄漏导致的。