-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
393 lines (230 loc) · 240 KB
/
atom.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Punmy</title>
<subtitle>Codes, thoughts and stories</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://punmy.cn/"/>
<updated>2020-08-02T09:17:15.947Z</updated>
<id>http://punmy.cn/</id>
<author>
<name>Nico</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>WWDC20 10642 使用 Create ML 打造图片和视频的风格转化模型</title>
<link href="http://punmy.cn/2020/07/31/WWDC20_10642.html"/>
<id>http://punmy.cn/2020/07/31/WWDC20_10642.html</id>
<published>2020-07-31T12:35:27.000Z</published>
<updated>2020-08-02T09:17:15.947Z</updated>
<content type="html"><![CDATA[<blockquote><p>本文源自 WWDC 2020 - 10642:Build Image and Video Style Transfer models in Create ML</p></blockquote><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>本文主要介绍了苹果 Create ML 团队新推出一项新的机器学习能力:Style Transfer,也就是风格转化的机器学习能力。文中将会为大家介绍如何构建图片和视频的风格转化模型。</p><a id="more"></a><p><code>Style Transfer</code> 是一项新的机器学习任务,今年可在 Create ML 应用程序中使用。它可以将风格和内容两张图像融合在一起。</p><p>我们先来看下它可以实现的效果。左右分别是风格图像和准备风格化的内容图像,下方是输出的风格化后的图片。</p><p><img src="/media/15962782673196/15962886733780.jpg" alt></p><p><img src="/media/15962782673196/15962903563477.jpg" alt></p><p>我们可以看到,该模型能够将颜色、形状和纹理从风格图像转移到内容图像。</p><p>风格图像的颜色非常重要,会直接影响输出图像的样式。例如下方的例子,训练模型会学习我们给出的风格图像的白色背景和黑色线条,以此来创建黑白线条风格的结果图像。</p><p> <img src="/media/15962782673196/15962909413792.jpg" alt></p><p>当然,绘画不是风格图像的唯一来源,你可以使用各式各样与众不同的风格图像来表达您的创造力。例如下面的几个示例。这些只是你可以使用风格转化的几个示例。</p><p> <img src="/media/15962782673196/15962912568517.jpg" alt></p><p>但是,我们的能力不止于此,我们甚至可以直接将风格转化的模型直接应用到视频上。每帧的风格转化速度都足够快,以保持流畅的视频风格转化。因此可以为视频 App 实时应用风格转化模型。使用 A13 仿生芯片时,我们可以达到每秒120帧的处理速度。</p><h2 id="如何训练模型"><a href="#如何训练模型" class="headerlink" title="如何训练模型"></a>如何训练模型</h2><p>要训练样式转移模型,我们需要提供一些训练数据,包括一张风格图像和一个包含许多内容图像的文件夹。该模型从图像中学习内容和样式之间的平衡。为了获得最佳结果,用于训练的内容图像应与您期望在推理时风格化的图像相似。<br>我们可以调整模型参数来优化样式转移模型的行为。您可以使用风格强度和风格密度这两个模型参数来配置模型的行为。</p><h3 id="1、风格强度"><a href="#1、风格强度" class="headerlink" title="1、风格强度"></a>1、风格强度</h3><p>风格强度参数可以调整样式和内容之间的平衡。在风格强度较低的时候,只有部分图像背景会被风格化,而在风格强度很高的时候,整个图像都会被明显风格化。<br>我们可以看看下图,就能对比出他们的区别。</p><p> <img src="/media/15962782673196/15963528452891.jpg" alt></p><h3 id="2、风格密度"><a href="#2、风格密度" class="headerlink" title="2、风格密度"></a>2、风格密度</h3><p>使用风格密度参数,可以让该模型学习风格图像的细节。</p><p>我们拿下面的例子来看,左图使用了比较小的风格密度,右图则使用了毕竟大的风格密度。可以看到,风格密度越大,训练出来的模型会更加着眼于风格图像的细节,更突出颜色和笔触。反之则相反。</p><p>这会产生不同的风格化结果。我们可以使用风格密度参数来探索各种此类样式。</p><p><img src="/media/15962782673196/15963531836031.jpg" alt></p><h3 id="训练模型"><a href="#训练模型" class="headerlink" title="训练模型"></a>训练模型</h3><p>我们可以方便地使用“Create ML”来训练模型。首先直接创建一个新工程,拖入风格图像,并导入包含数百个内容图像的文件夹。接着我们可以选择针对图像或者视频进行优化。然后调整上述的两个参数,来控制内容和风格之间的平衡。默认参数在大多数情况下都能很好地工作。</p><p>最后,单击工具栏中的训练按钮。该应用程序会处理风格和内容图像,并立即开始训练模型。</p><p>每五次迭代会遇到一个模型检查点,它会将我们的测试图像风格化,以预览效果。这样我们可以使用它来交互式地、可视化地观察模型训练的过程。</p><p>在任何时候,我都可以通过单击工具栏中的快照按钮来捕获模型的快照。快照其实就是一个当前模型的拷贝,以后可以在 App 中直接使用。</p><p>训练模型仅需几分钟,完成训练后,可以拖入新的测试图像或视频,对模型进行预览和测试。完成训练的模型的大小很小,不到1MB。这样我们可以很方便地将它嵌入 App 中。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>风格转化模型性能极高,可以轻松地与其他Apple技术(例如 ARKit)结合使用,实现实时的风格转化效果。模型的尺寸很小,可以很方便地嵌入应用。模型训练时间大大减少,使培训体验变得生动有趣。我们可以在几分钟内训练完模型,然后快速迭代使用不同的风格和模型参数进行尝试。</p>]]></content>
<summary type="html">
本文主要介绍了苹果 Create ML 团队新推出一项新的机器学习能力:Style Transfer,也就是风格转化的机器学习能力。文中将会为大家介绍如何构建图片和视频的风格转化模型。
</summary>
<category term="WWDC" scheme="http://punmy.cn/categories/WWDC/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="WWDC" scheme="http://punmy.cn/tags/WWDC/"/>
</entry>
<entry>
<title>WWDC20 10621 支持性能密集型的应用和游戏</title>
<link href="http://punmy.cn/2020/06/25/WWDC20_10621.html"/>
<id>http://punmy.cn/2020/06/25/WWDC20_10621.html</id>
<published>2020-06-25T14:40:09.000Z</published>
<updated>2020-08-02T09:22:53.435Z</updated>
<content type="html"><![CDATA[<p>iOS 和 iPadOS 提供了强大的功能,可帮助我们开发人员在所有设备上,运行复杂的应用和游戏。但是,在某些情况下,一些对性能要求十分苛刻的应用可能只能在具有 A12 仿生芯片,或更高性能的设备上提供最佳的体验。在 Xcode 12 上,对于性能密集型的应用或游戏,开发人员可以启用一项设置,来表明自己的产品对于性能有着特殊的需求。</p><a id="more"></a><h2 id="设备能力要求选项"><a href="#设备能力要求选项" class="headerlink" title="设备能力要求选项"></a>设备能力要求选项</h2><p>设备能力要求选项(Required device capabilities)是 info.plist 中的一个设置项。它其实并不是一个全新的设置项,Xcode 在之前就已经加入了这个选项。有的同学可能不太熟悉这个选项,它其实就是用来声明我们的应用需要的一些特殊能力,这些特殊能力是和设备相关的,例如 ARKit,它只能在比较新的一些设备上运行。这和我们指定应用能够支持的最低系统版本有点类似,但因为这些特殊能力不仅和系统版本有关系,也依赖于设备本身的硬件支持,所以需要用这个选项来另行声明。</p><p>有了这个选项,我们就可以声明一系列应用运行时所需的,与设备硬件相关的能力。然后,在我们把应用上传到 App Store 时,苹果就可以获取到我们的应用需要的一系列硬件能力。而后,App Store 在进行应用的分发时,就可以确保只让支持相关硬件能力的设备,下载到我们的应用。避免用户下载完应用后,一运行,才发现自己手机不支持。</p><p>即使用户强行安装了应用,iOS 也会直接阻止应用运行。</p><h3 id="现有的设备能力要求选项"><a href="#现有的设备能力要求选项" class="headerlink" title="现有的设备能力要求选项"></a>现有的设备能力要求选项</h3><p>之前,为了确保一些特殊的应用或者游戏能够限制在特定高性能的机器上运行,已经有了一些可选项,例如:</p><ul><li>Metal</li><li>ARKit</li></ul><p><img src="/media/15962781684496/15962782248952.jpg" alt></p><p>Metal 高性能渲染只有在 A7 芯片上才能运行,而 ARKit 只有在支持 AR 的设备上才能运行,一般来说是 A9 或更高的芯片。设置了对应的选项之后,就可以确保应用只在支持 Metal 或是 ARKit 的设备上运行。</p><h3 id="新的选项"><a href="#新的选项" class="headerlink" title="新的选项"></a>新的选项</h3><p>随着 iOS 14 的推出,现在 Xcode 12 中增加了一个新的设备要求选项——指定需要 A12 仿生芯片以上的处理器。有了这个选项,我们就可以将 PC 级别的游戏,或是专业软件移植到 iPhone 或者 iPad 上。</p><p>新的选项的键值为 <code>iphone-ipad-minimum-performance-a12</code>。该项设置在 Xcode 12 上开始生效。</p><p>声明了这项要求后,就指定了需要 A12 仿生芯片以上等级的芯片。该等级包含了:</p><ul><li>6核 CPU 和4核 GPU</li><li>第二代神经网络引擎</li><li>ARKit 3个数量的人体识别和动作捕捉</li><li>Metal GPU 系列 Apple 5</li></ul><p>目前支持该等级的设备包括:</p><ul><li>iPhone 11</li><li>iPhone 11 Pro</li><li>iPhone SE 2</li><li>iPad Mini 5th</li><li>iPad Pro 4th</li></ul><h2 id="如何设置"><a href="#如何设置" class="headerlink" title="如何设置"></a>如何设置</h2><p>打开 info.plist,找到 <code>Required device capabilities</code> 设置项,如果没有则新增一个。该设置项是个数组。在该设置项下新增一个 item,item 的值选择 A12 的选项,即可完成设置。</p><p><img src="/media/15962781684496/15962782421348.jpg" alt></p><h2 id="何时使用这个选项"><a href="#何时使用这个选项" class="headerlink" title="何时使用这个选项"></a>何时使用这个选项</h2><p>首先,苹果肯定是鼓励大家兼容越多机型越好的,不然旧设备各种应用运行不了,用户也会有很大意见。因此,这个设置不到万不得已,尽量不要使用。</p><p>使用这个设置项的一个前提是,我们已经对我们的应用或者游戏,进行了极致的性能优化。<br>只有我们已经尽可能地优化性能了,但还是无法兼容 A12 以下的设备。或是我们必须用到 A12 芯片的某些特有能力,我们才来考虑是否要启用这个设置项。需要注意的是,该设置项需要在 iOS 14 下才能生效。</p><p>注意,一旦开启该设置项,应用就无法被 A12 以下的设备下载和安装。为了测试设置项是否生效,我们可以提前用 TestFlight 来进行验证。</p><h2 id="阅读更多"><a href="#阅读更多" class="headerlink" title="阅读更多"></a>阅读更多</h2><p>更多关于 Metal 相关的性能优化,可以参见以下两个 Session:</p><ul><li>Delivering Optimized Metal Apps and Games</li><li>Harness Apple GPUs with Metal</li></ul>]]></content>
<summary type="html">
iOS 和 iPadOS 提供了强大的功能,可帮助我们开发人员在所有设备上,运行复杂的应用和游戏。但是,在某些情况下,一些对性能要求十分苛刻的应用可能只能在具有 A12 仿生芯片,或更高性能的设备上提供最佳的体验。在 Xcode 12 上,对于性能密集型的应用或游戏,开发人员可以启用一项设置,来表明自己的产品对于性能有着特殊的需求。
</summary>
<category term="WWDC" scheme="http://punmy.cn/categories/WWDC/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="WWDC" scheme="http://punmy.cn/tags/WWDC/"/>
</entry>
<entry>
<title>iOS耗电量和性能优化的全新框架</title>
<link href="http://punmy.cn/2019/06/16/wwdc_417_metrics.html"/>
<id>http://punmy.cn/2019/06/16/wwdc_417_metrics.html</id>
<published>2019-06-16T15:50:03.000Z</published>
<updated>2019-06-18T15:56:47.925Z</updated>
<content type="html"><![CDATA[<blockquote><p><a href="https://developer.apple.com/videos/play/wwdc2019/417/" target="_blank" rel="noopener">Session 417, Improving Battery Life and Performance</a> (MetricsKit)</p></blockquote><p>App 的耗电量和性能表现是用户体验的一个重要部分,耗电量过大或是性能很差的 App 会导致糟糕的用户体验。为了改善用户体验以及延长电池寿命,在 Session 417 中,苹果推出了三项新的电量和性能监测工具,分别用于开发阶段、内测阶段、以及线上阶段。相信通过本文,你会对你的 App 接下去的耗电量和性能优化的方向,有更好的计划。</p><a id="more"></a><p>本次苹果推出的三项工具分别是:</p><ul><li>XCTest Metrics (开发和测试阶段)</li><li>MetricsKit (内测阶段和线上阶段)</li><li>Xcode Metrics Organizer (线上阶段)</li></ul><p><img src="/media/15606640304676/15606648571132.jpg" alt></p><p>Session 417 中,苹果的工程师首先为我们介绍了 Metrics 能够监测的指标,以及各项指标的意义。然后,由三项新工具的负责人,依次为我们介绍了这三项新工具。下面我们进入正文。</p><h2 id="概览"><a href="#概览" class="headerlink" title="概览"></a>概览</h2><p>今年,苹果推出的性能指标监测工具可以分为两大类,分别是耗电量统计和性能监测。之所以要把耗电量统计单独作为一大类,是因为电量对于 iOS 而言真的十分重要。iOS 以其普遍落后的电池容量,做到超越大部分安卓设备的续航表现,很大一部分原因都归功于 iOS 优秀的电池管理和严格的后台任务管理,。</p><p>下面我们从这两大类分别介绍下它们的主要监测指标。</p><p><img src="/media/15606640304676/15606650268386.jpg" alt></p><h3 id="1-Battery-Metrics"><a href="#1-Battery-Metrics" class="headerlink" title="1.Battery Metrics"></a>1.Battery Metrics</h3><p><img src="/media/15606640304676/battery.jpg" alt="battery"></p><p>耗电量的指标可以分为几种,Metrics 可以分别进行统计,可以统计的几个耗电量大户分别是:</p><ul><li>Processing</li><li>Location</li><li>Display</li><li>Networking</li><li>Accessories (蓝牙)</li><li>Multimedia</li><li>Camera</li></ul><p>下面我们大致介绍下前四个:</p><h4 id="Processing"><a href="#Processing" class="headerlink" title="Processing"></a>Processing</h4><blockquote><p>CPU time, GPU time, etc.</p></blockquote><p>这些指标可以来度量和理解 App 的复杂度,它们可以用来比对各个功能的算法效率,发现无效的渲染等。</p><h4 id="Location-Metrics"><a href="#Location-Metrics" class="headerlink" title="Location Metrics"></a>Location Metrics</h4><blockquote><p>Cumulative usage time, bakcground time, etc.</p></blockquote><p>可以用来了解定位的使用情况。</p><p>例如:</p><ul><li>定位多余的后台定位</li><li>过高的定位精度</li></ul><h4 id="Display-Metrics"><a href="#Display-Metrics" class="headerlink" title="Display Metrics"></a>Display Metrics</h4><blockquote><p>Average Pixel Luminance (简称 APL,平均像素亮度,即每个像素的平均亮度)</p></blockquote><p>在 X/XS 手机上,OLED 屏幕显示的 UI 的颜色直接影响到能耗。</p><ul><li>越亮的颜色 = 越高的能耗(高 APL)</li><li>越暗的颜色 = 越低的能耗(低 APL)</li></ul><h4 id="Network-Metrics"><a href="#Network-Metrics" class="headerlink" title="Network Metrics"></a>Network Metrics</h4><blockquote><p>Upload and download bytes, connectivity, etc</p></blockquote><p>尽可能优化网络使用,因为它是一项高能耗的任务。</p><p>例如:</p><ul><li>检查各个预期的上传/下载,看看能否推迟</li><li>了解弱网络环境下的能耗情况(高耗能)</li></ul><h3 id="2-Performance-Metrics"><a href="#2-Performance-Metrics" class="headerlink" title="2.Performance Metrics"></a>2.Performance Metrics</h3><p><img src="/media/15606640304676/performance.jpg" alt="performance"></p><p>性能指标包括以下几项:</p><ul><li>Hangs</li><li>Disk</li><li>Application Launch</li><li>Memory</li><li>Custom Intervals</li></ul><p>苹果着重介绍了前四个。</p><h4 id="Hang-Metrics"><a href="#Hang-Metrics" class="headerlink" title="Hang Metrics"></a>Hang Metrics</h4><p>Hang Metrics 就是我们常说的卡顿监测 ANR,它可以用来:</p><ul><li>查找哪些地方的任务可以移到后台线程</li><li>利用各种 dispatches 和 queues 来减少卡顿的概率</li></ul><h4 id="Disk-Metrics"><a href="#Disk-Metrics" class="headerlink" title="Disk Metrics"></a>Disk Metrics</h4><p>这个指标记录的是磁盘逻辑写入,它用来度量磁盘使用情况,可以用来:</p><ul><li>定位多余的磁盘写入</li><li>检查合并策略</li></ul><h4 id="Application-Launch-Metrics"><a href="#Application-Launch-Metrics" class="headerlink" title="Application Launch Metrics"></a>Application Launch Metrics</h4><p>可以用来度量 App 启动或恢复所消耗的时间。</p><ul><li>了解启动时的耗时因素,例如数据库加载对启动耗时的影响有多大</li><li>查看启动和恢复这两种路径下的不同耗时</li></ul><h4 id="Memory-Metrics"><a href="#Memory-Metrics" class="headerlink" title="Memory Metrics"></a>Memory Metrics</h4><p>内存的管理很重要,它也可以影响到应用的启动速度,以及在后台时,被终止的可能性。<br>Memory Metrics 用于查看平均内存占用,以及峰值内存占用。它可以用来检测内存的使用情况,例如:</p><ul><li>定位难以复现的内存泄漏</li><li>减少应用挂起后的平均内存占用(有助于推迟应用被终止的时间)</li></ul><hr><p><strong>了解完主要的一些指标后,让我们进入正题。</strong></p><h2 id="1、在-XCTest-中监测性能(XCTest-Metrics)"><a href="#1、在-XCTest-中监测性能(XCTest-Metrics)" class="headerlink" title="1、在 XCTest 中监测性能(XCTest Metrics)"></a>1、在 XCTest 中监测性能(XCTest Metrics)</h2><p>单元测试支持检测更多的性能数据。这是本 Session 推出的第一项新功能。</p><h3 id="使用-XCTest-监测性能指标"><a href="#使用-XCTest-监测性能指标" class="headerlink" title="使用 XCTest 监测性能指标"></a>使用 XCTest 监测性能指标</h3><p>XCTest Metrics 是开发和测试过程中用来衡量应用性能的工具。</p><p>在 Xcode 11 以前,XCTest 就支持跑性能测试,但是,我们只能通过设定一些性能指标的基准线,来进行性能方面的监测。然而,性能指标其实包含多个维度,因此今年 XCTest 增加了一些新的指标。包括:</p><ul><li>CPU</li><li>memory</li><li>storage</li><li>clock and OSSignpost</li><li>custom Metrics</li></ul><p>另外,在应用运行的过程中 Xcode 中可以查看到应用在 CPU、内存等各个子系统上的整体性能消耗。但这些信息比较粗略,如果你想深入挖掘更多性能信息,或者诊断一些复杂问题,就可以使用 Instrument。Instrument 中提供了一些性能检测的模板,用来诊断内存、响应速度、数据读写、耗电量等问题,可以更详细、更精确地展示性能数据。</p><h3 id="XCTest-示例"><a href="#XCTest-示例" class="headerlink" title="XCTest 示例"></a>XCTest 示例</h3><p>以往测量一段代码的性能表现只需要在 <code>measure</code> 代码块中编写需要检测性能的代码:</p><p><img src="/media/15606640304676/xctestbefore.jpg" alt="xctestbefore"></p><p>而现在,我们只需在调用 <code>measure</code> 时增加想要监测的指标作为参数,就可以从多个维度监测代码块的性能表现,十分简单:</p><p><img src="/media/15606640304676/15608420618102.jpg" alt></p><p>而检测应用启动耗时,更是容易。不需要任何代码,创建 XCTest 的时候就已经自动生成:</p><p><img src="/media/15606640304676/15608420813847.jpg" alt></p><p>同样地,如果我们设置基准线(<code>baseline</code>),那么每次运行测试时,Xcode 都会自动对比 我们设定的基准线,如果启动耗时高于基准线,那么测试就不通过。</p><blockquote><p>需要注意的是,在运行 XCTest 来测试 App 的性能时,不要启用 Xcode 的 debugger(去掉下图中勾选的选项),也不要开启 Xcode 的一些诊断选项,例如僵尸对象检测、内存分配记录等,以避免它们影响到应用的性能表现。可以在项目中创建一个新的 scheme 来关闭这些干扰项。</p></blockquote><p><img src="/media/15606640304676/15608468771088.jpg" alt></p><p>XCTest 新增的各种 metrics 除了在应用的 UITest 中检测性能,还可以在 Unit Test 中检测应用性能。除了官方提供的 CPU、内存、存储、时钟和 OSSignpost 之外,我们也可以自定义性能指标,利用 XCTest 的 baseline,来监控各个功能的性能是否有变差。</p><p>XCTest 也可以用于 A/B 测试,它可以低成本地对不同算法进行 A/B 测试,从而我们可以选取性能更优的算法。</p><p>此外,因为 XCTest 可以和 Xcode 以及 Xcode Server 配合得很好,因此这些性能测试还可以在日常开发和持续集成中使用,让我们随时了解 App 的性能表现。</p><h2 id="2、跟踪线上数据(Field-Metrics)"><a href="#2、跟踪线上数据(Field-Metrics)" class="headerlink" title="2、跟踪线上数据(Field Metrics)"></a>2、跟踪线上数据(Field Metrics)</h2><p>我们可以主动搜集用户的性能数据。</p><h3 id="跟踪线上数据的好处"><a href="#跟踪线上数据的好处" class="headerlink" title="跟踪线上数据的好处"></a>跟踪线上数据的好处</h3><ul><li>利用内测用户和线上用户的数据量</li><li>发现测试时遗漏的问题</li><li>追踪各个版本迭代过程中性能指标的变化</li><li>了解新功能和 A/B 测试的影响</li></ul><h3 id="MetricsKit"><a href="#MetricsKit" class="headerlink" title="MetricsKit"></a>MetricsKit</h3><p>MetricsKit 用于收集电池用量信息和各项性能指标。它能够帮助我们记录下我们指定的关键代码块执行的时候,App 的各项性能指标。线上性能数据上报的重要性无需多言,以往我们绞尽脑汁也难以做到的性能数据上报,现在通过 MatricesKit 即可完成。</p><p>MetricsKit 使用起来很简单,它会在一天结束后,将过去24小时搜集的性能数据归集在一起,然后在下一次启动 App 后,在 delegate 的回调中提供给我们。关于这个代理回调的频率,苹果的官方文档中是这么说的:</p><blockquote><p>The system then sends a report as an array of MXMetricPayload objects at most once per day. The array contains the metrics from the past 24 hours and any previously undelivered daily reports.</p></blockquote><p>也就是说,每天我们的应用最多只会收到一次回调,该次回调会把上一段 24 小时收集到的数据返回给我们。同时,如果在上一个 24 小时之前,存在老数据没有返回给我们的,也会在该次回调中一并返回。返回的数据会存储成数组的形式,每个数组的元素表示一天的数据。</p><p>这个数据返回的频率,与 MetricsKit 的底层实现有关。在之前的 iOS 版本中,需要用户安装电量分析的 profile 之后,系统才会生成这种每 24 小时一份的数据库。Power Log 底层会以高效的方式收集各项性能数据,猜测可能是硬件实现的,然后将各项数据存储于 PL/SQL 格式的数据库中,每 24 小时生成一份数据库文件。因此,MetricsKit 应该是直接解析了这份数据库文件,然后将数据返回给 App。</p><p><img src="/media/15606640304676/15608499923330.jpg" alt="Power Log 导出的原始数据"></p><p>顺便一提,苹果做了这个解析工作之后,之前美图的测试同学做的许多自动化解析 Power Log 数据的工具差不多都可以退休了,这下总算省事了很多。不过老的 iOS 版本可能还是需要用自己的工具去解析,也还是有用的。</p><h3 id="如何获取-MetricsKit-的数据"><a href="#如何获取-MetricsKit-的数据" class="headerlink" title="如何获取 MetricsKit 的数据"></a>如何获取 MetricsKit 的数据</h3><p><img src="/media/15606640304676/metric.jpg" alt="metri"></p><p>如上图所示,要获得 MetricsKit 收集的数据,首先我们要实现一个 <code>Subscriber</code> 的类,并将 <code>Subscriber</code> 实例注册到 <code>MXMetricManager</code> 单例中,以便接受数据。当 MetricsKit 收集完 24 小时的数据后,就会将数据发送给它的所有 <code>Subscriber</code>。这时我们需要实现 <code>MXMetricManager</code> 的代理方法,来接收数据。</p><p><img src="/media/15606640304676/15608429145157.jpg" alt></p><p>收到数据后,我们就可以对数据进行处理,或是将数据上报到服务端。</p><p>对于开发者而言,等待 24 小时再获取数据显然是不科学的,苹果也考虑得比较周到,我们在 Xcode Debug 菜单中就可以模拟触发一次数据上报的回调。</p><h3 id="MXSignpost-打点"><a href="#MXSignpost-打点" class="headerlink" title="MXSignpost 打点"></a>MXSignpost 打点</h3><p>MetricsKit 还有一个特别有意思的,对我们也特别重要的接口,那就是 <code>MXSignpost</code>。我们利用 <code>MXSignpost</code> 就可以针对关键代码块打点,记录性能数据。</p><p>例如,我们可以对视频合成、视频播放等关键业务场景的性能损耗进行记录,了解 App 的性能热点在哪个流程上,然后针对性地进行优化。</p><p>这个接口初看起来和去年 Instrument 中增加的 <code>os_signpost</code> 的打点功能有点相似。用法也很像,只需要在目标代码前后打上点,系统就会自动记录信息。</p><p>而追根溯源,我们可以发现,其实 <code>MXSignpost</code> 就是对 <code>os_signpost</code> 的封装,因此它才能够获取到线程级的性能数据。它们的原理和行为也是一致的。<code>os_signpost</code> 的性能非常好,因此一开始我非常开心,心想,那 <code>MXSignpost</code> 的性能想必也非常优秀。</p><p>然而,<code>MXSignpost</code> 的头文件给我泼了盆冷水。苹果在 <code>MXSignpost</code> 的头文件上特别注明,切莫直接将 <code>MXSignpost</code> 替换成 <code>os_signpost</code>,否则,如果原本 <code>os_signpost</code> 打点的量比较多的话,将会导致 App 的性能<strong>显著下降</strong>。</p><p><img src="/media/15606640304676/15608555531297.jpg" alt></p><p>此外,自定义的打点数量也受到系统的限制,不能无限制地打点。因为过多的打点,也会输出大量日志,导致磁盘占用过大。如下图所示。但没有看到苹果有说明具体的数量限制,我们只能先自己保持克制了。</p><p><img src="/media/15606640304676/15608527327680.jpg" alt></p><p>对 <code>os_signpost</code> 感兴趣的同学可以参考王乐之前的文章:<a href="https://vongloo.me/2019/02/22/WWDC-2018-Signpost/" target="_blank" rel="noopener">WWDC-2018-Signpost</a> .</p><p>同样的,执行关键代码块时记录下来的各项数据,也会在上述 24 小时的数据生成后,一并返回给我们的 <code>Subscriber</code>。返回的数据中会给出各个 <code>MXSignpost</code> 执行的时候的累计 CPU 耗时、内存占用、卡顿时间等信息。我们同样可以将这些数据上传到服务器,由后台进行统计分析。这样,我们就能从大量线上用户那里搜集的真实数据中,及早发现性能消耗的热点</p><p><img src="/media/15606640304676/mxSignpost.jpg" alt="mxSignpost"></p><h2 id="3、Xcode-Metrics-Organizer"><a href="#3、Xcode-Metrics-Organizer" class="headerlink" title="3、Xcode Metrics Organizer"></a>3、Xcode Metrics Organizer</h2><p>简单来说,这是苹果为我们自动搜集用户性能数据的工具。</p><h3 id="什么是-Xcode-Metrics-Organizer"><a href="#什么是-Xcode-Metrics-Organizer" class="headerlink" title="什么是 Xcode Metrics Organizer"></a>什么是 Xcode Metrics Organizer</h3><ul><li>开箱即用的电量和性能分析工具</li><li>App 无需做出任何代码的修改</li><li>从设备上的数据搜集到服务器上的数据归集,全流程都做到隐私保护</li></ul><p>目前,它能提供电量、启动耗时、卡顿监测、内存监测、磁盘逻辑写入的数据上报。</p><p>这应该就是苹果之前预埋在 iOS 上的 <a href="https://punmy.cn/2018/06/12/iOS%20最全面的功耗分析之——Power%20Log.html">Power Log</a> 等工具获取到的数据。因此,从现在起,开发者升级 Xcode 后就能尝试使用这个工具了。</p><h3 id="Xcode-Metrics-Organizer-如何运行的"><a href="#Xcode-Metrics-Organizer-如何运行的" class="headerlink" title="Xcode Metrics Organizer 如何运行的"></a>Xcode Metrics Organizer 如何运行的</h3><p>当用户使用 App 时,iOS 会自动记录应用的各项性能指标,而后发送到苹果的服务器上。服务器搜集用户的这些性能数据,并且自动进行分析,生成报告。当我们开发者查看 Xcode Metrics Organizer 的时候,看到的就是性能分析的结果。但是如果用户量不够多的情况下,Xcode Metrics Organizer 上有可能看不到数据。</p><p><img src="/media/15606640304676/organizer.jpg" alt="organize"></p><p>Xcode Metrics Organizer 最大的特点就是它开箱即用。开发者们无需对自己的 App 做出任何改动,即日起就能马上体验这项功能。根据我之前调研 <a href="https://punmy.cn/2018/06/12/iOS%20最全面的功耗分析之——Power%20Log.html">Power Log</a> 得到的信息,苹果在记录这些性能数据时,可以做到对 App 的性能毫无影响,并且因为是系统级的记录,准确度非常高。</p><h3 id="如何使用-Xcode-Metrics-Organizer"><a href="#如何使用-Xcode-Metrics-Organizer" class="headerlink" title="如何使用 Xcode Metrics Organizer"></a>如何使用 Xcode Metrics Organizer</h3><p>在 Xcode 的 <code>Window</code> - <code>Organizer</code> 中,可以看到一个新增的 <code>Metrics</code>,这就是 Xcode Metrics Organizer。</p><p>它可以查看最新版本的 App 的各项性能指标,同时,也可以查看历史版本的性能指标,并进行对比。</p><p><img src="/media/15606640304676/15608386396337.jpg" alt></p><p>在电池用量统计中,这份报告分别展示了 App 在前后台的耗电量占比。同时,它分门别类地显示了各种耗电的活动的占比,例如定位、网络等。这和之前 iOS 版本中就有的 <a href="https://punmy.cn/2018/06/12/iOS%20最全面的功耗分析之——Power%20Log.html">Power Log</a> 记录的数据一模一样。</p><p><img src="/media/15606640304676/15608387012575.jpg" alt></p><p>内存方面,则是提供了峰值内存和平均内存的数据。</p><p><img src="/media/15606640304676/15608388613361.jpg" alt></p><p>另外,值得一提的是,我们还可以按照 iPhone/iPad 分别查看数据,或是单独查看某个型号的数据,以分析不同设备之间的性能表现,来精细化地对某些设备进行优化。我们有时候会遇到同样的功能,在不同设备上有不同的性能表现,例如某些功能在新机型上特别卡顿,这种时候区分设备查看性能数据就能够发现此类问题。然后再利用上文提到的更加精细的性能检测工具来进行调试,就可以定位到问题。</p><h2 id="隐私策略"><a href="#隐私策略" class="headerlink" title="隐私策略"></a>隐私策略</h2><p>苹果在发布会上多次强调,数据搜集的全过程都会保护用户隐私,不会导致用户隐私泄露。这也就意味着,用户是可以选择是否与我们共享这些性能数据,因此我们收集到的数据并非是所有用户的。用于控制这项隐私权限的开关位于 iOS 的 <code>设置</code> - <code>隐私</code> - <code>分析</code> - <code>共享 iPhone 分析</code>,如果用户关闭了这个开关,那么他的性能数据就无法被我们获取到,包括 Xcode Metrics Organizer 中苹果自动搜集的数据,以及 MetricsKit 中系统返回给我们的数据。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本 session 先是介绍了 Xcode 11 既有的,在开发调试阶段用于分析电量和性能的工具,然后推出了三个新的工具:</p><ul><li>XCTest Metrics</li><li>MetricsKit</li><li>Xcode Metrics Organizer</li></ul><p><img src="/media/15606640304676/summary.jpg" alt="summary"></p><p>这些工具可以覆盖开发、内测、线上全流程的性能数据搜集,囊括了整体数据和精细化的数据。</p><p>做过 iOS 性能检测的同学都知道,得益于苹果严格的权限控制,iOS 上的性能检测十分之困难。特别是耗电量的统计,尽管对我们十分重要,但为数不多的几种方法,不仅麻烦,而且只能局限于研发阶段,无法获取线上数据。为了测量 App 的耗电量和性能消耗,不少团队甚至购买了电流计和红外线测温枪(说的就是我们团队),用最原始粗暴的手段来获取粗略的数据。</p><p>看到本次 WWDC 推出的 Metrics 其实我并不吃惊,苹果在数据上报方面做出的努力其实早就有迹可循。我们都知道,iOS 系统本身是有对电量的使用情况进行记录和分析的,所以我们才能在系统设置里看到过去一段时间里,各个 App 的前台工作时间和耗电情况。只是以往这些数据只用于 <code>设置</code> 中的耗电量展示,以及 <code>Sysdiagnose</code> 系统诊断中。不过我们可以通过一些方式导出这些数据。之前在调研 iOS 耗电量检测的时候,我曾经写过一篇关于 Power Log 的文章:<strong><a href="https://punmy.cn/2018/06/12/iOS%20最全面的功耗分析之——Power%20Log.html">iOS 最全面的功耗分析之——Power Log</a></strong> . 感兴趣的同学可以看一下。(当然,现在已经不能说 Power Log 是最全面的功耗分析工具了)。</p><p>总的来说,本次苹果推出的 Metrics 可以说填补了 iOS 平台上性能监测的空白,使得 iOS 平台上,从开发、测试,到发布,整个流程中 App 的耗电量和性能监测成为可能。并且,这些性能监测反馈给我们的各项指标,能够帮助我们在开发的过程中做出更好的决策,让我们的 App 获得更好的性能,这也是一个优秀 App 的必要条件。</p>]]></content>
<summary type="html">
App 的耗电量和性能表现是用户体验的一个重要部分,耗电量过大或是性能很差的 App 会导致糟糕的用户体验。为了改善用户体验以及延长电池寿命,在 Session 417 中,苹果推出了三项新的电量和性能监测工具,分别用于开发阶段、内测阶段、以及线上阶段。相信通过本文,你会对你的 App 接下去的耗电量和性能优化的方向,有更好的计划。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="framework" scheme="http://punmy.cn/tags/framework/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
<category term="性能优化" scheme="http://punmy.cn/tags/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>Xcode分布式编译调研distcc</title>
<link href="http://punmy.cn/2019/06/11/distcc_on_xcode.html"/>
<id>http://punmy.cn/2019/06/11/distcc_on_xcode.html</id>
<published>2019-06-11T15:59:03.000Z</published>
<updated>2019-06-11T16:03:54.959Z</updated>
<content type="html"><![CDATA[<p>这几天研究了下在 Xcode 上使用 <strong>distcc</strong> 进行分布式编译,无疾而终,在此记录下过程中遇到的一些<strong>主要问题</strong>,以备后续研究。</p><h2 id="distcc"><a href="#distcc" class="headerlink" title="distcc"></a>distcc</h2><p>distcc 是一个开源的分布式编译工具,它可以将 C 语言的编译任务分发到多台机器上进行编译,最终再发回主机进行链接,以此加快编译速度。</p><a id="more"></a><h2 id="使用-PSPDF-的方案,本地编译成功,远程编译失败"><a href="#使用-PSPDF-的方案,本地编译成功,远程编译失败" class="headerlink" title="使用 PSPDF 的方案,本地编译成功,远程编译失败"></a>使用 PSPDF 的方案,本地编译成功,远程编译失败</h2><p>最初 distcc 的分布式编译方案就是在 PSPDF 的 Blog 上看到的,之前配置 CCache 的时候也参考过他们的 Blog。原文地址在此:<a href="https://pspdfkit.com/blog/2017/crazy-fast-builds-using-distcc/" target="_blank" rel="noopener">Crazy Fast Builds Using Distcc</a></p><p>文中提到,目前 distcc 原生不支持 Xcode + clang,因为 Xcode 调用 clang 编译时,使用了许多特殊的选项,distcc 缺少了对这些选项的支持。因此,PSPDF 团队提供了一份打了补丁的 distcc。下面我快速总结下他们的方案,详细的讲解请看原文。</p><h3 id="配置步骤"><a href="#配置步骤" class="headerlink" title="配置步骤"></a>配置步骤</h3><ul><li>在配置好 CCache 的情况下(不是必要条件,只是为了方便配置)</li><li>先使用 brew 安装原生 distcc:命令行中 <code>brew install distcc</code></li><li>拉下 PSPDF 打补丁后的源代码:<a href="https://github.com/PSPDFKit-labs/distcc" target="_blank" rel="noopener">PSPDF distcc</a></li><li>编译源码:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">./autogen.sh</span><br><span class="line">./configure --without-libiberty --disable-Werror</span><br><span class="line">make</span><br></pre></td></tr></table></figure><ul><li>在 <code>/usr/local/Cellar/distcc/</code> 目录下,找到刚刚用 brew 安装的 distcc 的所有二进制文件,用刚刚编译出来的几个二进制文件替换;</li><li>在 <code>~/.distcc/</code> 目录下创建 <code>hosts</code> 文件,写入远程的编译机器的地址,如<code>172.16.0.1</code>;</li><li>让 Xcode 调用 distcc:在配置了 CCache 的工程目录下,把 <code>ccache-clang</code> 脚本中原有的命令都注释掉,换成<code>exec distcc clang "$@"</code>;(这是一个偷懒办法,使用包含 CCache 的工程就是为了利用 <code>ccache-clang</code> 脚本拦截 <code>clang</code> 的调用。此外,还可以在这行命令前,加上<code>export DISTCC_VERBOSE=0</code>来输出 <code>distcc</code> 的日志)</li><li>本地配置好后,给远端的编译服务器也按上述步骤</li></ul><h3 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h3><ul><li>在编译的机器上跑起 distcc 的后台编译线程:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">distccd --no-detach --daemon --allow 172.16.0.0/16 --allow 127.0.0.1 --log-stderr --verbose</span><br></pre></td></tr></table></figure><ul><li>在本机 <code>brew install watch</code>,然后新开一个终端窗口执行 <code>watch distccmon-text</code>,用来监控本机任务分发的情况;</li><li>Xcode 开始编译;</li></ul><h3 id="遇到的问题"><a href="#遇到的问题" class="headerlink" title="遇到的问题"></a>遇到的问题</h3><ul><li>如果只把 <code>localhost</code> 作为编译服务器,则编译没问题,输出 COMPILE_OK;</li><li>如果不在本地编译,把任务分发到远端的编译服务器,则远端全部编译失败 <code>COMPILE_ERROR exit code 1</code>,然后 distcc 回滚到本地编译;</li></ul><h2 id="使用-distcc-with-pump-mode"><a href="#使用-distcc-with-pump-mode" class="headerlink" title="使用 distcc with pump mode"></a>使用 distcc with pump mode</h2><p>看到网上有一个类似的错误,声称开启 <code>pump mode</code>后可以解决上述问题,于是尝试 <code>distcc with pump mode</code>。</p><blockquote><p><code>pump mode</code>是指不仅将编译任务分发到远端,而且还将预处理的任务分发到远端,这使得传输的数据量变得非常大,但是当远端服务器数量够多,且带宽超过100M 的情况下,它可能会极大提升编译速度。</p></blockquote><h3 id="开启方法"><a href="#开启方法" class="headerlink" title="开启方法"></a>开启方法</h3><ul><li>在之前提到的 <code>hosts</code> 文件中的所有 IP 后面都加上<code>,lzo,cpp</code>,如<code>172.16.0.1,lzo,cpp</code>;</li><li>在<code>ccache-clang</code>的那句脚本上,加上 <code>pump</code>,变成<code>exec pump distcc clang "$@"</code>;</li></ul><h3 id="遇到的问题-1"><a href="#遇到的问题-1" class="headerlink" title="遇到的问题"></a>遇到的问题</h3><ul><li>直接编译,发现报错,缺少某些<code>.so .sh .py</code>文件,尝试将本地编译生成的一些对应的文件拷贝到 <code>/usr/local/Cellar/distcc/</code> 目录下,最终不再报类似错误;</li><li>解决上述编译错误的问题后,Xcode 显示编译成功,但远端的控制台发现编译失败。检查后发现,似乎是本地分析代码依赖的时候,发生了错误,导致无法分发预处理的任务,在本地完成了预处理,而本地预处理后,分发编译任务给远端时,就又出现了最初的本地编译成功,远端而编译失败的情况。错误信息如下:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">__________Using distcc-pump from /usr/local/Cellar/distcc/3.2rc1/bin</span><br><span class="line">__________Using 1 distcc server in pump mode</span><br><span class="line">WARNING include server: Preprocessing locally. Include server not covering: Could not locate name of translation unit: ['/Users/zj-db0524/Library/Developer/Xcode/DerivedData/MTHawkeye-bgzsavpygorewzcnobhqjehindcn/Index/DataStore', '/Users/zj-db0524/Library/Developer/Xcode/DerivedData/MTHawkeye-bgzsavpygorewzcnobhqjehindcn/Build/Intermediates.noindex/Pods.build/Debug-iphonesimulator/AFNetworking.build/Objects-normal/x86_64/AFImageDownloader.dia', '/Users/zj-db0524/Documents/Code/MTHawkeye/MTHawkeyeDemo/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m']. for translation unit 'unknown translation unit' </span><br><span class="line">distcc[74951] (dcc_talk_to_include_server) Warning: include server gave up analyzing</span><br><span class="line">distcc[74951] (dcc_build_somewhere) Warning: failed to get includes from include server, preprocessing locally</span><br><span class="line">distcc[74951] ERROR: compile /Users/zj-db0524/Documents/Code/MTHawkeye/MTHawkeyeDemo/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m on 172.16.3.77,lzo,cpp failed</span><br><span class="line">distcc[74951] (dcc_build_somewhere) Warning: remote compilation of '/Users/zj-db0524/Documents/Code/MTHawkeye/MTHawkeyeDemo/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m' failed, retrying locally</span><br><span class="line">distcc[74951] Warning: failed to distribute /Users/zj-db0524/Documents/Code/MTHawkeye/MTHawkeyeDemo/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m to 172.16.3.77,lzo,cpp, running locally instead</span><br><span class="line">distcc[74951] (dcc_please_send_email_after_investigation) Warning: remote compilation of '/Users/zj-db0524/Documents/Code/MTHawkeye/MTHawkeyeDemo/Pods/AFNetworking/UIKit+AFNetworking/AFImageDownloader.m' failed, retried locally and got a different result.</span><br><span class="line">distcc[74951] (dcc_note_discrepancy) Warning: now using plain distcc, possibly due to inconsistent file system changes during build</span><br><span class="line">__________Warning: 1 pump-mode compilation(s) failed on server, but succeeded locally.</span><br><span class="line">__________Distcc-pump was demoted to plain mode.</span><br><span class="line">See the Distcc Discrepancy Symptoms section in the include_server(1) man page.</span><br><span class="line">__________Shutting down distcc-pump include server</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>分布式编译十分美好,在 Xcode 上理论上是可行的,Xcode 4 之前甚至内嵌了 distcc。但是目前 distcc 目前原生不支持 Xcode,且其社区不是很活跃,特别是 Xcode 上的应用成功的案例更是少之又少,几乎为零,提及它的内容大多是几年前发布的。以此初步判断,目前除非投入较多精力寻找变通手段,进行适配,否则较难实现分布式编译。欢迎成功实践了 distcc 或其他分布式编译方式的朋友和我</p>]]></content>
<summary type="html">
distcc 是一个开源的分布式编译工具,它可以将 C 语言的编译任务分发到多台机器上进行编译,最终再发回主机进行链接,以此加快编译速度
</summary>
<category term="工具" scheme="http://punmy.cn/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>使用cocoapods-packager打包静态库</title>
<link href="http://punmy.cn/2019/05/25/%E4%BD%BF%E7%94%A8cocoapods-packager%E6%89%93%E5%8C%85%E9%9D%99%E6%80%81%E5%BA%93.html"/>
<id>http://punmy.cn/2019/05/25/使用cocoapods-packager打包静态库.html</id>
<published>2019-05-25T07:41:03.000Z</published>
<updated>2019-05-25T07:44:05.086Z</updated>
<content type="html"><![CDATA[<h2 id="什么是-cocoapods-packager"><a href="#什么是-cocoapods-packager" class="headerlink" title="什么是 cocoapods-packager"></a>什么是 cocoapods-packager</h2><p>cocoapods-packager 是 CocoaPods 提供的一个打包工具,可以根据 podspec 的配置,方便地将源码打包为 framework。</p><a id="more"></a><h2 id="使用-Packager-打包"><a href="#使用-Packager-打包" class="headerlink" title="使用 Packager 打包"></a>使用 Packager 打包</h2><p>在原有仓库中,使用 pod package 命令,将原有的源码打包为静态库:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod package APodSpec.podspec --library</span><br></pre></td></tr></table></figure><p>其中,–library 指定 packager 打出静态库。</p><blockquote><p>PS. 如果需要排除源码中依赖的第三方库,可以在打包命令中添加 <code>--exclude-deps</code> 选项。</p></blockquote><p>如果仓库没有问题的话,会得到一个与当前 podspec 版本号对应的,名为 <code>APodSpec-x.x.x</code> 的文件夹。里面就会有已经打包好的 .a 文件和一些打包过程中的中间产物,以及一份自动生成的静态库版本的 .podspec 配置文件。</p><p>这里需要注意的是,不能直接使用它自动生成的这份 .podspec 配置文件,这份配置文件实际上是给 framework 用的,而不是给 .a 的。</p><h2 id="配置静态库的-podspec-文件"><a href="#配置静态库的-podspec-文件" class="headerlink" title="配置静态库的 podspec 文件"></a>配置静态库的 podspec 文件</h2><p>cocoapods-packager 打包静态库的功能存在 Bugs,让人感觉像是把打包 framework 的模块拿过来随便改改充数的。它有两个问题,首先,它在打包后不会将头文件拷贝出来和 .a 放到一起,.a 所在的目录只有光秃秃的一个静态库;其次,它自动生成的 .podspec 文件中配置的竟然是 <code>s.ios.vendored_framework = 'ios/APodSpec.framework'</code>,但我们生成的是 .a 静态库,根本就不存在 framework。</p><p>这些问题在 CocoaPods 中很早就被提了 <a href="https://github.com/CocoaPods/cocoapods-packager/issues/53" target="_blank" rel="noopener">issue</a>,但 CocoaPods 表示——虽然这是 Bugs,但是他们是不会修复的。因此,我们需要修正一下 podspec 文件。</p><p>最简单的方法就是把仓库中原有的 podspec 文件拷贝一份过来改一改。</p><p>首先我们需要指定静态库:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s.vendored_libraries = "Path/to/APodSpec.a"</span><br></pre></td></tr></table></figure><p>然后,<strong>去掉</strong>原有的 <code>s.public_header_files</code> 字段,并按照以下方式指定静态库的头文件:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s.source_files = "APodSpec/**/*.h"</span><br></pre></td></tr></table></figure><p>需要注意的是,这里不应该使用 <code>s.public_header_files</code>,而是用 <code>s.source_files</code> 去指定。</p><p>最后,去除一些没用的配置,比如 <code>s.resources</code> 等,就可以了。</p><h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2><p>要测试配置是否有问题的话,可以直接拿新的 podspec 文件替换掉原有的试一试,在 Demo 中 pod install 后能编译成功的话基本就没问题了。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>需要注意的是,使用 packager 打包的前提是,SDK 的 spec 足够规范。</p><p>因为 packager 在实际的打包时,并不是使用本地的工程代码进行打包,而是会去 podspec 文件中配置的 Git 仓库中拉取远端的代码。它会根据 podspec 中指定的版本号去远端仓库中拉取对应 tag 的代码到临时目录中,进行打包。</p><p>因此,如果你的 podspec 文件填写错误,或是忘记在仓库中打 tag,抑或是你的仓库没有 push 到 Spec 中,导致 CocoaPods 没办法索引到你指定的代码版本,就可能导致打包失败。</p>]]></content>
<summary type="html">
cocoapods-packager 是 CocoaPods 提供的一个打包工具,可以根据 podspec 的配置,方便地将源码打包为 framework。
</summary>
<category term="工具" scheme="http://punmy.cn/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>代码比对神器 Kaleidoscope</title>
<link href="http://punmy.cn/2019/02/28/%E6%95%88%E7%8E%87%E7%A5%9E%E5%99%A8%20Kaleidoscope.html"/>
<id>http://punmy.cn/2019/02/28/效率神器 Kaleidoscope.html</id>
<published>2019-02-28T15:31:03.000Z</published>
<updated>2019-03-05T15:38:21.945Z</updated>
<content type="html"><![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>Kaleidoscope 是一个非常强大的比对工具,可以十分方便地比对文本、图片、文件夹等内容。搭配上 SourceTree,能够大大提升 Git 的效率。<br><a id="more"></a></p><p>Kaleidoscope 可以用来:</p><ul><li>比对任意文字、图片、文件夹</li><li>Code Review 利器,可以比对 Git 中不同 commit、不同分支上的代码</li><li>快速解决 Git 合并冲突</li><li>…</li></ul><p>SourceTree 和 Kaleidoscope 配合起来可以使得 Code Review 和冲突解决变得十分高效。他们配合起来可以做到,在 SourceTree 中选择任意两个 Commit,按下快捷键就唤起 Kaleidoscope 进行比对。对于重视 Code Review 的团队来说,可以说是一个非常能够提升体验的效率工具。我想只有 Code Review 体验的提高,才能让团队里的成员更有 Code Review 的意愿,才能真正推动 Code Review 的进行。</p><p>下面我将在本文中分享一下 SourceTree + Kaleidoscope 的配置。</p><p><img src="/media/15514477375753/15514507029604.jpg" alt></p><h2 id="配置步骤"><a href="#配置步骤" class="headerlink" title="配置步骤"></a>配置步骤</h2><h3 id="1、安装好-SourceTree-和-Kaleidoscope"><a href="#1、安装好-SourceTree-和-Kaleidoscope" class="headerlink" title="1、安装好 SourceTree 和 Kaleidoscope"></a>1、安装好 SourceTree 和 <a href="https://xclient.info/s/kaleidoscope.html" target="_blank" rel="noopener">Kaleidoscope</a></h3><h3 id="2、进入-Kaleidoscope-菜单-gt-Intergration…-安装命令行工具"><a href="#2、进入-Kaleidoscope-菜单-gt-Intergration…-安装命令行工具" class="headerlink" title="2、进入 Kaleidoscope 菜单 > Intergration… 安装命令行工具"></a>2、进入 Kaleidoscope 菜单 > Intergration… 安装命令行工具</h3><p>如下图所示,遵循指示,把<code>Kaleidoscope</code>和<code>Git</code>两个 Tab 中的命令行工具都安装好。安装完成后,左侧会出现✅标志。<br><img src="/media/15514477375753/15514479975302.jpg" alt></p><h3 id="3、打开-SourceTree-gt-Preference-gt-Diff-配置-External-Diff-Merge-选项"><a href="#3、打开-SourceTree-gt-Preference-gt-Diff-配置-External-Diff-Merge-选项" class="headerlink" title="3、打开 SourceTree > Preference > Diff 配置 External Diff / Merge 选项"></a>3、打开 SourceTree > Preference > Diff 配置 External Diff / Merge 选项</h3><p>Diff 和 Merge 的工具都选择 Custom,然后填入如下配置:</p><p>Diff Command: <code>/usr/local/bin/ksdiff</code><br>Arguments: <code>--partial-changeset --relative-path "$MERGED" -- "$LOCAL" "$REMOTE"</code></p><p>Merge Command: <code>/usr/local/bin/ksdiff</code><br>Arguments: <code>--merge --output "$MERGED" --base "$BASE" -- "$LOCAL" --snapshot "$REMOTE" --snapshot</code></p><blockquote><p>使用 Custom 配置是因为 SourceTree 对 Kaleidoscope 的原生支持有 Bug</p></blockquote><p><img src="/media/15514477375753/15514483067659.jpg" alt></p><h3 id="4、配置-Custom-Actions-以便快速唤起对比工具"><a href="#4、配置-Custom-Actions-以便快速唤起对比工具" class="headerlink" title="4、配置 Custom Actions 以便快速唤起对比工具"></a>4、配置 Custom Actions 以便快速唤起对比工具</h3><p>在 Custom Actions 中增加一个配置,配上你希望唤醒对比工具的快捷键,这里我使用 ⇧+⌘+D。<br>然后在 Script 中填入:<code>git</code>,在 Parameters 中填入:<code>difftool -y -t sourcetree $SHA</code>。然后就配置完成了。</p><p><img src="/media/15514477375753/15514488614668.jpg" alt></p><h2 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h2><ul><li><strong>比对任意两个 commit 之间的改动</strong>: 按住⌘,选择两个commit,点击刚刚配置的快捷键,即可唤起 Kaleidoscope</li><li><strong>查看某个文件的改动</strong>:直接右键单击文件,选择 External Diff(也可以对照上面的方法加个快捷键)</li><li><strong>解决冲突</strong>:右键单击冲突的文件,使用外部工具解决冲突(如下图)</li></ul><p><img src="/media/15514477375753/15514499170907.jpg" alt></p>]]></content>
<summary type="html">
Kaleidoscope是一个非常强大的比对工具,可以十分方便地比对文本、图片、文件夹等内容。搭配上SourceTree,能够大大提升Git的效率。特别是在Code Review和解决冲突时,可以有效提高体验。
</summary>
<category term="工具" scheme="http://punmy.cn/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>iOS上的端序</title>
<link href="http://punmy.cn/2018/12/08/Endianness%20In%20iOS.html"/>
<id>http://punmy.cn/2018/12/08/Endianness In iOS.html</id>
<published>2018-12-08T11:31:03.000Z</published>
<updated>2018-12-08T11:53:00.790Z</updated>
<content type="html"><![CDATA[<h2 id="什么是端序"><a href="#什么是端序" class="headerlink" title="什么是端序"></a>什么是端序</h2><p>端序,也叫字节序,是指计算机上多字节数据类型的存储规则。在日常的编码中可能不太需要关心端序,但当涉及到一些比较底层的任务,例如 socket 编程时,就绕不开它。<br><a id="more"></a></p><p>端序(Endian)有两种规则,一种是大端序,一种是小端序。大端序是指将高位字节存放在低位地址,而小端序则是将低位字节存放在低位地址。<br>以<code>0x01234567</code>为例。</p><p>大端序:</p><table><thead><tr><th>低地址</th><th></th><th></th><th>高地址</th></tr></thead><tbody><tr><td>0x01</td><td>0x23</td><td>0x45</td><td>0x67</td></tr></tbody></table><p>小端序:</p><table><thead><tr><th>低地址</th><th></th><th></th><th>高地址</th></tr></thead><tbody><tr><td>0x67</td><td>0x45</td><td>0x23</td><td>0x01</td></tr></tbody></table><h2 id="iOS上的端序"><a href="#iOS上的端序" class="headerlink" title="iOS上的端序"></a>iOS上的端序</h2><p>目前为止,iOS 上采用的端序是小端序。在 NSByteOrder.h 和 OSByteOrder.h 中有许多和端序相关的定义,其中当前系统的端序可以通过 <code>NSHostByteOrder()</code> 获取到:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="built_in">NSHostByteOrder</span>() == <span class="built_in">NS_BigEndian</span>) {</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"BigEndian"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="端序的转换"><a href="#端序的转换" class="headerlink" title="端序的转换"></a>端序的转换</h2><p>不同的设备或协议可能采用不同的端序,例如,iPhone 和 x86 架构的 PC 都采用小端序,USB协议也采用小端序;而许多网络协议,如 TCP/IP 协议,以及部分 PC 则采用大端序。此外,ARM 等平台的端序则是可以配置的。因此,当涉及到 socket、信号处理等比较底层的操作时,我们需要注意不同场合下所需的端序,按需进行转换。</p><p>不同的语言中通常都有对应的 <code>ntoh*</code> 和 <code>hton*</code> 方法,来进行端序的转换。TCP/IP 协议中规定了数据采用大端序,因此大端序也常常被称作网络端序。相反地,小端则被称作主机端序。这也是我们常常见到的端序转换方法名 <code>ntoh*</code>(network-to-host)、<code>hton*</code>(host-to-network) 的由来。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 转换端口号的端序</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sockaddr_in</span> <span class="title">addr</span>;</span></span><br><span class="line">addr.sin_port = htons(port);</span><br><span class="line">addr.sin_family = AF_INET;</span><br><span class="line">addr.sin_addr.s_addr = inet_addr(kLocalhost);</span><br></pre></td></tr></table></figure><p>根据数据的字节数的不同,Darwin 为我们提供了一系列常用的宏来转换端序:<code>ntohs</code>(16bits)、<code>ntohl</code>(32bits)、<code>ntohll</code>(64bits)、<code>htons</code>(16bits)、<code>htonl</code>(32bits)、<code>htonll</code>(64bits).如果需要进行更长字节数的端序转换,则需要我们自己实现。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Int16 的端序转换宏</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> ntohs(x)__DARWIN_OSSwapInt16(x)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 判断是否常量,使用不同的转换方式</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> __DARWIN_OSSwapInt16(x) \</span></span><br><span class="line"> ((<span class="keyword">__uint16_t</span>)(__builtin_constant_p(x) ? __DARWIN_OSSwapConstInt16(x) : _OSSwapInt16(x)))</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 常量的端序转换方法,在预处理阶段完成(uint16)</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> __DARWIN_OSSwapConstInt16(x) \</span></span><br><span class="line"> ((<span class="keyword">__uint16_t</span>)((((<span class="keyword">__uint16_t</span>)(x) & <span class="number">0xff00</span>) >> <span class="number">8</span>) | \</span><br><span class="line"> (((<span class="keyword">__uint16_t</span>)(x) & <span class="number">0x00ff</span>) << <span class="number">8</span>)))</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 运行时的端序转换方法(uint16)</span></span><br><span class="line">__DARWIN_OS_INLINE</span><br><span class="line"><span class="keyword">__uint16_t</span></span><br><span class="line">_OSSwapInt16(</span><br><span class="line"> <span class="keyword">__uint16_t</span> _data</span><br><span class="line">)</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> ((<span class="keyword">__uint16_t</span>)((_data << <span class="number">8</span>) | (_data >> <span class="number">8</span>)));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查看这些宏的定义可以发现,Darwin 会先判断待转换的数据是否常量,如果是常量,则直接进行移位运算,在编译前的预处理阶段就完成转换;如果是变量,则替换为相应的内联方法,在运行时进行转换。</p>]]></content>
<summary type="html">
端序,也叫字节序,是指计算机上多字节数据类型的存储规则。在日常的编码中可能不太需要关心端序,但当涉及到一些比较底层的任务,例如 socket 编程时,就绕不开它。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="基础" scheme="http://punmy.cn/tags/%E5%9F%BA%E7%A1%80/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
</entry>
<entry>
<title>巧妙利用KVO实现精准的VC耗时检测</title>
<link href="http://punmy.cn/2018/06/18/15278496835424.html"/>
<id>http://punmy.cn/2018/06/18/15278496835424.html</id>
<published>2018-06-18T08:50:03.000Z</published>
<updated>2018-06-18T08:50:37.000Z</updated>
<content type="html"><![CDATA[<p>本文主要想分享一下我在根据这篇博客 <a href="http://satanwoo.github.io/2017/11/27/KVO-Swizzle/" target="_blank" rel="noopener">《一种基于KVO的页面加载,渲染耗时监控方法》</a> 实现 VC 耗时检测的过程中,产生并解决的疑惑,以及在该博客中发现的问题。本文最终输出了一个用于检测 VC 加载耗时的小工具:<a href="https://github.com/panmingyang2009/VCProfiler" target="_blank" rel="noopener">VCProfiler</a>。</p><a id="more"></a><blockquote><p>本文的实现基于 <a href="https://github.com/SatanWoo?page=2&tab=repositories" target="_blank" rel="noopener">@盗版五子棋</a> 的<a href="http://satanwoo.github.io/2017/11/27/KVO-Swizzle/" target="_blank" rel="noopener">博客</a>,并在其基础之上进行了一点改进。最终的源代码发布在 Github 上:<a href="https://github.com/panmingyang2009/VCProfiler" target="_blank" rel="noopener">VCProfiler</a>。</p></blockquote><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>View Controller 的加载耗时优化是 App 性能优化中的一个重要环节,也是用户对于 App 流畅度的一个直观感知。</p><p>公司内部的性能监测工具中,也一直有对 VC 的加载耗时进行检测,但是之前的方法是在 <code>load</code> 方法中,用 Method Swizzle 的方式替换 <code>UIViewController</code> 的 <code>init</code>、<code>loadView</code>、<code>ViewDidLoad</code> 等方法,并在其中记录对应的时间戳,以此来计算 VC 加载过程中各个阶段的耗时。</p><p>这样的检测方式不是很精准,因为我们 hook 的是 <code>UIViewController</code> 的方法,如果我们在自己的 VC 中重写了对应的方法,并执行了一些耗时的操作,那么这些操作的时间就没有被计算进去。因此我们需要改进现有的检测方式,以便更加精准地测量 VC 加载过程中的耗时。</p><h2 id="发现新大陆"><a href="#发现新大陆" class="headerlink" title="发现新大陆"></a>发现新大陆</h2><p>在研究新方法的过程中,我发现了<a href="https://github.com/SatanWoo?page=2&tab=repositories" target="_blank" rel="noopener">@盗版五子棋</a> 写的一篇博客<a href="http://satanwoo.github.io/2017/11/27/KVO-Swizzle/" target="_blank" rel="noopener">《一种基于KVO的页面加载,渲染耗时监控方法》</a>,这篇博客中描述了一种很巧妙的检测 VC 耗时的思路。</p><p>这个思路利用了 KVO 在实现上的一些特性,简单来说,就是在 VC 创建的时候,故意触发 KVO,让 runtime 在我们自定义的 VC 对象上,再动态地包装一层 KVO 的子类,然后像之前一样 hook 这个子类 VC 的各个关键的方法,在其中进行时间的记录。这样一来,我们不再仅仅是检测父类的耗时,而是直接能够测量出我们自己 VC 本身的方法耗时。</p><h2 id="我的疑惑"><a href="#我的疑惑" class="headerlink" title="我的疑惑"></a>我的疑惑</h2><p>具体的一些原理和实现方式 <a href="https://github.com/SatanWoo?page=2&tab=repositories" target="_blank" rel="noopener">@盗版五子棋</a> 的博客中已经写的非常详细,我在此就不再赘述。但在读那篇博客的过程中,我心里一直有个疑问:</p><p><strong>既然 hook 了父类,也就是 <code>UIViewController</code> 的 <code>init</code> 方法,那为什么不直接在父类的 <code>init</code> 方法中,获取子类 VC 的类型,然后替换子类 VC 的各个关键方法?</strong></p><p>底下的评论中也有类似的疑问,但当时没有看到作者的明确回复,因此我就分别按照两种方法实现了一下 VC 的耗时检测,在实现的过程中,突然意识到了使用 KVO 的优势。</p><h2 id="为何选择-KVO"><a href="#为何选择-KVO" class="headerlink" title="为何选择 KVO"></a>为何选择 KVO</h2><p>直接 hook 各个 VC 类的方法,在一般的情况下也是能实现相应的耗时检测的,但是在各个 VC 之间存在继承关系的时候,会导致<strong>子类和父类重复 hook</strong>,甚至一不小心就会在 Method Swizzle 的时候把 IMP 换错掉。</p><p>假设我们有两个 VC,一个是 A,一个是 B,B 继承于 A。如果直接 hook 各个 VC 的方法,那么 A 和 B 都会被 hook 一次,当我们在加载 B 的时候,A、B 两个类的耗时检测都会被触发。而对于 KVO 的方法而言,由于每次 hook 的都是系统动态生成的 KVO 类,完全不会影响到原有的 A 和 B,所以 B 继承下来的 A 依然是纯净的 A,不会受到干扰。</p><p>这是我觉得 KVO 的方法带来的最大的好处。</p><h2 id="发现的问题"><a href="#发现的问题" class="headerlink" title="发现的问题"></a>发现的问题</h2><h3 id="问题1、不需要-hook-init-方法"><a href="#问题1、不需要-hook-init-方法" class="headerlink" title="问题1、不需要 hook init 方法"></a>问题1、不需要 hook init 方法</h3><p>原文在提到 hook <code>UIViewController</code> 的 init 方法的时候,同时 hook 了三个 init 方法:</p><ul><li>init</li><li>initWithCoder:</li><li>initWithNibName:bundle:</li></ul><p>但其实 <code>-init</code> 方法是没有必要 hook 的,因为 <code>UIViewController</code> 的 <code>-init</code> 方法最终也是调用 <code>initWithNibName:bundle:</code> 来进行初始化。如果同时 hook 了 <code>-init</code> 方法,会导致一次多余的操作。</p><p>因此,我们只需要 hook 两个初始化方法就够了:</p><ul><li>initWithCoder:</li><li>initWithNibName:bundle:</li></ul><h3 id="问题2、KVORemover-对-VC-的引用要用-unsafe-unretained"><a href="#问题2、KVORemover-对-VC-的引用要用-unsafe-unretained" class="headerlink" title="问题2、KVORemover 对 VC 的引用要用 unsafe_unretained"></a>问题2、KVORemover 对 VC 的引用要用 unsafe_unretained</h3><p>另一个问题是,原文中提到,使用一个 KVO remover 在 dealloc 的时候移除 VC 的 KVO:</p><blockquote><p>然后我们构建一个移除器,这个移除器<strong>弱引用</strong>保存了vc的实例和对应的keypath……<br>……<br>而在对应的移除器的dealloc方法里,我们把kvo监听给移除就可以了。</p></blockquote><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)dealloc</span><br><span class="line">{</span><br><span class="line"><span class="meta">#ifdef DEBUG</span></span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"WZQKVORemover called"</span>);</span><br><span class="line"><span class="meta">#endif</span></span><br><span class="line"> <span class="keyword">if</span> (_obj) {</span><br><span class="line"> [_obj removeObserver:[WZQKVOObserverStub stub] forKeyPath:_keyPath];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实际实现中,使用 <code>associate object</code> 来移除 KVO 的正确性确实是有保障的,但是,如果 remover 保存的 VC 实例,也就是<code>obj</code>属性,使用了 <code>weak</code> 来修饰,那么在 remover 进入 <code>-dealloc</code> 方法的时候,上述代码中的 <code>if (_obj)</code> 判断将永远为 false。因为在 dealloc 的时候,weak 修饰的属性已经被置为 nil(但是在此处断点的话,Xcode 中仍然看得到 obj 原先的值),也就无法正常移除 KVO。</p><p>因此 remover 中引用的 VC 实例需要使用 <code>unsafe_unretained</code> 来修饰。 </p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>利用 KVO 动态创建子类的方式来 hook VC,并计算耗时,这确实是一个很有意思的想法,同时也非常的简单有效。此外,由于所有的工作都可以在 <code>UIViewController</code> 的 Category 中实现,也使得这种方式基本没有侵入性。鉴于 <a href="https://github.com/SatanWoo?page=2&tab=repositories" target="_blank" rel="noopener">@盗版五子棋</a> 文章中的 Github 链接是无效的,我已经将实现好的代码发布在了 Github 上,有需要的同学可以自取:<a href="https://github.com/panmingyang2009/VCProfiler" target="_blank" rel="noopener">VCProfiler</a>。</p>]]></content>
<summary type="html">
本文主要想分享一下我在根据这篇博客《一种基于KVO的页面加载,渲染耗时监控方法》实现 VC 耗时检测的过程中,产生并解决的疑惑,以及在该博客中发现的问题。本文最终输出了一个用于检测 VC 加载耗时的小工具:VCProfiler
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>iOS 最全面的功耗分析之——Power Log</title>
<link href="http://punmy.cn/2018/06/12/iOS%20%E6%9C%80%E5%85%A8%E9%9D%A2%E7%9A%84%E5%8A%9F%E8%80%97%E5%88%86%E6%9E%90%E4%B9%8B%E2%80%94%E2%80%94Power%20Log.html"/>
<id>http://punmy.cn/2018/06/12/iOS 最全面的功耗分析之——Power Log.html</id>
<published>2018-06-12T04:31:03.000Z</published>
<updated>2019-03-03T14:07:48.420Z</updated>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>功耗分析是移动应用开发中一个非常重要的课题,也是衡量应用性能表现的一个重要指标。但是在 iOS 设备上,由于苹果严格的限制,我们一直较难开展功耗分析的工作。</p><a id="more"></a><p>在 iOS 10 以前,我们还可以通过 IOKit 中的 IOPMPowerSource 私有接口,获取较为详细的电量信息,如电量、电压、电池温度等一系列的信息,腾讯的 GT 就是通过该接口获取电池的信息。然而,在 iOS 10 以及更高的系统中,该接口也被封印了,现在读取该接口,只能获取到很鸡肋的信息,如下图所示。</p><p><img src="/media/15284462930320/15287021385503.jpg" alt="iOS 10 及之后的系统版本只能获取到零星的数据"></p><p>因此,iOS 平台上急切地需要一个功耗分析的工具。</p><h2 id="Power-Log(Sysdiagnose)"><a href="#Power-Log(Sysdiagnose)" class="headerlink" title="Power Log(Sysdiagnose)"></a>Power Log(Sysdiagnose)</h2><p>我们都知道,iOS 系统本身是有对电量的使用情况进行记录和分析的,所以我们才能在系统设置里看到过去一段时间里,各个 App 的前台工作时间和耗电情况。在进行了一整天的调研后,我震惊地发现,iOS 的耗电记录是可以导出的,并且它记录的详细程度简直令人发指!简单地说,它包括了过去几天里,系统整体的详细功耗情况、各个 App 在各个硬件上的耗电情况(包括第三方 App),等等一系列详细的数据。</p><p>有的同学可能遇到过,在向苹果反馈 Bug 时,有时苹果的工程师会要求你附上设备的诊断日志,其中,在遇到电池续航相关的问题时,苹果的工程师会让你提供一下电池的诊断日志。在电池的诊断日志中,就包含了电池电量的使用记录。</p><blockquote><p>这些诊断工具在之前被苹果称为 Sysdiagnose,现在,它们被苹果统一归类到 Bug Reporting 的主题中,电池续航相关的诊断日志被称为“Power Log”。</p></blockquote><p>我在苹果的 Bug Reporting 中发现了一份<a href="https://download.developer.apple.com/iOS/iOS_Logs/Battery_Life_Logging_Instructions.pdf" target="_blank" rel="noopener">电池诊断日志的导出指南</a>,其中仅仅简短地介绍了导出日志的几个步骤。我按照其中的步骤成功导出了电池续航日志,发现该日志其实是一个数据库,其中储存了几十张表。至于这几十张表中存储的内容是什么,各个字段的含义是什么,以及如何分析其中的数据,苹果的指南中则只字未提。好在经过一番查找后,我发现了腾讯的一篇<a href="https://cloud.tencent.com/developer/article/1006222" target="_blank" rel="noopener">博客</a> 中有提到其中几张表的内容,有了这个开头后,我便顺腾摸瓜,大致了解了其中的结构。</p><p>下面,我简单地介绍一下,如何通过这个方法获取最全面的功耗信息,以及如何分析其中的数据。</p><h2 id="获取数据"><a href="#获取数据" class="headerlink" title="获取数据"></a>获取数据</h2><p>苹果在<a href="https://download.developer.apple.com/iOS/iOS_Logs/Battery_Life_Logging_Instructions.pdf" target="_blank" rel="noopener">电池诊断日志的导出指南</a>中详细说明了导出日志的步骤。我这里简要说明一下大概的步骤。</p><ul><li>首先在你的测试机上,安装电量分析的 <a href="https://developer.apple.com/services-account/download?path=/iOS/iOS_Logs/BatteryLife.mobileconfig" target="_blank" rel="noopener">profile</a>,安装完成后,iOS 才会记录最详细的功耗数据,并开放读取;(推荐下载后用 Airdrop 发送到手机上安装)</li><li>第二步,连接上 iTunes 并同步,这时 iTunes 就会自动把手机上的功耗的历史记录拷贝到电脑上;</li><li>第三步,断开设备,运行你的 App,这时设备已经在记录功耗信息,记得留意你运行 App 时的时间,因为稍后要和数据库中的时间戳进行匹配;</li><li>第四部,再次连接上 iTunes 并同步,这时 iTunes 就会自动把手机上的详细功耗记录拷贝到电脑上;</li></ul><h3 id="诊断日志的目录"><a href="#诊断日志的目录" class="headerlink" title="诊断日志的目录"></a>诊断日志的目录</h3><p>到 iTunes 的同步文件夹(<br><code>~/Library/Logs/CrashReporter/MobileDevice/你的手机名/</code>)下,找到以<code>Powerlog_</code>开头,后缀是<code>.PLSQL</code>或者<code>.PLSQL.gz</code>的几个文件,这些就是记录了所有功耗信息的数据库文件了,可以使用简单的数据库查看工具打开看看。</p><p><img src="/media/15284462930320/15287660449685.jpg" alt="诊断日志的目录"></p><h2 id="分析数据"><a href="#分析数据" class="headerlink" title="分析数据"></a>分析数据</h2><p>打开数据库后,我们可以看到里面有数百张表。苹果没有解释这些表的具体作用,只介绍了如何导出,因为苹果实际上只打算用这份记录来诊断问题,目前并没有直接向开发者开放。但我们可以通过各个表中的字段名,来了解各个表的大概用途。此外,腾讯的文档中介绍了其中比较重要的七张表,这节省了我们不少时间。下面列举一下关键的几张表的作用。</p><table><thead><tr><th>表名</th><th>内容</th></tr></thead><tbody><tr><td>PLBatteryAgent_EventBackward_Battery</td><td>整机的电量信息,包含电流、电压、温度等信息。(每20秒记录一条数据)</td></tr><tr><td>PLBatteryAgent_EventBackward_Battery_UI</td><td>剩余电量百分比。(每20秒记录一条数据)</td></tr><tr><td>PLIOReportAgent_EventBackward_EnergyModel</td><td>整机不同硬件上的详细功耗数据。分别记录了 CPU、GPU、DRAM 等硬件的耗电量。</td></tr><tr><td>PLAccountingOperator_Aggregate_RootNodeEnergy</td><td>各个 App 的详细耗电数据。记录各个 App 在各个硬件上的耗电量。(每小时更新一次数据)</td></tr><tr><td>PLAccountingOperator_EventNone_Nodes</td><td>各个硬件对应的 Node ID,以及各个 App 的对应的 Node ID。</td></tr><tr><td>PLAccountingOperator_EventNone_AllApps</td><td>手机中安装的所有 App 的信息</td></tr><tr><td>PLApplicationAgent_EventForward_Application</td><td>App 运行状态记录。记录各个 App 在某个时间段以什么状态运行。</td></tr><tr><td>PLAppTimeService_Aggregate_AppRunTime</td><td>App 的运行时长统计。(每小时更新一次数据。</td></tr><tr><td>PLBatteryAgent_EventForward_LightningConnectorStatus</td><td>Lighting 接口连接状态</td></tr><tr><td>PLBatteryAgent_EventNone_BatteryConfig</td><td>电池的配置信息。包括电池容量、循环计数、电池寿命、电池温度等信息。</td></tr><tr><td>PLBatteryAgent_EventNone_BatteryShutdown</td><td>电池导致的意外关机记录。</td></tr><tr><td>PLButtonAgent_EventPoint_Button</td><td>疑似物理按键的点击记录。</td></tr><tr><td>PLCameraAgent_EventForward_Camera</td><td>相机使用记录。记录了相机类型和使用相机的 App</td></tr><tr><td>PLConfigAgent_EventNone_Config</td><td>本机的一些配置信息和一些系统设置。</td></tr><tr><td>PLDisplayAgent_Aggregate_UserTouch</td><td>屏幕点击计数。每 15 分钟记录一条数据。</td></tr><tr><td>PLDisplayAgent_EventForward_Display</td><td>屏幕亮度信息。包括流明、尼特、亮度滑竿值等信息。</td></tr><tr><td>PLProcessNetworkAgent_EventPoint_Connection</td><td>网络连接记录。记录了发起网络连接的 App、地址、端口等信息。</td></tr><tr><td>PLXPCAgent_EventPoint_CacheDelete</td><td>清除缓存的记录。包括申请的空间大小、清除缓存的耗时、清除的缓存大小、服务名称、紧急程度等信息。</td></tr></tbody></table><p>我们结合几张表就可以简单地分析出一些衡量耗电量的维度,例如:</p><h3 id="1、绘制电量百分比变化曲线"><a href="#1、绘制电量百分比变化曲线" class="headerlink" title="1、绘制电量百分比变化曲线"></a>1、绘制电量百分比变化曲线</h3><p>直接读取 <code>PLBatteryAgent_EventBackward_Battery_UI</code> 表中的数据即可。</p><h3 id="2、iPhone-整体耗电量和温度变化"><a href="#2、iPhone-整体耗电量和温度变化" class="headerlink" title="2、iPhone 整体耗电量和温度变化"></a>2、iPhone 整体耗电量和温度变化</h3><p>直接读取 <code>PLBatteryAgent_EventBackward_Battery</code> 表中的电量、温度数据,数据的记录间隔是 20 秒,基本上满足了各种各样的需求。</p><h3 id="3、分析特定-App-的详细功耗数据"><a href="#3、分析特定-App-的详细功耗数据" class="headerlink" title="3、分析特定 App 的详细功耗数据"></a>3、分析特定 App 的详细功耗数据</h3><p>结合 <code>PLAccountingOperator_Aggregate_RootNodeEnergy</code> 和 <code>PLAccountingOperator_EventNone_Nodes</code> 两张表,可以得到某个 Bundle ID 对应的 App 在各个硬件上的耗电情况。</p><p><img src="/media/15284462930320/15287863692382.jpg" alt="存储了硬件节点 ID 的表"></p><p>由于 <code>PLAccountingOperator_Aggregate_RootNodeEnergy</code> 中的每条数据,是记录该时间点之前一个小时内的耗电情况,所以我们可以知道在每个小时内,这个 App 在各个硬件上的耗电情况。例如,我们可以查到美拍在过去一个小时内的耗电情况,如下图所示,从中我们可以找出耗电较多的硬件,其中,耗电最多的是 RootNodeID 为 10 的硬件,也就是屏幕。</p><p><img src="/media/15284462930320/15287872779775.jpg" alt="美拍在一个小时内各个硬件的耗电情况"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Power Log 能够为我们提供十分详细和准确的功耗数据,从细节到整体都能够兼顾到。并且根据苹果官方的说法,记录详细的功耗数据并不会导致 iPhone 性能的下降,只是会占据一定的磁盘空间。不过,也因为它的数据量太大,导致它对我们不太友好。我们需要用 SQL 去读取、处理庞杂的原始数据,才能得到理想的信息。最好的方案是为此开发一个读取和分析的前端,这样才能够提高效率。此外,由于它的数据是离线获取的,这在某些情况下可能不是很方便。</p><p>但总的来说,Power Log 是十分强大的,它记录的信息之丰富让人印象深刻,也为我们的功耗分析提供了更好更强大的手段。</p>]]></content>
<summary type="html">
功耗分析是移动应用开发中一个非常重要的课题,也是衡量应用性能表现的一个重要指标。苹果官方提供的诊断日志能够获取到极其详细的 App 功耗、电池温度、硬件状态等信息信息,以下是 Power Log 的详细介绍。
</summary>
<category term="工具" scheme="http://punmy.cn/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>条件变量(Condition Variables)</title>
<link href="http://punmy.cn/2018/06/07/%E6%9D%A1%E4%BB%B6%E5%8F%98%E9%87%8F%EF%BC%88Condition%20Variables%EF%BC%89.html"/>
<id>http://punmy.cn/2018/06/07/条件变量(Condition Variables).html</id>
<published>2018-06-06T17:31:03.000Z</published>
<updated>2018-06-06T17:33:17.000Z</updated>
<content type="html"><![CDATA[<h2 id="1、是什么"><a href="#1、是什么" class="headerlink" title="1、是什么"></a>1、是什么</h2><p>条件变量是并发编程中的一种同步机制。条件变量使得线程能够阻塞到等待某个条件发生后,再继续执行。条件变量能够实现强大并且高效的同步机制,但是要用好条件变量,也需要我们做出不少努力。</p><a id="more"></a><h2 id="2、为什么需要条件变量"><a href="#2、为什么需要条件变量" class="headerlink" title="2、为什么需要条件变量"></a>2、为什么需要条件变量</h2><p>设想一下这样子的场景:在生产者消费者模型中,我们希望在生产者制造出 100 个产品后,庆祝一下。<br>如果我们直接用 mutex 互斥锁来实现的话,那么我们需要在某个线程上不断地轮询:现在是不是做出 100 个产品了?伪代码如下:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Thread Producer</span></span><br><span class="line">produce () {</span><br><span class="line"> mutex_lock()</span><br><span class="line"> count++</span><br><span class="line"> mutex_unlock() </span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Thread A</span></span><br><span class="line">celebrateAfter100 () {</span><br><span class="line"> <span class="keyword">while</span> (<span class="number">1</span>) {</span><br><span class="line"> mutex_lock()</span><br><span class="line"> <span class="keyword">if</span> (count >= <span class="number">100</span>) {</span><br><span class="line"> mutex_unlock()</span><br><span class="line"> <span class="keyword">break</span></span><br><span class="line"> }</span><br><span class="line"> mutex_unlock()</span><br><span class="line"> sleep(<span class="number">100</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Celebrate!</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>相当于我们要进行大量无效的询问,才能知道条件已经满足,并且每次询问都是需要加锁的,这无疑是一种资源的浪费。</p><p>而条件变量则高效地解决了这个问题。使用条件变量的情况下,我们可以直接等待某个条件的发生,而不需要主动轮询。有了条件变量,上述伪代码就可以很方便地改写成:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Thread Producer</span></span><br><span class="line">produce () {</span><br><span class="line"> mutex_lock()</span><br><span class="line"> count++</span><br><span class="line"> mutex_unlock() </span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (count >= <span class="number">100</span>)</span><br><span class="line"> cond_signal(condition) <span class="comment">// 条件满足啦,通知一个等待的线程</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// Thread A</span></span><br><span class="line">celebrateAfter100 () {</span><br><span class="line"> mutex_lock()</span><br><span class="line"> <span class="keyword">while</span>(count < <span class="number">100</span>)</span><br><span class="line"> cond_wait(condition) <span class="comment">// 等到条件满足再继续执行</span></span><br><span class="line"> mutex_unlock()</span><br><span class="line"> <span class="comment">// Celebrate!</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在相当于是在条件满足的时候,由生产者通知 Thread A,而不是让 Thread A 傻傻地去不断轮询,变得高效了很多。</p><h2 id="3、如何正确使用条件变量"><a href="#3、如何正确使用条件变量" class="headerlink" title="3、如何正确使用条件变量"></a>3、如何正确使用条件变量</h2><p>来看一个简单的栗子:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><pthread.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">int</span> value = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">pthread_mutex_t</span> mutex;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">pthread_cond_t</span> condition;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">setup</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> pthread_mutex_init(&mutex, <span class="literal">NULL</span>);</span><br><span class="line"> pthread_cond_init(&condition, <span class="literal">NULL</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">destroy</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> pthread_mutex_destroy(&mutex);</span><br><span class="line"> pthread_cond_destroy(&condition);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">waitCondition</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> pthread_mutex_lock(&mutex);</span><br><span class="line"> <span class="keyword">while</span> (value == <span class="number">0</span>) {</span><br><span class="line"> pthread_cond_wait(&condition, &mutex); <span class="comment">// 开始等待,并立即解锁 mutex</span></span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&mutex);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">triggerCondition</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> pthread_mutex_lock(&mutex);</span><br><span class="line"></span><br><span class="line"> value = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> pthread_mutex_unlock(&mutex);</span><br><span class="line"> pthread_cond_broadcast(&condition); <span class="comment">// 广播</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="3-1、创建和销毁条件变量"><a href="#3-1、创建和销毁条件变量" class="headerlink" title="3.1、创建和销毁条件变量"></a>3.1、创建和销毁条件变量</h3><p>首先,条件变量在使用前必须初始化,pthread_cond_init 和 pthread_cond_destroy 方法可以用来动态创建和销毁条件变量。</p><p>同时,条件变量和互斥锁一样,也有静态创建方式,静态方式使用 <code>PTHREAD_COND_INITIALIZER</code> 常量,如下:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pthread_cond_t condition = PTHREAD_COND_INITIALIZER;</span><br></pre></td></tr></table></figure><p>此外,因为条件变量必须配合互斥锁使用,所以也要创建一个互斥锁。</p><h3 id="3-2、与互斥锁配合使用"><a href="#3-2、与互斥锁配合使用" class="headerlink" title="3.2、与互斥锁配合使用"></a>3.2、与互斥锁配合使用</h3><p>为了防止发生竞争条件,条件变量必须与互斥锁搭配使用。<strong>pthread_cond_wait 函数的调用</strong> 和 <strong>临界区</strong> 都需要受到互斥锁的保护。</p><h3 id="3-3、等待条件的发生"><a href="#3-3、等待条件的发生" class="headerlink" title="3.3、等待条件的发生"></a>3.3、等待条件的发生</h3><p>当条件不满足时,使用 <code>pthread_cond_wait</code> 或者 <code>pthread_cond_timedwait</code> 函数,来让线程进入休眠。当函数正常返回时,返回值为 0。</p><p>这两个函数的区别在于,<code>pthread_cond_timedwait</code> 函数提供了超时返回的能力,我们可以设定一个超时时间,来避免永久的等待。当到达超时时间后,条件变量仍未满足的话,函数会返回 <code>ETIMEOUT</code>。其中 <code>abstime</code> 以绝对时间的形式出现,0 表示格林尼治时间1970年1月1日0时0分0秒,这里常常有人误解为相对时间。</p><p>这两个 wait 函数的调用,都要在获取 mutex 锁后进行。</p><p>看到这里可能有的人会觉得疑惑:如果在 wait 之前锁住了 mutex,那其他线程在试图进入临界区时(上文 <code>value = 1</code> 的那行代码),不就永远获取不到 mutex 了吗?</p><p>这确实是让许多初学者觉得困惑的地方。其实函数 pthread_cond_wait 会在线程即将休眠之前,释放 mutex。因此,在线程休眠之后,其他线程就能正常锁住 mutex 了。</p><p>而后,等到其他线程触发了条件,并且 unlock 了 mutex 之后,休眠的线程在 wait 函数中会再次锁住 mutex,然后继续执行代码。</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">pthread_mutex_lock(&mutex);</span><br><span class="line"><span class="keyword">while</span> (value == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">/* 解锁 mutex,线程开始休眠,等待条件变量触发...</span></span><br><span class="line"><span class="comment"> * 等到条件变量被触发,线程被唤醒,</span></span><br><span class="line"><span class="comment"> * 在 pthread_cond_wait 返回之前,会再次锁住 mutex */</span></span><br><span class="line"> pthread_cond_wait(&condition, &mutex); </span><br><span class="line">}</span><br><span class="line">pthread_mutex_unlock(&mutex);</span><br></pre></td></tr></table></figure><h3 id="3-4、条件触发"><a href="#3-4、条件触发" class="headerlink" title="3.4、条件触发"></a>3.4、条件触发</h3><p>其他线程可以在条件满足后,通过调用 <code>pthread_cond_signal</code> 或者 <code>pthread_cond_broadcast</code> 来触发条件变量。</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> triggerCondition()</span><br><span class="line">{</span><br><span class="line"> pthread_mutex_lock(&mutex);</span><br><span class="line"></span><br><span class="line"> value = <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> pthread_mutex_unlock(&mutex);</span><br><span class="line"> pthread_cond_broadcast(&condition); <span class="comment">// 唤醒所有等待中的线程,不需要加锁</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>pthread_cond_signal</code> 函数可以唤醒一个处于等待中的线程,当有多个线程等待时,它会自动根据线程的优先级选择一个线程唤醒。但是某些特殊情况下,该函数可能会唤醒不止一个线程。</p><p><code>pthread_cond_broadcast</code> 函数则是用“广播”的方式唤醒所有等待中的线程。例如读写锁的实现中,在写入完毕后,可以用它来唤醒所有等待中的读取操作。</p><p>值得注意的是,无论是 <code>pthread_cond_signal</code> 还是 <code>pthread_cond_broadcast</code> 都不保证唤醒的正确性。也就是说,休眠中的线程有可能在被唤醒后,发现条件依旧不满足。这是由于在函数的实现中,为了追求高性能,而放弃了一定的准确性。这通常被称为“虚假唤醒”。</p><p>此外,<code>pthread_cond_signal</code> 和 <code>pthread_cond_broadcast</code> 函数都不需要在 mutex 锁中调用。</p><h2 id="4、注意"><a href="#4、注意" class="headerlink" title="4、注意"></a>4、注意</h2><p>尽管条件变量的使用是较为简单的,但是其中也有不少的“坑”需要大家注意。下面介绍几个比较值得注意的问题。</p><h3 id="4-1、要考虑解锁和唤醒的顺序"><a href="#4-1、要考虑解锁和唤醒的顺序" class="headerlink" title="4.1、要考虑解锁和唤醒的顺序"></a>4.1、要考虑解锁和唤醒的顺序</h3><p>由于 <code>pthread_cond_signal</code> 和 <code>pthread_cond_broadcast</code> 函数的调用都不需要加锁,所以它们放到 <code>pthread_mutex_unlock</code> 之前或者之后执行都是可以的。但在实际使用中,需要根据具体情况考虑它们的顺序,来使得程序高效运行。</p><p>当 signal 操作发生在 unlock 之前时,其他等待的线程被唤醒,但 mutex 锁可能仍然被 signal 的线程持有着,导致被唤醒的线程无法获取到 mutex 锁,从而再次进入休眠。通常情况下,这种调用顺序就会对代码的执行效率产生不良的影响。但是在 Java 下,必须采用这种顺序进行调用,否则会发生异常。</p><h3 id="4-2、要使用-while-而不是-if,避免虚假唤醒"><a href="#4-2、要使用-while-而不是-if,避免虚假唤醒" class="headerlink" title="4.2、要使用 while 而不是 if,避免虚假唤醒"></a>4.2、要使用 while 而不是 if,避免虚假唤醒</h3><p>细心观察可以发现,我们在等待的线程中,使用的是 <code>while (条件不成立)</code> 的方式来调用 wait 函数,而不是使用 <code>if</code> 语句。</p><p>这是由于 wait 函数被唤醒时,存在虚假唤醒等情况,导致唤醒后发现,条件依旧不成立。因此需要使用 <code>while</code> 语句来循环地进行等待,直到条件成立为止。</p><h3 id="4-3、timewait-是-absolute-time"><a href="#4-3、timewait-是-absolute-time" class="headerlink" title="4.3、timewait 是 absolute time"></a>4.3、timewait 是 absolute time</h3><p><code>pthread_cond_timedwait</code> 函数的 <code>abstime</code> 指的是超时的绝对时间,而不是相对现在的时间间隔。这点经常会有人误会。</p><h3 id="4-4、pthread-cond-timedwait-不一定会准时返回"><a href="#4-4、pthread-cond-timedwait-不一定会准时返回" class="headerlink" title="4.4、pthread_cond_timedwait 不一定会准时返回"></a>4.4、pthread_cond_timedwait 不一定会准时返回</h3><p>如果 <code>pthread_cond_timedwait</code> 超时到了,但是这个时候 mutex 锁被其他线程持有,导致本线程不能锁定 mutex,无法进入临界区,那么 <code>pthread_cond_timedwait</code> 就无法立即返回。</p><h2 id="5、NSCondition"><a href="#5、NSCondition" class="headerlink" title="5、NSCondition"></a>5、NSCondition</h2><p>NSCondition 是 Objective-C 中对条件变量的封装,它的底层也是基于上文所述的 POSIX 的条件变量。用法也和上文的结构相似。<br>它的独特之处在于,它同时封装了一个互斥锁和一个条件变量,所有的加锁和条件的操作都可以直接通过 <code>NSCondition</code> 对象完成。<br>官方示例如下:</p><p>等待条件:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">[cocoaCondition lock];</span><br><span class="line"><span class="keyword">while</span> (timeToDoWork <= <span class="number">0</span>)</span><br><span class="line"> [cocoaCondition wait];</span><br><span class="line"></span><br><span class="line">timeToDoWork--;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Do real work here.</span></span><br><span class="line"></span><br><span class="line">[cocoaCondition unlock];</span><br></pre></td></tr></table></figure><p>发送信号:</p><figure class="highlight objc"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[cocoaCondition lock];</span><br><span class="line">timeToDoWork++;</span><br><span class="line">[cocoaCondition signal];</span><br><span class="line">[cocoaCondition unlock];</span><br></pre></td></tr></table></figure><h2 id="6、参考文献"><a href="#6、参考文献" class="headerlink" title="6、参考文献"></a>6、参考文献</h2><ul><li><a href="https://linux.die.net/man/3/pthread_cond_wait" target="_blank" rel="noopener">https://linux.die.net/man/3/pthread_cond_wait</a></li><li><a href="https://linux.die.net/man/3/pthread_cond_signal" target="_blank" rel="noopener">https://linux.die.net/man/3/pthread_cond_signal</a></li><li><a href="https://www.ibm.com/support/knowledgecenter/en/ssw_aix_71/com.ibm.aix.genprogc/condition_variables.htm" target="_blank" rel="noopener">https://www.ibm.com/support/knowledgecenter/en/ssw_aix_71/com.ibm.aix.genprogc/condition_variables.htm</a></li><li><a href="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html" target="_blank" rel="noopener">https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html</a></li><li><a href="https://bestswifter.com/ios-lock/" target="_blank" rel="noopener">https://bestswifter.com/ios-lock/</a></li><li><a href="https://blog.zorro.im/posts/ios-muti-threading-synchronization.html" target="_blank" rel="noopener">https://blog.zorro.im/posts/ios-muti-threading-synchronization.html</a></li><li><a href="https://stackoverflow.com/questions/16522858/understanding-of-pthread-cond-wait-and-pthread-cond-signal" target="_blank" rel="noopener">https://stackoverflow.com/questions/16522858/understanding-of-pthread-cond-wait-and-pthread-cond-signal</a></li><li><a href="https://blog.csdn.net/gettogetto/article/details/53872929" target="_blank" rel="noopener">https://blog.csdn.net/gettogetto/article/details/53872929</a></li></ul>]]></content>
<summary type="html">
条件变量是并发编程中的一种同步机制。条件变量使得线程能够阻塞到等待某个条件发生后,再继续执行。条件变量能够实现强大并且高效的同步机制,但是要用好条件变量,也需要我们做出不少努力。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="基础" scheme="http://punmy.cn/tags/%E5%9F%BA%E7%A1%80/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="并发编程" scheme="http://punmy.cn/tags/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/"/>
</entry>
<entry>
<title>HTTP/2 初探</title>
<link href="http://punmy.cn/2017/08/31/15041894895980.html"/>
<id>http://punmy.cn/2017/08/31/15041894895980.html</id>
<published>2017-08-31T14:24:00.000Z</published>
<updated>2017-10-25T16:31:01.000Z</updated>
<content type="html"><![CDATA[<p>HTTP/2 是一个比它的几个前任更快、更简单、更稳定的 HTTP 协议。在 HTTP/2 中,我们可以摒弃掉很多以往针对 HTTP/1.1 想出来的“歪招儿”,因为它们的解决方案都内置在了传输层中。不仅如此,它还为我们进一步优化应用和提升性能提供全新的机会!</p><a id="more"></a><h2 id="从-SPDY-到-HTTP-2"><a href="#从-SPDY-到-HTTP-2" class="headerlink" title="从 SPDY 到 HTTP/2"></a>从 SPDY 到 HTTP/2</h2><p>SPDY 是 Google 在 2009 年发布的一个实验性协议,它是一个应用层的网络传输协议,也是 HTTP/2 的前身。SPDY 并不是为替代 HTTP 而生,它的目的是通过多路复用、请求优化和 HTTP 头部压缩等功能,来最小化 HTTP/1.1 的各种性能限制所导致的延迟。具体来说,这个项目设定的目标如下:</p><ul><li>页面加载时间 (PLT) 减少 50%。</li><li>无需网站作者修改任何内容。</li><li>将部署复杂性降至最低,无需变更网络基础设施。</li><li>与开源社区合作开发这个新协议。</li><li>收集真实性能数据,验证这个实验性协议是否有效。</li></ul><p><img src="/media/15041894895980/15066130155524.jpg" alt></p><p>首次发布后不久,Google 分享了 SPDY 协议的实现结果、文档和源代码,宣布在实验室条件下取得了 55% 的速度提升。</p><p>几年后的 2012 年,这个新的实验性协议得到了 Chrome、Firefox 和 Opera 的支持,越来越多的网站开始在部署 SPDY。事实上,在行业中被越来越多地采用之后,SPDY 已经具备了成为一个标准的条件。</p><p>观察到这一趋势后,HTTP 工作组将这一工作提上议事日程,吸取 SPDY 的经验教训,并在此基础上制定了官方“HTTP/2”标准。在拟定草案、向社会征集 HTTP/2 建议并经过内部讨论之后,HTTP 工作组决定将 SPDY 规范作为新 HTTP/2 协议的基础。</p><p>在接下来几年中,SPDY 和 HTTP/2 继续共同演化,SPDY 作为实验性分支,为 HTTP/2 标准测试新功能和建议,对要包含到 HTTP/2 标准中的每条建议进行测试和评估。最终,这个过程持续了三年,期间产生了十余个中间草案。</p><p>最终,在2015 年初,IETF 审阅了新的 HTTP/2 标准并批准发布。SPDY 与 HTTP/2 的共同演化使得它在诞生之日就已经是一个经过大量实践的标准,发布后不久,就得到了广泛应用。</p><h2 id="为何选择-HTTP-2"><a href="#为何选择-HTTP-2" class="headerlink" title="为何选择 HTTP/2"></a>为何选择 HTTP/2</h2><h3 id="HTTP-1-x-的问题"><a href="#HTTP-1-x-的问题" class="headerlink" title="HTTP/1.x 的问题"></a>HTTP/1.x 的问题</h3><p>想了解 HTTP/2 的优势,就得从 HTTP/1.x 存在的问题说起。</p><p>HTTP/1.x 中存在许多问题使得它越来越难以满足快速发展的互联网。</p><p>其中,HTTP/1.x 最大的问题就是一个连接同时只能处理一个请求。这意味着同一个连接下发起的多个请求只能逐个请求和接收。同时,由于没有优先级管理,HTTP/1.x 同一个连接中的多个请求只能采取 FIFO 的方式,依次完成。如果连接被某个耗时长的请求占用,那么其它所有请求就只能排队等候,直到收到回复或者超时。这就是所谓的队头阻塞(Head-of-Line Blocking,HOLB)。</p><p><img src="/media/15041894895980/6c2e2d17e0d84ebdbc41ee6d6ac72ee1.jpg.png" alt="HTTP/1.1 without pipelining"></p><p>这个问题的一个解决方案就是 HTTP pipelining。通过 HTTP pipelining,在同一个连接上,可以不等队伍前方的请求收到响应,就先行发送后续的请求。这样一来请求就不会有延迟,使得服务器可以提早开始处理请求,一些耗时长的,如涉及数据库查询的请求,就可以提早进行,理论上可以提高 HTTP 的效率。</p><p><img src="/media/15041894895980/292cd91a29f14288afa5f8638fd4d7e0.jpg.png" alt="HTTP/1.1 with pipelining"></p><p>然而,响应还是需要逐个排队接收,所以它并不是真正的多路复用,但至少是个不错的提升了(如果它按照理论运行的话)。在 pipelining 开启后,HOLB 依旧存在,因为 response 仍然是逐个接收的。甚至和之前比起来,使用 pipelining 后,HOLB 更明显了。此外,由于 pipelining 实现起来复杂度比较高,在实践中还常常出现各种错误,所以它并没有被广泛推广。事实上,大多数主流的浏览器都禁用了 pipelining。</p><p>另一个解决方案就是对同一个 host 同时开启多个 HTTP 连接,这样就可以并行请求和接收,能够更快地获取到多个资源。但这仍然存在一些问题,例如建立连接产生的资源损耗等。并且同个域名存在最大连接数,所以为了提高连接数,一些网站只好将网页上的资源部署到不同的域名下。除此之外,由于 TCP 的拥塞控制使得 TCP 连接在建立后有一个缓启动的过程,所以多连接的方案实际上贡献的性能提升并没有想象中那么多。</p><p>此外,HTTP/1.x 还存在文本协议开销大、缺乏首部压缩等问题。综合这些来看,HTTP/1.x 只意味着更高的系统需求,和更低的性能表现。</p><h3 id="HTTP-2-的改进"><a href="#HTTP-2-的改进" class="headerlink" title="HTTP/2 的改进"></a>HTTP/2 的改进</h3><h4 id="二进制分帧层"><a href="#二进制分帧层" class="headerlink" title="二进制分帧层"></a>二进制分帧层</h4><p>HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。二进制分帧层指的是位于传输层与应用层的高级 HTTP API 之间一个新编码机制。HTTP 的语义(包括各种动词、方法、标头)都不受影响,不同的是传输期间对它们的编码方式变了。HTTP/1.x 协议以换行符作为纯文本的分隔符,而 HTTP/2 将所有传输的信息分割为更小的消息和帧,并采用二进制格式对它们编码。其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。</p><p><img src="/media/15041894895980/15066757724992.jpg" alt="二进制分帧层"></p><p>简言之,HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧对应着特定数据流中的消息。所有这些都在一个 TCP 连接内复用。这是 HTTP/2 协议所有其他功能和性能优化的基础。</p><blockquote><p>由于编码方式的不同,HTTP/2 无法向下兼容,这也是 HTTP/2 的版本号是 2 而不是 1.2 的主要原因。</p></blockquote><h4 id="连接复用"><a href="#连接复用" class="headerlink" title="连接复用"></a>连接复用</h4><p>在 HTTP/1.x 中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP 连接。但 HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。这是 HTTP 2 最重要的一项增强。</p><p><img src="/media/15041894895980/14740804026266.jpg" alt="14740804026266"></p><p>HTTP/2 中的新二进制分帧层解决了 HTTP/1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。最终使得应用速度更快、开发更简单、部署成本更低。</p><blockquote><p>值得注意的是,HTTP/2 解决了 HTTP 的 HOL 阻塞,但并没有解决 TCP 上的 HOL 阻塞。</p></blockquote><h4 id="数据流优先级"><a href="#数据流优先级" class="headerlink" title="数据流优先级"></a>数据流优先级</h4><p>将 HTTP 消息分解为很多独立的帧之后,客户端和服务器交错传输这些帧的顺序,就成为关键的性能决定因素。为了做到这一点,HTTP/2 允许每个数据流都有一个优先级和依赖关系。数据流依赖关系和权重的组合使得服务器可以使用此信息控制 CPU、内存和其他资源的分配,确保将高优先级响应以最优方式传输至客户端。</p><blockquote><p>数据流依赖关系和权重表示传输优先级,而不是要求,因此不能保证特定的处理或传输顺序。即,客户端无法强制服务器通过数据流优先级以特定顺序处理数据流。</p></blockquote><h4 id="单一连接"><a href="#单一连接" class="headerlink" title="单一连接"></a>单一连接</h4><p>有了新的分帧机制后,HTTP/2 不再依赖多个 TCP 连接去并行复用数据流;每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别设定优先级。因此,所有 HTTP/2 连接都是持久的,而且每个来源仅需要一个连接,随之带来诸多性能优势。</p><p>大多数 HTTP 传输都是短暂且急促的,而 TCP 则针对长时间的批量数据传输进行了优化。 通过重用相同的连接,HTTP/2 既可以更有效地利用每个 TCP 连接,也可以显著降低整体协议开销。不仅如此,使用更少的连接还可以减少占用的内存和处理空间。这降低了整体运行成本并提高了网络利用率和容量。 </p><h4 id="标头压缩"><a href="#标头压缩" class="headerlink" title="标头压缩"></a>标头压缩</h4><p>每个 HTTP 传输都承载一组标头,这些标头说明了传输的资源及其属性。 在 HTTP/1.x 中,标头中的数据始终以纯文本形式发送,通常会给每个传输增加 500–800 字节的开销。如果使用 HTTP Cookie,增加的开销有时会达到上千字节。为了减少此开销和提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应中的标头数据,这种格式采用两种简单但是强大的技术:</p><ul><li>这种格式支持通过静态 Huffman 编码对传输的标头字段进行编码,从而减小了各个传输的大小。</li><li>这种格式要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表,此列表随后会用作参考,对之前传输的值进行有效编码。</li></ul><p>作为一种进一步优化方式,HPACK 压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段的列表;动态表最初为空,将根据在特定连接内改变的值进行更新。</p><p><img src="/media/15041894895980/15075304165259.jpg" alt="HPACK 标头压缩"></p><h4 id="流控制"><a href="#流控制" class="headerlink" title="流控制"></a>流控制</h4><p>流控制是一种阻止发送方向接收方发送大量数据的机制。这很容易让人联想到 TCP 流控制,它们所要解决的问题很相似。不过,由于 HTTP/2 数据流在一个 TCP 连接内复用,TCP 流控制既不够精细,也无法提供必要的应用级 API 来调节各个数据流的传输。为了解决这一问题,HTTP/2 提供了一组简单的构建块,允许客户端和服务器实现其自己的流控制。</p><p>例如,HTTP2 的流控制允许浏览器仅提取一部分特定资源,通过将流控制窗口减小为零来暂停提取,稍后再行恢复。换句话说,它允许浏览器提取图像预览,进行显示并允许其他高优先级数据流继续传输,然后在更关键的资源完成加载后恢复提取。</p><h4 id="服务端推送"><a href="#服务端推送" class="headerlink" title="服务端推送"></a>服务端推送</h4><p>HTTP/2 新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无需客户端明确地请求。之所以要提供这个服务,是因为一个文档被请求回来时,往往还需要再次请求很多文档内的其他资源,如果这些资源的请求不用客户端发起,而是服务端提前预判发给客户端,那么就会减少大量时延。</p><p>HTTP2 协议也没有规定服务器端到底该怎样推送这个资源。服务端可以自己制定不同的策略,可以是根据客户端明确写出的推送请求;或者是服务端通过学习得来;再或者是通过额外的HTTP首部想服务端表明意向。</p><p>这个服务的特点是:</p><ul><li>只有建立连接后,服务器才可以推送资源(发送 PUSH_PROMISE 帧),也就是说服务器不能无缘无故的主动向客户端推送资源。</li><li>客户端可以发送 RST_STREAM 拒绝服务器推送来的资源。</li><li>推送的资源可以由不同页面共享</li><li>服务器可以按照优先级来推送资源</li></ul><h2 id="iOS-对-HTTP-2-的支持"><a href="#iOS-对-HTTP-2-的支持" class="headerlink" title="iOS 对 HTTP/2 的支持"></a>iOS 对 HTTP/2 的支持</h2><p>在 iOS 上,NSURLSession 提供了对 HTTP/2 的支持。只要服务器支持 HTTP/2,系统就会自动使用它,否则将自动选择 HTTP/1.1 或其它可用协议。</p><blockquote><p>需要注意的是,iOS 只支持加密连接的 HTTP/2 协议,HTTP/2 服务器需要支持 ALPN 或者 NPN 加密连接。</p></blockquote><hr><p><strong>移动客户端网络部分的不少初步优化还比较依赖于 HTTP/2 的推进。MTHawkeye 中已经加入了对 HTTP/2 的检测,大家平时在开发的过程中,可以关注下,是否有可能将现存的 HTTP/1.x 升级到 HTTP/2。</strong></p><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><ul><li><a href="https://hpbn.co/http2/" target="_blank" rel="noopener">HTTP/2 简介</a></li><li><a href="https://hpbn.co/brief-history-of-http/" target="_blank" rel="noopener">Brief History of HTTP</a></li><li><a href="https://blog.chromium.org/2009/11/2x-faster-web.html" target="_blank" rel="noopener">Google 公布 HTTP/2 的博客</a></li><li><a href="https://developer.apple.com/videos/play/wwdc2015/711/" target="_blank" rel="noopener">WWDC 711</a></li><li><a href="https://brianbondy.com/blog/119/what-you-should-know-about-http-pipelining" target="_blank" rel="noopener">HTTP pipelining</a></li><li><a href="https://ye11ow.gitbooks.io/http2-explained/content/part5.html" target="_blank" rel="noopener">ALPN 和 NPN 简介</a></li><li><a href="https://docs.google.com/document/d/1RNHkx_VvKWyWg6Lr8SZ-saqsQx7rFV-ev2jRFUoVD34/edit" target="_blank" rel="noopener">QUIC 文档</a></li></ul>]]></content>
<summary type="html">
HTTP/2 是一个比它的几个前任更快、更简单、更稳定的 HTTP 协议。使用HTTP/2加速你的应用。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="HTTP2" scheme="http://punmy.cn/tags/HTTP2/"/>
<category term="基础" scheme="http://punmy.cn/tags/%E5%9F%BA%E7%A1%80/"/>
<category term="网络" scheme="http://punmy.cn/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
</entry>
<entry>
<title>五分钟重拾正则表达式</title>
<link href="http://punmy.cn/2017/08/04/15018607710939.html"/>
<id>http://punmy.cn/2017/08/04/15018607710939.html</id>
<published>2017-08-04T15:01:00.000Z</published>
<updated>2017-10-25T16:11:37.000Z</updated>
<content type="html"><![CDATA[<h2 id="什么是正则表达式"><a href="#什么是正则表达式" class="headerlink" title="什么是正则表达式"></a>什么是正则表达式</h2><p>正则表达式(Regular Expression,常简写为Regex)是一种表示文本规则的代码。在编写处理字符串的程序时,经常会有查找、替换符合某些规则的字符串的需要,正则表达式就是用于描述这些规则的工具。</p><a id="more"></a><p>大多数人都在电脑上使用过用于文件查找的通配符,例如用“*.png”来查找所有的PNG格式的文件。正则表达式和通配符类似,也是用来进行文本匹配的工具。只是比起通配符,它能进行更精确的匹配,同时,也更为复杂。</p><p>正则表达式事实上是一种轻量级、简洁的编程语言,几乎所有的高级编程语言都支持正则表达式(语法不一定完全相同)。此外,大部分的代码编辑器,如 Sublime、VS Code 也都支持正则表达式的查找替换。因此,在学习正则表达式的时候,可以在 Sublime 之类的编辑器中进行尝试。</p><blockquote><p>注:文件通配符与正则表达式无关。</p></blockquote><h2 id="基础语法"><a href="#基础语法" class="headerlink" title="基础语法"></a>基础语法</h2><h3 id="字符"><a href="#字符" class="headerlink" title="字符"></a>字符</h3><p>正则表达式的语法中有普通字符和一些被称为“元字符”的特殊字符。</p><p>包括所有字母和数字字符在内的大部分字符,都是普通字符。普通字符只能匹配它们本身,如正则表达式:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line">只能匹配 ios 这个字符串(区分大小写)。</span><br><span class="line"></span><br><span class="line">下表是元字符及其行为的一个完整列表(转自维基百科。不是唯一的,不同的解析引擎可能略有不同)。</span><br><span class="line"></span><br><span class="line">| 字符 | 描述 |</span><br><span class="line">| --- | --- |</span><br><span class="line">| \ | 将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape)、或一个向后引用(backreferences)、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\\”匹配“\”而“\(”则匹配“(”。 |</span><br><span class="line">| \^ | 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,\^也匹配“\n”或“\r”之后的位置。 |</span><br><span class="line">| $ | 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。 |</span><br><span class="line">| * | 匹配前面的子表达式零次或多次。例如,zo*能匹配“z”、“zo”以及“zoo”。*等价于{0,}。 |</span><br><span class="line">| + | 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。 |</span><br><span class="line">| ? | 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“do”或“does”中的“do”。?等价于{0,1}。 |</span><br><span class="line">| {n} | n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。 |</span><br><span class="line">| {n,} | n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。 |</span><br><span class="line">| {n,m} | m和n均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。 |</span><br><span class="line">| ? | 非贪心量化(Non-greedy quantifiers):当该字符紧跟在任何一个其他重复修饰符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”。 |</span><br><span class="line">| . | 匹配除“\n”之外的任何单个字符。要匹配包括“\n”在内的任何字符,请使用像“(.|\n)”的模式。 |</span><br><span class="line">| (pattern) | 匹配pattern并获取这一匹配的子字符串。该子字符串用于向后引用。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“\(”或“\)”。 |</span><br><span class="line">| (?:pattern) | 匹配pattern但不获取匹配的子字符串(shy groups),也就是说这是一个非获取匹配,不存储匹配的子字符串用于向后引用。这在使用或字符“(|)”来组合一个模式的各个部分是很有用。例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。 |</span><br><span class="line">| (?=pattern) | 正向肯定预查(look ahead positive assert),在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 |</span><br><span class="line">| (?!pattern) | 正向否定预查(negative assert),在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始 |</span><br><span class="line">| (?<=pattern) | 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。 |</span><br><span class="line">| (?<!pattern) | 反向否定预查,与正向否定预查类似,只是方向相反。例如“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。 |</span><br><span class="line">| x|y | 匹配x或y。例如,“z|food”能匹配“z”或“food”。“(?:z|f)ood”则匹配“zood”或“food”。 |</span><br><span class="line">| [xyz] | 字符集合(character class)。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。特殊字符仅有反斜线\保持特殊含义,用于转义字符。其它特殊字符如星号、加号、各种括号等均作为普通字符。脱字符\^如果出现在首位则表示负值字符集合;如果出现在字符串中间就仅作为普通字符。连字符 - 如果出现在字符串中间表示字符范围描述;如果如果出现在首位则仅作为普通字符。 |</span><br><span class="line">| [\^xyz] | 排除型字符集合(negated character classes)。匹配未列出的任意字符。例如,“[\^abc]”可以匹配“plain”中的“plin”。 |</span><br><span class="line">| [a-z] | 字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。 |</span><br><span class="line">| [\^a-z] | 排除型的字符范围。匹配任何不在指定范围内的任意字符。例如,“[\^a-z]”可以匹配任何不在“a”到“z”范围内的任意字符。 |</span><br><span class="line">| [:name:] | 增加命名字符类(named character class)[注 1]中的字符到表达式。只能用于方括号表达式。 |</span><br><span class="line">| [=elt=] | 增加当前locale下排序(collate)等价于字符“elt”的元素。例如,[=a=]可能会增加ä、á、à、ă、ắ、ằ、ẵ、ẳ、â、ấ、ầ、ẫ、ẩ、ǎ、å、ǻ、ä、ǟ、ã、ȧ、ǡ、ą、ā、ả、ȁ、ȃ、ạ、ặ、ậ、ḁ、ⱥ、ᶏ、ɐ、ɑ。只能用于方括号表达式。 |</span><br><span class="line">| [.elt.] | 增加排序元素(collation element)elt到表达式中。这是因为某些排序元素由多个字符组成。例如,29个字母表的西班牙语,"CH"作为单个字母排在字母C之后,因此会产生如此排序“cinco, credo, chispa”。只能用于方括号表达式。 |</span><br><span class="line">| \b | 匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。 |</span><br><span class="line">| \B | 匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。 |</span><br><span class="line">| \cx | 匹配由x指明的控制字符。例如,\cM匹配一个Control-M或回车符。x的值必须为A-Z或a-z之一。否则,将c视为一个原义的“c”字符。 |</span><br><span class="line">| \d | 匹配一个数字字符。等价于[0-9]。 |</span><br><span class="line">| \D | 匹配一个非数字字符。等价于[\^0-9]。 |</span><br><span class="line">| \f | 匹配一个换页符。等价于\x0c和\cL。 |</span><br><span class="line">| \n | 匹配一个换行符。等价于\x0a和\cJ。 |</span><br><span class="line">| \r | 匹配一个回车符。等价于\x0d和\cM。 |</span><br><span class="line">| \s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。 |</span><br><span class="line">| \S | 匹配任何非空白字符。等价于[\^ \f\n\r\t\v]。 |</span><br><span class="line">| \t | 匹配一个制表符。等价于\x09和\cI。 |</span><br><span class="line">| \v | 匹配一个垂直制表符。等价于\x0b和\cK。 |</span><br><span class="line">| \w | 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。 |</span><br><span class="line">| \W | 匹配任何非单词字符。等价于“[\^A-Za-z0-9_]”。 |</span><br><span class="line">| \ck | 匹配控制转义字符。k代表一个字符。等价于“Ctrl-k”。用于ECMA语法。 |</span><br><span class="line">| \xnn | 十六进制转义字符序列。匹配两个十六进制数字nn表示的字符。例如,“\x41”匹配“A”。“\x041”则等价于“\x04&1”。正则表达式中可以使用ASCII编码。. |</span><br><span class="line">| \num | 向后引用(back-reference)一个子字符串(substring),该子字符串与正则表达式的第num个用括号围起来的捕捉群(capture group)子表达式(subexpression)匹配。其中num是从1开始的十进制正整数,其上限可能是9[注 2]、31、[注 3]99甚至无限。[注 4]例如:“(.)\1”匹配两个连续的相同字符。 |</span><br><span class="line">| \n | 标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。 |</span><br><span class="line">| \nm | 3位八进制数字,标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,若n和m均为八进制数字(0-7),则\nm将匹配八进制转义值nm。 |</span><br><span class="line">| \nml | 如果n为八进制数字(0-3),且m和l均为八进制数字(0-7),则匹配八进制转义值nml。 |</span><br><span class="line">| \un | Unicode转义字符序列。其中n是一个用四个十六进制数字表示的Unicode字符。例如,\u00A9匹配版权符号(©)。 |</span><br><span class="line"></span><br><span class="line">### 转义</span><br><span class="line">当需要匹配元字符本身的时候,为元字符加上“ \ ”转义即可。</span><br><span class="line"></span><br><span class="line">### 匹配字符集合</span><br><span class="line">方括号“ [ ] ”用于表示要匹配的字符所属的字符集合。可以将所有可能匹配到的字符枚举出来,如:</span><br><span class="line">```[ios789]</span><br></pre></td></tr></table></figure></p><p>可以匹配到 i 或者 o 或者 s 或者 7 或者 8 或者 9 。</p><p>也可以根据 ASCII码的顺序,将某个范围内的字符都包括在内,如上述的正则表达式等价于:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">同时也能够在其中使用元字符,如:</span><br><span class="line">```[\w,\n]</span><br></pre></td></tr></table></figure></p><p>可以匹配一个单词字符,或者一个逗号,或者一个换行符。</p><p>上述三个例子中,一次都只能匹配一个字符,如果要匹配多个字符,可以后接数量的修饰,如:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">表示匹配两个或者三个字母;</span><br><span class="line">```[a-zA-Z]+</span><br></pre></td></tr></table></figure></p><p>表示匹配至少一个字母;</p><p>此外,也可以用<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">```[^0-9]</span><br></pre></td></tr></table></figure></p><p>表示匹配一个数字以外的所有字符。</p><h3 id="匹配定位点"><a href="#匹配定位点" class="headerlink" title="匹配定位点"></a>匹配定位点</h3><p>定位点能够将正则表达式固定到一行或整个字符串的起始位置或结尾。它们还能够创建匹配一个单词的开头、结尾或内部字符的表达式。</p><blockquote><p>需要注意的是,定位点匹配到的并不是一个实际的字符,而只是一个位置。</p></blockquote><p>例如,在表达式<figure class="highlight plain"><figcaption><span>匹配单词边界。 该表达式与 “never” 中的 “er” 匹配,但与 “verb” 中的 “er” 不匹配。</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">下表包含正则表达式定位点以及它们的含义:</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">| 字符 | 说明 |</span><br><span class="line">| --- | --- |</span><br><span class="line">| ^ | 匹配输入字符串开始的位置。 如果标志中包括 m(多行搜索)字符,^ 还将匹配 \n 或 \r 后面的位置。 |</span><br><span class="line">| $ | 匹配输入字符串结尾的位置。 如果标志中包括 m(多行搜索)字符,$ 还将匹配 \n 或 \r 前面的位置。 |</span><br><span class="line">| \b | 匹配一个字边界,即字与空格间的位置。 |</span><br><span class="line">| \B | 非字边界匹配。|</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">### 子串的捕获</span><br><span class="line">使用括号可以捕获其中的子串,并将其保存作为变量,以用于后续的匹配或替换。假设正则表达式是一个小型计算机程序,那么捕获子串就是它输出的一部分。</span><br><span class="line"></span><br><span class="line">在实际使用中,可能会捕获很多子串,被捕获的子串从左向右编号,也就是只需要对左括号计数。引用时使用```\x```的格式,子串编号从```\1```开始,```\0```表示原字符串本身。</span><br><span class="line"></span><br><span class="line">假设正则表达式如下:</span><br></pre></td></tr></table></figure></p><p>(\w+) had a ((\w+) \w+)<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">那么对于字符串:</span><br></pre></td></tr></table></figure></p><p>I had a nice day<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">该正则表达式捕获到的子串如下:</span><br></pre></td></tr></table></figure></p><p>\0: I had a nice day<br>\1: I<br>\2: nice<br>\3: nice day<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">### 或</span><br><span class="line">正则表达式中允许对多个匹配选项之间进行分组,相当于“或”的作用。</span><br><span class="line">如正则表达式:</span><br><span class="line">```(Chapter|Section) [1-9][0-9]{0,1}</span><br></pre></td></tr></table></figure></p><p>在匹配字符串:<br><figure class="highlight plain"><figcaption><span>3 Section 90```</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">时,可以匹配到 ```Chapter``` 和 ```Section</span><br></pre></td></tr></table></figure></p><h2 id="Objective-C-中的正则表达式"><a href="#Objective-C-中的正则表达式" class="headerlink" title="Objective-C 中的正则表达式"></a>Objective-C 中的正则表达式</h2><p>Objective-C 中有专门的一个正则表达式类 —— NSRegularExpression,使用较为方便。此外在 NSPredicate 中也支持用正则表达式进行查询。OC 头文件中的注释对它们的具体用法作了详细的介绍,本文就再赘述了。</p><blockquote><p>值得注意的是,OC 中的正则表达式中,匹配到的子串是用<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">## 示例</span><br><span class="line"></span><br><span class="line">### 将表格替换为 Markdown 格式</span><br><span class="line"></span><br><span class="line">上文中从维基百科上拷贝下来的表格:</span><br></pre></td></tr></table></figure></p></blockquote><p>字符 描述<br>\ 将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape)、或一个向后引用(backreferences)、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\”匹配“\”而“(”则匹配“(”。<br>^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。<br>$ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。</p><ul><li>匹配前面的子表达式零次或多次。例如,zo<em>能匹配“z”、“zo”以及“zoo”。</em>等价于{0,}。</li></ul><ul><li>匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">Markdown 格式的表格:</span><br></pre></td></tr></table></figure></li></ul><table><thead><tr><th>字符</th><th>描述</th></tr></thead><tbody><tr><td>\</td><td>将下一个字符标记为一个特殊字符(File Format Escape)、或一个原义字符(Identity Escape)、或一个向后引用(backreferences)、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。序列“\”匹配“\”而“(”则匹配“(”。</td></tr><tr><td>\^</td><td>匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,\^也匹配“\n”或“\r”之后的位置。</td></tr><tr><td>$</td><td>匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。</td></tr><tr><td>*</td><td>匹配前面的子表达式零次或多次。例如,zo<em>能匹配“z”、“zo”以及“zoo”。</em>等价于{0,}。</td></tr><tr><td>+</td><td>匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。</td></tr></tbody></table><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">匹配的正则表达式:</span><br><span class="line">``` ^(.*?)\t(.*)$</span><br></pre></td></tr></table></figure><p>替换的正则表达式:<br><figure class="highlight plain"><figcaption><span>\1 | \2 | ```</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"></span><br><span class="line">### 美拍滤镜 plist 格式替换</span><br><span class="line"></span><br><span class="line">原格式:</span><br><span class="line"></span><br><span class="line">```xml</span><br><span class="line"><dict></span><br><span class="line"><key>id</key></span><br><span class="line"><integer>3002</integer></span><br><span class="line"><key>inputSource</key></span><br><span class="line"><array></span><br><span class="line"><dict></span><br><span class="line"><key>index</key></span><br><span class="line"><integer>1</integer></span><br><span class="line"><key>source</key></span><br><span class="line"><string>3002/gleam.png</string></span><br><span class="line"></dict></span><br><span class="line"></array></span><br><span class="line"><key>name</key></span><br><span class="line"><string>晨露</string></span><br><span class="line"><key>nameEN</key></span><br><span class="line"><string>Shimmer</string></span><br><span class="line"><key>nameTW</key></span><br><span class="line"><string>微光</string></span><br><span class="line"><key>percent</key></span><br><span class="line"><real>0.7</real></span><br><span class="line"><key>shaderType</key></span><br><span class="line"><integer>1</integer></span><br><span class="line"><key>statisticsId</key></span><br><span class="line"><string>fli3002</string></span><br><span class="line"><key>thumb</key></span><br><span class="line"><string>gleam2.png</string></span><br><span class="line"></dict></span><br></pre></td></tr></table></figure></p><p>目标格式:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>ColorFilter<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">integer</span>></span>58<span class="tag"></<span class="name">integer</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>ColorFilterConfigPath<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>preFilters/3030/filterConfig.plist<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>Icon<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>perfume2.png<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>MVID<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>fli3030<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>Title<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>嘉年华<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>TitleTranslation<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>zh-Hant<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>嘉年華<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>en<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>Carnival<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dict</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dict</span>></span></span><br></pre></td></tr></table></figure><p>查找的正则表达式:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><dict>[\w|\W]*?<key>name</key>\W+<string>([\w|\W]*?)</string>\W+<key>nameEN</key>\W+<string>([\w|\W]*?)</string>\W+<key>nameTW</key>\W+<string>([\w|\W]*?)</string>[\w|\W]*?<key>statisticsId</key>\W+<string>([a-zA-Z]+([0-9]+))</string>\W+<key>thumb</key>\W+<string>([\w|\W]*?)</string>\W+</dict></span><br></pre></td></tr></table></figure></p><p>替换的正则表达式:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><dict>\n<key>ColorFilter</key>\n<integer>58</integer>\n<key>ColorFilterConfigPath</key>\n<string>preFilters/\5/filterConfig.plist</string>\n<key>Icon</key>\n<string>\6</string>\n<key>MVID</key>\n<string>\4</string>\n<key>Title</key>\n<string>\1</string>\n<key>TitleTranslation</key>\n<dict>\n<key>zh-Hant</key>\n<string>\3</string>\n<key>en</key>\n<string>\2</string>\n</dict>\n</dict></span><br></pre></td></tr></table></figure></p><p>###歌词信息读取</p><p>歌词文件的样式(尖括号中为时间戳,后接一段歌词):</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><0,626>killing <627,626>spring<627,626>kill ing<627,626>我是一段 歌词### <627,626>我是一句歌词~ ~~</span><br></pre></td></tr></table></figure><p>用于匹配的正则表达式:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><([0-9]+),([0-9]+)>([\w|\W]+?)(?=<|$)</span><br></pre></td></tr></table></figure><p>可以将匹配到的子串转换为 JSON 格式:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">{\n"stamp1": \1,\n"stamp2": \2,\n"content": \3\n},\n</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
五分钟重拾正则表达式。本文简要介绍了正则表达式的使用,能帮助你快速拾起正则表达式。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="正则表达式" scheme="http://punmy.cn/tags/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/"/>
<category term="技能" scheme="http://punmy.cn/tags/%E6%8A%80%E8%83%BD/"/>
</entry>
<entry>
<title>iOS Search APIs</title>
<link href="http://punmy.cn/2017/03/06/15158218121366.html"/>
<id>http://punmy.cn/2017/03/06/15158218121366.html</id>
<published>2017-03-06T08:40:00.000Z</published>
<updated>2018-04-07T10:51:43.000Z</updated>
<content type="html"><![CDATA[<h2 id="Introduce"><a href="#Introduce" class="headerlink" title="Introduce"></a>Introduce</h2><p>iOS 9 之后,Apple 开放了三大 Search APIs,以便用户能在 Spotlight、Safari 等搜索入口,搜索到应用中的内容,这十分有助于提升应用的用户活跃度。</p><a id="more"></a><p><img src="/media/15158218121366/QQ20170314-163826.png" alt="搜索入口"></p><p>这三组 Search APIs 分别是:</p><ul><li><strong>NSUserActivity</strong>:能够为用户在 App 中的历史活动建立索引,例如到达某个关键页面,或是浏览到某个内容页。以便以后用户能够通过搜索结果恢复到该页面。</li><li><strong>Core Spotlight</strong>:为 App 中的一些重要内容,在设备上建立索引,以提供快速入口。</li><li><strong>Web markup</strong>:关联服务器上的内容到搜索结果中,用户即使没有安装 App,也能在搜索结果中看到相关内容。</li></ul><p>本文主要介绍前两种 API —— NSUserActivity 和 Core Spotlight。</p><blockquote><p><strong>Note</strong>: 虽然全局搜索(app search)功能在 iOS 9 后就可用了,但是实际上,部分旧机型仍然不支持 NSUserActivity 和 Core Spotlight 的搜索功能,如 iPhone 4s、iPad 2、iPad(第三代)、iPad mini,以及 iPod touch 5.</p></blockquote><h2 id="Core-Spotlight"><a href="#Core-Spotlight" class="headerlink" title="Core Spotlight"></a>Core Spotlight</h2><blockquote><p>Index App Content</p></blockquote><p>Core Spotlight 主要用于为 App 中的一些重要内容(比较静态),在设备上建立索引。例如,应用的一些常用功能页面,以及一些文档、音视频内容,以便用户能够很方便地访问这些常用内容。</p><p><img src="/media/15158218121366/numbers_2x.png" alt="Core Spotlight"></p><p>使用前需要 import <code>Core Spotlight</code>。</p><h3 id="1、建立索引"><a href="#1、建立索引" class="headerlink" title="1、建立索引"></a>1、建立索引</h3><p>索引对应的类是<code>CSSearchableItem</code>。建立索引前,要先创建索引的属性集<code>CSSearchableItemAttributeSet</code>。</p><p><code>CSSearchableItemAttributeSet</code>中包含了大量的属性,包括音视频信息、文件信息、出版信息、联系人信息等等。实例化属性集时,就要指定属性集的类型,如<code>kUTTypeImage</code>,这会影响到索引的显示样式。这些常量声明于<code>MobileCoreServices.UTCoreTypes</code>中,因此需要 <strong>import</strong> 这个头文件。属性集中比较常用的属性为 title、contentDescription 和 keywords,title 就是索引显示的标题,contentDescription 是对内容的描述,keywords 则是索引的关键词数组,官方建议关键词为 5 个左右。需要注意的是,title 默认就能被搜索到,不需要加入 keywords。</p><p>设置完索引的属性集之后,就可以通过<code>CSSearchableItem</code>的<code>init(uniqueIdentifier: domainIdentifier: attributeSet:)</code>来实例化一个索引。其中,参数<code>uniqueIdentifier</code>是该索引的唯一标识符,可以通过这个标识符来删除这条索引;<code>domainIdentifier</code>是索引所在的域名标识,域名标识可以用于将索引分类存放,可以根据它来删除同一个<code>domainIdentifier</code>下的所有索引。</p><p>接下来便是建立索引。建立索引需要调用<code>CSSearchableIndex</code>实例的<code>indexSearchableItems()</code>方法。它能够批量建立索引,并提供一个回调。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">createSearchIndex</span><span class="params">(with followers: [User])</span></span> {</span><br><span class="line"> <span class="keyword">if</span> #available(iOS <span class="number">9.0</span>, *) {</span><br><span class="line"> <span class="keyword">var</span> searchableItems = [<span class="type">CSSearchableItem</span>]()</span><br><span class="line"> users.forEach { (follower) <span class="keyword">in</span></span><br><span class="line"> <span class="comment">// 索引的属性集(只用于控制索引在被搜索时的属性,不能存储数据)</span></span><br><span class="line"> <span class="keyword">let</span> attributeSet = <span class="type">CSSearchableItemAttributeSet</span>(itemContentType: kUTTypeImage <span class="keyword">as</span> <span class="type">String</span>)</span><br><span class="line"> <span class="comment">// 索引的标题(标题也能被搜索到)</span></span><br><span class="line"> attributeSet.title = follower.name</span><br><span class="line"> <span class="comment">// 索引的描述</span></span><br><span class="line"> attributeSet.contentDescription = <span class="string">"我的爱豆"</span></span><br><span class="line"> <span class="comment">// 用于建立索引的关键词</span></span><br><span class="line"> attributeSet.keywords = [<span class="string">"punmy"</span>, <span class="string">"爱豆"</span>, follower.identifier]</span><br><span class="line"> <span class="comment">// Spotlight 索引的类</span></span><br><span class="line"> <span class="keyword">let</span> searchableItem = <span class="type">CSSearchableItem</span>(uniqueIdentifier: <span class="string">"punmy://user?id=<span class="subst">\(follower.identifier)</span>"</span>, domainIdentifier: <span class="string">"com.meitu.yy.followers"</span>, attributeSet: attributeSet)</span><br><span class="line"> searchableItems.append(searchableItem)</span><br><span class="line"> }</span><br><span class="line"> <span class="type">CSSearchableIndex</span>.<span class="keyword">default</span>().indexSearchableItems(searchableItems, completionHandler: { (error) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> error = error {</span><br><span class="line"> <span class="type">DDLogError</span>(<span class="string">"Create searchable index failed, error: <span class="subst">\(error.localizedDescription)</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">cleanSearchIndex</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">if</span> #available(iOS <span class="number">9.0</span>, *) {</span><br><span class="line"> <span class="comment">// 删除整个域名标识下的索引</span></span><br><span class="line"> <span class="type">CSSearchableIndex</span>.<span class="keyword">default</span>().deleteSearchableItems(withDomainIdentifiers: [<span class="string">"com.punmy.followers"</span>]) { (error) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> <span class="number">_</span> = error {</span><br><span class="line"> <span class="type">DDLogError</span>(<span class="string">"delete searchable items failed, error: <span class="subst">\(error?.localizedDescription)</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>调用<code>indexSearchableItems()</code>方法成功后,就能立即在 Spotlight 中搜索到刚刚添加的索引了。</p><p><img src="/media/15158218121366/[email protected]" alt="Spotlight 搜索"></p><p>通过 <code>CSSearchableIndex</code> 实例的几个删除方法,就能部分,或者全部删除索引。</p><h3 id="2、实现回调"><a href="#2、实现回调" class="headerlink" title="2、实现回调"></a>2、实现回调</h3><p>要处理从搜索结果中进入 App 的回调事件,需要在 <strong>AppDelegate</strong> 中实现<code>application:continueUserActivity:restorationHandler:</code>方法。这个回调在许多地方都会用到,如 Core Spotlight、Siri Kit、Handoff 等。可以在这个页面中,利用 <code>userActivity</code> 的 <code>userInfo</code> 字典中的数据,跳转到相应的页面。</p><blockquote><p><strong>Note</strong>:值得注意的是,之前我们为<code>userActivity</code>所设置的属性集,在这里都不存在了。只有保存在 <code>userInfo</code> 中的数据,才能在这个回调中获取到。</p></blockquote><p>之前创建<code>CSSearchableItem</code>时传入的<code>uniqueIdentifier</code>参数,在此处可以通过 <code>userInfo</code> 中的 <code>CSSearchableItemActivityIdentifier</code> key值读取到,可以参考一下代码。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">application</span><span class="params">(<span class="number">_</span> application: UIApplication, <span class="keyword">continue</span> userActivity: NSUserActivity, restorationHandler: @escaping <span class="params">([Any]?)</span></span></span> -> <span class="type">Void</span>) -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">if</span> #available(iOS <span class="number">9.0</span>, *) {</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> identifier = userActivity.userInfo?[<span class="type">CSSearchableItemActivityIdentifier</span>] <span class="keyword">as</span>? <span class="type">String</span> {</span><br><span class="line"> <span class="comment">// 跳转到对应页面</span></span><br><span class="line"> restoreActivity(scheme: identifier)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上文我们已经提到过,有许多功能的回调入口都是这个方法,那么如何区分不同入口呢?<br>一种办法是像上方代码一样,判断<code>userInfo</code>中是否存在 key 为<code>CSSearchableItemActivityIdentifier</code>的值;另一种办法可以判断<code>userActivity</code>的 <code>activityType</code>属性的值,是否为<code>CSSearchableItemActionType</code>。</p><p>在某些功能中,userActivity 的 <code>activityType</code> 是设置为自定义域名的,如下面将会讲到 <strong>NSUserActivity</strong> 部分。但在 Core Spotlight 中,所有的索引的类型都被系统设置为<code>CSSearchableItemActionType</code>。</p><h2 id="NSUserActivity"><a href="#NSUserActivity" class="headerlink" title="NSUserActivity"></a>NSUserActivity</h2><blockquote><p>Index Activities and Navigation Points</p></blockquote><p><strong>NSUserActivity</strong> 主要用于为用户在 App 中的<strong>历史活动</strong>建立索引。NSUserActivity 的作用有些类似于浏览器中的“历史记录”,只是记录的内容是由我们来决定的。NSUserActivity 可以配合 Handoff 一起使用,以便在其他设备上继续当前活动,有关 Handoff 可以参照:<a href="https://code.tutsplus.com/tutorials/an-introduction-to-handoff--cms-24349" target="_blank" rel="noopener">Handoff 介绍</a>。</p><p><img src="/media/15158218121366/movie_2x.png" alt="历史活动"></p><p>我们可以在用户使用 App 的时候,将他浏览过的一些关键节点,或是重要的内容页面,在系统中建立一个索引,保存必要的数据。日后用户在 Spotlight 等地方搜索相关内容时,就能在搜索结果中显示该活动记录(UserActivity)。当用户点击该搜索结果时,系统会调起 App,并传入之前保存的数据,应用就能以此来恢复现场。</p><h3 id="1、建立索引-1"><a href="#1、建立索引-1" class="headerlink" title="1、建立索引"></a>1、建立索引</h3><p>首先,我们需要在用户浏览到某些页面的时候,将该页面加入系统索引。例如在 <code>viewDidLoad()</code>的时候:</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// MARK: - Indexing activity</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">let</span> user</span><br><span class="line"><span class="keyword">var</span> activity? <span class="comment">// 务必对 Activity 进行强引用</span></span><br><span class="line"> </span><br><span class="line"><span class="keyword">override</span> <span class="function"><span class="keyword">func</span> <span class="title">viewDidLoad</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.viewDidLoad()</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">let</span> activity = <span class="type">NSUserActivity</span>(activityType: <span class="string">"com.meitu.punmy.user"</span>)</span><br><span class="line"> <span class="comment">// 索引的标题</span></span><br><span class="line"> activity.title = user.name </span><br><span class="line"> <span class="keyword">if</span> #available(iOS <span class="number">9.0</span>, *) {</span><br><span class="line"> <span class="comment">// 索引的属性集(只影响搜索结果的显示样式,不能用于存储数据)</span></span><br><span class="line"> <span class="keyword">let</span> attributeSet = <span class="type">CSSearchableItemAttributeSet</span>(itemContentType: kUTTypeContact <span class="keyword">as</span> <span class="type">String</span>) </span><br><span class="line"> <span class="comment">// 索引的描述</span></span><br><span class="line"> attributeSet.contentDescription = user.desc</span><br><span class="line"> <span class="comment">// 能被搜索到的关键词(索引的标题也能被搜索到)</span></span><br><span class="line"> attributeSet.keywords = [user.identifier, <span class="string">"punmy"</span>]</span><br><span class="line"> <span class="comment">// 与项目本身的 scheme 搭配使用,便于跳转 </span></span><br><span class="line"> <span class="keyword">let</span> scheme = <span class="string">"punmy://user?id=<span class="subst">\(user.identifier)</span>"</span></span><br><span class="line"> <span class="comment">// 关联已有的 SpotLight Item</span></span><br><span class="line"> attributeSet.relatedUniqueIdentifier = scheme </span><br><span class="line"> </span><br><span class="line"> activity.contentAttributeSet = attributeSet</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 只有 userInfo 中能够保存数据</span></span><br><span class="line"> activity.userInfo = [<span class="string">"scheme"</span>: scheme] </span><br><span class="line"> <span class="comment">// 是否建立索引</span></span><br><span class="line"> activity.isEligibleForSearch = <span class="literal">true</span></span><br><span class="line"> <span class="comment">// 是否支持 Handoff 功能</span></span><br><span class="line"> activity.isEligibleForHandoff = <span class="literal">false</span></span><br><span class="line"> <span class="comment">// 索引的过期时间,默认一个月</span></span><br><span class="line"> activity.expirationDate = <span class="type">Date</span>(timeIntervalSinceNow: <span class="number">7</span> * <span class="number">24</span> * <span class="number">60</span> * <span class="number">60</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 激活 Activity</span></span><br><span class="line"> activity.becomeCurrent() </span><br><span class="line"> <span class="comment">// 必须对 Activity 进行强引用,否则 Activity 很可能不会被加入索引</span></span><br><span class="line"> <span class="keyword">self</span>.activity = activity</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>要完成这个任务,我们先要创建一个 <strong>NSUserActivity</strong>对象,用于保存当前页面的一些必要信息,以及要建立的索引的一些属性。</p><p>创建 NSUserActivity 对象时,需要传入一个初始化参数<code>activityType</code>,这个<code>activityType</code>类似于 Core Spotlight 中的域名标识,只是作用上有些区别。在 Spotlight 等入口,搜索结果被点击时,系统会根据这个标识,去区分由哪个应用来恢复这个活动。在进入应用后,我们也能够在回调中,利用这个属性来区分活动。</p><p>为了让系统知道我们的应用能处理哪些 activityType,我们也需要在 Info.plist 中,创建一个名为<code>NSUserActivityTypes</code>的 String 数组,标识出所有我们的应用能够处理的 activityType。</p><p>通过设置 <code>NSUserActivity</code>的<code>contentAttributeSet</code>属性,我们可以自定义索引在搜索结果中的样式。设置给<code>contentAttributeSet</code>属性的是一个<code>CSSearchableItemAttributeSet</code>实例,与 Core Spotlight 中的属性集相似,它包含大量的属性可以设置。</p><blockquote><p><strong>Note</strong>:Activity 在调用了 <code>becomeCurrent()</code> 方法激活后,必须对 Activity 进行强引用,防止它 dealloc 了,否则 Activity 很可能不会被加入索引。相关讨论见 Apple 论坛:<a href="https://forums.developer.apple.com/message/13640#13640" target="_blank" rel="noopener">Search APIs are working</a></p></blockquote><h3 id="2、实现回调-1"><a href="#2、实现回调-1" class="headerlink" title="2、实现回调"></a>2、实现回调</h3><p><strong>NSUserActivity</strong> 的回调入口与 <strong>Core Spotlight</strong> 的很相似。同样在<code>AppDelegate</code>中实现<code>application:continueUserActivity:restorationHandler:</code>方法,像上面说到的,我们可以通过 <code>userActivity</code> 的 <code>activityType</code> 属性来区分活动类型。此时<code>userActivity</code>的属性集也已经为空,只能访问到 <code>userInfo</code> 中的数据。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">application</span><span class="params">(UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: [AnyObject]? -> Void)</span></span> -> <span class="type">Bool</span> {</span><br><span class="line"> <span class="keyword">if</span> userActivity.activityType == <span class="string">"com.meitu.punmy.user"</span> {</span><br><span class="line"> <span class="comment">// Restore app state for this userActivity and associated userInfo value.</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Web-markup"><a href="#Web-markup" class="headerlink" title="Web markup"></a>Web markup</h2><blockquote><p>Engage Web Content</p></blockquote><p>由于本次分享的主要针对 Core Spotlight 和 NSActivity,Web markup 就只作简要介绍。</p><p>Web Markup允许应用将它们的内容映射到一个网站(如网页版美拍),从而在 Spotlight 或 Safari 中进行搜索,即使用户没有安装相关应用。用户点击相关结果后,如果没有安装应用,则通过 Safari 打开,如果已经安装应用,则跳转到对应页面。</p><p>苹果有类似于搜索引擎爬虫的机器人,能够抓取支持 Web markup 的网站来获取所需的信息(需要后台配置,并结合 Universal Link 使用)。抓取结果会储存在苹果的云索引服务器上,通过 Safari 和 Spotlight 提供给用户。</p><p>有关 Web markup 的详细内容可以访问官方文档:<a href="https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/WebContent.html" target="_blank" rel="noopener">Mark Up Web Content</a></p><h2 id="联合使用-Core-Spotlight-和-NSUserActivity"><a href="#联合使用-Core-Spotlight-和-NSUserActivity" class="headerlink" title="联合使用 Core Spotlight 和 NSUserActivity"></a>联合使用 Core Spotlight 和 NSUserActivity</h2><p>苹果在官方文档中表示,三大搜索 API 是为了联合使用而设计的,混合使用多种 API 有助于提升搜索的覆盖率。但在实际使用中,混合使用多种 API 会有一些坑,下面大致讲一下一些注意事项。</p><p>###1、唯一标识的使用###<br>Core Spotlight 中的 <code>uniqueIdentifier</code>,NSUserActivity 中的 <code>relatedUniqueIdentifier</code> 属性,以及 Web markup 中的 <code>webpageURL</code>,都会被系统用于索引的<strong>关联</strong>。但系统不会平等地处理这几个东西。</p><p>例如,如果我们分别通过 Core Spotlight 和 NSActivity 生成了两个相同标识的索引/活动,那么在 Spotlight 中搜索到的只会是通过 Core Spotlight 设置的索引;而 NSActivity 生成的索引,则是用于 Siri Kit(如果你有使用 Siri Kit 的话)。</p><p>###2、索引的更新和删除###<br>通过 Core Spotlight 建立的索引,可以通过 CSSearchableIndex 实例的三个删除方法进行删除。也可以设置<strong>过期时间</strong>,由系统自动清除。默认的过期时间为一个月。<br>而通过 NSActivity 生成的索引,只能设置过期时间,由系统管理,无法手动删除。</p><p>要更新相同 API 建立的索引,只需要再建立一个相同 identifier 的索引或者活动,系统就会自动更新对应索引。但是,不同 API 生成的索引,即使 identifier 相同,也不会相互更新内容。</p><p>###3、搜索排序###<br>苹果对于搜索的排序主要依据以下几个维度:</p><ul><li>用户浏览 App 中内容的频率 (当我们调用 NSUserActivity 的<code>becomeCurrent()</code>方法时,系统会进行统计)</li><li>用户对于应用中的内容的参与度(由“互动率”决定。“互动率”是基于两个数据计算的,分别是:用户点击与你应用相关的条目的次数,以及搜索结果中显示的应用相关的条目的数量)</li><li>你的网站中某个网址的受欢迎程度,以及可用的结构化数据量。(Web markup)</li></ul><p>坊间传闻,苹果为了防止世界被破坏,守护 iOS 的生态圈,在搜索结果的排序上,花费了重金进行优化。没有好好维护 iOS 生态圈的话,可能会导致应用的索引被降低排名,或是被踢出搜索结果。因此在使用搜索 API 时,我们需要注意:</p><ol><li>防止过度索引;(不要把一大堆有的没的数据,都丢到系统索引中去)</li><li>尽快将用户带入内容页;(避免中间步骤,以及降低 App 启动时间)</li><li>如果创建的 NSUserActivity 与已有的 Core Spotlight 索引相同,那么就将它们用相同的 identifier 关联起来,这样每次激活 NSUserActivity 时,也能提升 Core Spotlight 索引的排名;</li></ol><p><img src="/media/15158218121366/apple.png" alt="延迟进入内容页"></p><h2 id="END"><a href="#END" class="headerlink" title="END"></a>END</h2><p>使用 Search APIs 可以有效提升应用的用户体验,但要注意各种 API 的适用场景,同时维护好 iOS 的生态圈。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>官方文档:<a href="https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/" target="_blank" rel="noopener">App Search Programming Guide</a></p>]]></content>
<summary type="html">
iOS 9 之后,Apple 开放了三大 Search APIs,以便用户能在 Spotlight、Safari 等搜索入口,搜索到应用中的内容,这十分有助于提升应用的用户活跃度。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="基础" scheme="http://punmy.cn/tags/%E5%9F%BA%E7%A1%80/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="framework" scheme="http://punmy.cn/tags/framework/"/>
</entry>
<entry>
<title>OSX/iOS 网络抓包工具 Charles 入门</title>
<link href="http://punmy.cn/2016/10/06/14757432517519.html"/>
<id>http://punmy.cn/2016/10/06/14757432517519.html</id>
<published>2016-10-06T08:40:00.000Z</published>
<updated>2018-04-07T10:08:11.000Z</updated>
<content type="html"><![CDATA[<p><img src="/media/14757432517519/Charles.jpg" alt="Charles"></p><p>在 iOS 开发中,我们常常要涉及到网络编程,而网络编程中的调试往往令人头痛。此外,我们也有时也会需要抓取一些 App 的网络包,以便了解它网络请求的API。本篇要介绍的 Charles 就是一款非常好用的网络调试工具,它可以轻松地满足我们的上述需求。</p><a id="more"></a><blockquote><p>本篇博客中所使用的环境为:macOS Sierra 10.12,Charles 4.0.1,iPod Touch 5。</p></blockquote><h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2><h3 id="启动-Charles"><a href="#启动-Charles" class="headerlink" title="启动 Charles"></a>启动 Charles</h3><p>Charles 的下载安装这里就不再赘述。启动 Charles 后,我们可以看到 Charles 的主界面。</p><p><img src="/media/14757432517519/Charles%E7%AA%97%E5%8F%A3.jpg" alt="Charles窗口"></p><p>窗口的左侧是抓取到的网络包,通常 Charles 启动后我们就可以看到 Mac 上的一些网络包开始出现在这里。</p><p>窗口右侧是选中的网络包的详细信息,如 URL、状态码等。</p><p>窗口上方是一些常用的工具。鼠标经过时就会有相应的提示,后面再细说。</p><p>右下角是 Charles 当前的状态。可以显示 Recording、BreakPoints、Rewrite 等功能是否启用。</p><h3 id="连接设备"><a href="#连接设备" class="headerlink" title="连接设备"></a>连接设备</h3><p>要抓取 iOS 设备上的网络包,首先要将 iOS 设备和装有 Charles 的电脑连接到同一个局域网下,并将 Charles 设置为 iOS 的 HTTP 代理,这样 Charles 就可以捕捉到所有进出 iOS 的网络包。设置的步骤如下:</p><ol><li>确保装有 Charles 的 mac 和等待调试的 iOS 设备在同一局域网内;</li><li><p>查找 mac 的 IP 地址(可以在 Charles 菜单栏的 Help -> Local IP Address 直接查看);<br><img src="/media/14757432517519/14757643277589.jpg" alt="快速查看本机 IP"></p></li><li><p>在 iOS 设备上的 设置 -> Wi-Fi 中,点击已连接 WiFi 右侧的 详细按钮(蓝色),将 WiFi 详情页底部的 HTTP 代理 设为手动,并在 服务器 一栏填入上一步中 mac 的 IP 地址,在 端口 一栏填入 Charles 的默认代理端口 8888,然后退出设置;<br><img src="/media/14757432517519/14757646090593.jpg" alt="代理设置"></p></li><li><p>此时如果网络正常的话,Charles 会提示有设备企图连接,同意连接,便完成了代理的设置。</p></li></ol><p><img src="/media/14757432517519/14757642106306.jpg" alt="连接请求"></p><blockquote><p>注意:许多公司的内部局域网会限制设备间的互相访问,这可能会导致 Charles 抓取不到网络包,这种情况下可以尝试自己创建一个热点。</p></blockquote><p>连接完成后,窗口左侧开始出现 Charles 抓到的包,说明进展顺利。</p><h2 id="抓包"><a href="#抓包" class="headerlink" title="抓包"></a>抓包</h2><h3 id="捕获"><a href="#捕获" class="headerlink" title="捕获"></a>捕获</h3><p>Charles 启动后,就处于 Recording 状态,会自动捕捉网络包,点击窗口上方工具栏的第二个按钮可以停止捕捉。</p><p>在窗口左侧的选择上方的 Structure 或 Sequence 可以切换网络包的显示方式。</p><ul><li>Structure 是根据主机名以树状显示,便于查看同一主机名的网络包,每当有新的请求到达时,相应的主机名就会用黄色高亮闪烁。我们通常都是采用这个方式。</li><li>Sequence 则是根据网络请求的时间顺序依次显示,在这个模式下,整个窗口的布局也会有所变化。</li></ul><p>选中某个网络包后,右侧就会显示该网络包的详细信息。</p><p><img src="/media/14757432517519/14757655232099.jpg" alt="微信朋友圈的网络请求"></p><p>开始抓包之后,我们会发现,只有 HTTP 的包被成功解析了,HTTPS 的包都处于加密状态。要想查看 HTTPS 的包内容,可以查看本文中 SSL 代理的小节。</p><h3 id="过滤"><a href="#过滤" class="headerlink" title="过滤"></a>过滤</h3><p>开始抓包后,随着时间的推移,抓取到的网络包越来越多,这是我们可以使用过滤功能,来过滤出我们想要的网络请求。</p><p>在 Proxy -> Recording Settings 中可以设置网络包的过滤选项。</p><p><img src="/media/14757432517519/14757660613993.jpg" alt="过滤选项"></p><p>过滤选项中有 Include 和 Exclude 两种选项。只有当 Include 为空时,Exclude 中的设置才会生效。过滤条件支持通配符。</p><h2 id="修改网络包"><a href="#修改网络包" class="headerlink" title="修改网络包"></a>修改网络包</h2><h3 id="修改历史请求"><a href="#修改历史请求" class="headerlink" title="修改历史请求"></a>修改历史请求</h3><p>可以将历史的请求修改后,再次发送。</p><p>只需选中某个请求,点击上方工具栏中的蓝色钢笔按钮(第四个),就可以进行修改。完成修改后,点击下方的 Execute 执行请求。</p><p><img src="/media/14757432517519/14757676735559.jpg" alt="修改请求"></p><h3 id="断点"><a href="#断点" class="headerlink" title="断点"></a>断点</h3><p>窗口上方的工具栏中,第四个按钮就是 Enable BreakPoints,用来启用或禁用断点。也可以在 Proxy -> BreakPoints Setting 中设置更多具体内容。</p><p>在 Charles 中可以像调试程序一样添加断点。方法是右键点击左侧窗口的某个请求,选择 BreakPoints 添加断点。这样当这个请求发出或者收到 response 的时候,就会先被 Charles 拦截下来,并触发断点。</p><p><img src="/media/14757432517519/14759352396110.jpg" alt="某个 request 的断点"></p><p>触发断点后,可以对断点的网络包进行各种编辑,然后再继续。点击 Execute 就可以继续。</p><p>同时,也可以在 Proxy -> BreakPoints Setting 设置断点的各种规则。例如,是在 request 的时候触发还是 response 的时候。</p><p><img src="/media/14757432517519/14759362841562.jpg" alt="断点设置"></p><p>由于设置断点时,Charles 是先拦截下整个网络包,再触发断点,当网络包比较大的时候,常常会导致应用超时,触发网络错误的警告,因此,自动地根据规则修改网络包有时显得尤为重要。这就是下面要说的<strong>篡改</strong>。</p><h3 id="篡改(Rewrite)"><a href="#篡改(Rewrite)" class="headerlink" title="篡改(Rewrite)"></a>篡改(Rewrite)</h3><p>Rewrite 是按照一组事先设置的规则,篡改特定的网络包中的数据。</p><p>在 Tools -> Rewrite 中,选中 Enable Rewrite 来开启 Rewrite。</p><p><img src="/media/14757432517519/14759368101329.jpg" alt="Rewrite"></p><p>勾选 Debug in Error Log 选项,就能在 Charles 控制台中看到 Rewrite 的记录。</p><p>首先要在右侧的规则列表中添加一个新规则。在新规则中添加要 Rewrite 的 Location,然后再下方添加具体的篡改规则。规则中可以使用通配符。</p><p><img src="/media/14757432517519/14759370781229.jpg" alt="Rewrite Rule"></p><p>这样稍后匹配条件的网络包到达的时候,Charles 就会自动将其中的内容按规则篡改。</p><h3 id="映射"><a href="#映射" class="headerlink" title="映射"></a>映射</h3><p>Charles提供的映射功能可以将本地文件或者远程的服务器作为某个请求的 Response。可以方便地进行一些特殊的测试。</p><h4 id="Map-Local"><a href="#Map-Local" class="headerlink" title="Map Local"></a>Map Local</h4><p>本地映射,在 Tools -> Map Local。可以选择一个本地文件作为某个请求的 Response,并且 Charles 会帮你封装好 Response。<br><img src="/media/14757432517519/14759386662038.jpg" alt="Map Local"></p><h4 id="Remote-Remote"><a href="#Remote-Remote" class="headerlink" title="Remote Remote"></a>Remote Remote</h4><p>远程映射和本地映射的功能类似,只是将数据源换成了远程服务器。相当于将请求交给另一个服务器处理</p><h2 id="网络环境模拟"><a href="#网络环境模拟" class="headerlink" title="网络环境模拟"></a>网络环境模拟</h2><p>Charles 还可以模拟不同网速环境,可以很方便地测试应用在网络差的情况下的 bug。</p><p>在 Proxy -> Throttle Settings 中勾选 Enable Throttling,或者直接点击窗口上方的工具栏中的乌龟🐢按钮就可以启用,这个按钮十分形象。</p><p>在 Proxy -> Throttle Settings 中,添加要针对的 Locations,如果选中了 Only for selected hosts,并且Locations中有数据,则只有 Locations 列表中的请求会被限速,否则会对全局限速。在 Throttle Configuration 中可以对网络环境进行十分详细的配置,包括网络的稳定程度、网速、环境等。</p><p><img src="/media/14757432517519/14759391242777.jpg" alt="Throttle Setting"></p><h2 id="SSL-代理"><a href="#SSL-代理" class="headerlink" title="SSL 代理"></a>SSL 代理</h2><p>在使用 Charles 的过程中,我们会发现,只有未加密的 Http 请求才能被 Charles 正确的解析出数据,其余的 Https 请求都处于加锁的状态,但我们不可避免的需要抓取 Https 的包。SSL 代理就可以完美解决这个问题。</p><p>要启用 SSL 代理,先要在 Proxy -> SSL Proxying Settings 中勾选 Enable SSL Proxying,然后配置要代理的 Location,一般可以直接填星号,以匹配所有请求。</p><p><img src="/media/14757432517519/14759409498353.jpg" alt="SSL设置"></p><p><strong>接下来还要安装 Charles 的证书</strong>。</p><p>Charles 中的 HTTPS 代理的原理是,Charles 充当一个中间人,针对目标服务器动态地生成一个使用 Charles 根证书(Charles CA Certificate)签名的证书;请求发生的时候, Charles 会接收 web 服务器的证书,而把自己生成的证书给客户端看。</p><p>因此在在使用 Charles 作为 HTTPS 代理时,客户端在请求 HTTPS 接口的时候会弹出安全警告,提示 Charles 根证书不被信任。我们需要添加 Charles 根证书为信任证书中。</p><p>方法如下:</p><p>1、点击 Help -> SSL Proxying,根据被抓包设备的类型,来选择对应的安装选项(如果是 OSX 就直接选择 Install Charles Root Certificate);</p><p>2、如果是iOS真机,则会弹出下面的提示,此时不用按上面的提示来配置代理,只要按照上文的步骤配置过代理了就可以了。然后在 Safari 中打开 chls.pro/ssl 安装 Charles 的证书,就 OK 了。<br><img src="/media/14757432517519/14759406599047.jpg" alt="提示"></p><p>设置好 SSL 代理后,HTTPS 请求就统统解锁啦!</p><p><img src="/media/14757432517519/ssl%E4%BB%A3%E7%90%86.jpg" alt="ssl代理"></p><blockquote><p>Charles 是一个强大的抓包调试工具,它的功能远不止这些,但本篇作为一篇入门的博客,就先介绍这么多啦,更多功能等待大家探索~</p></blockquote><h2 id="套餐"><a href="#套餐" class="headerlink" title="套餐"></a>套餐</h2><h3 id="Postman"><a href="#Postman" class="headerlink" title="Postman"></a>Postman</h3><p>Charles 搭配 Postman 更好用噢~<br>Postman 是 Chrome 浏览器中的一个小应用,可以在 Chrome 应用商城中找到。是居家旅行测试 Web API 的好帮手!</p>]]></content>
<summary type="html">
Charles 是一款非常好用的网络调试工具。
</summary>
<category term="工具" scheme="http://punmy.cn/categories/%E5%B7%A5%E5%85%B7/"/>
<category term="网络" scheme="http://punmy.cn/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="工具" scheme="http://punmy.cn/tags/%E5%B7%A5%E5%85%B7/"/>
</entry>
<entry>
<title>心跳之旅—💗—iOS用手机摄像头检测心率(PPG)</title>
<link href="http://punmy.cn/2016/07/28/15231176397746.html"/>
<id>http://punmy.cn/2016/07/28/15231176397746.html</id>
<published>2016-07-28T12:56:00.000Z</published>
<updated>2018-04-07T17:04:26.000Z</updated>
<content type="html"><![CDATA[<p>[前情提要] 光阴似箭,日月如梭,最近几年,支持心率检测的设备愈发常见了,大家都在各种测空气测雪碧的,如火如荼,于是我也来凑一凑热闹。<strong>[0]</strong><br>这段时间,我完成了一个基于iOS的心率检测Demo,只要稳定地用指尖按住手机摄像头,它就能采集你的心率数据。Demo完成后,我对心率检测组件进行了封装,并提供了默认动画和音效,能够非常方便导入到其他项目中。在这篇博客里,我将向大家分享一下我完成心率检测的过程,以及,期间我遇到的种种困难。</p><a id="more"></a><h4 id="本文中涉及到的要点主要有:"><a href="#本文中涉及到的要点主要有:" class="headerlink" title="本文中涉及到的要点主要有:"></a>本文中涉及到的要点主要有:</h4><ul><li>AVCapture</li><li>Core Graphics</li><li>Delegate & Block</li><li>RGB -> HSV</li><li>带通滤波</li><li>基音标注算法(TP-Psola)</li><li>光电容积脉搏波描记法(PhotoPlethysmoGraphy, PPG)</li></ul><p>在开始之前,我先为大家展示一下最后成品的效果:</p><p><img src="/media/15231176397746/a.jpeg" alt="心率检测的ViewController"></p><p>上图展示的是心率检测过程中的主要界面。<br>在检测的过程中,应用能够实时捕捉心跳的波峰,计算相应的心率,并以Delegate或Block的形式回调,在界面上显示相应的动画和音效。</p><hr><p>##〇、剧情概览</p><p>好吧,😂其实上面的前情提要都是我瞎掰的,这个Demo是我来到公司的第一天接到的任务。刚接到任务的时候其实是有点懵逼的,原本以为刚入职两天可能都是要看看文档,或者拖拖控件,写写界面什么的,结果Xcode都还没装好,突然接到一个心率检测的任务,顿时压力就大起来了😨,赶紧拍拍屁股起来找资料。</p><p>心率检测的APP在我高三左右就有了,我清楚地记得当时,年少无知的我还误以为,大概又是哪个刁民闲着无聊恶搞的流氓应用,特地下载下来试了一下,没想到居然真的能测。。。<br><img src="/media/15231176397746/b.jpeg" alt="总有刁民想害朕"><br>当时就震惊地打开了某度查了这类应用的原理。所以现在找起资料来还是比较有方向性的。</p><p>花了一天的时间找资料,发现在手机心率检测方面,网上相关的东西还是比较少。不过各种资料参考下来,基本的实现思路已经有了。</p><blockquote><p><strong>任务清单</strong></p><ul><li>实现心率检测</li></ul></blockquote><hr><h2 id="一、整体思路"><a href="#一、整体思路" class="headerlink" title="一、整体思路"></a>一、整体思路</h2><h3 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h3><p>首先说一说用手机摄像头实现心率检测所用到的原理。<br>我们知道,现在市面上有非常多具备心率检测功能的可穿戴设备,比如各种手环以及各种Watch,其实从本质上讲,我们这次要用到的原理跟这些可穿戴设备所用到的原理并无二致,它们都是基于<strong>光电容积脉搏波描记法(PhotoPlethysmoGraphy, PPG)</strong>。</p><p><img src="/media/15231176397746/c.jpg" alt="iWatch的心率传感器发出的绿光"></p><p>PPG是追踪可见光(通常为绿光)在人体组织中的反射。它具备一个可见光<strong>光源</strong>来照射皮肤,再使用<strong>光电传感器</strong>采集被皮肤反射回来的光线。PPG有两种模式,透射式和<strong>反射式</strong>,像一般的手环手表这样,光源和传感器在同一侧的,就是反射式;而医院中常见的夹在指尖上的通常是透射式的,即光源和传感器在不同侧。<br>皮肤本身对光线的反射能力是相对稳定的,但是心脏泵血使得血管容积周期性地变化,导致反射光也呈现出<strong>周期性的波动值</strong>,特别是在指尖这种毛细血管非常丰富的部位,这种周期性的波动很容易被观察到。</p><blockquote><p>使用iPhone的系统相机就可以轻易地用肉眼观察到这种波动——在录像中打开闪光灯,然后用手指轻轻覆盖住摄像头,就能观察到满屏的红色图像会随着心跳产生一阵一阵的明暗变化,如下图(请忽略满屏的摩尔纹)。</p></blockquote><p><img src="/media/15231176397746/d.jpg" alt="直接用肉眼就能观察到相机图像的明暗变化"></p><p>至于,为什么可穿戴设备上用的光源大多数都是绿光,我们用手机闪光灯的白光会不会有问题。这主要是因为绿光在心率检测中产生的信噪比比较大,有利于心率的检测,用白光也是完全没问题的。详情可以移步知乎:<a href="https://www.zhihu.com/question/27391584" target="_blank" rel="noopener">各种智能穿戴的心率检测功能</a> 。我在这里就不细说了。</p><h3 id="我的思路"><a href="#我的思路" class="headerlink" title="我的思路"></a>我的思路</h3><p>我们已经知道我们需要用闪光灯和摄像头来充当PPG的光源和传感器,那么下面就来分析一下后续整体的方案。下面是我搜集完数据之后大致画出的一个流程图。</p><p><img src="/media/15231176397746/e.jpg" alt="整体思路"></p><ol><li>首先我们需要采集相机的数据,这一步可以使用AVCapture;</li><li>然后按照某种算法,对每一帧图像计算出一个相应的<strong>特征值</strong>并保存到数组中,算法可以考虑取<strong>红色分量</strong>或者转换为<strong><a href="https://zh.wikipedia.org/wiki/HSL和HSV色彩空间" target="_blank" rel="noopener">HSV</a></strong>再计算;</li><li>在得到一定量的数据后,我们对这个时间段内的数据进行<strong>预处理</strong>,譬如进行滤波,过滤掉一些噪声,可以参考一篇博客:<a href="http://blog.csdn.net/shengzhadon/article/details/46803401" target="_blank" rel="noopener">巴特沃斯滤波器</a>;</li><li>接下来,就可以进行心率计算,这一步可能涉及到一些数字信号处理的内容,例如<strong>波峰检测</strong>,信号<strong>频率</strong>计算,可以使用Accelerate.Framework的vDSP处理框架,Accelerate框架的用法可以参考:<a href="http://stackoverflow.com/questions/3398753/using-the-apple-fft-and-accelerate-framework" target="_blank" rel="noopener">StackOverFlow的一个回答</a>(最终我并没有使用,原因后面会提到);</li><li>最终就可以得到心率。</li></ol><hr><h2 id="二、初步实现"><a href="#二、初步实现" class="headerlink" title="二、初步实现"></a>二、初步实现</h2><p>有了大概的方案之后,我决定着手进行实现了。</p><h4 id="1)视频流采集"><a href="#1)视频流采集" class="headerlink" title="1)视频流采集"></a>1)视频流采集</h4><p>我们前面已经提到,我们要用AVCapture进行视频流的采集。在使用AVCapture的时候,需要先建立AVCaptureSession,相当于是一个传输流,用来连接数据的输入输出,然后分别建立输入和输出的连接。因此,为了更加直观,我先做了一个类似于相机的Demo,把AVCapture采集到的相机图像直接传输到一个Layer上。</p><ol><li><p><strong>创建AVCaptureSession</strong><br>AVCaptureSession的配置过程类似于一次数据库事务的提交。开始配置前必须调用<code>[_session beginConfiguration];</code>来开始配置;完成所有的配置工作后,再调用<code>[_session commitConfiguration];</code>来提交此次配置。<br>因此,整个配置过程大致是这样的:</p><pre><code>/** 建立输入输出流 */_session = [AVCaptureSession new];/** 开始配置AVCaptureSession */[_session beginConfiguration];/* * 配置session * (建立输入输出流) * ... *//** 提交配置,建立流 */[_session commitConfiguration];/** 开始传输数据流 */[_session startRunning]; </code></pre></li><li><p><strong>建立输入流From Camera</strong><br>要从相机建立输入流,就得先获取到照相机设备,并且对它进行相应的配置。这里对照相机的配置最关键的是要打开闪光灯常亮。此外,再设置一下白平衡、对焦等参数的锁定,来保证后续的检测过程中,不会因为相机的自动调整而导致特征值不稳定。</p><pre><code>/** 获取照相机设备并进行配置 */AVCaptureDevice *device = [self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];if ([device isTorchModeSupported:AVCaptureTorchModeOn]) { NSError *error = nil; /** 锁定设备以配置参数 */ [device lockForConfiguration:&error]; if (error) { return; } [device setTorchMode:AVCaptureTorchModeOn]; [device unlockForConfiguration];//解锁}</code></pre><p>需要注意的是,照相机Device的配置过程中,需要事先<strong>锁定</strong>它,锁定成功后才能进行配置。并且,在配置闪光灯等参数前,必须事先判断当前设备是否支持相应的闪光灯模式或其他功能,确保当前设备支持才能够进行设置。<br>此外,对于相机的配置,还有一点非常重要:<strong>记得调低闪光灯亮度!!</strong></p></li></ol><blockquote><p>长期打开闪光灯会使得电池发热,这对电池是一种伤害。在我调试的过程中,曾经无数次调着调着忘了闪光灯还没关,最后整只手机发热到烫手的程度才发现,直接进化成小米~ 所以,尽量将闪光灯的亮度降低,经过我的测试,即使闪关灯亮度开到最小也能够测得清晰的心率。</p></blockquote><p> 接下来就是利用配置好的device<strong>创建输入流:</strong></p><pre><code>/** 建立输入流 */NSError *error = nil;AVCaptureDeviceInput *deviceInput = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];if (error) { NSLog(@"DeviceInput error:%@", error.localizedDescription); return;}</code></pre><ol><li><p><strong>建立输出流 To AVCaptureVideoDataOutput</strong><br>建立输出流需要用到<code>AVCaptureVideoDataOutput</code>类。我们需要创建一个<code>AVCaptureVideoDataOutput</code>类并设置它的像素输出格式为32位的<strong>BGRA</strong>格式,这似乎是iPhone相机的默认格式<em>(经@熊皮皮提出,除了这种格式,还有两种YUV的格式)</em>。后续我们读取图像Buffer中的像素时,也是按照这个顺序(BGRA)去读取像素点的数据。设置中需要用一个<code>NSDictionary</code>来作为参数。<br>我们还要设置<code>AVCaptureVideoDataOutput</code>的代理,并创建一个新的线程(FIFO)来给输出流运行。</p><pre><code>/** 建立输出流 */AVCaptureVideoDataOutput *videoDataOutput = [AVCaptureVideoDataOutput new];NSNumber *BGRA32PixelFormat = [NSNumber numberWithInt:kCVPixelFormatType_32BGRA];NSDictionary *rgbOutputSetting;rgbOutputSetting = [NSDictionary dictionaryWithObject:BGRA32PixelFormat forKey:(id)kCVPixelBufferPixelFormatTypeKey];[videoDataOutput setVideoSettings:rgbOutputSetting]; // 设置像素输出格式[videoDataOutput setAlwaysDiscardsLateVideoFrames:YES]; // 抛弃延迟的帧dispatch_queue_t videoDataOutputQueue = dispatch_queue_create("VideoDataOutputQueue", DISPATCH_QUEUE_SERIAL);[videoDataOutput setSampleBufferDelegate:self queue:videoDataOutputQueue];</code></pre></li><li><p><strong>连接到AVCaptureSession</strong><br>建立完输入输出流,就要将它们和<code>AVCaptureSession</code>连接起来啦!<br>这里需要注意的是,必须先判断是否能够添加,再进行添加操作,如下所示。</p><pre><code>if ([_session canAddInput:deviceInput]) [_session addInput:deviceInput];if ([_session canAddOutput:videoDataOutput]) [_session addOutput:videoDataOutput];</code></pre></li><li><p><strong>实现代理协议的方法,获取视频帧</strong><br>上面的步骤中,我们将<code>self</code>设为<code>AVCaptureVideoDataOutput</code>的<code>delegate</code>,那么现在我们就要在self中实现<code>AVCaptureVideoDataOutputSampleBufferDelegate</code>的方法<code>xxx didOutputSampleBuffer xxx</code>,这样在视频帧到达的时候我们就能够在这个方法中获取到它。</p><pre><code>#pragma mark - AVCaptureVideoDataOutputSampleBufferDelegate & Algorithm- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { /** 读取图像Buffer */ CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); // // 我们可以在这里 // 计算这一帧的 // 特征值。。。 // /** 转成位图以便绘制到Layer上 */ CGImageRef quartzImage = CGBitmapContextCreateImage(context); /** 绘图到Layer上 */ id renderedImage = CFBridgingRelease(quartzImage); dispatch_async(dispatch_get_main_queue(), ^(void) { [CATransaction setDisableActions:YES]; [CATransaction begin]; _imageLayer.contents = renderedImage; [CATransaction commit]; });}</code></pre></li></ol><p>做到这里,我们已经获得了一个类似于相机的Demo,在屏幕上可以输出摄像头采集的画面了,接下来,我们就要在这个代理方法中对每一帧图像进行特征值的计算。</p><h4 id="2)采样(计算特征值)"><a href="#2)采样(计算特征值)" class="headerlink" title="2)采样(计算特征值)"></a>2)采样(计算特征值)</h4><p>采样过程中,最关键的就是如何将一幅图像转换为一个对应的特征值。<br>我先将所有像素点转换为一个像素点(RGB):</p><p><img src="/media/15231176397746/f.jpg" alt="累加合成一个像素点"></p><p>转换成一个像素点之后,我们只剩下RGB三个数值,事情就变简单得多。在设计采样的算法的过程中,我进行了许多种尝试。<br>我先试着简单地使用R、G、B分量中的其中一个直接作为信号输入,结果都不理想。</p><p><strong>- HSV色彩空间</strong><br>想到之前图形学的课上有介绍过<strong><a href="https://zh.wikipedia.org/wiki/HSL和HSV色彩空间" target="_blank" rel="noopener">HSV色彩空间</a></strong>,是将颜色表示为色相、饱和度、明度(Hue, Saturation, Value)三个数值。</p><p> <img src="/media/15231176397746/g.png" alt="HSV色彩空间\[5\]"><br>我想,既然肉眼都能观察到图像颜色的变化,而RGB又没有明显的反映,那HSV的三个维度中应该有某个维度是能够反映出它的变化的。我便试着转换为HSV,结果发现<strong>色相H</strong>随脉搏的变化很明显!于是,我就先确定用H值来作为特征值。</p><p>我简单地用Core Graphics直接在图像的Layer上画出H数值的折线:</p><p><img src="/media/15231176397746/h.png" alt="色相H随脉搏的变化"></p><h4 id="3)心率计算"><a href="#3)心率计算" class="headerlink" title="3)心率计算"></a>3)心率计算</h4><p>为了使得曲线更加直观,我对特征值稍做处理,又改变了一下横坐标的比例,得到如下截图。现在心率信号稳定以后,波峰已经比较明显了,我们开始进行心率的计算。</p><p><img src="/media/15231176397746/i.jpg" alt="缩放后,稳定的时候的心率信号"></p><p>最初,我想到的是利用<a href="http://stackoverflow.com/questions/3398753/using-the-apple-fft-and-accelerate-framework" target="_blank" rel="noopener">快速傅里叶变换(FFT)</a>对信号数组进行处理。FFT可以将时域的信号转换成<strong>频域</strong>的信号,也就得到了一段信号在各个频率上的分布,这样,我们就能通过判断占比最大的频率,就差不多能确定心率了。<br>但是可能由于我缺乏信号处理的相关知识,经过将近两天的研究,我还是看不懂跟高数课本一样的文档。。。<br>于是我决定先用<strong>暴力</strong>的方法算出心率,等能用的Demo出来之后,看看效果如何,再考虑研究算法的优化。</p><blockquote><p>通过上面的曲线,我们可以看出,在信号稳定的时候,波峰还是比较清晰的。因此我想,我可以设置一个<strong>阈值</strong>,进行<strong>波峰的检测</strong>,只要信号超过阈值,就判定该帧处于一个波峰。然后再设置一个<strong>状态机</strong>,完成波峰波谷之间的状态转换,就能检测出波峰了。<br>因为从AVCapture得到的图像帧数为<strong>30帧</strong>,也就是说,每一帧代表1/30s的时间。那么我只需要数一数从第一个波峰到最后一个波峰之间,经过了多少帧,检测到了多少波峰,那么,就能算出每个波峰间的周期,也就能算出<strong>心率</strong>了。</p></blockquote><p>这个想法非常简单,但是存在一个问题,那就是,<strong>阈值的设置</strong>。波峰的凸起程度并不是恒定的,有时明显,有时微弱。因此,一个固定的阈值肯定不能满足实际检测的需求。于是我想到我们可以根据心跳曲线波动的上下范围,来<strong>实时确定</strong>一个合适的阈值。我做了如下修改:</p><blockquote><p>每次进行心率计算的时候,先找出整个数组的极大和极小值,确定数据上下波动的范围。<br>然后,根据这个范围的一个百分比,来确定阈值。</p></blockquote><p>也就是说,一个特征值只有超过了整组数据的百分之多少,它才会被判定为波峰。<br>根据这个方法,我每隔一段时间对数据进行一遍检测,在Demo中实现了心率的计算,又对界面进行了简单的实现,大致的效果如下。</p><p><img src="/media/15231176397746/j.png" alt="缩放后,稳定的时候的心率信号"></p><p>使用的过程中还存在一定程度的误检率,不过总算是实现了心率检测~ 🎉🎉🎉</p><hr><h2 id="三、性能优化"><a href="#三、性能优化" class="headerlink" title="三、性能优化"></a>三、性能优化</h2><p>在我粗略实现了心率检测的功能后,Leader提出了对性能进行优化的要求,顺便向我普及了一波Instruments的用法(以前我一直没有用过🙊)。</p><p><strong>任务清单</strong></p><ul><li>性能优化</li><li>封装组件(delegate或block的形式);</li><li>提供两种默认动画;</li></ul><p>我用Instrument分析了心率检测过程中的CPU占用,发现占用率很高,维持在50%~60%左右。不过这在我的预料中,因为我的算法确实很暴力😂——每帧的图像是1920x1080尺寸的,在1/30秒内,要对这200多万个像素点进行遍历计算,还要转换成位图显示在layer上,隔一段时间还要计算一次心率。。。</p><p>我分析了CPU占用比较多的部分,归纳了几个可以考虑优化的方向</p><ul><li>降低采样范围</li><li>降低采样率</li><li>取消AV输出</li><li>降低分辨率</li><li>改进算法,去除冗余计算</li></ul><ol><li>降低采样范围<br>现在的采样算法是对所有的像素点进行一次采样,我想着是否能够缩小采样的范围,例如只对中间某块区域采样,但试验后我发现,只对某块区域采样会使得检测到的波峰变得模糊,说明个别区域的采样并不具有代表性。<br>接着我又想到了一个新的办法。我发现图像中,临近像素点的颜色差异很小,那么我可以跳跃着采样,每隔几列、每隔几行采样一次,这样一方面可以减少工作量,一方面对采样的效果的影响也可以减少。</li></ol><p><img src="/media/15231176397746/k.jpg" alt="跳跃着采样"></p><p> 采样的方式就像上图展示的一样,再设置一个常量用来调节每次跳跃的间距。这样一来,理论上,每次占用的时间就可以降低为原来的1/n^2,大大减少。经过几次尝试后,可以看到,采样算法所在的函数的CPU占用比例由原来的31%降低到了14%了。</p><blockquote><p>在分析CPU占用时,我发现在循环中对RGB分别累加时,第一个R的运算占用100倍以上的时间。开始时以为可能是Red分量数值较大,计算难度大,Leader建议我使用位运算,但是我改成位运算后,瓶颈依旧存在,弄得我十分困惑。后来我试着把RGB的计算顺序换一下,结果发现,瓶颈和R无关,不论RGB,只要谁在第一位,谁就会成为瓶颈。后来我想到,这应该是CPU和内存之间的数据传输造成的瓶颈,因为像素点都存在一块很大的内存块里,在取第一个数据的时候可能速度比较慢,然后后面取临近数据的时候可能就有Cache了,所以速度回提高两个数量级。</p></blockquote><ol><li>降低采样率<br>降低采样率就是将视频的帧数降低,我记得,不知道是香农还是谁,有一个定理,大概的意思就是说,采样率只要达到频率的两倍以上,就能检测出信号的频率。<br>(<strong>经<a href="http://www.jianshu.com/users/d67ddecd40fe/" target="_blank" rel="noopener">coderMoe</a>童鞋指出,此处正式名称为“奈奎斯特–香农采样定理”</strong>)<br>人的心跳上限一般是160/分钟,也就是不到3Hz,那理论上,我们的采样率只要达到6帧/秒以上,就能够计算出频率。<br>不过,由于我之前使用的算法还不是特别稳定,所以,当时我没有对采样率进行改变。</li></ol><ul><li><p>取消AV输出<br>之前我为了方便看效果,将采集到的视频图像输出到了界面上的一层Layer上,其实这个画面完全没必要显示出来。因此我去除了这部分的功能,这样一来,整体的CPU占用就降低到了33%以下。</p></li><li><p>降低分辨率<br>目前我们采集视频的大小是1920x1080,其实我们并不需要分辨率这么高。降低分辨率一方面可以减少需要计算的像素点,另一方面可以减少IO的时间。<br>在我将分辨率降低到640x480:</p><pre><code>if ([_session canSetSessionPreset:AVCaptureSessionPreset640x480]) { /** 降低图像采集的分辨率 */ [_session setSessionPreset:AVCaptureSessionPreset640x480];}</code></pre></li></ul><p>结果非常惊人,整体的CPU占用率直接降低到了<strong>5%</strong>左右!</p><ul><li>改进算法,去除冗余计算<br>最后,我对算法中一些冗余的计算进行了优化,不过,由于CPU占用已经降低到了5%左右,真正的瓶颈已经消除,所以这里的改进并没有很明显的变化。</li></ul><hr><h2 id="四、封装"><a href="#四、封装" class="headerlink" title="四、封装"></a>四、封装</h2><p>此前,我们已经完成了一个大致可用的心率监测Demo,但在此之前,我着重考虑的都是如何尽快实现心率检测的功能,对整体的结构和对象的封装都没有太多的考虑,简直把OC的面向对象用成了面向过程。<br>那么我们接下来的一个重要任务,就是对我们的心率检测进行<strong>封装</strong>,使它成为一个可复用的组件。</p><p><strong>任务清单</strong></p><ul><li>封装组件并提供合理接口(delegate或block的形式);</li><li>提供两种默认动画;</li></ul><h4 id="封装ViewController"><a href="#封装ViewController" class="headerlink" title="封装ViewController"></a>封装ViewController</h4><p>最开始的时候,我想到的是对ViewController进行封装,这样别人有需要心率检测的时候,就可以弹出一个心率监测的ViewController,上面带有一些检测过程中的动画效果,检测完成后自动dismiss,并且返回检测到的心率。<br>我在protocol中声明了三个接口:</p><pre><code>/** * 心率检测ViewController的代理协议 */@protocol MTHeartBeatsCaptureViewControllerDelegate <NSObject>@optional- (void)heartBeatsCaptureViewController:(MTHeartBeatsCaptureViewController *)captureVC didFinishCaptureHeartRate:(int)rate;- (void)heartBeatsCaptureViewControllerDidCancel:(MTHeartBeatsCaptureViewController *)captureVC;- (void)heartBeatsCaptureViewController:(MTHeartBeatsCaptureViewController *)captureVC DidFailWithError:(NSError *)error;@end</code></pre><p>我将三个方法都设为了optional的,因为我还在ViewController中设置了三个相应的Block供外部使用,分别对应三个方法。</p><pre><code>@property (nonatomic, copy)void(^didFinishCaptureHeartRateHandle)(int rate);@property (nonatomic, copy)void(^didCancelCaptureHeartRateHandle)();@property (nonatomic, copy)void(^didFailCaptureHeartRateHandle)(NSError *error);</code></pre><h4 id="封装心率检测类"><a href="#封装心率检测类" class="headerlink" title="封装心率检测类"></a>封装心率检测类</h4><p>对ViewController进行封装之后,我们可以看到,还是比较<strong>不合理</strong>的。这意味着别人只能使用我们封装起来的界面进行心率检测,如果使用组件的人有更好的交互方案,或者有特殊的逻辑需求,那他使用起来就会很不方便。因此,我们很有必要进行<strong>更深层次的封装</strong>。<br>接下来,我将会剥离出心率检测的类,进行封装。</p><p>首先,我一点点剥离出心率检测的关键代码,放进新的<code>MTHeartBeatsCapture</code>类中。剥离的差不多之后,就发现满屏的代码都是红色的Error😲,花了一个下午,才把项目恢复到能运行的状态。</p><p>我在心率检测类中设置了两个方法:<strong>启动和停止</strong>。使用起来很方便。</p><pre><code>/** 开始检测心率 */- (NSError *)start;/** 停止检测心率 */- (void)stop;</code></pre><p>然后,我重新设计了一个心率检测器的<strong>回调接口</strong>,依旧是delegate和block并存的。新的接口如下:</p><pre><code>/** * 心率检测器的代理协议; * 可以选择Delegate或者block来获得通知, * 因此protocol中所有方法均为可选方法 */@protocol MTHeartBeatsCaptureDelegate <NSObject>@optional/** 检测到一次波峰(跳动),可通过返回值选择是否停止检测 */- (BOOL)heartBeatsCapture:(MTHeartBeatsCapture *)capture heartBeatingWithRate:(int)rate;/** 失去稳定信号 */- (void)heartBeatsCaptureDidLost:(MTHeartBeatsCapture *)capture;/** 得到新的特征值(30帧/秒) */- (void)heartBeatsCaptureDataDidUpdata:(MTHeartBeatsCapture *)capture@end</code></pre><p>我在新的接口中加入了<code>heartBeatsCaptureDidLost:</code>,方便在特征值波动剧烈的时候进行回调,这样外部就能提醒用户姿势不对。而第三个方法,则是为了之后外部的动画view能够做出类似于心电图一样的动画效果,而对外传出数据。<br>我还移除了检测成功的回调<code>didFinishCaptureHeartRate:</code>,换成了<code>heartBeatingWithRate:</code>,把成功时机的判断交给了外部,当外部的开发人员认为检测的心率足够稳定了,就可以返回YES来停止检测。<br>此外,我还移除了遇到错误的回调<code>DidFailWithError:</code>,因为我发现,几乎所有可能遇到的错误,都是发生在开始前的准备阶段,因此,我改成了在<code>start</code>方法中返回<strong>错误信息</strong>,并且枚举出错误类型作为code,封装成<strong>NSError</strong>。</p><pre><code>typedef NS_OPTIONS(NSInteger, CaptureError) { CaptureErrorNoError = 0, /**< 没有错误 */ CaptureErrorNoAuthorization = 1 << 0, /**< 没有照相机权限 */ CaptureErrorNoCamera = 1 << 1, /**< 不支持照相机设备,很可能处于模拟器上 */ CaptureErrorCameraConnectFailed = 1 << 2, /**< 相机出错,无法连接到照相机 */ CaptureErrorCameraConfigFailed = 1 << 3, /**< 照相机配置失败,照相机可能被其他程序锁定 */ CaptureErrorTimeOut = 1 << 4, /**< 检测超时,此时应提醒用户正确放置手指 */ CaptureErrorSetupSessionFailed = 1 << 5, /**< 视频数据流建立失败 */};</code></pre><p>主要的工作完成后,Leader给我提了不少意见,主要还是封装上存在的一些问题,很多地方没有必要对外公开,应该尽可能地<strong>对外隐藏</strong>,接口也应该尽量地精简,没必要的功能要尽可能的去掉。特别是对外公开的一个特征值数组(NSMutableArray),对外应该不可变,这一点我一直没有考虑到。</p><h4 id="封装动画-amp-改进动画"><a href="#封装动画-amp-改进动画" class="headerlink" title="封装动画&改进动画"></a>封装动画&改进动画</h4><p>心率检测类封装完成后,我又剥离出显示心跳波形的部分,封装成一个<code>MTHeartBeatsWaveView</code>,使用的时候只要将动画View赋给<code>MTHeartBeatsCapture</code>作为delegate,该view上就能获取到特征值数据并进行显示。</p><p><strong>动画改进:</strong>在测试的过程中,我发现波形动画显示的<strong>波形</strong>不太理想,View的大小是初始化的时候就确定的,但是心跳波动的<strong>幅度变化</strong>是比较大的,有时候一马平川,堪比飞机场,有时候波澜壮阔,直接超出View的范围。<br>因此我对动画的显示做了一个改进:能够根据当前波形的<strong>范围</strong>,计算出合适的缩放比,对心跳曲线的Y坐标进行动态的<strong>缩放</strong>,使它的上下幅度适合当前的View。<br>这个改进大大提高了用户体验。</p><hr><h2 id="五、优化"><a href="#五、优化" class="headerlink" title="五、优化"></a>五、优化</h2><p>我们可以看到,先前得到的曲线已经能较好地反映出心脏的搏动,但是现在进行心率的计算还是存在一定的<strong>误检率</strong>。上图中展示的清晰的心跳曲线,实际上是比较<strong>理想的时候</strong>,测试中会发现,采样得到的数据经常存在较大的<strong>噪声和扰动</strong>,导致心率计算中经常会有波峰的误判。因此,我在以下两方面做了优化,来提高心率检测的准确度。</p><h4 id="1、在预处理环节进行滤波"><a href="#1、在预处理环节进行滤波" class="headerlink" title="1、在预处理环节进行滤波"></a>1、在预处理环节进行滤波</h4><p><img src="/media/15231176397746/l.jpg" alt="得到的曲线有时含有比较多的噪声"></p><p> 分析一下心率曲线里的噪声,我们会发现,噪声中含有一些<strong>高频噪声</strong>,这部分噪声可能是手指的细微抖动造成的,也可能是相机产生的一些噪点。因此,我找到了一个简易的实时的<strong>带通滤波器</strong>,对之前我们采样获得到的H值进行处理,滤除了一部分高频和低频的噪声。</p><p><img src="/media/15231176397746/m.jpg" alt="加入滤波器处理后的心率信号"></p><p>在经过滤波器的处理之后,我们得到的曲线就更加平滑啦。</p><h4 id="2、参考TP-Psola算法,排除伪波峰"><a href="#2、参考TP-Psola算法,排除伪波峰" class="headerlink" title="2、参考TP-Psola算法,排除伪波峰"></a>2、参考TP-Psola算法,排除伪波峰</h4><p>经过滤波器的处理之后,我们会发现,在每个心跳周期中,总会有一个小波峰,因为它不是真正的波峰,因此我称它为“<strong>伪波峰</strong>”,这个伪波峰非常明显,有时也会干扰到我们心率的检测,被算法误判为心跳波峰,导致心率直接<strong>翻倍</strong>。</p><p>这个伪波峰出现是因为,除了外部的噪声之外,心脏本身的跳动周期中也会出现许多的“<strong>杂波</strong>”。我们来看一次心跳的完整过程。</p><p><img src="/media/15231176397746/n.gif" alt="心电图波形产生过程的动画 \[1\]"></p><p>上图是一次心跳周期中,心脏的状态变化以及对应产生的波段。可以看到,在心脏收缩前后,人体也会有电信号刺激心脏舒张,这在心电图上会表现出若干次的波动。而血压也会有相应的变化,我们检测到的数据的波动就是这样形成的。</p><p><img src="/media/15231176397746/o.png" alt="正常心电周期 \[2\]"></p><p>因此,这个伪波峰的形成是无法避免的,现有的通过阈值来判断波峰的方法很容易被欺骗,还是要考虑算法的改进,因此我又想到了快速傅里叶变换。</p><p>由于我对信号处理知之甚少,我看了两天的快速傅里叶,还是没有进展。于是我请教了部门里的前辈们,大家非常热情,推荐了不少方案和资料。其中一位实验室音频处理的博伟学长,碰巧在新人入职培训时和我分到了同一组,我就趁着闲暇的时候请教了他一些相关的问题。他觉得心率的波形比较简单,没必要用快速傅里叶变换,并且向我推荐了<strong>基音检测</strong>算法。</p><p><img src="/media/15231176397746/p.jpg" alt="基音标注"></p><p>简单地说,这个算法会标注出可能的波峰,然后通过动态规划排除掉伪波峰,就能得到真正的波峰啦。我根据这个算法的思路,实现了一个简化版的伪波峰排除算法。经过改进后的心率检测,经测试准确度达到了和Apple watch差不多的程度。(自我感觉良好😂,求轻喷~~)</p><h4 id="实时波峰检测"><a href="#实时波峰检测" class="headerlink" title="实时波峰检测"></a>实时波峰检测</h4><p>我还希望提供一个实时的心跳动画,因此我还实现了一个实时的波峰检测。这样每次检测到一个波峰之后,就可以立刻通知delegate或者block,在界面上做出动画。</p><p><img src="/media/15231176397746/q.jpg" alt="心率检测的ViewController"></p><hr><h2 id="歇-后-语"><a href="#歇-后-语" class="headerlink" title="歇-后-语"></a>歇-后-语</h2><blockquote><p>由于这一章节是<strong>歇</strong>了一阵子之后才写的,因此我把它叫做——<strong>歇后语</strong>。</p></blockquote><p>这个心率检测的项目前后一共做了三个礼拜左右,虽然第一个Demo用了三四天就完成,但是后续的封装和优化却用了两个星期的时间,嗯,感触颇深。。。</p><p>从最开始的incredible,到最后的好意思说堪比Apple Watch,真的是一个很有成就感的过程。虽然期间遇到了不少困难,甚至有那么一两次觉得自己真的无解了,但到最后总能熬过去,山重水复疑无路,柳暗花明又一村。真的忍不住要念诗了,感觉很充实,很开心。</p><p>在做这个项目的过程中,我也得到了许多人的帮助。部门里的各位前辈、同事,在看到我的提问之后,非常热情地向我提供意见和资料。希望这篇博客会对大家有所帮助。谢谢大家~</p><hr><p>【更新于2016/8/10】</p><blockquote><p>经<a href="http://www.jianshu.com/users/d67ddecd40fe/" target="_blank" rel="noopener">coderMoe</a>童鞋指出,文中 [三、2.降低采样率] 提到的 “定理” 正式的名称为“<a href="http://www.bing.com/knows/search?q=采样定理&mkt=zh-cn" target="_blank" rel="noopener">奈奎斯特采样定理</a>”。</p></blockquote><p>感谢这段时间以来,大家的鼓励和支持,前阵子我写这篇文章的时候,是万万没有想到会得到这么多人的关注的,实在是受宠若惊。有很多人详细地阅读了这篇博客,并且提出了重要的<strong>意见</strong>,甚至还有几位客官打赏了我(但是简书取现要满100RMB才行,所以目前我还无法享用这笔增肥基金🙊哈哈),真的很感谢你们。</p><p>我当时写这篇博客也花了不少时间,只怪我语文没学好,在言辞表达上、逻辑结构上,没能做得更好,大家如果有什么意见建议、或者不同的见解,希望能<strong>不吝赐教</strong>~~大家的关注和交流会让我更有动力分享博客,要知道,写作对我这种工科生而言,真的是,<strong>“体力活”</strong>。😂</p><p>有想要进一步关注我的朋友,可以收藏一下我正在搭建的<strong>博客</strong>,域名正在备案中,不过博客系统是已经搭起来了,有兴趣的朋友请移步:<a href="http://www.punmy.cn/" target="_blank" rel="noopener">punmy.cn</a>😋</p><p>另外,关于许多朋友非常关心的<strong>开源</strong>的问题,这两天上班比较忙,但是我会在近期确定是否开源,届时会通过简书更新,<strong>感谢关注</strong>!</p><hr><p>【更新于2016/8/18】<br>感谢大家的厚爱,收到Leader的回复,这个项目暂时不开源,不好意思。</p><p>但是大家如果有什么问题,欢迎继续和我探讨!😊</p><hr><p>【更新于2016/8/19】</p><p>另外,有朋友指出,iPhone相机支持的原始数据格式有三种,一种是文中提到的BGRA,另两种似乎是YUV的格式,我对这方面不太了解,感谢提出,详情请看文档。</p><hr><p>【更新于2018/4/8】<br>前段时间重新在 Github 上搭建了自己的博客,最近才有空把之前简书上的博客搬了过来,想开始维护自己的博客了。</p><hr><h2 id="The-End"><a href="#The-End" class="headerlink" title="The End."></a>The End.</h2><hr><h2 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h2><ul><li>光电容积脉搏波描记法”<br><a href="https://en.wikipedia.org/wiki/Photoplethysmogram" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Photoplethysmogram</a></li><li>滤波:<a href="http://blog.csdn.net/shengzhadon/article/details/46803401" target="_blank" rel="noopener">http://blog.csdn.net/shengzhadon/article/details/46803401</a> </li><li>Accelerate.framework:DSP处理框架(vDSP):<br><a href="http://stackoverflow.com/questions/3398753/using-the-apple-fft-and-accelerate-framework" target="_blank" rel="noopener">http://stackoverflow.com/questions/3398753/using-the-apple-fft-and-accelerate-framework</a> </li><li>core Graphics画图:<a href="http://www.mamicode.com/info-detail-841887.html" target="_blank" rel="noopener">http://www.mamicode.com/info-detail-841887.html</a> </li><li>心率检测论文:<a href="http://www.doc88.com/p-0307201762779.html" target="_blank" rel="noopener">http://www.doc88.com/p-0307201762779.html</a></li><li>通过脸部识别心率:<a href="http://people.csail.mit.edu/mrub/vidmag/" target="_blank" rel="noopener">http://people.csail.mit.edu/mrub/vidmag/</a> </li></ul><h2 id="外部引用"><a href="#外部引用" class="headerlink" title="外部引用"></a>外部引用</h2><blockquote><p><em>[0]: 写出“前情提要”的时候,脑子里蹦出的是:previously on marvel agents of shield😂</em></p></blockquote><blockquote><p><em>[1]: 引用自维基百科,由Kalumet - selbst erstellt = 自己的作品,<a href="http://creativecommons.org/licenses/by-sa/3.0/" title="Creative Commons Attribution-Share Alike 3.0" target="_blank" rel="noopener">CC BY-SA 3.0</a>,<a href="https://commons.wiki/media.org/w/index.php?curid=438152" target="_blank" rel="noopener">https://commons.wiki/media.org/w/index.php?curid=438152</a></em></p></blockquote><blockquote><p><em>[2]: 引用自维基百科,由Derivative: Hazmat2Original: Hank van Helvete - 此档案源起于以下档案或由以下档案加以编辑而成: EKG Complex en.svg,<a href="http://creativecommons.org/licenses/by-sa/3.0" title="Creative Commons Attribution-Share Alike 3.0" target="_blank" rel="noopener">CC BY-SA 3.0</a>,<a href="https://commons.wiki/media.org/w/index.php?curid=31447770" target="_blank" rel="noopener">https://commons.wiki/media.org/w/index.php?curid=31447770</a></em></p></blockquote><blockquote><p><em>[5]:引用自维基百科,由(3ucky(3all - Uploaded to en:File:HSV cone.png first (see associated log) by (3ucky(3all; then transfered to Commons by Moongateclimber.,<a href="http://creativecommons.org/licenses/by-sa/3.0" title="Creative Commons Attribution-Share Alike 3.0" target="_blank" rel="noopener">CC BY-SA 3.0</a>,<a href="https://commons.wiki/media.org/w/index.php?curid=943857" target="_blank" rel="noopener">https://commons.wiki/media.org/w/index.php?curid=943857</a></em></p></blockquote>]]></content>
<summary type="html">
这是一个基于iOS的心率检测Demo,只要稳定地用指尖按住手机摄像头,它就能采集你的心率数据。Demo完成后,我对心率检测组件进行了封装,并提供了默认动画和音效,能够非常方便导入到其他项目中。在这篇博客里,我将向大家分享一下我完成心率检测的过程,以及,期间我遇到的种种困难。
</summary>
<category term="Code" scheme="http://punmy.cn/categories/Code/"/>
<category term="iOS" scheme="http://punmy.cn/tags/iOS/"/>
<category term="Camera" scheme="http://punmy.cn/tags/Camera/"/>
<category term="Article" scheme="http://punmy.cn/tags/Article/"/>
</entry>
</feed>