-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
217 lines (153 loc) · 225 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[iOS 一对多 delegate 的简单应用]]></title>
<url>%2F2019%2F02%2F26%2FiOS%20%E4%B8%80%E5%AF%B9%E5%A4%9A%20delegate%20%E7%9A%84%E7%AE%80%E5%8D%95%E5%BA%94%E7%94%A8%2F</url>
<content type="text"><![CDATA[title: "iOS 一对多 delegate 的简单应用" date: 2019-02-26 tags: [delegate,iOS] categories: [] permalink: keywords: custom_title: description: 现在回过头看这种实现很简单易懂,但是在之前固定思维的限制下,使用了大量繁杂、迂回且不可靠的方式,所以多看这些优秀框架的源码,可以获得很多思考、设计、解决问题的思路。 --- ### 背景 在之前我们尝试过 hook webView 的 delegate,可以查看【 [iOS 如何优雅地 hook 系统的 delegate 方法](https://www.daizi.me/2017/11/01/iOS%20%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%20hook%20%E7%B3%BB%E7%BB%9F%E7%9A%84%20delegate%20%E6%96%B9%E6%B3%95%EF%BC%9F/)】。当时的思路是,由于 delegate 只能是一对一的消息传递,后面设置的 delegate 对象会替换前面设置的 delegate 对象,因此在无入侵的情况下,需要通过 setDelegate 方法找到设置的 delegate 对象,然后 hook 该对象实现的 protocol 方法,这种方法迂回且比较容易出问题(过多操作运行时)。 现在我们可以通过更简单的方式来实现,在 hook webView 的初始化方法后,使用一对多委托方式,将 webView 的 delegate 设置为我们内部实现相关协议的对象,使得我们的 hook 代码不影响原始代码的 delegate 实现。 ### 启发 为什么突然想到可以这样来实现功能?其实启发是来源于优秀的开源框架 [WebViewJavascriptBridge](https://github.com/marcuswestin/WebViewJavascriptBridge) ,WebViewJavascriptBridge 可以简洁优雅地实现原生和 JS 通信。 WebViewJavascriptBridge 在 Native 端实现桥接功能的简化流程是: JS 向 Native 发消息是构造特殊的 url request,由 Native 端拦截并解析 URL 请求来实现通信。那么一定会需要 webView 拦截 URL(`webView:decidePolicyForNavigationAction:request:frame:decisionListener:`) 的回调来处理。 那么如何在 delegate 只能一对一传递的情况下,既保证原始的 delegate 实现,又能在 WebViewJavascriptBridge 内部实现 delegate 的回调? ### 走进源码 我们可以猜想,WebViewJavascriptBridge 一定使用了某种有效方式解决了这个问题。接下来让我们走进源码来看具体的实现: 1、在桥接类的初始化方法中,将 webView 实例对象传入,并设置其 delegate 为桥接类。(这时候如果外部的控制器提前设置了 delegate 会被覆盖失效) ```` WebViewJavascriptBridge* bridge; _bridge = [WebViewJavascriptBridge bridgeForWebView:webView]; ```` 2、如果调用方也想作为 delegate 代理,不能通过设置 webView 的 delegate 来实现,这样会覆盖上面桥接类的 delegate,导致桥接类无法正常工作,WebViewJavascriptBridge 的做法是将待设置 delegate 对象传入桥接类,并赋值给 `_webViewDelegate` (weak 变量,在 self 释放后可以随即设置为 nil,不会造成循环应用)。 ```` [_bridge setWebViewDelegate:self]; ```` ```` - (void)setWebViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate { _webViewDelegate = webViewDelegate; } ```` 3、在桥接类对应 delegate 方法回调时,调用 _webViewDelegate 相应的实现方法, ```` - (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { // another handler if (_webViewDelegate && [_webViewDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:request:frame:decisionListener:)]) { [_webViewDelegate webView:webView decidePolicyForNavigationAction:actionInformation request:request frame:frame decisionListener:listener]; } } ```` ### 扩展及总结 如果想实现不入侵地实现一对多 delegate,可以通过 hook webView 初始化方法以及 setDelegate 方法,并注意设置 delegate 的时机,防止被覆盖而失效。 另外,WebViewJavascriptBridge 的实现实际上是一对二的代理,虽然对我来说已经够用了。但如果有同学想扩展为一对多 delegate,可以尝试实现一个桩类,将桩类设置为 delegate,并使用数组管理其他想实现 delegate 协议的对象,实现一对多 delegate。 现在回过头看这种实现很简单易懂,但是在之前固定思维的限制下,使用了大量繁杂、迂回且不可靠的方式,所以多看这些优秀框架的源码,可以获得很多思考、设计、解决问题的思路。]]></content>
</entry>
<entry>
<title><![CDATA[构建第一个 Swift 区块链应用]]></title>
<url>%2F2018%2F12%2F08%2Fappcoda-blockchain-introduction%2F</url>
<content type="text"><![CDATA[title: "构建第一个 Swift 区块链应用" date: 2018-12-08 tags: [Swift,区块链] categories: [] permalink: appcoda-blockchain-introduction keywords: custom_title: description: 构建第一个 Swift 区块链应用 --- 原文链接=https://appcoda.com/blockchain-introduction/ 作者=Sai Kambampati 原文日期=2018-05-31 区块链作为一项革命性的技术,开始受到越来越多追捧。为什么呢?因为区块链是许多加密数字货币的底层技术,比如:比特币(BTC),以太坊(ETH)以及莱特币(LTC)。区块链具体是如何工作的?本篇教程会涵盖所有区块链相关的知识,还会教你如何构建 Swift “区块链”。下面让我们开始吧! ### 区块链的工作原理 顾名思义,区块链是一条由不同区块连接组成的链。每一个块包含三个信息:数据、哈希(hash)、以及前置区块的哈希。 **1、数据** – 由于应用场景不同,存储在区块中的数据由区块链的类型决定。例如,在比特币区块链中,存储的数据是交易信息:转账金额和交易双方的信息。 **2、哈希** – 你可以将哈希看做数字指纹,用来唯一标识一个区块及其数据。哈希的重要之处在于它是一个独特的字母数字代码,通常是 64 个字符。当一个区块被创建时,哈希也随之创建。当一个区块被修改,哈希也随之修改。因此,当你想要查看在区块上所做的任何变更时,哈希就显得非常重要。 **3、前置区块的哈希** – 通过存储前置区块的哈希,你可以还原每个区块连接成区块链的过程!这使得区块链安全性特别高。 我们来看下这张图片: ![区块链](https://appcoda.com/wp-content/uploads/2018/05/blockchain-explained.png) 你可以看到,每一个区块包含数据(图片中没有指明)、哈希以及前置区块的哈希。例如,黄色区块包含自身的哈希:H7s6,以及红色区块的哈希:8SD9。这样它们就构成了一条相互连接的链。现在,假如有一个黑客准备恶意篡改红色的区块。请记住,每当块以任何方式被篡改时,该区块的哈希都会改变!当下一个区块检查并发现前置哈希不一致时,黑客将无法访问它,因为他与前置区块的联系被切断了(译者注:即如果黑客想要要篡改一个区块的话,就需要把这个区块后面的所有区块都要改掉,而这个工作量是很难实现的)。 这使得区块链特别安全,几乎不可能回滚或者篡改任何数据。虽然哈希为保密和隐私提供了巨大的保障,但是还有两个更加安全妥当的措施让区块链更加安全:工作量证明(Proof-of-Work)以及智能合约(Smart Contracts)。本文我不会深入讲解,你可以[在这里](https://hackernoon.com/what-on-earth-is-a-smart-contract-2c82e5d89d26)了解更多相关知识。 区块链最后一个保证自身安全性的方式是基于其定位。和大多数存储在服务器和数据库的数据不同,区块链使用的是点对点(P2P)网络。P2P 是一种允许任何人加入的网络,并且该网络上的数据会分发给每一个接收者。 每当有人加入这个网络,他们就会获得一份区块链的完整拷贝。每当有人新建一个区块,就会广播给全网。在将该块添加到链之前,节点会通过几个复杂的程序确定该块是否被篡改。这样,所有人、所有地方都可以使用这个信息。如果你是 *HBO 美剧硅谷* 的粉丝,对此应该不会感到陌生。在该剧中,主演(Richard)使用一种相似的技术创造了新型互联网(译者注:有趣的是剧中还发行了区块链数字货币 PiedPaperCoin,感兴趣的童鞋可以刷一下这部剧)。 因为每个人都有区块链或者节点的一份拷贝,他们可以达成一种共识并决定哪部分区块是有效的。因此,如果你想要攻击某个区块,你必须同时攻击网络上 50% 以上的区块(译者:51% 攻击),使得你的区块可以追上并替换原区块链。所以区块链或许是过去十年所创造的最安全的技术之一。 ### 关于示例程序 现在你已经对区块链的原理有了初步的认识,那么我们就开始写示例程序吧!你可以在这里下载[原始项目](https://github.com/appcoda/BlockchainDemo/raw/master/BlockchainStarter.zip)。 如你所见,我们有两个比特币钱包。第一个账户 1065 有 500 BTC,而第二个账户 0217 没有 BTC。我们通过 send 按钮可以发送比特币到另外的账户。为了赚取 BTC,我们可以点击 Mine 按钮,可以获得 50 BTC 的奖励。我们主要工作是查看控制台输出,观察两个账户间的交易过程。 ![这里写图片描述](https://appcoda.com/wp-content/uploads/2018/05/blockchain-2.png) 在左侧导航栏可以看到两个很重要的类:`Block` 和 `Blockchain`。目前这两个类都是空实现,我会带着你们在这两个类中写入相关逻辑。下面让我们开始吧! ![这里写图片描述](https://appcoda.com/wp-content/uploads/2018/05/blockchain-3.png) ### 在 Swift 中定义区块 首先打开 `Block.swift` 并添加定义区块的代码。在此之前,我们需要定义区块是什么。前面我们曾定义过,区块是由三部分组成:哈希、实际记录的数据以及前置区块的哈希。当我们想要构建我们的区块链时,我们必须知道该区块是第一个还是第二个。我们可以很容易地在 Swift 的类中做如下定义: ``` var hash: String! var data: String! var previousHash: String! var index: Int! ``` 现在需要添加最重要的代码。我曾提过区块在被修改的情况下,哈希也会随之变化,这是区块链如此安全的特性之一。因此我们需要创建一个函数去生成哈希,该哈希由随机字母和数字组成。这个函数只需要几行代码: ``` func generateHash() -> String { return NSUUID().uuidString.replacingOccurrences(of: "-", with: "") } ``` `NSUUID`是一个代表通用唯一值的对象,并且可以桥接成 UUID。它可以快速地生成 32 个字符串。本函数生成一个 UUID,删除其中的连接符,然后返回一个 `String`,最后将结果作为区块的哈希。`Block.swift`现在就像下面: ![这里写图片描述](https://appcoda.com/wp-content/uploads/2018/05/blockchain-4.png) 现在我们已经定义好了 `Block` 类,下面来定义 Blockchain 类,首先切换到 `Blockchain.swift` 。 ### 在 Swift 中定义区块链 和之前一样,首先分析区块链的基本原理。用非常基础的术语来说,区块链只是由一连串的区块连接而成,也可以说是一个由多个条目组成的列表。这是不是听起来很熟悉呢?其实这就是数组的定义!而且这个数组是由区块组成的!接下来添加以下代码: ``` var chain = [Block]() ``` ``` 快速提示: 这个方法可以应用于计算机科学世界里的任何事物。如果你遇到大难题,可以尝试把它分解成若干个小问题,以此来建立起解决问题的方法,正如我们解决在 Swift 中如何添加区块和区块链的问题。 ``` 你会注意到数组里面是我们前面定义的 `Block` 类,这就是区块链所需要的所有变量。为了完成功能,我们还需要在类中添加两个函数。请尝试着根据我之前教过的方法解答这个问题。 > 区块链中的两个主要函数是什么? 我希望你能认真思考并回答这个问题!实际上,区块链的两个主要功能是创建创世区块(最初的始块),以及在其结尾添加新的区块。当然现在我不打算实现分叉区块链和添加智能合约的功能,但你必须了解这两个是基本功能!将以下代码添加到 `Blockchain.swift`: ``` func createGenesisBlock(data:String) { let genesisBlock = Block() genesisBlock.hash = genesisBlock.generateHash() genesisBlock.data = data genesisBlock.previousHash = "0000" genesisBlock.index = 0 chain.append(genesisBlock) } func createBlock(data:String) { let newBlock = Block() newBlock.hash = newBlock.generateHash() newBlock.data = data newBlock.previousHash = chain[chain.count-1].hash newBlock.index = chain.count chain.append(newBlock) } ``` 1、我们添加的第一个函数的作用是创建创世区块。为此,我们创建了一个以区块数据为入参的函数。然后定义了一个类型为 `Block` 的变量 `genesisBlock`,它拥有此前在 `Block.swift` 中定义的所有变量和函数。我们将 `generateHash()` 赋值给哈希,将输入的 `data` 参数赋值给数据。由于这是第一个区块,我们将前置区块的哈希设为 0000,这样我们就可以知道这是起始区块。最后我们将 `index` 设为 0,并将这个区块加入到区块链中。 2、我们创建的第二个函数适用于 `genesisBlock` 之后的所有区块,并且能创建剩余的区块。你会注意到它与第一个函数非常相似。唯一的区别是,我们将 `previousHash` 的值设置为前一个区块的哈希值,并将 `index` 设置为它在区块链中的位置。就这样,区块链已经定义好了!你的代码应该看起来跟下图一样! ![这里写图片描述](https://appcoda.com/wp-content/uploads/2018/05/blockchain-5.png) ### 钱包后端 现在切换到 `ViewController.swift`,我们会发现所有的 outlet 都已经连接好了。我们只需要处理交易,并将其输出到控制台。 然而在此之前,我们需要稍微研究一下比特币的区块链。比特币是由一个总账户产生的,我们将这个账号的编号称为 0000。当你挖到一个 BTC,意味着你解决了数学问题,因此会发行一定数量的比特币作为奖励。这提供了一个发币的高明方法,并且可以激励更多人去挖矿。在我们的应用,让我们把挖矿奖励设为 100 BTC。首先,在视图控制器中添加所需的变量: ``` let firstAccount = 1065 let secondAccount = 0217 let bitcoinChain = Blockchain() let reward = 100 var accounts: [String: Int] = ["0000": 10000000] let invalidAlert = UIAlertController(title: "Invalid Transaction", message: "Please check the details of your transaction as we were unable to process this.", preferredStyle: .alert) ``` 首先定义号码为 1065 和 0217 的两个账号。然后添加一个名为 `bitcoinChain` 的变量作为我们的区块链,并将 `reward` 设为 100。我们需要一个主帐户作为所有比特币的来源:即创世帐户 0000。里面有 1000 万个比特币。你可以把这个账户想象成一个银行,所有因奖励产生的 100 个比特币都经此发放到合法账户中。我们还定义了一个提醒弹窗,每当交易无法完成时就会弹出。 现在,让我们来编写几个运行时需要的通用函数。你能猜出是什么函数吗? 1、第一个函数是用来处理交易的。我们需要确保交易双方的账户,能够接收或扣除正确的金额,并将这些信息记录到我们的区块链中。 2、下一个函数是在控制台中打印整个记录 —— 它将显示每个区块及其中的数据。 3、最后一个是用于验证区块链是否有效的函数,通过校验下一个区块的 `previousHash`和上一个区块 `hash` 是否匹配。由于我们不会演示任何黑客方法,因此在我们的示例程序中,区块链是永远有效的。 ### 交易函数 下面是一个通用的交易函数,请在我们定义的变量下方输入以下代码: ``` func transaction(from: String, to: String, amount: Int, type: String) { // 1 if accounts[from] == nil { self.present(invalidAlert, animated: true, completion: nil) return } else if accounts[from]!-amount < 0 { self.present(invalidAlert, animated: true, completion: nil) return } else { accounts.updateValue(accounts[from]!-amount, forKey: from) } // 2 if accounts[to] == nil { accounts.updateValue(amount, forKey: to) } else { accounts.updateValue(accounts[to]!+amount, forKey: to) } // 3 if type == "genesis" { bitcoinChain.createGenesisBlock(data: "From: \(from); To: \(to); Amount: \(amount)BTC") } else if type == "normal" { bitcoinChain.createBlock(data: "From: \(from); To: \(to); Amount: \(amount)BTC") } } ``` 代码量看起来好像很大,但主要是定义了每个交易需要遵循的一些规则。一开始是函数的四个参数: `to`, `from`, `amount`, `type`。前三个参数不需要再解释了,而 `Type` 主要用于定义交易的类型。总共有两个类型:正常类型(normal) 和创世类型(genesis)。正常类型的交易会发生在账户 1065 和 2017 之间,而创世类型将会涉及到账户 0000。 1、第一个 `if-else` 条件语句处理转出账户的信息。如果账户不存在或者余额不足,将会提示交易不合法并返回。 2、第二个 `if-else` 条件语句处理转入账户的信息。如果账户不存在,则创建新账户并转入相应的比特币。反之,则向该账户转入正确数量的比特币。 3、最后一个 `if-else`条件语句处理交易类型。如果类型是创世类型,则添加一个创世区块,否则创建一个新的区块存储数据。 ### 打印函数 为了确保交易正确执行,在每个交易结束后,我们希望拿到所有交易的清单。以下是我们在交易函数下方的代码,用来打印相关信息: ``` func chainState() { for i in 0...bitcoinChain.chain.count-1 { print("\tBlock: \(bitcoinChain.chain[i].index!)\n\tHash: \(bitcoinChain.chain[i].hash!)\n\tPreviousHash: \(bitcoinChain.chain[i].previousHash!)\n\tData: \(bitcoinChain.chain[i].data!)") } redLabel.text = "Balance: \(accounts[String(describing: firstAccount)]!) BTC" blueLabel.text = "Balance: \(accounts[String(describing: secondAccount)]!) BTC" print(accounts) print(chainValidity()) } ``` 这是一个简单的循环语句,遍历 `bitcoinChain` 中的所有区块,并打印区块号码,哈希,前置哈希,以及存储的数据。同时我们更新了界面中的标签(label),这样就可以显示账户中正确的 BTC 数量。最后,打印所有的账户(应该是 3 个),并校验区块链的有效性。 现在你应该会在函数的最后一行发现一个错误。这是由于我们还没有实现 `chainValidity()` 函数,让我们马上开始吧。 ### 有效性函数 判断一个链是否有效的标准是:前置区块的哈希与当前区块所表示的是否匹配。我们可以再次用循环语句来遍历所有的区块: ``` func chainValidity() -> String { var isChainValid = true for i in 1...bitcoinChain.chain.count-1 { if bitcoinChain.chain[i].previousHash != bitcoinChain.chain[i-1].hash { isChainValid = false } } return "Chain is valid: \(isChainValid)\n" } ``` 和之前一样,我们遍历了 `bitcoinChain`中的所有区块,并检查了前置区块的 `hash` 是否与当前区块的 `previousHash` 一致。 就酱!我们已经将定义了所有需要的函数!你的 `ViewController.swift` 应该如下图一样: ![这里写图片描述](https://appcoda.com/wp-content/uploads/2018/05/blockchain-6.png) 收尾工作就是连接按钮和函数啦。让我们马上开始最后的部分吧! ### 让一切关联起来 当我们的应用第一次启动时,需要创世账户 0000 发送 50 BTC 到我们的第一个账户。再从第一个账户转账 10 BTC 到第二个账户,这只需要寥寥三行代码。最后 `viewDidLoad` 中的代码如下: ``` override func viewDidLoad() { super.viewDidLoad() transaction(from: "0000", to: "\(firstAccount)", amount: 50, type: "genesis") transaction(from: "\(firstAccount)", to: "\(secondAccount)", amount: 10, type: "normal") chainState() self.invalidAlert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) } ``` 我们使用已定义好的函数转账,并调用 `chainState()` 函数。最后,我们还在 `invalidAlert` 中添加了一个标题为 OK 的 `UIAlertAction`。 现在让我们来实现剩下的四个函数:`ReMeNe()`、`BrimeMeNe()`、`ReSdEnter()`和`BuLeScript()`。 ### 挖矿函数 挖矿函数特别简单,只需要三行代码。添加以下代码: ``` @IBAction func redMine(_ sender: Any) { transaction(from: "0000", to: "\(firstAccount)", amount: 100, type: "normal") print("New block mined by: \(firstAccount)") chainState() } @IBAction func blueMine(_ sender: Any) { transaction(from: "0000", to: "\(secondAccount)", amount: 100, type: "normal") print("New block mined by: \(secondAccount)") chainState() } ``` 在第一个挖矿函数中,我们使用交易函数从创世账户发送了 100 BTC 到第一个账户。我们打印了挖矿的区块,然后打印了区块链的状态。同样地,在 `blueMine` 函数中,我们转给了第二个账户 100 BTC。 ### 发送函数 发送函数和挖矿函数略微相似: ``` @IBAction func redSend(_ sender: Any) { if redAmount.text == "" { present(invalidAlert, animated: true, completion: nil) } else { transaction(from: "\(firstAccount)", to: "\(secondAccount)", amount: Int(redAmount.text!)!, type: "normal") print("\(redAmount.text!) BTC sent from \(firstAccount) to \(secondAccount)") chainState() redAmount.text = "" } } @IBAction func blueSend(_ sender: Any) { if blueAmount.text == "" { present(invalidAlert, animated: true, completion: nil) } else { transaction(from: "\(secondAccount)", to: "\(firstAccount)", amount: Int(blueAmount.text!)!, type: "normal") print("\(blueAmount.text!) BTC sent from \(secondAccount) to \(firstAccount)") chainState() blueAmount.text = "" } } ``` 首先,我们检查 `redAmount` 或者 `blueAmount` 的文本值是否为空。如果为空,则弹出无效交易的提示框。如果不为空,则继续下一步。我们使用交易函数从第一个账户转账到第二个账户(或者反向转账),金额为输入的数量。我们打印转账金额,并调用 `chainState()` 方法。最后,清空文本框。 就酱,工作完成!请检查你的代码是否和图中一致: ![这里写图片描述](https://appcoda.com/wp-content/uploads/2018/05/blockchain-7.png) 现在运行应用并测试一下。从前端看,这就像一个正常的交易应用,但是运行在屏幕背后的可是区块链啊!请尝试使用应用将 BTC 从一个帐户转移到另一个帐户,随意测试,尽情把玩吧! ### 结论 在这个教程中,你已经学会了如何使用 Swift 创建区块链,并且创建了你自己的比特币交易系统。请注意,真正加密货币的后端,和我们上面的实现完全不一样,因为它需要用智能合约实现分布式,而本例仅用于学习。 在这个示例中,我们将区块链技术应用于加密货币,然而你能想到区块链的其他应用场景吗?请留言分享给大家!希望你能学到更多新东西! 为了参考,你可以从 GitHub 下载[完整的示例](https://github.com/appcoda/BlockchainDemo)。]]></content>
</entry>
<entry>
<title><![CDATA[使非法状态不可表示]]></title>
<url>%2F2018%2F05%2F08%2Fmaking-illegal-states-unrepresentable%2F</url>
<content type="text"><![CDATA[title: "使非法状态不可表示" date: 2018-05-08 tags: [Swift] categories: [Swift 进阶] permalink: making-illegal-states-unrepresentable description: 你知道 URLSession 能够同时返回一个响应和错误吗? --- 原文链接=https://oleb.net/blog/2018/03/making-illegal-states-unrepresentable/ 作者=Ole Begemann 原文日期=2018-03-27 > 你知道 URLSession 能同时返回响应和错误吗? [我之前介绍过](https://oleb.net/blog/2015/07/swift-type-system/),Swift 强类型系统的一个主要优点是天生具备编译器强制遵循的文档规范。 ## 类型是编译器强制遵循的文档规范 类型为函数的行为设立了一种“界限”,因此一个易用的 API 应该精心选择输入输出类型。 仔细思考以下 Swift 函数声明: ``` func / (dividend: Int, divisor: Int) -> Int ``` 在不阅读任何函数实现的情况下,你就可以推断出这应该是[整型除法](http://mathworld.wolfram.com/IntegerDivision.html),因为返回的类型不可能是小数。相较之下,如果函数的返回类型是既可以表示整型,也可以表示浮点型数值的 [`NSNumber`](https://developer.apple.com/documentation/foundation/nsnumber),那你就只能祈祷开发者自觉遵循文档只返回整数。 随着类型系统的表现越来越好,这种使用类型来记录函数行为的技巧变得越来越有用。如果 `Swift` 有一个[`NonZeroInt` 类型](#quote1)代表“除了 0 之外的整型”,那么除法函数可能就会变成下面这样: ``` func / (dividend: Int, divisor: NonZeroInt) -> Int ``` 类型检查不允许传入的除数为 0,因此你不用关心函数如何处理除数为 0 的错误。函数会中断吗?会返回一个没有意义的值吗?如果你用的是上一种定义,就必须在文档里单独说明特殊情况的处理方式。 ## 使非法状态成为不可能 我们可以把这个观点转换为一条通用规则:**使用类型让你的程序无法表现非法状态**。 如果你想学习更多相关知识,可以看看 Brandon Williams 和 Stephen Celis 的最新视频系列 [Point-Free](https://www.pointfree.co/)。他们讲了很多这方面的知识和相关话题,前八集真的特别棒,我强烈推荐大家去订阅,你会学到很多东西。 在[第四集](https://www.pointfree.co/episodes/ep4-algebraic-data-types)关于代数数据类型([algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type))的视频中,Brandon 和 Stephen 讨论了如何组合 `enums` 和 `structs`(或者 `tuples`)来精确表示期望状态的类型,并且让所有非法状态无法表示。在视频的最后,他们用 Apple 的 [URLSession](https://developer.apple.com/documentation/foundation/urlsession) API 作为反面教材进行介绍,因为这个 API 没有使用最合适的类型,这就引出了本文的子标题——“你知道 URLSession 能同时返回响应和错误吗?”。 ## URLSession Swift 的类型系统比 Objective-C 更富有表现力。然而,很多 Apple 自己的 API 也没有利用这个优势,可能是因为没空更新老旧的 API,或者是为了维持 Objective-C 的兼容性。 在 iOS 中发起一个[网络请求](https://developer.apple.com/documentation/foundation/urlsession/1410330-datatask)的通用方法: ``` class URLSession { func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask } ``` 回调函数的参数是三个可选值:[`Data?`](https://developer.apple.com/documentation/foundation/data),[`URLResponse?`](https://developer.apple.com/documentation/foundation/urlresponse) 和 [`Error?`](https://developer.apple.com/documentation/swift/error)。这将产生 `2 × 2 × 2 = 8` 种[可能的状态](#quote2),但是其中有多少种是合法的呢? 引述 [Brandon 和 Stephen](https://www.pointfree.co/episodes/ep4-algebraic-data-types) 的观点:“这里有很多状态毫无意义”。有些组合很明显没有意义,另外我们基本可以确定,这三个值不可能全为 `nil` 或全为非 `nil`。 ## 响应和错误能够同时非 nil 其他状态就很棘手了,在这里 Brandon 和 Stephen 犯了一点小错误:他们认为 API 要么返回一个有效的 `Data` 和 `URLResponse`,要么返回一个 `Error`。毕竟接口不可能同时返回一个非 `nil` 的响应和错误。看起来很有道理,对不对? 但事实上这是错误的。`URLResponse` 封装了服务器的 [HTTP 响应头部](https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html),只要接收到一个有效的响应头部, `URLSession` API 就会一直给你提供这个值,无论后续的阶段请求是否出错(例如取消和超时)。因而 API 的完成处理中有可能包含一个有效的 `URLResponse` 和非 `nil` 的错误值(但是没有 `Data`)。 如果你对 `URLSession` 代理(delegate)API 比较熟悉的话,应该不会太惊讶,因为代理方法就是分成 [`didReceiveResponse`](https://developer.apple.com/documentation/foundation/urlsessiondatadelegate/1410027-urlsession) 和 [`didReceiveData`](https://developer.apple.com/documentation/foundation/urlsessiondatadelegate/1411528-urlsession)。实际上,[`dataTask(with:completionHandler:)`的文档](https://developer.apple.com/documentation/foundation/urlsession/1410330-datatask)也提到了这个问题: > 如果收到服务器的响应,那么**无论请求成功或失败**,响应参数都会有值。 不过,我敢打赌 Cocoa 开发人员普遍对此抱有误解。仅仅在过去的四周,我就看到[两](https://davedelong.com/blog/2018/03/02/apple-networking-feedback/)篇[文章](https://ruiper.es/2018/03/03/ras-s2e1/)的作者犯了同样的错误(至少没有领悟其中的真谛)。 说真的,我很喜欢这个充满讽刺意味的事实:Brandon 和 Stephen 试图指出由于类型问题导致的 API 缺陷,但在指出错误的同时,这个类型问题又让他们犯了另一个错误。如果原始 API 使用了更好的类型,那么这两个错误就都能避免,这反而证明了他们的观点:一个有更加严格类型的 API 能够避免错误使用。 ## 示例代码 如果你想自己体验一下 `URLSession` 的功能,你可以复制以下代码到 Swift playground: ``` import Foundation import PlaygroundSupport // 如果返回 404,把 URL 换成随便一个大文件 let bigFile = URL(string: "https://speed.hetzner.de/1GB.bin")! let task = URLSession.shared.dataTask(with: bigFile) { (data, response, error) in print("data:", data as Any) print("response:", response as Any) print("error:", error as Any) } task.resume() // 过几秒之后取消下载 DispatchQueue.main.asyncAfter(deadline: .now() + 3) { task.cancel() } PlaygroundPage.current.needsIndefiniteExecution = true ``` 这段代码首先下载一个大文件,然后在几秒后取消。最后,完成的处理中返回了一个非 `nil` 的响应和错误。 (这里假设指定的时间间隔内,能够获取到服务器响应的头部,但不能完成下载。如果你的网速非常慢或者非常变态,请自行调整这个时间参数) ## 正确的类型应该是什么? Brandon 和 Stephen 随后在 [Point-Free 的第九集视频](https://www.pointfree.co/episodes/ep9-algebraic-data-types-exponents)中发布了他们对问题的跟进。他们认为“正确”的参数类型应该是: ``` (URLResponse?, Result) ``` 我不同意,因为如果有数据,就一定有响应,不可能只有数据没有响应。我认为应该是这样的: ``` Result ``` 解读:你将要么得到数据和响应(后者肯定不是 `nil`),要么得到一个错误和一个可选类型的响应。不可否认,我的建议与一般的 `Result` 类型定义相悖,因为它将失败参数约束为不能符合 `Error` 的 [Error](https://developer.apple.com/documentation/swift/error) 协议—`(Error, URLResponse?) `。目前 [Swift 论坛正在讨论](https://forums.swift.org/t/adding-result-to-the-standard-library/6932/58) `Error` 约束是否有必要。 ## Result 类型 由于 `URLResponse` 参数的非直观行为,`URLSession` 的API 显得特别棘手。但是 Apple 几乎所有的基于回调的异步 API 都有相同的问题,它们所提供的类型使得非法状态可以表示。 如何解决这个问题呢? Swift 的通用方案是定义一个 [Result 类型](https://github.com/antitypical/Result/blob/03fba33a0a8b75492480b9b2e458e88651525a2a/Result/Result.swift)—一个可以代表通用成功值或错误的枚举。最近,又有人试图将 [Result 添加到标准库](https://forums.swift.org/t/adding-result-to-the-standard-library/6932/20)。 如果 Swift 5 添加了 `Result`(大胆假设),Apple 可能(更大胆的假设)会自动导入类似这样 `completionHandler: (A?, Error?) -> Void as (Result) -> Void` 的 Cocoa API,将四个可表现的状态转为两个。在那之前(如果真的会发生的话),我建议你还是先自己[实现转换](https://oleb.net/blog/2017/01/result-init-helper/)。 长远来看,Swift 终有一天能从语言层面正确支持异步 API。社区和 Swift 团队可能会提出新的解决方案,[把现有的 Cocoa API 移植到新系统中](https://gist.github.com/lattner/429b9070918248274f25b714dcfc7619#conversion-of-imported-objective-c-apis),就像把 Objective-C 的 `NSError **` 参数作为抛出(throwing)函数引入 Swift 一样。不过不要太过期待,Swift 6 之前肯定实现不了。 ---------- 1、你可以自己定义一个 `NonZeroInt` 类型,但是没有办法告诉编译器“如果有人尝试用零去初始化这个类型,就引发一个错误”。你必须依赖运行时检查。 不过,引入这样的类型通常是个不错的想法,因为类型的用户可以在初始化之后依赖于所声明的不变性。我还没有在其他地方看到一个 `NonZeroInt` 类型,保证类型为非空集合的自定义类型更受欢迎。 2、我只是把“`nil`”或“非`nil`”作为可能的状态。显然,非 `nil` 数据值可以具有无数种可能的状态,并且对于其他两个参数也是如此。但是这些状态对我们来说并不好玩。]]></content>
</entry>
<entry>
<title><![CDATA[iOS 如何优雅地 hook 系统的 delegate 方法?]]></title>
<url>%2F2017%2F11%2F01%2FiOS%20%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E5%9C%B0%20hook%20%E7%B3%BB%E7%BB%9F%E7%9A%84%20delegate%20%E6%96%B9%E6%B3%95%EF%BC%9F%2F</url>
<content type="text"><![CDATA[title: "iOS 如何优雅地 hook 系统的 delegate 方法?" date: 2017-11-1 tags: [Objective-C,hook,swizzling,runtime] categories: [Objective-C] description: hook 系统的 delegate 方法的一些实践心得。 --- 在 iOS 开发中我们经常需要去 hook 系统方法,来满足一些特定的应用场景。 比如使用 Swizzling 来实现一些 AOP 的日志功能,比较常见的例子是 hook `UIViewController` 的 `viewDidLoad` ,动态为其插入日志。 这当然是一个很经典的例子,能让开发者迅速了解这个知识点。不过正如现在的娱乐圈,diss 天 diss 地,如果我们也想 hook 天,hook 地,顺便 hook 一下系统的 delegate 方法,该怎么做呢? 所以就进入今天的主题:**如何优雅地 hook 系统的 delegate 方法?** #### hook 系统类的实例方法 首先,我们回想一下 hook `UIViewController` 的 `viewDidLoad` 方法,我们需要使用 category,为什么需要 category 呢?因为在 category 里面才能在不入侵源码的情况下,拿到实例方法 `viewDidLoad` ,并实现替换: ``` #import "UIViewController+swizzling.h" #import @implementation UIViewController (swizzling) + (void)load { // 通过 class_getInstanceMethod() 函数从当前对象中的 method list 获取 method 结构体,如果是类方法就使用 class_getClassMethod() 函数获取. Method fromMethod = class_getInstanceMethod([self class], @selector(viewDidLoad)); Method toMethod = class_getInstanceMethod([self class], @selector(swizzlingViewDidLoad)); // 这里直接交换方法,不做判断,因为 UIViewController 的 viewDidLoad 肯定实现了。 method_exchangeImplementations(fromMethod, toMethod); } // 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。 - (void)swizzlingViewDidLoad { NSString *str = [NSString stringWithFormat:@"%@", self.class]; NSLog(@"日志打点 : %@", self.class); [self swizzlingViewDidLoad]; } @end ``` 这个例子里面,有一个注意点,通常我们创建 `ViewController` 都是继承于 `UIViewController`,因此如果想要使用这个日志打点功能,在自定义 `ViewController` 里面需要调用 `[super viewDidLoad]`。所以一定需要明白,这个例子是替换 `UIViewController` 的 `viewDidLoad`,而不是全部子类的 `viewDidLoad`。 ``` @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // } @end ``` #### hook webView 的 delegate 方法 这个需求最初是项目中需要统计所有 `webView` 相关的数据,因此需要 hook webView 的 `delegate` 方法,今天也是以此为例,主要是 hook `UIWebView`(`WKWebView`类似)。 首先,我们需要明白,调用 delegate 的对象,是继承了 UIWebViewDelegate 协议的对象,因此如果要 hook delegate 方法,我们先要找到这个对象。 因此我们需要 hook [UIWebView setDelegate:delegate] 方法,拿到 delegate 对象,才能动态地替换该方法。这里 swizzling 上场: ``` @implementation UIWebView(delegate) +(void)load{ // hook UIWebView Method originalMethod = class_getInstanceMethod([UIWebView class], @selector(setDelegate:)); Method swizzledMethod = class_getInstanceMethod([UIWebView class], @selector(hook_setDelegate:)); method_exchangeImplementations(originalMethod, swizzledMethod); } - (void)dz_setDelegate:(id)delegate{ [self dz_setDelegate:delegate]; // 拿到 delegate 对象,在这里做替换 delegate 方法的操作 } @end ``` 这里有个局限性,源码中需要调用 `setDelegate:` 方法,这样才会调用 `dz_setDelegate:`。 接下来就是重点了,我们需要根据两种情况去动态地 hook delegate 方法,以 hook `webViewDidFinishLoad:` 为例: - delegate 对象实现了 `webViewDidFinishLoad:` 方法。则交换实现。 - delegate 对象未实现了 `webViewDidFinishLoad:` 方法。则动态添加该 delegate 方法。 下面是 category 实现的完整代码,实现了以上两种情况下都能正确统计页面加载完成的数据: ``` static void dz_exchangeMethod(Class originalClass, SEL originalSel, Class replacedClass, SEL replacedSel, SEL orginReplaceSel){ // 原方法 Method originalMethod = class_getInstanceMethod(originalClass, originalSel); // 替换方法 Method replacedMethod = class_getInstanceMethod(replacedClass, replacedSel); // 如果没有实现 delegate 方法,则手动动态添加 if (!originalMethod) { Method orginReplaceMethod = class_getInstanceMethod(replacedClass, orginReplaceSel); BOOL didAddOriginMethod = class_addMethod(originalClass, originalSel, method_getImplementation(orginReplaceMethod), method_getTypeEncoding(orginReplaceMethod)); if (didAddOriginMethod) { NSLog(@"did Add Origin Replace Method"); } return; } // 向实现 delegate 的类中添加新的方法 // 这里是向 originalClass 的 replaceSel(@selector(replace_webViewDidFinishLoad:)) 添加 replaceMethod BOOL didAddMethod = class_addMethod(originalClass, replacedSel, method_getImplementation(replacedMethod), method_getTypeEncoding(replacedMethod)); if (didAddMethod) { // 添加成功 NSLog(@"class_addMethod_success --> (%@)", NSStringFromSelector(replacedSel)); // 重新拿到添加被添加的 method,这里是关键(注意这里 originalClass, 不 replacedClass), 因为替换的方法已经添加到原类中了, 应该交换原类中的两个方法 Method newMethod = class_getInstanceMethod(originalClass, replacedSel); // 实现交换 method_exchangeImplementations(originalMethod, newMethod); }else{ // 添加失败,则说明已经 hook 过该类的 delegate 方法,防止多次交换。 NSLog(@"Already hook class --> (%@)",NSStringFromClass(originalClass)); } } @implementation UIWebView(delegate) +(void)load{ // hook WebView Method originalMethod = class_getInstanceMethod([UIWebView class], @selector(setDelegate:)); Method swizzledMethod = class_getInstanceMethod([UIWebView class], @selector(dz_setDelegate:)); method_exchangeImplementations(originalMethod, swizzledMethod); } - (void)dz_setDelegate:(id)delegate{ [self dz_setDelegate:delegate]; // 获得 delegate 的实际调用类 // 传递给 UIWebView 来交换方法 [self exchangeUIWebViewDelegateMethod:delegate]; } #pragma mark - hook webView delegate 方法 - (void)exchangeUIWebViewDelegateMethod:(id)delegate{ dz_exchangeMethod([delegate class], @selector(webViewDidFinishLoad:), [self class], @selector(replace_webViewDidFinishLoad:),@selector(oriReplace_webViewDidFinishLoad:)); } // 在未添加该 delegate 的情况下,手动添加 delegate 方法。 - (void)oriReplace_webViewDidFinishLoad:(UIWebView *)webView{ NSLog(@"统计加载完成数据"); } // 在添加该 delegate 的情况下,使用 swizzling 交换方法实现。 // 交换后的具体方法实现 - (void)replace_webViewDidFinishLoad:(UIWebView *)webView { NSLog(@"统计加载完成数据"); [self replace_webViewDidFinishLoad:webView]; } @end ``` 与 hook 实例方法不相同的地方是,交换的两个类以及方法都不是 `[self class]`,在实现过程中: 1. 判断 delegate 对象的 delegate 方法(`originalMethod`)是否为空,为空则用 `class_addMethod` 为 delegate 对象添加方法名为 (`webViewDidFinishLoad:`) ,方法实现为(`oriReplace_webViewDidFinishLoad:`)的动态方法。 2. 若已实现,则说明该 delegate 对象实现了 `webViewDidFinishLoad:` 方法,此时不能简单地交换 `originalMethod` 与 `replacedMethod`,因为 `replaceMethod` 是属于 `UIWebView` 的实例方法,没有实现 delegate 协议,无法在 hook 之后调用原来的 delegate 方法:`[self replace_webViewDidFinishLoad:webView];`。 因此,我们也需要将 `replace_webViewDidFinishLoad:` 方法动态添加到 delegate 对象中,并使用添加后的方法和源方法交换。 #### 结语 以上,通过动态添加方法并替换的方式,可以在不入侵源码的情况下,优雅地 hook 系统的 delegate 方法。通过合理使用 runtime 期间几个方法的特性,使得 hook 系统未实现的 delegate 方法成为可能。 最后献上:[github 源码地址](https://github.com/lin493369/HookDelegateDemo)]]></content>
</entry>
<entry>
<title><![CDATA[伪单例模式]]></title>
<url>%2F2017%2F04%2F10%2FiOS%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5%2F</url>
<content type="text"><![CDATA[title: "伪单例模式" date: 2017-04-10 tags: [Objective-C,单例] categories: [Objective-C] description: 实际上,本文讲述的是在明知是伪单例的情况下,如何正确地管理伪单例的生命周期,文中若有不实之处,希望大家提出宝贵的意见。 --- 本文仅探讨 iOS 中单例的适用场景. 如需单例教程及其定义作用的请访问:[http://www.jianshu.com/p/5226bc8ed784](http://www.jianshu.com/p/5226bc8ed784)。 最近在做项目的重构工作,翻看了一下源码,发现了各种历史遗留问题。其中随处可见的单例,产生了万物皆单例的现象(说好的万物皆对象呢?)。 在与前开发人员沟通后,对方坚持使用单例的原因如下: * 代码简洁,不需要声明属性以及创建新的实例对象,需要的时候就可以马上调用。 * 方便管理对象的生命周期,把对象的创建和销毁时机都掌握在开发人员手中,可以控制对象的销毁时机。 * 历史遗留,iOS 系统类中随处可见的单例,我们的前辈们也都是这么用的,那就这么干吧。 第一点无法反驳,单例确实很好用,写起来有种欲仙欲死的快感。但是,不管副作用的话,毒品产生的快感大概比这更甚吧。作为一个有追求的程序猿,怎么能被普通的感官快感所诱惑,我们的目标是星辰大海好吗。 第二点无法直视,既然是单例为什么要手动销毁呢。这时候就有人说了,比如退出登录后,需要把账户的单例销毁。作为需要全局使用的对象,这样的需求确实无可厚非,那么如果这个单例对象只是在一个地方使用到了呢?需要特地建一个单例并手动去管理单例的释放时机吗?这还是单例吗,这是假单例吧。 ### 真单例 吐槽完毕。进入正题,单例作为一个变态的全局变量,首先看他的定义: > 保证一个类仅有一个实例,并提供一个访问它的全局访问点。 那么他的使用场景很简单且很明确: > * 在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在 APP 开发中我们可能在任何地方都要使用用户的信息,那么可以在登录的时候就把用户信息存放在一个文件里面,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。 > > * 有的情况下,某个类可能只能有一个实例。比如说你写了一个类用来播放音乐,那么不管任何时候只能有一个该类的实例来播放声音。再比如,一台计算机上可以连好几个打印机,但是这个计算机上的打印程序只能有一个,这里就可以通过单例模式来避免两个打印任务同时输出到打印机中,即在整个的打印过程中我只有一个打印程序的实例。 综上所述,不遵守以上定义的单例都是伪单例,例如用户信息单例就是典型的伪单例。 ### 伪单例 使用伪单例并没有什么错,我们不需要咬文爵字,只要有合适的应用场景,并承认自己是伪单例,我们也可以开开心心地使用它。 那么我们今天就来好好谈谈伪单例的正确使用姿势(不管是不是你创造的,既然接盘了你就要负责到底)。 首先本文中对伪单例的定义: > 需要管理生命周期,并且长时间不需要销毁的单例对象。 即在单例对象的基础上,需要对其生命周期进行管理,并且在应用启动期间如没有特殊情况,会一直存活。 #### 伪单例的销毁 伪单例的销毁要基于其创建的方式,常规的有两种:同步锁、GCD。 ```` static InstanceSync *instance = nil; @implementation InstanceSync // 同步锁方式 +(instancetype)shareInstance{ @synchronized (self) { if (!instance) { instance = [[self alloc]init]; } } return instance; } ```` ```` static InstanceSync *instance = nil; static dispatch_once_t onceToken; @implementation InstanceSync // GCD 方式 +(instancetype)shareInstance{ dispatch_once(&onceToken, ^{ instance = [[self alloc]init]; }); return instance; } ```` 首先我们使用同步锁的单例来试验一下,一般我们销毁一个对象是将其置为空,即可以释放,如下: ```` NSLog(@"instanceSync : %@",[InstanceSync shareInstance]); InstanceSync *instanceSync = [InstanceSync shareInstance]; instanceSync = nil; NSLog(@"instanceSync : %@",[InstanceSync shareInstance]); ```` 实际上,这样并不能销毁这个对象: ```` 2017-04-10 10:54:10.449 instanceSync : 2017-04-10 10:54:10.449 instanceSync : ```` 其实在常规单例的内部都有一个全局静态变量,我们需要对其置空才能释放该单例对象: ```` -(void)destoryInstance{ instance = nil; } -(void)dealloc{ NSLog(@"%@ occur",NSStringFromSelector(_cmd)); } ```` 那么我们再来尝试一下: ``` NSLog(@"instanceSync : %@",[InstanceSync shareInstance]); InstanceSync *instanceSync = [InstanceSync shareInstance]; [instanceSync destoryInstance]; NSLog(@"instanceSync : %@",[InstanceSync shareInstance]); ``` ``` 2017-04-10 11:05:22.112 instanceSync : 2017-04-10 11:05:22.112 instanceSync : 2017-04-10 11:05:24.366 dealloc occur ``` 可以看到伪单例对象 `[InstanceSync shareInstance]` 并没有马上进入 `dealloc`,而是在打印完第二 log 后才进入 `dealloc`;因此这里需要注意: > 如果伪单例对象被外部变量所持有,那么在释放单例对象时,需要确保所有持有变量都被释放后,才可以进入单例的释放。因此不建议将单例赋值给外部变量,以免无法在预期内释放单例对象。 此外再次调用 `[InstanceSync shareInstance]` 将会产生新的对象,这也是易于理解的,那么如果使用 GCD 的方式能否产生新的对象? 实际上,这就取决于你销毁对象的方式: ``` -(void)destoryInstance{ instance = nil; // 销毁静态全局变量 onceToken = nil; // 销毁 GCD onceToken } ``` 如果只销毁静态全局变量,那么调用该方法后,将不会产生新的对象: ``` 2017-04-10 11:21:37.917 instanceGCD : 2017-04-10 11:21:37.918 instanceGCD : (null) 2017-04-10 11:21:37.918 dealloc occur ``` 如果销毁 GCD onceToken ,那么不论销毁静态全局变量,都会产生新的对象。 ### 结束 实际上,本文讲述的是在明知是伪单例的情况下,如何正确地管理伪单例的生命周期,文中若有不实之处,希望大家提出宝贵的意见。]]></content>
</entry>
<entry>
<title><![CDATA[iOS 10 添加推送功能注意点及问题汇总]]></title>
<url>%2F2016%2F09%2F22%2FiOS10%E6%B7%BB%E5%8A%A0%E6%8E%A8%E9%80%81%E5%8A%9F%E8%83%BD%E8%AE%BE%E7%BD%AE%E6%B5%81%E7%A8%8B%E5%8F%8A%E6%B3%A8%E6%84%8F%E7%82%B9%2F</url>
<content type="text"><![CDATA[title: "iOS 10 添加推送功能注意点及问题汇总" date: 2016-09-22 tags: [iOS10,Notification,推送] categories: [Notification] description: 很多童鞋从 iOS 9 升级到 iOS 10 后,发现推送功能有很多问题,特此总结. --- 对于 iOS 9 升级到 iOS 10 推送功能不正常的问题,总结了一下要点,亲们可以根据以下步骤,逐步排查问题,也可以逐步实现 iOS 10 的推送功能。 1、在项目 target 中,打开` Capabilitie —> Push Notifications`,并会自动在项目中生成 .entitlement 文件。(很多同学升级后,获取不到 deviceToken,大概率是由于没开这个选项) ![Capabilitie —> Push Notifications ](http://upload-images.jianshu.io/upload_images/928928-0c0e9c11b9d9a9eb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) ![自动生成 .entitlement](http://upload-images.jianshu.io/upload_images/928928-d9e5b390c8799506.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 2、确保添加了 `UserNotifications.framework`,并 import 到 `AppDelegate`,记得实现 `UNUserNotificationCenterDelegate` 。 ``` #import @interface AppDelegate : UIResponder @end ``` 3、在 `didFinishLaunchingWithOptions` 方法中,首先实现 `UNUserNotificationCenter` delegate,并使用 `UIUserNotificationSettings` 请求权限。 ``` //注意,关于 iOS10 系统版本的判断,可以用下面这个宏来判断。不能再用截取字符的方法。 #define SYSTEM_VERSION_GRATERTHAN_OR_EQUALTO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending) -(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ if(SYSTEM_VERSION_GRATERTHAN_OR_EQUALTO(@"10.0")){ UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge) completionHandler:^(BOOL granted, NSError * _Nullable error){ if( !error ){ [[UIApplication sharedApplication] registerForRemoteNotifications]; } }]; } return YES; } ``` 4、最后实现以下两个回调。 ``` //====================For iOS 10==================== -(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{ NSLog(@"Userinfo %@",notification.request.content.userInfo); //功能:可设置是否在应用内弹出通知 completionHandler(UNNotificationPresentationOptionAlert); } //点击推送消息后回调 -(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler{ NSLog(@"Userinfo %@",response.notification.request.content.userInfo); } ``` 注意:需要根据系统版本号来判断是否使用新的 `UserNotifications.framework`,因此,不要着急删除 iOS 10 以前的代码。 有问题,敬请留言探讨。]]></content>
</entry>
<entry>
<title><![CDATA[iOS 10 IDFA 新策略]]></title>
<url>%2F2016%2F08%2F31%2F%E8%8E%B7%E5%8F%96IDFA%E8%BF%94%E5%9B%9E%E5%85%A8%E9%9B%B6%E9%94%99%E8%AF%AF%2F</url>
<content type="text"><![CDATA[title: "iOS 10 IDFA 新策略 " date: 2016-08-31 tags: [iOS10,IDFA] categories: [iOS10] description: 相信很多人将获取 IDFA 作为应用的唯一标识的替代方案,因此对 IDFA 有很大的需求。。 --- 相信很多人将获取 IDFA 作为应用的唯一标识的替代方案,因此对 IDFA 有很大的需求。 但是最近很多同学在获取 IDFA 时发现返回: ``` 00000000-0000-0000-0000-000000000000 ``` 很不幸,实际上是由于:这些设备升级到 iOS10,并且用户开启了限制广告跟踪。 ![这里写图片描述](http://img.blog.csdn.net/20160831102842189) > 在 iOS 10 之前:当用户开启限制广告跟踪,仍然可以将 IDFA 用于不同的用途,除了不能用于投放特定广告目标。 但是,iOS 10 之后,对 IDFA 做了变更,参照官方文档所述: > Important > > In iOS 10.0 and later, the value of advertisingIdentifier is all zeroes when the user has limited ad tracking. > > 在 iOS 10.0 以后,当用户开启限制广告跟踪,advertisingIdentifier 的值将是全零。 在这种情况下,如果你依然使用 IDFA 作为唯一标识符的话,可能会有大危机,推荐一个替代方案 [OpenIDFA](https://github.com/ylechelle/OpenIDFA)(一个基于可持续、隐私、友好的 identifier 方案)。 以上,有用到 IDFA 并且将其作为标识用户唯一手段的童鞋请悉知,虽是小改动,但对刚需开发者来说还是蛮严重的,特别 iOS 10 正式版放出之后,可能将是大灾难(危言耸听。。)。]]></content>
</entry>
<entry>
<title><![CDATA[iOS 10 添加本地推送(Local Notification)]]></title>
<url>%2F2016%2F07%2F01%2FiOS10%E6%B7%BB%E5%8A%A0%E6%9C%AC%E5%9C%B0%E6%8E%A8%E9%80%81%EF%BC%88LocalNotification)%2F</url>
<content type="text"><![CDATA[title: "iOS 10 添加本地推送(Local Notification)" date: 2016-06-31 tags: [iOS10,Notification,推送] categories: [Notification] description: 本文主要查看了 iOS 10 的相关文档,整理出了在 iOS 10 下的本地推送通知. --- 前言 -- iOS 10 中废弃了 `UILocalNotification`( `UIKit Framework`) 这个类,采用了全新的 `UserNotifications Framework` 来推送通知,从此推送通知也有了自己的标签 `UN`(这待遇真是没别人了),以及对推送功能的一系列增强改进(两个 extension 和 界面的体验优化),简直是苹果的亲儿子,因此推送这部分功能也成为开发中的重点。 本文主要查看了 iOS 10 的相关文档,整理出了在 iOS 10 下的本地推送通知,由于都是代码,就不多做讲解,直接看代码及注释,有问题留言讨论哦。 ---------- 新的推送注册机制 -------- 注册通知(`Appdelegate.m`): ``` #import #import "AppDelegate.h" @interface AppDelegate () @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 使用 UNUserNotificationCenter 来管理通知 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; //监听回调事件 center.delegate = self; //iOS 10 使用以下方法注册,才能得到授权 [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError * _Nullable error) { // Enable or disable features based on authorization. }]; //获取当前的通知设置,UNNotificationSettings 是只读对象,不能直接修改,只能通过以下方法获取 [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { }]; return YES; } #pragma mark - UNUserNotificationCenterDelegate //在展示通知前进行处理,即有机会在展示通知前再修改通知内容。 -(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{ //1. 处理通知 //2. 处理完成后条用 completionHandler ,用于指示在前台显示通知的形式 completionHandler(UNNotificationPresentationOptionAlert); } @end ``` 推送本地通知 ------ ``` //使用 UNNotification 本地通知 +(void)registerNotification:(NSInteger )alerTime{ // 使用 UNUserNotificationCenter 来管理通知 UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; //需创建一个包含待通知内容的 UNMutableNotificationContent 对象,注意不是 UNNotificationContent ,此对象为不可变对象。 UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; content.title = [NSString localizedUserNotificationStringForKey:@"Hello!" arguments:nil]; content.body = [NSString localizedUserNotificationStringForKey:@"Hello_message_body" arguments:nil]; content.sound = [UNNotificationSound defaultSound]; // 在 alertTime 后推送本地推送 UNTimeIntervalNotificationTrigger* trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:alerTime repeats:NO]; UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:@"FiveSecond" content:content trigger:trigger]; //添加推送成功后的处理! [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"本地通知" message:@"成功添加推送" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]; [alert addAction:cancelAction]; [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:YES completion:nil]; }]; } ``` iOS 10 以前本地推送通知: ``` + (void)registerLocalNotificationInOldWay:(NSInteger)alertTime { // ios8后,需要添加这个注册,才能得到授权 // if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) { // UIUserNotificationType type = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; // UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:type // categories:nil]; // [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; // // 通知重复提示的单位,可以是天、周、月 // } UILocalNotification *notification = [[UILocalNotification alloc] init]; // 设置触发通知的时间 NSDate *fireDate = [NSDate dateWithTimeIntervalSinceNow:alertTime]; NSLog(@"fireDate=%@",fireDate); notification.fireDate = fireDate; // 时区 notification.timeZone = [NSTimeZone defaultTimeZone]; // 设置重复的间隔 notification.repeatInterval = kCFCalendarUnitSecond; // 通知内容 notification.alertBody = @"该起床了..."; notification.applicationIconBadgeNumber = 1; // 通知被触发时播放的声音 notification.soundName = UILocalNotificationDefaultSoundName; // 通知参数 NSDictionary *userDict = [NSDictionary dictionaryWithObject:@"开始学习iOS开发了" forKey:@"key"]; notification.userInfo = userDict; // ios8后,需要添加这个注册,才能得到授权 if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) { UIUserNotificationType type = UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound; UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:type categories:nil]; [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; // 通知重复提示的单位,可以是天、周、月 notification.repeatInterval = NSCalendarUnitDay; } else { // 通知重复提示的单位,可以是天、周、月 notification.repeatInterval = NSDayCalendarUnit; } // 执行通知注册 [[UIApplication sharedApplication] scheduleLocalNotification:notification]; } ```]]></content>
</entry>
<entry>
<title><![CDATA[iOS 简单易懂的 Block 回调使用和解析]]></title>
<url>%2F2016%2F03%2F11%2FiOS%E7%AE%80%E5%8D%95%E6%98%93%E6%87%82%E7%9A%84Block%E5%9B%9E%E8%B0%83%E4%BD%BF%E7%94%A8%E5%92%8C%E8%A7%A3%E6%9E%90%2F</url>
<content type="text"><![CDATA[title: "iOS 简单易懂的 Block 回调使用和解析 " date: 2016-03-11 tags: [Objective-C,block] categories: [Objective-C] description: 本文主要讲的是 Block 回调的使用,以及 Block 是如何实现这种神奇的回调两部分来讲的。 --- 前言 -- 老实说在早前我已经学会了如何使用 Block 来做一些方法回调,传递参数的功能,并且用 Block 简单封装了第三方的网络库(AFNetworking)。虽说对 Block 的应用说不上得心应手,但是却是极其地喜欢使用这种设计模式,并且在项目中也大量地使用了。 但是,最近一位即将参加面试的学弟问我,什么是 Block 呢?我蒙圈了,但是毕竟是学长,我假装淡定地反问道:你所理解的 Block 是什么呢?学弟说:是一段封装的代码块,并可以放在任意位置使用,还可以传递数据。我心里暗喜,这孩子还是图样了,于是语重心长地说:Block 的本质是可以截取自动变量的匿名函数。但是说出这句话我就后悔了,这句话他喵的到底是个什么意思?看着学弟满意地走了之后,我就疯狂地上网查资料,万一下次这个熊孩子深究起来可不就破坏了我英明神武的形象了,但是并没有很满意的答案,大多是照文档描述了 Block 的定义以及基本用法,不然就是高深地去探讨 Block 底层的实现机制,显然这些都不适合让一个初学者既能学会使用又能够没有疑惑地使用。 本文主要讲的是 **Block 回调的使用**,以及 **Block 是如何实现这种神奇的回调**两部分来讲的。 Block 回调实现 --------- 不着急,先跟着我实现最简单的 Block 回调传参的使用,如果你能举一反三,基本上可以满足了 OC 中的开发需求。已经实现的同学可以跳到下一节。 首先解释一下我们例子要实现什么功能(其实是烂大街又最形象的例子): 有两个视图控制器 A 和 B,现在点击 A 上的按钮跳转到视图 B ,并在 B 中的textfield 输入字符串,点击 B 中的跳转按钮跳转回 A ,并将之前输入的字符串 显示在 A 中的 label 上。也就是说 A 视图中需要回调 B 视图中的数据。 想不明白的同学可以看一看最终实现的效果图: ![block example](http://upload-images.jianshu.io/upload_images/928928-80838951ad6524a9?imageMogr2/auto-orient/strip) 这里不再对 [Block 的语法](http://blog.csdn.net/totogo2010/article/details/7839061)做说明了,不了解的同学可以点[传送门](http://blog.csdn.net/totogo2010/article/details/7839061)。 首先,我们需要定义两个试图控制器 AViewController 和 BViewController,现在我们需要思考一下,Block 应该在哪里定义呢? 我们可以简单地这样思考,需要回调数据的是 A 视图,那么 Block 就应该在 B 中定义,用于获取传入回调数据。 因此我们在 BViewController.h 中定义如下: ``` //BViewController.h #import typedef void(^CallBackBlcok) (NSString *text);//1 @interface BViewController : UIViewController @property (nonatomic,copy)CallBackBlcok callBackBlock;//2 @end ``` 在这里,代码 1 用 typedef 定义了 `void(^) (NSString *text)`的别名为 `CallBackBlcok` 。这样我们就可以在代码 2 中,使用这个别名定义一个 Block 类型的变量 `callBackBlock`。 在定义了 `callBackBlock` 之后,我们可以在 B 中的点击事件中添加 `callBackBlock` 的传参操作: ``` //BViewController.m - (IBAction)click:(id)sender { self.callBackBlock(_textField.text); //1 [self.navigationController popToRootViewControllerAnimated:YES]; } ``` 这样我们就可以在想要获取数据回调的地方,也就 A 的视图中调用 block: ``` // AViewController.m - (IBAction)push:(id)sender { BViewController *bVC = [self.storyboard instantiateViewControllerWithIdentifier:@"BViewController"]; bVC.callBackBlock = ^(NSString *text){ // 1 NSLog(@"text is %@",text); self.label.text = text; }; [self.navigationController pushViewController:bVC animated:YES]; } ``` 代码 1 中,通过对回调将 B 中的数据传递到代码块中,并赋值给 A 中的 label,实现了整个回调过程。 上例是通过将 block 直接赋值给 block 属性,也可以通过方法参数的方式传递 block 块。 由于考虑有的小伙伴翻墙比较困难,完整的示例代码放在 git.oschina.net 上,代码地址:[BlockMagic](http://git.oschina.net/xiaodaizi/BlockMagic) 。 关于 Block 的疑惑 ------------ 到目前为止,一切看起来都很美好(如果你照着上面的例子做的话),功能正常, A 视图中也获取到数据了。但是某些人可能就要说了,你的代码有问题,你的思路有问题,你这是误人子弟。 是的,代码的确还有问题,第一个问题就是循环引用的问题,在 A 视图的block 代码块中: ``` bVC.callBackBlock = ^(NSString *text){ NSLog(@"text is %@",text); self.label.text = text; }; ``` 代码 `self.label.text = text;` ,在 Block 中引用 self ,也就是 A ,而 A 创建并引用了 B ,而 B 引用 `callBackBlock`,此时就形成了一个循环引用,而编译器也不会报任何错误,我们需要非常小心这个问题(面试百分百问到我会乱说?)。此时我们通常的解决方法是使用弱引用来解除这个循环: ``` __weak AViewController *weakSelf = self; bVC.callBackBlock = ^(NSString *text){ NSLog(@"text is %@",text); // self.label.text = text; weakSelf.label.text = text; }; ``` 第二个问题是我自己对 Block 的理解不到位,我们都知道 Block 能截取自动变量,并且是不能在 Block 块中进行修改的(除非用`__block`修饰符),但是很明显 `weakSelf.label.text`的值被修改了,并且没有用`__block`修饰符, 这是为什么呢?因为 `label` 是个全局变量,而如果像如下的局部变量 `a` 是不能修改的,编译器也会报错: ![局部变量](http://upload-images.jianshu.io/upload_images/928928-13f64f2fd46f30fa?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 通过这个小例子发现的两个问题,也算是值得了。 Block 为什么能实现神奇的回调 -------------- 在这里我不会说什么实现原理,仅仅是个人对 Block 能实现神奇回调的理解,有错误的地方请大家指出。 在先前使用 Block 的过程中,虽然会使用,但是总是有一个疑惑,简单说来就是: 为什么在 A 中的 block 块能调用到 B 中的数据? 回顾一下我们在 B 中所实现的代码,不外乎定义了一个 Block 变量,并在适当的时候传入参数,那么为什么在调用了 `self.callBackBlock(_textField.text)` 之后,值就神奇传到了 A 中的 Block 块了呢? 通过整理使用的过程,我发现是我们的思维陷入了误区(可能是我个人),我们认为在 B 中传入 `_textField.text` 参数之后, A 中的 Block 块就可以获取到值。虽然思路是对的,但其实是不完整,导致我们形成了回调的数据是通过某种底层实现传递过去的错觉,这就使得我们认为这不需要深究。 事实是,通过简单的整理我们可以发现完整的回调流程应该是这样的: ![回调流程](http://upload-images.jianshu.io/upload_images/928928-e4de1ec1692eb173?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 1. block 代码块赋值给 `bVC.callBackBlock`,此时 `callBackBlock` 的指针就指向这个代码块。 2. 调用 `callBackBlock(NSString *text)` 3. 由于 `callBackBlock` 的指针是指向 A 中的 block 代码块,因此执行代码块的代码,实现回调。 很显然之前我忽略了代码块赋值给 `callBackBlock` 的这个操作(羞愧)。 现在再通过一段代码可以更清晰地理解这个原理: ``` bVC.callBackBlock = ^(NSString *text){ //1 NSLog(@"text is %@",text); }; bVC.callBackBlock = ^(NSString *text){ //2 NSLog(@"text b is %@",text); }; ``` 上述代码中,我们对 `callBackBlock`进行了两次赋值,结果会怎么样呢? ![two block](http://upload-images.jianshu.io/upload_images/928928-2788415bb549621d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 可以看出来,Block 的回调只对代码 2 生效,因为`callBackBlock`的指针最后指向了代码 2 的代码块。所以并没有什么神奇的魔法,也没什么隐藏的底层机制(这里指的是方便理解的底层)让你可以带着疑惑去使用它。 总结 -- 我这个人学习方法,总结起来就是看到新技术,先在自己的代码里跑一遍,能跑通,并且使用起来没有什么难度,就基本不会深究了(如果遇到某个熊孩子就坑了)。但是自我反思过,这样的学习方法是很不对的,写代码不能不求甚解,如果想要有所突破,不想局限于码农,一定要深入探究一下实现的机制,最起码要保证不带着疑惑去使用。]]></content>
</entry>
<entry>
<title><![CDATA[在 WordPress 中使用 Github README 标签]]></title>
<url>%2F2016%2F02%2F20%2F%E5%9C%A8WordPress%E4%B8%AD%E4%BD%BF%E7%94%A8GithubREADME%E6%A0%87%E7%AD%BE%2F</url>
<content type="text"><![CDATA[title: "在 WordPress 中使用 Github README 标签" date: 2016-02-20 tags: [博客,WordPress,Github] categories: [博客技巧] description: Github 上的很多框架和包都在他们的 README 文件中使用 “badges”(标签)记录 repository 的不同属性。 --- 原文链接=http://dev.iachieved.it/iachievedit/github-readme-badges-in-wordpress/ 作者=Joe 原文日期=2016/01/24 ------------------ Github 上的很多框架和包都在他们的 README 文件中使用 “badges”(标签)记录 repository 的不同属性。 - 一个 repository 的 Travis 构建(译者注:Travis CI 是开源持续集成构建项目)是否通过 - 一个 release 版本代码的下载次数 - 代码支持的平台(为苹果设备开发时尤其有用) ![这里写图片描述](http://dev.iachieved.it/iachievedit/wp-content/uploads/2016/01/githubbadges.png) 自2014年6月初次发布以来,[Swift 编程语言](https://en.wikipedia.org/wiki/Swift_%28programming_language%29)已经经历过了一系列的改变和版本。每一个发行版本都包含了破坏性的改变。从这篇文章开始,我已经开始使用标签去指明文章所兼容的 Swift 版本。 添加标签 -------------- 你可以在你的 WordPress 文章中通过两种技术使用标签。严格地获取标签的最好方式是使用内联图片设计使之看起来像,好吧,就是像标签。你也可以自己创建图片或者使用类似 [Shields.io](http://shields.io/) 的服务去链接标签。不管什么方式,为了在你的页面展示标签你都应该使用``。一个通过 Shields.io 链接标签的描述例子如下: ``` ``` 这会出现这样的标签:Swift 2.2 此外你还可以使用 Markdown 语法(如你在 Github 的 README.md 文件所见的那样)。为了在 WordPress 中使用 Markdown,你可以加载 [jetpack](https://wordpress.org/plugins/jetpack/) 组件,然后激活它的 Markdown 组件。通过 Markdown 的支持激活创建一篇新的文章,并且可以在文章编辑这样的类型: ![Swift 2.2](https://img.shields.io/badge/Swift-2.2-orange.svg?style=flat) 这使用了 [Markdown 的图片语法](https://daringfireball.net/projects/markdown/syntax#img),并且可以出现这样的标签:Swift 2.2 Shields.io ---------- Shields.io 的设计理念是: “标签是一个服务”。换句话说,无需担心创建你自己的标签,Shields.io 会为你创建标签。大多数的 Shields.io 标签在语义上和“一些东西”的状态捆绑在一起。例如,URL:https://img.shields.io/github/downloads/atom/atom/total.svg 提供了一个标签指明 [Atom](https://github.com/atom/atom) 程序被下载的所有次数。Shields.io 首先通过联系 Github 的接口获取到真实的下载数量,然后返回生成的图片。 上述 Swift 的例子使用了这个 Shields.io URL: `https://img.shields.io/badge/--.svg`。我们通过提供如下几个选项使用他: - SUBJECT 为 Swift - STATUS 为 2.2 - COLOR 为 orange 当然,orange 是由于它是Swift的代表色。 准确的评价 ----- 我非常推荐每一个Swift 博主开始使用标签 (或者一些等同的形式)去指明 Swift 语言的版本,如例子所示的那样。例如,尽管 C 风格的循环已经在 Swift 2.2 中废弃了,但是会在 3.0 中产生错误。当某个人看到你的 2.2 版本的文章,但是试图使用 3.0 的编译器运行代码,他们至少应该知道有些代码可能不兼容。]]></content>
</entry>
<entry>
<title><![CDATA[如何简单地模拟 NSURLSeesion 的返回数据]]></title>
<url>%2F2016%2F01%2F18%2FAnEasyWayToStubNSURLSession%2F</url>
<content type="text"><![CDATA[title: "如何简单地模拟 NSURLSeesion 的返回数据" date: 2016-01-18 tags: [test,效率,NSURLSeesion] categories: [Test] description: 如果你熟悉我这个博客的话,你可能知道我检查问题时,最喜欢的方法是模拟 `NSURLSeesion` 返回的数据。那么我们到底要做什么呢,其实是模拟方法的回调数据 --- 原文链接=http://swiftandpainless.com/an-easy-way-to-stub-nsurlsession/ 作者=dom 原文日期=2016/01/09 ------------------ 如果你熟悉我这个博客的话,你可能知道我检查问题时,最喜欢的方法是模拟 `NSURLSeesion` 返回的数据。 那么我们到底要做什么呢,其实是模拟方法的回调数据。而这里的 `NSURLSession`指的是伪造 web API 的响应。这样做有一些好处,例如: 1. 我们不需要一个可用的 web API 来开发我们应用程序的网络请求。 2. 能够立马响应,反馈周期更短。 3. 测试程序能在没有网络连接的电脑上运行。 一般来说,模拟 `NSURLSession` 的请求返回数据是通过 `NSURLProtocol` 来完成的。具体的用例请查看 [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs) 和 [Mockingjay](https://github.com/kylef/Mockingjay)。使用 `NSURLProtocol` 的优势在于,当你使用诸如 [Alamofire](https://github.com/Alamofire/Alamofire) 这样的网络请求库时,也能正常模拟数据回调。这种方式很棒,但是对我来说代码太多了。我必须去学习和理解这些代码,以在我的测试中获得预期的效果。 一个简单的解决方案 --------- 我将使用 `NSURLSession` 来做网络请求。下面是如何伪造我的请求返回数据。 为了让它看起来更简单,我已经写了一个 `NSURLSession` 的替换类和一个协议。整合起来如下所示: ``` import Foundation public protocol DHURLSession { func dataTaskWithURL(url: NSURL, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask func dataTaskWithRequest(request: NSURLRequest, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask } extension NSURLSession: DHURLSession { } public final class URLSessionMock : DHURLSession { var url: NSURL? var request: NSURLRequest? private let dataTaskMock: URLSessionDataTaskMock public init(data: NSData?, response: NSURLResponse?, error: NSError?) { dataTaskMock = URLSessionDataTaskMock() dataTaskMock.taskResponse = (data, response, error) } public func dataTaskWithURL(url: NSURL, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask { self.url = url self.dataTaskMock.completionHandler = completionHandler return self.dataTaskMock } public func dataTaskWithRequest(request: NSURLRequest, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask { self.request = request self.dataTaskMock.completionHandler = completionHandler return self.dataTaskMock } final private class URLSessionDataTaskMock : NSURLSessionDataTask { typealias CompletionHandler = (NSData!, NSURLResponse!, NSError!) -> Void var completionHandler: CompletionHandler? var taskResponse: (NSData?, NSURLResponse?, NSError?)? override func resume() { completionHandler?(taskResponse?.0, taskResponse?.1, taskResponse?.2) } } } ``` 如上,用来伪造数据的完整帮助代码是 47 行。并且所有代码清晰易懂,既没有 swizzling,也没有复杂的方法。是不是很棒! 使用 -- 为了能够在测试中使用 `NSURLSession` 替换类,我们需要在代码中注入依赖。一种可能的方式是使用一个懒属性: ``` lazy var session: DHURLSession = NSURLSession.sharedSession() ``` 然后一个示例测试可能会是这样的: ``` func testFetchingProfile_ReturnsPopulatedUser() { // Arrage let responseString = "{\"login\": \"dasdom\", \"id\": 1234567}" let responseData = responseString.dataUsingEncoding(NSUTF8StringEncoding)! let sessionMock = URLSessionMock(data: responseData, response: nil, error: nil) let apiClient = APIClient() apiClient.session = sessionMock // Act apiClient.fetchProfileWithName("dasdom") // Assert let user = apiClient.user let expectedUser = User(name: "dasdom", id: 1234567) XCTAssertEqual(user, expectedUser) } ``` 我很喜欢这样的解决方案,因为我只要花几分钟时间,通过阅读五十多行代码就能理解替换类。并且没有涉及到 `NSURLProtocol` 和 `swizzling`。 这个 `NSURLSession` 的替换类在 [github](https://github.com/dasdom/DHURLSessionStub) 上,并且也可以通过CocoaPods 下载。 让我知道你的想法。]]></content>
</entry>
<entry>
<title><![CDATA[Swift:带有私有设置方法的公有属性]]></title>
<url>%2F2016%2F01%2F08%2FSwift-PublicPropertiesWithPrivateSetters%2F</url>
<content type="text"><![CDATA[title: "Swift:带有私有设置方法的公有属性" date: 2016-01-08 tags: [Swift,封装] categories: [Swift 入门] description: Swift可以很方便地创建带有私有设置方法的公有属性。这可以让你的代码更加安全和简洁。 --- 原文链接=http://www.thomashanning.com/public-properties-with-private-setters/ 作者=Thomas 原文日期=2015/12/24 ---------- Swift可以很方便地创建带有私有设置方法的公有属性。这可以让你的代码更加安全和简洁。 封装 -- 封装从根本上意味着类的信息和状态应该对外部类隐藏,只有类自身可以操作。因此,所有的 bug 和 逻辑错误更加不可能发生了。 通常你会使用 setter 以及 getter 来达到封装的目的。然而,有时候你可能不想对外提供类中的设置方法。对于这样的情况,你可以使用带有私有设置方法的属性。 例子 -- 假设我们想要创建一个代表圆的类,那么圆的半径应该是可以改变的。而且,该圆的面积和直径应该可以从圆的实例中获取,而这两个属性不允许被外部类更改。出于性能考虑,面积和直径只能计算一次。 所以这个圆类应该是这样的: ``` class Circle { private var area: Double = 0 private var diameter: Double = 0 var radius: Double { didSet { calculateFigures() } } init(radius:Double) { self.radius = radius calculateFigures() } private func calculateFigures() { area = M_PI * radius * radius diameter = 2 * M_PI * radius } func getArea() -> Double { return area } func getDiameter() -> Double { return diameter } } ``` 现在所有的需求都满足啦。然而,Swift 提供了一种更好的方式,可以使得这段代码更加简洁: 带有私有设置方法的属性 ----------- 通过在属性前面使用 `private(set)` ,属性就被设置为默认访问等级的 getter 方法,但是 setter 方法是私有的。所以我们可以去掉两个 getter 方法: ``` class Circle { private(set) var area: Double = 0 private(set) var diameter: Double = 0 var radius: Double { didSet { calculateFigures() } } init(radius:Double) { self.radius = radius calculateFigures() } private func calculateFigures() { area = M_PI * radius * radius diameter = 2 * M_PI * radius } } ``` 当然也可以为属性设置公有的 getter 方法: ``` public class Circle { public private(set) var area: Double = 0 public private(set) var diameter: Double = 0 public var radius: Double { didSet { calculateFigures() } } public init(radius:Double) { self.radius = radius calculateFigures() } private func calculateFigures() { area = M_PI * radius * radius diameter = 2 * M_PI * radius } } ``` 对象 -- 在这个例子中,属性只是 `Double` 值。然而,如果是一个对象,可以通过使用对象的方法来操作!使用私有设置方法只允许设置一个全新的对象,在使用过程中应铭记这一点。]]></content>
</entry>
<entry>
<title><![CDATA[iOS 启动时优化]]></title>
<url>%2F2016%2F01%2F05%2FiOS%E5%90%AF%E5%8A%A8%E6%97%B6%E4%BC%98%E5%8C%96(1)%2F</url>
<content type="text"><![CDATA[title: "iOS 启动时优化" date: 2016-01-05 tags: [优化,Facebook,启动时] categories: [性能优化] description: Facebook 工程师通过一系列系统的考量寻求优化解决方案的方式。首先通过建立优化的度量指标,明确优化方向,分解优化目标,分步达到优化目的,最后统一测试优化效果。 --- ------------------ 译者:本文虽是针对 Facebook 应用的启动时优化,文中所说的大部分优化策略对于小型应用来说意义可能并不是很大,但是重要的是,我们应学习Facebook 工程师通过一系列系统的考量寻求优化解决方案的方式。首先通过建立优化的度量指标,明确优化方向,分解优化目标,分步达到优化目的,最后统一测试优化效果。相较于杂乱无章,碰运气式的优化经验,这种清晰有条理的解决方式,着实令人敬佩。 --------- 提高 Facebook 应用的性能已经成为 Facebook 持续关注的领域。因为我们相信一个高性能的应用能够传递一种吸引人且令人愉悦的体验。每个 Facebook 应用的用户都必须做的一件事是启动应用(我们特指这个动作为 ”应用启动“)。因此,这是一个很好的优化目标。 稳定的度量 ----- 实现最好的性能度量标准和相应的目标,鼓舞我们专注于提升应用的品质,并且我们相信这将会产生很大的影响。度量必须易懂、经得起推敲,并且需要精确地捕捉到将要被优化的体验。对基于性能的度量,我们已经发现在使用应用过程中,最好是使用那些被捕捉到感知的交互。理想情况下,这些度量应该和一个通过基础设施的单一执行通道有一对一的联系。对于应用程序的启动,确定用于衡量的关键位置是一个挑战。这需要采取几次迭代去简化我们的测量和移除边界问题。 应用启动是一个特别不固定的概念,因为现在存在很多种应用启动的方式。应用可以在后台或者前台启动,甚至可以在后台启动,但是在完成初始化之前转换为前台。你可以通过点击一条通知或者通过一个 URL 打开应用。Facebook 应用甚至可以通过其他应用来打开,因为他们需要通过 Facebook 来实现第三方登录。在现实场景中,主要的交互还是最直接的方式:你点击桌面的应用图标,然后跳转启动。因而,我们选择这个作为应用启动的入口。 当启动入口明确之后,我们必须去计算出何时应用启动是完成的。同样地,我们观察用户的使用模式,发现用户喜欢打开应用(首先跳转到新闻摘要),然后等待摘要的加载。我们断定“摘要完成加载”是应用启动一个很好的终点。我们采取了一些微调使得这个终点契合用户的使用情况。我们可以通过重复地观察应用的启动,围绕度量标准来提高应用的性能。 一旦确定了我们认为有代表性的启动入口和终点,我们把启动问题分解成两种类型: 1. 冷启动。指的是当应用还没准备好运行时,我们必须加载和构建整个应用。这包括设置屏幕底部的分栏菜单,确保用户是否被合适地登录,以及处理其他更多的事情。“引导”程序是在`applicationDidFinishLaunching:withOptions:`方法中开始的。 2. 热启动。指的是应用已经运行但是在后台被挂起(比如用户点击了 home 健),我们只需要知道在何时应用进入后台。在这种情况下,我们的应用通过 `applicationWillEnterForeground:` 接收到前台的事件,紧接着应用恢复。 我们决定主要优化冷启动,主要有两个原因。首先,冷启动其实是包括热启动的(冷启动初始化应用并获得摘要;热启动只获得摘要),所以有更多的地方需要优化和微调。第二,冷启动需要做额外的初始化工作,所以相较而言更慢,导致需要更长的启动等待时间。 优化冷启动体验 ------- 我们把冷启动问题分解成三个阶段,进而我们可以有针对性地解决。每个阶段都有一些列变数和挑战。 1. 请求时间:从应用启动到摘要请求离开设备(译者:应该是向服务器发送URL请求算结束时间)的时间。 2. 网络时间:从摘要请求离开设备到服务器响应返回的时间。 3. 响应处理时间:从响应返回到新数据展示在屏幕的时间。 我们直观上认为冷启动性能主要被网络请求和响应处理影响了。这个结论是由于我们假定我们在客户端花的时间比较少,并且我们设法让请求的获取更加快速。然而,当我们用 instrument 去检测时,我们发现数据非常出人意料。它展现出了完全不同的结果,我们发现摘要请求花了大部分时间。另外,响应的处理时间也非常短。因此,我们重新把优化的焦点放在初始化阶段。 摘要请求发送的初始化 ---------- 所以为什么这个阶段花费了那么多时间呢?很多 iOS 应用并没有这样一个问题——他们在那个阶段并没有很多工作需要做,除了初始化视图控制器和发送网络请求。然而,对于 Facebook 来说,大部分时间被用来开始的时候去设置不同功能块。下面是我们应用中的主要功能块的流程概览。 ![这里写图片描述](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfp1/t39.2365-6/12057214_1016971454990542_827610883_n.png) 这看起来好像是很复杂的应用启动设置。但需要重视的是,这些功能块对于 Facebook 应用来说是非常重要的提升,可以提高应用体验,并且使得工程师能够在不同的应用规模下更快地开发。 正如我们所关注的这个流程,我们通过优化独立的部分获得了一些主要的成果。然而,由于未来支持新特性的初始化以及额外提供支持的基础设施,这些成果会慢慢地抵消掉。这使得我们重新考虑如何去解决问题。但我们重新开始,我们认为这个阶段的目标是简单地发送摘要的网络请求。但是为什么摘要请求发出去得这么慢?这是由于很多依赖被添加到摘要的初始化中了。然而,他们并不都是必要的 — 对于摘要请求来说,最少的需要一个有效的验证 token 以及摘要光标(新闻摘要的位置)。因此,我们减少了摘要请求的依赖,让它逐渐地更加接近应用的启动。这允许应用的剩余部分在摘要响应的同时进行初始化。由于这些重构,我们获得了显著的收益。 网络和服务器时间 -------- 根据我们在第一阶段的经验,我们继续把这个阶段分解成更小的部分。网络请求/响应看起来像这样: ![这里写图片描述](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xtp1/t39.2365-6/12056998_991399770918380_262846919_n.png) 我们注意到,一旦请求正在排队,发送请求出去之后就有一个时间间隔。这很好解释 — 在冷启动中,网络连接并不是一个开放的、安全的 TCP 连接。一个连接的建立需要三次握手,平均为几百毫秒。当摘要请求第一次发送时,无法避免要花掉这些时间。长远来看,这可以通过缓存 SSL 证书来解决。但是再次强调,我们退回来的目的并不是为了发送 TCP 请求,而是为了从服务器通过任何可能的方式获得请求信息。 我们提出了一个创造性的解决方案 — UDP 启动。本质上,我们在通过 TCP 发送摘要请求时,先发送一个编码过的包含摘要请求的 UDP 包到服务器。这样做的目的是唤醒服务器更早地去获取和缓存数据。当真正的摘要请求通过 TCP 到达时,服务器只需见到地从缓存内容中构造出响应,并发回客户端。这个技术使得我们可以减少几百毫秒的耗时。 当我们持续深入研究服务器端时,我们开始尝试使用 层-取(story-fetching)策略。过去我们已经做了一批摘要请求的 3+7 层。原因很简单:下载次数和被下载的层成正比。因此,把请求分割成两块,允许开始的三层先进来,其余的七个随后进来。通过提升我们的基础设施,我们已经能够升级为 1+1+X 策略,这已经接近于流了。这样就减少了服务器必须处理第一层的时间,并且能够减少下载的时间,使得可以在最快的时间内与用户交互。通过这样的努力,这样我们又减少了几百毫秒的耗时。 摘要响应处理 ------ 正如在前面提到的那样,我们以为在启动时会在这里花费大量的时间。但是这个想法被证明是错误的。更加使人好奇的是,我们注意到时间并没有花在处理和加工层上面。时间被花在运行应用服务和竞争资源上面。我们注意到这是我们优化网络和服务器时间的副作用,因为摘要请求返回得太早了。尽管大多数的服务是不重要的。因此,我们开发出一个简单的机制去序列化这些工作直到应用完成启动,并且使用先进先出的方式去执行。这样可以用更少的连接去处理所有层,大大地减少了获得响应和展示在屏幕之间的时间。 总结 -- 很难理解我们在过去几个月走了多远。总之,在一对一的比较中,我们发现我们成功地优化了一秒多的耗时。 优化这个特殊的交互是一个长期的过程,需要建立一个稳定的度量,这个度量必须是易懂的、符合真实世界性能特征,此外要不断地重新思考问题,以提出创新的解决方案。我们希望这可以帮助使用 Facebook 的人有更好的、令人愉悦的用户体验。 你也可以看看 [Greg Moeck在 2015 年的演讲](https://youtu.be/ifozUqqC0TY?t=11m5s) 。]]></content>
</entry>
<entry>
<title><![CDATA[关于 Swift 演变的趣味探讨]]></title>
<url>%2F2015%2F12%2F27%2Finteresting-discussions-on-swift-evolution%2F</url>
<content type="text"><![CDATA[title: "关于 Swift 演变的趣味探讨" date: 2015-12-27 tags: [Swift] categories: [Swift 入门] permalink: interesting-discussions-on-swift-evolution description: 记得我曾分享过一些想法和建议,比如:newtype 。一个是建议 Swift 推出一个 newtype 的关键词,它可以添加完全不同于原生的可扩展的派生类型。 --- 原文链接=http://ericasadun.com/2015/12/15/interesting-discussions-on-swift-evolution/ 作者=Erica Sadun 原文日期=2015/12/15 ------------------ 记得我曾分享过一些想法和建议,比如: ### newtype 一个是建议 Swift 推出一个 `newtype` 的关键词,它可以添加完全不同于原生的可扩展的派生类型。例如: ```swift newtype Currency = NSDecimal ``` 这创建了一个拥有所有 `NSDecimal` 所有行为的 `Currency` 类型。然而,你不能让一个 `NSDecimal` 类型的元素和一个 `Currency` 类型的元素相加,因为 Swift 中有类型检测。此外,你也可以扩展 `Currency` 类型。这样看起来就更加有针对性,因为不需要子类化或者添加新的存储属性。 `newtype` 的另一个特性是能够创建柯里化类型: ```swift newtype Counter = Dictionary ``` 类型是部分确定的,具体行为可以在扩展中实现,从而能包含键(key)类型不相同但值类型都是 Int 的字典。 期待看到你们的评论。 ### self 另外一个提议是将 `self` 作为强制前缀,取代上下文语境推断。Greg Parker 在回复中写道: > 在 Objective-C 中 `self.property` 这种写法很不优雅。 > > 第一种方法是只使用 `property`。但是同名变量(ivar)会产生歧义,Swift 没有这样的问题。 > > 第二种方法是用 `property` 访问属性,用 `self->ivar` 去访问同名变量。这是不可行的,因为会和现有的大量代码冲突。Swift 也没有这样的问题。 ### 前置条件与断言(Precondition vs Assert) Dave Abrahams 提出了一个有关重命名断言和前置条件的建议,我立刻将其中的一些深刻见解记在笔记本上: > 从语言设计层面来说,这两个函数扮演不同的角色: > – assert:检查内部的错误代码。 > – precondition:检查客户端给你的参数是否有效。 > > 两者的区别很大,第二个要求有公共文档,第一个不需要。 > > 例如:在 Swift 的标准库中,我们保证永远不会出现内存错误,除非你调用 (Obj)C 代码或者使用一个明确地标着「unsafe」的结构。我们需要去检验客户端参数,为了避免给了非法的参数引起内存泄露,我们要在参数中文档化这些需求作为前置条件,并且使用(等价的)precondition() 去检验它。我们还有一系列的内部合理检查,用以确定我们代码假定的正确性,而类型系统还不能保证这个代码的假定。由于这些原因,我们使用(等价的)assert(),因为我们不想降低*你的*代码性能(使用合理的检查)。 > > 下面是几个具体的例子: ```swift /// 一个集合,其中的元素类型为 Element public struct Repeat : CollectionType { ... /// 获取 `position` 位置的元素 /// /// - 要求: `position` 是 `self` 中的有效位置并且 `position != endIndex`. public subscript(position: Int) -> Element { _precondition(position >= 0 && position < count, "Index out of range") return repeatedValue } } extension String.UTF8View { ... private func _encodeSomeContiguousUTF16AsUTF8(i: Int) -> (Int, UTF8Chunk) { _sanityCheck(elementWidth == 2) _sanityCheck(!_baseAddress._isNull) let storage = UnsafeBufferPointer(start: startUTF16, count: self.count) return _transcodeSomeUTF16AsUTF8(storage, i) } } ``` > 在第一个例子中,我们有一个判断客户的 collection 没有越界的前置条件。在这个例子中,我们其实可以不做检查,因为越界也不会导致内存错误(因为返回的都是同一个 repeatedValue),但是我们还是加上了这个检查,这样我们的用户可以快速发现他们的 bug 。 > > 第二个例子中是一个私有函数,它只能在我们保证 elementWidth == 2 和 _baseAddress 不为 null 的条件下调用(_sanityCheck 在 stdlib 下等价于 assert)。因为这是私有函数,使用者就是我们自己,所以看起来这个检查可以省略。但是有时候会出意外,比如后续的开发者可能会错误地使用它,因此我们需要添加检查。因为我们在 debug 和 release 的环境下运行我们的测试,并且有较高的测试覆盖率,因此(如果错误使用函数)断言很可能在某处被触发。 > > 读完上面的内容,你可能认为 assert() 只能在私有方法中使用,而 precondition() 只能在公共方法中使用。事实并非如此;你可以内联任何私有方法到继承的公有方法的方法体内,因此合理的检查依然有意义。前置条件检查也会偶尔在私有方法中使用,最简单的例子就是公有方法转私有方法,复制代码的时候可以把原来的前置条件检查提取成一个私有的辅助方法(Helper)。 > > *注意,有些前置条件实际上不会被执行,所以你不能指望所有的前置条件都被执行。]]></content>
</entry>
<entry>
<title><![CDATA[如何简单地为测试切换 App Delegate]]></title>
<url>%2F2015%2F12%2F19%2FHowtoEasilySwitchYourAppDelegateforTesting%2F</url>
<content type="text"><![CDATA[title: "如何简单地为测试切换 App Delegate" date: 2015-12-19 tags: [AppDelegate,test,效率] categories: [Test] description: 这是因为当你测试运行时,首先要启动你的应用——而这个过程可能做了很多事情,大量耗时的操作。而这些耗时的操作在测试的时候并不是我们所需要的。我们应该如何避免这个问题? --- 原文链接=http://qualitycoding.org/app-delegate-for-tests/ 作者=Jon Reid 原文日期=2015/03/17 ------------------ [测试驱动的开发最大好处是能够有快速反馈](http://qualitycoding.org/benefit-of-tdd/)(译者:这是作者的另一篇文章,讲述了测试驱动的好处,有兴趣的可以看看)。所以,为了确保你的 TDD 效率,最好的方式就是尽可能快地获得反馈。 但是很多 iOS 开发者会在测试的时候使用生产环境(译者:应用开发中的不同阶段,一般分为开发环境 development,处于产品开发阶段;生产环境 production,即正式上线的环境,更详细的请参照 [Development, testing, acceptance and production](https://en.wikipedia.org/wiki/Development,_testing,_acceptance_and_production))的 app delegate。这是一个影响效率的问题。 你的常规 app delegate 在用于测试时是否跟龟速一样? ![这里写图片描述](http://qualitycoding.org/jrwp/wp-content/uploads/2015/03/turtle.jpg) 这是因为当你测试运行时,首先要启动你的应用——而这个过程可能做了很多事情,大量耗时的操作。而这些耗时的操作在测试的时候并不是我们所需要的。 我们应该如何避免这个问题? 测试流程 ---- Apple 习惯将单元测试归为两类:应用测试和逻辑测试。这个区别是非常重要的,因为在以前,应用测试只能在设备上运行,除非你使用完全不同的第三方测试框架。 但是这个差异现在消失了,因为 Apple 允许我们在模拟器上运行应用测试。Apple 花了很多时间来更新文档,直到在他们最新的[Xcode测试](https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/testing_with_xcode/Introduction/Introduction.html)才更新了这部分说明,Apple 现在称之为 "app tests" 和 "library tests"。这就使事情简化为你是开发一个应用还是一个库。并且 Xcode 为你设置了一个测试用的 target ,这正是你所需要的。 如果我现在开发一个应用(或者一个需要运行应用的库),我总是会运行应用测试,所以我[停止去试图区分这两种类型的测试](http://qualitycoding.org/xcode-unit-testing/)。但是由于 Xcode 是在一个运行的应用的上下文环境下执行应用测试,测试流程就变成这样: 1. 启动模拟器 2. 在模拟器中,启动应用 3. 将测试 bundle 注入运行的应用 4. 运行测试 那么我们怎么才能加快这个流程呢?我们可以在第二步中做文章,让应用尽可能快地启动。 普通的 app delegate ---------------- 在开发环境下,启动应用可能会关闭很多任务。Ole Begemann 在 [Revisiting the App Launch Sequence on iOS](http://oleb.net/blog/2012/02/app-launch-sequence-ios-revisited/)中进行了详细的解释,但是根本上, `UIApplicationMain()` 最终会调用 app delegate去执行 `application:didFinishLaunchingWithOptions:` 。具体的流程一般取决于你的应用,但是很少会像下面这么做: 1. 创建 Core Data。 2. 配置根视图控制器 3. 检测网络连通性 4. 向服务器发送一个网络请求去取回最近的配置,例如应该在根视图中展示的东西。 因此在开始测试之前要做很多事情。难道不能使我们的测试不受干扰,如果我们想要的只是运行我们的测试程序? 让我们来解决这个问题,下面是具体方案。 改变 main 函数 ----------- 让我们改变我们的 main 函数,如下所示: ``` #import #import "AppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } } ``` 我们现在想要去检查是否我们在运行测试代码。如果想要这么做的话,我们想要去使用一个不同的 app delegate。我们可以这么做: 最早的版本 ``` #import #import "AppDelegate.h" #import "TestingAppDelegate.h" int main(int argc, char *argv[]) { @autoreleasepool { BOOL isTesting = NSClassFromString(@"XCTestCase") != Nil; Class appDelegateClass = isTesting ? [TestingAppDelegate class] : [AppDelegate class]; return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass)); } } ``` 从根本上来说,如果 XCTestCase 链接好了,我们就会使用 `TestingAppDelegate`。否则,我们退而使用生产环境的 app delegate。然后我们启动应用时可以选择我们想要的 app delegate。(注意:TestingAppDelegate 必须在生产环境的 target 中) 现在这些代码已经实现了来回切换。上述部分的实现从根本上和我原先的文章一致。因为有一段时间,根据评论中的建议,我将代码改为: ``` @autoreleasepool { Class appDelegateClass = NSClassFromString(@”XYZTestingAppDelegate”); if( appDelegateClass == nil ) { appDelegateClass = [DOAAppDelegate class]; } return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass)); } ``` 但是在Xcode7上不能正常运行,所以我又改回原始版本。 如果你想在单元测试外部使用 XCTest 该怎么办,例如 UI 测试?为了取代为 XCTestCase 做的测试,你可以设置一个环境变量,通过 getenv 来测试。 提供 TestingAppDelegate -------------------------- 这里需要创建一个 TestingAppDelegate 类。正如下面代码所示: TestingAppDelegate.h ``` #import @interface TestingAppDelegate : UIResponder @property (nonatomic, strong) UIWindow *window; @end ``` TestingAppDelegate.m ``` #import "TestingAppDelegate.h" @implementation TestingAppDelegate @end ``` 正如你所看到的那样,不要做任何事。 (在早先的 iOS 版本中,我必须添加更多的代码,导致 TestingAppDelegate 会创建一个 window,给这个 window 设置一个不做任何事情的根视图,然后让其可见。现在看来没必要了。) 快速反馈的本质 ------- 最重要的事情是我们已经从本质上减少了测试过程中启动应用的步骤。尽管还有一些不必要的开销,但是并不多。这是实现快速反馈过程中重要的一步,这样我们就可以从 TDD 中获得更多。 甚至当你开始一个新的项目,我推荐尽早使用这样的方法,因为你真正的app delegate最终会变得日益庞大。让我们在襁褓中阻止这种问题,然后保持快速的反馈。 另外一个好处是,通过完全控制哪部分该测试,什么时候测试,我们现在可以编写跟生产环境的app delegate完全不同的单元测试。这显然是双赢的。]]></content>
</entry>
<entry>
<title><![CDATA[Core Animation & Facebook's POP]]></title>
<url>%2F2015%2F12%2F17%2FCoreAnimation%26Facebook'sPOP%2F</url>
<content type="text"><![CDATA[title: "Core Animation & Facebook's POP" date: 2015-12-17 tags: [个人,POP,CoreAnimation,Facebook] categories: [Animation] description: 对比系列,是个人比较喜欢的一种学习方式,通过对比,找出不同技术的优缺点,可以更合理地使用这些武器,俗话说:好钢用在刀刃上,大抵如此。本文对 CoreAnimation 和 Facebook 的 POP 动画库进行了对比。 --- ------------------ 前言 -- 相信很多人对实现 iOS 中的动画效果都特别头疼,往往懒得动手,功能实现不就得了,何必要那么花哨、装13的东西。但是看到别人的炫酷动效,心中又瘙痒不已,便下定决心学习,于是开始翻看 Core Animation、UIView动画(其实是[对Core Animation的一种封装](http://www.jianshu.com/p/72f4cca98b0e))相关资料。不小心看到一群大神正在热烈讨论,钻一进去一看,原来是 [POP](https://github.com/facebook/pop) (潜意识:Facebook出品必属精品),这还学什么Core Animation,果断pod一个来玩玩,于是你就左手CA,右手 POP 开森地把玩起来了。 此时,你可能已经学会了CA的基本使用方法,也对UIView动画的便捷感到惊喜,但是不满足的你,显然有更高的追求,POP 以其灵活的用法,丰富的动效,完整的API文档,深得很多程序员的喜爱。作为一个有逼格的程序员,这么流行的框架,必然是值得深入学习的,但是你是否考虑过这样的第三方动画框架是否存在什么不足。因此,作为一个有追求的程序员,有必要来稍微深入地探讨一下 Core Animation 和 POP 不同点。 Core Animation 工作机制 -------------- 首先我们需要了解CA是如何工作的。每当我们创建并添加动画到 layer 时,[QuartzCore](https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/QuartzCoreRefCollection/index.html) 框架就会把动画的参数打包好,然后通过 IPC (处理器)发送给名为 [backboardd](https://theiphonewiki.com/wiki/Backboardd) 的后台处理程序。你的应用也会发送当前展示在屏幕上的每一个 layer 的信息。 backboardd 会处理 layer 的结构体系然后通过 OpenGL 绘制出来。它还会处理你已经添加过的动画(也可以是视图,因为视图本质是包裹着 layer的)。你一定要理解的是,backboardd 使得动画的每一帧都可以在你的应用中完全独立。这里唯一的回调是动画的开始和结束(详见`CAAnimationDelegate` 协议)。你的应用完全不会参与动画的绘制,这些绘制完全独立于你的应用进程(除非你明确地在你的应用中通过[动画通用属性](http://www.objc.io/issue-12/animating-custom-layer-properties.html)要求绘制动画帧)。这意味着你可以继续在主线程做其他事情,并且不会影响到 [CAAnimation](https://developer.apple.com/Library/mac/documentation/GraphicsImaging/Reference/CAAnimation_class/index.html) 的性能。如果你阻塞了你的主线程,或者你在调试器中暂停了你的程序,你的动画还是会继续执行。 但是你可能会有这样的疑问:每个 [CALayer](https://developer.apple.com/library/mac/Documentation/GraphicsImaging/Reference/CALayer_class/index.html) 不是还有一个 presentationLayer 属性吗? > presentationLayer的官方解释: > > “While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.” 当CAAnimation发生时,你在屏幕上看到的实际上是 presentation layer 的改变。如果你访问 presentation layer,QuartzCore 将会计算现有的帧状态,并且使用这个帧状态去构建 presentation layer 对象。因为动画状态在动画执行期间一直处于改变,因此你将会获得近似值。 POP 工作机制 -------- 现在有很多优秀的第三方动画库,POP 因为其使用灵活、功能强大、文档齐全,所以备受好评,先看一下官方介绍: > POP是一个在iOS与OS X上通用的极具扩展性的动画引擎 它在基本的静态动画的基础上增加的弹簧动画与衰减动画 > 使之能创造出更真实更具物理性的交互动画 POP的API可以快速的与现有的ObjC代码集成并可以作用于任意对象的任意属性 > POP是个相当成熟且久经考验的框架 Facebook出品的令人惊叹的Paper应用中的所有动画和效果即出自POP 更为详细的介绍和使用请查看[官方文档](https://github.com/facebook/pop)以及里脊串的 [POP介绍与使用实践(快速上手动画)](http://adad184.com/2015/03/11/intro-to-pop/)。 POP 本质上是基于定时器的动画库,使用每秒 60 频率的定时器,即时钟频率为 1/60 秒(为了匹配 iOS 显示屏帧率),使得动画刷新绘制频率与屏幕刷新频率一致。很多这类动画库都使用 CADisplayLink 做为一个回调源。 一旦定时器刷新,动画库计算动画的进程,这意味着动画库会计算那些活动的东西的状态(通常是layer 属性,如 bound,opactiy,transform 等)。然后动画库提供最新计算的值给有动画的 layer (或者其他对象)。最主要的区别是,layer 的状态将会在这种情况下改变。 由于 layer 的一些参数已经被改变,你的应用必须通过 IPC 通知 backboardd 处理这些变化。当 backboardd 接收到变化通知(同时接收到的还有应用中的 layer 树),它将在屏幕上重绘一切东西。这意味着,你应用中做的每一个动画帧都会传送数据到 backboardd (即通知 backboardd ),因为 backboardd 完全不知道 layer 发生了什么事情。综上,你的应用就是在这种情况下运行动画的。 Core Animation 和 POP 运行动画对比 --------------------------- 由于 POP 是基于定时器定时刷新添加动画的原理,那么如果将动画库运行在主线程上,会由于线程阻塞的问题导致动画效果出现卡顿、不流畅的情况。更为关键的是,你不能将动画效果放在子线程,因为你不能将对 view 和 layer 的操作放到主线程之外。 为了验证上述的观点,我做了一个实验,首先用CA动画制作一个可以旋转的 view: ``` UIView *viewCA = [[UIView alloc]initWithFrame:CGRectMake(50,50, 100, 100)]; viewCA.backgroundColor = [UIColor blueColor]; [self.view addSubview:viewCA]; CABasicAnimation *caAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; caAnimation.toValue = @(M_PI); caAnimation.duration = 2.0; caAnimation.repeatCount = 500; [viewCA.layer addAnimation:caAnimation forKey:@"anim"]; ``` 再创建一个利用 POP 动画库制作的可旋转 view: ``` UIView *viewPOP = [[UIView alloc]initWithFrame: CGRectMake(CGRectGetWidth(self.view.bounds) - 100 - 50, 50, 100, 100)]; viewPOP.backgroundColor = [UIColor yellowColor]; [self.view addSubview:viewPOP]; POPBasicAnimation *popAnimation = [POPBasicAnimation animationWithPropertyNamed:kPOPLayerRotation]; popAnimation.toValue = @(M_PI); popAnimation.duration = 2.0; popAnimation.repeatCount = 500; [viewPOP.layer pop_addAnimation:popAnimation forKey:@"rotation"]; ``` 在没有线程阻塞的情况下,对比两个动画库的运行效果如下: ![这里写图片描述](http://img.blog.csdn.net/20151217112919922) 可以看出来虽然在没有线程阻塞,但是 POP 的动画在结束时有一个明显的停止动作,是因为 POP 的动画效果不好吗? 答案是 `timingFunction`。 CoreAnimation 和 POPBasicAnimation提供同样的四种 `timingFunction`: > kCAMediaTimingFunctionLinear > kCAMediaTimingFunctionEaseIn > kCAMediaTimingFunctionEaseOut > kCAMediaTimingFunctionEaseInEaseOut > kCAMediaTimingFunctionDefault 重点说一下:kCAMediaTimingFunctionDefault(引自:[iOS-Core-Animation-Advanced-Techniques(五)](http://www.cocoachina.com/ios/20150105/10829.html)) > 它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说是默认的,但还是要记住当创建显式的CAAnimation它并不是默认选项(换句话说,默认的图层行为动画用kCAMediaTimingFunctionDefault作为它们的计时方法)。 如果不设置 `timingFunction` 属性,那么在使用 CA 的情况下, `timingFunction` 是 `kCAMediaTimingFunctionLinear` 的,而 POP 却是`kCAMediaTimingFunctionEaseOut` ,因此我们只要添加这么一行代码: ``` popAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; ``` 现在再看效果: ![这里写图片描述](http://img.blog.csdn.net/20151217114608713) 可以看出来,在主线程没有阻塞的情况下,两种动画库的表现并无差异(POP 就是🐂) 现在我们来制造一点难度,人工利用线程的 sleep 增加一个主线程阻塞: ``` - (void)repeatedlyBlockMainThread { NSLog(@"blocking main thread!"); [NSThread sleepForTimeInterval:0.25]; [self performSelector:@selector(repeatedlyBlockMainThread) withObject:nil afterDelay:1]; } ``` 然后再 `viewDidLoad` 里面调用 : ``` [self performSelector:@selector(repeatedlyBlockMainThread) withObject:nil afterDelay:1]; ``` 现在再来看一下两者的动画效果: ![这里写图片描述](http://img.blog.csdn.net/20151217121603261) 很明显,我们可以看出来,由于添加了主线程阻塞,利用 POP 制作的动画视图,在每隔 1s 都会卡顿一下,而 CA 的视图却完全不受主线程阻塞的影响。 总结 --- 通过这次简单的对比,我们从工作机制上了解了 CA 和 POP 两个动画库的基本原理,并用简单的动画效果对比,重现了在主线程阻塞的情况下两者的差异,很显然, POP 受主线程阻塞的影响很大,在使用过程中,应避免在有可能发生主线程阻塞的情况下使用 POP ,避免制作卡顿的动画效果,产生不好的用户体验。文中提出了 POP 的这种缺点,但是 POP 毕竟是久经考验的动画技术,本人也正在学习中,有错误的地方吝请指正。 对比系列,是个人比较喜欢的一种学习方式,通过对比,找出不同技术的优缺点,可以更合理地使用这些武器,俗话说:好钢用在刀刃上,大抵如此。]]></content>
</entry>
<entry>
<title><![CDATA[UIAlertController 测试的修正]]></title>
<url>%2F2015%2F12%2F02%2FUIAlertController%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BF%AE%E6%AD%A3%2F</url>
<content type="text"><![CDATA[title: "UIAlertController 测试的修正" date: 2015-12-02 tags: [dom,test] categories: [Swift] description: 首先,我们在 UIAlertAction 中添加一个类方法去创建 action 。在 ViewController.swift 中增加如下扩展: --- 原文链接=http://swiftandpainless.com/correction-on-testing-uialertcontroller/ 作者=dom 原文日期=2015/11/25 ------------------ 两个月前,我曾发布了一篇[ 如何测试 UIAlertController ](http://swiftandpainless.com/how-to-test-uialertcontroller-in-swift/)的文章。一个读者发现测试没有如期地起作用: > [@dasdom](https://twitter.com/dasdom) 你的测试是正常的,但是在 `MockUIAction` 中的简便 `init` 方法没有被调用。 你不能重写 `init` 方法,看起来像是 iOS 的bug。 > — Larhythimx (@Larhythmix) [25. November 2015](https://twitter.com/Larhythmix/status/669456137041915905) Larhythimx 说的完全正确。模拟程序的初始化方法从来没有调用。为什么我在写这个测试用例的时候没有发觉呢?那是因为 handler 确实被调用了,看起来就像 `UIAlertAction` 真的把 handler 作为内部变量去存储动作的 handler 闭包。这是非常脆弱的,并且 Larhythimx 在另一个 tweet 指出在他的测试程序中 handler 是 `nil`。 所以作为黄金通道(即编写不需要改变实现的测试)走不通,那就退而求其次用别的方法。 首先,我们在 `UIAlertAction` 中添加一个类方法去创建 action 。在 ViewController.swift 中增加如下扩展: ``` extension UIAlertAction { class func makeActionWithTitle(title: String?, style: UIAlertActionStyle, handler: ((UIAlertAction) -> Void)?) -> UIAlertAction { return UIAlertAction(title: title, style: style, handler: handler) } } ``` 在 `MockAlertAction` 中增加这个重写方法: ``` override class func makeActionWithTitle(title: String?, style: UIAlertActionStyle, handler: ((UIAlertAction) -> Void)?) -> MockAlertAction { return MockAlertAction(title: title, style: style, handler: handler) } ``` 在实现代码中,我们现在可以使用类方法去创建 alert 动作: ``` let okAction = Action.makeActionWithTitle("OK", style: .Default) { (action) -> Void in self.actionString = "OK" } let cancelAction = Action.makeActionWithTitle("Cancel", style: .Default) { (action) -> Void in self.actionString = "Cancel" } alertViewController.addAction(cancelAction) ``` 为了确保我们的测试用例正常,如我们预期地工作,将`MockAlertAction`的 `handler` 属性重命名为 `mockHandler`: ``` var mockHandler: Handler? ``` 此外,我们为动作的模拟标题添加测试。为取消动作的测试应该像这样: ``` func testAlert_FirstActionStoresCancel() { sut.Action = MockAlertAction.self sut.showAlert(UIButton()) let alertController = sut.presentedViewController as! UIAlertController let action = alertController.actions.first as! MockAlertAction action.mockHandler!(action) XCTAssertEqual(sut.actionString, "Cancel") XCTAssertEqual(action.mockTitle, "Cancel") } ``` 这个测试在此前的版本将会失败,因为初始化方法没有被调用,因此模拟标题也没有得到设置。 你可以在 [github](https://github.com/dasdom/TestingAlertExperiment) 上找到正确的版本。 再次感谢 Larhythimx 的推特!]]></content>
</entry>
<entry>
<title><![CDATA[Xcode:用于管理多个 target 配置的 XCConfig 文件]]></title>
<url>%2F2015%2F11%2F24%2FXcode-%E7%94%A8%E4%BA%8E%E7%AE%A1%E7%90%86%E5%A4%9A%E4%B8%AAtarget%E9%85%8D%E7%BD%AE%E7%9A%84XCConfig%E6%96%87%E4%BB%B6%2F</url>
<content type="text"><![CDATA[title: "Xcode:用于管理多个 target 配置的 XCConfig 文件" date: 2015-11-24 tags: [Tomasz Szulc] categories: [Swift] description: 让我们来看看 XCConfig 文件如何才能在多个拥有不同配置的 target 中良好地工作。 --- 原文链接: http://szulctomasz.com/xcode-xcconfig-files-for-managing-targets-configurations/ 作者: Tomasz Szulc 原文日期: 2015/11/14 ------------------ 让我们来看看 XCConfig 文件如何才能在多个拥有不同配置的 target 中良好地工作。 今天我本计划学习一些新东西,因此我搜索了 [mozilla/firefox-ios](https://github.com/mozilla/firefox-ios) 库(译者:这是在火狐浏览器在 github 的一个开源项目)的相关信息,接着我发现他们会在项目中使用大量的配置文件。 我曾经在几个项目中使用 XCConfig ,但是我并没有在现在开发的项目中使用它。因为这个项目有多个不同配置的 target,因此我开始思考如何才能有效且简单地管理这些 target 。 用例 -- 这个项目现在已经被我的团队接手了。客户的团队先开发了大约一年半的时间,最后决定将项目完全外包出去。这个项目一个麻烦的事就是 target 有不同的配置,因此如何更好地解决,是个棘手的问题。 项目由十个应用 target 组成,两个总的 target 做些业务,以及一个测试 target 。每一个 target 使用不同的尾部和不同的 “api keys”,以及其他像用于hockeyapp(HockeyApp 是一个用来分发你的程序并收集应用的崩溃报告的收集框架,类似友盟) token 的键(key)。每一个 target 有自己的预处理宏,如:“TARGET_A”, “TARGET_B”等...(虚构的名字)。然后,token,api keys,后端的 url 被存储在 plist 文件中。因此很自然地,就需要一些类来封装这个文件,并且有语法分析程序以及可以提供给我们适当的键。这个类有超过两百行的代码,对我来说,仅仅阅读这些数据就要花费很多时间。 因此,我想可能可以使用 XCConfig 文件来简化和替代使用语法分析程序和十个个预处理宏(一个 target)去决定从 plist 文件应该返回什么值。你可以在下面找到我的解决方案。可能不是最好的方案,但是此刻应该是最好的。如果你有更好的方案,我很愿意去拜读 :) 概述 -- 核心思想是使用一些有层级的配置文件。第一层是用于存储最普通的数据,第二层用于区分debug和release模式,最后一层用于关联特殊 target 的设置。 ![这里写图片描述](http://szulctomasz.com/wp-content/uploads/2015/11/diagram_1.png) Common.xcconfig --------------- 这个文件存储着类似应用名称,应用版本,bundle version,以及其他 debug和 release target 中通用的常见配置。 ``` // // Common.xcconfig // // APP_NAME = App APP_VERSION = 1.6 APP_BUNDLE_ID = 153 ``` 考虑到为十个 target 改变相应的应用版本和 bundle 可能会消耗很多时间。其他的选项可能会创建聚合的 target ,这样可以在每次 Cmd+B的时候更新Info-plist 文件,但是我会避免这样的情况并且让项目不会比现在更复杂。 Common.debug 和 Common.release ------------------------------- 这个文件能够存储可用于debug和release target的最常用配置。文件包含Common.xcconfig并且能够重写它的变量。如:你可以通过重写一个变量,轻易地把每个debug target 的应用名称改为“App Debug”。对于存储常见的用于开发和发行版本target的 API Key,这里也是很好的地方。 ***提示:使用通用配置文件和 CocoaPods*** 如果你使用 CocoaPods,你应该相应地在你的配置文件之一中包括(include)Pods.debug.xcconfig 或者 Pods.release.xcconfig。我推荐先在项目信息标签中设置你的配置文件然后执行 `pod install`去让Pod 项目重新配置。在安装之后,你应该及时地把 Pod 配置文件中的其中一个包括(include)到你自己的文件中去。 ``` Error: [!] CocoaPods did not set the base configuration of your project because your project already has a custom config set. In order for CocoaPods integration to work at all, please either set the base configurations of the target TARGET_NAME to Pods/Target Support Files/Pods/Pods.debug.xcconfig or include the Pods/Target Support Files/Pods/Pods.debug.xcconfig in your build configuration. ``` ``` // // Common.debug.xcconfig // // #include "Common.xcconfig" #include "Pods/Target Support Files/Pods/Pods.debug.xcconfig" APP_NAME = App Debug API_KEY_A = API_KEY_HERE API_KEY_B = API_KEY_HERE ``` PerTarget.xcconfig ------------------ 我确实不需要在这个层级使用 debug/release 配置文件(因为项目中的其他遗留问题),所以我只是用包括适当的 Common.debug.xcconfig 或者 Common.release.xcconfig 的 PerTarget.xcconfig 文件。但是最好应该有 debug 和 release 配置文件。在这个层级,你可以配置关联到特殊 target 的东西。 ``` // // Develop.xcconfig // // #include "Common.debug.xcconfig" BACKEND_URL = http:\/\/develop.api.szulctomasz.com SOME_KEY_A = VALUE_HERE SOME_KEY_B = VALUE_HERE ``` 访问变量 ------ 所有的配置文件被存储了。现在是时候去使用他们了。像我例子中有这么多的target,我可以把 Info.plist 文件的数量减少到只有一个,由于所有的不同的地方都已经在 xcconfig 文件中了,所以这一个文件可以替代多个文件。 你可以看到在你通过这些配置文件构建应用之后,有一些值出现在项目的 Build Setting 的 “User-Defined”部分。 如果你想要使用配置文件中的变量,例如,在一个target的 Info.plist 文件中,你需要使用这种写法:`$(VARIABLE)`。使用这种方式,你可以设置“Bundle Identifier”, “Bundle name”, “Bundle version”以及其他你想要配置的事项。 在代码中访问其他变量看起来有点不一样,我发现最简单的方法就是在 Info.plist 中创建附加的区域,通过使用相同的变量名称和使用上述的写法去设置值。这样你就可以在你的代码中读到这些值。 ``` if let dictionary = NSBundle.mainBundle().infoDictionary { let appName = dictionary["APP_NAME"] as! String let appVersion = dictionary["APP_VERSION"] as! String let appBuildVersion = dictionary["APP_BUILD_VERSION"] as! String print("\(appName) \(appVersion) (\(appBuildVersion))") let backend = (dictionary["BACKEND_URL"] as! String).stringByReplacingOccurrencesOfString("\\", withString: "") print("backend: \(backend)") } ``` 这里是 [tomkowz/demo-xcconfig](https://github.com/tomkowz/demo-xcconfig) 的代码,从里面你可以看到一些使用 xcconfig 文件的例子。 总结 -- Xcode 配置文件给出了配置 target 的简易方式,并且支持方便地维护项目配置。在我用例中,可以很棒地切换到这些文件,因为现在维护项目配置和我没有使用这个解决方案之前比起来简单了很多。]]></content>
</entry>
<entry>
<title><![CDATA[Swift 中的函数式编程]]></title>
<url>%2F2015%2F11%2F16%2FSwift%E4%B8%AD%E7%9A%84%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B%2F</url>
<content type="text"><![CDATA[title: "Swift 中的函数式编程" date: 2015-11-16 tags: [Natasha The Robot,函数式编程] categories: [Swift] description: Swift 一个强有力的语言特性就是能够用多样的函数式风格去编写代码。这在社区中看起来非常地激动人心。 --- 原文链接=http://natashatherobot.com/functional-programming-in-swift/ 作者=Natasha The Robot 原文日期=2015/11/13 ------------------ Swift 一个强有力的语言特性就是能够用多样的函数式风格去编写代码。这在社区中看起来非常地激动人心。 我在去年年底花费了一些时间学习函数式编程,所以我可以写更好的 Swift 代码。因此,我非常推荐你们也花时间去学习一下! 另外,我非常推荐推荐你们去看 [Functional Swift conference](http://2014.funswiftconf.com/)上的每一个视频。 所以在花了这么多时间后,我想要总结一下个人有关于函数式编程在 Swift 应用的一些思考。 跟着概念走 ----- 函数式编程是令人生畏的,这要归咎于单子(monads)和 函数子(functors)!然而,一旦你领悟了它的核心概念,那么函数式编程的思想会超级简单: > “函数式编程是一个编程范例…它把计算作为数学函数的评估,并避免改变状态和可变数据。”— [维基百科](https://en.wikipedia.org/wiki/Functional_programming) 所以核心就是你应该用数学的方式去编写代码。你的函数应该有清晰的输入和输出,并且不会像可变对象一样有全局副作用。这就是了! 避免可变状态 ------ 这和上述的注意点类似。函数式编程要编写的是没有副作用的数学代码。 在 Swift 中使用结构体和协议帮助你避免可变状态。 我极度推荐观看 [@andy_matuschak](https://twitter.com/andy_matuschak)的 [Controlling Complexity in Swift](https://realm.io/news/andy-matuschak-controlling-complexity/),这可以让你理解如何去实现以及最终的代码会如何地强大。 可读性第一 ----- 我发现很多高级的函数式代码,通常由于五个以上的习惯性编程而变得特别难以阅读。如果你遵从函数式编程的概念,有很多方法让你的代码变得更清楚。 但是在今天结束之前,还要多说一句,如果你在一个团队中工作,最重要的事就是让代码可读性更强。如果一个内部或者一个新的开发者加入你们的团队,他们会不会完全迷失了?如果你专注于编写易读的代码(取代好玩和花哨的写法),他们可能会很快就有产出。 记住一点,可读性的优先级永远比花哨的代码高(除非你的目标就是用一个好玩的副作用去实现好玩和花哨的程序)。 不要和 framework 作对 ---------------- 当然,在 iOS 编程中,由于 Cocoa framework 的建立和用户的输入输出,没有副作用显然是不可能的(在纯粹的数学世界,确实存在完全没有外部副作用,但那不是我们生活的世界!)。 例如,如果你创建了一个通用的转换器(formatter)(例如货币转换器),并用在代码中的一些地方,用单例是一个很好的方法。你还必须为UI Layer 使用 `UIViewControllers` 以及 `UIViews`。总有办法去脱离你的逻辑,进而让很好的不可变组件去帮助你可变化这些东西,但是不要过火地把 freamwork 改变为面目全非(可读,不可读)的状态。 学习高阶的函数式编程 ---------- 再次强调,你不应该执着于在你的 Swift 代码中使用花哨的技巧(除非你只是为了试验、或者好玩)。我非常推荐学习极限的函数式编程,以此来理解高级的概念,并且要找出 Swift 中的函数式代码行。 开始先花一些时间阅读 [Functional Programming in Swift](https://www.objc.io/books/fpinswift/) !这里有[更多的资源](http://natashatherobot.com/reading-functional-programming/)去帮助你开始学习!]]></content>
</entry>
<entry>
<title><![CDATA[让我们来搞崩 Cocoa 吧(黑暗代码)]]></title>
<url>%2F2015%2F11%2F10%2F%E8%AE%A9%E6%88%91%E4%BB%AC%E6%90%9E%E5%B4%A9cocoa%2F</url>
<content type="text"><![CDATA[title: "让我们来搞崩 Cocoa 吧(黑暗代码)" date: 2015-11-10 tags: [mikeash,Cocoa,crash,崩溃] categories: [Objective-C] description: 在传统的文章中,我们一直致力于如何编写高效稳定的代码,努力提高代码的鲁棒性。然而在本文中,我们将会改变一下思维方式,采用破坏的方式去挖掘 Cocoa 的一些特性,虽然文中作者表现出一种“病态”的破坏心理,但正因为有这种精神,通过文中那些黑暗代码,可以让我们更加深刻地理解 Cocoa 。 --- 原文链接=https://mikeash.com/pyblog/friday-qa-2014-01-10-lets-break-cocoa.html 作者=Mikeash 原文日期=2014/01/10 ------------------ 译者:在传统的文章中,我们一直致力于如何编写高效稳定的代码,努力提高代码的鲁棒性。然而在本文中,我们将会改变一下思维方式,采用破坏的方式去挖掘 Cocoa 的一些特性,虽然文中作者表现出一种“病态”的破坏心理,但正因为有这种精神,通过文中那些黑暗代码,可以让我们更加深刻地理解 Cocoa 。 ------------------ [让我们编写系列](https://mikeash.com/pyblog/?tag=letsbuild)文章是这个博客中我最喜欢的部分。但是,有时候搞崩程序比编写他们更有趣。现在,我将要开发一些好玩且不同寻常的方式去让 Cocoa 崩溃。 带有 NUL 的字符串 ----------- NUL(译者:应该为 '\0') 字符在 ASCII 和 Unicode 中代表 0,是一个不寻常的麻烦鬼。当在 C 字符串中时,它不作为一个字符,而是一个代表字符串结束的标识符。在其他的上下文环境中,它就会跟其他字符一样了。 当你混合 C 字符串和其它上下文环境,就会产生很有趣的结果。例如:`NSString` 对象,使用 NUL 字符毫无问题: ``` NSString *s = @"abc\0def"; ``` 如果我们认真的话,我们可以使用 lldb 打印它: ``` (lldb) p (void)[[NSFileHandle fileHandleWithStandardOutput] writeData: [s dataUsingEncoding: 5]] abcdef ``` 然而,展示这个字符串更为典型的方式是,字符串被当做 C 字符串在某个点结束。由于 '\0' 字符意味着 C 字符串的结尾,因此字符串会在转换时缩短: ``` (lldb) po s abc (lldb) p (void)NSLog(s) LetsBreakCocoa[16689:303] abc ``` 原始的字符已然包含预计的字符数量: ``` (lldb) p [s length] (unsigned long long) $1 = 7 ``` 试图对这个字符串进行操作会让你真正感到困惑: ``` (lldb) po [s stringByAppendingPathExtension: @"txt"] abc ``` 如果你不知道字符串的中间包含一个 NUL ,这类问题会让你感到这个世界满满的恶意。 一般来说,你不会遇到 NUL 字符,但是它很有可能通过加载外部资源的数据进来。`-initWithData:encoding:` 会很轻易地读入零比特并且在返回的 `NSString` 中产生 NUL 字符。 循环容器 ---- 这里有一个数组: ``` NSMutableArray *a = [NSMutableArray array]; ``` 这里有一个数组包含其他的数据: ``` NSMutableArray *a = [NSMutableArray array]; NSMutableArray *b = [NSMutableArray array]; [a addObject: b]; ``` 目前为止,看起来还不错。现在我们让一个数组包含自身: ``` NSMutableArray *a = [NSMutableArray array]; [a addObject: a]; ``` 猜猜会打印出什么? ``` NSLog(@"%@", a); ``` 以下就是调用堆栈的信息(译者:bt 命令为打印调用堆栈的信息): ``` (lldb) bt * thread #1: tid = 0x43eca, 0x00007fff8952815a CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 154, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8) frame #0: 0x00007fff8952815a CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 154 frame #1: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #2: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #3: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #4: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #5: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #6: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #7: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #8: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #9: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #10: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 frame #11: 0x00007fff895282da CoreFoundation`-[NSArray descriptionWithLocale:indent:] + 538 ``` 这里还删除了上千个栈帧。描述方法无法处理递归容器,所以它持续尝试去追踪到“树”的结束,并最终发生异常。 我们可以用它跟自身比较对等性: ``` NSLog(@"%d", [a isEqual: a]); ``` 这姑且看起来是 YES。让我们创造另一个结构上相同的数组 b 然后用 a 和它比较: ``` NSMutableArray *b = [NSMutableArray array]; [b addObject: b]; NSLog(@"%d", [a isEqual: b]); ``` 很抱歉: ``` (lldb) bt * thread #1: tid = 0x4412a, 0x00007fff8946a8d7 CoreFoundation`-[NSArray isEqualToArray:] + 103, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3fff28) frame #0: 0x00007fff8946a8d7 CoreFoundation`-[NSArray isEqualToArray:] + 103 frame #1: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #2: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #3: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #4: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #5: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #6: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #7: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 frame #8: 0x00007fff8946aa07 CoreFoundation`-[NSArray isEqualToArray:] + 407 frame #9: 0x00007fff8946f6b7 CoreFoundation`-[NSArray isEqual:] + 71 ``` 对等性检查同样也不知道如何处理递归容易。 循环视图 ---- 你可以用`NSView`实例做同样的实验: ``` NSWindow *win = [self window]; NSView *a = [[NSView alloc] initWithFrame: NSMakeRect(0, 0, 1, 1)]; [a addSubview: a]; [[win contentView] addSubview: a]; ``` 为了让这个程序崩溃,你只需要尝试去显示视窗。你甚至不需要去打印一个描述或者做对等性比较。当试图去显示视窗时,应用就会由于尝试去追踪底部的视图结构而崩溃。 ``` (lldb) bt * thread #1: tid = 0x458bf, 0x00007fff8c972528 AppKit`NSViewGetVisibleRect + 130, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=2, address=0x7fff5f3ffff8) frame #0: 0x00007fff8c972528 AppKit`NSViewGetVisibleRect + 130 frame #1: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #2: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #3: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #4: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #5: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #6: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #7: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #8: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #9: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #10: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #11: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #12: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #13: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #14: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #15: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #16: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #17: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #18: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #19: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 frame #20: 0x00007fff8c9725c6 AppKit`NSViewGetVisibleRect + 288 ``` Hash Abuse 滥用 Hash ------- 让我们创建一个实例一直等于其他类的类 AlwaysEqual,但是 hash 值并不一样: ``` @interface AlwaysEqual : NSObject @end @implementation AlwaysEqual - (BOOL)isEqual: (id)object { return YES; } - (NSUInteger)hash { return random(); } @end ``` 这显然违反了 Cocoa 的要求,当两个对象被认为是相等时,他们的 hash 应该总是返回相等的值。当然,这不是非常严格的强制要求,所以上述代码依然可以编译和运行。 让我们添加一个实例到 `NSMutableSet` 中: ``` NSMutableSet *set = [NSMutableSet set]; for(;;) { AlwaysEqual *obj = [[AlwaysEqual alloc] init]; [set addObject: obj]; NSLog(@"%@", set); } ``` 这产生了一个有趣的日志: ``` LetsBreakCocoa[17069:303] {( )} LetsBreakCocoa[17069:303] {( , )} LetsBreakCocoa[17069:303] {( , )} LetsBreakCocoa[17069:303] {( , , )} LetsBreakCocoa[17069:303] {( , , )} LetsBreakCocoa[17069:303] {( , , )} ``` 每次运行都不能保证一样,但是综合看起来就是这样。`addObject:`通常先添加一个新对象,然后在更多的对象添加进来的时候很少成功,最后顶部只有三个对象。现在这个集合包含三个看起来是独一无二的对象,而且看起来应该不会包含更多的对象了。所以,在重写 `isEqual:` 时总是应该重写 `hash`方法。 滥用 Selector ----------- Selector 是一个特殊的数据类型,在运行期用于表示方法名。在我们习惯中,它们必须是独一无二的字符串,尽管它们并不是严格地要求是字符串。在现在的 Objective-C 运行期,它们是字符串,并且我们都知道利用 Selector 去搞崩程序是很好玩儿的事。 马上行动,下面就是一个例子: ``` SEL sel = (SEL)""; [NSObject performSelector: sel]; ``` 当编译和运行之后,在运行期产生了很令人费解的错误: ``` LetsBreakCocoa[17192:303] *** NSForwarding: warning: selector (0x100001f86) for message '' does not match selector known to Objective C runtime (0x6100000181f0)-- abort LetsBreakCocoa[17192:303] +[NSObject ]: unrecognized selector sent to class 0x7fff75570810 ``` 通过创建奇怪的 selector,会产生真正奇怪的错误: ``` SEL sel = (SEL)"]: unrecognized selector sent to class 0x7fff75570810"; [NSObject performSelector: sel]; LetsBreakCocoa[17262:303] +[NSObject ]: unrecognized selector sent to class 0x7fff75570810]: unrecognized selector sent to class 0x7fff75570810 ``` 你甚至让错误看起来像是停止响应完整信息的 NSObject : ``` SEL sel = (SEL)"alloc"; [NSObject performSelector: sel]; LetsBreakCocoa[46958:303] *** NSForwarding: warning: selector (0x100001f77) for message 'alloc' does not match selector known to Objective C runtime (0x7fff8d38d879)-- abort LetsBreakCocoa[46958:303] +[NSObject alloc]: unrecognized selector sent to class 0x7fff75570810 ``` 显然,这不是真正的 alloc selector,它是一个碰巧指向一个包含 "alloc" 字符串的伪装 selector。但是,runtime 依然把它打印为 alloc 。 伪造对象 ---- 虽然现在越来越复杂,但是 Objective-C 依然是分配给所有对象类的大内存中的一小块内存。在这样的思维下,我们就可以创造一个伪造对象: ``` id obj = (__bridge id)(void *)&(Class){ [NSObject class] }; ``` 这些伪造对象也完全能工作: ``` NSMutableArray *array = [NSMutableArray array]; for(int i = 0; i < 10; i++) { id obj = (__bridge id)(void *)&(Class){ [NSObject class] }; [array addObject: obj]; } NSLog(@"%@", array); ``` 上述代码不仅可以运行并且打印日志如下: ``` LetsBreakCocoa[17543:303] ( "", "", "", "", "", "", "", "", "", "" ) ``` 可惜的是,看起来所有伪造对象都是以同样的地址结束的。但是还是可以继续工作的。好了,当你退出方法并且 autorelease pool 试图去清理时: ``` (lldb) bt * thread #1: tid = 0x46790, 0x00007fff8b3d55c9 libobjc.A.dylib`realizeClass(objc_class*) + 156, queue = 'com.apple.main-thread, stop reason = EXC_BAD_ACCESS (code=1, address=0x7fff00006000) frame #0: 0x00007fff8b3d55c9 libobjc.A.dylib`realizeClass(objc_class*) + 156 frame #1: 0x00007fff8b3d820c libobjc.A.dylib`lookUpImpOrForward + 98 frame #2: 0x00007fff8b3cb169 libobjc.A.dylib`objc_msgSend + 233 frame #3: 0x00007fff8940186f CoreFoundation`CFRelease + 591 frame #4: 0x00007fff89414ad9 CoreFoundation`-[__NSArrayM dealloc] + 185 frame #5: 0x00007fff8b3cd65a libobjc.A.dylib`(anonymous namespace)::AutoreleasePoolPage::pop(void*) + 502 frame #6: 0x00007fff89420d72 CoreFoundation`_CFAutoreleasePoolPop + 50 frame #7: 0x00007fff8551ada7 Foundation`-[NSAutoreleasePool drain] + 147 ``` 因为这些伪造对象没有合适分配内存,所以一旦autorelease pool 试图在方法返回时去操作它们,就会出现严重的错误,并且内存会被重写。 KVC --- 下面是一个类数组: ``` NSArray *classes = @[ [NSObject class], [NSString class], [NSView class] ]; NSLog(@"%@", classes); LetsBreakCocoa[17726:303] ( NSObject, NSString, NSView ) ``` 下面一个这些类实例的数组: ``` NSArray *instances = [classes valueForKeyPath: @"alloc.init.autorelease"]; NSLog(@"%@", instances); LetsBreakCocoa[17726:303] ( "", "", "" ) ``` 键值编码并不意味着要这样使用,但是看起来也可以正常运行。 调用者检查 ---- 编译器的 `builtin __builtin_return_address` 方法可以返回调用你的代码的地址: ``` void *addr = __builtin_return_address(0); ``` 因此,我们可以获取调用者的信息,包括它的名字: ``` Dl_info info; dladdr(addr, &info); NSString *callerName = [NSString stringWithUTF8String: info.dli_sname]; ``` 通过这个,我们可以做一些穷凶极恶的事(译者:并不认为是穷凶极恶的事,反而可作为调用动态方法的一种可选方法,虽然并不可靠),比如说完全可以根据不同的调用者调用合适的方法: ``` @interface CallerInspection : NSObject @end @implementation CallerInspection - (void)method { void *addr = __builtin_return_address(0); Dl_info info; dladdr(addr, &info); NSString *callerName = [NSString stringWithUTF8String: info.dli_sname]; if([callerName isEqualToString: @"__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__"]) NSLog(@"Do some notification stuff"); else NSLog(@"Do some regular stuff"); } @end ``` 这里是一些测试的代码: ``` id obj = [[CallerInspection alloc] init]; [[NSNotificationCenter defaultCenter] addObserver: obj selector: @selector(method) name: @"notification" object: obj]; [[NSNotificationCenter defaultCenter] postNotificationName: @"notification" object: obj]; [obj method]; LetsBreakCocoa[47427:303] Do some notification stuff LetsBreakCocoa[47427:303] Do some regular stuff ``` 当然,这种方式不是很可靠,因为 `__CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__`是 Apple 的内部符号,并且很有可能在未来修改。 Dealloc Swizzle --------------- 让我们使用 swizzle (方法调配技术)去调配`-[NSObject dealloc]`到一个不做任何事情的方法。在 ARC 下获得 @selector(dealloc) 有点棘手,因为我们不能直接读取它了: ``` Method m = class_getInstanceMethod([NSObject class], sel_getUid("dealloc")); method_setImplementation(m, imp_implementationWithBlock(^{})); ``` 现在我们坐下来欣赏这个例子所产生的混乱(简直就是代码界的黑暗料理): ``` for(;;) @autoreleasepool { [[NSObject alloc] init]; } ``` 调配 dealloc 方法导致这个代码完美且合理地疯狂泄露,因为对象不能在任何地方被摧毁。 总结 -- 用全新和有趣的方法搞崩 Cocoa 能够提供无尽的娱乐性。这也在真实的代码里体现出来了。想起我第一次遇到字符串中嵌入了 NUL ,那是充满痛苦的调试经历。其他只是为了好玩和适当的教学目的。 > That's it for today! Come back next time for more fun and games. > Friday Q&A is driven by reader suggestions, as always, so if you have > something you'd like to see discussed here, send it in!]]></content>
</entry>
<entry>
<title><![CDATA[【WatchOS 2教程系列四】WatchConnectivity:通过用户信息共享所有数据]]></title>
<url>%2F2015%2F11%2F09%2FWatchConnectivity--%E9%80%9A%E8%BF%87%E7%94%A8%E6%88%B7%E4%BF%A1%E6%81%AF%E5%85%B1%E4%BA%AB%E6%89%80%E6%9C%89%E6%95%B0%E6%8D%AE%2F</url>
<content type="text"><![CDATA[title: "【WatchOS 2教程系列四】WatchConnectivity:通过用户信息共享所有数据" date: 2015-11-09 tags: [Natasha The Robot] categories: [WatchOS 入门] description: 通过 User Info 实现后台数据传输应该在你确保所有数据被传输的情况下(不仅仅像 Application Context)。用户信息数据是在FIFO(先进先出)队列中排队传输的,所以不会有东西被重写。 --- 原文链接=http://natashatherobot.com/watchconnectivity-user-info/ 作者=Natasha The Robot 原文日期=2015/10/21 ---------- 在看这篇文章之前,确认你已经看过之前发布的几篇 WatchOS 2 的文章: - [WatchOS 2: Hello, World](http://natashatherobot.com/watchos-2-hello-world/) - [WatchConnectivity Introduction: Say Goodbye To The Spinner](http://natashatherobot.com/watchconnectivity-introduction-say-goodbye-to-the-spinner/) - [WatchConnectivity: Say Hello to WCSession](http://natashatherobot.com/watchconnectivity-say-hello-to-wcsession/) 通过 User Info 实现后台数据传输应该在你确保所有数据被传输的情况下(不仅仅像[Application Context](http://natashatherobot.com/watchconnectivity-application-context/))。用户信息数据是在FIFO(先进先出)队列中排队传输的,所以不会有东西被重写。 一个典型的例子是在一个短信应用中使用它 —— 最后一条信息是确保能看到完整对话和上下文的重要部分。亦或者如果用户更新了他们文件信息的一小部分,则所有的修改应该被同步到 Watch 文件。 在这个教程中,我将会构建一个食物 emoji (表情符号)的社交应用,因为我是个吃货,并且我喜欢🍦! 另外,这个应用可以成为一个基于食品杂货店展示的 Apple Watch 应用 —— 你在手机上选择打算购买的食物 emoji ,然后跳转到应用上,这样你就好像在食品杂货店上浏览商品了! ![这里写图片描述](http://natashatherobot.com/wp-content/uploads/Screen-Shot-2015-10-21-at-5.16.42-AM.png) 免责声明 ---- 对于这个应用需要知道的是,我将会写很多抽象的数据更新层用于整个应用,因为 UI 中的多个地方需要有数据源更新,所以在示例应用中将会过度设计。 我同样尝试了不同的架构,尤其是 Swift 中的,所以如果你有任何有关于在Swift中更好地抽象数据层的建议反馈,请在评论中提出。 步骤 -- 在这个教程中,我假设你已经知道如何在 Xcode 创建一个简单的单视图应用,以及创建一个简单的食物 Emoji 列表的表视图。如果你遇到任何问题,可以参考这个 [FoodSelectionViewController](https://github.com/NatashaTheRobot/WatchConnectivityUserInfoDemo/blob/master/WCUserInfoDemo/FoodSelectionViewController.swift) 。 同样地,我也假设已经知道如何创建一个 Watch 应用并且在 Interface.storyboard 中做过基本的样式。如果你需要帮助,请查看[WatchOS 2: Hello, World tutorial](http://natashatherobot.com/watchos-2-hello-world/) 和 [WatchKit: Let’s Create a Table tutorial](http://natashatherobot.com/watchkit-create-table/). 最后,你需要会创建基础的用于管理 `WCSession` 的单例,以及在 `AppleDelegate` 中的`application:didFinishLaunchingWithOptions`和在Watch 扩展中`ExtensionDelegate` 的`applicationDidFinishLaunching`中启动它。如果不会的话,请查看 [WatchConnectivity: Say Hello to WCSession tutorial](http://natashatherobot.com/watchconnectivity-say-hello-to-wcsession/)。 你的 iOS 应用看起来应该像这样: ``` // in your iOS app import WatchConnectivity class WatchSessionManager: NSObject, WCSessionDelegate { static let sharedManager = WatchSessionManager() private override init() { super.init() } private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil private var validSession: WCSession? { // paired - the user has to have their device paired to the watch // watchAppInstalled - the user must have your watch app installed // Note: if the device is paired, but your watch app is not installed // consider prompting the user to install it for a better experience if let session = session where session.paired && session.watchAppInstalled { return session } return nil } func startSession() { session?.delegate = self session?.activateSession() } } ``` 在 Watch 应用中应该有这样的代码: ``` // in your WatchKit Extension import WatchConnectivity class WatchSessionManager: NSObject, WCSessionDelegate { static let sharedManager = WatchSessionManager() private override init() { super.init() } private let session: WCSession = WCSession.defaultSession() func startSession() { session.delegate = self session.activateSession() } } ``` 当然,如果你需要额外的提示,你可以参考这个 [教程的源码](https://github.com/NatashaTheRobot/WatchConnectivityUserInfoDemo)。 现在让我们开始有趣的部分吧 🚀。 Sending Data 发送数据 ---- 在应用中,每当用户选择一个食物项,都需要在后台传输给 Watch 应用。这意味着 iOS 应用是发送者。显然这是非常简单的。 只需扩展iOS应用的 WatchSessionManager 单例去传输用户数据: Swift ``` // in your iOS app // MARK: User Info // use when your app needs all the data // FIFO queue extension WatchSessionManager { // Sender func transferUserInfo(userInfo: [String : AnyObject]) -> WCSessionUserInfoTransfer? { return validSession?.transferUserInfo(userInfo) } } ``` 所以现在,当用户选择一个食物的 cell ,你可以简单地调用以下的方法: ``` // FoodSelectionViewController.swift class FoodSelectionViewController: UITableViewController { private let food = ["🍦", "🍮", "🍤","🍉", "🍨", "🍏", "🍌", "🍰", "🍚", "🍓", "🍪", "🍕"] // Table Data Source methods truncated // MARK: Table view delegate override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) { let foodItem = food[indexPath.row] WatchSessionManager.sharedManager.transferUserInfo(["foodItem" : foodItem]) } } ``` 就酱!已选择的食物项就在 FIFO 队列中了,并且将会发送给 Watch 应用 ! 接收数据 ---- 现在 Watch 应用必须接收数据。这也是很简单的。只需要实现`WCSessionDelegate`中的`session:didReceiveUserInfo:`方法。 ``` // in your WatchKit Extension // MARK: User Info // use when your app needs all the data // FIFO queue extension WatchSessionManager { // Receiver func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) { // handle receiving user info // this will be filled in in the Updating Data section below } } ``` 更新数据 ---- 现在你接收到了数据,让我们开始最棘手的部分吧。尝试去更新你的 Watch 扩展的 `InterfaceController` 以及其他视图或者数据源。一个方式是通过 `NSNotificationCenter`,但是我将会尝试用不同的方法。这个部分可以用多种方法来做,并且对于应用来说有点过度设计,因此要记清楚这个。 既然我们用 Swift,我的目标是尽快地去改变值类型模型。不幸的是,正如我在 `WCSession` 中所提到的,`WCSessionDelegate`只能在一个`NSObject` 中实现。为了减轻这个,我创建了一个可以携带用户信息数据的 `DataSource` 值。因为用户信心是在一个 FIFO 队列顺序接收的,`DataSource` 应该持续追踪在队列中接收的数据。 ``` // in your WatchKit Extension struct DataSource { let items: [Item] enum Item { case Food(String) case Unknown } init(items: [Item] = [Item]()) { self.items = items } func insertItemFromData(data: [String : AnyObject]) -> DataSource { let updatedItems: [Item] if let foodItem = data["foodItem"] as? String { updatedItems = [.Food(foodItem)] + items } else { updatedItems = [.Unknown] + items } return DataSource(items: updatedItems) } } ``` 我可以现在设置一个 protocol ,通过更新的数据源更新所有需要知道数据改变的部分: ``` // in your WatchKit Extension // WatchSessionManager.swift protocol DataSourceChangedDelegate { func dataSourceDidUpdate(dataSource: DataSource) } ``` 现在让我们进入有趣的部分!你的`WatchSessionManager`将需要以某种方式去追踪所有的`dataSourceChangedDelegates`。这可以通过一个数组以及一个可以添加和删除数组中的delegate方法来实现。`WatchSessionManager`还需要持续追踪最近的`DataSource`拷贝,这样就可以使用`DataSource`中的数据去创建一个带有最新数据的新 `DataSource`: ``` // in your WatchKit Extension // WatchSessionManager.swift class WatchSessionManager: NSObject, WCSessionDelegate { static let sharedManager = WatchSessionManager() private override init() { super.init() } private let session: WCSession = WCSession.defaultSession() private var dataSource = DataSource() private var dataSourceChangedDelegates = [DataSourceChangedDelegate]() func startSession() { session.delegate = self session.activateSession() } func addDataSourceChangedDelegate(delegate: T) { dataSourceChangedDelegates.append(delegate) } func removeDataSourceChangedDelegate(delegate: T) { for (index, dataSourceDelegate) in dataSourceChangedDelegates.enumerate() { if let dataSourceDelegate = dataSourceDelegate as? T where dataSourceDelegate == delegate { dataSourceChangedDelegates.removeAtIndex(index) break } } } } ``` 当用户信息接收到后我们可以添加实现了: ``` // in your WatchKit Extension // WatchSessionManager.swift // MARK: User Info // use when your app needs all the data // FIFO queue extension WatchSessionManager { // Receiver func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) { // handle receiving user info dispatch_async(dispatch_get_main_queue()) { [weak self] in if let dataSource = self?.dataSource.insertItemFromData(userInfo) { self?.dataSource = dataSource self?.dataSourceChangedDelegates.forEach { $0.dataSourceDidUpdate(dataSource) } } } } } ``` 现在我们只需要确保我们的`InterfaceController`继承了`DataSourceChangedDelegate`,并且被`WatchSessionManager`持续追踪着: ``` // in your WatchKit Extension // InterfaceController.swift class InterfaceController: WKInterfaceController, DataSourceChangedDelegate { @IBOutlet var foodTable: WKInterfaceTable! override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) WatchSessionManager.sharedManager.addDataSourceChangedDelegate(self) loadTableData(DataSource()) } override func didDeactivate() { // remove InterfaceController as a dataSourceChangedDelegate // to prevent memory leaks WatchSessionManager.sharedManager.removeDataSourceChangedDelegate(self) super.didDeactivate() } // MARK: DataSourceUpdatedDelegate // update the table once the data is changed! func dataSourceDidUpdate(dataSource: DataSource) { loadTableData(dataSource) } } private extension InterfaceController { private func loadTableData(dataSource: DataSource) { foodTable.setNumberOfRows(dataSource.items.count, withRowType: "FoodTableRowController") for (index, item) in dataSource.items.enumerate() { if let row = foodTable.rowControllerAtIndex(index) as? FoodTableRowController { switch item { case .Food(let foodItem): row.foodLabel.setText(foodItem) case .Unknown: row.foodLabel.setText("¯\\_(ツ)_/¯") } } } } } ``` 就是这样啦!]]></content>
</entry>
<entry>
<title><![CDATA[减少在Facebook iOS应用中的FOOMs]]></title>
<url>%2F2015%2F11%2F04%2F%E5%87%8F%E5%B0%91%E5%9C%A8FacebookiOS%E5%BA%94%E7%94%A8%E4%B8%AD%E7%9A%84FOOMs%EF%BC%88%E8%BF%90%E8%A1%8C%E5%9C%A8%E5%89%8D%E5%8F%B0%E7%9A%84%E5%86%85%E5%AD%98%E4%B8%8D%E8%B6%B3%EF%BC%89%2F</url>
<content type="text"><![CDATA[title: "减少在Facebook iOS应用中的FOOMs" date: 2015-11-04 tags: [内存不足,Facebook,FOOMs] categories: [性能优化] description: 在 Facebook,我们一直致力于让应用稳定、快速、可靠。在 Facebook 的 iOS 应用上,我们已经做了很多工作去减少应用的崩溃率以及全面提高应用的稳定性。此前,大多数的崩溃都是由于常规性错误,一般都会伴随着相应代码行的栈回溯信息,并且提供了可能导致问题所在的提示信息。 --- 原文链接 = https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/ 作者 = [Ali Ansari](https://code.facebook.com/posts/1146930688654547/reducing-fooms-in-the-facebook-ios-app/#) [Grzegorz Pstrucha](https://www.facebook.com/gpstrucha) 原文日期 = 2015-08-24 ---------- 译者:本文是 Facebook 应用的开发工程师在发现应用运行中屡禁不死的崩溃问题,通过逐步地调查和研究崩溃发生的时间点,提出了自有的解决方案,虽然文中并没有很详细的解决方案,但是对于如何排查和解决问题的方式以及对应用性能严谨的态度,是很值得我们借鉴的。 ---------- 在 Facebook,我们一直致力于让应用稳定、快速、可靠。在 Facebook 的 iOS 应用上,我们已经做了很多工作去减少应用的崩溃率以及全面提高应用的稳定性。此前,大多数的崩溃都是由于常规性错误,一般都会伴随着相应代码行的栈回溯信息,并且提供了可能导致问题所在的提示信息。 当我们继续解决崩溃问题时,我们观察到需要解决的崩溃比例正在下降,但是我们注意到 App Store 指出社区继续出现令人失望的应用崩溃。我们深入研究了用户报告,并且从理论上说明内存不足(out-of-memory events (OOMs))可能正在发生。OOMs 一般发生在系统运行在低内存的环境下,OS 为了回收内存而终止应用。它既可能发生在前台,也可以是后台。我们在内部称之为 FOOMs 和 BOOMs — 当我们说应用爆炸(BOOM)了,好像很好玩的样子。 从用户的角度来看,一个前台内存不足导致的崩溃和常规的崩溃是不好分辨的。一般分为几种情况,应用异常终止,似乎消失,以及用户返回设备主屏幕。如果内存的消耗速度急速增长,那么应用会在不接到任何通知的情况下被终止掉。在 iOS 中,OS 会将内存警告发给应用,但是不能保证 OS 一定会在终止应用之前给应用发送警告信息。这就导致我们无法轻易地知道应用是否是由于内存压力而被 OS 终止。 分析问题 ------------------------------- 为了掌握应用由于 OOM 崩溃而终止的频率,我们从所有已知的途径列举应用可能终止的情况并记录他们。这样问题就转变为"导致应用重启的是什么?" 应用需要重启的原因如下: 1. 应用已经更新 2. 应用退出或终止 3. 应用崩溃 4. 用户强制退出应用 5. 设备重启(包括 OS 升级) 6. 应用在前台或者后台内存不足(OOM) 通过排除处理,寻找区别于其他重启原因的实例,借此我们可以找出 OOM 发生的时间。此外,我们还追踪应用进入后台和前台的时间,借此我们可以精确地把 OOMs 分为 BOOMs 和 FOOMs。 ![这里写图片描述](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xft1/t39.2365-6/11891371_510065462479783_784800705_n.jpg) 日志显示在设备处于低内存状态下,有很高的比率发生 OOMs 。当应用进程在受内存限制的设备上像驱逐一样被终止,真的非常令人沮丧。查看相关的日志记录帮助我们验证排除法的效果,并且能继续提高日志记录(我们无法准确验证所有的事例,例如应用升级)。 我们最初在减少 OOMs 所做的努力,是试图在应用不再需要内存时,就尽可能快地主动缩小应用的内存占用。不幸的是,我们没有发现 OOM 崩溃的数量没有有切实的改变,所以我们把关注点转移到大的内存分配上,开始观察那些可能被泄露的内存(没有清理干净的),尤其是潜在的循环引用。 内存使用分析 ---------------------- 当我们开始解决内存泄露问题时,我们看到 OOM 崩溃率有所降低,但是依然没有达到我们预期。紧接着,我们深入研究 Apple 的 Instruments 应用的 memory profiler,并且注意到只要应用打开任何 web 网页,一个重复样式的 `UIWebView` 就会分配大量的内存。我们还发现内存经常没有回收,即使在用户离开了网页并且 web 视图被关闭的情况下。 我们试图做过大量的优化,例如清理缓存和内容,但是应用进程的内存占用在跳转向 web 视图时总是显著增长。iOS包含一个新的类 — `WKWebView` — 它把大多数的工作都放在了分开的进程里,这意味着大多数跟内存相关的 web 视图使用将不会分配给我们的进程。在低内存的事件中,web 视图的进程将会被终止,但是我们的应用有很大可能会继续存活下去。在我们把应用迁移为 `WKWebView` 后,我们确切地看到 OOMs 发生的比率有了显著的降低。Yay! ![这里写图片描述](https://fbcdn-dragon-a.akamaihd.net/hphotos-ak-xfp1/t39.2365-6/11891352_794615773990111_422386740_n.jpg) 内存分配比率 ------------------ 当通过 Instruments 分析内存使用时,我们还发现应用中分配了大量的临时内存(~30 MB),然后马上释放掉。如果 CPU 在这个分配过程中是空闲的,那么 OS 会终止程序。我们要禁止此类临时分配,这可以帮助我们在 30% 确定场景中减少 OOM 崩溃,我们还实验并发现,相较于重复分配和释放内存,分配一次然后管理内存对于应用的可靠性是更好的。 阻止内存恶化 ---------------------- 即使用了 `WKWebView`,我们仍然发现一点点内存泄露都能够显著地导致影响 OOM 的发生比率。在我们通常的发布计划和贡献给应用的许多的团队中,在发布的应用中捕获和阻止内存泄露是非常重要的。我们改变了扫描设备,独创性地设计了用于测试移动性能,为了记录大量进程中的常驻内存,允许扫描设备去标记恶化情况,只要它们被添加了。这已经帮助我们把 OOM 发生比率保持在比最初解决问题时低得多的水平上。 应用内部的内存分析器 ------------------------- 上一个在这个项目中我们使用的关键技术是去构造一个应用内部的内存分析器,通过追踪所有的 Objective-C 对象的内存分配进而快速分析应用。我们把这个配置在扫描仪上,然后在里面建立我们的应用。 它是如何工作的:对于系统中的每一个类,维护一个当前活动的实例的数量。我们可以在任何点要求它打印出每一个类对象的现存数目。然后我们就可以分析这些数据任何异常的 release-to-release 用以辨认我们应用中总体上的内存分配模式,如果计数急剧变化,这一般可以验证为内存泄露。我们准备去用一种性能足够用并且不会产生对用户有影响的方法去实现。 下面简要说明我们的策略以及我们是如何追踪 NSObject 的内存分配。 我们一开始创建一个内存分配追踪类。这是个超级直接和简单的类,有统计实例数量的公共方法用于统计实例数量的增加和减少。我们使用 C++ 而不是Objective-C,是由于那样可以最小化追踪器的内存分配和 CPU 占有率。 ``` class AllocationTracker { static AllocationTracker* tracker(); void incrementInstanceCountForClass(Class aCls); void decrementInstanceCountForClass(Class aCls); std::vector countsSnapshot(); ... } ``` 然后我们可以使用 iOS 的方法调配技术(称为“swizzling”,使用runtime 的`class_replaceMethod`方法),用-`fb_originalAlloc` 和 `-fb_originalDealloc`方法去替换标准的iOS方法 `+alloc` 和`+dealloc`。 然后我们用新实现的增加和减少的分配和释放实例数量的方法相应地替代`+alloc` 和 `+dealloc`。 ``` @implementation NSObject (AllocationTracker) + (id)fb_newAlloc { id object = [self fb_originalAlloc]; AllocationTracker::tracker()->incrementInstanceCountForClass([object class]); return object; } - (void)fb_newDealloc { AllocationTracker::tracker()->decrementInstanceCountForClass([object class]); [self fb_originalDealloc]; } @end ``` 然后,当应用运行时,我们可以调用快照方法有规律地打印当前存活实例的数量。 应用可靠性 ----------------------- 一旦我们在 Facebook 的 iOS 应用中实施更改去解决内存问题,我们会看到 (F)OOMs 和用户的应用崩溃报告有显著的降低。OOM 崩溃对于我们来说是盲点,因为没有正式的体系或者 API 可以随意检测到它们。没有人喜欢一个应用突然关闭。但是使用某些工具,或者最新的 iOS 技术,以及一些灵巧的方法去解决这个问题,能够让我们的应用更加可靠,并且保证你不会在打开 web 视图查看一篇有趣的文章(就像你在看的这篇文章)时突然关闭。 > Additional thanks to Linji Yang, Anoop Chaurasiya, Flynn Heiss, > Parthiv Patel, Justin Pasqualini, Cloud Xu, Gautham Badrinathan, Ari > Grant, and many others for helping reduce the FOOM rate.]]></content>
</entry>
<entry>
<title><![CDATA[【WatchOS 2教程系列三】WatchConnectivity学习之WCSession]]></title>
<url>%2F2015%2F10%2F25%2FWatchConnectivity-%E5%AD%A6%E4%B9%A0%E4%B9%8BWCSession(1)%2F</url>
<content type="text"><![CDATA[title: "【WatchOS 2教程系列三】WatchConnectivity学习之WCSession" date: 2015-10-25 tags: [WatchConnectivity] categories: [WatchOS 入门] description: WCSession 就是 WatchConnectivity 的魔法。所以让我们赶紧深挖它吧! --- 原文链接 = http://natashatherobot.com/watchconnectivity-say-hello-to-wcsession/ 作者 = Natasha The Robot 原文日期 = 2015-09-21 ---------- 在读这篇文章之前,请检查一下你是否已经学习了之前两篇关于`WatchOS 2`的文章: [WatchOS 2: Hello, World](http://natashatherobot.com/watchos-2-hello-world/) [WatchConnectivity Introduction: Say Goodbye To The Spinner](http://natashatherobot.com/watchconnectivity-introduction-say-goodbye-to-the-spinner/) `WCSession`就是`WatchConnectivity`的魔法。所以让我们赶紧深挖它吧! `WCSession.defaultSession()`会返回`WCSession`的单例,用于`iOS`和`Watch app`之间的数据传输。但是,在使用`WCSession`时仍有一些值得注意的地方。 首先,你必须设置一个`session`的`delegate`并启动它。 > 默认的`session`用于两个相应app的通信(例如`iOS app`和它的原生WatchKit的扩展)。这个`session`提供发送、接收、追踪状态的方法。 > > 启动一个`app`时,应该在默认的`session`上设置一个`delegate`并启动它。这将允许系统填充状态属性和提供任何优秀的背景传输。—— `Apple` 文档说明。 所以你的代码应该写成这样: ``` let session = WCSession.defaultSession() session.delegate = self session.activateSession() ``` 在这里,我推荐将你的`WCSession`作为一个单例,这样你就可以在`app`中随意使用它: ``` import WatchConnectivity // Note that the WCSessionDelegate must be an NSObject // So no, you cannot use the nice Swift struct here! class WatchSessionManager: NSObject, WCSessionDelegate { // Instantiate the Singleton static let sharedManager = WatchSessionManager() private override init() { super.init() } // Keep a reference for the session, // which will be used later for sending / receiving data private let session = WCSession.defaultSession() // Activate Session // This needs to be called to activate the session before first use! func startSession() { session.delegate = self session.activateSession() } } ``` 所以你可以在`AppDelegate`的`application:didFinishLaunchingWithOptions`方法中启动你的`session`,并且可以在`app`的任意地方使用: ``` @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { // truncated... func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { // Set up and activate your session early here! WatchSessionManager.sharedManager.startSession() return true } // truncated... } ``` 但是启动`session`是远远不够的。你需要通过`WCSession`的多重检查机制,使得你的应用不需要做格式化传输数据的额外工作。 检查设备支持 ----------- > 检查iOS设备是否支持session,WatchOS都是支持session的。 如果你有一个普遍适用的`app`,例如,`iPad`将不会支持`WCSession`(因为`iPad`不能和`Watch`配对)。因此需要确保在`iOS`项目中做`isSupported()`检查: ``` if WCSession.isSupported() { let session = WCSession.defaultSession() session.delegate = self session.activateSession() } ``` 这意味着你的`WatchSessionManager`单例需要适应不支持`WCSession`的场景(使用选项): ``` // Modification to the WatchSessionManager in the iOS app only class WatchSessionManager: NSObject, WCSessionDelegate { // truncated ... see above section // the session is now an optional, since it might not be supported private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil // starting a session has to now deal with it being an optional func startSession() { session?.delegate = self session?.activateSession() } ``` 用于 Watch 的 iOS App 状态 ----------------------- 如果你从`iOS app`发送数据到`Watch`,你需要做一些额外的检查,这样当`Watch`处于无法接受数据的状态时,你就不会浪费`CPU`资源去处理用于传输的数据。 **配对** 显然,为了从`iOS`设备传输数据到`Watch`,用户必须有一个`Watch`并且和`iOS`设备配对。 **安装 Watch app** 一个用户可能有一对设备,但是他们可能删除了设备上的`Watch App`,所以为了数据传输,你需要检查你的应用确实有安装在所配对的`Apple Watch`上面。 如果用户有一对设备,但是未安装你的`app`,那么如果用户能够从你的`watch app版本`中获得好处,那将成为做这个检查和提示用户安装`watch app`的关键点。 当单例中的`session`保持工作时,为了让这些检查更加简单,并且能够在应用中随意使用,我喜欢在`iOS app`中创建一个`validSession`变量: ``` // Modification to the WatchSessionManager in the iOS app only class WatchSessionManager: NSObject, WCSessionDelegate { // truncated... see above private let session: WCSession? = WCSession.isSupported() ? WCSession.defaultSession() : nil // Add a validSession variable to check that the Watch is paired // and the Watch App installed to prevent extra computation // if these conditions are not met. // This is a computed property, since the user can pair their device and / or // install your app while using your iOS app, so this can become valid private var validSession: WCSession? { // paired - the user has to have their device paired to the watch // watchAppInstalled - the user must have your watch app installed // Note: if the device is paired, but your watch app is not installed // consider prompting the user to install it for a better experience if let session = session where session.paired && session.watchAppInstalled { return session } return nil } // truncated... see above } ``` **判断并发是否可用** 最后,如果你在`app`中有使用并发,你必须检查并发是否可用。我不会在`WatchConnectivity`教程中说明过多并发的细节,但是如果你想要知道更多,可以观看超级有用和全面的 [WWDC 2015 Creating Complications with ClockKit session](https://developer.apple.com/videos/wwdc/2015/?id=209)。 **sessionWatchStateDidChange** 为了以防你的`iOS app`需要`WCSession`状态变化的信息,这里有一个delegate方法,专门用于通知`WCSession`的状态变化: ``` /** Called when any of the Watch state properties change */ func sessionWatchStateDidChange(session: WCSession) { // handle state change here } ``` 例如,如果你的`app`需要安装`Watch App`,你可以实现这个`delegate`方法,然后去检测你的`Watch App`是否真正安装了,并且让用户根据`iOS app`中的添加流顺序去完成最终的安装。 检查设备可达状态 -- 为了正确在`iOS`和`Watch`使用`Interactive Messaging`传输数据,你需要做一些额外的工作以确保两个`app`处于可达状态: `Watch app`上的可达能力需要所配对的`iOS`设备在重启后至少已经解锁一次了。这个属性能够用于决定`iOS`设备是否需要被解锁。如果`reachable`设为`NO`,可能是由于设备重启过,需要解锁。如果处于这种状态,`Watch`将会展示一个提示框建议用户去解锁他们配对的`iOS`设备。 在使用`Interactive Messaging`时,我喜欢增加一个额外的`valideReachableSession`变量到我的单例中: ``` // MARK: Interactive Messaging extension WatchSessionManager { // Live messaging! App has to be reachable private var validReachableSession: WCSession? { // check for validSession on iOS only (see above) // in your Watch App, you can just do an if session.reachable check if let session = validSession where session.reachable { return session } return nil } ``` 如果`session`是不可达的,你可以如`Apple`所建议的那样,提示用户去解锁他们的iOS设备。为了知道用户解锁了他们的设备,实现`sessionReachabilityDidChange`的`delegate`方法: ``` func sessionReachabilityDidChange(session: WCSession) { // handle session reachability change if session.reachable { // great! continue on with Interactive Messaging } else { // 😥 prompt the user to unlock their iOS device } } ``` 以上!现在你应该已经知道了`WCSession`的一些要领,所以我们将会学习更加好玩的部分 —— 真正使用它在`iOS`和`Watch`之间接收和发送收据! 你可以在`Github`查看完整的[WatchSessionManager单例](https://gist.github.com/NatashaTheRobot/6bcbe79afd7e9572edf6)。]]></content>
</entry>
<entry>
<title><![CDATA[Storyboard Reference, Strong IBOutlet, Scene Dock in iOS 9]]></title>
<url>%2F2015%2F10%2F20%2FStoryboardReference%2CStrongIBOutlet%2CSceneDockiOS9%2F</url>
<content type="text"><![CDATA[title: "Storyboard Reference, Strong IBOutlet, Scene Dock in iOS 9" date: 2015-10-20 tags: [storyboard,strong,IB] categories: [iOS 9] description: Apple 已经对 Xib 和 Storyboard 文件做了很多优化。并且由于这些优化,你现在可以将 IBOutlet 定义为 strong ,替代原来的 weak 。 --- 原文链接=https://www.invasivecode.com/weblog/storyboard-strong-iboutlet-scene-dock/ 作者=[Geppy](http://twitter.com/geppyp) 原文日期=2015-10-11 ------------------ 在这个教程中,我想要展示一些有关于`Xcode 7`中`Interface Builder`的新特性,我相信这将会改变你对`Storyboards`的看法。 Strong 引用的 IBOutlet ------------------- `Apple`已经对`Xib`和`Storyboard`文件做了很多优化。并且由于这些优化,你现在可以将`IBOutlet`定义为`strong`,替代原来的`weak`。`Apple`曾在上一届的`WWDC`上指出这一点,因此让我们来看一下其中的更多细节。你可以从这个[文档](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html)中找到管理Nib文件中对象生命周期的章节: > `Outlet`一般来说应该为`weak`,除了在`nib`文件 ( 或者`iOS`, `storyboard scene`) 中的File’s > Owner的顶级对象,这个对象可以是`strong`。你创建的`Outlets`应该为`weak`,原因如下: > > - 你创建的一个`view controller`视图的子视图或者`window controller`窗体视图的`Outlets`,是对象之间的弱引用,不应该有依赖关系。 > - `strong`的`outlet`通常是特殊的`framework`类(如:`UIViewController`视图的`outlet`,或者`NSWindowController`视窗的`outlet`)。 正如这个段落所解释的一样,`view controller`视图的子视图 `outlet`应该为`weak`,因为这个视图已经被`nib`文件的顶级对象所拥有了。然而,当一个`Outlet`被定义为`weak`指针时,`ARC`会在编译期间调用以下函数: ``` id objc_storeWeak(id *object, id value); ``` 这个函数把对象的值作为`key`,并把它添加到`table`中。这个`table`被称为`weak table`。`ARC`使用这个`table`去存储应用中的所有的`weak`指针。现在,当对象被`deallocated`时,`ARC`将会指向`weak table`并且将`weak`引用置为`nil`。同时,`ARC`将会调用: ``` void objc_destroyWeak(id * object); ``` 紧接着,注销这个对象并再次调用`objc_destroyWeak`: ``` objc_storeWeak(id *object, nil); ``` 这种`weak`引用关联的生命周期是`strong`引用的2-3倍。所以,通过避免简单地定义`outlets`为`strong`,使用弱引用是一种运行期间的通用做法。 我想这个决策与已废弃的`viewDidUnload`方法有关。知道`iOS 5`,这个方法被用于清空在低内存环境下的视图。正如文档中解释的那样: > 在iOS 5之前,当发生低内存警告或者当前view > controller的视图不被需要时,在视图被释放之后,系统会选择性地调用这个方法。这个方法让你可以进行最后的清理工作。如果你的视图存储了视图或者其子视图的单独引用,你应该使用这个方法去释放这些引用。 在那时,定义一个属性为`weak`是有意义的,因为这就不用在`viewDidUnload`额外地释放对象。但是在`iOS 9`中,我相信我们已经有足够的时间去避免使用这个方法。因此,在`IBOutlets`定义`weak`是没意义的。 现在 Storyboard 的限制 --------------- `Apple`是在`iOS 5`中开始提出`storyboards`的。在此之前,使用`Interface Builder`的`nib`文件是创建UI的唯一途径。在`iOS`开发中,单个文件中操纵多个`nib`文件是很普遍的。然而,为了理解应用流以及`view controller`如何连接在一起,开发者需要去每一个`view controller`类内去找出跳转到下一个界面的桥接点。这是一个非常耗费时间的工序,尤其当你不是应用的原始开发者时。 `Apple`提出`Storyboards`用以简化这个过程,并帮助开发者能够对整个应用程序流有完全的控制。除此之外,`storyboards`允许你在一个文件中拥有一个`view controller`视图(通过添加 `.storyboard`文件)。用这种方式,你可以看到整个程序的流状态,并且能够方便地理解`view controller`的连接关系。然而,`storyboards`也引出了一些问题。把所有的`nib`文件都放在一个文件中显然是非常便利并且能完美工作,但是这只是在你为单人开发的前提下。只要你的团队扩大了,你会使用版本控制,例如`git`或者`subversion`,这时你就会讨厌`storyboards`。因为,当把修改合并到一个通用的`git branch`时,就会产生冲突,而解决此类冲突是很头疼的。在编译期间,`nib`会被编译成`XML`文件。所以,为了解决合并冲突,你需要比较两个巨大的`XML`文件,并且要尝试理清哪部分是你修改的,哪部分是你同事修改的。此外,`Apple`经常修改这个文件格式。所以,试图去理解并且反转`storyboard`格式是非常浪费时间的。 例如,在`iNVASIVECODE`(这是作者所在的公司),我们倾向于使只用`storyboards`去构建`app`原型。我们的设计师能够在几个小时内设计出一个能够在`iOS`设备上运行的原型,有时候只需要几分钟。这样可以在不写一行代码的情况下使用`storyboards`。所以,`storyboards`对于构建原型来说是非常方便的,但是不建议在开发期间使用。 另一个`storyboard`的重要局限是不能添加不属于一个场景体系的视图。我个人认为跟前面所说的合并问题相比,这是一个更为致命的限制。只要能够使用,我必定会使用`IB`。我喜欢这个,因为这可以避免写代码。但是使用`storyboards`,不能添加场景体系以外的视图。因此,当我需要额外的视图时,我就强迫自己去使用`nib`。 `Storyboards`还有一个额外的局限就是过渡问题。在`iOS 7`以及之后的`iOS 8`,`Apple`提出了在两个`view controller`之间创建一个定制过渡的新方法。当你运行一个`segue`时,这个新方法需要创建不能使用`storyboard`的特殊对象。所以,如果你想要添加定制的过渡方法到你的`view controllers`,你要避免使用`storyboards`。 但是猜猜看!`Xcode 7`和`iOS 9`为我们解决了所有的这些问题。 Storyboard Reference -------------------- 在`Xcode 7`中,我们有一个在多个`storyboards`中组织`scenes`的新方法,并且能对它们进行引用。让我们来看一个实践的例子。下载这个我已经准备好的[例子](http://www.invasivecode.com/documents/Multiboard.zip)。打开它,并且选择`Main.storyboard`文件。我已经为了准备好了一系列组织在一个`tab bar controller`下`view controller`。每一个`tab`包含一个`navigation controller`。下面的图片强调了示例项目的`storyboard`部分。 ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard1.png) 正如你所看到的那样,`tab bar controller`包含了三个`navigation controller`。每一个`navigation controller`控制着不同的视图控制器。现在,想象一下在这个项目里和其他开发者一起工作。正如我前面描述的那样,使用同一个`storyboard`文件是非常令人头疼的,因为你们每个人都会修改它。你可以把着三个`navigation`分支分割成三个`storyboard`文件。然而,当你准备在运行期从一个`storyboard`跳转到另外一个时,你必须加载相应的`storyboard`文件。这需要增加额外的代码。 `xcode 7`允许你创建多个`storyboards`,并且可以方便地操纵它们。选择顶部的`navigation controller` 以及两个`view controller`,如下图所示: ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard2.png) 选择好之后,打开菜单栏的`Editor`,然后选择`Refactor to Storyboard`(如图) ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard3.png) 为新的`storyboard`取一个名字(如图)。我将它命名为`First.storyboard`。 ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard4.png) 点击保存。正如你所见到的那样,一个新的`storyboard`已经被添加到你的项目中了。让我们回到`Main.storyboard`,你将会看到如下的对象。 ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard5.png) 这个称之为`Storyboard Reference`,它确实为新建的`First.storyboard`的引用,并且替换了先前选择的三个`view controller`。最棒的是如果你双击`storyboard`引用,`Xcode 7`会打开所引用的`storyboard`。因此,当你想要控制应用流时,你可以方便地导向不同的`storyboard`。在运行期间,当`segue`指向的一个`Storyboard Reference`被执行时,这个被引用的`storyboard`中的初始化`view controller`会被加载。此外,`Storyboard References`还能够引用相同的`storyboard`。 另外,你也可以手工创建一个新的`storyboard`,然后添加一个`Storyboard Reference`到起始的`storyboard`中。让我们来试一下。 创建一个新的`storyboard`并命名为`Third.storyboard`。在`Main.storyboard`文件中,从`Object Library`中添加新的`Storyboard Reference`。选择`Storyboard Reference`并且打开相应的`Attributes Inspector`。如下图所示: ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard6.png) 在这个字段中,选择你想要引用的`storyboard`(在我们的例子中是`Third`)。如果这个字段为空白,则被引用的`storyboard`是定义的`Storyboard Reference`。`Reference ID`指向在目的`storyboard`中的一个特定`scene`。如果你置空的话,初始化`view controller`会加载。最后,`Bundle`字段需要被置为包含目的`storyboard`的`bundle`。如果你留空的话,就会使用源`storyboard`的`bundle`。 ![Storyboard Reference Assignment](http://www.invasivecode.com/blogimages/storyboard/storyboard7.png) 在`Third.storyboard`文件中,你需要添加一个新的`view controller`并将其作为初始化的`view controller`。之后,只要`view controller`是`Main storyboard`的一部分,你可以都可以运行`app`并且导航到那里。 所有,现在你可以在多个文件里组织你的`storyboard`,并且可以保持这些`storyboard`的引用。此外,每一个`storyboard`能够被分配给一个不同的开发者,而你不需要去考虑`view controller`间的连接组合。这真是非常方便。 Scene Dock and Extra Views -------------------------- 这是我最喜欢的特性。现在,我能够在`storyboard`中添加在`scene`体系外的视图。为了让你明白它使如何工作的,我们先创建一个新的项目。将其命名为`ExtraView`,打开`Main storyboard`,在顶部的`First Responder`和`Exit`之间添加一个新的`view`。如下图所示(这个叫做`Scene Dock`): ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard8.png) 把这个`view`的大小调整为 1500×120 像素。然后在这个`view`的顶层添加一个大小为 240×112 的小`view`。把这个视图放到大视图的中心,然后增加顶部和底部的约束 (constants = 8),宽度约束(constant = 240) 以及水平居中的约束。然后添加一个`scrollview`到`view controller`中,将其居中,并添加`trailing`和`leading space`约束 (constant = 0),高度约束(constant=128),最后增加垂直居中约束。在`ViewController.swift`中,添加下列两个`outlet`: ``` @IBOutlet var externalView: UIView! @IBOutlet var scrollView: UIScrollView! ``` 将它们连接到`scrollview`以及外面的`view`。最后,添加`viewDidAppear:`方法: ``` override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) scrollView.contentSize = externalView.frame.size scrollView.addSubview(externalView) } ``` 然后运行项目,可以看到的是,你现在可以添加额外的视图(可以任意添加),并且可以在运行期间很方便地加载出来。你可以下载这个[示例](http://www.invasivecode.com/documents/ExtraView.zip)来加深理解。 定制Transitions ------------------ 这是`Xcode 7`中`storyboard`另外一个很酷的新特性。具体的细节我将会留到之后发布的文章,这里我只想先给你一些你能做什么的想法。如果你在多`storyboard`项目中选择任意的`action segue`,并且打开`Attributes Inspector`,你将会看到一个新的字段`Segue Class`,正如下图所示: ![这里写图片描述](http://www.invasivecode.com/blogimages/storyboard/storyboard9.png) 你可以创建一个`UIStoryboardSegue`的子类,然后遵从`UIViewControllerTransitioningDelegate` 协议。然后,在类中实现`animationControllerForPresentedController: presentingController: sourceController:` 以及 `animationControllerForDismissedController:`。此外,你还需要创建两个`NSObject`的子类,遵从`UIViewControllerAnimatedTransitioning delegate`。在这些类中,你必须实现两个方法:`transitionDuration:` 和 `animateTransition:`。 我将会在接下来的文章中介绍其中的细节。 总结 ----------- `Xcode 7`的`Storyboards`增加了很多便利的新特性。我们现在可以创建`storyboard references`,在`scene`体系外增加视图,并且可以使用新的定制视图过渡。我还讨论了为什么你应该将`outlet`定义为`strong`引用而不是`weak`。 作者介绍 ---- Geppy Geppy Parziale ([@geppyp](http://twitter.com/geppyp)) is cofounder of InvasiveCode ([@invasivecode](http://twitter.com/invasivecode)). He has developed iOS applications and [taught iOS development](http://ios-training.invasivecode.com/) since 2008. He worked at Apple as iOS and OS X Engineer in the Core Recognition team. He has developed several iOS and OS X apps and frameworks for Apple, and many of his development projects are top-grossing iOS apps that are featured in the App Store.]]></content>
</entry>
<entry>
<title><![CDATA[Playgrounds中的字面量(Literals)]]></title>
<url>%2F2015%2F10%2F12%2FPlaygrounds%E4%B8%AD%E7%9A%84%E5%AD%97%E9%9D%A2%E9%87%8F%EF%BC%88Literals%EF%BC%89%2F</url>
<content type="text"><![CDATA[title: "Playgrounds中的字面量(Literals)" date: 2015-10-12 tags: [Literals,字面量,Playgrounds] categories: [Swift 入门] description: Xcode 7.1 的新特性是能够把文件、图像以及颜色等字面量嵌入你的 playground 的代码。字面量是你原生格式数据的实际值,可以直接在 Xcode 里面编辑。 --- 原文链接 = https://developer.apple.com/swift/blog/?id=33 作者 = Apple's blog 原文日期 = 2015-10-07 -------------------------- `Xcode 7.1` 的新特性是能够把文件、图像以及颜色等字面量嵌入你的`playground`的代码。字面量是你原生格式数据的实际值,可以直接在`Xcode`里面编辑。例如,在编写代码时没必要指明`myImage.jpg` —— 只需要从`Finder`中拖入图片,然后实际的图片就会在你代码行中显示。`playground`将会呈现色块,替代原先用`RGB`值显示颜色的方法。`playground`中使用字面量的效果和代码编写的效果相似,你可以在传统的`Swift`代码中任意选择使用,但显然字面量是一种更为有效的方式。 除了看起来很酷之外,字面量能够更快速地编辑资源。你可以使用颜色选择器中的调色板快速地选择一个不同的颜色。还可以从`Finder`中拖入和拖出文件到`playground`代码,并可以直接使用。你甚至可以在你现在的光标处添加字面量,可通过选择`Editor > Insert File, Image`或者`Color` 字面量。双击一个字面量可以很简单地选择其他值。 如果需要的话,资源会被拷贝到`playground`的资源目录,所以`playground`需要的所有东西都包含在文档当中。由于字面量是你代码的一部分,所以你也可以准确地对你的源代码进行拷贝,粘贴,移动以及删除操作。 Swift代码中的字面量 ---------------------- 字面量可以转换成特殊的平台类型,默认的转换列举如下: ![这里写图片描述](http://img.blog.csdn.net/20151011213551834) 为了获得字面量完全内嵌的使用经验,你必须在`playground`中使用它。然而,如果你拷贝了使用字面量的代码并粘贴到你的`Swift`主源代码中,粘贴的代码也将会如你期望的那样工作,并且`Xcode`将会简单地把字面量呈现为纯文本。 为了让你开始使用字面量,我们已经在这个里面博客包含了一个非常简短的`playground`示例。下载最新的[Xcode 7.1 beta](http://developer.apple.com/xcode/download)去试用这个[playground](http://developer.apple.com/swift/blog/downloads/Literals.zip)。 附加的文档 ----- `Xcode 7.1 beta 3`的文档包括一个已更新的`playgrounds`帮助文档,其中有很多`playgrounds`中强大特性的新信息,包括字面量中的新内容。这里有一个直接的相关子页面的链接:[添加图片字面量](https://developer.apple.com/library/prerelease/ios/recipes/Playground_Help/Chapters/AddImageLiteral.html#//apple_ref/doc/uid/TP40015166-CH49-SW1),[添加颜色字面量](https://developer.apple.com/library/prerelease/ios/recipes/Playground_Help/Chapters/AddColorLiteral.html#//apple_ref/doc/uid/TP40015166-CH50-SW1),[添加文件字面量](https://developer.apple.com/library/prerelease/ios/recipes/Playground_Help/Chapters/AddFileLiteral.html#//apple_ref/doc/uid/TP40015166-CH51-SW1)。 以下的截图证实了字面量在`Xcode 7.1` 中是如何显示的: ![这里写图片描述](https://devimages.apple.com.edgekey.net/swift/blog/images/literals.jpg) 下载:[Literals.playground](https://developer.apple.com/swift/blog/downloads/Literals.zip)]]></content>
</entry>
<entry>
<title><![CDATA[【WatchOS 2教程系列二】WatchConnectivity介绍:告别加载等待]]></title>
<url>%2F2015%2F10%2F10%2FWatchConnectivity%E4%BB%8B%E7%BB%8D%EF%BC%9A%E5%91%8A%E5%88%AB%E5%8A%A0%E8%BD%BD%E7%AD%89%E5%BE%85%2F</url>
<content type="text"><![CDATA[title: "【WatchOS 2教程系列二】WatchConnectivity介绍:告别加载等待" date: 2015-10-10 tags: [WatchConnectivity] categories: [WatchOS 入门] description: 在 WatchOS 2 上最有价值的新特性就是 WatchConnectivity,虽然用户可能看不到,但是这个特性能让你的 WatchOS app 更加好用。 --- 原文链接 = http://natashatherobot.com/watchconnectivity-introduction-say-goodbye-to-the-spinner/ 作者 = Natasha The Robot 原文日期 = 2015-09-21 ---------- 在`WatchOS 2` 上最有价值的新特性就是`WatchConnectivity`,虽然用户可能看不到,但是这个特性能让你的`WatchOS app`更加好用。 `WatchConnectivity`是`WatchOS 2`框架中用于`Watch App`和`iOS`设备传输数据的。`WatchConnectivity` 关键的部分是,它使你的应用程序在用户决定看看你的`Complication or Glance or App`时能够有必要的数据。这意味着用户想要看你的`app or glance or complication`时,他们希望马上看到他们想要看到的数据,而不是愚蠢的加载等待。 毕竟,`Apple Watch`是一个活动的设备。虽然用户们可能想要看一两眼在`iOS app` 上超级可爱的刷新动画,但显然他们不会忍受在活动的设备上看到这样的动画。设想一下,如果用户每次在他们常规的手表上查看时间,引入眼帘的是一个加载等待界面,那将会非常愚蠢,并且如果你在你的`app`上这么做也是一样愚蠢的。 现在你不必再担心了,`WatchConnectivity`可以完全解决这个难题,它可以毫无压力地实现传输你的`app`上的数据到你的`Watch App`上,并且整个过程是无缝透明的,以至于你的用户不需要知道任何东西。 所以让我们开始深挖吧!`WatchConnectivity`有两个部分-后台传输(`background transfers`)和交换信息(`interactive messaging`)。我将会在未来的教程里探究它的每一个部分的更多细节,但是这里只是一个概述,思考传输时应该使用哪一种传输模型: Background Transfers -------------------- 后台传输用在你的`iOS`或者`Watch App`不需要马上获得信息时。当然,在你的用户抬起他们的手腕时查看`app`里面的最新数据时,它会显示数据,但是此前他们不需要任何数据。 因为后台传输应用于传输不是马上就需要的数据,`Apple`认为使用后台传输的最佳时机是-当有些东西依赖于如:电池容量,网络连接,使用模式等时。 在你的`iOS`和`Watch app`之间的后台传输数据有三种方式: Application Context ------------------- 当你的`Watch App`只需要展示最新的信息时,使用`Application Context`。例如,当你的`Glance`显示了比分,用户不会在意两分钟以前的 4-2 比分,他们只在乎现在的比分是 4-4 。另一个使用示例的例子是交通运输`app`,你不需要关心五分钟以前最后的一辆公交车在公交站的左边,他们只关心下一辆公交车什么时候到。 所以`Application Context`的工作方式是把数据块排成队列,并且如果在传输之前有一个新的可用数据块,原始的数据将会被新数据取代,然后再传输这个数据,除非它又被其它更新的数据块代替。 [Tutorial: Sharing The Latest Data via Application Context](http://natashatherobot.com/watchconnectivity-application-context/) User Info --------- `User Info`是用于当你需要确认你的所有数据是被传输过的(不像`Application Context`)。`User Info`的数据是在一个`FIFO (first-in-first-out)` 队列中顺序传输的,所以没有东西被重写。 一个例子是你可能想要在一个文本消息的`app`中使用它-对于一个完整的会话和上下文环境来说,最后一条信息和第一条信息是同等重要的。如果用户更新了他们简介信息中的一小部分,`Watch` 简介中也应该同步这些更新。 File Transfer ------------- 这个是不解自明的。在你的`iOS`和`Watch app`之间使用`File Transfer`去传输文件,例如图片或者`plists`。文件传输一个很棒的特性是你可以包含一个`meta-data`字典,其中包含你的文件名和数据,比如说这样你就可以排序你的图片。 Interactive Messaging --------------------- 使用`Interactive Messaging` 能够实时地在你的`iOS`和`Wach app`之间传输数据!一个绝佳的示例就是愤怒的小鸟`app`的`Watch`版本和`iPhone`版本-用户点击`Watch`,但是小鸟在手机上飞。按钮点击通过`Interactive Messaging`被传输到手机上了。 ![这里写图片描述](http://natashatherobot.com/wp-content/uploads/flappybirdwatch.gif) 一个需要注意的地方,`Interactive Messaging`需要`iPhone`开启`"reachable"`状态。根据`Apple's`文档解释: ``` “Reachability in the Watch app requires the paired iOS device to have been unlocked at least once after reboot.” ``` TLDR ---- 我爱死[Kristina Thai’s WatchConnectivity post](http://www.kristinathai.com/watchos-2-how-to-communicate-between-devices-using-watch-connectivity/) 里面区别传输的图解了: ![这里写图片描述](http://natashatherobot.com/wp-content/uploads/Screen-Shot-2015-09-21-at-8.17.29-AM.png) 同时,本文参考了`Curtis Herbert`的文章 [Getting Data to Your WatchOS 2 App](http://blog.curtisherbert.com/data-synchronization-with-watchos/) 中底部的`Watch OS 2 observations`部分。]]></content>
</entry>
<entry>
<title><![CDATA[【WatchOS 2教程系列一】WatchOS 2: Hello, World]]></title>
<url>%2F2015%2F10%2F02%2FWatchOS2-Hello%2CWorld(2)%2F</url>
<content type="text"><![CDATA[title: "【WatchOS 2教程系列一】WatchOS 2: Hello, World" categories: WatchOS 入门 tags: [WatchOS] date: 2015-10-02 09:00:00 description: 欢迎学习我的`WatchOS 2`系列教程。我将会从最简单的部分开始让你学习`WatchOS 2`。一个 "Hello,World" app。是的,虽然这个程序足够简单,但是其中还是有一些注意点。 --- 原文链接 = http://natashatherobot.com/watchos-2-hello-world/ 作者 = Natasha The Robot 原文日期 = 2015-09-21 ---------- 欢迎学习我的`WatchOS 2`系列教程。我将会从最简单的部分开始让你学习`WatchOS 2`。一个 "Hello,World" app。是的,虽然这个程序足够简单,但是其中还是有一些注意点。 首先,创建一个新的`Single View Application`。我将假设你已经知道如何创建一个基本的`Xcode`项目。现在进入有趣的部分: 1. 在 `Xcode`, 打开 `File -> New -> Target` ![这里写图片描述](http://natashatherobot.com/wp-content/uploads/Screen_Shot_2015-09-21_at_7_23_42_AM.png) 2. 选择`watchOS -> Application`。注意在`iOS`菜单下面有一个`Apple Watch`选项。不要选择这个选项!因为我曾经做过 `WatchKit apps`,所以系统自动选择了这个错误的选项 :( 。 ![这里写图片描述](http://natashatherobot.com/wp-content/uploads/ItsAWatchWorld_xcodeproj.png) 3. 选择`WatchKit App` 然后点击`Next`。注意**你的Watch App项目名称不能和iOS名称相同** - 因为这样会创建两个名字相同的`target`!你可以随时通过你的`Info.plist`改变`Bundle Display Name`。同时注意`Include Complication`选项不要勾选 :) 。 ![这里写图片描述](http://natashatherobot.com/wp-content/uploads/Screenshot_9_21_15__7_35_AM.png) 4. 点击`Finish`。就酱!来看一下你的新 `App` 和 `Extension` 吧。跟 `WatchKit` 一样,这里有一个用于 Watch 的 `target`,并且附带了一个 `Storyboard`,以及用于编写程序逻辑的 `Extension`。 ![Menubar_and_ItsAWatchWorld_xcodeproj_and_MyPlayground_6_36_06_PM_playground](http://natashatherobot.com/wp-content/uploads/Menubar_and_ItsAWatchWorld_xcodeproj_and_MyPlayground_6_36_06_PM_playground.png) 开心地编程吧!!]]></content>
</entry>
<entry>
<title><![CDATA[【 swift 】17 条Swift最佳实践规范]]></title>
<url>%2F2015%2F09%2F24%2F%E6%9C%80%E4%BD%B3Swift%E5%AE%9E%E8%B7%B5(1)%2F</url>
<content type="text"><![CDATA[title: 【 swift 】17 条Swift最佳实践规范 categories: Swift 入门 tags: swift date: 2015-09-24 description: 这篇文章是我根据在 SwiftGraphics 工作时的一系列笔记整理出来的。文中大多数建议是经过深思熟虑的意见和论点,但仍可以有其他类似的方法解决问题。因此,如果其他方案是有意义的,这些方案会被附加提出。 --- 这是一篇最佳的 `Swift` 软件开发实践教程。 前言 -- 这篇文章是我根据在 `SwiftGraphics` 工作时的一系列笔记整理出来的。文中大多数建议是经过深思熟虑的意见和论点,但仍可以有其他类似的方法解决问题。因此,如果其他方案是有意义的,这些方案会被附加提出。 这个最佳实践不是强加或者推荐 `Swift` 在程序、面向对象或者函数风格上的应用。更重要的是,这里要讲述的是务实的方法。如有需要的话,某些建议可能会集中在面向对象或者实用的解决方法。 这篇文章讲述的范围主要针对 `Swift` 语言以及 `Swift` 标准库。即便如此,如果能提出一个独特的 `Swift` 的视角和见解,我们依然会提供诸如 `Swift` 在`Mac OS`,`IOS`,`WatchOS`以及`TVOS`上使用的特别建议。而如何在`Xcode` 和 `LLDB` 上有效地使用 `Swift`,这样的建议也会以 `Hints & tips` 的风格提供。 这个过程需要付出很多的努力,非常感谢为本文做出贡献的那些人。 此外,可以在[Swift-Lang slack](http://swift-lang.schwa.io/)里面讨论。 贡献者须知 -------------------- 请先确保所有的示例是可以运行的(某些示例可能不是正确)。这个`markdown` 能够转换成一个 `Mac OS X playground`。 黄金准则 ---- 一般来说,`Apple`都是正确的,遵循 `Apple's` 喜欢的或者示范的处理方式。在任何情况下,你都应该遵循 `Apple's` 的代码风格,正如他们在"`The Swift Programming Language"`这本书里面的定义一样。然而 `Apple` 是个大公司,我们将会看到很多在示例代码中的差异。 永远不要仅仅为了减少代码量而去写代码。尽量依赖Xcode中的自动补全代码,自动建议 , 复制和粘贴。详尽的代码描述风格对其他代码维护者来说是非常有好处的。即便如此,过度的冗余也会失去 Swift 的重要特性:类型推断。 最佳实践 ---- 命名 -- 正如 `Swift Programming Language`中的类型名称都是以大驼峰命名法命名的(例如:`VehicleController`)。 变量和常量则以小驼峰命名法命名(例如:`vehicleName`)。 你应该使用 `Swift` 模板去命名你的代码而不是使用`Objective-C`类前缀的风格(除非和 `Objective-C` 接连)。 不要使用任何匈牙利标识法(`Hungarian notation`)命名(例如:k为常量,m为方法),应使用简短的命名并且使用`Xcode`的类型 `Quick Help (⌥ + click)` 去查明变量的类型。同样地,不要使用小写字母+下划线(`SNAKE_CASE`)的命名方式。 唯一比较特别的是`enum`值的命名,这里需要使用大驼峰命名法(这个也是遵循`Apple`的`Swift Programming Language`风格): ``` enum Planet { case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune } ``` 在所有可能的情况里,名称的不必要减少和缩写都应该避免,将来你应该能在没有任何损害和依赖`Xcode`的自动补全功能的情况下,确切地指出类型特征"`ViewController`"。非常普遍的缩写如`URL`是允许的。缩写应该用所有字母大写(`URL`)或者所有字母小写(`url`)表示。对类型和变量使用相同的规则。如果`url`是个类型,则应该为大写,如果是个变量,则应该为小写。 注释 -- 注释不应该用来使代码无效,注释代码会使代码无效且影响代码的整洁。如果你想要移除代码,但是仍想保留以防代码在以后会用到,你应该依赖 `git` 或者 `bug tracker` 。 类型推断 ---- 在可能的地方,使用Swift的类型推断以减少多余的类型信息。例如,正确的写法: ``` var currentLocation = Location() ``` 而不是: ``` var currentLocation: Location = Location() ``` Self 推断 ------ 让编译器在所有允许的地方推断`self`。在`init`中设置参数以及`non-escaping closures`中应该显性地使用`self`。例如: ``` struct Example { let name: String init(name: String) { self.name = name } } ``` 参数列表类型推断 ------ 在一个闭包表达式(`closure expression`)中指定参数类型可能导致代码更加冗长。只有当需要指定类型时。 ``` let people = [ ("Mary", 42), ("Susan", 27), ("Charlie", 18), ] let strings = people.map() { (name: String, age: Int) -> String in return "\(name) is \(age) years old" } ``` 如果编译器能够推断类型,则应该去掉类型定义。 ``` let strings = people.map() { (name, age) in return "\(name) is \(age) years old" } ``` 使用排序好的参数编号命名("$0","$1","$2")能更好地减少冗余,这经常能够完整匹配参数列表。只有当closure的参数名称中没有过多的信息时,使用编号命名。(例如特别简单的`maps`和`filters`)。 `Apple`能够并将会改变由`Objective-C frameworks`转换过来的`Swift`的参数类型。例如,选项被移除或者变为自动展开等。我们应有意地指定你的选项并依赖`Swift`去推断类型,减少在这种情况下程序中断的风险。 你总是应该有节制地指定返回类型。例如,这个参数列表明显过分冗余: ``` dispatch_async(queue) { () -> Void in print("Fired.") } ``` 常量 -- 在类型定义的时候,常量应该在类型里声明为`static`。例如: ``` struct PhysicsModel { static var speedOfLightInAVacuum = 299_792_458 } class Spaceship { static let topSpeed = PhysicsModel.speedOfLightInAVacuum var speed: Double func fullSpeedAhead() { speed = Spaceship.topSpeed } } ``` 使用`static`修饰常量可以允许他们在被引用的时候不需要实例化类型。 除了单例以外,应尽量避免生成全局常量。 计算型类型属性(Computed Properties) ---------------------------- 当你只需要继承`getter`方法时,返回简单的`Computed`属性即可。例如,应该这样做: ``` class Example { var age: UInt32 { return arc4random() } } ``` 而不是: ``` class Example { var age: UInt32 { get { return arc4random() } } } ``` 如果你在属性中添加了`set`或者`didSet`,那么你应该显示地提供`get`方法。 ``` class Person { var age: Int { get { return Int(arc4random()) } set { print("That's not your age.") } } } ``` 实例转换(Converting Instances) ---- 当创建代码去从一个类型转换到另外的`init()`方法: ``` extension NSColor { convenience init(_ mood: Mood) { super.init(color: NSColor.blueColor) } } ``` 在`Swift`标准库中,对于把一个类型的实例转换为另外一种,现在看来init方法是比较喜欢用的一种方式。 `"to"`方法是另外一种比较合理的技术(尽管你应该遵循`Apple`的引导去使用`init`方法): ``` struct Mood { func toColor() -> NSColor { return NSColor.blueColor() } } ``` 而你可能试图去使用一个getter,例如: ``` struct Mood { var color: NSColor { return NSColor.blueColor() } } ``` `getters`通常由于应该返回可接受类型的组件而受到限制。例如,返回了`Circle`的实例是非常适合使用`getter`的,但是转换一个`Circle`为`CGPath`最好在`CGPath`上使用`"to"`函数或者`init()`扩展。 单例(Singletons) -------------- 在Swift中单例是很简单的: ``` class ControversyManager { static let sharedInstance = ControversyManager() } ``` `Swift`的`runtime`会保证单例的创建并且采用线程安全的方式访问。 单例通常只需要访问"`sharedInstance`"的静态属性,除非你有不得已的原因去重命名它。注意,不要用静态函数或者全局函数去访问你的单例。 (因为在`Swift`中单例太简单了,并且持续的命名已经耗费了你太多的时间,你应该有更多的时间去抱怨为什么单例是一个反模式的设计,但是避免花费太多时间,你的同伴会感谢你的。) 使用扩展来组织代码 ------------------------------------------- 扩展应该被用于组织代码。 一个实例的次要方法和属性应该移动到扩展中。注意,现在并不是所有的属性类型都支持移动到扩展中,为了做到最好,你应该在这个限制中使用扩展。 你应该使用扩展去帮助组织你的实例定义。一个比较好的例子是,一个`view controller`继承了`table view data source` 和 `delegate protocols`。为了使table view中的代码最小化,把`data source`和`delegate`方法整合到扩展中以适应相应的`protocol`。 在一个单一的源文件中,在你觉得能够最好地组织代码的时候,把一些定义加入到扩展中。不要担心把`main class`的方法或者`struct`中指向方法和属性定义的方法加入扩展。只要所有文件都包涵在一个`Swift`文件中,那就是没问题的。 反之,`main`的实例定义不应该指向定义在超出`main Swift` 文件范围的扩展的元素。 链式 Setters -------------------------- 对于简单的`setters`属性,不要使用链式`setters`方法当做便利的替代方法。 正确的做法: ``` instance.foo = 42 instance.bar = "xyzzy" ``` 错误的做法: ``` instance.setFoo(42).setBar("xyzzy") ``` 相较于链式setters,传统的setters更为简单和不需要过多的公式化。 错误处理 -------------------- Swift 2.0 的 do/try/catch 机制非常棒。 避免使用`try!` ---------- 一般来说,使用如下写法: ``` do { try somethingThatMightThrow() } catch { fatalError("Something bad happened.") } ``` 而不是: ``` try! somethingThatMightThrow() ``` 即使这种形式特别冗长,但是它提供了context让其他开发者可以检查这个代码。 在更详尽的错误处理策略出来之前,如果把`try!`当做一个临时的错误处理是没问题的。但是建议你最好周期性地检查你代码,找出其中任何有可能逃出你代码检查的非法try!。 避免使用try? try?是用来“压制”错误,而且只有当你确信对错误的生成不关心时,try?才是有用的。一般来说,你应该捕获错误并至少打印出错误。 过早返回&Guards ----------------------------------- 可能的话,使用guard声明去处理过早的返回或者其他退出的情况(例如,fatal errors 或者 thorwn errors)。 正确的写法: ``` guard let safeValue = criticalValue else { fatalError("criticalValue cannot be nil here") } someNecessaryOperation(safeValue) ``` 错误的写法: ``` if let safeValue = criticalValue { someNecessaryOperation(safeValue) } else { fatalError("criticalValue cannot be nil here") } ``` 或者: ``` if criticalValue == nil { fatalError("criticalValue cannot be nil here") } someNecessaryOperation(criticalValue!) ``` 这个flatten code以其他方式进入一个if let 代码块,并且在靠近相关的环境中过早地退出了,而不是进入else代码块。 甚至当你没有捕获一个值(guard let),这个模式在编译期间也会强制过早退出。在第二个if的例子里,尽管代码flattend得像guard一样,但是一个毁灭性的错误或者其他返回一些无法退出的进程(或者基于确切实例的非法态)将会导致crash。一个过早的退出发生时,guard声明将会及时发现错误,并将其从else block中移除。 "Early"访问控制 ------------------------------------ 即使你的代码没有分离成独立的模块,你也应该经常考虑访问控制。把一个定义标记为`private` 或者 `internal` 对于代码来说相当于一个轻量级的文档。每一个阅读代码的人都会知道这个元素是不能“触碰”的。反之,把一个定义为`public` 就相当于邀请其他代码去访问这个元素。我们最好显示地指明而不是依赖`Swift`的默认访问控制等级。(`internal`) 如果你的代码库在将来不断扩张,它可能会被分解成子模块.这样做,会使一个已经装饰着访问控制信息的代码库更加方便、快捷。 限制性的访问控制 ------------------ 一般来来说,当添加访问控制到你的代码时,最好有详尽的限制。这里,使用`private` 比`internal` 更有意义,而使用`internal`显然比`public`更好。(注意:`internal`是默认的)。 如有需要,把代码的访问控制变得更加开放是非常容易的(沿着这样的途径:`"private" to "internal" to "public")` 。过于开放的访问控制代码被其他代码使用可能不是很合适。有足够限制的代码能够发现不合适和错误的使用,并且能提供更好的接口。一个例子就是一个类型公开地暴露了一个internal cache。 而且,代码的限制访问限制了“暴露的表面积”,并且允许代码在更小影响其他代码的情况下重构。其他的技术如:`Protocol Driven Development` 也能起到同样的作用。]]></content>
</entry>
<entry>
<title><![CDATA[【OC】开发中选择delegate 还是 block]]></title>
<url>%2F2015%2F09%2F23%2F%E3%80%90OC%E3%80%91delegateorblock%2F</url>
<content type="text"><![CDATA[title: 【OC】开发中选择delegate 还是 block categories: Objective-C tags: [OC,delgate,block] date: 2015-09-23 description: 有人问了我一个很棒的问题,我把这个问题总结为:开发过程中该选择 `blocks or delegates`?当我们需要实现回调的时候,使用哪一种方式比较合适呢? --- ``` By Joe Conway,CEO | Jul 11,2013 11:29:00 AM ``` 译者注:译文已在`CocoaChina`上发布。 http://www.cocoachina.com/ios/20150927/13525.html 前文:网络上找了很多关于delegation和block的使用场景,发现没有很满意的解释,后来无意中在stablekernel找到了这篇文章,文中作者不仅仅是给出了解决方案,更值得我们深思的是作者独特的思考和解决问题的方式,因此将这篇文章翻译过来,和诸君探讨,翻译的很多地方不是很到位,望大家提出意见建议。 ---------- 有人问了我一个很棒的问题,我把这个问题总结为:“开发过程中该选择 `blocks or delegates`?当我们需要实现回调的时候,使用哪一种方式比较合适呢?” 一般在这种情况下,我喜欢问我自己:“如果问题交给`Apple`,他会怎么做呢?”当然,我们都知道`Apple`肯定知道怎么做,因为从某一层面上看,`Apple`的文档就是一本用来指导我们如何使用设计模式的指导书。 因此我们需要去研究一下`Apple`分别是在什么情况下使用`delegate`和`block`,如果我们发现了`Apple`做这种选择的套路,我们就可以构建出一些可以帮助在我们在自己的代码中做相同选择的规则。 如果要找出`Apple`使用`delegate`的场景很简单,我们只要搜索官方文档中的”`delegate`”,就会获取到很多使用`delegation`的类。 但是搜索`Apple`中有关使用`blocks`的文档就有点困难了,因为我们不能直接搜索文档中的`^` 。然而,`Apple`声明方法时有很好的命名习惯(这也是我们精通`IOS`开发的一项必备技能)。例如:一个以`NSString`为参数的方法,方法的`selector`就会有`String`字眼,像`initWithString`;`dateFromString`;`StartSpeaingString`。 当`Apple`的方法使用`block`,这个方法将会有”`Handler`”,”`Completion`”或者简单的“`Block`”作为`selector`;因此我们可以在标准的`IOS API`文档中搜索这些关键词,用以构建一个可信任的`block`用例列表。 大多数`delegate protocols` 都拥有几个消息源。 ----------------------------------- 以我正在看的`GKMatch(A GKMatch object provides a peer-to-peer network between a group of devices that are connected to Game Center`,是`IOS API`中用来提供一组设备连接到`Game Center`点对点网络的对象)为例。从这个类中可以看到消息的来源分别是:当从其他玩家那接收到数据,当玩家切换了状态,当发生错误或者当一个玩家应该被重新邀请。这些都是不同的事件。如果`Apple`在这里使用`block`,那么可能会有以下两种解决方式: 一、可以对应每一个事件注册相应的block,显然这种方式是不合理的。( `If someone writes a class that does this in Objective-C, they are probably an asshole.`) 二、创建一个可以接受任何可能输入的`block` ``` void (^matchBlock)(GKMatchEvent eventType, Player *player, NSData *data, NSError *err); ``` 很明显这种方式既不简便又不易读,所以你可能从未看过这样的解决方案。如果你看过这样的解决方式,但是这显然是一个糟糕至极的代码行,你不会有精力去维护这个。 因此,我们可以得出一个结论:如果对象有超过一个以上不同的事件源,使用`delegation`。 一个对象只能有一个`delegate` --------------------- 由于一个对象只能有一个`delegate`,而且它只能与这个`delegate`通信。让我们看看`CLLocationManager` 这个类,当发现地理位置后,`location manager` 只会通知一个对象(有且只有一个)。当然,如果我们需要更多的对象去知道这个更新,我们最好创建其他的`location manager`。 这里有的人可能想到,如果`CLLocationManager`是个单例呢?如果我们不能创建`CLLocationManager`的其他实例,就必须不断地切换`delegate`指针到需要地理数据的对象上(或者创建一个只有你理解的精密的广播系统)。因此,这样看起来,`delegatetion`在单例上没有多大意义。 关于这点,最好的印证例子就是`UIAccelerometer`。在早期版本的`IOS`中,单例的 `accelerometer` 实例有一个`delegate`,导致我们必须偶尔切换一下。这个愚蠢的问题在之后的`IOS`版本被修改了,现在,任意一个对象都可以访问`CMMotionManager block`,而不需要阻止其他的对象来接收更新。 因此,我们可以得出另一个结论:“如果一个对象是单例,不要使用`delegation`” 一般的`delegate`方法会有返回值 ---------------------- 如果你观察一些`delegate`方法(几乎所有的`dataSource`方法)都有一个返回值。这就意味着`delegating`对象在请求某些东西的state(对象的值,或者对象本身),而一个`block`则可以合理地包含`state`或者至少是可以推断`state`,因此`block`真正是对象的一个属性。 让我们思考一下一个有趣的场景,如果向一个`block`提问:`What do you think about Bob?`。`block`可能会做两件事情:发送一个消息去捕获对象并询问这个对象怎么看待`Bob`,或者直接返回一个捕获的值。如果返回了一个对象的响应,我们应该越过这个`block`直接获取这个对象。如果它返回了一个捕获的值,那么这应该是一个对象的属性。 从以上的观察,我们可以得出结论:如果对象的请求带有附加信息,更应该使用`delegation` 过程 vs 结果(`Process vs. Results`) --------------------------------- 如果查看`NSURLConnectionDelegate` 以及 `NSURLConnectionDataDelegate`,我们在可以`protocol`中看到这样的消息:我将要做什么(如: `willSendRequest`,将要发送请求)、到目前为止我知道的信息(如:`canAuthenticateAgainstProtectionSpace`)、我已经完成这些啦( `didReceiveResponse`,收到请求的回复,即完成请求)。这些消息组成一个流程,而那些对流程感兴趣的delegate将会在每一步得到相应的通知。 当我们观察`handler`和完整的方法时,我们发现一个`block`包含一个响应对象和一个错误对象。显然这里没有任何有关“我在哪里,我正在做什么的”的交互。 因此我们可以这样认为,`delegate`的回调更多的面向过程,而`block`则是面向结果的。如果你需要得到一条多步进程的通知,你应该使用`delegation`。 而当你只是希望得到你请求的信息(或者获取信息时的错误提示),你应该使用block。(如果你结合之前的三个结论,你会发现`delegate`可以在所有事件中维持`state`,而多个独立的`block`确不能) 从上面我们可以得出两个关键点。首先,如果你使用`block`去请求一个可能失败的请求,你应当只使用一个`block`。我们可以看到如下的代码: ``` [fetcher makeRequest:^(id result) { // do something with result } error:^(NSError *err) { // Do something with error }]; ``` 上面代码的可读性明显比下面`block`的可读性差(作者说这个是他个人不谦虚的观点,译者认为没有那么严重) ``` [fetcher makeRequest:^(id result, NSError *err) { if(!err) { // handle result } else { // handle error } }]; ```]]></content>
</entry>
<entry>
<title><![CDATA[好的错误处理原则]]></title>
<url>%2F2015%2F09%2F18%2Fgood-errors-swiftlang%2F</url>
<content type="text"><![CDATA[title: "好的错误处理原则" date: 2015-09-18 tags: [Erica Sadun] categories: [Swift 入门] permalink: good-errors-swiftlang --- 为了处理错误,我们可以抛出一些遵循 `ErrorType` 协议的实例。最简单的例子,创建一个 `struct` 并且抛出错误,就像如下示例: ``` public struct SomethingWentWrong : ErrorType {} ...something happens... throw SomethingWentWrong() ``` 在所有可能最好的描述里,你的错误信息阅读起来不应该像随机的幸运纸片那样杂乱无章,而应该是像建设性指针那样,能够解释你的失败。应避免出现下列的写法: ``` public enum SomethingWentWrongError: ErrorType { case YouWillFindNewLove case AClosedMouthGathersNoFeet case CynicsAreFrustratedOptimists case IfEverythingIsComingYourWayYouAreInTheWrongLane } ``` 优秀的错误处理机制应该更易于我们在特定语境中理解错误: - 逻辑清晰。一个优秀的错误信息应该表明问题是什么,问题的起因是什么,问题的源头在哪里,以及如何解决这个问题。我们可以从 `Foundation` 里面的错误处理获取灵感,并在你的错误反馈中提供错误原因和恢复方法。 - 保持精准。 当你的错误返回一个错误点时,错误信息越具体,使用你代码的程序员就越有可能使用这个具体的信息修复他们的代码,或者在运行代码时找到变通的办法。 - 整合细节。`Swift` 错误机制允许我们创建结构体,关联值,并且提供了错误产生位置和产生原因的重要上下文信息。因此我们应该创建更多有关错误的详尽信息。 - 语义清楚。不要单纯为了让错误信息简短就去限制错误信息的字数。正确的写法: `Unable to access uninitialized data store`;错误的写法: `Uninitialized`。同时,应该正确界定正在处理错误的问题说明,避免出现不必要的冗余描述。 - 添加支持。当你加入 API 和 文档的引用后, 可以进一步帮助你解释上下文和支持的恢复方法。使用链接和片段是很好的方式,但是全文档说明就没必要了。允许使用像快速帮助这样的特性去适当地理解这段代码的作用,而不是试图去篡改他们。 - 避免使用术语。特别是当你的错误,可能在超出你正在运行的上下文环境下产生时。因此,推荐选择简单且更加通俗的语言,而不是专业的术语名称。 - 措辞应优雅礼貌。在错误信息中不应该羞辱你的同伴,你的管理者,或者那些和你一起努力奋斗开发 `API` 的人。此外,应减少错误信息中幽默的写法,幽默的效果并不大,一个自嘲意味的错误信息有可能成为将来某个时刻的隐患。 > 译者注:以苹果开发的角度来说,异常处理可以细分成对于「错误(`error`)」和「异常(`exception`)」两类情况的处理,前者是可以预见到发生可能性的、可以恢复的非致命错误,比如找不到指定位置的文件或者网络忽然断开之类;后者则是无法预见通常也不可恢复的致命错误,比如磁盘坏了、内存没了之类。本文中的异常处理应是错误(`error`)处理。]]></content>
</entry>
</search>