-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
585 lines (341 loc) · 738 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
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Hexo</title>
<link href="http://example.com/atom.xml" rel="self"/>
<link href="http://example.com/"/>
<updated>2020-06-27T16:00:00.000Z</updated>
<id>http://example.com/</id>
<author>
<name>JonyFang</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>WWDC20 10146 - App Clips 的配置及链接处理</title>
<link href="http://example.com/2020/06/28/2020-06-28-configure-and-link-appClips/"/>
<id>http://example.com/2020/06/28/2020-06-28-configure-and-link-appClips/</id>
<published>2020-06-27T16:00:00.000Z</published>
<updated>2020-06-27T16:00:00.000Z</updated>
<content type="html"><![CDATA[<blockquote><p>视频链接: <ahref="https://developer.apple.com/videos/play/wwdc2020/10146/">WWDC 2020- Configure and link your app clips</a>。</p></blockquote><p>本篇介绍了 <code>App Clips</code> 链接处理所需要知道的所有内容。<code>App Clips</code>通过最简化的方式,为你的用户提供了一个体验应用程序的入口。当你的用户需要App 的具体功能来处理某项操作时,<code>App Clips</code> 会通过<code>deep-linked</code> 的方式无缝将 App的具体模块呈现给用户。本篇会介绍 <code>App Clip</code>内链接的处理和配置链接所需的操作。</p><span id="more"></span><h2 id="前言">1.前言</h2><p>本篇内容结构: <imgsrc="https://images.xiaozhuanlan.com/photo/2020/975ffdd236dffaf2177a32dd3ee982c3.png" /></p><p>首先,让我们通过一个示例,来看下如何通过一个 <code>App Clip</code>来完成一个具体的功能处理。</p><h2 id="app-clip-应用示例---订购冰沙">2.App Clip 应用示例 -订购冰沙🍧</h2><p>[00:49] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/1ac178171456fc4dd673da8fb72929fe.png" />想象着你现在在经过一个冰沙摊。你想要来一份冷冰沙。这时,你发现了一个带有标语的<code>NFC</code> 标签,上面写着“贴近此处订购”。你按照提示将手机贴近了<code>NFC</code> 标签,之后在你的 iPhone屏幕的底部会弹出一个卡片,上面写着购买沙冰的一些简短说明,而这个卡片即是我们要介绍的<code>App Clip</code>。当你按照卡片的提示,点击了上面的“打开”按钮后,将在手机上启动<code>App Clip</code>的订购处理,这个处理会将你直接带到冰沙的订购页面。之后,你通过 Apple Pay完成支付,成功买到了冰沙。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/b4002385b34f7fd66286bb75d14c6b97.png" />上述购买冰沙的这一过程,即发生了购买的关联处理。我们从程序的角度来看下这一过程是怎样的。NFC标签实际上是一个编码的<code>URL</code>,对应注册了一个 <code>App Clip</code>体验,关于如何注册将在后面部分进行详细展开。<code>NFC</code> 触发弹出<code>App Clip</code>,当你点击 <code>App Clip</code>上的“打开”按钮后,<code>App Clip</code> 将启动带有通过<code>NSUserActivity</code> 传递的URL,之后 <code>App Clip</code>将你直接带到订购页面。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/d8df3036e23df847f422195e47b1fca6.png" /></p><p>除了 <code>NFC</code>,活动链接也可以出现在其他地方。你的<code>URL</code>,可以编码在物理标签中;也可以与地图上的实际位置相关联。下面,让我们逐一介绍这些链接的处理方法。</p><h2 id="链接方式">3. 链接方式</h2><p>[02:03] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/7865000791ede1dcbaa508b15b36c68a.png" />正如在前面的示例中提到的那样,<code>NFC</code> 标签可以编码入一个<code>App Clip URL</code>,用户可以通过手机放到 <code>NFC</code>上来打开 <code>App Clip</code>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/05da30dda931d78b9af6bb8cda85904c.png" /><code>App Clip URL</code>也可以编码入一个二维码中,人们可以通过扫描二维码来触发<code>App Clip</code>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/f047608a83859953345412dbca680899.png" /><imgsrc="https://images.xiaozhuanlan.com/photo/2020/e823d9a130595e5d7db728b94b240fea.png" /><code>App Clip</code>可以显示在已注册商家的地图位置卡上,也可以显示在<code>Siri</code> 附近建议中。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/0bc3d18ede6dfd4b7712b8aae3e9a8f1.png" />如果你的网页配置了 <code>App Clip</code>对应的智能应用横幅,则你也可以从 Safari 打开<code>App Clip</code>。用户可以通过点击该横幅中的打开按钮以打开<code>App Clip</code>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/549b4d655f39f233862fcc5d61d92d42.png" />当用户在 <code>Messages app</code> 中发送该站点的 <code>URL</code>时,它会以特殊的 <code>App Clip</code>链接气泡显示,该气泡让用户可以选择在 <code>App Clip</code> 或在 Safari中打开链接。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/2ef221227c0b2d1e6e8203a3533d8db4.png" />此外,为了给大家带来更好的体验,苹果将于今年晚些时候发布新的<code>App Clip</code> 码,这是让你的用户发现你的 <code>App Clip</code>的最佳方式。它在视觉上是美观而独特的,因此当用户看到它时,便知道有一个<code>App Clip</code> 在等待他们去使用。每个 <code>App Clip</code>码都对应编码了一个<code>URL</code>。苹果将于今年晚些时候发布可以创建这些独特<code>App Clip</code> 码的工具。</p><p>现在我们已经知道了可以让用户进入到你的 <code>App Clip</code>的不同方法。接下来,让我们逐步了解 <code>App Clip</code>开发人员在设置链接体验过程中所需要做的步骤。</p><h2 id="链接到你的-app-clip">4. 链接到你的 App Clip</h2><p>[03:38] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/d54f65449e68143aee00bb35ec0496b2.png" />首先,你必须对 <code>Web 服务器</code>和<code>App Clip 项目</code>进行更改。以让这些链接可以由<code>App Clip</code> 处理。接下来,你必须配置 <code>App Clip</code>卡片,该卡片用于向用户介绍 <code>App Clip</code> 的信息,也是<code>App Clip</code> 体验的一部分。你可以在 App Store Connect上设置默认和高级 <code>App Clip</code>体验。下面,让我们配置一个智能应用横幅,以在网页上显示你的<code>App Clip</code>。如果你可以通过更好和更简化的<code>App Clip</code>体验来交付网页内容,可以考虑添加此标语,以此来为你的用户提供一种可以从该网页访问<code>App Clip</code> 的方法。首先,让我们开始配置<code>Web服务器</code> 和 <code>App Clip</code> 以进行链接处理。</p><h3 id="配置web服务器和-app-clip-以进行链接处理">4.1 配置Web服务器和 AppClip 以进行链接处理</h3><p>[04:28] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/2300c85c4ee85c70b959e4fe521021e8.png" />你的网站和你的 <code>App Clip</code> 之间的关联必须经过验证,以便<code>App Clip</code> 能够显示内容来代替网站的<code>URL</code>。为了将你的 <code>App Clip</code>与服务器安全地关联,你将需要 Web 服务器上的<code>apple-app-site-association</code> 文件,以及 <code>App Clip</code>上适当的关联域权限。之后,你必须更新 <code>App Clip</code>中的代码以处理传入 <code>NSUserActivity</code> 的链接。首先,让我们更新Web 服务器上的 <code>apple-app-site-association</code> 文件。</p><h4 id="更新-apple-app-site-association-文件">4.1.1 更新apple-app-site-association 文件</h4><p>[05:06] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/8c73058bcf00f33456a357544e7a68d2.png" />该文件位于服务器的根文件夹中的 <code>./well-known</code>的子目录中。如果你之前已经为应用程序设置了<code>通用链接(Universal Links)</code>,则可能已经在服务器上设置了该文件。根字典已经具有其他条目,例如Web 凭据和应用程序链接。要在此文件中声明新的 <code>App Clip</code>关联,请在根字典中添加另一个项,其键为 <code>“appclips”</code>,并且该值包含一个字典,该字典包含单个 <code>apps</code>键,该键设置为包含 <code>App Clip</code> 的应用标识符的数组。</p><h4 id="添加关联的域权限">4.1.2 添加关联的域权限</h4><p>[05:45] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/a0b2421f7505c719759b69b712f45d1b.png" />接下来,让我们更新 <code>App Clip</code> 项目以添加关联的域权利。在Xcode 中,进入你的项目设置并添加 <code>Associated Domains</code>功能。在 <code>Domains</code> 下,添加一个新的字符串<code>appclips:</code>。现在,你的网站和 <code>App Clip</code>均已设置了相关的域,让我们添加代码来处理<code>NSUserActivity</code>,其中包含传递到你的<code>App Clip</code>中的 URL。</p><h4 id="处理-nsuseractivity">4.1.3 处理 NSUserActivity</h4><p>[06:15] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/db0a08bea2ecd894f6a085b8a22fde54.png" />如果你的 <code>App Clip</code> 采用了新的 <code>SwiftUI</code>应用程序生命周期,则可以通过上图中的方法为网络浏览用户活动添加处理程序。在该方法中,你可以从<code>NSUserActivity</code> 获取 <code>webpageURL</code>属性。然后,你可以解析该 URL并将用户定向到链接的内容。请记住,当用户升级或下载了主 App以后,<code>App Clip</code> 会直接打开主App。因此,请确保你的应用程序也具有类似的代码来处理<code>通用链接(Universal Links)</code>的网址。<imgsrc="https://images.xiaozhuanlan.com/photo/2020/b1692df0d3952c3f2575b275d8ec1063.png" />如果你的 <code>App Clip</code> 使用 <code>UIKit SceneDelegate</code>生命周期,则上图是一些类似的处理代码,用于处理 <code>UIScene</code>委托中传入的用户活动。要了解有关如何设置关联域和处理<code>NSUserActivities</code> 的信息,请参阅会话:<ahref="https://developer.apple.com/videos/play/wwdc2020/10098">What's Newin Universal Link</a>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/a72153f06b33e72b892ac3ff3c4529f2.png" />如果你需要在 Xcode 中调试 <code>App Clip</code> 中 URL处理相关的代码,可以指定要传递到 <code>App Clip</code> 的测试 URL。在Xcode 中打开 <code>Scheme</code> 编辑器。选择 <code>Arguments</code>选项卡。在 <code>Environment Variables</code> 下,指定<code>_XCAppClipURL</code> 变量。现在,当你从 Xcode 运行你的<code>App Clip</code> 时,它将使用此 URL 启动。现在,我们已经完成了 Web服务器和 <code>App Clip</code> 的配置,接下来,我们来配置<code>App Clip</code> 体验。</p><h3 id="在-app-store-connect-上配置-app-clip-体验">4.2 在 App StoreConnect 上配置 App Clip 体验</h3><p>[07:51] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/7dd3b58d209c31a74110da1030a52c8f.png" />每次 <code>App Clip</code> 体验都是从用户触发看到的<code>App Clip</code> 卡片(App Clip 链接)开始的。它展示了有关<code>App Clip</code> 的信息,并征得用户的同意以打开它。</p><h4 id="app-clip-设计规则">4.2.1 App Clip 设计规则</h4><p>[08:04] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/4b70355c3b33b8df3899305dcd2eab08.png" />当你在提交用于配置 <code>App Clip</code>卡的元数据时,请遵循标题和副标题长度上的这些要求,以实现卡片的最佳布局。为了满足所有设备上的最佳用户体验,还有图片大小,宽高比和格式上的要求。你所选择的图片需要遵循此<code>App Clip</code> 操作所提供的准则。现在,让我们在 App Store Connect上设置 <code>App Clip</code> 卡片。</p><h4 id="在-app-store-connect-上设置-app-clip-卡片">4.2.2 在 App StoreConnect 上设置 App Clip 卡片</h4><p>[08:34] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/fc27237382cf79f88f95b8b852d727c0.png" />在将包含你的应用程序和你的 <code>App Clip</code> 的构建交付给 App StoreConnect 之后,<code>App Clip</code> 会在 App Store Connect上进行显示,会看到一个新的<code>“App Clip配置”</code>模块。你可以在此处开始设置默认和高级的<code>App Clip</code> 体验。默认的 <code>App Clip</code>体验的元数据包括活动卡的宣传图,文案介绍和相关的交互操作。你可以从中选择预定义的操作列表。此元数据将在Safari 中的智能应用横幅中、“消息”中的 App Clip 链接气泡中以及在显示的App Clip 卡片中进行使用。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/0f281c9df07418bce39d4dcd17953f26.png" />如果你希望不仅可以从 Safari 和消息中访问<code>App Clip</code>,你可以通过单击此处的“开始”按钮来进行高级<code>App Clip</code> 体验的设置。每种高级 <code>App Clip</code>体验均绑定到可以在物理标签(例如 NFC 标签或二维码)中编码的URL,因此可以从这些物理调用方法中启动 <code>App Clip</code>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/14c48130e492dbe27f6c842b0f96e24d.png" />确认进行高级设置后,你将到达此页面,你可以在其中指定高级<code>App Clip</code> 体验的 URL。对于同一个<code>App Clip</code>,你可以设置多个高级 <code>App Clip</code>体验,每种体验都有不同的URL。后面会介绍一些有关<code>多个 Clip 体验</code>的示例。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/ca1d8c6778cf8fd896859ab77e6d9218.png" />进入下一页后,你可以设置图片,标题,副标题,并为 <code>App Clip</code>卡选择一个触发事件来获得这种体验。你也可以选择将此 <code>App Clip</code>体验与实际位置相关联。现在,让我们看一下高级 <code>App Clip</code>体验的一些用例以及为这些体验注册 URL 的最佳实践。</p><h4 id="多种-app-clip-的体验">4.2.3 多种 App Clip 的体验</h4><p>[10:28] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/79c80faf77406686f1b279b234188fac.png" /><code>单个 Clip</code>可以处理不同类型的任务,因此可以为<code>同一个 Clip</code> 自定义不同的<code>App Clip</code> 体验。例如,这家餐厅的 <code>App Clip</code>为客户提供两种类型的体验。一种是美食订购体验,用户可以通过<code>Clip</code> 前往查看菜单并下单;另一种是餐桌预订体验,客人可以通过<code>Clip</code> 前往预定餐桌。在这种情况下,可以通过设置两个高级<code>App Clip</code> 体验来满足需求。一个用于<code>melamela.example/order</code>,其中图片和副标题可以分别是餐厅的菜单图片和菜单描述,方便菜单查看和下单;另一个用于<code>melamela.example/reservation</code>,其中图片和副标题可以分别是对应餐桌的图片和文字描述,方便餐桌预定。</p><h4 id="最佳实践">4.2.4 最佳实践</h4><h5 id="指定-app-clip-的体验-url">4.2.4.1 指定 App Clip 的体验 URL</h5><p>[11:23]</p><p>关于 URL 映射到 <code>App Clip</code>的方式要记住的一件事是,它是基于最特定的前缀与已注册的<code>App Clip</code> 体验 URL的匹配。这意味着你无需为企业注册每个可能的<code>App Clip URL</code>。但是请记住,即使你注册的体验 URL仅用作前缀,你的 <code>App Clip</code> 也必须能够处理使用确切的注册 URL启动的情况。在通过“Siri附近的建议”和“地图”调用你的 <code>App Clip</code>时,可能会发生这种情况。</p><h5 id="示例-自行车租赁">4.2.4.2 示例-自行车租赁</h5><p>[12:00] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/6b65754ee3adadac41208ad57d254558.png" /><imgsrc="https://images.xiaozhuanlan.com/photo/2020/fcb1b3019740906051c6e683e122518a.png" />这个示例,我们将介绍如何通过注册一个 <code>App Clip 体验 URL</code>来与多个调用 URL一起使用。自行车商店有在线自行车租赁系统。它有成百上千的自行车出租,由ID 编号标识。预订这些自行车的 URL 将在查询字符串参数中指定该ID。幸运的是,由于这些 URL是基于前缀匹配进行匹配的,因此该自行车商店无需为每个 URL 预先注册<code>App Clip</code> 体验。只需注册一个<code>App Clip 体验 URL</code>:<code>https://bikesrental.example/rent</code>。这足以为所有具有该前缀和不同查询字符串参数的所有自行车 URL 提供<code>App Clip 体验</code>。</p><h5 id="示例-咖啡店">4.2.4.3 示例-咖啡店</h5><p>[12:50] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/930e2fc4114d8251fdf9745dcd453e75.png" /><imgsrc="https://images.xiaozhuanlan.com/photo/2020/5f0c02274737b552c2246a89fda8b5ec.png" /><imgsrc="https://images.xiaozhuanlan.com/photo/2020/228e1ebccd91c6224af9626b5be9a95a.png" /><imgsrc="https://images.xiaozhuanlan.com/photo/2020/4ac7b286e6b7447da57fd200aa4dcd47.png" />这是另一个示例,用于说明选择 URL 进行注册以获取高级<code>App Clip</code>的策略。在这个示例中,咖啡馆是一个大型连锁店,拥有多个地点,每个地点基本上都为其客户提供相似的体验。由于所有位置的URL 格式都统一,且均以 <code>https://brighteggcafe.example/store/</code>开头,因此我们只需为该 URL 前缀注册 App Clip体验即可。当客户点击指向其任何商店的链接时,他们将获得相同的<code>App Clip</code>卡片。但是,假设咖啡店希望为其<code>库比蒂诺(Cupertino)</code>旗舰店提供更特别的<code>App Clip</code>体验。要解决此问题,你还可以使用不同的宣传图和描述文本为特定的<code>App Clip 体验</code>注册 Cupertino 商店URL。这里的主要要点是,你可以注册一个更短,更通用的 URL前缀,以覆盖大多数情况,并仅在需要提供不同的<code>App Clip 体验</code>时才注册一个更特定的 URL。</p><p>有关在 App Store Connect 上设置默认和高级<code>App Clip 体验</code>的详细信息,请参阅会话: <ahref="https://developer.apple.com/videos/play/wwdc2020/10651/">What'sNew in App Store Connect</a> 。有关<code>App Clip 卡片</code>设计的最佳实践,请参阅会话:<ahref="https://developer.apple.com/videos/play/wwdc2020/10172">DesignGreat App Clips</a> 。</p><h3 id="配置智能应用横幅以打开-app-clip">4.3 配置智能应用横幅以打开 AppClip</h3><p>[14:17] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/df37ff98b54a4fb58e47c475aee464a6.png" />现在到了最后一步,通过处理链接来触发你的<code>App Clip</code>。可以通过设置智能应用横幅,以打开你的<code>App Clip</code>。当发送出配置了此标语的网页 URL时,接收人可以通过智能应用横幅从 Safari 或“消息”中的网页打开<code>App Clip</code>。如果你之前已经为应用设置了智能应用横幅,应该已经熟悉了添加到网页HTML 中的 <code>apple-itunes-app</code>的元标记,是用来指定应用的唯一标识符。要为你的 <code>App Clip</code>配置此横幅,请将 <code>app-clip-bundle-id</code> 内容属性设置为<code>App Clip</code> 的捆绑包标识符。你还应该继续设置<code>app-id</code> 属性,这样对于使用 iOS 14之前系统的用户,将保持之前的网页形式。Safari在显示以下内容之前,将验证网站和 <code>App Clip</code>之间的域关联情况。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/4931abeea062fe744868952da5fa5dc4.png" />默认情况下,当用户点击智能应用横幅的“打开”按钮时。他们将看到为此<code>App Clip</code> 配置的默认<code>App Clip 卡片</code>。但是,如果使用高级<code>App Clip 体验</code>注册此URL,则可以自定义体验的元数据,以便用户可以在横幅中看到更具描述性的标题,并获得针对该<code>App Clip</code> 执行的任务量身定制的<code>App Clip卡片</code>。</p><p>为了演示我们上述内容所谈论到的 <code>App Clip</code>的链接时涉及的内容,你可以通过下方的视频链接快进到<strong><code>15分48秒</code></strong> 查看完整的演示视频。</p><p>视频链接:<ahref="https://developer.apple.com/videos/play/wwdc2020/10146/">https://developer.apple.com/videos/play/wwdc2020/10146/</a></p><h2 id="在-testflight-中为你的-app-clip-添加测试调用点">5. 在 TestFlight中为你的 App Clip 添加测试调用点</h2><p>[21:22] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/a953d1f94901d1094bc8fdfee5275d5b.png" />此外,还需要简要介绍下如何对 <code>App Clip</code> 进行 beta测试。在你将包含应用程序和 <code>App Clip</code> 的构建交付给 App StoreConnect之后,你可以在 TestFlight 中找到一个新的<code>App Clip 模块</code>,该模块可以用于为你的 <code>App Clip</code>添加测试调用点,以便 Beta 测试人员可以测试待开放的不同<code>App Clip 体验</code>的 URL。 <imgsrc="https://images.xiaozhuanlan.com/photo/2020/8a2c348d025213e731e0918baac68fe3.png" />单击 <code>Add App Clip Invocation</code>(添加AppClip调用),然后设置你希望 Beta 测试人员试用的<code>App Clip 体验</code>的标题和 URL。有关在 App Store Connect中测试和提交 <code>App Clip</code> 的更多信息,请参阅会话:<ahref="https://developer.apple.com/videos/play/wwdc2020/10651/">What'sNew in App Store Connect</a>。</p><h2 id="总结">6. 总结</h2><p>[22:05] <imgsrc="https://images.xiaozhuanlan.com/photo/2020/9c664627b6da57d20b6f16ca62b37dbb.png" />在本篇的内容里,我们已经向你展示了以下内容:</p><ol type="1"><li>如何通过为新的 <code>App Clip</code> 服务类型设置关联的域并在<code>App Clip</code> 中处理网络浏览 <code>NSUserActivity</code>来处理到你的 <code>App Clip</code> 中的链接。</li><li>如何在 App Store Connect 上配置默认和高级<code>App Clip 体验</code>,包括有关注册<code>App Clip 体验</code>时使用哪些 URL 的最佳实践;</li><li>如何设置智能应用横幅,以在网页上打开 <code>App Clip</code>;</li><li>最后是如何在 TestFlight 中测试 <code>App Clip</code> 的新功能。</li></ol><p>感谢您的阅读,也期待您带来精彩的 App Clip~</p><blockquote><p>相关内容: - <ahref="https://developer.apple.com/videos/play/wwdc2020/10118">WWDC 2020- Create app clips for other businesses</a> - <ahref="https://developer.apple.com/videos/play/wwdc2020/10172">WWDC 2020- Design great app clips</a> - <ahref="https://developer.apple.com/videos/play/wwdc2020/10174">WWDC 2020- Explore app clips</a> - <ahref="https://developer.apple.com/videos/play/wwdc2020/10120">WWDC 2020- Streamline your app clip</a> - <ahref="https://developer.apple.com/videos/play/wwdc2020/10651">WWDC 2020- What's new in App Store Connect</a> - <ahref="https://developer.apple.com/videos/play/wwdc2020/10098">WWDC 2020- What's new in Universal Links</a> - <ahref="https://developer.apple.com/videos/play/wwdc2019/717">WWDC 2019 -What's New in Universal Links</a></p></blockquote>]]></content>
<summary type="html"><blockquote>
<p>视频链接: <a
href="https://developer.apple.com/videos/play/wwdc2020/10146/">WWDC 2020
- Configure and link your app clips</a>。</p>
</blockquote>
<p>本篇介绍了 <code>App Clips</code> 链接处理所需要知道的所有内容。
<code>App Clips</code>通过最简化的方式,为你的用户提供了一个体验应用程序的入口。当你的用户需要
App 的具体功能来处理某项操作时,<code>App Clips</code> 会通过
<code>deep-linked</code> 的方式无缝将 App
的具体模块呈现给用户。本篇会介绍 <code>App Clip</code>
内链接的处理和配置链接所需的操作。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="WWDC" scheme="http://example.com/categories/WWDC/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="WWDC" scheme="http://example.com/tags/WWDC/"/>
<category term="App Clips" scheme="http://example.com/tags/App-Clips/"/>
</entry>
<entry>
<title>iOS 内存相关梳理</title>
<link href="http://example.com/2020/04/08/2020-04-08-about-ram/"/>
<id>http://example.com/2020/04/08/2020-04-08-about-ram/</id>
<published>2020-04-07T16:00:00.000Z</published>
<updated>2020-04-07T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>iOS 的内核是 XNU,XNU 是 Darwin 的一部分,而 Darwin 又是基于 FreeBSD和 NetBSD 开发,集成了 Mach 微内核,BSD 是基于 UNIX。虽然 Linux 也是基于UNIX,但 Darwin 和 Linux 没有直接继承的关系。内核 Darwin 是 C写的,中层框架和库时 C 和 Objective-C 写的。</p><p>本文先从一般桌面操作系统的内存机制入手;接着从 iOS 系统层进行分析 iOS的内存机制及 iOS 系统运行时的内存占用情况;最后到 iOS 中单个 App的内存管理。</p><span id="more"></span><h2 id="一般操作系统的内存机制">一般操作系统的内存机制</h2><p>在分析 iOS内存机制前,先看下一般操作系统(这里的一般操作系统指桌面操作系统)的内存机制是怎样的。</p><h3 id="冯诺伊曼结构">冯·诺伊曼结构</h3><p>冯·诺伊曼结构(Von Neumannarchitecture),也称<strong>冯·诺伊曼模型</strong>(Von Neumannmodel)或<strong>普林斯顿结构</strong>(Princetonarchitecture),是一种将程序指令存储器和数据存储器合并在一起的电脑设计概念结构。即将计算机指令进行编码后存储在计算机的存储器中,需要的时候可以顺序地执行程序代码,从而控制计算机运行,这就是冯.诺依曼计算机体系的开端。</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/386feb11d1fa9b59567f.png/kTePoRVhNC6MuKF.png"alt="冯·诺伊曼结构的设计概念" /><figcaption aria-hidden="true">冯·诺伊曼结构的设计概念</figcaption></figure><p>冯·诺依曼结构的优势。第一次将存储器和运算器分开,指令和数据都放在存储器中,为计算机的通用性奠定了基础。虽然在规范中计算单元依然是核心,但冯·诺依曼结构事实上导致了以存储器为核心的现代计算机的诞生。</p><p>冯·诺依曼结构的瓶颈。冯·诺依曼结构实现了计算机大提速,却也埋下了一个隐患:在内存容量指数级提升以后,CPU和内存之间的数据传输带宽成为了瓶颈。简而言之,由于 CPU的读写速率比存储器高,在每次去内存里取字节时,CPU都需要等待存储器。这就造成了 CPU性能的浪费。目前的解决办法是通过多核+多级缓存来缓解这一瓶颈问题。</p><h3 id="存储器的多级缓存">存储器的多级缓存</h3><p>冯·诺依曼结构瓶颈的解决方式之一是设置多级缓存。先来看下存储器的层级结构。</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/abc428d68b8a682bc051.png/7jW5XfAIisGqmg2.png"alt="存储器的层级结构" /><figcaption aria-hidden="true">存储器的层级结构</figcaption></figure><p>如上存储器的层次结构图,能看到用到的存储器有SRAM、DRAM、磁盘等。这样,操作系统中的存储器就构成了一个金字塔,越往上的存储器速度越快,价格越贵,容量也越小。当CPU 接收到指令后,它会最先向 CPU 中的一级缓存(L1Cache)去寻找相关的数据,然一级缓存是与 CPU同频运行的,但是由于容量较小,所以不可能每次都命中。这时 CPU会继续向下一级的二级缓存(L2Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘。</p><p>存储器分为两大类:</p><ul><li><p><strong>易失性存储:</strong>读写速度快,但断电后数据会丢失,容量小价格高。随机访问存储器(RAM)就属于这一类,RAM又分为 <strong>SRAM(静态)</strong>和<strong>DRAM(动态)</strong>。如上图的 <strong>L1~L3</strong> 属于SRAM,L4 属于 DRAM,通常 SRAM 主要集中在 CPU 芯片内部,价格昂贵,其中 L0寄存器本身就是 CPU 的组成部分之一,读写速度快。</p></li><li><p><strong>非易失性存储:</strong>读写速度较慢,但断电后数据不会丢失,容量大价格相对低。计算机使用的硬盘就是ROM 的一种,手机用的 Flash 也属于 Rom。这里的<strong>只读存储器ROM</strong>,随着计算机发展已经支持了读写,只是沿用了之前的名称。</p></li></ul><p>采用多级缓存提升效率,是用到了<strong><code>局部性原理(Principle of locality)</code></strong>,即被使用过的存储器内容在未来可能被再次使用,它附近的数据项也大概率会被使用。当我们访问某个数据项是,将它周围数据项也放到对应缓存中,这样一定程度上节约了访问存储器的时间,提高了效率。</p><h3 id="虚拟内存">虚拟内存</h3><p>在知道存储器分为多级缓存后,这里自然引出了一个概念:物理内存。这里的物理内存指物理存储器为运行时的操作系统及进程提供的存储空间,是真实的物理空间及地址。但如果将这些物理地址直接暴露出去,会存在很多的危险性。为了解决这个问题,随之出现了要介绍的<strong><code>虚拟内存</code></strong>。</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/99b54600bc9b1744ed35.png/2Sr7mUI6Z5OHFnl.png"alt="虚拟内存与物理内存的关系" /><figcaption aria-hidden="true">虚拟内存与物理内存的关系</figcaption></figure><p>对于每个进程来说,操作系统通过虚拟内存,为每个进程提供了一个<strong><code>连续并私有的地址空间</code></strong>,从而保护每个进程的地址空间不被其他进程干扰。如上图,有了虚拟内存后,进程访问的是分配给它的虚拟内存,而虚拟内存实际可能映射到物理内存及磁盘的任何区域。</p><h3 id="cpu-寻址方式">CPU 寻址方式</h3><p>在存储器里以字节为单位存储信息,为正确地存取信息,每个字节单元给以一个唯一的存储器地址,称为物理地址(PhysicalAddress)。</p><p>物理地址之后拓展支持了分段和分页。内存的分段和分页管理方式都属于内存的不连续分配。什么是不连续分配?就是把程序分割成一块一块的装入内存,在物理上不用彼此相连,在逻辑上使用段表或页表将离散分布的这些小块串起来形成逻辑上连续的程序。</p><p>在基本的分页概念中,把程序分成等长的小块。这些小块叫做<strong><code>页(Page)</code></strong>,同样内存也被分成了和页面同样大小的<strong><code>页框(Frame)</code></strong>,一个页可以装到一个页框里。在执行程序的时候,我们根据一个页表去查找某个页面在内存的某个页框中,由此完成了逻辑到物理的映射。</p><p>分段和分页有很多类似的地方,但是最大的区别在于分页对于用户来说是没什么逻辑意义的,分页是为了完成离散存储,所有的页面大小都一样,对程序员来说这就像碎纸机一样,出来的东西没有完整意义。但是分段不一样,分段不定长,分页由系统完成,分段有时在编译过程中会指定划分,因此可以保留部分逻辑特征,容易实现分段共享。iOS下的每个进程空间先分段,每个段内再分页,所以物理地址是由<strong><code>段号 + 段内页号 + 页内地址</code></strong>组成。</p><blockquote><p>上述内容,参考自《计算机操作系统》,更多可自行查看。</p></blockquote><p>在早期计算机系统中,程序员都是直接访问物理地址进行编程,当程序出现错误时,整个系统都会瘫痪,或者在多进程系统中,当一个进程出现问题,对属于另外一个进程的数据或者指令区域进行写操作,会导致另外一个进程崩溃。于是虚拟地址就被提出,软件使用虚拟地址访问内存,而处理器负责虚拟地址到物理地址的映射工作,地址转换是靠CPU中的<strong><code>内存管理单元(Memory Management Unit,即 MMU)</code></strong>来完成。处理器采用多级页表来进行多次查找最终找到真正的物理地址。当处理器发现页表中找不到真正对应的物理地址时,就会发出一个异常,挂起寻址错误的进程,但是其他进程仍然可以正常工作。从虚拟地址到物理地址的转换过程可知:由于页表是存放在内存中的,使用一级页表进行地址转换时,每次读/写数据需要访问两次内存,第一次访问一级页表获得物理地址,第二次才是真正的读/写数据;使用两级页表时,每次读/写数据需要访问三次内存,访问两次页表(一级页表和二级页表)获得物理地址,第三次才是真正的读/写数据。</p><p>拿处理器访问两级页表举例说明,当处理器拿到一个需要访问内存的<strong>虚拟地址A</strong>,首先查找 MMU里面页表地址寄存器得到页表在内存中的物理地址,然后 MMU通过访问内存控制器去访问内存中的两级页表得到 A1、A2 两个地址,A1 和 A2按照一定规则组合得虚拟地址 A 的物理地址 B,然后处理器在通过访问物理地址B 得到内存数据。</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/0efd0a174d2853abd281.png/Jdaul48woRYkXDB.png"alt="虚拟寻址过程" /><figcaption aria-hidden="true">虚拟寻址过程</figcaption></figure><p>这一地址转换过程大大降低了CPU的性能,有什么改进办法?</p><p>程序执行过程中,所用到的指令、数据的地址往往集中在一个很小的范围内,其中的地址、数据经常多次使用,这称为程序访问的局部性。由此,通过使用一个高速、容量相对较小的存储器来存储近期用到的页表条目(段/大页/小页/极小页描述符),以避免每次地址转换时都到内存去查找,这样可以大幅度地提高性能。这个存储器用来帮助快速地进行地址转换,称为<strong><code>“转译查找缓存”(TLB Cache)</code></strong>。</p><p>当 CPU 发出一个虚拟地址时,MMU 首先访问 TLB Cache,如果 TLB Cache中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查;否则MMU访问页表(页表是在主存中)找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB Cache 中(如果 TLB Cache 已满,则利用 round-robin算法找到一个条目,然后覆盖它),下次再使用这个虚拟地址时就可以直接使用TLB Cache 中的地址描述符了。</p><p>TLB是一个<strong>内存管理单元</strong>用于改进虚拟地址到物理地址转换速度的缓存,位于MMU 中。</p><h3 id="swap-内存交换机制swap-inout">Swap 内存交换机制(SwapIn/Out)</h3><p>物理内存是计算机的实际内存大小,由 RAM芯片组成。虚拟内存则是虚拟出来的、使用磁盘代替内存。虚拟内存的出现,让机器内存不够的情况得到部分解决。当程序运行起来由操作系统做具体虚拟内存到物理内存的替换和加载(相应的页与段的虚拟内存管理)。这里的虚拟内存交换过程即所谓的Swap。</p><p>当用户提交程序,然后产生进程在机器上运行。机器会判断当前物理内存是否还有空闲允许进程调入内存运行,如果有则直接调入内存进行;如果没有,则会根据优先级选择一个进程挂起,把该进程交换到Swap Space中等待,然后把新的进程调入到内存中运行。根据这种换入和换出,实现了内存的循环利用,让用户感觉不到内存的限制。从这也可以看出Swap 扮演了一个非常重要的角色,就是暂存被换出的进程。</p><h2 id="ios-的内存机制">iOS 的内存机制</h2><p>官方给出的关于内存的相关文档介绍:<ahref="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/ManagingMemory.html#//apple_ref/doc/uid/10000160-SW1">MemoryUsage Performance Guidelines</a>,看到最后的更新时间为 2013 年 04月,可以在需要时拿来作为参考。文档主要介绍的几点内容:</p><ul><li>关于虚拟内存系统</li><li>内存分配的技巧</li><li>缓存和内存清理</li><li>跟踪内存的使用情况</li><li>查找内存泄露</li><li>启用 malloc 调试功能</li><li>查看虚拟内存的使用情况</li></ul><h3 id="ios-对比桌面操作系统">iOS 对比桌面操作系统</h3><p>基于前面对一般桌面操作系统的了解和官方提供的文档,来对比看下在 iOS中的内存。</p><p>首先 iOS也和其他操作系统一样使用了虚拟内存机制,但区别于桌面操作系统的是:iOS不支持内存交换机制(Swap)。</p><p>iOS 不支持 Swap 机制主要的两个原因:</p><ul><li>一方面,因为 iPhone 使用的是闪存Flash,频繁的读写会影响闪存的寿命</li><li>另一方面,相比于桌面操作系统的电脑,手机的闪存空间很有限</li></ul><p>iOS 在内存优化上也下了很多心思,用到了内存压缩机制(Compressedmemory),后面会具体介绍。Stackoverflow 上面查找看到一份关于 iOS中单应用可用最大内存的测试报告(<ahref="https://stackoverflow.com/a/15200855">iOS app maximum memorybudget</a>)。</p><figure class="highlight text"><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></pre></td><td class="code"><pre><span class="line">device: (crash amount/total amount/percentage of total)</span><br><span class="line"></span><br><span class="line">iPad1: 127MB/256MB/49%</span><br><span class="line">iPad2: 275MB/512MB/53%</span><br><span class="line">iPad3: 645MB/1024MB/62%</span><br><span class="line">iPad4: 585MB/1024MB/57% (iOS 8.1)</span><br><span class="line">iPad Mini 1st Generation: 297MB/512MB/58%</span><br><span class="line">iPad Mini retina: 696MB/1024MB/68% (iOS 7.1)</span><br><span class="line">iPad Air: 697MB/1024MB/68%</span><br><span class="line">iPad Air 2: 1383MB/2048MB/68% (iOS 10.2.1)</span><br><span class="line">iPad Pro 9.7": 1395MB/1971MB/71% (iOS 10.0.2 (14A456))</span><br><span class="line">iPad Pro 10.5”: 3057/4000/76% (iOS 11 beta4)</span><br><span class="line">iPad Pro 12.9” (2015): 3058/3999/76% (iOS 11.2.1)</span><br><span class="line">iPad Pro 12.9” (2017): 3057/3974/77% (iOS 11 beta4)</span><br><span class="line">iPad Pro 11.0” (2018): 2858/3769/76% (iOS 12.1)</span><br><span class="line">iPad Pro 12.9” (2018, 1TB): 4598/5650/81% (iOS 12.1)</span><br><span class="line">iPad 10.2: 1844/2998/62% (iOS 13.2.3)</span><br><span class="line">iPod touch 4th gen: 130MB/256MB/51% (iOS 6.1.1)</span><br><span class="line">iPod touch 5th gen: 286MB/512MB/56% (iOS 7.0)</span><br><span class="line">iPhone4: 325MB/512MB/63%</span><br><span class="line">iPhone4s: 286MB/512MB/56%</span><br><span class="line">iPhone5: 645MB/1024MB/62%</span><br><span class="line">iPhone5s: 646MB/1024MB/63%</span><br><span class="line">iPhone6: 645MB/1024MB/62% (iOS 8.x)</span><br><span class="line">iPhone6+: 645MB/1024MB/62% (iOS 8.x)</span><br><span class="line">iPhone6s: 1396MB/2048MB/68% (iOS 9.2)</span><br><span class="line">iPhone6s+: 1392MB/2048MB/68% (iOS 10.2.1)</span><br><span class="line">iPhoneSE: 1395MB/2048MB/69% (iOS 9.3)</span><br><span class="line">iPhone7: 1395/2048MB/68% (iOS 10.2)</span><br><span class="line">iPhone7+: 2040MB/3072MB/66% (iOS 10.2.1)</span><br><span class="line">iPhone8: 1364/1990MB/70% (iOS 12.1)</span><br><span class="line">iPhone X: 1392/2785/50% (iOS 11.2.1)</span><br><span class="line">iPhone XS: 2040/3754/54% (iOS 12.1)</span><br><span class="line">iPhone XS Max: 2039/3735/55% (iOS 12.1)</span><br><span class="line">iPhone XR: 1792/2813/63% (iOS 12.1)</span><br><span class="line">iPhone 11: 2068/3844/54% (iOS 13.1.3)</span><br><span class="line">iPhone 11 Pro Max: 2067/3740/55% (iOS 13.2.3)</span><br></pre></td></tr></table></figure><p>由上数据,可以看到以 iPhone 11 Pro Max 为例,内存的最大空间为3740MB,应用可使用的最大空间为 2067MB,占了 55%。iOS的总内存空间虽然很有限,但 iOS给每个进程分配的虚拟内存空间还是非常大的。</p><p>由上,iOS 对比桌面操作系统,同样使用了虚拟地址,没有使用 Swap内存交换机制,而是通过<strong>内存压缩机制(Compressedmemory)</strong>来最大化利用内存。</p><h3 id="ios-系统内存">iOS 系统内存</h3><p>以下分析参考自苹果在 WWDC 2018 上的 Session:<ahref="https://developer.apple.com/videos/play/wwdc2018/416/">416. iOSMemory Deep Dive</a></p><p>在前面关于<strong>操作系统 CPU寻址方式</strong>中提到了内存采用了分段+分页的管理方式。具有 VM机制的操作系统,会对每个运行的进程创建一个<strong>虚拟地址空间</strong>,该空间的大小有操作系统决定。<strong>虚拟地址空间</strong>会被分为相同大小的块,这些块被称为<strong><code>内存页(Memory Page)</code></strong>。计算机处理器和它的内存管理单元(MMU)维护着一张将程序的虚拟地址空间映射到物理地址上的分页表(PageTable)。iOS 中虚拟内存和物理内存的分页大小都是 16KB。</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/b902078f0d39dc9274bf.jpg/In3GzcqtwgmlMDe.jpg"alt="内存大小的计算方式" /><figcaption aria-hidden="true">内存大小的计算方式</figcaption></figure><p>iOS的<strong><code>内存页(Memory Page)</code></strong>主要分两类:CleanPage 和 Dirty Page。</p><h4 id="clean-page">Clean Page</h4><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/e13e4f2b88d5790a3e96.jpg/v3mIA1cDxPnLyfG.jpg"alt="Clean Page" /><figcaption aria-hidden="true">Clean Page</figcaption></figure><p>对于一般的桌面操作系统,Clean Memory 是能够 Page Out 的部分。Page Out指将优先级低的内存数据交换到磁盘上,但 iOS 不支持 Swap,所以 Clean Page在 iOS 是指只能够被系统清理出内存且在需要时能重新加载数据的Page。包含的类型有:</p><ul><li>应用的二进制可执行文件</li><li>Memory mappedfiles:<code>.jpg</code>、<code>.data</code>、<code>.modal</code>等文件。</li><li>Frameworks* :<code>_DATA_CONST</code>字段。需要主意的是:这个字段在创建的时候是 Clean Page类型的,但如果在程序运行起来时,我们对系统方法进行了Swizzling,就会把这个内存页变成 Dirty Page。</li></ul><h4 id="dirty-page">Dirty Page</h4><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/b4c698064931640a30e2.jpg/T6g3AFxocj48Kd1.jpg"alt="Dirty Page" /><figcaption aria-hidden="true">Dirty Page</figcaption></figure><p>Dirty Page 指不能被系统回收的内存占用。包含的类型有:</p><ul><li>所有堆上的对象(如 malloc、Array、NSCache、UIViews、String)</li><li>图片解析缓冲(如 CGRasterData、ImageIO)</li><li>Frameworks(如 <code>_DATA</code>、<code>_DATA_DIRTY</code>)</li></ul><p>可以看到 Framework 既有 Clean Page,也有 Dirty Page。</p><h4 id="compressed-memory">Compressed Memory</h4><p>当内存紧张时,系统会将暂不访问的物理内存进行压缩,直到下一次访问的时候进行解压。例如当我们使用Dictionary 去缓存数据的时候,假设现在使用了 3页内存,当不访问的时候可能会被压缩为 1 页,再次使用到时候又会解压成 3页。如下图:</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/a53dbf4f41003c34e496.jpg/P1GsYMVLTXD73zZ.jpg"alt="Dictionary 压缩前后" /><figcaption aria-hidden="true">Dictionary 压缩前后</figcaption></figure><p>Compressed Memory 是一种用 CPU 时间换空间的方式。</p><h4 id="内存警告">内存警告</h4><p>当 App 收到内存警告时,苹果给出了一些关于内存警告的一些想法:</p><ul><li>并不是所有的内存警告都是 App 本身造成的。如使用 App的过程中接听到电话,也可能触发内存警告。</li><li>内存压缩机制使得内存释放变得比较复杂。如前面 Dictinary的例子。假设我们收到内存警告,我们可能会决定将字典中的一些数据删除。在我们重新访问压缩后的Page 时,系统会先解压这块内存,Dictionary Page 就会从一个变为 3个;之后释放 Dictionary 所占的 Page;此时实际释放的 Page 还是 1个。因为操作过程中有一个解压的过程,很容易造成内存紧张的状态。</li><li>不要一味的缓存,要找到 CPU计算和内存性能之间的平衡点。相比较使用字典缓存,苹果更推荐使用NSCache。NSCache分配的内存可以由系统自动释放,官方针对内存警告也做了优化。</li></ul><p>我们平时关心的内存占用其实是 Dirty Size 和 Compressed Size两部分,所以当我们想要优化内存时,尽量从这两部分入手。</p><p>App 中内存占用(Memory Footprint)有一定的限制: -不同设备的内存限制不同 - App 都具有相当高的占用空间限制 - 提供给Extensions 内存比较少 - 如果内存超过了限制范围,App 会抛出<strong><code>EXC_RESOURCE_EXCEPTION</code></strong> 异常</p><p>附带 Stackoverflow 上查找看到一份关于 iOS中单应用可用最大内存的测试报告(<ahref="https://stackoverflow.com/a/15200855">iOS app maximum memorybudget</a>)</p><h3 id="ios-app-内存">iOS App 内存</h3><p>iOS系统层面的内存,大多由系统自动完成。通常开发者讨论的内存管理,实际上是进程内部语言层面的内存管理。iOS中一个 App 对应一个进程。</p><h4 id="app-内存空间">App 内存空间</h4><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/dcdcfb36153bac4759b7.png/GmokglS7Mr3C1vJ.png"alt="内存分区" /><figcaption aria-hidden="true">内存分区</figcaption></figure><p>内存分区按高地址到低地址依次为: - 栈区(Stack) - 堆区(Heap) -静态存储区(Static) - 常量区在程序中使用的常量(如常量字符串)存储在该区域。在程序结束后,由系统释放。- 代码区存放函数体的二进制代码。运行程序实际上是执行代码,代码要执行就需要先加载入内存。</p><p>展开介绍下<code>栈区(Stack)</code>、<code>堆区(Heap)</code>和<code>静态存储区(Static)</code>。</p><h5 id="栈区stack">栈区(Stack)</h5><p>栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。</p><p>在执行函数时,函数内局部变量的存储单元(指非静态的局部变量,如:函数参数、在函数内所声明对象的指针等)都会在栈上进行创建,函数执行结束时(出作用域时),这些存储单元会被自动释放。栈区的内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量是有限的,当系统的栈区大小不够分配时,系统会提示栈溢出。官方也给出了iOS 中栈空间的大小,子线程为 512KB,主线程为 1MB(<ahref="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html">官方链接</a>),如下图:</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/e563acd58cce47ca72a4.jpg/1Ci4XNVDuAJ2lR9.jpg"alt="iOS 中栈区空间限制" /><figcaption aria-hidden="true">iOS 中栈区空间限制</figcaption></figure><p>栈是向低地址扩展的,是一块连续的内存区域,且栈顶的地址和栈的最大容量是由系统预先规定的,遵循FILO,不产生内存碎片。只要栈的剩余空间大于所申请空间,系统讲为程序提供内存;否则将报异常提示栈溢出。因此,能从栈获得的空间较小。开发过程中,需要留意的是:像大量的局部变量,深递归,函数循环调用都可能导致栈溢出而运行崩溃。</p><h5 id="堆区heap">堆区(Heap)</h5><p>堆区中的变量由开发者进行分配和释放。操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。大多系统,会在这块内存空间中的首地址处记录本次分配的大小,以使得内存空间释放时正确。另外,由于找到的空闲堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。</p><p>堆是向高地址扩展的,是不连续的内存区域。这是由于系统使用了链表来存储空闲的内存地址,而链表的遍历方向是由低地址向高地址。堆的大小由系统中有效虚拟内存决定。因此,堆的空间比较灵活,也比较大,由于堆的特性,也容易产生内存碎片,但用起来较为方便。</p><h5 id="静态存储区static">静态存储区(Static)</h5><p>这块内存在程序编译时就已经分配好,在程序的整个运行期间这块内存都会存在。它主要用来存放<strong><code>静态变量</code></strong>、<strong><code>全局变量</code></strong>和<strong><code>常量</code></strong>。事实上<strong><code>全局变量</code></strong>也是静态的,因此,也叫<strong><code>全局静态存储区</code></strong>。</p><p><strong><code>静态存储区</code></strong>分为两部分: -数据区:<strong><code>全局变量</code></strong>和<strong><code>静态变量</code></strong>的存储是放在一起的,初始化的<strong><code>全局变量</code></strong>和<strong><code>静态变量</code></strong>存放在一块区域- BSS区:未初始化的<strong><code>全局变量</code></strong>和<strong><code>静态变量</code></strong>在相邻的另一块区域。</p><p>在程序结束运行后,这块内存由系统释放。</p><h2 id="引用计数">引用计数</h2><p>移动端的内存管理技术,主要有 GC(Garbage Collection垃圾回收)的标记清楚算法和苹果使用的引用计数方法。</p><p>早期的 iOS 开发通过手动引用计数(MRC - Mannul ReferenceCounting)的方式手动管理引用计数,由于 MRC 维护成本的原因,苹果在 2011年的 WWDC 提出了自动引用计数(ARC - Automatic Reference Countin)。ARC背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的位置插入引用计数管理代码。遵循谁申请谁释放的原则。虽然ARC帮助我们解决了引用计数大部分的问题,但开发过程中如果不留意会很容易出现类似循环引用而导致的内存泄露的问题。移动设备的内存资源是有限的,当App运行时占用的内存超过限制后,会被强制杀掉,用户体验会被极大降低。为了提升App 质量,开发者需要重视应用的内存管理问题。</p><p>引用计数(ReferenceCount)是一种管理对象生命周期的方式。在创建一个新对象时,它的引用计数为1;每当该被引用时,它的引用计数+1;每当引用该对象的对象释放时,它的引用计数 -1。当该对象的引用计数为 0时,说明该对象不再被任何对象使用,这时该对象会被销毁,内存回收。过程如下图:</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/a1058d1f53d4e3694e58.png/v3UIusgnDVSdcb7.png"alt="对象的引用计数" /><figcaption aria-hidden="true">对象的引用计数</figcaption></figure><p>一个需要主意的点:当对象被释放时,它的 retainCount 不一定为0。如下代码:</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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)testRetainCount {</span><br><span class="line"> <span class="built_in">NSObject</span> *object = [[<span class="built_in">NSObject</span> alloc] init];</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Reference Count = %u"</span>, [object retainCount]);</span><br><span class="line"> [object release];</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Reference Count = %u"</span>, [object retainCount]);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>输出结果如下:</p><figure class="highlight bash"><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">Reference Count = 1</span><br><span class="line">Reference Count = 1</span><br></pre></td></tr></table></figure><p>会发现 object 在 release 前后的引用计数都为 1。这是为什么?</p><p>是因为当最后一次执行 release时,系统知道马上就要回收内存,没有必要再将 retainCount - 1。因为不管是否-1,该对象都确定会被回收,而对象被回收后,所在的内存区域包括 retainCount的值已经没有意义。这里不将 1 变为0,是为了减少一次内存写操作,进而加速对象的回收。</p><p>ARC 虽然帮助开发者解决了 iOS 开发过程中绝大部分的内存管理问题,但底层Core Foundation 对象的部分不在 ARC的管理范围内,需要开发者自己维护这些对象的引用计数。</p><h2 id="循环引用">循环引用</h2><p>引用计数管理内存的方式是:当对象自己被销毁时,其成员变量引用计数-1。但如果出现下面的引用情况:</p><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/d0491f03c376635bf453.png/HYQmhOWGxsoFyV7.png"alt="相互持有对象" /><figcaption aria-hidden="true">相互持有对象</figcaption></figure><p>上图中,对象 A 持有对象 B,同时对象 B 也持有对象 A。在外界对对象 A和对象 B 没有其他任何引用的情况下,对象 A 若想释放,只能先释放对象B;但对象 B 若想释放,同样需要先释放对象A。这样就出现了循环引用(Reference Cycle)的问题。</p><p>解决循环引用问题主要有两种方式:</p><ul><li>第一种是主动断开循环,通过置 nil 主动释放的方式</li><li>第二种是通过使用弱引用</li></ul><p>弱引用虽然持有对象,但不会增加被持有对象的引用计数,这样就避免了循环引用的产生。弱引用用的比较多的场景比如:delegate模式的使用。</p><figure><img src="https://i.loli.net/2020/09/14/uCNqZEKrQhRnijS.png"alt="弱引用 Delegate" /><figcaption aria-hidden="true">弱引用 Delegate</figcaption></figure><p>如上图的例子,两个 ViewController。场景是 ViewController A 弹出ViewController B,在 ViewController B 做完一些操作后,将一些数据返回给ViewController A。这时,因为 delegate是弱引用,不会变更引用计数,这样就避免了循环引用的产生。</p><h2 id="弱引用的实现原理">弱引用的实现原理</h2><p>系统对于每一个有弱引用的对象,都维护了一个表来记录它所有弱引用的指针地址。当一个对象的引用计数为0 时,系统就通过这张表,找到所有的弱引用指针,继而把它们都置为 nil。</p><p>更细节的关于弱引用的实现原理,后面会有单独的一篇来分析。</p><h2 id="oom">OOM</h2><p>OOM 是 Out of Memory 的缩写,指当 App 占用的内存达到了 iOS 系统对单个App 占用内存上限后会被系统强杀掉的现象。这是一种由 iOS 的 JetSam机制导致的一种“另类”崩溃,并且日志无法通过信号捕捉到。</p><p>JetSam机制,指的是操作系统为了控制内存资源过度使用而采用的一种资源管控机制。</p><p>在面对 OOM类问题时,会考虑到两个方面的问题。一方面是,如何知道系统对单个 App允许占用内存的上限值?另一方面是,如何定位OOM?依次来看下对应的解决方案。</p><h3 id="如何获取内存上限值">如何获取内存上限值?</h3><h4 id="jetsamevent-日志">JetsamEvent 日志</h4><p>我们可以从<strong><code>设置 - 隐私 - 分析与改进</code></strong>这条路径看到系统的日志,找到以JetsamEvent开头的系统日志,我们可以通过隔空投送到电脑或直接在手机上查看这些日志内容。</p><p>在这类系统日志中,查找崩溃原因时如果看到<code>"reason" : "pre-process-limit"</code>,则表示崩溃是由于App 占用的内存超过了系统对单个 App的内存限制。对应查找<code>"rpages"</code> 对应的值,这个值表示 App占用的内存页数量。</p><p>日志内容的结构如下:</p><figure class="highlight plaintext"><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">"rpages" : 89600,</span><br><span class="line">"reason" : "per-process-limit",</span><br></pre></td></tr></table></figure><p>通过 JetsamEvent 日志获取了内存页数量 rpages 为89600,只要再知道内存页大小的值,就可以计算出单个 App 的内存上限值。</p><p>继续在 JetsamEvent 日志中查找以 <code>"pageSize"</code> 对应的值,为16384。通过下面的计算公式得到值为 1.4G。</p><ul><li>内存上限值 = pageSize * rpages / 1024 / 1024 MB</li></ul><p>JetsamEvent 日志是系统在杀掉 App后留在手机中的,属于系统级日志,存放在系统目录下,App上线后开发者是没有权限获得的。</p><p>那么 iOS 是怎么监控内存压力的?</p><p>iOS 系统会开启优先级最高的线程 <code>vm_pressure_monitor</code>来监控系统的内存压力情况,并通过一个堆栈来维护所有 App 的进程。此外,iOS系统还会维护一个内存快照表,用于保存每个进程内存页的消耗情况。</p><p>当<code>vm_pressure_monitor</code>线程发现某 App 内存有压力了,会为该App 发送通知,也就是 <code>didReceiveMemoryWarnning</code>代理。通过这个代理,可以写需要的内存释放代码,以避免 App被系统强制杀死。</p><p>iOS系统内核有一个数组,专门用于维护线程的优先级。优先级由高到低依次是:<strong>内核用线程的优先级> 操作系统 > 前台 App > 后台运行 App</strong></p><p>苹果考虑到手持设备存储空间有限,在 iOS 中去掉了Swap,这样虚拟内存就没办法记录到外部的存储上,进而苹果引入了MemoryStatus 机制。</p><p>MemoryStatus 机制的主要思路是,在 iOS上弹出尽可能多的内存共当前应用使用。把这个机制落到优先级上,就是先强杀后台应用;如果内存还不够就强杀掉当前应用。MemoryStatus机制会开启一个 <code>memorystatus_jetsam_thread</code> 线程,这个线程和<code>vm_pressure_monitor</code>没有关系。<code>memorystatus_jetsam_thread</code>线程只负责强杀应用和记录日志,不会发送通知消息;<code>vm_pressure_monitor</code>线程也无法获取强杀应用的消息。</p><p>除了内存过大的原因会被系统强杀,还有三种内存问题也会被强杀:</p><ul><li>访问未分配的内存。XNU 会报 EXC_BAD_ACCESS 错误,发出 SIGSEGV Signal#11 信号。这类报错绝大多数是由于对一个已经释放的对象进行 release操作造成的。</li><li>访问已分配但未提交的内存。XNU会拦截分配物理内存,出现问题的线程分配内存页时会被冻结。</li><li>没有遵守权限访问内存。内存页的权限标准类似 UNIX文件权限,如果对只读权限的内存页进行写入就会出错,XNU 发出 SIGSEGVSignal #7 信号。</li></ul><p>第一种和第三种问题可以通过崩溃信息获取到,在收集崩溃信息时如果是这两类,可以把内存分配的记录同时收集,用于分析不合理内存分配和优化。</p><h4 id="通过-xnu-获取">通过 XNU 获取</h4><p>XNU 中有专门用于获取内存上限值的函数和宏,可以通过<code>memorystatus_priority_entry</code>结构体得到进程的优先级和内存上限值。结构体中 priority表示进程的优先级;limit 表示进程的内存上限值。相关源码如下:</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 获取进程的 pid、优先级、状态、内存阈值等信息</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">memorystatus_priority_entry</span> {</span></span><br><span class="line"> <span class="type">pid_t</span> pid;</span><br><span class="line"> <span class="type">int32_t</span> priority;</span><br><span class="line"> <span class="type">uint64_t</span> user_data;</span><br><span class="line"> <span class="type">int32_t</span> limit;</span><br><span class="line"> <span class="type">uint32_t</span> state;</span><br><span class="line">} <span class="type">memorystatus_priority_entry_t</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 基于下面这些宏可以达到查询内存阈值等信息,也可以修改内存阈值等</span></span><br><span class="line"><span class="comment">/* Commands */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_GET_PRIORITY_LIST 1</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_SET_PRIORITY_PROPERTIES 2</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_GET_JETSAM_SNAPSHOT 3</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_GET_PRESSURE_STATUS 4</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_SET_JETSAM_HIGH_WATER_MARK 5 <span class="comment">/* Set active memory limit = inactive memory limit, both non-fatal */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_SET_JETSAM_TASK_LIMIT 6 <span class="comment">/* Set active memory limit = inactive memory limit, both fatal */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_SET_MEMLIMIT_PROPERTIES 7 <span class="comment">/* Set memory limits plus attributes independently */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES 8 <span class="comment">/* Get memory limits plus attributes */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_ENABLE 9 <span class="comment">/* Set the task's status as a privileged listener w.r.t memory notifications */</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_PRIVILEGED_LISTENER_DISABLE 10 <span class="comment">/* Reset the task's status as a privileged listener w.r.t memory notifications */</span></span></span><br><span class="line"><span class="comment">/* Commands that act on a group of processes */</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> MEMORYSTATUS_CMD_GRP_SET_PROPERTIES 100</span></span><br></pre></td></tr></table></figure><p>XNU 详细相关源码链接: - <ahref="https://opensource.apple.com/source/xnu/xnu-3248.20.55/bsd/sys/kern_memorystatus.h.auto.html">kern_memorystatus.h</a>- <ahref="https://opensource.apple.com/source/xnu/xnu-3789.70.16/bsd/kern/kern_memorystatus.c.auto.html">kern_memorystatus.c</a></p><p>通过 XNU 宏获取内存限制,需要越狱来获取 root权限,正常情况下开发者看不到这些信息。</p><h4 id="通过内存警告获取">通过内存警告获取</h4><p>前面提到内存警告时,系统的内存监控线程会给相关 App发送通知<code>didReceiveMemoryWarnning</code>。我们可以利用这个内存压力代理事件来动态获取内存上限值。系统在强制杀死App 前会有 6s 的时间,这段时间足够我们获取记录内存信息。iOS系统提供了一个 <code>task_info</code>函数,我们可以在发生内存警告时,通过 <code>task_info_t</code> 结构内的<code>resident_size</code> 字段获取当前 App占用了多少内存。具体代码如下:</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#import <span class="string"><mach/mach.h></span></span></span><br><span class="line">- (int64_t)memoryUsage {</span><br><span class="line"> int64_t memoryUsageInByte = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">struct</span> task_basic_info taskBasicInfo;</span><br><span class="line"> mach_msg_type_number_t size = <span class="keyword">sizeof</span>(taskBasicInfo);</span><br><span class="line"> kern_return_t kernelReturn = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t) &taskBasicInfo, &size);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span>(kernelReturn == KERN_SUCCESS) {</span><br><span class="line"> memoryUsageInByte = (int64_t) taskBasicInfo.resident_size;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Memory in use (in bytes): %lld"</span>, memoryUsageInByte);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Error with task_info(): %s"</span>, mach_error_string(kernelReturn));</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> memoryUsageInByte;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但测试的时候,我们会发现计算出的值跟 Instruments里看到的内存大小不一致,甚至相差及时MB。resident_size(驻留内存)确实无法反映真实的物理内存,而且 Xcode 的Debug Gauge 使用的也是 <code>phys_footprint</code>,这点从 WebKit 和 XNU的源码可以佐证。</p><p><ahref="https://github.com/WebKit/webkit/blob/52bc6f0a96a062cb0eb76e9a81497183dc87c268/Source/WTF/wtf/cocoa/MemoryFootprintCocoa.cpp">WebKit相关源码</a>: <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="function"><span class="type">size_t</span> <span class="title">memoryFootprint</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="type">task_vm_info_data_t</span> vmInfo;</span><br><span class="line"> <span class="type">mach_msg_type_number_t</span> count = TASK_VM_INFO_COUNT;</span><br><span class="line"> <span class="type">kern_return_t</span> result = <span class="built_in">task_info</span>(<span class="built_in">mach_task_self</span>(), TASK_VM_INFO, (<span class="type">task_info_t</span>) &vmInfo, &count);</span><br><span class="line"> <span class="keyword">if</span> (result != KERN_SUCCESS)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">static_cast</span><<span class="type">size_t</span>>(vmInfo.phys_footprint);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p><ahref="https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/kern_memorystatus.c">XNU源码</a>中 JetSam 判断应用内存是否使用过大也是使用的<code>phys_footprint</code>。WWDC 2018 Session <ahref="https://developer.apple.com/videos/play/wwdc2018/416">iOS MemoryDeep Dive</a> 对 <code>footprint</code> 这块也有介绍。</p><p>贴近 JetSam 机制,更准确的内存计算方式应该是通过<code>phys_footprint</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><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"><span class="meta">#import <span class="string"><mach/mach.h></span></span></span><br><span class="line">- (int64_t)memoryUsage {</span><br><span class="line"> int64_t memoryUsageInByte = <span class="number">0</span>;</span><br><span class="line"> task_vm_info_data_t vmInfo;</span><br><span class="line"> mach_msg_type_number_t count = TASK_VM_INFO_COUNT;</span><br><span class="line"> kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);</span><br><span class="line"> <span class="keyword">if</span>(kernelReturn == KERN_SUCCESS) {</span><br><span class="line"> memoryUsageInByte = (int64_t) vmInfo.phys_footprint;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Memory in use (in bytes): %lld"</span>, memoryUsageInByte);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Error with task_info(): %s"</span>, mach_error_string(kernelReturn));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> memoryUsageInByte;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="如何定位-oom">如何定位 OOM?</h3><p>OOM 分为两大类,Foreground OOM / Background OOM,即 FOOM 和BOOM。其中 FOOM 是指 App在前台因消耗内存过多引起系统强杀。对用户而言,表现跟 crash 一样。</p><p>现在主流的 OOM 检测库有两个: - <ahref="https://github.com/facebook/FBAllocationTracker">FBAllocationTracker</a>- <a href="https://github.com/Tencent/OOMDetector">OOMDetector</a></p><p>Facebook 早在 2015 年 8 月提出 FOOM检测办法,大致原理是排除各种情况后,剩余的情况是 FOOM,原文:<ahref="https://engineering.fb.com/ios/reducing-fooms-in-the-facebook-ios-app/">ReducingFOOMs in the Facebook iOS app</a>。可以使用 Facebook 的 <ahref="https://github.com/facebook/FBAllocationTracker">FBAllocationTracker</a>工具监控 OC 对象分配,用 fishhook 工具 hook malloc/free等接口监控堆内存分配,每隔 1 秒,把当前所有 OC 对象个数、TOP 200最大堆内存及其分配堆栈,用文本 log 输出到本地。</p><p>这个方案的不足点:</p><ul><li>监控粒度不够细,像大量分配小内存引起的质变无法监控,另外 fishhook只能 hook 自身 app 的 C 接口调用,对系统库不起作用;</li><li>打 log间隔不好控制,间隔过长可能丢失中间峰值情况,间隔过短会引起耗电、I/O频繁等性能问题;</li><li>上报的原始 log 靠人工分析,缺少好的页面工具展现和归类问题。</li></ul><p>在这之后微信开源了 <ahref="https://github.com/Tencent/OOMDetector">OOMDetector</a>,使用了更底层的<code>malloc_logger_t</code>记录当前存活对象的内存分配信息(包括分配大小和分配堆栈)。分配堆栈可以用backtrace 函数捕获,但捕获到的地址是虚拟内存地址,不能从符号表 dsym解析符号。所以还要记录每个 image 加载时的偏移slide,这样<strong>符号表地址=堆栈地址-slide</strong>。另外,还做了数据归类。具体的实现方案可以查看原文:<ahref="https://wetest.qq.com/lab/view/367.html">iOS微信内存监控</a></p><h2 id="oom-常见问题">OOM 常见问题</h2><h3 id="uigraphicsendimagecontext">UIGraphicsEndImageContext</h3><p>UIGraphicsBeginImageContext 和 UIGraphicsEndImageContext必须成双出现,不然会造成 context 泄漏。另外 Xcode 的 Analyze也能扫出这类问题。</p><h3 id="uiwebview">UIWebView</h3><p>无论是打开网页,还是执行一段简单的 js 代码,UIWebView 都会占用 App大量内存。而 WKWebView不仅有出色的渲染性能,且有自己独立进程,一些网页相关的内存消耗移到自身进程里,最适合取替UIWebView。</p><h3 id="autoreleasepool">autoreleasepool</h3><p>通常 autoreleased 对象是在 runloop 结束时才释放。如果在循环里产生大量autoreleased 对象,内存峰值会猛涨,甚至出现 OOM。适当的添加autoreleasepool 能及时释放内存,降低峰值。</p><h3 id="互相引用">互相引用</h3><p>比较容易出现互相引用的地方是 block 里使用了 self,而 self 又持有这个block,只能通过代码规范来避免。另外 NSTimer 的 target、CAAnimation 的delegate,是对 Object 强引用。</p><h3 id="大图片压缩">大图片压缩</h3><p>当我们在缩小一幅图像的时候,会按照取平均值的办法把多个像素点变成一个像素点,这个过程称为<code>Downsampling</code>。通常图片缩放接口可以如下写法:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/6fea8de4263822575352.png/egZGq8j2vNoIdlO.png" /></p><p>但处理大分辨率图片时,往往容易出现 OOM,原因是<code>-[UIImage drawInRect:]</code>在绘制时,先解码图片,再生成原始分辨率大小的bitmap,这是很消耗内存。解决方法是使用更底层的 <code>ImageIO</code>接口,它可以直接读取图像大小和元数据信息,不会带来额外的内存开销。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/5ac66d5b10a17fa72db3.png/EnCVrcjPLiM5Hwg.png" /></p><h3 id="大图加载显示">大图加载显示</h3><p><a href="https://developer.apple.com/videos/play/wwdc2018/416/">WWDC2018 Session 416:iOS Memory Deep Dive</a>提出建议使用<code>UIGraphicsImageRenderer</code> 代替<code>UIGraphicsBeginImageContextWithOptions</code>, 该方法从 iOS 10引入了,在 iOS 12上会自动选择最佳的图片格式,可以减少很多内存。如果想修改颜色,可以直接修改tintColor,不会有额外的内存开销。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/6395d89661731dbe0c46.png/5sB8VbmaoDzWJYF.png" /></p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/ea37043b9c82f5b187c8.png/m8KDjyH7We2CvQ5.png" /></p><blockquote><p>图片在 iOS 上的显示原理:<ahref="https://developer.apple.com/videos/play/wwdc2018/219/">WWDC 2018Session 219:Image and Graphics Best Practices</a> 对应的翻译文稿:<ahref="https://juejin.im/post/6844903618429059086">《WWDC2018图像最佳实践》</a></p></blockquote><h3 id="大图切换前后台时的优化">大图切换前后台时的优化</h3><p>假设在 App里展示了一张很大的图片,当我们切换到后台去做其它的操作时,这个图片还在占用内存。我们应该考虑在合适的时机去回收这类占用过大的数据。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/58b98c0cca99e6005aa9.png/3eovruaADx9OtRQ.png" /></p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/f860bb5c70726ccd4dc6.png/Hn9qeN8Gt7sPjyA.png" /></p><h3 id="大视图">大视图</h3><p>大视图是指 View 的 size过大,自身包含要渲染的内容。超长文本如常见的炸群消息,通常几千甚至几万行。如果把它绘制到同一个View里,那将会消耗大量内存,同时造成严重卡顿。最好做法是把文本划分成多个View 绘制,利用 TableView 的复用机制,减少不必要的渲染和内存占用。</p><h2 id="内存检测工具">内存检测工具</h2><p>列出一些内存分析的工具:</p><h3 id="xcode-memory-gauge">Xcode Memory Gauge</h3><p>在 Xcode 中,你可以通过 <code>Memory Gauge</code> 工具,快速查看 App运行时的内存情况,包括内存最高占用、最低占用,以及在所有进程中的占用比例等。如果想要查看更详细的数据,就需要用到<code>Instruments</code> 了。</p><h3 id="instruments">Instruments</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/651b352b5793f4208660.jpg/CDbBw261f8Tk4jq.jpg" /></p><p>在 Instruments 中,你可以使用 Allocations、Leaks、VM Tracker 和Virtual Memory Trace 对 App 进行多维度分析。</p><ul><li><p>Allocations:可以查看虚拟内存占用、堆信息、对象信息、调用栈信息,VMRegions信息等。可以利用这个工具分析内存,并针对地进行代码优化。</p></li><li><p>Leaks:用于检测内存泄漏。</p></li><li><p>VM Tracker:可以查看内存占用信息,查看各类型内存的占用情况,比如dirty memory 的大小等等,可以辅助分析内存过大、内存泄漏等原因。</p></li><li><p>Virtual Memory Trace:有内存分页的具体信息,具体可以参考 <ahref="https://developer.apple.com/videos/play/wwdc2016/411/">WWDC 2016 -Syetem Trace in Depth</a>。</p></li></ul><h3 id="debug-debugger---memory-resource-exceptions">Debug Debugger -Memory Resource Exceptions</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/e699e9ffea864e9b79df.jpg/B4QRZi1rjYfHA82.jpg" /></p><p>当使用 Xcode 10 以前的版本进行调试时,在内存过大时,debug session会直接终止,并且在控制台打印出异常。从 Xcode 10 开始,debugger会自动捕获 EXC_RESOURCE RESOURCE_TYPE_MEMORY异常,并断点在触发异常抛出的地方,十分方便定位问题。</p><h3 id="xcode-memory-debugger">Xcode Memory Debugger</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/cd3cec53cbb0debef28c.jpg/VSiODU2dQFC3Wj4.jpg" /></p><p>通过这个工具,可以直观地查看内存中所有对象的内存使用情况,对象相互间的依赖关系,对定位那些因为循环引用导致的内存泄露问题十分有帮助。我们也可以点击 <code>File -> Export Memory Graph</code> 将其导出为memgraph 文件,在命令行中使用 Developer Tool对其进行分析。使用这种方式,我们可以在任何时候对过去某时的 App内存使用进行分析。</p><h3 id="vmmap">vmmap</h3><p>用于查看虚拟内存。</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看详细报告</span></span><br><span class="line">vmmap App.memgraph</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看摘要报告</span></span><br><span class="line">vmmap --summary App.memgraph</span><br><span class="line"></span><br><span class="line"><span class="comment"># vmmap and AWK 查看所有动态库的Ditry Pages的总和</span></span><br><span class="line">vmmap -pages PlanetPics.memgraph | grep <span class="string">'.dylib'</span> | awk <span class="string">'{sum += $6} END { print "Total Dirty Pages:"sum}'</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看vmmap的文档</span></span><br><span class="line">man vmmap</span><br></pre></td></tr></table></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/4101ca15d6ecf90e9e3e.jpg/7rxQh6R1a8PNjU3.jpg"alt="vmmap App.memgraph" /><figcaption aria-hidden="true">vmmap App.memgraph</figcaption></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/c2b0efa3d8e33cbb6201.jpg/BcIjiVXNEHxOen3.jpg"alt="vmmap --summary App.memgraph" /><figcaption aria-hidden="true">vmmap --summary App.memgraph</figcaption></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/92c5cea0428600c40e12.jpg/gQR4JEojBHMKcOr.jpg"alt="vmmap -pages PlanetPics.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'" /><figcaption aria-hidden="true">vmmap -pages PlanetPics.memgraph | grep'.dylib' | awk '{sum += $6} END { print "Total DirtyPages:"sum}'</figcaption></figure><h3 id="leaks">leaks</h3><p>用于查看泄露的内存。</p><figure class="highlight bash"><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">leaks MyApp.memgraph</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看 leaks 的文档</span></span><br><span class="line">man leaks</span><br></pre></td></tr></table></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/54cd0a43e02af3577397.jpg/d8qOi4Nyu7kKxoL.jpg"alt="leaks MyApp.memgraph" /><figcaption aria-hidden="true">leaks MyApp.memgraph</figcaption></figure><h3 id="heap">heap</h3><p>查看堆区内存。</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看所有堆区对象的内存使用</span></span><br><span class="line">heap App.memgraph</span><br><span class="line"></span><br><span class="line"><span class="comment"># 默认情况下是按照对象数量进行排序,通常情况下它们不会造成什么内存问题。</span></span><br><span class="line"><span class="comment"># 我们更关心的那些为数不多但占用大量内存的对象</span></span><br><span class="line"><span class="comment"># 参数 -sortBySize,按照内存占用大小顺序来查看所有堆区对象的内存使用</span></span><br><span class="line">heap App.memgraph -sortBySize</span><br><span class="line"></span><br><span class="line"><span class="comment"># 当确定是哪个类型的对象占用了太多内存之后,可以得到每个对象的内存地址</span></span><br><span class="line">heap App.memgraph -addresses all | <classes-pattern></span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看 heap 的文档</span></span><br><span class="line">man heap</span><br></pre></td></tr></table></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/f1d3f2241ea1ac2c01ba.jpg/LEYjbD7zI9vA2PQ.jpg"alt="heap App.memgraph -sortBySize" /><figcaption aria-hidden="true">heap App.memgraph-sortBySize</figcaption></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/e81f7edf616ec46726f7.jpg/htz4MHnWjuOgsFm.jpg"alt="heap App.memgraph -addresses all | " /><figcaption aria-hidden="true">heap App.memgraph -addresses all |<classes-pattern></figcaption></figure><h3 id="enabling-malloc-stack-logging">Enabling Malloc StackLogging</h3><p>在 <strong>Product -> Scheme -> Edit Scheme ->Diagnostics</strong> 中,开启 <strong>Malloc Stack</strong>功能,建议使用 <strong>Live Allocations Only</strong> 选项。之后 lldb会记录调试过程中对象创建的堆栈,配合 malloc_history工具,就可以定位到那些占用了过大内存的对象是哪里创建的。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/5ca0e5eb0749ef021f2b.jpg/soVWiYbZBxQ9j7e.jpg" /></p><h3 id="malloc_history">malloc_history</h3><p>查看内存分配历史。</p><figure class="highlight bash"><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">malloc_history App.memgraph [address]</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看文档</span></span><br><span class="line">man malloc_history</span><br></pre></td></tr></table></figure><figure><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/b54804489d5c94e9f393.jpg/gn9PbhJxAtj5DVO.jpg"alt="malloc_history App.memgraph [address]" /><figcaption aria-hidden="true">malloc_history App.memgraph[address]</figcaption></figure><blockquote><p>参考文章: - <a href="https://juejin.im/post/6844903902169710600">iOSMemory 内存详解</a> - <ahref="https://zhuanlan.zhihu.com/p/92286186">存储器层次结构</a> - <ahref="https://blog.csdn.net/qq_21792169/article/details/82956472">浅谈CPU寻址内存机制</a>- <ahref="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/AboutMemory.html#//apple_ref/doc/uid/20001880-BCICIHAB">MemoryUsage Performance Guidelines</a> - <ahref="https://blog.devtang.com/2016/07/30/ios-memory-management/">理解iOS 的内存管理</a> - <ahref="https://wetest.qq.com/lab/view/367.html">iOS微信内存监控</a> - <ahref="https://juejin.im/post/6844903621276991502">WWDC 2018:iOS内存深入研究</a> - <ahref="https://developer.apple.com/videos/play/wwdc2018/416/">WWDC 2018Session 416:iOS Memory Deep Dive</a></p></blockquote>]]></content>
<summary type="html"><p>iOS 的内核是 XNU,XNU 是 Darwin 的一部分,而 Darwin 又是基于 FreeBSD
和 NetBSD 开发,集成了 Mach 微内核,BSD 是基于 UNIX。虽然 Linux 也是基于
UNIX,但 Darwin 和 Linux 没有直接继承的关系。内核 Darwin 是 C
写的,中层框架和库时 C 和 Objective-C 写的。</p>
<p>本文先从一般桌面操作系统的内存机制入手;接着从 iOS 系统层进行分析 iOS
的内存机制及 iOS 系统运行时的内存占用情况;最后到 iOS 中单个 App
的内存管理。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="基础" scheme="http://example.com/tags/%E5%9F%BA%E7%A1%80/"/>
</entry>
<entry>
<title>iOS 启动速度优化调研</title>
<link href="http://example.com/2019/12/15/2019-12-15-ios-speed/"/>
<id>http://example.com/2019/12/15/2019-12-15-ios-speed/</id>
<published>2019-12-14T16:00:00.000Z</published>
<updated>2019-12-14T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>本篇主要是对应用启动时间优化的梳理。</p><span id="more"></span><h2 id="启动过程的技术调研">启动过程的技术调研</h2><p>App 总启动时间 t 分为两部分:</p><ul><li>main() 之前的加载时间 t1</li><li>main() 之后的加载时间 t2</li></ul><p>即 t = t1 + t2。</p><p>其中 t1 = 系统 dylib(动态链接库)加载时间 + App可执行文件加载时间;t2 = 从 main() 方法执行到 AppDelegate 类中<code>didFinishLaunchingWithOptions:</code> 方法执行结束前的时间。</p><p>依次看下 t1、t2 都做了什么。</p><h3 id="main-调用之前的加载">main() 调用之前的加载</h3><p>App 启动后,系统会先加载 App 中所有的可执行文件(.o文件集合);然后加载动态链接库 dyld(dyld是专门用来加载动态链接库的)。</p><p>dyld 从可执行文件中递归所有依赖的动态链接库。动态链接库有:</p><ul><li>iOS 中所有系统 framework</li><li>libobjc(用于加载 OC runtime 方法)</li><li>libSystem(如 GCD 的 libdispatch、Block 的 libsystem_blocks)</li></ul><p>系统链接库和 App 本身的可执行文件,都是 image,每个 App 是以 image为单位进行加载的。</p><h4 id="image">image</h4><p>image 有:</p><ul><li>可执行文件(.o 文件)</li><li>dylib 动态链接库(动态链接库+相应资源包,如 UIKit、Foundation等)</li></ul><h4 id="关于动态链接">关于动态链接</h4><p>动态链接的好处:</p><ul><li>代码公用。很多程序动态链接这些 lib,但内存和磁盘中只有一份。</li><li>易于维护。因被依赖的 lib 在程序运行时才链接,所以这些 lib可以很容易被更新。</li><li>减少了可执行文件的体积。相比静态链接,动态链接不需要在编译时打包到包内,可执行文件小了很多。</li></ul><p>所有动态链接库 framework、静态库 .a、所有类编译后的.o文件,最终都是通过 dyld(动态链接器)加载到内存中。每个 image 都由一个ImageLoader 的类来负责加载。</p><h4 id="imageloader">ImageLoader</h4><p>image 表示二进制文件,ImageLoader的作用是将这些文件加载到内存,且一一对应,每个 ImageLoader对应加载一个文件。</p><p>在程序运行时,先将动态链接的 image 递归加载,再从可执行文件 image递归加载所有符号。</p><h4 id="动态链接库加载的具体流程">动态链接库加载的具体流程</h4><p>加载主要分为 5 步:</p><ul><li>load dylibs image</li><li>rebase image</li><li>bind image</li><li>objc setup</li><li>initializers</li></ul><ol type="1"><li>load dylibs image</li></ol><p>在加载每个动态库时,dyld 需要: - 分析依赖的动态库 - 找到动态库的mach-o 文件 - 打开文件 - 验证文件 - 在系统核心注册文件签名 -对动态库的每个 segment 调用 mmap()</p><p>系统库由于被优化,加载会很快,这里加载可做的优化有: -减少非系统库的依赖 - 合并非系统库 - 部分库,通过拷贝代码的方式引入</p><ol start="2" type="1"><li>rebase/bind image</li></ol><p>由于 ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,需要先修复image 的指针,再指向正确地址。</p><p>rebase 修复指向当前 image 内部的资源指针;bind 指向 image外部的资源指针。</p><p>rebase 的步骤: - 将镜像读入内存 - 以 page为单位进行加密验证,保证不会被篡改</p><p>rebase 之后再进行 bind,bind 步骤: - 查询符号表,指向跨 image的资源</p><p>该阶段可优化的点: - 减少 objc 类的数量,减少 selector 数量 - swift多使用 struct,以减少符号数量</p><ol start="3" type="1"><li>objc setup</li></ol><p>这一步: - 注册 objc 类 - 把 category 的定义插入方法列表 - 保证每个selector 唯一</p><p>如果前面减少了依赖和减少了 objc 类数量及 selector数量,则这一步不在需要额外优化。</p><ol start="4" type="1"><li>initializers</li></ol><p>前面三步都是在修改<code>_DATA segment</code>,这一步开始在堆和栈中写入内容。具体有:</p><ul><li>objc <code>+load</code></li><li>其他构造函数(如 c++)</li></ul><p>具体顺序:</p><ul><li>dyld 开始将程序二进制文件初始化</li><li>交由 ImageLoader 读取 image,包含了类、方法及各种符号</li><li>由于 runtime 向 dyld 绑定了回调。当 image 加载到内存后,dyld 会通知runtime 去处理</li><li>runtime 收到通知后,调用 mapImages 做析构和处理。接着 loadImages中调用 callloadmethods 方法。遍历所有加载进来的class,按继承层级依次调用 class 的 +load 方法及其 category 的 +load方法</li></ul><p>到这里,可执行文件和动态库的所有内容(class、selector、IMP等)都已按格式加载到内存,被runtime 所管理。接着,runtime 的一些黑科技,如 swizzle 才可以生效。</p><p>在初始化完成后,dyld 调用 main 函数。如果 App 是第一次被运行,App的代码会被 dyld 缓存,因此杀掉进程再次打开 App时,会发现还是很快。但如果该 App 长时间未启动或当前 dyld 的缓存被其他App占用,则需要再进行前面的链接加载过程,时间会长些。这也是冷启动和热启动的概念。</p><h4 id="t1-的时间如何得到">t1 的时间如何得到?</h4><p>真机调试时,Scheme 中 Run 开启<strong><code>DYLD_PRINT_STATISTICS</code></strong>,会得到类似如下输入(截图来自官方session):</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/a16a510624ed746e621c.png/3ElR127IdxqugBH.png" /></p><p>根据图可以看到主要耗时在 image 加载和 oc 类的初始化。</p><p>所以针对 main() 调用之前的加载可以优化的点有:</p><ul><li>减少不必要的 framework,以减少动态链接的耗时</li><li>合并或删减一些 oc 类。可以通过 AppCode 检测当前没用的类<ul><li>无用静态变量</li><li>废弃的方法</li></ul></li><li>将 +load 方法中的实现尽量延迟到 +initialize</li></ul><h3 id="main-调用之后的加载">main() 调用之后的加载</h3><p>main() 调用之后,主要进行的是初始化相关的服务,显示首页内容。</p><p>视图的渲染分三个阶段:</p><ul><li>准备阶段。图片的解码。</li><li>布局阶段。首页所有 UIView 的 <code>layoutSubViews</code> 运行</li><li>绘制阶段。首页所有 UIView 的 <code>drawRect:</code> 运行</li></ul><p>接着,是启动之后的必要初始化、一些数据的创建和读取。</p><p>所以针对 main() 调用之后可优化的点有:</p><ul><li>使用代码加载首页视图,不使用 xib</li><li>一些非必要的初始化可以延后</li><li>每次 NSLog 会隐式创建一个 Calendar,只在内测时开启 log</li><li>启动时的网络请求异步处理</li></ul><p>main() 调用之后的耗时,可以借助 Instruments 的 Time Profiler工具查看。</p>]]></content>
<summary type="html"><p>本篇主要是对应用启动时间优化的梳理。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
</entry>
<entry>
<title>iOS IPA 包体积优化</title>
<link href="http://example.com/2019/11/10/2019-11-10-ios-ipa/"/>
<id>http://example.com/2019/11/10/2019-11-10-ios-ipa/</id>
<published>2019-11-09T16:00:00.000Z</published>
<updated>2019-11-09T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>记录 IPA 包体积优化过程中的一些思路。</p><span id="more"></span><h2 id="背景">背景</h2><p>随着业务量的不断增加,应用安装包大小也在迅速的增长,而安装包大小的增加对应用的推广拉新会造成比较大的影响。因为这样,我们进行了2 期的安装包大小的优化,第一期优化从 107MB 到 84MB,第二期优化8MB。本篇主要是用于记录优化过程中的一些思路。</p><h2 id="优化方向">优化方向</h2><p>优化前我们首先要确定的是<strong>优化指标</strong>及<strong>优化方向</strong>。我们最终是以ipa 包中 <strong>xxx.app</strong>大小作为优化前后的对比项。优化方向上,首先我们需要看下 ipa 包中<strong>xxx.app</strong> 有哪些组成。</p><p>解压 <strong>xxx.app</strong>,会看到的模块有: -AppIcons。各尺寸的应用图标。 - Assets.car。Asset Catalog 的编译产物。 -Frameworks。存放着动态库。 - 编译后的可执行文件 - PlugIns。插件路径(如Today Extension) - 其他</p><p>通过文件结构,我们大概有的优化思路是: -资源文件(图片或配置文件)的处理 -代码的处理,以减小编译后可执行文件大小 - Today Extension 的代码优化 -编译配置是否有可优化的点</p><p>根据这 4 个优化方向,具体看下对应的优化方案。</p><h2 id="优化方案">优化方案</h2><h3 id="资源文件优化">资源文件优化</h3><h4 id="图片资源">图片资源</h4><p>关于图片,我们使用的也是通用的方案,主要是两方面的处理: -无用图片的移除 - 图片压缩</p><p>项目从 3.8.0 版本开始,图片资源除去启动图及部分场景下使用的AppIcon,其他图片全部迁移到了 <strong>Images.xcassets</strong>中,所以针对 Images.xcassets 进行图片处理。</p><ol type="1"><li>无用图片的移除<ul><li>通过资源关键字进行全局匹配,筛选出未使用的资源文件</li><li>这里我们使用的是 <ahref="https://github.com/tinymind/LSUnusedResources">LSUnusedResources</a></li><li>查找出图片后,需要人工再对图片的使用情况做确定。之后对应删除</li></ul></li><li>图片压缩<ul><li>经过上一步的无用图移除,剩下的图片是当前版本所需要的,可以对这些图片进行无损压缩</li><li>因为 <ahref="https://tinypng.com/">TinyPNG</a>有限制,最终我们使用的是批量处理工具<a href="https://github.com/iSparta/iSparta">iSparta</a></li></ul></li></ol><p>比较惊讶的是,我们查找到了 15869.44KB(15MB)的无用图片及压缩了6MB。</p><h4 id="查找重复文件">查找重复文件</h4><p>我们通过 <ahref="https://github.com/adrianlopezroche/fdupes">fdupes</a>工具来扫描查找重复文件。fdupes 是通过校验所有资源的MD5,筛选出项目的重复资源。文件比较顺序依次为:</p><ul><li>大小对比</li><li>部分 MD5 签名对比</li><li>完整 MD5 签名对比</li><li>逐字节对比</li></ul><p>相关命令: <figure class="highlight bash"><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">brew install fdupes</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看某文件夹下的重复文件</span></span><br><span class="line">fdupes -Sr ./filepath > outlog.txt</span><br></pre></td></tr></table></figure></p><h4 id="大文件转下载">大文件转下载</h4><p>非必要的大文件资源,如字体库、皮肤资源、子页面大图,不放到 ipa包中,转为下载的方式。</p><h4 id="关于-images.xcassets">关于 Images.xcassets</h4><p>iOS9 开始苹果建议将图片资源文件放入 Images.xcassets中。Images.xcassets中的图片在加载后会有缓存,可以提升加载速度;打包时会进行自动的 PNG图片压缩,再根据运行设备的不同分发 x2/x3 图片资源。</p><p>Pod 库也可以通过 Images.xcassets 来存放图片资源,在 podspec 中指定resource_bundles:</p><figure class="highlight ruby"><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">s.resource_bundles = {</span><br><span class="line"><span class="string">'podname'</span> => [<span class="string">'./Assets/*.xcassets'</span>]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>相关的编译配置: - Compress PNG Files - Remove Text Metadata From PNGFiles</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/e65d83300e63700e28dc.jpg/eapx2uVosiERQjK.jpg" /></p><h3 id="代码层面优化">代码层面优化</h3><p>按责分配模块,review代码,去除重复代码。也是工作量比较大的一个模块。</p><h3 id="编译选项配置">编译选项配置</h3><ol type="1"><li><p>指定编译生成包所支持的架构 <strong>Build Settings -> ValidArchitectures</strong> 中,因为不再支持 iOS9 以下及 32位,我们去掉了对armv7 的支持,以减小 ipa 包大小。 <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/ba49ac08d769e824ad74.jpg/xYdZObM5yheKuHP.jpg" /></p></li><li><p>编译器优化级别 <strong>Build Settings -> OptimizationLevel</strong> 在 release 版选择 Fastest,Smallest。会优化可执行文件的大小,使其尽可能小。</p></li><li><p>去除符号信息 <strong>Build Settings</strong> 中 <strong>StripLinked Product / Deployment Postprocessing</strong> 在 release版本设置为 YES,可以去除不必要的调试符号。 Strip Linked Product 默认为YES,Deployment Postprocessing 默认为 NO,而 Strip Linked Product 在Deployment Postprocessing 设置为 YES 的时候才生效。</p></li></ol><p>这些配置选项的在旧版 Xcode生成的项目中不一定默认遵循,可以检查下,以达到一定的编译优化。</p><h3 id="app-thinning">App Thinning</h3><p>iOS9 开始引入了 App Thinning,相关 Session 介绍:<ahref="https://developer.apple.com/videos/play/wwdc2015/404/">WWDC15 -App Thinning in Xcode</a>。</p><p>App Thinning的概念是,当用户安装应用时,苹果会根据当前用户的机型来选择合适的资源文件及对应CPU 架构的二进制可执行文件(即不会同时存在 armv7、arm64的情况),减少了用户的下载流量和安装占用的空间。</p><p>App Thinning 的开启方式是,在上传 App 时,配置 App Thinning即可。</p><p>App Thinning 分为三部分: - Slicing - Bitcode - On-DemandResources</p><h4 id="slicing">Slicing</h4><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/c4ba946b8b6249162e83.png/kt5oGmqTswx2Qgc.png" /></p><p>原理入图所示。在我们将 Archive 的 App Record 传到 iTunes Connect后,它会被分割为不同的变体,不同的点是<strong>CPU架构的二进制可执行文件</strong>(如 armv7 或 arm64)及<strong>资源文件</strong>(如 Image.xcassets 中 x2/x3 的图片)。</p><p>当用户从 App Store 下载时,只会下载特定的变体。</p><h4 id="bitcode">Bitcode</h4><p>Bitcode 是编译好的程序中间码,在苹果推出新的架构或有新的 LLVM优化时,不需要重新上传 App。</p><p>开启方式是,<strong>Build Settings -> Enable Bitcode</strong> 置为YES 即可,需要 App 中所有的静态库和动态库都支持,避免编译失败。</p><h4 id="on-demand-resources">On-Demand Resources</h4><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/7a84fc48eaedc392f459.png/xbujfLloSBYDdH3.png" /></p><p>按需加载资源。在 Build Settings 中需要开启 <strong>Enable On DemandResource</strong>,接着在 <strong>Resource Tags</strong>中添加对应的资源文件,使用时通过 <code>NSBundleResourceRequest</code>获取按需加载的资源文件。</p><p>更多详细介绍:<ahref="https://developer.apple.com/videos/play/wwdc2015/214/">WWDC15 -Introducing On Demand Resources</a></p><h3 id="其他方向">其他方向</h3><ul><li>Framework 只保留需要的指令集。可以通过 lipo 命令处理。</li><li>App Extension 中使用动态库,以和主 App 共享同一个库。</li><li>第三方库在引入时慎重,尽量只引入需要的模块。</li></ul><h2 id="总结">总结</h2><p>主要是从资源文件、code review 及编译选项配置几个方面,进行 App的瘦身。过程中要利用苹果自身的机制,如 Image.xcassets、App Thinning机制。</p><p>iOS项目在开发初期,我们很容易忽略安装包大小的增长,除去后期的优化,在日常开发中也需要关注的一些点。</p><ul><li>资源文件在引入时,需要按需做无损压缩和有损压缩</li><li>不要为了一个小功能而引入一个大的库</li><li>平时开发过程中,废弃模块要及时清理</li><li>同类开源库,只引入一个</li><li>最好能够有预警机制,打完包后,能够生产一份分析文件,列出与上次包的大小对比</li></ul>]]></content>
<summary type="html"><p>记录 IPA 包体积优化过程中的一些思路。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="优化相关" scheme="http://example.com/categories/%E4%BC%98%E5%8C%96%E7%9B%B8%E5%85%B3/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="优化相关" scheme="http://example.com/tags/%E4%BC%98%E5%8C%96%E7%9B%B8%E5%85%B3/"/>
</entry>
<entry>
<title>YYText 中的 CoreText</title>
<link href="http://example.com/2019/11/07/2019-11-07-yytext/"/>
<id>http://example.com/2019/11/07/2019-11-07-yytext/</id>
<published>2019-11-06T16:00:00.000Z</published>
<updated>2019-11-06T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>富文本框架里<ahref="https://github.com/ibireme/YYText">YYText</a>在性能方面的表现很出色,它基于CoreText 做了大量基础处理并实现了两个上层视图组件:YYLabel 和YYTextView。在了解富文本处理之前,我们还需要对 CoreText基础知识做一些了解。本篇主要梳理 YYText 中 CoreText的底层基础部分处理。</p><span id="more"></span><h2 id="框架概述">框架概述</h2><p>iOS 中我们常会在主线程中进行 UI的绘制,但当绘制压力过大时会造成页面卡顿情况的出现。一种解决思路是,通过多线程在异步线程进行图形的绘制,以减轻主线程的压力。</p><p>YYText 框架的实现思路也是这样的。</p><ul><li>创建自定义绘制线程</li><li>在该线程中创建图形上下文</li><li>通过 CoreText 绘制富文本,通过 CoreGraphics绘制图片、阴影、边框等</li><li>最后将绘制完成得到的位图,回到主线程展示</li></ul><h2 id="coretext-工具类">CoreText 工具类</h2><p>关于 CoreText 的结构图: <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/b3540a4be1b7d1fa7dc5.png/t6qPkf4aDEJ3SzH.png" /><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/cbf7d3ce0902742b3474.png/TDtzIo6BAir9SRf.png" /></p><h3 id="yytextrundelegate">YYTextRunDelegate</h3><p>富文本中为定制一段区域的大小,可以在富文本中插入 key 为<code>kCTRunDelegateAttributeName</code> 的<code>CTRunDelegateRef</code>实例。通过这种方式来预留空白,以用来填充图片,进行图文的混排。作者可能考虑到CFRunDelegateRef本身的使用会比较繁琐,为了简易使用,进行了封装。也就是这里的YYTextRunDelegate。</p><p>内部实现的思路:</p><ul><li>通过 <code>CTRunDelegateCreate()</code> 创建一<code>CTRunDelegateRef</code></li><li>通过 <code>__bridge_retained</code> 转移内存管理,持有一个<code>YYTextRunDelegate</code> 对象</li></ul><p>一些细节处理:</p><ul><li>内存管理问题。<code>CTRunDelegateRef</code> 实例持有<code>YYTextRunDelegate</code>,当 <code>CTRunDelegateRef</code>实例释放时,会调用 <code>DeallocCallback()</code>,将内存管理权限转移给<code>YYTextRunDelegate</code> 局部变量的 ARC。</li><li><strong>CTRunDelegateCreate()</strong> 里做了 copy操作。这里是深拷贝,目的是为了创建一个副本,以保证配置数据的安全性,不会被篡改。</li></ul><h3 id="yytextline">YYTextLine</h3><p>创建一个富文本,拿到 CTRunRef、CTLineRef 及一些结构数据(如ascent、descent)。</p><h4 id="line-位置及大小的计算">line 位置及大小的计算</h4><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="comment">// 不考虑竖排版</span></span><br><span class="line">_bounds = <span class="built_in">CGRectMake</span>(_position.x, _position.y - _ascent, _lineWidth, _ascent + _descent);</span><br><span class="line">_bounds.origin.x += _firstGlyphPos;</span><br></pre></td></tr></table></figure><ul><li>**_position** 指 line 的 origin 点位于 context 上下文的坐标转换为UIKit 坐标系的值。</li><li><code>_position.y - _ascent</code> 表示 y 起始位置</li><li><code>_ascent + _descent</code> 表示 line 高度</li><li><code>_firstGlyphPos</code> 表示第一个 run 相对 line 的偏移</li></ul><h4 id="找出占位-run">找出占位 run</h4><p>基本原理是通过 <code>CTRunDelegateRef</code> 占位,再用<code>YYTextAttachment</code> 填充。当遍历 line 里的 run 时,若该 run内有 <code>YYTextAttachment</code>,说明是占位 run,计算这个 run的位置和大小,便于后面填充。</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">runPosition.x += _position.x;</span><br><span class="line">runPosition.y = _position.y - runPosition.y;</span><br><span class="line">runTypoBounds = <span class="built_in">CGRectMake</span>(runPosition.x, runPosition.y - ascent, runWidth, ascent + descent);</span><br></pre></td></tr></table></figure><ul><li><code>runPosition</code> 表示相对 line 的 x/y 偏移量</li><li>最后,缓存 <code>YYTextAttachment</code> 和 run 位置大小信息</li></ul><h3 id="yytextcontainer">YYTextContainer</h3><p>CTFrameRef 需要通过 <strong>CTFramesetterCreateFrame()</strong>创建,该方法需要 <code>CGPathRef</code> 作为参数,作者封装了<code>YYTextContainer</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> Example:</span></span><br><span class="line"><span class="comment"> </span></span><br><span class="line"><span class="comment"> ┌─────────────────────────────┐ <------- container</span></span><br><span class="line"><span class="comment"> │ │</span></span><br><span class="line"><span class="comment"> │ asdfasdfasdfasdfasdfa <------------ container insets</span></span><br><span class="line"><span class="comment"> │ asdfasdfa asdfasdfa │</span></span><br><span class="line"><span class="comment"> │ asdfas asdasd │</span></span><br><span class="line"><span class="comment"> │ asdfa <----------------------- container exclusion path</span></span><br><span class="line"><span class="comment"> │ asdfas adfasd │</span></span><br><span class="line"><span class="comment"> │ asdfasdfa asdfasdfa │</span></span><br><span class="line"><span class="comment"> │ asdfasdfasdfasdfasdfa │</span></span><br><span class="line"><span class="comment"> │ │</span></span><br><span class="line"><span class="comment"> └─────────────────────────────┘</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>开发者可以通过 CGSize 来设定富文本大小,也可以通过 UIBezierPath定制路径。同时,CoreText 还支持镂空效果,通过 exclusionPaths 控制。</p><h2 id="yytextlayout">YYTextLayout</h2><p>包含了布局一富文本所有的信息,这个文件里也包含很多绘制相关的 C代码。YYTextLayout 负责计算各种数据,为后面的绘制做准备。</p><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">+ (YYTextLayout *)layoutWithContainer:(YYTextContainer *)container text:(<span class="built_in">NSAttributedString</span> *)text range:(<span class="built_in">NSRange</span>)range;</span><br></pre></td></tr></table></figure></p><h3 id="计算绘制路径和路径的位置矩形">计算绘制路径和路径的位置矩形</h3><p>基于 YYTextContainer 对象计算得到 CGPathRef。UIKit 转为 CoreText坐标,需要先进行坐标处理。得到 <code>pathBox</code>,pathBox是真正的绘制区域相对于绘制上下文的位置和大小。在后面计算 line 和 run位置时,都需要这里的 <code>cgPathBox.origin</code>。</p><h3 id="初始化-ctframesetterref-和-ctframeref">初始化 CTFramesetterRef和 CTFrameRef</h3><h3 id="计算-line-总-frame">计算 line 总 frame</h3><ul><li>前面 TextContainer 得到 <code>CTFrameRef</code></li><li>接着遍历所有的 line,结合 <code>cgPathBox.origin</code> 计算得到每个line 的位置和大小</li><li>最后将每个 line 的 rect 合并,得到包含所有 line 的最小位置矩形<code>textBoundingRect</code>。</li></ul><h3 id="计算-line-的行数">计算 line 的行数</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/7a58bc3f6a122b31bb2b.png/4PrDjvsxtheB2JY.png" />在有排除路径的情况下,一行可能有多个 line。所以需要计算每个 line所在的行。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/b23223b6bca5fee4e817.png/QhupfCHj2dWVDBI.png" />当 line 高度大于 lastline 高度时。若 lastline 的 baseline 在 line 的<strong>y0~y1</strong> 之间,说明未换行。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/2abb36cf39c2807606d7.png/AsBquM2NzxicHyD.png" />当 line 高度小于 lastline 高度时。若 line 的 baseline 在 lastline 的<strong>y0~y1</strong> 之间,说明未换行。</p><p>确定了 line 的换行规则,可以计算得到 line 的行数。</p><h3 id="获取行上下边界的数组">获取行上下边界的数组</h3><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"><span class="keyword">typedef</span> <span class="keyword">struct</span> {</span><br><span class="line"> <span class="built_in">CGFloat</span> head;</span><br><span class="line"> <span class="built_in">CGFloat</span> foot;</span><br><span class="line">} YYRowEdge;</span><br></pre></td></tr></table></figure><p>YYRowEdge 表示每一行的上下边界。遍历所有 line,当当前 line 和 lastline 为同一行时,取 line 和 last line 最大上下边界;当当前 line 和 lastline 不同行时,取当前 line 的上下边界。</p><p>结果如图示: <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/69ce417926aca2205f6a.png/6MbvWlCkwo5p48L.png" /></p><p>中间的间隙为行间距,YYText 将行间距进行了均分,如图示:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/5c774bbbfd037cf8e4c2.png/u9tU4bMrEmYCXzV.png" /></p><h3 id="计算绘制区域的总大小">计算绘制区域的总大小</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/feaac3ecc6d318774d1f.png/7cnjGSNq46xm2Rd.png" /></p><p>前面通过 <code>YYTextContainer</code> 计算得到绘制路径的位置矩形pathBox(上图蓝色区域)。但这是实际绘制区域的大小,但应用场景中还会有inset、borderWidth 之类的情况。所以实际业务需要的绘制区域会更大。</p><h3 id="line-截断">line 截断</h3><p>当富文本超过限制时,通常会看到文本最后有点省略号:<code>text...</code>。YYText支持自定义后缀,即 <code>truncationToken</code>。</p><p>YYTextLine 总是在富文本最后,当 lastLineText 超出绘制范围,通过<code>CTLineCreateTruncatedLine(...)</code> 创建自动计算的截断line,会返回一个 <code>CTLineRef</code>,框架将其转化为<code>YYTextLine</code> 作为 YYTextLayout 的一个属性<code>truncatedLine</code>。</p><h2 id="自定义富文本属性">自定义富文本属性</h2><p>原理是遍历富文本,找到某个 run 是否包含自定义的key,接着做对应的绘制逻辑。</p><h3 id="图文混排的实现">图文混排的实现</h3><p>前面提到过,如果想要在富文本中添加 UIImage、UIView之类的附件,需要先设置一个占位符<code>CTRunDelegateRef</code>,具体看下占位的逻辑。</p><h4 id="对齐方式">对齐方式</h4><p>图文混排添加附件有三种对齐方式:居上对齐、居中对齐、局下对齐。通过ascent、descent、baseline 控制。</p><ol type="1"><li>居上对齐</li></ol><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/c77b89cab659beec5e82.png/OrcxWt1kuhi6KYU.png" /></p><p>run 的 ascent 对齐文本的 ascent。</p><ol start="2" type="1"><li>居下对齐</li></ol><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/a21bd03b2e62334617e3.png/O7B5XUmNozHy6FY.png" /></p><p>run 的 descent 对齐文本的 descent。若 run 太矮,则居上对齐文本的baseline。</p><ol start="3" type="1"><li>居中对齐</li></ol><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/16e3a2bbae069f8052f3.png/eQ5WIxaGZvYuglR.png" /></p><p>居中相对会复杂些,run 的 center 和文本的 center 对齐。<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">center = (ascent + descent) / 2</span><br></pre></td></tr></table></figure></p><p>若 run 太矮,则贴着 baseline。</p><h4 id="绘制附件">绘制附件</h4><p>绘制逻辑在 <strong>YYTextLayout</strong> 的<code>YYTextDrawAttachment(...)</code> 方法中。</p><ol type="1"><li>若附件为 UIImage,会根据占位 run 的位置和大小,通过 CoreGraphics API绘制图片:</li></ol><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"><span class="built_in">CGImageRef</span> ref = image.CGImage;</span><br><span class="line"><span class="keyword">if</span> (ref) {</span><br><span class="line"> <span class="built_in">CGContextSaveGState</span>(context);</span><br><span class="line"> <span class="built_in">CGContextTranslateCTM</span>(context, <span class="number">0</span>, <span class="built_in">CGRectGetMaxY</span>(rect) + <span class="built_in">CGRectGetMinY</span>(rect));</span><br><span class="line"> <span class="built_in">CGContextScaleCTM</span>(context, <span class="number">1</span>, <span class="number">-1</span>);</span><br><span class="line"> <span class="built_in">CGContextDrawImage</span>(context, rect, ref);</span><br><span class="line"> <span class="built_in">CGContextRestoreGState</span>(context);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="2" type="1"><li>若附件为 UIView、CALayer,需要传入额外的superView、superLayer。再将绘制的 UIView、CALayer 添加到superView、superLayer。</li></ol><h3 id="点击高亮的实现">点击高亮的实现</h3><p>YYTextHighlight 包含单击和长按的回调、及一些显示的属性配置。</p><p>如 YYLabel 中的触发逻辑。先判断点击的 CGPoint对应的富文本位置,检测文本是否有对应的手势处理。若有对应的YYTextHighlight 处理,则更换 YYTextLine 为高亮YYTextLine,重绘。松手时,恢复 YYTextLine。</p><p>简而言之,通过检测点击 CGPoint 是否有对应的手势处理,若有替换对应的YYTextLine,重新绘制。</p><h2 id="总结">总结</h2><p>本篇主要是对 YYText 中 CoreText 处理的原理做了梳理,从 CTFrameRef 到CTLineRef 到 CTRunRef。接着基于 line 和 run 做具体的展开,如:</p><ul><li>line 的行数确定</li><li>line 的换行规则</li><li>line 的截断</li><li>run 的对齐</li><li>富文本附件的添加原理</li></ul>]]></content>
<summary type="html"><p>富文本框架里<a
href="https://github.com/ibireme/YYText">YYText</a>在性能方面的表现很出色,它基于
CoreText 做了大量基础处理并实现了两个上层视图组件:YYLabel 和
YYTextView。在了解富文本处理之前,我们还需要对 CoreText
基础知识做一些了解。本篇主要梳理 YYText 中 CoreText
的底层基础部分处理。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="源码阅读" scheme="http://example.com/categories/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="源码阅读" scheme="http://example.com/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
</entry>
<entry>
<title>iOS 编译过程梳理</title>
<link href="http://example.com/2019/09/14/2019-09-14-ios-analyse-llvm/"/>
<id>http://example.com/2019/09/14/2019-09-14-ios-analyse-llvm/</id>
<published>2019-09-13T16:00:00.000Z</published>
<updated>2019-09-13T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>iOS 开发常用的语言是 Objective-C 和Swift,两者都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU 上执行,所以执行效率很高。本篇主要用于梳理 Objective-C的编译过程。</p><span id="more"></span><h2 id="编译器的概述">编译器的概述</h2><p>编译器的作用是把我们的高级语言转换成机器可以识别的机器码,经典的设计结构如下:</p><p><img src="https://i.loli.net/2020/09/18/ayW6iNYAZ7FCqDz.png" /></p><ul><li>前端(Frontend):语法分析,语义分析和生成中间代码。在这个过程中,也会对代码进行检查,如果发现出错的或需要警告的会标注出来。</li><li>优化器(Optimizer):会进行 BitCode 的生成,链接期优化等工作。</li><li>后端(Backend):针对不同的架构,生成对应的机器码。</li></ul><h2 id="clang-llvm-的编译过程">Clang + LLVM 的编译过程</h2><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#import <span class="string"><Foundation/Foundation.h></span></span></span><br><span class="line"> </span><br><span class="line"><span class="type">int</span> main (<span class="type">int</span> argc, <span class="keyword">const</span> <span class="type">char</span> * argv[])</span><br><span class="line">{</span><br><span class="line"> <span class="keyword">@autoreleasepool</span></span><br><span class="line"> { </span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Hello, Obj"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>命令行执行:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">clang -ccc-print-phases -framework Foundation main.m -o main</span><br></pre></td></tr></table></figure><p>可以看到编译源文件需要的几个阶段为:</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">$ Desktop clang -ccc-print-phases -framework Foundation main.m -o main</span><br><span class="line"></span><br><span class="line">0: input, <span class="string">"Foundation"</span>, object</span><br><span class="line">1: input, <span class="string">"main.m"</span>, objective-c</span><br><span class="line">2: preprocessor, {1}, objective-c-cpp-output</span><br><span class="line">3: compiler, {2}, ir</span><br><span class="line">4: backend, {3}, assembler</span><br><span class="line">5: assembler, {4}, object</span><br><span class="line">6: linker, {0, 5}, image</span><br><span class="line">7: bind-arch, <span class="string">"x86_64"</span>, {6}, image</span><br></pre></td></tr></table></figure><p>注释: -<code>2: preprocessor, {1}, objective-c-cpp-output</code>:预处理,编译器前端- <code>3: compiler, {2}, ir</code>:编译生成中间码 ir -<code>4: backend, {3}, assembler</code>:LLVM 后端生成汇编 -<code>5: assembler, {4}, object</code>:生成机器码 -<code>6: linker, {0, 5}, image</code>:链接器 -<code>7: bind-arch, "x86_64", {6}, image</code>:生成可执行的二进制文件(image)</p><p>梳理下流程: 1. <strong>预处理阶段</strong>:import 头文件替换;macro宏展开;处理预编译指令 2.<strong>词法分析</strong>:预处理完成后进入词法分析,将输入的代码转化为一系列符合特定语言的词法单元(token流)。样式如下: <figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">$ xcrun -sdk iphonesimulator clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m</span><br><span class="line"> </span><br><span class="line">annot_module_include <span class="string">'#import <Foundation/Foundation.h>'</span>Loc=<main.m:1:1></span><br><span class="line">int <span class="string">'int'</span> [StartOfLine]Loc=<main.m:3:1></span><br><span class="line">identifier <span class="string">'main'</span> [LeadingSpace]Loc=<main.m:3:5></span><br><span class="line">l_paren <span class="string">'('</span> [LeadingSpace]Loc=<main.m:3:10></span><br><span class="line">int <span class="string">'int'</span>Loc=<main.m:3:11></span><br><span class="line">identifier <span class="string">'argc'</span> [LeadingSpace]Loc=<main.m:3:15></span><br><span class="line">comma <span class="string">','</span>Loc=<main.m:3:19></span><br><span class="line">const <span class="string">'const'</span> [LeadingSpace]Loc=<main.m:3:21></span><br><span class="line">char <span class="string">'char'</span> [LeadingSpace]Loc=<main.m:3:27></span><br><span class="line">star <span class="string">'*'</span> [LeadingSpace]Loc=<main.m:3:32></span><br><span class="line">identifier <span class="string">'argv'</span> [LeadingSpace]Loc=<main.m:3:34></span><br><span class="line">l_square <span class="string">'['</span>Loc=<main.m:3:38></span><br><span class="line">r_square <span class="string">']'</span>Loc=<main.m:3:39></span><br><span class="line">r_paren <span class="string">')'</span>Loc=<main.m:3:40></span><br><span class="line">l_brace <span class="string">'{'</span> [StartOfLine]Loc=<main.m:4:1></span><br><span class="line">at <span class="string">'@'</span> [StartOfLine] [LeadingSpace]Loc=<main.m:5:5></span><br><span class="line">identifier <span class="string">'autoreleasepool'</span>Loc=<main.m:5:6></span><br><span class="line">l_brace <span class="string">'{'</span> [StartOfLine] [LeadingSpace]Loc=<main.m:6:5></span><br><span class="line">identifier <span class="string">'NSLog'</span> [StartOfLine] [LeadingSpace]Loc=<main.m:7:9></span><br><span class="line">l_paren <span class="string">'('</span>Loc=<main.m:7:14></span><br><span class="line">at <span class="string">'@'</span>Loc=<main.m:7:15></span><br><span class="line">string_literal <span class="string">'"Hello, Obj"'</span>Loc=<main.m:7:16></span><br><span class="line">r_paren <span class="string">')'</span>Loc=<main.m:7:28></span><br><span class="line">semi <span class="string">';'</span>Loc=<main.m:7:29></span><br><span class="line">r_brace <span class="string">'}'</span> [StartOfLine] [LeadingSpace]Loc=<main.m:8:5></span><br><span class="line"><span class="built_in">return</span> <span class="string">'return'</span> [StartOfLine] [LeadingSpace]Loc=<main.m:9:5></span><br><span class="line">numeric_constant <span class="string">'0'</span> [LeadingSpace]Loc=<main.m:9:12></span><br><span class="line">semi <span class="string">';'</span>Loc=<main.m:9:13></span><br><span class="line">r_brace <span class="string">'}'</span> [StartOfLine]Loc=<main.m:10:1></span><br><span class="line">eof <span class="string">''</span>Loc=<main.m:10:2></span><br></pre></td></tr></table></figure> 3.<strong>语法分析</strong>:将词法分析得到的 token流进行语法静态分析(StaticAnalysis),输出<strong>抽象语法树(AST)</strong>,过程中会校验语法是否错误。<figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">$ xcrun -sdk iphonesimulator clang -fmodules -fsyntax-only -Xclang -ast-dump main.m</span><br><span class="line"></span><br><span class="line">TranslationUnitDecl 0x7fbdd301ba08 <<<span class="string">invalid sloc>> <invalid</span> sloc> <undeserialized declarations></span><br><span class="line">|-TypedefDecl 0x7fbdd301c2a0 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit __int128_t <span class="string">'__int128'</span></span><br><span class="line">| `-BuiltinType 0x7fbdd301bfa0 <span class="string">'__int128'</span></span><br><span class="line">|-TypedefDecl 0x7fbdd301c310 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit __uint128_t <span class="string">'unsigned __int128'</span></span><br><span class="line">| `-BuiltinType 0x7fbdd301bfc0 <span class="string">'unsigned __int128'</span></span><br><span class="line">|-TypedefDecl 0x7fbdd301c3b0 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit SEL <span class="string">'SEL *'</span></span><br><span class="line">| `-PointerType 0x7fbdd301c370 <span class="string">'SEL *'</span> imported</span><br><span class="line">| `-BuiltinType 0x7fbdd301c200 <span class="string">'SEL'</span></span><br><span class="line">|-TypedefDecl 0x7fbdd301c498 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit <span class="built_in">id</span> <span class="string">'id'</span></span><br><span class="line">| `-ObjCObjectPointerType 0x7fbdd301c440 <span class="string">'id'</span> imported</span><br><span class="line">| `-ObjCObjectType 0x7fbdd301c410 <span class="string">'id'</span> imported</span><br><span class="line">|-TypedefDecl 0x7fbdd301c578 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit Class <span class="string">'Class'</span></span><br><span class="line">| `-ObjCObjectPointerType 0x7fbdd301c520 <span class="string">'Class'</span> imported</span><br><span class="line">| `-ObjCObjectType 0x7fbdd301c4f0 <span class="string">'Class'</span> imported</span><br><span class="line">|-ObjCInterfaceDecl 0x7fbdd301c5d0 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit Protocol</span><br><span class="line">|-TypedefDecl 0x7fbdd301c948 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit __NSConstantString <span class="string">'struct __NSConstantString_tag'</span></span><br><span class="line">| `-RecordType 0x7fbdd301c740 <span class="string">'struct __NSConstantString_tag'</span></span><br><span class="line">| `-Record 0x7fbdd301c6a0 <span class="string">'__NSConstantString_tag'</span></span><br><span class="line">|-TypedefDecl 0x7fbdd3059000 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit __builtin_ms_va_list <span class="string">'char *'</span></span><br><span class="line">| `-PointerType 0x7fbdd301c9a0 <span class="string">'char *'</span></span><br><span class="line">| `-BuiltinType 0x7fbdd301baa0 <span class="string">'char'</span></span><br><span class="line">|-TypedefDecl 0x7fbdd30592e8 <<<span class="string">invalid sloc>> <invalid</span> sloc> implicit __builtin_va_list <span class="string">'struct __va_list_tag [1]'</span></span><br><span class="line">| `-ConstantArrayType 0x7fbdd3059290 <span class="string">'struct __va_list_tag [1]'</span> 1</span><br><span class="line">| `-RecordType 0x7fbdd30590f0 <span class="string">'struct __va_list_tag'</span></span><br><span class="line">| `-Record 0x7fbdd3059058 <span class="string">'__va_list_tag'</span></span><br><span class="line">|-ImportDecl 0x7fbdd30b4218 <main.m:1:1> col:1 implicit Foundation</span><br><span class="line">`-FunctionDecl 0x7fbdd30b44e0 <line:3:1, line:10:1> line:3:5 main <span class="string">'int (int, const char **)'</span></span><br><span class="line"> |-ParmVarDecl 0x7fbdd30b4270 <col:11, col:15> col:15 argc <span class="string">'int'</span></span><br><span class="line"> |-ParmVarDecl 0x7fbdd30b4390 <col:21, col:39> col:34 argv <span class="string">'const char **'</span>:<span class="string">'const char **'</span></span><br><span class="line"> `-CompoundStmt 0x7fbdd30c9190 <line:4:1, line:10:1></span><br><span class="line"> |-ObjCAutoreleasePoolStmt 0x7fbdd30c9148 <line:5:5, line:8:5></span><br><span class="line"> | `-CompoundStmt 0x7fbdd30c9130 <line:6:5, line:8:5></span><br><span class="line"> | `-CallExpr 0x7fbdd30c90f0 <line:7:9, col:28> <span class="string">'void'</span></span><br><span class="line"> | |-ImplicitCastExpr 0x7fbdd30c90d8 <col:9> <span class="string">'void (*)(id, ...)'</span> <FunctionToPointerDecay></span><br><span class="line"> | | `-DeclRefExpr 0x7fbdd30c8fb0 <col:9> <span class="string">'void (id, ...)'</span> Function 0x7fbdd30b4620 <span class="string">'NSLog'</span> <span class="string">'void (id, ...)'</span></span><br><span class="line"> | `-ImplicitCastExpr 0x7fbdd30c9118 <col:15, col:16> <span class="string">'id'</span>:<span class="string">'id'</span> <BitCast></span><br><span class="line"> | `-ObjCStringLiteral 0x7fbdd30c9060 <col:15, col:16> <span class="string">'NSString *'</span></span><br><span class="line"> | `-StringLiteral 0x7fbdd30c9038 <col:16> <span class="string">'char [11]'</span> lvalue <span class="string">"Hello, Obj"</span></span><br><span class="line"> `-ReturnStmt 0x7fbdd30c9180 <line:9:5, col:12></span><br><span class="line"> `-IntegerLiteral 0x7fbdd30c9160 <col:12> <span class="string">'int'</span> 0</span><br></pre></td></tr></table></figure> 4. <strong>CodeGen 生成 IR 中间代码</strong>:CodeGen负责将语法树自顶向下遍历翻译成 <code>LLVM IR</code>,<code>IR</code>是编译过程中前端的输出后端的输入。 5. <strong>Optimize 优化IR</strong>:到这里 LLVM 会做一些优化工作,在 Xcode的编译设置里可以设置优化级别 -01, -03, -0s,也可以写自己的 Pass,Pass 是LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM完整的优化和转化。附件:<ahref="http://llvm.org/docs/WritingAnLLVMPass.html">官方 Pass 教程</a>。6. <strong>LLVM Bitcode 生成字节码</strong>:如果开启了bitcode,苹果会做进一步优化。若有新的后端架构,依旧可以用这份优化过的bitcode 去生成。 7. <strong>生成汇编</strong> 8.<strong>生成目标文件</strong> 9. <strong>生成可执行文件</strong></p><p>我们通过一个命令行操作来复现上述的编译过程:</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 词法分析</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m</span><br><span class="line"><span class="comment"># 语法分析</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -fmodules -fsyntax-only -Xclang -ast-dump main.m</span><br><span class="line"><span class="comment"># CodeGen 生成 IR</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -S -fobjc-arc -emit-llvm main.m -o main.ll</span><br><span class="line"><span class="comment"># Optimize 优化 IR</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll</span><br><span class="line"><span class="comment"># LLVM Bitcode</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -emit-llvm -c main.m -o main.bc</span><br><span class="line"><span class="comment"># 生成汇编</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -S -fobjc-arc main.m -o main.s</span><br><span class="line"><span class="comment"># 生成目标文件</span></span><br><span class="line">xcrun -sdk iphonesimulator clang -fmodules -c main.m -o main.o</span><br><span class="line"><span class="comment"># 生成可执行文件</span></span><br><span class="line">xcrun -sdk iphonesimulator clang main.o -o main</span><br><span class="line"><span class="comment"># 至此,即生成了可供 iphonesimulator 执行的 `可执行文件`</span></span><br></pre></td></tr></table></figure><h2 id="xcode-build-的流程">Xcode Build 的流程</h2><p>我们在 Xcode 中使用 <strong>Command + B</strong> 或 <strong>Command +R</strong> 时,即完成了一次编译,我们来看下这个过程做了哪些事情。</p><p>编译过程分为四个步骤:</p><ul><li>预编译(Pre-process):宏替换、删除注释、展开头文件,产生 .i文件。</li><li>编译(Compliling):把前面生成的 .i 文件转化为汇编语言,产生 .s文件。</li><li>汇编(Asembly):把汇编语言 .s 文件转化为机器码文件,产生 .0文件。</li><li>链接(Link):对 .o文件中的对于其他库的引用的地方进行引用,生成最后的可执行文件。也包括多个.o 文件进行 link。</li></ul><p>通过解析 Xcode 编译 log,可以发现 Xcode 是根据 Target进行编译的。我们可以通过 Xcode 中的 Build Phases、Build Settings 及Build Rules 来控制编译过程。</p><ul><li>Build Settings:这一栏下是对编译的细节进行设定,包含 build过程的每个阶段的设置选项(包含编译、链接、代码签名、打包)。</li><li>BuildPhases:用于控制从源文件到可执行文件的整个过程,如编译哪些文件,编译过程中执行哪些自定义脚本。例如CocoaPods 在这里会进行相关配置。</li><li>BuildRules:指定了不同的文件类型该如何编译。一般我们不需要修改这里的内容。如果需要对特定类型的文件添加处理方法,可以在这里添加规则。</li></ul><p>每个 Target 的具体编译过程也可以通过 log 日志获得。大致过程为:</p><ul><li>编译信息写入辅助文件(如Entitlements.plist),创建编译后的文件架构</li><li>写入辅助信息(.hmap文件)。将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件。</li><li>运行预设的脚本。如 Cocoapods 会在 Build Phases中预设一些脚本(CheckPods Manifest.lock)。</li><li>编译 .m 文件,生成可执行文件 Mach-O。每次进行了 LLVM的完整流程:前端(词法分析 - 语法分析 - 生成 IR)、优化器(优化IR)、后端(生成汇编 - 生成目标文件 - 生成可执行文件)。使用<code>CompileC</code> 和 <code>clang</code> 命令。 CompileC 是xcodebuild 内部函数的日志记录表示形式,它是 build.log文件中有关编译的基本信息来源。</li><li>链接需要的库。如Foundation.framework,AFNetworking.framework...</li><li>拷贝资源文件到目标包</li><li>编译 storyboard 文件</li><li>链接 storyboard 文件</li><li>编译 Asset 文件。如果使用 Asset.xcassets来管理图片,这些图片会被编译为机器码,除了 icon 和 launchIamge。</li><li>处理 infoplist</li><li>执行 CocoaPods脚本,将在编译项目前已编译好的依赖库和相关资源拷贝到包中。</li><li>拷贝 Swift 标准库</li><li>创建 .app 文件并对其签名</li></ul><h2 id="dsym-文件">dSYM 文件</h2><p>在每次编译完成之后,都会生成一个 dsym 文件。dsym 文件中,存储了 16进制的函数地址映射。</p><p>当 App执行打包后的二进制文件时,实际是通过地址来调用方法的。我们可能会用三方的统计工具,例如Fabric 在 App Crash 时会帮我们抓到 crash 时的调用栈,这些调用栈里会包含crash 地址的调用信息。这时可以通过 dSYM文件由地址映射到具体的函数位置。</p><h2 id="预处理">预处理</h2><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEBUG</span></span><br><span class="line"><span class="comment">//...</span></span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line"><span class="comment">//...</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><p>比如我们常会有这样的场景:测版版本使用测试服务器数据,正式版本使用生产服务器数据。我们可以分别为debug 和 release 设置相关的预处理宏。假设名为<code>DEVSERVER</code>。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/060bca21863b7582fdd6.jpg/nVfbQ6pHdCGz15u.jpg" /></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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">ifdef</span> DEVSERVER</span></span><br><span class="line"><span class="comment">// 测试服务器</span></span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line"><span class="comment">// 生产服务器</span></span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure><h2 id="插入脚本">插入脚本</h2><p>如果我们的项目中使用了 CocoaPods,在 Build Phase 里会看到<code>[CP]</code> 开头的脚本。这些是 CocoaPods 插入的脚本。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/efcdf611a0096daf661f.jpg/8mYkXBCc6wnGLfx.jpg" /></p><ul><li><strong>Check Pods Manifest.lock</strong>:检查 cocoapod管理的三方库是否需要更新</li><li><strong>Embed PodsFramework</strong>:运行脚本来链接三方库的静态/动态库</li></ul><p>这些配置信息都存在 <code>.xcodeproj</code> 文件里。CocoaPods 通过修改<code>.xcodeproj</code>,配置编译期的脚本,以保证三方库正确的编译链接。</p><p>这里也有个比较常用的脚本操作。例如,每当我们 Archive时,通常情况下都需要手动修改 target 的 build版本,我们可以让这一步实现自动化。</p><p>添加方式: - Xcode -> Target -> Build Phase -> New RunScript Phase - 写入如下脚本代码 - 勾选 <strong>Run script only wheninstalling</strong> - 重命名脚本为 <strong>[ProjectName]Increase buildnumber</strong></p><figure class="highlight sh"><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"># 脚本信息</span></span><br><span class="line">buildNumber=$(/usr/libexec/PlistBuddy -c <span class="string">"Print CFBundleVersion"</span> <span class="string">"<span class="variable">${PROJECT_DIR}</span>/<span class="variable">${INFOPLIST_FILE}</span>"</span>)</span><br><span class="line">buildNumber=$((<span class="variable">$buildNumber</span> + <span class="number">1</span>))</span><br><span class="line">/usr/libexec/PlistBuddy -c <span class="string">"Set :CFBundleVersion <span class="variable">$buildNumber</span>"</span> <span class="string">"<span class="variable">${PROJECT_DIR}</span>/<span class="variable">${INFOPLIST_FILE}</span>"</span></span><br></pre></td></tr></table></figure><p>这段脚本实现的是:读取当前 plist 的 build 版本号,在其基础上+1,再写入 plist 文件中。</p><h2 id="脚本编译打包">脚本编译打包</h2><p>对于 CI 来说,脚本编译打包十分有用,一个自动打包的配置可做参考:</p><ul><li><ahref="https://github.com/monetking/AutoPacking-iOS">monetking/AutoPacking-iOS</a></li></ul><h2 id="提高项目-build-速度">提高项目 Build 速度</h2><p>随着项目的迭代,源代码及三方库的引入都在持续增加,很明显会发现编译速度变慢。基于之前对编译过程的了解,可以一定程度上优化编译速度。</p><h3 id="指标建立">指标建立</h3><p>首先,编译的快慢我们需要确定一个度量,最直观的表现是<strong>编译时间</strong>。我们通过在终端输入如下指令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ defaults write com.apple.dt.Xcode ShowBuildOperationDuration YES</span><br></pre></td></tr></table></figure><p>完成后重启Xcode,进行一次编译,编译完成后在顶栏即可看到对应的编译时间。</p><h3 id="代码层面的优化">代码层面的优化</h3><h4 id="前向声明forward-declaration">前向声明(ForwardDeclaration)</h4><p>Objective-C 中 <code>#import</code> 和 <code>@class</code>都可以引入一个类。他们区别是:</p><ul><li><code>#import</code>会包含这个类的所有信息,包括<strong>实体变量</strong>和<strong>方法</strong>;而<code>@class</code>只是告诉编译器,其后面声明的名称是<strong>类的名称</strong>,至于这些类是如何定义的,暂不考虑。</li><li>在头文件中,一般只需要知道被引用的类的名称就可以。不需要知道其内部的实体变量和方法,所以在头文件中一般使用<code>@class</code> 来声明这个名称是类的名称。 而在实现类里面使用<code>#import</code>来包含这个被引用类的头文件,因为会用到这个引用类的内部的<strong>实体变量</strong>和<strong>方法</strong>。</li><li>在编译效率方面考虑,如果你有 100 个头文件都 <code>#import</code>了同一个头文件,或者这些文件是依次引用的,如 A–>B, B–>C, C–>D这样的引用关系。当最开始的那个头文件有变化时,后面所有引用它的类都需要重新编译,如果类有很多,这将耗费大量的时间。而是用<code>@class</code> 不会。</li><li>如果有循环依赖关系,如:A–>B, B–>A这样的相互依赖关系,如果使用 <code>#import</code>来相互包含,那么就会出现编译错误,如果使用 <code>@class</code>在两个类的头文件中相互声明,则不会有编译错误出现。</li></ul><p>所以,一般 <code>@class</code> 是放在 <strong>interface</strong>中的,只是为了在 <strong>interface</strong>中引用这个类,把这个类作为一个类型来用的。在实现这个接口的实现类中,如果需要引用这个类的实体变量或者方法之类的,还是需要<strong>import</strong> 在 <strong><span class="citation"data-cites="class">@class</span></strong> 中声明的类进来。使用<code>@class</code>,只能用来定义变量,不能继承,也不能调用该类的方法和变量。使用<code>#import</code> 则可以进行。</p><p>简而言之,是为了编译器能大大提高 <strong>#import</strong>的替换速度。</p><h4id="对常用工具类进行打包framework.a">对常用工具类进行打包(Framework/.a)</h4><p>常用的工具类一般不会有改动,我们可以提前将这部分模块打包成 Framework或静态库。这样编译时这部分代码不需要重新进行编译了。</p><h4 id="常用头文件放到预编译文件里">常用头文件放到预编译文件里</h4><p>Xcode 中 <code>.pch</code> 文件是预编译文件。这里的内容在执行 XcodeBuild 之前就已经被预编译,并引入到每一个 .m 文件中。</p><h3 id="编译器选项的优化">编译器选项的优化</h3><h4 id="控制-dsym-文件的生成">控制 dSYM 文件的生成</h4><p>Debug 模式下,不生成 dSYM 文件。dSYM 文件内存储的是调试信息,在 Debug模式下,我们可以借助 Xcode 和 LLDB 完成调试,不需要生成额外的 dSYM文件。这样可以提高一部分的编译速度。</p><h3 id="编译器优化">编译器优化</h3><p>Debug 模式下,关闭编译器优化。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/6e6e0da2149efef12e17.jpg/qwybdhIQkl8tumF.jpg" /></p><p>Xcode 11 中已默认关闭。</p><blockquote><p>参考内容: - <a href="https://objccn.io/issue-6-1/">Build Rules</a> -<ahref="https://github.com/LeoMobileDeveloper/Blogs/blob/master/iOS/iOS%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B%E7%9A%84%E5%8E%9F%E7%90%86%E5%92%8C%E5%BA%94%E7%94%A8.md">iOS编译过程的原理和应用</a> - <ahref="https://ming1016.github.io/2017/03/01/deeply-analyse-llvm/">深入剖析iOS 编译 Clang / LLVM</a></p></blockquote>]]></content>
<summary type="html"><p>iOS 开发常用的语言是 Objective-C 和
Swift,两者都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在
CPU 上执行,所以执行效率很高。本篇主要用于梳理 Objective-C
的编译过程。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="编译原理" scheme="http://example.com/categories/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86/"/>
<category term="技术" scheme="http://example.com/categories/%E6%8A%80%E6%9C%AF/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="编译原理" scheme="http://example.com/tags/%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86/"/>
</entry>
<entry>
<title>iOS 中 JS 与原生交互</title>
<link href="http://example.com/2019/07/01/2019-07-01-ios-js-native/"/>
<id>http://example.com/2019/07/01/2019-07-01-ios-js-native/</id>
<published>2019-06-30T16:00:00.000Z</published>
<updated>2019-06-30T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>本篇用于梳理 WKWebView 中 JS 与原生的交互,及 JavaScriptCore框架在交互过程中起的作用。</p><span id="more"></span><h2 id="wkwebview-中-js-调用-oc">WKWebView 中 JS 调用 OC</h2><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 添加 scriptMessageHandler</span></span><br><span class="line">- (<span class="type">void</span>)addScriptMessageHandler:(<span class="type">id</span> <<span class="built_in">WKScriptMessageHandler</span>>)scriptMessageHandler name:(<span class="built_in">NSString</span> *)name;</span><br><span class="line"></span><br><span class="line"><span class="comment">// WKScriptMessageHandler 中对应处理 scriptMessage</span></span><br><span class="line">- (<span class="type">void</span>)userContentController:(<span class="built_in">WKUserContentController</span> *)userContentController didReceiveScriptMessage:(<span class="built_in">WKScriptMessage</span> *)message;</span><br></pre></td></tr></table></figure><p>使用示例,在初始化 WKWebView 时,初始化WKWebViewConfiguration,添加对应的 ScriptMessageHandler。实例代码:</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><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">WKWebViewConfiguration</span> *configuration = [[<span class="built_in">WKWebViewConfiguration</span> alloc] init];</span><br><span class="line">configuration.userContentController = [<span class="built_in">WKUserContentController</span> new];</span><br><span class="line">[configuration.userContentController addScriptMessageHandler:<span class="keyword">self</span> name:<span class="string">@"btnClick"</span>];</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - WKScriptMessageHandler</span></span><br><span class="line">- (<span class="type">void</span>)userContentController:(<span class="built_in">WKUserContentController</span> *)userContentController didReceiveScriptMessage:(<span class="built_in">WKScriptMessage</span> *)message {</span><br><span class="line"> <span class="keyword">if</span> ([message.name isEqualToString:<span class="string">@"btnClick"</span>]) {</span><br><span class="line"> <span class="built_in">NSDictionary</span> *jsData = message.body;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"%@"</span>, message.name, jsData);</span><br><span class="line"> <span class="comment">// 读取 js function 的字符串</span></span><br><span class="line"> <span class="built_in">NSString</span> *jsFunctionString = jsData[<span class="string">@"result"</span>];</span><br><span class="line"> <span class="comment">// 拼接调用该方法的 js 字符串</span></span><br><span class="line"> <span class="comment">// convertDictionaryToJson: 方法将 NSDictionary 转成 JSON 格式的字符串</span></span><br><span class="line"> <span class="built_in">NSString</span> *jsonString = [<span class="built_in">NSDictionary</span> convertDictionaryToJson:@{<span class="string">@"test"</span>:<span class="string">@"123"</span>, <span class="string">@"data"</span>:<span class="string">@"321"</span>}];</span><br><span class="line"> <span class="built_in">NSString</span> *jsCallBack = [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"(%@)(%@);"</span>, jsFunctionString, jsonString];</span><br><span class="line"> <span class="comment">// 执行回调</span></span><br><span class="line"> [<span class="keyword">self</span>.weWebView evaluateJavaScript:jsCallBack completionHandler:^(<span class="type">id</span> _Nullable result, <span class="built_in">NSError</span> * _Nullable error) {</span><br><span class="line"> <span class="keyword">if</span> (error) {</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"err is %@"</span>, error.domain);</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>注:message 的 body 只能是NSNumber、NSString、NSDate、NSArray、NSDictionary、NSNull这几种类型。我们需要在回调环境下,将 js 回调转为 string后传给原生,执行回调方法。</p><h2 id="wkwebview-中-oc-调用-js">WKWebView 中 OC 调用 JS</h2><p>核心方法:</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">- (<span class="type">void</span>)evaluateJavaScript:(<span class="built_in">NSString</span> *)javaScriptString completionHandler:(<span class="type">void</span> (^ _Nullable)(_Nullable <span class="type">id</span>, <span class="built_in">NSError</span> * _Nullable error))completionHandler;</span><br></pre></td></tr></table></figure><p>OC 调用 JS:</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"><span class="built_in">NSString</span> *jsFounction = [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"getAppConfig('%@')"</span>, APP_CHANNEL_ID];</span><br><span class="line">[<span class="keyword">self</span>.weWebView evaluateJavaScript:jsFounction completionHandler:^(<span class="type">id</span> object, <span class="built_in">NSError</span> * _Nullable error) {</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"obj:%@---error:%@"</span>, object, error);</span><br><span class="line">}];</span><br></pre></td></tr></table></figure><h2 id="javascriptcore-框架">JavaScriptCore 框架</h2><p>苹果从 iOS7 开始将 JavaScriptCore 框架引入了 iOS系统中,成为了系统内置框架,框架名为 JavaScriptCore.framework。</p><h3 id="框架结构">框架结构</h3><p>苹果官方对 JavaScriptCore 框架的说明:<ahref="https://developer.apple.com/documentation/javascriptcore">JavaScriptCore</a>。</p><p>结构上 JavaScriptCore 框架主要分为:</p><ul><li>JSVirtualMachine</li><li>JSContext</li><li>JSValue</li></ul><h4 id="jsvirtualmachine">JSVirtualMachine</h4><p>JSVirtualMachine 为 JavaScript代码的运行提供的一个虚拟机环境。同一时间内,JSVirtualMachine只能执行一个线程,若想执行对个线程执行任务,需要创建多个JSVirtualMachine。每个 JSVirtualMachine 都有自己的 GC(垃圾回收器Garbage Collector),多个 JSVirtualMachine 之间的对象无法传递。</p><p>JSVirtualMachine 是一个抽象的 JavaScript虚拟机,是提供给开发者开发使用的。它的核心是JavaScriptCore,JavaScriptCore引擎是一个真实的虚拟机,包含了虚拟机的解释器和运行时部分。解释器用来将高级脚本语言编译成字节码,运行时用来管理运行时的内存空间。</p><h4 id="jscontext">JSContext</h4><p>JSContext 是 JavaScript 运行环境的上下文,负责原生和 JavaScript的数据传递。</p><h4 id="jsvalue">JSValue</h4><p>JSValue 是 JavaScript 的值对象。用来记录 JavaScript的原始值,并提供进行原生值对象转换的接口方法。</p><p>三者的关系: <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/f5ccbacb6db730ffae5a.png/Ca4VUBSJfoew5si.png" /></p><p>入图可以看到,一个 JSVirtualMachine 里包含了多个 JSContext, 同一个JSContext 中又可以有多个 JSValue。</p><p>JSVirtualMachine、JSContext、JSValue 类提供的接口,可以让原生: -执行 JS 代码 - 访问 JS 变量 - 访问和执行 JS 函数 - 也可以让 JS调用原生方法</p><p>那执行 JavaScript 代码的 JavaScriptCore 和原生应用是怎么交互的?</p><h3 id="jscore-与原生的交互">JSCore 与原生的交互</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/626c54fd15dc5f2be09d.png/jNJtDcYFboS5QXL.png" /></p><p>如图可以看到每个 JSVirtualMachine对应一个原生的线程,JSVirtualMachine 使用 JSValue 与原生线程通信,遵循JSExport 协议。这样原生线程可以将类方法和属性提供给 JavaScriptCore使用,JavaScriptCore 也可以将 JSValue 提供给原生线程使用。</p><p>当然 JavaScriptCore 与原生的交互,必须有 JSContext。JSContext 若 init初始化,默认会使用系统创建的 JSVirtualMachine。若想指定JSVirtualMachine,需要通过 <code>initWithVirtualMachine</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建 JSVirtualMachine 对象 jsvm</span></span><br><span class="line">JSVirtualMachine *jsvm = [[JSVirtualMachine alloc] init];</span><br><span class="line"><span class="comment">// 使用 jsvm 的 JSContext 对象 ct</span></span><br><span class="line">JSContext *ct = [[JSContext alloc] initWithVirtualMachine:jsvm];</span><br></pre></td></tr></table></figure><h4 id="原生调用-js">原生调用 JS</h4><p>再看下 JavaScriptCore 在原生代码中调用 JavaScript 的示例:</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></pre></td><td class="code"><pre><span class="line">JSContext *context = [[JSContext alloc] init];</span><br><span class="line"><span class="comment">// 解析执行 JavaScript 脚本</span></span><br><span class="line">[context evaluateScript:<span class="string">@"var i = 4 + 8"</span>];</span><br><span class="line"><span class="comment">// 转换 i 变量为原生对象</span></span><br><span class="line"><span class="built_in">NSNumber</span> *number = [context[<span class="string">@"i"</span>] toNumber];</span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"var i is %@, number is %@"</span>, context[<span class="string">@"i"</span>], number);</span><br></pre></td></tr></table></figure><p>可以看到 JSContext 通过 <strong>evaluateScript:</strong> 方法返回JSValue 对象。苹果官方 <ahref="https://developer.apple.com/documentation/javascriptcore/jsvalue">JSValue的相关说明</a>。</p><p>官方提供了 3 个可以将 JavaScript对象值类型直接转化为原生类型的接口:</p><ul><li>toNumber 方法。将 js 值转为 NSNumber 对象</li><li>toArray 方法。将 js 值转为 NSArray 对象</li><li>toDictionary 方法。如果变量是 object 类型,可以通过 toDictionary 将js 值转为 NSDictionary 对象</li></ul><p>如若想在原生中使用 JS 的函数方法,可以通过<code>callWithArguments</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 解析执行 JavaScript 脚本</span></span><br><span class="line">[context evaluateScript:<span class="string">@"function addition(x, y) { return x + y}"</span>];</span><br><span class="line"><span class="comment">// 获得 addition 函数</span></span><br><span class="line">JSValue *addition = context[<span class="string">@"addition"</span>];</span><br><span class="line"><span class="comment">// 传入参数执行 addition 函数</span></span><br><span class="line">JSValue *resultValue = [addition callWithArguments:@[@(<span class="number">4</span>), @(<span class="number">8</span>)]];</span><br><span class="line"><span class="comment">// 将 addition 函数执行的结果转成原生 NSNumber 来使用。</span></span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"function is %@; reslutValue is %@"</span>,addition, [resultValue toNumber]);</span><br></pre></td></tr></table></figure><p>简而言之,我们可以通过 evaluateScript 方法,在原生中执行 JS脚本,并使用 JS 的值对象和函数对象。</p><p>那 JavaScript 有如何调用原生的代码?</p><h4 id="js-调用原生">JS 调用原生</h4><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"><span class="comment">// 在 JSContext 中使用原生 Block 设置一个减法 subtraction 函数</span></span><br><span class="line">context[<span class="string">@"subtraction"</span>] = ^(<span class="type">int</span> x, <span class="type">int</span> y) {</span><br><span class="line"> <span class="keyword">return</span> x - y;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数</span></span><br><span class="line">JSValue *subValue = [context evaluateScript:<span class="string">@"subtraction(4,8);"</span>];</span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"substraction(4,8) is %@"</span>,[subValue toNumber]);</span><br></pre></td></tr></table></figure><p>如上代码即完成了一次 JS 对原生的调用。</p><ul><li>先在 JSContext 中使用原生 Block 设置一个减法函数</li><li>在通过这个 context 用 JavaScript 代码调用原生减法函数</li></ul><p>除了使用 Block 的方式,还可以通过 JSExport 协议来实现在 JS中调用原生代码。即原生代码遵循 JSExport 协议,以提供给 JavaScript调用。</p><h3 id="jscore-引擎的组成">JSCore 引擎的组成</h3><p>JavaScriptCore 是一个很复杂的模块,更多的放到后面再深:<ahref="https://ming1016.github.io/2018/04/21/deeply-analyse-javascriptcore/">深入剖析JavaScriptCore</a></p><h2 id="总结">总结</h2><ul><li>WKWebView 中 JS 调用 OC</li><li>WKWebView 中 OC 调用 JS</li><li>JavaScriptCore 框架结构</li><li>JSVirtualMachine</li><li>JSContext</li><li>JSValue</li><li>JavaScriptCore 与原生的交互<ul><li>OC 调用 JS</li><li>JS 调用 OC</li></ul></li></ul>]]></content>
<summary type="html"><p>本篇用于梳理 WKWebView 中 JS 与原生的交互,及 JavaScriptCore
框架在交互过程中起的作用。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
</entry>
<entry>
<title>OSSpinLock 与 os_unfair_lock</title>
<link href="http://example.com/2018/07/11/2018-07-11-osspinlock-to-osUnfairLock/"/>
<id>http://example.com/2018/07/11/2018-07-11-osspinlock-to-osUnfairLock/</id>
<published>2018-07-10T16:00:00.000Z</published>
<updated>2018-07-10T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>OSSpinLock 因为线程安全问题已被 Apple 废弃。苹果建议使用<code>os_unfair_lock</code> 替换,<code>os_unfair_lock</code>是互斥锁。</p><span id="more"></span><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#import <span class="string"><os/lock.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 声明锁</span></span><br><span class="line">os_unfair_lock _lock;</span><br><span class="line"><span class="comment">// 初始化锁</span></span><br><span class="line">_lock = OS_UNFAIR_LOCK_INIT;</span><br><span class="line"><span class="comment">// 加锁</span></span><br><span class="line">os_unfair_lock_lock(&_lock);</span><br><span class="line"><span class="comment">// 解锁</span></span><br><span class="line">os_unfair_lock_unlock(&_lock);</span><br></pre></td></tr></table></figure></p><blockquote><p>相关阅读: - <ahref="https://blog.jonyfang.top/2016/03/11/2016-03-11-ios-lock/">iOS中的锁</a></p></blockquote>]]></content>
<summary type="html"><p>OSSpinLock 因为线程安全问题已被 Apple 废弃。苹果建议使用
<code>os_unfair_lock</code> 替换,<code>os_unfair_lock</code>
是互斥锁。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="多线程" scheme="http://example.com/categories/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="多线程" scheme="http://example.com/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
</entry>
<entry>
<title>RunLoop 梳理</title>
<link href="http://example.com/2018/06/14/2018-06-14-ios-runloop/"/>
<id>http://example.com/2018/06/14/2018-06-14-ios-runloop/</id>
<published>2018-06-13T16:00:00.000Z</published>
<updated>2018-06-13T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>RunLoop 是 iOS 比较的核心之一,本篇用于梳理 RunLoop相关的概念和底层实现。</p><span id="more"></span><h2 id="什么是-runloop">什么是 RunLoop?</h2><p>对于一个线程,一般情况下一次只执行一个任务,如果任务执行完成线程也会立刻退出。而对于一个App,我们希望的是,即使 App内没有任何的任务需要线程去处理了,但不能够让这些线程退出。即不能退出应用。这时就需要一种机制既能让线程能随时处理事件,且在处理完成后不退出。比如主线程,在我们启动应用之后就会一直存在,当有交互发生时,对应去处理相关的事件。RunLoop就很好了满足了这个需求,它能够在没有收到消息的时候让线程休眠以避免资源浪费,当有消息来时会立刻唤醒线程处理。</p><p>这也是我们需要 RunLoop 的原因。</p><h2 id="runloop-与线程之间的关系">RunLoop 与线程之间的关系</h2><p>苹果不允许直接创建RunLoop,它提供了两个自动获取的函数:<code>CFRunLoopGetMain()</code> 和<code>CFRunLoopGetCurrent()</code>。CFRunLoop 是基于 pthread来管理的。</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><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">// 全局的 Dictionary,</span></span><br><span class="line"><span class="comment">// key 是 pthread_t, value 是 CFRunLoopRef</span></span><br><span class="line"><span class="type">static</span> CFMutableDictionaryRef loopsDic;</span><br><span class="line"><span class="comment">// 访问 loopsDic 时的锁</span></span><br><span class="line"><span class="type">static</span> CFSpinLock_t loopsLock;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 获取一个 pthread 对应的 RunLoop。</span></span><br><span class="line">CFRunLoopRef _CFRunLoopGet(<span class="type">pthread_t</span> thread) {</span><br><span class="line"> OSSpinLockLock(&loopsLock);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (!loopsDic) {</span><br><span class="line"> <span class="comment">// 第一次进入时,初始化全局 Dic,并先为主线程创建一个 RunLoop。</span></span><br><span class="line"> loopsDic = CFDictionaryCreateMutable();</span><br><span class="line"> CFRunLoopRef mainLoop = _CFRunLoopCreate();</span><br><span class="line"> CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 直接从 Dictionary 里获取。</span></span><br><span class="line"> CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (!loop) {</span><br><span class="line"> <span class="comment">// 取不到时,创建一个</span></span><br><span class="line"> loop = _CFRunLoopCreate();</span><br><span class="line"> CFDictionarySetValue(loopsDic, thread, loop);</span><br><span class="line"> <span class="comment">// 注册一个回调,当线程销毁时,同时销毁其对应的 RunLoop。</span></span><br><span class="line"> _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> OSSpinLockUnLock(&loopsLock);</span><br><span class="line"> <span class="keyword">return</span> loop;</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line">CFRunLoopRef <span class="title function_">CFRunLoopGetMain</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> _CFRunLoopGet(pthread_main_thread_np());</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line">CFRunLoopRef <span class="title function_">CFRunLoopGetCurrent</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> _CFRunLoopGet(pthread_self());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到,线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的Dictionary 里。第一次进入时,初始化全局 Dictionary,并先为主线程创建一个RunLoop,将关系保存在 Dictionary 中。线程结束时销毁RunLoop。我们只能在一个线程的内部获取其 RunLoop(主线程除外)。</p><h2 id="nsrunloop-和-cfrunloopref-的关系">NSRunLoop 和 CFRunLoopRef的关系</h2><p>NSRunLoop 是基于 CFRunLoopFef 封装。CGRunLoopRef 属于 CoreFoundation框架,提供的是纯 c 函数的 API,这些 API 是线程安全的。NSRunLoop提供的是面向对象的 API,这些 API 不是线程安全的。</p><h2 id="runloop-的内部实现逻辑">RunLoop 的内部实现逻辑</h2><p>RunLoop 的内部实现逻辑大致如下:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/ce7309d48082a4247e3f.png/sMkc5hvuyQe23bO.png" /></p><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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 用 DefaultMode 启动</span></span><br><span class="line"><span class="type">void</span> <span class="title function_">CFRunLoopRun</span><span class="params">(<span class="type">void</span>)</span> {</span><br><span class="line"> CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, <span class="number">1.0e10</span>, <span class="literal">false</span>);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 用指定的 Mode 启动,允许设置 RunLoop 超时时间</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">CFRunLoopRunInMode</span><span class="params">(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle)</span> {</span><br><span class="line"> <span class="keyword">return</span> CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"><span class="comment">// RunLoop 的实现</span></span><br><span class="line"><span class="type">int</span> <span class="title function_">CFRunLoopRunSpecific</span><span class="params">(runloop, modeName, seconds, stopAfterHandle)</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 首先根据 modeName 找到对应 mode</span></span><br><span class="line"> CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, <span class="literal">false</span>);</span><br><span class="line"> <span class="comment">// 如果 mode 里没有 source/timer/observer, 直接返回。</span></span><br><span class="line"> <span class="keyword">if</span> (__CFRunLoopModeIsEmpty(currentMode)) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 1. 通知 Observers: RunLoop 即将进入 loop。</span></span><br><span class="line"> __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 内部函数,进入loop</span></span><br><span class="line"> __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {</span><br><span class="line"> </span><br><span class="line"> Boolean sourceHandledThisLoop = NO;</span><br><span class="line"> <span class="type">int</span> retVal = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。</span></span><br><span class="line"> __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);</span><br><span class="line"> <span class="comment">// 3. 通知 Observers: RunLoop 即将触发 Source0 (非 port) 回调。</span></span><br><span class="line"> __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);</span><br><span class="line"> <span class="comment">// 执行被加入的 block</span></span><br><span class="line"> __CFRunLoopDoBlocks(runloop, currentMode);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 4. RunLoop 触发 Source0 (非 port) 回调。</span></span><br><span class="line"> sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);</span><br><span class="line"> <span class="comment">// 执行被加入的 block</span></span><br><span class="line"> __CFRunLoopDoBlocks(runloop, currentMode);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 5. 如果有 Source1 (基于 port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。</span></span><br><span class="line"> <span class="keyword">if</span> (__Source0DidDispatchPortLastTime) {</span><br><span class="line"> Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)</span><br><span class="line"> <span class="keyword">if</span> (hasMsg) <span class="keyword">goto</span> handle_msg;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。</span></span><br><span class="line"> <span class="keyword">if</span> (!sourceHandledThisLoop) {</span><br><span class="line"> __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。</span></span><br><span class="line"> <span class="comment">// • 一个基于 port 的 Source 的事件。</span></span><br><span class="line"> <span class="comment">// • 一个 Timer 到时间了</span></span><br><span class="line"> <span class="comment">// • RunLoop 自身的超时时间到了</span></span><br><span class="line"> <span class="comment">// • 被其他什么调用者手动唤醒</span></span><br><span class="line"> __CFRunLoopServiceMachPort(waitSet, &msg, <span class="keyword">sizeof</span>(msg_buffer), &livePort) {</span><br><span class="line"> mach_msg(msg, MACH_RCV_MSG, port); <span class="comment">// thread wait for receive msg</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。</span></span><br><span class="line"> __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 收到消息,处理消息。</span></span><br><span class="line"> handle_msg:</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。</span></span><br><span class="line"> <span class="keyword">if</span> (msg_is_timer) {</span><br><span class="line"> __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())</span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 9.2 如果有 dispatch 到 main_queue 的 block,执行 block。</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (msg_is_dispatch) {</span><br><span class="line"> __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);</span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 9.3 如果一个 Source1 (基于 port) 发出事件了,处理这个事件</span></span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);</span><br><span class="line"> sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);</span><br><span class="line"> <span class="keyword">if</span> (sourceHandledThisLoop) {</span><br><span class="line"> mach_msg(reply, MACH_SEND_MSG, reply);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 执行加入到 Loop 的 block</span></span><br><span class="line"> __CFRunLoopDoBlocks(runloop, currentMode);</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (sourceHandledThisLoop && stopAfterHandle) {</span><br><span class="line"> <span class="comment">// 进入 loop 时参数说处理完事件就返回。</span></span><br><span class="line"> retVal = kCFRunLoopRunHandledSource;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (timeout) {</span><br><span class="line"> <span class="comment">// 超出传入参数标记的超时时间了</span></span><br><span class="line"> retVal = kCFRunLoopRunTimedOut;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (__CFRunLoopIsStopped(runloop)) {</span><br><span class="line"> <span class="comment">// 被外部调用者强制停止了</span></span><br><span class="line"> retVal = kCFRunLoopRunStopped;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (__CFRunLoopModeIsEmpty(runloop, currentMode)) {</span><br><span class="line"> <span class="comment">// source/timer/observer 一个都没有了</span></span><br><span class="line"> retVal = kCFRunLoopRunFinished;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 如果没超时,mode 里没空,loop 也没被停止,那继续 loop。</span></span><br><span class="line"> } <span class="keyword">while</span> (retVal == <span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 10. 通知 Observers: RunLoop 即将退出。</span></span><br><span class="line"> __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="runloop-的-mode">RunLoop 的 Mode</h2><p>开发过程中常遇到的一个场景是,在我们写 NSTimer 时为了避免滑动事件影响NSTimer 的回调,我们通常会把 mode 置为<code>NSRunLoopCommonModes</code>。</p><p>这里的 <code>NSRunLoopCommonModes</code> 是什么?</p><p>先看下 <code>CFRunLoopMode</code> 和 <code>CFRunLoop</code>的结构:</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> __<span class="title">CFRunLoopMode</span> {</span></span><br><span class="line"> CFStringRef _name; <span class="comment">// Mode Name, 例如 @"kCFRunLoopDefaultMode"</span></span><br><span class="line"> CFMutableSetRef _sources0; <span class="comment">// Set</span></span><br><span class="line"> CFMutableSetRef _sources1; <span class="comment">// Set</span></span><br><span class="line"> CFMutableArrayRef _observers; <span class="comment">// Array</span></span><br><span class="line"> CFMutableArrayRef _timers; <span class="comment">// Array</span></span><br><span class="line"> ...</span><br><span class="line">};</span><br><span class="line"> </span><br><span class="line"><span class="class"><span class="keyword">struct</span> __<span class="title">CFRunLoop</span> {</span></span><br><span class="line"> CFMutableSetRef _commonModes; <span class="comment">// Set</span></span><br><span class="line"> CFMutableSetRef _commonModeItems; <span class="comment">// Set<Source/Observer/Timer></span></span><br><span class="line"> CFRunLoopModeRef _currentMode; <span class="comment">// Current Runloop Mode</span></span><br><span class="line"> CFMutableSetRef _modes; <span class="comment">// Set</span></span><br><span class="line"> ...</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>主线程的 RunLoop 里有两种 Mode:<code>kCFRunLoopDefaultMode</code> 和<code>UITrackingRunLoopMode</code>。这两种 Mode都被标记为<code>Common</code>属性。App 平常所处的 mode 是<code>kCFRunLoopDefaultMode</code>;当有滑动事件触发时,RunLoop 会切换mode 到 <code>UITrackingRunLoopMode</code>。</p><p>前面提到创建一个 Timer,默认会被加到 DefaultMode,正常情况下 Timer会得到重复回调。但如果此时滑动一个 ScrollView,RunLoop 就会将 mode切换为 TrackingRunLoopMode,这时 Timer 就不会被正常的回调。当然,此时Timer 也不会影响到滑动操作。</p><p>到这里,我们大概知道了 RunLoop 系统预置的 model 有这几种:</p><ul><li>NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认模式</li><li>UITrackingRunLoopMode:滑动模式</li><li>NSRunLoopCommonModes:通用模式,无论处于滑动或默认状态都能够响应实践</li></ul><h3 id="commonmodes">CommonModes</h3><p>一个 Mode 可以通过将其 ModeName 添加到 RunLoop 的<code>commonModes</code> 中,来将自己设置为<code>CommonMode</code>。每当 RunLoop 的内容发生变化时,RunLoop都会将事件同步到 CommonMode 的 mode item,什么是 mode item后文会讲到。这也解释了为什么 timer 加入到 NSRunLoopCommonModes中会被正确的回调。</p><p>在 NSRunLoop 这一层没有提供操作 Mode 的接口,在<strong>CFRunLoopRef</strong> 对外提供了两个操作 Mode 的接口,只有增加mode 的接口:</p><figure class="highlight c"><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">CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);</span><br><span class="line">CFRunLoopRunInMode(CFStringRef modeName, ...);</span><br></pre></td></tr></table></figure><h3 id="mode-item">mode item</h3><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> __<span class="title">CFRunLoopMode</span> {</span></span><br><span class="line"> CFStringRef _name; <span class="comment">// Mode Name, 例如 @"kCFRunLoopDefaultMode"</span></span><br><span class="line"> CFMutableSetRef _sources0; <span class="comment">// Set</span></span><br><span class="line"> CFMutableSetRef _sources1; <span class="comment">// Set</span></span><br><span class="line"> CFMutableArrayRef _observers; <span class="comment">// Array</span></span><br><span class="line"> CFMutableArrayRef _timers; <span class="comment">// Array</span></span><br><span class="line"> ...</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>在 <code>__CFRunLoopMode</code> 结构体中的<code>_sources0</code>、<code>_sources1</code>、<code>_observers</code>、<code>_timers</code>都属于 mode item。结构图如下:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/7a53120e056aa7787acd.png/PasF52LWn3NKwHS.png" /></p><ul><li><p>Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。</p></li><li><p>Source1 包含了一个 mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source 能主动唤醒 RunLoop 的线程。</p></li><li><p>Timer 是基于时间的触发器,它和 NSTimer 是toll-free bridged的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。</p></li><li><p>Observer 是观察者,每个 Observer 都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="title function_">CF_OPTIONS</span><span class="params">(CFOptionFlags, CFRunLoopActivity)</span> {</span><br><span class="line"> kCFRunLoopEntry = (<span class="number">1UL</span> << <span class="number">0</span>), <span class="comment">// 即将进入Loop</span></span><br><span class="line"> kCFRunLoopBeforeTimers = (<span class="number">1UL</span> << <span class="number">1</span>), <span class="comment">// 即将处理 Timer</span></span><br><span class="line"> kCFRunLoopBeforeSources = (<span class="number">1UL</span> << <span class="number">2</span>), <span class="comment">// 即将处理 Source</span></span><br><span class="line"> kCFRunLoopBeforeWaiting = (<span class="number">1UL</span> << <span class="number">5</span>), <span class="comment">// 即将进入休眠</span></span><br><span class="line"> kCFRunLoopAfterWaiting = (<span class="number">1UL</span> << <span class="number">6</span>), <span class="comment">// 刚从休眠中唤醒</span></span><br><span class="line"> kCFRunLoopExit = (<span class="number">1UL</span> << <span class="number">7</span>), <span class="comment">// 即将退出Loop</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure></p></li></ul><p>一个 mode item 可以被同时加入多个 mode。但一个 item 被重复加入同一个mode 时是无效的。如果一个 mode 中一个 item 都没有,则 RunLoop会直接退出,不进入循环。</p><h2 id="runloop-启动和退出">RunLoop 启动和退出</h2><h3 id="启动">启动</h3><p>这边以 NSRunLoop 为例,CFRunLoopRef 类似, 启动有 3 个方法: - run -runUntilDate: - runMode:beforeDate:</p><p>run 底层是不断(循环)调用 <code>runMode:beforeDate:</code>来达到运行目的。<code>runUntilDate:</code> 底层也是调用<code>runMode:beforeDate:</code> 来运行,和 run不同的是,在指定的时间也就是 UntilDate 参数到后会停止调用。</p><h3 id="退出">退出</h3><p>在系统提供的停止 RunLoop 方法只有<code>CFRunLoopStop()</code>,<code>CFRunLoopStop()</code>方法只会结束当前的 RunLoop 调用,而不会结束后续的调用。也就意味着如果你是用方法一也就是 run 的方式启动 RunLoop,那么这个 RunLoop不会被退出,因为它会不断的启动,因为 run 底层是不断(循环)调用<code>runMode:beforeDate:</code> 来达到运行目的。如果你是使用<code>runUntilDate:</code> 启动的,那么超时结束后会自动终止RunLoop,如果是 <code>runMode:beforeDate:</code> 那么你可以精确的控制RunLoop 的停止。</p><h2 id="runloop-的应用">RunLoop 的应用</h2><p>RunLoop 用途广泛,如AutoreleasePool,PerformSelecter,GCD,AsyncDisplayKit等都有涉及到。</p><blockquote><p>参考文章: - <ahref="https://opensource.apple.com/tarballs/CF/">CoreFoundation 源码</a>- <a href="https://blog.ibireme.com/2015/05/18/runloop/">深入理解RunLoop</a> - <ahref="https://mazengyi.dev/2017/08/10/runloop/#runloop-%E5%90%AF%E5%8A%A8%E5%92%8C%E9%80%80%E5%87%BA">RunLoop</a></p></blockquote>]]></content>
<summary type="html"><p>RunLoop 是 iOS 比较的核心之一,本篇用于梳理 RunLoop
相关的概念和底层实现。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="底层原理" scheme="http://example.com/categories/%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86/"/>
<category term="技术" scheme="http://example.com/categories/%E6%8A%80%E6%9C%AF/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="底层原理" scheme="http://example.com/tags/%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86/"/>
</entry>
<entry>
<title>AutoreleasePool 实现原理</title>
<link href="http://example.com/2018/06/12/2018-06-12-objc-autoreleasePool/"/>
<id>http://example.com/2018/06/12/2018-06-12-objc-autoreleasePool/</id>
<published>2018-06-11T16:00:00.000Z</published>
<updated>2018-06-11T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>AutoreleasePool(自动释放池)是 OC 中一种内存自动回收的机制。在 MRC中,可以通过 <code>[obj autorelease]</code> 来延迟内存的释放;而 ARC中的 <code>autorelease</code>方法是被禁用的,无法主动调用,但对象的内存任在我们不知情的情况下被很好的管理。这就是依赖于背后的Autorelease 机制,那么是如何管理的呢?</p><span id="more"></span><h2 id="autoreleasepool">AutoreleasePool</h2><p>在 <code>main.m</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> main(<span class="type">int</span> argc, <span class="type">char</span> * argv[]) {</span><br><span class="line"> <span class="built_in">NSString</span> * appDelegateClassName;</span><br><span class="line"> <span class="keyword">@autoreleasepool</span> {</span><br><span class="line"> <span class="comment">// Setup code that might create autoreleased objects goes here.</span></span><br><span class="line"> appDelegateClassName = <span class="built_in">NSStringFromClass</span>([AppDelegate <span class="keyword">class</span>]);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">UIApplicationMain</span>(argc, argv, <span class="literal">nil</span>, appDelegateClassName);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们通过编译:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xcrun -sdk iphonesimulator clang -rewrite-objc main.m</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> * argv[])</span> </span>{</span><br><span class="line"> NSString * appDelegateClassName;</span><br><span class="line"> <span class="comment">/* @autoreleasepool */</span> { __AtAutoreleasePool __autoreleasepool; </span><br><span class="line"></span><br><span class="line"> appDelegateClassName = <span class="built_in">NSStringFromClass</span>(((<span class="built_in">Class</span> (*)(id, SEL))(<span class="type">void</span> *)objc_msgSend)((id)<span class="built_in">objc_getClass</span>(<span class="string">"AppDelegate"</span>), <span class="built_in">sel_registerName</span>(<span class="string">"class"</span>)));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">UIApplicationMain</span>(argc, argv, __null, appDelegateClassName);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看到 <code>@aotoreleasepool</code> 被编译器转换成了<code>__AtAutoreleasePool __autoreleasepool</code>。查找得到<code>__AtAutoreleasePool</code> 对应的结构如下:</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="keyword">struct</span> <span class="title class_">__AtAutoreleasePool</span> {</span><br><span class="line"> __AtAutoreleasePool() {atautoreleasepoolobj = <span class="built_in">objc_autoreleasePoolPush</span>();}</span><br><span class="line"> ~__AtAutoreleasePool() {<span class="built_in">objc_autoreleasePoolPop</span>(atautoreleasepoolobj);}</span><br><span class="line"> <span class="type">void</span> * atautoreleasepoolobj;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>结构体内实际上是两个 objc方法:<code>objc_autoreleasePoolPush()</code> 和<code>objc_autoreleasePoolPop()</code>。</p><p>打开 objc 源码 - <ahref="https://github.com/DeveloperErenLiu/RuntimeAnalyze"><strong>DeveloperErenLiu/RuntimeAnalyze/objc4-750</strong></a>,对应查找到它们在<code>NSObject.mm</code> 的实际实现:</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></pre></td><td class="code"><pre><span class="line"><span class="type">void</span> *</span><br><span class="line"><span class="title function_">objc_autoreleasePoolPush</span><span class="params">(<span class="type">void</span>)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">return</span> AutoreleasePoolPage::push();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">objc_autoreleasePoolPop</span><span class="params">(<span class="type">void</span> *ctxt)</span></span><br><span class="line">{</span><br><span class="line"> AutoreleasePoolPage::pop(ctxt);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这两个函数实际上都是对 <code>AutoreleasePoolPage</code>的封装。所以自动释放机制的核心也在这个类。</p><h2 id="autoreleasepoolpage">AutoreleasePoolPage</h2><p><code>AutoreleasePoolPage</code> 的结构图如下:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/5711a8165f7251a6d87a.png/OZ3YF4nbiufgR9e.png" /></p><p>我们会注意到两个字段:<code>child</code> 和<code>parent</code>。<code>AutoreleasePool</code> 是由若干<code>AutoreleasePoolPage</code> 以双向链表的形式组合而成。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/f3ecc1c47eece3a84969.png/6DWdVtYCL5Qk2ZG.png" /></p><p>需要注意的几个点:</p><ul><li>每个 <code>AutoreleasePoolPage</code> 对象会开启4096字节(4kb)内存(<ahref="https://zh.wikipedia.org/zh-hans/4K%E5%AF%B9%E9%BD%90">4kb对齐</a>的原因,虚拟内存每个扇区 4096 字节)。</li><li><code>depth</code> 表示链表深度,即节点个数。</li><li><code>id *next</code> 指针作为游标指向当前页栈顶(即最新加入的autorelease 对象的下一个位置)。</li></ul><p>单个 <code>AutoreleasePoolpage</code> 的结构如下:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/7ea4f8de7d25210e0666.png/FKgd95r2QYRh3Lq.png" /></p><h2 id="objc_autoreleasepoolpush">objc_autoreleasePoolPush()</h2><p>每个 <code>AutoreleasePoolPage</code> 对象会开启4096字节(4kb)内存,除了自身实例变量所占空间,剩下的空间全部拿来存储autorelease 对象的地址。</p><p>每当进行一次 <code>objc_autoreleasePoolPush</code> 调用时,runtime都会向当前的 AutoreleasePoolPage 中添加一个<code>哨兵对象</code>,值为nil,</p><p>哨兵对象的定义为:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"># <span class="keyword">define</span> POOL_BOUNDARY nil</span></span><br></pre></td></tr></table></figure><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/579367e654c728fc414f.png/CS4RKhJfFkXYajT.png" /></p><p>注:图中的 <code>obj1</code>、<code>obj2</code> 表示<code>AutoreleasePoolPage</code> 自身除了 autorelease 对象的实例。</p><p>到这里会有个疑问:为什么需要插入一个哨兵对象?放到后面解释。</p><p>添加完哨兵对象后,将 <code>next</code> 指针指向下一个添加 Autorelease对象的位置。当当前 AutoreleasePoolPage 满了,开启一个新的AutoreleasePoolPage,并更新 <code>child</code> 和 <code>parent</code>指针,以组成双向链表。</p><h2 id="objc_autoreleasepoolpop">objc_autoreleasePoolPop()</h2><p><code>objc_autoreleasePoolPush</code>会有个返回值,这个返回值正是前面提到的哨兵对象。<code>objc_autoreleasePoolPop()</code>调用时会把哨兵对象作为入参。之后根据传入的哨兵对象地址找到哨兵对象对应的<code>AutoreleasePoolPage</code>;在当前 page中,对所有晚于哨兵对象插入的 Autorelease 对象发送 release消息,到哨兵对象后,销毁当前 page;再根据 parent 向前继续进行pop,知道第一个哨兵对象所在 page 释放完成。</p><h2 id="关于-autoreleasepool-的几个问题">关于 AutoreleasePool的几个问题</h2><h3 id="autoreleasepool-是怎么释放的">AutoreleasePool是怎么释放的?</h3><p>第一种是 main.m 里的 AutoreleasePool。</p><p>App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是<code>_wrapRunLoopWithAutoreleasePoolHandler()</code>。</p><p>第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用<code>_objc_autoreleasePoolPush()</code> 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。</p><p>第二个 Observer 监视了两个事件:BeforeWaiting(准备进入休眠) 时调用<code>_objc_autoreleasePoolPop()</code> 和<code>_objc_autoreleasePoolPush()</code>释放旧的池并创建新池;Exit(即将退出Loop) 时调用<code>_objc_autoreleasePoolPop()</code> 来释放自动释放池。这个 Observer的 order 是2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。</p><p>第二种是手动创建的局部 AutoreleasePool。</p><p>根据当前 loop 的情况,进行创建和释放。</p><h3 id="什么对象会加入-autoreleasepool">什么对象会加入autoreleasePool?</h3><ul><li><code>alloc</code>/<code>new</code>/<code>copy</code>/<code>mutableCopy</code>等持有对象的方法,不会加入<code>autoreleasePool</code>;其他不持有对象的方法通过<code>objc_autoreleaseReturnValue</code> 和<code>objc_retainAutoreleasedReturnValue</code> 来判断是否需要加入autoreleasePool,这是编译器的优化。</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 自己生成并持有对象,不需要加入 autoreleasePool</span></span><br><span class="line"><span class="type">id</span> object = [[<span class="built_in">NSObject</span> alloc] init]; </span><br><span class="line"><span class="comment">// MRC:不是自己生成并且不持有对象,需要加入autoreleasePool</span></span><br><span class="line"><span class="type">id</span> object2 = [<span class="built_in">NSMutableArray</span> array];</span><br><span class="line"><span class="comment">// MRC:不是自己生成,但持有对象,不需要加入autoreleasePool</span></span><br><span class="line"><span class="type">id</span> object3 = [<span class="built_in">NSMutableArray</span> array];</span><br><span class="line">[object3 <span class="keyword">retain</span>];</span><br></pre></td></tr></table></figure><ul><li><p>iOS5 及之前的编译器,关键字 <code>__weak</code>修饰的对象,会自动加入 <code>autoreleasePool</code>;iOS5及之后的编译器,则直接调用的 release,不会加入autoreleasePool。</p></li><li><p><code>id 指针(id *)</code>和<code>对象指针(NSError **)</code>,会自动加上关键字<code>__autorealeasing</code>,加入 autoreleasePool。</p></li></ul><h3 id="子线程中使用-autorelease-对象会内存泄漏吗">子线程中使用autorelease 对象会内存泄漏吗?</h3><p>子线程的 runloop 默认是不开启的,如果产生了 Autorelease 对象,会调用<code>autoreleaseNoPage</code> 方法。这个方法里会自动创建一个hotpage,默认生成一个 AutoreleasePoolPage 来添加 autorelease 对象。</p><blockquote><p>参考内容: - <ahref="https://blog.sunnyxx.com/2014/10/15/behind-autorelease/">黑幕背后的Autorelease</a></p></blockquote>]]></content>
<summary type="html"><p>AutoreleasePool(自动释放池)是 OC 中一种内存自动回收的机制。在 MRC
中,可以通过 <code>[obj autorelease]</code> 来延迟内存的释放;而 ARC
中的 <code>autorelease</code>
方法是被禁用的,无法主动调用,但对象的内存任在我们不知情的情况下被很好的管理。这就是依赖于背后的
Autorelease 机制,那么是如何管理的呢?</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="源码阅读" scheme="http://example.com/categories/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="源码阅读" scheme="http://example.com/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
</entry>
<entry>
<title>weak 源码分析</title>
<link href="http://example.com/2018/06/10/2018-06-10-objc-weak/"/>
<id>http://example.com/2018/06/10/2018-06-10-objc-weak/</id>
<published>2018-06-09T16:00:00.000Z</published>
<updated>2018-06-09T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>iOS内是通过引用计数来管理内存,引用计数的管理,很容易会出现“循环引用”问题。<code>weak</code>修饰符,也是开发日常最常用的打破循环引用方式。被 <code>weak</code>修饰符修饰的弱引用除了不会增加对象的引用计数外;在引用对象被释放后,这个弱引用会自动失效并置为nil。本篇总结分析下 Objective-C 中 <code>weak</code>都是怎么实现的。</p><p>分析源码基于:<ahref="https://github.com/DeveloperErenLiu/RuntimeAnalyze"><strong>DeveloperErenLiu/RuntimeAnalyze/objc4-799.1</strong></a>。</p><span id="more"></span><h2 id="objc_initweak">objc_initWeak()</h2><p>在入口文件 <strong>KCObjcTest/main.m</strong> 中写入如下代码:</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></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> main(<span class="type">int</span> argc, <span class="keyword">const</span> <span class="type">char</span> * argv[]) {</span><br><span class="line"> <span class="keyword">@autoreleasepool</span> {</span><br><span class="line"> <span class="built_in">NSObject</span> *obj = [[<span class="built_in">NSObject</span> alloc] init];</span><br><span class="line"> __<span class="keyword">weak</span> <span class="type">id</span> weakObj = obj;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>单步运行后,进入了 <code>NSObject.mm</code> 中的<code>objc_initWeak()</code> 方法。在 runtime 源码中的实现如下:</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></pre></td><td class="code"><pre><span class="line">id <span class="title function_">objc_initWeak</span><span class="params">(id *location, id newObj)</span></span><br><span class="line">{</span><br><span class="line"><span class="comment">// 查看对象是否有效</span></span><br><span class="line"><span class="comment">// 无效对象立刻置空指针</span></span><br><span class="line"> <span class="keyword">if</span> (!newObj) {</span><br><span class="line"> *location = nil;</span><br><span class="line"> <span class="keyword">return</span> nil;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating></span><br><span class="line"> (location, (objc_object*)newObj);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>从源码中可以看到 <code>objc_initWeak()</code>内部最后会调用<code>storeWeak()</code>方法,传入了三个模板参数,自己理解的这段代码的意思是:</p><p>该弱引用不存在已有指向的对象(DontHaveOld),同时需要指向新的对象(DoHaveNew),如果目标对象正在释放就崩溃处理(DoCrashIfDeallocating)。</p><p>到了 <code>storeWeak()</code> 这一步,看下它的内部实现。</p><h2 id="storeweak">storeWeak()</h2><p><code>storeWeak()</code> 在 runtime 源码中的实现如下:</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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这里传递了三个 bool 数值</span></span><br><span class="line"><span class="comment">// 使用 template 进行常量参数传递是为了优化性能</span></span><br><span class="line"><span class="comment">/** HaveOld: </span></span><br><span class="line"><span class="comment">- true:变量有值</span></span><br><span class="line"><span class="comment">- false:需要被及时清理,当前值可能为 nil</span></span><br><span class="line"><span class="comment">HaveNew:</span></span><br><span class="line"><span class="comment">- true:需要被分配的新值,当前值可能为 nil</span></span><br><span class="line"><span class="comment">- false:不需要分配新值</span></span><br><span class="line"><span class="comment">CrashIfDeallocating:</span></span><br><span class="line"><span class="comment">- true:newObj 已经释放或 newObj 不支持弱引用,该过程需要暂停</span></span><br><span class="line"><span class="comment">- false:用 nil 代替存储</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">HaveOld</span> {</span> DontHaveOld = <span class="literal">false</span>, DoHaveOld = <span class="literal">true</span> };</span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">HaveNew</span> {</span> DontHaveNew = <span class="literal">false</span>, DoHaveNew = <span class="literal">true</span> };</span><br><span class="line"><span class="class"><span class="keyword">enum</span> <span class="title">CrashIfDeallocating</span> {</span></span><br><span class="line"> DontCrashIfDeallocating = <span class="literal">false</span>, DoCrashIfDeallocating = <span class="literal">true</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">template <HaveOld haveOld, HaveNew haveNew,</span><br><span class="line"> CrashIfDeallocating crashIfDeallocating></span><br><span class="line"><span class="type">static</span> id </span><br><span class="line"><span class="title function_">storeWeak</span><span class="params">(id *location, objc_object *newObj)</span></span><br><span class="line">{</span><br><span class="line"> ASSERT(haveOld || haveNew);</span><br><span class="line"> <span class="keyword">if</span> (!haveNew) ASSERT(newObj == nil);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 初始化 previouslyInitializedClass 指针</span></span><br><span class="line"> <span class="comment">// 用于标记已经初始化的类</span></span><br><span class="line"> Class previouslyInitializedClass = nil;</span><br><span class="line"> id oldObj;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 声明新旧 SideTable</span></span><br><span class="line"> SideTable *oldTable;</span><br><span class="line"> SideTable *newTable;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 获得新值和旧值(若存在)辅助表的锁</span></span><br><span class="line"> <span class="comment">// 如果新旧值辅助表同时存在时,以锁的地址大小排序,防止锁的顺序问题</span></span><br><span class="line"> <span class="comment">// 若旧值在下面改变了,则重试</span></span><br><span class="line"> retry:</span><br><span class="line"> <span class="keyword">if</span> (haveOld) {</span><br><span class="line"> <span class="comment">// 若有旧值,通过指针获取目标对象</span></span><br><span class="line"> <span class="comment">// 再以目标对象的地址为索引,取得旧值对应的辅助表</span></span><br><span class="line"> oldObj = *location;</span><br><span class="line"> oldTable = &SideTables()[oldObj];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> oldTable = nil;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (haveNew) {</span><br><span class="line"> <span class="comment">// 若有新值,以新值的地址为索引,取得新值对应的辅助表</span></span><br><span class="line"> newTable = &SideTables()[newObj];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> newTable = nil;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 加锁操作,防止多线程中数据竞争</span></span><br><span class="line"> SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 线程冲突处理</span></span><br><span class="line"> <span class="comment">// 若有旧值,但 location 指向的对象地址不为 oldObj,那很可能被其它线程修改过</span></span><br><span class="line"> <span class="comment">// 解锁并重试</span></span><br><span class="line"> <span class="keyword">if</span> (haveOld && *location != oldObj) {</span><br><span class="line"> SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);</span><br><span class="line"> <span class="keyword">goto</span> retry;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 确保新值的 isa 已经调用 +initialize 初始化</span></span><br><span class="line"> <span class="comment">// 避免弱引用机制和 +initialize 机制间的死锁</span></span><br><span class="line"> <span class="keyword">if</span> (haveNew && newObj) {</span><br><span class="line"> <span class="comment">// 获取新值的 isa</span></span><br><span class="line"> Class cls = newObj->getIsa();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若 newObj isa 与 previouslyInitializedClass 不同,</span></span><br><span class="line"> <span class="comment">// 且 newObj 未被初始化</span></span><br><span class="line"> <span class="keyword">if</span> (cls != previouslyInitializedClass && </span><br><span class="line"> !((objc_class *)cls)->isInitialized()) </span><br><span class="line"> {</span><br><span class="line"> <span class="comment">// 解锁</span></span><br><span class="line"> SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);</span><br><span class="line"> <span class="comment">// 初始化 newObj</span></span><br><span class="line"> class_initialize(cls, (id)newObj);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若 newObj 已经完成执行 +initialize,这是最理想情况</span></span><br><span class="line"> <span class="comment">// 若这个 newObj 正在当前线程运行 +initialize</span></span><br><span class="line"> <span class="comment">// 如在 +initialize 方法里对自己的实例调用了 storeWeak</span></span><br><span class="line"> <span class="comment">// 需要手动对其增加保护策略,并设置 previouslyInitializedClass 指针进行标记</span></span><br><span class="line"> previouslyInitializedClass = cls;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">goto</span> retry;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 清理旧值</span></span><br><span class="line"> <span class="keyword">if</span> (haveOld) {</span><br><span class="line"> weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 设置新值</span></span><br><span class="line"> <span class="keyword">if</span> (haveNew) {</span><br><span class="line"> <span class="comment">// 把弱引用地址注册到 newObj 的弱引用条目</span></span><br><span class="line"> newObj = (objc_object *)</span><br><span class="line"> weak_register_no_lock(&newTable->weak_table, (id)newObj, location, </span><br><span class="line"> crashIfDeallocating);</span><br><span class="line"> <span class="comment">// 如果 weakStore 操作应该被拒绝,weak_register_no_lock 会返回 nil</span></span><br><span class="line"> <span class="comment">// 否则,对被引用对象设置弱引用标记位(is-weakly-referenced bit)</span></span><br><span class="line"> <span class="keyword">if</span> (newObj && !newObj->isTaggedPointer()) {</span><br><span class="line"> newObj->setWeaklyReferenced_nolock();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 之前不要设置 *location,这里需要更改指针指向</span></span><br><span class="line"> *location = (id)newObj;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 无新值,则不更改</span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 解锁,让其他线程可以访问 oldTable, newTable</span></span><br><span class="line"> SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> (id)newObj;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述代码可以看到,方法中核心的两个方法:<code>weak_unregister_no_lock</code>和 <code>weak_register_no_lock</code>。他们都是对 <code>SideTable</code>的实例进行操作。实际上 <code>SideTable</code>也是作为全局对象用于管理所有对象的引用计数和 weak 表,在 runtime启动时和主线程的 <code>AutoreleasePool</code> 一起创建。</p><p>接着来看下 <code>SideTable</code>。</p><h2 id="sidetable">SideTable</h2><p>在 <strong>NSObject.mm</strong> 文件中 <code>SideTable</code>结构体的源码为:</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">SideTable</span> {</span></span><br><span class="line"> <span class="type">spinlock_t</span> slock;</span><br><span class="line"> RefcountMap refcnts;</span><br><span class="line"> <span class="type">weak_table_t</span> weak_table;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到 SideTable 结构体主要的三个部分:</p><ul><li><strong><code>spinlock_t slock</code></strong>:用于原子操作的自旋锁,用于给 SideTable 上锁和解锁</li><li><strong><code>RefcountMap refcnts</code></strong>: 引用计数的 hash表。仅在未开启 isa 优化或在 isa 优化开启且 isa_t的引用计数溢出时才会用到。</li><li><strong><code>weak_table_t weak_table</code></strong>: 弱引用指针的hash 表。OC 中 weak 功能实现的核心数据结构。</li></ul><p>前面的 <code>storeWeak()</code> 里,runtime 是通过如下方式获取对象的<code>SideTable</code>:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">oldTable = &SideTables()[oldObj];</span><br></pre></td></tr></table></figure><p>先看下 <code>SideTables()</code> 的源码实现:</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ExplicitInit</span> {</span></span><br><span class="line"> <span class="keyword">alignas</span>(Type) <span class="type">uint8_t</span> _storage[<span class="keyword">sizeof</span>(Type)];</span><br><span class="line"></span><br><span class="line">public:</span><br><span class="line"> template <typename... Ts></span><br><span class="line"> <span class="type">void</span> <span class="title function_">init</span><span class="params">(Ts &&... Args)</span> {</span><br><span class="line"> new (_storage) Type(<span class="built_in">std</span>::forward<Ts>(Args)...);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Type &<span class="title function_">get</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// reinterpret_cast 是 C++ 标准转换运算符</span></span><br><span class="line"> <span class="comment">// 用来处理无关类型之间的转换,它会产生一个新的值</span></span><br><span class="line"> <span class="comment">// 这个值会有与原始参数(_storage)有完全相同的比特位</span></span><br><span class="line"> <span class="keyword">return</span> *reinterpret_cast<Type *>(_storage);</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> StripedMap<SideTable>& <span class="title function_">SideTables</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> SideTablesMap.get();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看到 <code>SideTables()</code> 返回的是一个 <code>StripedMap</code>哈希表,以对象的地址作为键值返回对应的 <code>SideTable</code>。</p><h2 id="stripedmap">StripedMap</h2><p><code>StripedMap</code> 是一个模板类,定义于<code>objc-private.h</code>文件中,提供了一个以地址为键值的哈希结构。</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></pre></td><td class="code"><pre><span class="line">template<typename T></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">StripedMap</span> {</span></span><br><span class="line"><span class="meta">#<span class="keyword">if</span> TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR</span></span><br><span class="line"> <span class="class"><span class="keyword">enum</span> {</span> StripeCount = <span class="number">8</span> };</span><br><span class="line"><span class="meta">#<span class="keyword">else</span></span></span><br><span class="line"> <span class="class"><span class="keyword">enum</span> {</span> StripeCount = <span class="number">64</span> };</span><br><span class="line"><span class="meta">#<span class="keyword">endif</span></span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="type">static</span> <span class="type">unsigned</span> <span class="type">int</span> <span class="title function_">indexForPointer</span><span class="params">(<span class="type">const</span> <span class="type">void</span> *p)</span> {</span><br><span class="line"> <span class="type">uintptr_t</span> addr = reinterpret_cast<<span class="type">uintptr_t</span>>(p);</span><br><span class="line"> <span class="comment">// 哈希操作</span></span><br><span class="line"> <span class="keyword">return</span> ((addr >> <span class="number">4</span>) ^ (addr >> <span class="number">9</span>)) % StripeCount;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public:</span><br><span class="line"> T& operator[] (<span class="type">const</span> <span class="type">void</span> *p) { </span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">array</span>[indexForPointer(p)].value; </span><br><span class="line"> }</span><br><span class="line"> <span class="type">const</span> T& operator[] (<span class="type">const</span> <span class="type">void</span> *p) <span class="type">const</span> { </span><br><span class="line"> <span class="keyword">return</span> const_cast<StripedMap<T>>(this)[p]; </span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p><code>StripedMap</code>重定义了数组运算符,传入对象的地址,通过哈希运算获得对应内容。在 runtime初始化后,会根据系统的不同,对应生成 8 或 64 个 <code>SideTable</code>留作以后使用。</p><p><code>SideTable</code> 里与弱引用有直接关系的是 weak 表。weak表通过哈希表实现,将目标对象的地址作为键值进行检索以获得对应的弱引用变量地址。由于一个对象可同时赋值给多个弱引用变量,所以对于一个键值,可以注册多个弱引用变量的地址。</p><p>接着看下 <code>weak_table</code> 的实现。</p><h2 id="weak_table">weak_table</h2><p>在 <strong>objc-weak.h</strong> 文件中 <code>weak_table_t</code>的源码为:</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">weak_table_t</span> {</span></span><br><span class="line"><span class="comment">// 弱引用条目列表</span></span><br><span class="line"> <span class="type">weak_entry_t</span> *weak_entries;</span><br><span class="line"> <span class="comment">// 弱引用条目的数量</span></span><br><span class="line"> <span class="type">size_t</span> num_entries;</span><br><span class="line"> <span class="comment">// 弱引用条目列表的大小</span></span><br><span class="line"> <span class="type">uintptr_t</span> mask;</span><br><span class="line"> <span class="comment">// 最大哈希偏移量</span></span><br><span class="line"> <span class="type">uintptr_t</span> max_hash_displacement;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>结构体中的 <code>weak_entries</code> 是一个动态列表,用来存储<code>weak_entry_t</code>类型的元素,需要对应到具体的内容。所以当出现冲突时还需要再处理,<code>max_hash_displacement</code>就是用于出现冲突后辅助检查检索的内容是否存在。</p><p>那 <code>weak_entry_t</code> 的结构又是怎样的?</p><h2 id="weak_entry_t">weak_entry_t</h2><p>在 <strong>objc-weak.h</strong> 文件中 <code>weak_entry_t</code>的源码为:</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><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> WEAK_INLINE_COUNT 4</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> REFERRERS_OUT_OF_LINE 2</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">weak_entry_t</span> {</span></span><br><span class="line"> DisguisedPtr<objc_object> referent;</span><br><span class="line"> <span class="class"><span class="keyword">union</span> {</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> {</span></span><br><span class="line"> <span class="type">weak_referrer_t</span> *referrers;</span><br><span class="line"> <span class="type">uintptr_t</span> out_of_line_ness : <span class="number">2</span>;</span><br><span class="line"> <span class="type">uintptr_t</span> num_refs : PTR_MINUS_2;</span><br><span class="line"> <span class="type">uintptr_t</span> mask;</span><br><span class="line"> <span class="type">uintptr_t</span> max_hash_displacement;</span><br><span class="line"> };</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> {</span></span><br><span class="line"> <span class="comment">// out_of_line_ness field is low bits of inline_referrers[1]</span></span><br><span class="line"> <span class="type">weak_referrer_t</span> inline_referrers[WEAK_INLINE_COUNT];</span><br><span class="line"> };</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="type">bool</span> <span class="title function_">out_of_line</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> (out_of_line_ness == REFERRERS_OUT_OF_LINE);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">weak_entry_t</span>& operator=(<span class="type">const</span> <span class="type">weak_entry_t</span>& other) {</span><br><span class="line"> <span class="built_in">memcpy</span>(this, &other, <span class="keyword">sizeof</span>(other));</span><br><span class="line"> <span class="keyword">return</span> *this;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">weak_entry_t</span>(objc_object *newReferent, objc_object **newReferrer)</span><br><span class="line"> : referent(newReferent)</span><br><span class="line"> {</span><br><span class="line"> inline_referrers[<span class="number">0</span>] = newReferrer;</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">1</span>; i < WEAK_INLINE_COUNT; i++) {</span><br><span class="line"> inline_referrers[i] = nil;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>在 <code>weak_entry_t</code>的结构中,<strong>目标对象</strong>和<strong>弱引用变量的指针</strong>都被封装在<strong>DisguisedPtr<objc_object></strong> 里。</p><p>同时用到了联合体,在联合体的内部有定长数组<code>inline_referrers[WEAK_INLINE_COUNT]</code> 和动态数组<code>weak_referrer_t *referrers</code>两种方式来存储弱引用对象的指针地址。通过 <code>out_of_line()</code>方法来判断采用哪种存储方式。当弱引用该对象的指针数目小于等于<code>WEAK_INLINE_COUNT</code> 时,使用定长数组。当超过<code>WEAK_INLINE_COUNT</code>时,会将定长数组中的元素转移到动态数组中,且之后都是用动态数组存储。</p><p>结合前面可以知道:弱引用表的结构是一个哈希表,key是所指对象的地址,value 是 weak指针的地址(它的值是所指对象的地址)数组。</p><p>那么弱引用表是怎么维护这些数据的?</p><h2 id="weak_register_no_lock">weak_register_no_lock()</h2><p>在 <strong>objc-weak.mm</strong> 文件中<code>weak_register_no_lock</code> 方法的源码为:</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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/** </span></span><br><span class="line"><span class="comment"> * 在弱引用表中查找对应的 weak_entry</span></span><br><span class="line"><span class="comment"> * 若找到,则向其中插入 weak 指针地址</span></span><br><span class="line"><span class="comment"> * 若未找到,新建一个 weak_entry</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param weak_table 全局弱引用表,类型为 weak_table_t</span></span><br><span class="line"><span class="comment"> * @param referent_id 弱指针</span></span><br><span class="line"><span class="comment"> * @param referrer_id 弱指针地址</span></span><br><span class="line"><span class="comment"> * @patam crashIfDeallocating 若被弱引用的对象正在析构,再次弱引用该对象是否 crash</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">id </span><br><span class="line"><span class="title function_">weak_register_no_lock</span><span class="params">(<span class="type">weak_table_t</span> *weak_table, id referent_id, </span></span><br><span class="line"><span class="params"> id *referrer_id, <span class="type">bool</span> crashIfDeallocating)</span></span><br><span class="line">{</span><br><span class="line"> objc_object *referent = (objc_object *)referent_id;</span><br><span class="line"> objc_object **referrer = (objc_object **)referrer_id;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果 referent 为 nil </span></span><br><span class="line"> <span class="comment">// 或 referent 是 TaggedPointer 计数方式,直接返回,不做任何操作</span></span><br><span class="line"> <span class="keyword">if</span> (!referent || referent->isTaggedPointer()) <span class="keyword">return</span> referent_id;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 确保被引用的对象可用(不在析构,且支持 weak 引用)</span></span><br><span class="line"> <span class="type">bool</span> deallocating;</span><br><span class="line"> <span class="keyword">if</span> (!referent->ISA()->hasCustomRR()) {</span><br><span class="line"> deallocating = referent->rootIsDeallocating();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> BOOL (*allowsWeakReference)(objc_object *, SEL) = </span><br><span class="line"> (BOOL(*)(objc_object *, SEL))</span><br><span class="line"> object_getMethodImplementation((id)referent, </span><br><span class="line"> @selector(allowsWeakReference));</span><br><span class="line"> <span class="keyword">if</span> ((IMP)allowsWeakReference == _objc_msgForward) {</span><br><span class="line"> <span class="keyword">return</span> nil;</span><br><span class="line"> }</span><br><span class="line"> deallocating =</span><br><span class="line"> ! (*allowsWeakReference)(referent, @selector(allowsWeakReference));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 正在析构的对象,不能够被弱引用</span></span><br><span class="line"> <span class="keyword">if</span> (deallocating) {</span><br><span class="line"> <span class="keyword">if</span> (crashIfDeallocating) {</span><br><span class="line"> _objc_fatal(<span class="string">"Cannot form weak reference to instance (%p) of "</span></span><br><span class="line"> <span class="string">"class %s. It is possible that this object was "</span></span><br><span class="line"> <span class="string">"over-released, or is in the process of deallocation."</span>,</span><br><span class="line"> (<span class="type">void</span>*)referent, object_getClassName((id)referent));</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> nil;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 在 weak_table 中找到 referent 对应的 weak_entry</span></span><br><span class="line"> <span class="comment">// 将 referrer 插入到 weak_entry_t 的引用数组中</span></span><br><span class="line"> <span class="type">weak_entry_t</span> *entry;</span><br><span class="line"> <span class="keyword">if</span> ((entry = weak_entry_for_referent(weak_table, referent))) {</span><br><span class="line"> append_referrer(entry, referrer);</span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 若未找到,新建一个</span></span><br><span class="line"> <span class="type">weak_entry_t</span> new_entry(referent, referrer);</span><br><span class="line"> weak_grow_maybe(weak_table);</span><br><span class="line"> weak_entry_insert(weak_table, &new_entry);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Do not set *referrer. objc_storeWeak() requires that the </span></span><br><span class="line"> <span class="comment">// value not change.</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> referent_id;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据代码过一下 <code>weak_register_no_lock()</code> 的内部实现。</p><ul><li>首先判断 <code>referent</code> 是否为 nil 或 <code>referent</code>是否用了 TaggedPointer 计数方式,如果是,直接返回不做任何操作。</li><li>再判断对象是否在析构,若是,根据 <code>crashIfDeallocating</code>判断是否抛出异常</li><li>如果对象不能被 weak 引用,直接返回 nil</li><li>当对象没有在析构且可以被 weak 引用,则调用<code>weak_entry_for_referent</code> 方法根据 <code>weak</code> 指针从<code>weak_table</code> 中查找对应的<code>weak_entry</code>。如果找到,则调用<code>append_referrer</code>方法,向 <code>weak_entry</code> 中插入 weak指针地址 <code>referrer</code>;反之,新建一个<code>weak_entry</code>。</li></ul><h3 id="weak_entry_for_referent">weak_entry_for_referent()</h3><p><code>weak_entry_for_referent</code> 方法主要是通过 weak 指针<code>referent</code> 在 <code>weak_table</code> 查找对应的<code>weak_entry_t</code>。在 <strong>objc-weak.mm</strong> 文件中<code>weak_entry_for_referent</code> 方法的源码为:</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="comment">/** </span></span><br><span class="line"><span class="comment"> * Return the weak reference table entry for the given referent. </span></span><br><span class="line"><span class="comment"> * If there is no entry for referent, return NULL. </span></span><br><span class="line"><span class="comment"> * Performs a lookup.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * @param weak_table 弱引用表</span></span><br><span class="line"><span class="comment"> * @param referent 弱指针,非 nil</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @return 返回查找到的 weak_entry_t</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">weak_entry_t</span> *</span><br><span class="line"><span class="title function_">weak_entry_for_referent</span><span class="params">(<span class="type">weak_table_t</span> *weak_table, objc_object *referent)</span></span><br><span class="line">{</span><br><span class="line"> ASSERT(referent);</span><br><span class="line"></span><br><span class="line"> <span class="type">weak_entry_t</span> *weak_entries = weak_table->weak_entries;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!weak_entries) <span class="keyword">return</span> nil;</span><br><span class="line"> <span class="comment">// 通过 & weak_table->mask 位操作,确保 index 不会越界</span></span><br><span class="line"> <span class="type">size_t</span> begin = hash_pointer(referent) & weak_table->mask;</span><br><span class="line"> <span class="type">size_t</span> index = begin;</span><br><span class="line"> <span class="type">size_t</span> hash_displacement = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (weak_table->weak_entries[index].referent != referent) {</span><br><span class="line"> index = (index+<span class="number">1</span>) & weak_table->mask;</span><br><span class="line"> <span class="comment">// 触发 bad weak table crash</span></span><br><span class="line"> <span class="keyword">if</span> (index == begin) bad_weak_table(weak_table->weak_entries);</span><br><span class="line"> hash_displacement++;</span><br><span class="line"> <span class="comment">// 当 hash_displacement(偏移量) 超过了 max_hash_displacement</span></span><br><span class="line"> <span class="comment">// 说明元素不在 hash 表中,返回 nil</span></span><br><span class="line"> <span class="keyword">if</span> (hash_displacement > weak_table->max_hash_displacement) {</span><br><span class="line"> <span class="keyword">return</span> nil;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> &weak_table->weak_entries[index];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="append_referrer">append_referrer()</h3><p>在 <strong>objc-weak.mm</strong> 文件中 <code>append_referrer</code>方法的源码为:</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><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><span class="line">60</span><br><span class="line">61</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/** </span></span><br><span class="line"><span class="comment"> * Add the given referrer to set of weak pointers in this entry.</span></span><br><span class="line"><span class="comment"> * Does not perform duplicate checking (b/c weak pointers are never</span></span><br><span class="line"><span class="comment"> * added to a set twice). </span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * @param entry The entry holding the set of weak pointers. </span></span><br><span class="line"><span class="comment"> * @param new_referrer The new weak pointer to be added.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">static</span> <span class="type">void</span> <span class="title function_">append_referrer</span><span class="params">(<span class="type">weak_entry_t</span> *entry, objc_object **new_referrer)</span></span><br><span class="line">{</span><br><span class="line"><span class="comment">// 判断 weak_entry 是否使用动态数组</span></span><br><span class="line"> <span class="keyword">if</span> (! entry->out_of_line()) {</span><br><span class="line"> <span class="comment">// 插入</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i < WEAK_INLINE_COUNT; i++) {</span><br><span class="line"> <span class="keyword">if</span> (entry->inline_referrers[i] == nil) {</span><br><span class="line"> entry->inline_referrers[i] = new_referrer;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 若未插入,代表静态数组已存满</span></span><br><span class="line"> <span class="comment">// 转换为动态数组</span></span><br><span class="line"> <span class="type">weak_referrer_t</span> *new_referrers = (<span class="type">weak_referrer_t</span> *)</span><br><span class="line"> <span class="built_in">calloc</span>(WEAK_INLINE_COUNT, <span class="keyword">sizeof</span>(<span class="type">weak_referrer_t</span>));</span><br><span class="line"> <span class="comment">// 将原静态数组中的项存储入动态数组</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i < WEAK_INLINE_COUNT; i++) {</span><br><span class="line"> new_referrers[i] = entry->inline_referrers[i];</span><br><span class="line"> }</span><br><span class="line"> entry->referrers = new_referrers;</span><br><span class="line"> entry->num_refs = WEAK_INLINE_COUNT;</span><br><span class="line"> entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;</span><br><span class="line"> entry->mask = WEAK_INLINE_COUNT<span class="number">-1</span>;</span><br><span class="line"> entry->max_hash_displacement = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> ASSERT(entry->out_of_line());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果动态数组中元素数 >= 数组总空间的3/4,则扩展数组空间为当前长度的一倍</span></span><br><span class="line"> <span class="comment">// 扩容完成,插入</span></span><br><span class="line"> <span class="keyword">if</span> (entry->num_refs >= TABLE_SIZE(entry) * <span class="number">3</span>/<span class="number">4</span>) {</span><br><span class="line"> <span class="keyword">return</span> grow_refs_and_insert(entry, new_referrer);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果不需要扩容,直接插入</span></span><br><span class="line"> <span class="comment">// '& (entry->mask)' 确保 begin 的位置只能大于或等于 mask (弱引用表大小)</span></span><br><span class="line"> <span class="type">size_t</span> begin = w_hash_pointer(new_referrer) & (entry->mask);</span><br><span class="line"> <span class="type">size_t</span> index = begin;</span><br><span class="line"> <span class="comment">// 用于记录 hash 偏移量</span></span><br><span class="line"> <span class="type">size_t</span> hash_displacement = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span> (entry->referrers[index] != nil) {</span><br><span class="line"> hash_displacement++;</span><br><span class="line"> index = (index+<span class="number">1</span>) & entry->mask;</span><br><span class="line"> <span class="keyword">if</span> (index == begin) bad_weak_table(entry);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (hash_displacement > entry->max_hash_displacement) {</span><br><span class="line"> entry->max_hash_displacement = hash_displacement;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 存入并更新 num_refs</span></span><br><span class="line"> <span class="type">weak_referrer_t</span> &ref = entry->referrers[index];</span><br><span class="line"> ref = new_referrer;</span><br><span class="line"> entry->num_refs++;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="weak_unregister_no_lock">weak_unregister_no_lock()</h2><p>若 weak 指针之前指向了弱引用,则会调用<code>weak_unregister_no_lock</code> 方法将旧的 <code>weak</code>指针地址移除。在 <strong>objc-weak.mm</strong> 文件中<code>weak_unregister_no_lock</code> 方法的源码为:</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/** </span></span><br><span class="line"><span class="comment"> * Unregister an already-registered weak reference.</span></span><br><span class="line"><span class="comment"> * This is used when referrer's storage is about to go away, but referent</span></span><br><span class="line"><span class="comment"> * isn't dead yet. (Otherwise, zeroing referrer later would be a</span></span><br><span class="line"><span class="comment"> * bad memory access.)</span></span><br><span class="line"><span class="comment"> * Does nothing if referent/referrer is not a currently active weak reference.</span></span><br><span class="line"><span class="comment"> * Does not zero referrer.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * FIXME currently requires old referent value to be passed in (lame)</span></span><br><span class="line"><span class="comment"> * FIXME unregistration should be automatic if referrer is collected</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param weak_table The global weak table.</span></span><br><span class="line"><span class="comment"> * @param referent The object.</span></span><br><span class="line"><span class="comment"> * @param referrer The weak reference.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">void</span></span><br><span class="line"><span class="title function_">weak_unregister_no_lock</span><span class="params">(<span class="type">weak_table_t</span> *weak_table, id referent_id, </span></span><br><span class="line"><span class="params"> id *referrer_id)</span></span><br><span class="line">{</span><br><span class="line"> objc_object *referent = (objc_object *)referent_id;</span><br><span class="line"> objc_object **referrer = (objc_object **)referrer_id;</span><br><span class="line"></span><br><span class="line"> <span class="type">weak_entry_t</span> *entry;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!referent) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 查找到 referent 所对应的 weak_entry_t</span></span><br><span class="line"> <span class="keyword">if</span> ((entry = weak_entry_for_referent(weak_table, referent))) {</span><br><span class="line"> <span class="comment">// 移除 referrer</span></span><br><span class="line"> remove_referrer(entry, referrer);</span><br><span class="line"> <span class="comment">// 移除后,要检查一下 weak_entry_t 的 hash 数组是否已经空了</span></span><br><span class="line"> <span class="type">bool</span> empty = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (entry->out_of_line() && entry->num_refs != <span class="number">0</span>) {</span><br><span class="line"> empty = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i < WEAK_INLINE_COUNT; i++) {</span><br><span class="line"> <span class="keyword">if</span> (entry->inline_referrers[i]) {</span><br><span class="line"> empty = <span class="literal">false</span>; </span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果 weak_entry_t 的 hash 数组为空</span></span><br><span class="line"> <span class="comment">// 则需要将 weak_entry_t 从 weak_table 中移除</span></span><br><span class="line"> <span class="keyword">if</span> (empty) {</span><br><span class="line"> weak_entry_remove(weak_table, entry);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Do not set *referrer = nil. objc_storeWeak() requires that the </span></span><br><span class="line"> <span class="comment">// value not change.</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述即为对一个对象做弱引用时底层所做的处理。通过弱引用对象,不会使其引用计数加一。那当对象释放时,所有弱引用该对象的指针有时如何自动置为nil 的?</p><h2 id="dealloc">dealloc</h2><p>当对象的引用计数为 0 时,该对象会进行释放,对应的源码如下:</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><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)dealloc {</span><br><span class="line"> _objc_rootDealloc(self);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="type">void</span></span><br><span class="line">_objc_rootDealloc(id obj)</span><br><span class="line">{</span><br><span class="line"> ASSERT(obj);</span><br><span class="line"></span><br><span class="line"> obj->rootDealloc();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">inline</span> <span class="type">void</span></span><br><span class="line"><span class="title function_">objc_object::rootDealloc</span><span class="params">()</span></span><br><span class="line">{</span><br><span class="line"><span class="comment">// 判断对象是否为 TaggedPointer,是则直接 return</span></span><br><span class="line"> <span class="keyword">if</span> (isTaggedPointer()) <span class="keyword">return</span>; <span class="comment">// fixme necessary?</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果对象是采用了优化的 isa 计数方式</span></span><br><span class="line"> <span class="comment">// 且 对象没有被弱引用 !isa.weakly_referenced</span></span><br><span class="line"> <span class="comment">// 且 没有关联对象 !isa.has_assoc</span></span><br><span class="line"> <span class="comment">// 且 没有自定义的 C++ 析构方法 !isa.has_cxx_dtor</span></span><br><span class="line"> <span class="comment">// 且 没有用到 SideTable 来引用计数 !isa.has_sidetable_rc</span></span><br><span class="line"> <span class="comment">// => 则直接释放</span></span><br><span class="line"> <span class="keyword">if</span> (fastpath(isa.nonpointer && </span><br><span class="line"> !isa.weakly_referenced && </span><br><span class="line"> !isa.has_assoc && </span><br><span class="line"> !isa.has_cxx_dtor && </span><br><span class="line"> !isa.has_sidetable_rc))</span><br><span class="line"> {</span><br><span class="line"> assert(!sidetable_present());</span><br><span class="line"> <span class="built_in">free</span>(this);</span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> object_dispose((id)this);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到底层调用了 <code>rootDealloc</code> 方法。</p><h3 id="object_dispose">object_dispose()</h3><p>在 <strong>objc-runtime-new.mm</strong> 文件中<code>object_dispose</code> 方法的源码为:</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/***********************************************************************</span></span><br><span class="line"><span class="comment">* object_dispose</span></span><br><span class="line"><span class="comment">* fixme</span></span><br><span class="line"><span class="comment">* Locking: none</span></span><br><span class="line"><span class="comment">**********************************************************************/</span></span><br><span class="line">id </span><br><span class="line"><span class="title function_">object_dispose</span><span class="params">(id obj)</span></span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (!obj) <span class="keyword">return</span> nil;</span><br><span class="line"></span><br><span class="line"> objc_destructInstance(obj); </span><br><span class="line"> <span class="built_in">free</span>(obj);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> nil;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/***********************************************************************</span></span><br><span class="line"><span class="comment">* objc_destructInstance</span></span><br><span class="line"><span class="comment">* Destroys an instance without freeing memory. </span></span><br><span class="line"><span class="comment">* Calls C++ destructors.</span></span><br><span class="line"><span class="comment">* Calls ARC ivar cleanup.</span></span><br><span class="line"><span class="comment">* Removes associative references.</span></span><br><span class="line"><span class="comment">* Returns `obj`. Does nothing if `obj` is nil.</span></span><br><span class="line"><span class="comment">**********************************************************************/</span></span><br><span class="line"><span class="type">void</span> *<span class="title function_">objc_destructInstance</span><span class="params">(id obj)</span> </span><br><span class="line">{</span><br><span class="line"> <span class="keyword">if</span> (obj) {</span><br><span class="line"> <span class="comment">// Read all of the flags at once for performance.</span></span><br><span class="line"> <span class="type">bool</span> cxx = obj->hasCxxDtor();</span><br><span class="line"> <span class="type">bool</span> assoc = obj->hasAssociatedObjects();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果有自定义的 C++ 析构方法,则调用 C++ 析构函数</span></span><br><span class="line"> <span class="keyword">if</span> (cxx) object_cxxDestruct(obj);</span><br><span class="line"> <span class="comment">// 如果有关联对象则移除关联对象</span></span><br><span class="line"> <span class="comment">// 并将其自身从 Association Manager 的 map 中移除</span></span><br><span class="line"> <span class="keyword">if</span> (assoc) _object_remove_assocations(obj);</span><br><span class="line"> <span class="comment">// 清除对象的相关引用</span></span><br><span class="line"> obj->clearDeallocating();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> obj;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="cleardeallocating">clearDeallocating()</h3><p>在 <strong>objc-object.h</strong> 文件中<code>clearDeallocating</code> 方法的源码为:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">inline</span> <span class="type">void</span> </span><br><span class="line"><span class="title function_">objc_object::clearDeallocating</span><span class="params">()</span></span><br><span class="line">{</span><br><span class="line"><span class="comment">// 判断对象是否采用了优化 isa 引用计数</span></span><br><span class="line"> <span class="keyword">if</span> (slowpath(!isa.nonpointer)) {</span><br><span class="line"> <span class="comment">// 没有,则清理对象存储在 SideTable 中的引用计数数据</span></span><br><span class="line"> sidetable_clearDeallocating();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 当采用了优化 isa 引用计数,则判断</span></span><br><span class="line"> <span class="comment">// 是否使用了 SideTable 的辅助引用计数 (isa.has_sidetable_rc)</span></span><br><span class="line"> <span class="comment">// 或是否有 weak 引用 (isa.weakly_referenced)</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {</span><br><span class="line"> <span class="comment">// Slow path for non-pointer isa with weak refs and/or side table data.</span></span><br><span class="line"> clearDeallocating_slow();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> assert(!sidetable_present());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="cleardeallocating_slow">clearDeallocating_slow()</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// Slow path of clearDeallocating() </span></span><br><span class="line"><span class="comment">// for objects with nonpointer isa</span></span><br><span class="line"><span class="comment">// that were ever weakly referenced </span></span><br><span class="line"><span class="comment">// or whose retain count ever overflowed to the side table.</span></span><br><span class="line">NEVER_INLINE <span class="type">void</span></span><br><span class="line"><span class="title function_">objc_object::clearDeallocating_slow</span><span class="params">()</span></span><br><span class="line">{</span><br><span class="line"> ASSERT(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 在全局的 SideTables 中,通过 this 指针为key,找到对应的 SideTable</span></span><br><span class="line"> SideTable& table = SideTables()[this];</span><br><span class="line"> table.lock();</span><br><span class="line"> <span class="comment">// 如果 obj 被弱引用</span></span><br><span class="line"> <span class="comment">// 在 SideTable 的 weak_table 中对 this 进行清理工作</span></span><br><span class="line"> <span class="keyword">if</span> (isa.weakly_referenced) {</span><br><span class="line"> weak_clear_no_lock(&table.weak_table, (id)this);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果采用了 SideTable 做引用计数</span></span><br><span class="line"> <span class="comment">// 在 SideTable 的引用计数中移除 this</span></span><br><span class="line"> <span class="keyword">if</span> (isa.has_sidetable_rc) {</span><br><span class="line"> table.refcnts.erase(this);</span><br><span class="line"> }</span><br><span class="line"> table.unlock();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="weak_clear_no_lock">weak_clear_no_lock()</h3><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/** </span></span><br><span class="line"><span class="comment"> * Called by dealloc; nils out all weak pointers that point to the </span></span><br><span class="line"><span class="comment"> * provided object so that they can no longer be used.</span></span><br><span class="line"><span class="comment"> * </span></span><br><span class="line"><span class="comment"> * @param weak_table </span></span><br><span class="line"><span class="comment"> * @param referent The object being deallocated. </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">void</span> </span><br><span class="line"><span class="title function_">weak_clear_no_lock</span><span class="params">(<span class="type">weak_table_t</span> *weak_table, id referent_id)</span> </span><br><span class="line">{</span><br><span class="line"> objc_object *referent = (objc_object *)referent_id;</span><br><span class="line"></span><br><span class="line"> <span class="type">weak_entry_t</span> *entry = weak_entry_for_referent(weak_table, referent);</span><br><span class="line"> <span class="keyword">if</span> (entry == nil) {</span><br><span class="line"> <span class="comment">/// XXX shouldn't happen, but does with mismatched CF/objc</span></span><br><span class="line"> <span class="comment">//printf("XXX no entry for clear deallocating %p\n", referent);</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// zero out references</span></span><br><span class="line"> <span class="type">weak_referrer_t</span> *referrers;</span><br><span class="line"> <span class="type">size_t</span> count;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 找出弱引用 referent 的弱指针地址数组以及数组长度</span></span><br><span class="line"> <span class="keyword">if</span> (entry->out_of_line()) {</span><br><span class="line"> referrers = entry->referrers;</span><br><span class="line"> count = TABLE_SIZE(entry);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> referrers = entry->inline_referrers;</span><br><span class="line"> count = WEAK_INLINE_COUNT;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i < count; ++i) {</span><br><span class="line"> <span class="comment">// 去除每一项对比置 nil 或报错</span></span><br><span class="line"> objc_object **referrer = referrers[i];</span><br><span class="line"> <span class="keyword">if</span> (referrer) {</span><br><span class="line"> <span class="keyword">if</span> (*referrer == referent) {</span><br><span class="line"> *referrer = nil;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (*referrer) {</span><br><span class="line"> _objc_inform(<span class="string">"__weak variable at %p holds %p instead of %p. "</span></span><br><span class="line"> <span class="string">"This is probably incorrect use of "</span></span><br><span class="line"> <span class="string">"objc_storeWeak() and objc_loadWeak(). "</span></span><br><span class="line"> <span class="string">"Break on objc_weak_error to debug.\n"</span>, </span><br><span class="line"> referrer, (<span class="type">void</span>*)*referrer, (<span class="type">void</span>*)referent);</span><br><span class="line"> objc_weak_error();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 由于 referent 要被释放了</span></span><br><span class="line"> <span class="comment">// 因此 referent 的 weak_entry_t 也要移除出 weak_table</span></span><br><span class="line"> weak_entry_remove(weak_table, entry);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="总结">总结</h2><p>weak 的实现原理在于底层维护了一份 weak_table_t 结构的哈希表,key为所指对象的地址,value 为 weak 指针的地址数组。weak关键字修饰的对象,代表弱引用,所引用对象的引用计数不会+1,在引用对象被释放时会自动置为 nil。</p><p>对象释放的过程中,通过底层触发 <code>clearDeallocating</code>函数方法,根据被释放对象地址通过查找获取得到所有 weak指针地址的数组,之后逐个遍历将它们置为 nil,最后把相关的 entry 从 weak表移除,最后清理对象的记录。</p><p>弱引用的核心部分有:SideTable、weak_table_t、weak_entry_t。关系图如下:</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/7a6ce4a3c88bdbcbad2c.png/9cx5EiUnX61sDPK.png" /></p><blockquote><p>参考内容: - <ahref="https://www.desgard.com/iOS-Source-Probe/Objective-C/Runtime/weak%20%E5%BC%B1%E5%BC%95%E7%94%A8%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F.html">weak弱引用的实现方式</a> - - <ahref="https://blog.csdn.net/u013378438/article/details/82767947">weak引用的底层实现原理</a></p></blockquote>]]></content>
<summary type="html"><p>iOS
内是通过引用计数来管理内存,引用计数的管理,很容易会出现“循环引用”问题。<code>weak</code>
修饰符,也是开发日常最常用的打破循环引用方式。被 <code>weak</code>
修饰符修饰的弱引用除了不会增加对象的引用计数外;在引用对象被释放后,这个弱引用会自动失效并置为
nil。本篇总结分析下 Objective-C 中 <code>weak</code>
都是怎么实现的。</p>
<p>分析源码基于:<a
href="https://github.com/DeveloperErenLiu/RuntimeAnalyze"><strong>DeveloperErenLiu/RuntimeAnalyze/objc4-799.1</strong></a>。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="Objective-C" scheme="http://example.com/categories/Objective-C/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="Objective-C" scheme="http://example.com/tags/Objective-C/"/>
</entry>
<entry>
<title>记一次 clang 编译错误的解决</title>
<link href="http://example.com/2018/05/03/2018-05-03-clang-fatal-error/"/>
<id>http://example.com/2018/05/03/2018-05-03-clang-fatal-error/</id>
<published>2018-05-02T16:00:00.000Z</published>
<updated>2018-05-02T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>记录 clang 编译错误的解决。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ clang -rewrite-objc main.m </span><br></pre></td></tr></table></figure><span id="more"></span><p>错误信息:</p><figure class="highlight bash"><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">main.m:9:9: fatal error: <span class="string">'UIKit/UIKit.h'</span> file not found</span><br><span class="line"><span class="comment">#import <UIKit/UIKit.h></span></span><br><span class="line"> ^~~~~~~~~~~~~~~</span><br><span class="line">1 error generated.</span><br></pre></td></tr></table></figure><p>替换方法:</p><figure class="highlight bash"><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></pre></td><td class="code"><pre><span class="line">$ $ clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m</span><br><span class="line"></span><br><span class="line">$ xcrun -sdk iphonesimulator clang -rewrite-objc main.m</span><br><span class="line"></span><br><span class="line">$ xcrun -sdk iphoneos clang -rewrite-objc main.m</span><br><span class="line"></span><br><span class="line">$ xcrun -sdk iphonesimulator13.0 clang -rewrite-objc main.m</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p>记录 clang 编译错误的解决。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ clang -rewrite-objc main.m </span><br></pre></td></tr></table></figure></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="开发报错" scheme="http://example.com/categories/%E5%BC%80%E5%8F%91%E6%8A%A5%E9%94%99/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="开发报错" scheme="http://example.com/tags/%E5%BC%80%E5%8F%91%E6%8A%A5%E9%94%99/"/>
</entry>
<entry>
<title>NSHashTable 和 NSMapTable</title>
<link href="http://example.com/2017/10/03/2017-10-03-nshashtable-and-nsmaptable/"/>
<id>http://example.com/2017/10/03/2017-10-03-nshashtable-and-nsmaptable/</id>
<published>2017-10-02T16:00:00.000Z</published>
<updated>2017-10-02T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>NSSet,NSDictionary,NSArray 是 Foundation框架关于集合操作的常用类。在 NSSet 中,objects 是被强引用的(stronglyreferenced),同样 NSDictionary 中的 keys 和 values 也会被 NSDictionary复制。如果我们想要存储一个 weak 类型的值或者使用一个没有实现 NSCopying协议的 object 作为 NSDictionary 的 key,就可以分别使用和NSSet,NSDictionary 地位相同的 NSHashTable,NSMapTable。</p><span id="more"></span><h2 id="nshashtable">NSHashTable</h2><p>NSHashTable 是更广泛意义的 NSSet,NSHashTable 相比 NSSet/NSMutableSet有如下特性:</p><ul><li>NSHashTable 是可变的</li><li>NSHashTable 可以持有 weak 类型的成员变量</li><li>NSHashTable 可以在添加成员变量是复制成员</li><li>NSHashTable 可以随意存储指针并利用指针的唯一性来进行 hash查重和对比(equal)操作</li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="built_in">NSHashTable</span> *hashTable = [<span class="built_in">NSHashTable</span> hashTableWithOptions:<span class="built_in">NSPointerFunctionsCopyIn</span>];</span><br><span class="line">[hashTable addObject:<span class="string">@"a"</span>];</span><br><span class="line">[hashTable addObject:<span class="string">@"b"</span>]; </span><br><span class="line">[hashTable addObject:@<span class="number">11</span>];</span><br><span class="line">[hashTable removeObject:<span class="string">@"b"</span>];</span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"Items: %@"</span>, [hashTable allObjects]);</span><br></pre></td></tr></table></figure><p>NSHashTable 是根据一个 option 参数来进行初始化,option 可选项有:</p><ul><li><strong>NSHashTableStrongMemory</strong>:对成员变量进行强引用,这是一个默认值,如果采用这个默认值,NSHashTable和 NSSet 就没有了区别。</li><li><strong>NSHashTableWeakMemory</strong>:对成员变量进行弱引用,object引用在最后释放的时候会被指向 NULL。</li><li><strong>NSHashTableCopyIn</strong>:在对象被加入集合之前进行复制。</li><li><strong>NSHashTableObjectPointerPersonality</strong>:用指针来等同代替实际的值,当打印这个指针的时候相当于调用description 方法。</li><li><strong>NSHashTableZeroingWeakMemory</strong>:已被抛弃,使用NSHashTableWeakMemory 代替。</li></ul><h2 id="nsmaptable">NSMapTable</h2><p>NSDictionary/NSMutableDictionary 会复制 keys 并通过强引用 values来实现存储。NSMapTable 是更广泛意义的 NSDictionary。NSMapTable 相比NSDictionary/NSMutableDictionary 有如下特性:</p><ul><li>NSMapTable 是可变的。</li><li>NSMapTable 可以通过弱引用来持有 keys 和 values。当 key 或 value 被deallocated 时,对应存储的内容也会被移除。</li><li>NSMapTable 可以在添加 value 的时候对 value 进行复制。</li></ul><p>NSMapTable 和 NSHashTable类似,可以随意的存储指针,并利用指针的唯一性来进行 hash 查重。</p><h3 id="应用">应用</h3><p>假设用 NSMapTable 来存储不用被复制的 keys 和被若引用的 value,这里的value 就可以是某个 delegate 或者一种弱类型。</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></pre></td><td class="code"><pre><span class="line"><span class="type">id</span> delegate = ...;</span><br><span class="line"><span class="built_in">NSMapTable</span> *mapTable = [<span class="built_in">NSMapTable</span> mapTableWithKeyOptions:<span class="built_in">NSMapTableStrongMemory</span></span><br><span class="line"> valueOptions:<span class="built_in">NSMapTableWeakMemory</span>];</span><br><span class="line">[mapTable setObject:delegate forKey:<span class="string">@"foo"</span>];</span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"Keys: %@"</span>, [[mapTable keyEnumerator] allObjects]);</span><br></pre></td></tr></table></figure><h2 id="总结">总结</h2><p>日常开发过程中 NSSet 和 NSDictionary可以解决我们的大多数需求,如果有内存相关处理问题时,可以借助于NSHashTable 和 NSMapTable。</p><blockquote><p>参考内容: - <ahref="https://nshipster.com/nshashtable-and-nsmaptable/">NSHashTable& NSMapTable</a></p></blockquote>]]></content>
<summary type="html"><p>NSSet,NSDictionary,NSArray 是 Foundation
框架关于集合操作的常用类。在 NSSet 中,objects 是被强引用的(strongly
referenced),同样 NSDictionary 中的 keys 和 values 也会被 NSDictionary
复制。如果我们想要存储一个 weak 类型的值或者使用一个没有实现 NSCopying
协议的 object 作为 NSDictionary 的 key,就可以分别使用和
NSSet,NSDictionary 地位相同的 NSHashTable,NSMapTable。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
</entry>
<entry>
<title>YYCache 源码梳理</title>
<link href="http://example.com/2017/10/01/2017-10-01-yycache/"/>
<id>http://example.com/2017/10/01/2017-10-01-yycache/</id>
<published>2017-09-30T16:00:00.000Z</published>
<updated>2017-09-30T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>本篇是对 <a href="https://github.com/ibireme/YYCache">YYCache</a>源码阅读过程中的梳理。YYCache 是一个线程安全的高性能<strong>Key-Value</strong> 缓存框架。代码质量很高,值得拿来学习。</p><span id="more"></span><h2 id="yycache-的框架结构">YYCache 的框架结构</h2><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/cb8ec6ffccc8e04e67e4.jpg/Obih8ZBfWxAgIDR.jpg" /></p><p>如上是 YYCache 的框架结构图,本篇按照下面的分类来进行梳理:</p><ul><li>YYCache</li><li>YYMemoryCache</li><li>YYDiskCache</li><li>NSMapTable</li><li>如何保证的线程安全</li></ul><h2 id="yycache">YYCache</h2><p><strong>YYCache.h</strong> 做一个简化:</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><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">YYCache</span> : <span class="title">NSObject</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">copy</span>, <span class="keyword">readonly</span>) <span class="built_in">NSString</span> *name; <span class="comment">//缓存名</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">strong</span>, <span class="keyword">readonly</span>) YYMemoryCache *memoryCache;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">strong</span>, <span class="keyword">readonly</span>) YYDiskCache *diskCache;</span><br><span class="line"></span><br><span class="line"><span class="comment">//</span></span><br><span class="line">- (<span class="type">BOOL</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="built_in">NSString</span> *key, <span class="type">BOOL</span> contains))block;</span><br><span class="line"></span><br><span class="line"><span class="comment">//</span></span><br><span class="line">- (<span class="keyword">nullable</span> <span class="type">id</span><<span class="built_in">NSCoding</span>>)objectForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)objectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="built_in">NSString</span> *key, <span class="type">id</span><<span class="built_in">NSCoding</span>> object))block;</span><br><span class="line"></span><br><span class="line"><span class="comment">//</span></span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="keyword">nullable</span> <span class="type">id</span><<span class="built_in">NSCoding</span>>)object forKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="keyword">nullable</span> <span class="type">id</span><<span class="built_in">NSCoding</span>>)object forKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line"></span><br><span class="line"><span class="comment">//</span></span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="built_in">NSString</span> *key))block;</span><br><span class="line"></span><br><span class="line"><span class="comment">//</span></span><br><span class="line">- (<span class="type">void</span>)removeAllObjects;</span><br><span class="line">- (<span class="type">void</span>)removeAllObjectsWithBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line">- (<span class="type">void</span>)removeAllObjectsWithProgressBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">int</span> removedCount, <span class="type">int</span> totalCount))progress</span><br><span class="line"> endBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">BOOL</span> error))end;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>接口内容包含了缓存框架所需要的增删改查,函数的命名很清晰,不一一注释了。</p><h3 id="接口实现">接口实现</h3><p>从接口文件可以看到,YYCache 内的增删改查都提供了有无 Block回调的两种方式。以有 Block 回调来看下,增删改查的实现。</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span> (^)(<span class="built_in">NSString</span> *key, <span class="type">BOOL</span> contains))block {</span><br><span class="line"> <span class="keyword">if</span> (!block) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> ([_memoryCache containsObjectForKey:key]) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>), ^{</span><br><span class="line"> block(key, <span class="literal">YES</span>);</span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> [_diskCache containsObjectForKey:key withBlock:block];</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)objectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span> (^)(<span class="built_in">NSString</span> *key, <span class="type">id</span><<span class="built_in">NSCoding</span>> object))block {</span><br><span class="line"> <span class="keyword">if</span> (!block) <span class="keyword">return</span>;</span><br><span class="line"> <span class="type">id</span><<span class="built_in">NSCoding</span>> object = [_memoryCache objectForKey:key];</span><br><span class="line"> <span class="keyword">if</span> (object) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>), ^{</span><br><span class="line"> block(key, object);</span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> [_diskCache objectForKey:key withBlock:^(<span class="built_in">NSString</span> *key, <span class="type">id</span><<span class="built_in">NSCoding</span>> object) {</span><br><span class="line"> <span class="keyword">if</span> (object && ![_memoryCache objectForKey:key]) {</span><br><span class="line"> [_memoryCache setObject:object forKey:key];</span><br><span class="line"> }</span><br><span class="line"> block(key, object);</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="type">void</span>)setObject:(<span class="type">id</span><<span class="built_in">NSCoding</span>>)object forKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span> (^)(<span class="type">void</span>))block {</span><br><span class="line"> [_memoryCache setObject:object forKey:key];</span><br><span class="line"> [_diskCache setObject:object forKey:key withBlock:block];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span> (^)(<span class="built_in">NSString</span> *key))block {</span><br><span class="line"> [_memoryCache removeObjectForKey:key];</span><br><span class="line"> [_diskCache removeObjectForKey:key withBlock:block];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeAllObjectsWithBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block {</span><br><span class="line"> [_memoryCache removeAllObjects];</span><br><span class="line"> [_diskCache removeAllObjectsWithBlock:block];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeAllObjectsWithProgressBlock:(<span class="type">void</span>(^)(<span class="type">int</span> removedCount, <span class="type">int</span> totalCount))progress</span><br><span class="line"> endBlock:(<span class="type">void</span>(^)(<span class="type">BOOL</span> error))end {</span><br><span class="line"> [_memoryCache removeAllObjects];</span><br><span class="line"> [_diskCache removeAllObjectsWithProgressBlock:progress endBlock:end];</span><br><span class="line"> </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如上实现可以看到:</p><ul><li>YYCache每次的增删改查操作都是优先操作**_memoryCache<strong>,再操作</strong>_diskCache**。</li><li>关于回调:<ul><li><code>-containsObjectForKey:withBlock:</code> 与<code>-objectForKey:withBlock:</code> 优先以 **_memoryCache**的返回数据回调。</li><li>其他方法,以 **_diskCache** 的回调为准。</li></ul></li></ul><p>到这里我们已经知道,YYCache 有哪些对数据项的操作接口;也可以看出在YYCache 这一层实际上并没有自身去处理数据,而是借助于 **_memoryCache** 和**_diskCache**。</p><p>依次来看下他们是如何操作数据的。</p><h2 id="lru">LRU</h2><p>前面提到 YYCache 对数据的操作借助于 **_memoryCache** 和**_diskCache**。这里延伸出一个问题:为什么缓存设计框架,需要同时存在内存缓存和磁盘缓存呢?</p><p>对于数据有个<strong>命中率</strong>的概念。所谓命中率,即:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">命中率 = 命中数 / (命中数 + 未命中数)</span><br></pre></td></tr></table></figure><p>命中率是判断一个缓存框架加速效果好坏的重要标准之一。对于缓存框架为什么分<strong>内存缓存</strong>与<strong>磁盘缓存</strong>,自己的理解是,<strong>命中数</strong>代表拿到了数据,<strong>未命中数据</strong>代表没拿到数据;而拿到数据的过程也分快和满,我们知道的是读取内存缓存会比磁盘缓存快很多。借助于这一点,在保证数据命中率的前提下,如果能够尽量使用内存缓存进行操作,是一个很好的提速方案。</p><p>当然,随着自我发问,这里也引出了另一个问题:</p><ul><li>内存是有限的,怎样在合适的时机将数据放到内存?</li></ul><p>YYCache 内部使用了 LRU 来达到这个目的。我们来看下 LRU 的源码实现,以YYMemoryCache 内实现为例。</p><p>YYMemoryCache是通过一个<strong>链表节点类</strong>(<code>_YYLinkedMapNode</code>)来保存某个单独的内存缓存;再通过一个<strong>双向链表类</strong>(<code>_YYLinkedMap</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><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">_YYLinkedMapNode</span> : <span class="title">NSObject</span> </span>{</span><br><span class="line"> <span class="keyword">@package</span></span><br><span class="line"> __<span class="keyword">unsafe_unretained</span> _YYLinkedMapNode *_prev; <span class="comment">// retained by dic</span></span><br><span class="line"> __<span class="keyword">unsafe_unretained</span> _YYLinkedMapNode *_next; <span class="comment">// retained by dic</span></span><br><span class="line"> <span class="type">id</span> _key; <span class="comment">// 缓存 key</span></span><br><span class="line"> <span class="type">id</span> _value; <span class="comment">// 缓存内容</span></span><br><span class="line"> <span class="built_in">NSUInteger</span> _cost; <span class="comment">// 缓存消耗</span></span><br><span class="line"> <span class="built_in">NSTimeInterval</span> _time; <span class="comment">// 上次访问时间</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">_YYLinkedMapNode</span></span></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><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><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">_YYLinkedMap</span> : <span class="title">NSObject</span> </span>{</span><br><span class="line"> <span class="keyword">@package</span></span><br><span class="line"> <span class="built_in">CFMutableDictionaryRef</span> _dic; <span class="comment">// 存放节点(_YYLinkedMapNode)</span></span><br><span class="line"> <span class="built_in">NSUInteger</span> _totalCost; <span class="comment">// 总消耗</span></span><br><span class="line"> <span class="built_in">NSUInteger</span> _totalCount; <span class="comment">// 节点总数</span></span><br><span class="line"> _YYLinkedMapNode *_head; <span class="comment">// 链表头节点</span></span><br><span class="line"> _YYLinkedMapNode *_tail; <span class="comment">// 链表尾节点</span></span><br><span class="line"> <span class="type">BOOL</span> _releaseOnMainThread; <span class="comment">// 是否主线程释放,默认 NO</span></span><br><span class="line"> <span class="type">BOOL</span> _releaseAsynchronously; <span class="comment">// 是否异步释放,默认 YES</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)insertNodeAtHead:(_YYLinkedMapNode *)node;</span><br><span class="line">- (<span class="type">void</span>)bringNodeToHead:(_YYLinkedMapNode *)node;</span><br><span class="line">- (<span class="type">void</span>)removeNode:(_YYLinkedMapNode *)node;</span><br><span class="line">- (_YYLinkedMapNode *)removeTailNode;</span><br><span class="line">- (<span class="type">void</span>)removeAll;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">_YYLinkedMap</span></span></span><br><span class="line"><span class="comment">// 方法实现省略...</span></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>从分析 LRU 实现角度出发,核心的几个属性分别为:</p><ul><li><code>_YYLinkedMapNode</code><ul><li><code>_prev</code>:前指针</li><li><code>_next</code>:后指针</li></ul></li><li><code>_YYLinkedMap</code><ul><li><code>_dic</code>:存放节点</li><li><code>_head</code>:头节点</li><li><code>_tail</code>:尾节点</li></ul></li></ul><p>带着这些属性,看下具体的源码对双向链表 LRU 的实现过程。</p><h3 id="insertnodeathead">insertNodeAtHead:</h3><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)insertNodeAtHead:(_YYLinkedMapNode *)node {</span><br><span class="line"><span class="comment">// 将 node 的 key-value 存入 Map 的 _dic</span></span><br><span class="line"> <span class="built_in">CFDictionarySetValue</span>(_dic, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(node->_key), (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(node));</span><br><span class="line"> _totalCost += node->_cost;</span><br><span class="line"> _totalCount++;</span><br><span class="line"> <span class="keyword">if</span> (_head) {</span><br><span class="line"> node->_next = _head;</span><br><span class="line"> _head->_prev = node;</span><br><span class="line"> _head = node;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> _head = _tail = node;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述: - 将 node 的 key-value 存入**_YYLinkedMap**的<code>_dic</code>。 - 更新 <code>_totalCost</code> 和<code>_totalCount</code>。 - 若 map 中有 <code>_head</code>,将 node插到 headNode,并更新 <code>_head</code> - 若 map 中无<code>_head</code>,初始化 <code>_head</code>、<code>_tail</code> 为node</p><h3 id="bringnodetohead">bringNodeToHead:</h3><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)bringNodeToHead:(_YYLinkedMapNode *)node {</span><br><span class="line"> <span class="keyword">if</span> (_head == node) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (_tail == node) {</span><br><span class="line"> _tail = node->_prev;</span><br><span class="line"> _tail->_next = <span class="literal">nil</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> node->_next->_prev = node->_prev;</span><br><span class="line"> node->_prev->_next = node->_next;</span><br><span class="line"> }</span><br><span class="line"> node->_next = _head;</span><br><span class="line"> node->_prev = <span class="literal">nil</span>;</span><br><span class="line"> _head->_prev = node;</span><br><span class="line"> _head = node;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述:</p><ul><li>若已经是 <code>_head</code>,return</li><li>若 node 为 <code>_tail</code>,更新 <code>_tail</code> 为 node前节点。将 node 放入当前头节点前,并修改 <code>_head</code></li><li>若 node 不为 <code>_tail</code>,更新 node 前后节点的<code>_next</code> 和 <code>_prev</code>(可以理解为先删除 node)。将node 放入当前头节点前,并修改 <code>_head</code>。</li></ul><h3 id="removenode">removeNode:</h3><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)removeNode:(_YYLinkedMapNode *)node {</span><br><span class="line"><span class="comment">// 在 _dic 中移除 node 对应的 key-value</span></span><br><span class="line"> <span class="built_in">CFDictionaryRemoveValue</span>(_dic, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(node->_key));</span><br><span class="line"> _totalCost -= node->_cost;</span><br><span class="line"> _totalCount--;</span><br><span class="line"> <span class="keyword">if</span> (node->_next) node->_next->_prev = node->_prev;</span><br><span class="line"> <span class="keyword">if</span> (node->_prev) node->_prev->_next = node->_next;</span><br><span class="line"> <span class="keyword">if</span> (_head == node) _head = node->_next;</span><br><span class="line"> <span class="keyword">if</span> (_tail == node) _tail = node->_prev;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述: - 从 Map 的 <code>_dic</code> 中移除 node 对应的 key-value- 更新 <code>_totalCost</code>、<code>_totalCount</code> - 更新 node后前节点的指针,需判断是否有 <code>_next</code> 和 <code>_prev</code> -若 node 为 <code>_head</code>,更新 <code>_head</code> - 若 node 为<code>_tail</code>,更新 <code>_tail</code></p><h3 id="removetailnode">removeTailNode:</h3><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></pre></td><td class="code"><pre><span class="line">- (_YYLinkedMapNode *)removeTailNode {</span><br><span class="line"> <span class="keyword">if</span> (!_tail) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> <span class="comment">// 取出 _tail,并从 map 的 _dic 中移除 key-value</span></span><br><span class="line"> _YYLinkedMapNode *tail = _tail;</span><br><span class="line"> <span class="built_in">CFDictionaryRemoveValue</span>(_dic, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(_tail->_key));</span><br><span class="line"> _totalCost -= _tail->_cost;</span><br><span class="line"> _totalCount--;</span><br><span class="line"> <span class="keyword">if</span> (_head == _tail) {</span><br><span class="line"> _head = _tail = <span class="literal">nil</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> _tail = _tail->_prev;</span><br><span class="line"> _tail->_next = <span class="literal">nil</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> tail;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述: - 根据 Map 的 <code>_tail</code> 取出尾节点 node,并从<code>_dic</code> 中移除 key-value - 更新<code>_totalCost</code>、<code>_totalCount</code> -若链表只有一个节点(<code>_head</code> == <code>_tail</code>),置空<code>_head</code> 和 <code>_tail</code> - 否则更新 <code>_tail</code> -返回被移除的尾节点 node</p><h3 id="removeall">removeAll:</h3><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><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)removeAll {</span><br><span class="line"> _totalCost = <span class="number">0</span>;</span><br><span class="line"> _totalCount = <span class="number">0</span>;</span><br><span class="line"> _head = <span class="literal">nil</span>;</span><br><span class="line"> _tail = <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">CFDictionaryGetCount</span>(_dic) > <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">CFMutableDictionaryRef</span> holder = _dic;</span><br><span class="line"> _dic = <span class="built_in">CFDictionaryCreateMutable</span>(<span class="built_in">CFAllocatorGetDefault</span>(), <span class="number">0</span>, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (_releaseAsynchronously) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> <span class="built_in">CFRelease</span>(holder); <span class="comment">// hold and release in specified queue</span></span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (_releaseOnMainThread && !pthread_main_np()) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(dispatch_get_main_queue(), ^{</span><br><span class="line"> <span class="built_in">CFRelease</span>(holder); <span class="comment">// hold and release in specified queue</span></span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">CFRelease</span>(holder);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述: - 重置<code>_totalCost</code>、<code>_totalCount</code>、<code>_head</code>、<code>_tail</code>- 判断是否 <code>_dic</code> 是否有内容,有则继续 - 若异步释放<code>_dic</code>,再判断是否主线程释放: - 主线程释放 -<code>YYMemoryCacheGetReleaseQueue()</code> 释放 -若主线程释放,判断当前是否在主线程: - 不在主线程,放到主线程释放 -在主线程,直接释放</p><h3 id="lru-小结">LRU 小结</h3><p>到这里,我们已经完成了针对 node增删改查的所有操作。在每次的操作过程中,都会根据需要修改链表中的节点和指针。这些也是实现LRU 的基础。YYCache 中 LRU 的实现依赖于这里的双向链表。</p><h2 id="yymemorycache">YYMemoryCache</h2><p>简化后的 <code>YYMemoryCache.h</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><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">YYMemoryCache</span> : <span class="title">NSObject</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="built_in">NSString</span> *name;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">readonly</span>) <span class="built_in">NSUInteger</span> totalCount;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">readonly</span>) <span class="built_in">NSUInteger</span> totalCost;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSUInteger</span> countLimit;</span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSUInteger</span> costLimit;</span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSTimeInterval</span> ageLimit;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSTimeInterval</span> autoTrimInterval;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> <span class="type">BOOL</span> shouldRemoveAllObjectsOnMemoryWarning;</span><br><span class="line"><span class="keyword">@property</span> <span class="type">BOOL</span> shouldRemoveAllObjectsWhenEnteringBackground;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="type">void</span>(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="type">void</span>(^didEnterBackgroundBlock)(YYMemoryCache *cache);</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> <span class="type">BOOL</span> releaseOnMainThread;</span><br><span class="line"><span class="keyword">@property</span> <span class="type">BOOL</span> releaseAsynchronously;</span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)containsObjectForKey:(<span class="type">id</span>)key;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">nullable</span> <span class="type">id</span>)objectForKey:(<span class="type">id</span>)key;</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="keyword">nullable</span> <span class="type">id</span>)object forKey:(<span class="type">id</span>)key;</span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="keyword">nullable</span> <span class="type">id</span>)object forKey:(<span class="type">id</span>)key withCost:(<span class="built_in">NSUInteger</span>)cost;</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="type">id</span>)key;</span><br><span class="line">- (<span class="type">void</span>)removeAllObjects;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Trim</span></span><br><span class="line">- (<span class="type">void</span>)trimToCount:(<span class="built_in">NSUInteger</span>)count;</span><br><span class="line">- (<span class="type">void</span>)trimToCost:(<span class="built_in">NSUInteger</span>)cost;</span><br><span class="line">- (<span class="type">void</span>)trimToAge:(<span class="built_in">NSTimeInterval</span>)age;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>顾名思义的接口,看的很舒服,不需要额外的注释。对应看下实现文件的源码实现。</p><h3 id="初始化">初始化</h3><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="keyword">instancetype</span>)init {</span><br><span class="line"> <span class="keyword">self</span> = <span class="variable language_">super</span>.init;</span><br><span class="line"> pthread_mutex_init(&_lock, <span class="literal">NULL</span>);</span><br><span class="line"> _lru = [_YYLinkedMap new];</span><br><span class="line"> _queue = dispatch_queue_create(<span class="string">"com.ibireme.cache.memory"</span>, DISPATCH_QUEUE_SERIAL);</span><br><span class="line"> </span><br><span class="line"> _countLimit = <span class="built_in">NSUIntegerMax</span>;</span><br><span class="line"> _costLimit = <span class="built_in">NSUIntegerMax</span>;</span><br><span class="line"> _ageLimit = DBL_MAX;</span><br><span class="line"> _autoTrimInterval = <span class="number">5.0</span>;</span><br><span class="line"> _shouldRemoveAllObjectsOnMemoryWarning = <span class="literal">YES</span>;</span><br><span class="line"> _shouldRemoveAllObjectsWhenEnteringBackground = <span class="literal">YES</span>;</span><br><span class="line"> </span><br><span class="line"> [[<span class="built_in">NSNotificationCenter</span> defaultCenter] addObserver:<span class="keyword">self</span> selector:<span class="keyword">@selector</span>(_appDidReceiveMemoryWarningNotification) name:<span class="built_in">UIApplicationDidReceiveMemoryWarningNotification</span> object:<span class="literal">nil</span>];</span><br><span class="line"> [[<span class="built_in">NSNotificationCenter</span> defaultCenter] addObserver:<span class="keyword">self</span> selector:<span class="keyword">@selector</span>(_appDidEnterBackgroundNotification) name:<span class="built_in">UIApplicationDidEnterBackgroundNotification</span> object:<span class="literal">nil</span>];</span><br><span class="line"> </span><br><span class="line"> [<span class="keyword">self</span> _trimRecursively];</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中 <code>_lru</code> 是 YYMemoryCache 与 <code>_YYLinkedMap</code>的一个联系点,所有的增删改查操作,都需要通过 <code>_lru</code>来实现。</p><p>除此之外,我们看到了一些属性的默认值,如: - 不限制<code>_countLimit</code>、<code>_costLimit</code>、<code>_ageLimit</code>- <code>_autoTrimInterval</code> 自动清理时间为 5 s -默认内存警告时,清空内存缓存 - 默认进入后台时,清空内存缓存 -开启自动清理,后面再细说清理过程</p><h3 id="增删改查的接口实现">增删改查的接口实现</h3><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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="type">id</span>)objectForKey:(<span class="type">id</span>)key {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> _YYLinkedMapNode *node = <span class="built_in">CFDictionaryGetValue</span>(_lru->_dic, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(key));</span><br><span class="line"> <span class="keyword">if</span> (node) {</span><br><span class="line"> node->_time = <span class="built_in">CACurrentMediaTime</span>();</span><br><span class="line"> [_lru bringNodeToHead:node];</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> <span class="keyword">return</span> node ? node->_value : <span class="literal">nil</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="type">id</span>)object forKey:(<span class="type">id</span>)key {</span><br><span class="line"> [<span class="keyword">self</span> setObject:object forKey:key withCost:<span class="number">0</span>];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="type">id</span>)object forKey:(<span class="type">id</span>)key withCost:(<span class="built_in">NSUInteger</span>)cost {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">if</span> (!object) {</span><br><span class="line"> [<span class="keyword">self</span> removeObjectForKey:key];</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> _YYLinkedMapNode *node = <span class="built_in">CFDictionaryGetValue</span>(_lru->_dic, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(key));</span><br><span class="line"> <span class="built_in">NSTimeInterval</span> now = <span class="built_in">CACurrentMediaTime</span>();</span><br><span class="line"> <span class="keyword">if</span> (node) {</span><br><span class="line"> _lru->_totalCost -= node->_cost;</span><br><span class="line"> _lru->_totalCost += cost;</span><br><span class="line"> node->_cost = cost;</span><br><span class="line"> node->_time = now;</span><br><span class="line"> node->_value = object;</span><br><span class="line"> [_lru bringNodeToHead:node];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> node = [_YYLinkedMapNode new];</span><br><span class="line"> node->_cost = cost;</span><br><span class="line"> node->_time = now;</span><br><span class="line"> node->_key = key;</span><br><span class="line"> node->_value = object;</span><br><span class="line"> [_lru insertNodeAtHead:node];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (_lru->_totalCost > _costLimit) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(_queue, ^{</span><br><span class="line"> [<span class="keyword">self</span> trimToCost:_costLimit];</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (_lru->_totalCount > _countLimit) {</span><br><span class="line"> _YYLinkedMapNode *node = [_lru removeTailNode];</span><br><span class="line"> <span class="keyword">if</span> (_lru->_releaseAsynchronously) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> [node <span class="keyword">class</span>]; <span class="comment">//hold and release in queue</span></span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (_lru->_releaseOnMainThread && !pthread_main_np()) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(dispatch_get_main_queue(), ^{</span><br><span class="line"> [node <span class="keyword">class</span>]; <span class="comment">//hold and release in queue</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="type">id</span>)key {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span>;</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> _YYLinkedMapNode *node = <span class="built_in">CFDictionaryGetValue</span>(_lru->_dic, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)(key));</span><br><span class="line"> <span class="keyword">if</span> (node) {</span><br><span class="line"> [_lru removeNode:node];</span><br><span class="line"> <span class="keyword">if</span> (_lru->_releaseAsynchronously) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> [node <span class="keyword">class</span>]; <span class="comment">//hold and release in queue</span></span><br><span class="line"> });</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (_lru->_releaseOnMainThread && !pthread_main_np()) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(dispatch_get_main_queue(), ^{</span><br><span class="line"> [node <span class="keyword">class</span>]; <span class="comment">//hold and release in queue</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeAllObjects {</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> [_lru removeAll];</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>需要知道的几个点: - 我们把增删改查统一称为访问数据 -在每次的访问数据开始前,加锁<code>pthread_mutex_lock(&_lock);</code> -每次访问数据(增、改)时,会更新 <code>_time</code> -在每次的访问数据结束后,解锁<code>pthread_mutex_unlock(&_lock);</code></p><p>关于锁的内容,放到后面和 YYDiskCache 一起对比分析。</p><h3 id="缓存清理策略">缓存清理策略</h3><p>我们从 **_YYLinkedMapNode** 的头文件结构可以知道,每份缓存 node都带有2个属性: - **NSUInteger _cost;<strong>:内存消耗 -</strong>NSTimeInterval _time;**:最新访问时间</p><p>从 **_YYLinkedMap** 的头文件结构中可以知道,缓存 map 带有的2个属性:- **NSUInteger _totalCost;<strong>:总缓存消耗 - </strong>NSUInteger_totalCount;**:总缓存数</p><p>再从 <strong>YYMemoryCache</strong> 的头文件中,知道了三个参数: -<strong>NSUInteger countLimit</strong>:缓存数量上限值 -<strong>NSUInteger costLimit</strong>:缓存消耗上限值 -<strong>NSTimeIntervalageLimit</strong>:缓存访问时间距离现在最久允许值</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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)_trimRecursively {</span><br><span class="line"> __<span class="keyword">weak</span> <span class="keyword">typeof</span>(<span class="keyword">self</span>) _<span class="keyword">self</span> = <span class="keyword">self</span>;</span><br><span class="line"> dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * <span class="built_in">NSEC_PER_SEC</span>)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, <span class="number">0</span>), ^{</span><br><span class="line"> __<span class="keyword">strong</span> <span class="keyword">typeof</span>(_<span class="keyword">self</span>) <span class="keyword">self</span> = _<span class="keyword">self</span>;</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">self</span>) <span class="keyword">return</span>;</span><br><span class="line"> [<span class="keyword">self</span> _trimInBackground];</span><br><span class="line"> [<span class="keyword">self</span> _trimRecursively];</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)_trimInBackground {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(_queue, ^{</span><br><span class="line"> [<span class="keyword">self</span> _trimToCost:<span class="keyword">self</span>->_costLimit];</span><br><span class="line"> [<span class="keyword">self</span> _trimToCount:<span class="keyword">self</span>->_countLimit];</span><br><span class="line"> [<span class="keyword">self</span> _trimToAge:<span class="keyword">self</span>->_ageLimit];</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述: - **_trimRecursively<strong>: - 初始化 YYMemoryCache后即开始递归调用 - <code>_autoTrimInterval</code> 自动清理时间默认为 5s- </strong>_trimInBackground**: - 根据 cost、count、age三个维度清理缓存</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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)_trimToCost:(<span class="built_in">NSUInteger</span>)costLimit {</span><br><span class="line"> <span class="type">BOOL</span> finish = <span class="literal">NO</span>;</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (costLimit == <span class="number">0</span>) {</span><br><span class="line"> [_lru removeAll];</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (_lru->_totalCost <= costLimit) {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (finish) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSMutableArray</span> *holder = [<span class="built_in">NSMutableArray</span> new];</span><br><span class="line"> <span class="keyword">while</span> (!finish) {</span><br><span class="line"> <span class="keyword">if</span> (pthread_mutex_trylock(&_lock) == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (_lru->_totalCost > costLimit) {</span><br><span class="line"> _YYLinkedMapNode *node = [_lru removeTailNode];</span><br><span class="line"> <span class="keyword">if</span> (node) [holder addObject:node];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> usleep(<span class="number">10</span> * <span class="number">1000</span>); <span class="comment">//10 ms</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (holder.count) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> [holder count]; <span class="comment">// release in queue</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 class="type">void</span>)_trimToCount:(<span class="built_in">NSUInteger</span>)countLimit {</span><br><span class="line"> <span class="type">BOOL</span> finish = <span class="literal">NO</span>;</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (countLimit == <span class="number">0</span>) {</span><br><span class="line"> [_lru removeAll];</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (_lru->_totalCount <= countLimit) {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (finish) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSMutableArray</span> *holder = [<span class="built_in">NSMutableArray</span> new];</span><br><span class="line"> <span class="keyword">while</span> (!finish) {</span><br><span class="line"> <span class="keyword">if</span> (pthread_mutex_trylock(&_lock) == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (_lru->_totalCount > countLimit) {</span><br><span class="line"> _YYLinkedMapNode *node = [_lru removeTailNode];</span><br><span class="line"> <span class="keyword">if</span> (node) [holder addObject:node];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> usleep(<span class="number">10</span> * <span class="number">1000</span>); <span class="comment">//10 ms</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (holder.count) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> [holder count]; <span class="comment">// release in queue</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 class="type">void</span>)_trimToAge:(<span class="built_in">NSTimeInterval</span>)ageLimit {</span><br><span class="line"> <span class="type">BOOL</span> finish = <span class="literal">NO</span>;</span><br><span class="line"> <span class="built_in">NSTimeInterval</span> now = <span class="built_in">CACurrentMediaTime</span>();</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (ageLimit <= <span class="number">0</span>) {</span><br><span class="line"> [_lru removeAll];</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (!_lru->_tail || (now - _lru->_tail->_time) <= ageLimit) {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (finish) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSMutableArray</span> *holder = [<span class="built_in">NSMutableArray</span> new];</span><br><span class="line"> <span class="keyword">while</span> (!finish) {</span><br><span class="line"> <span class="keyword">if</span> (pthread_mutex_trylock(&_lock) == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) {</span><br><span class="line"> _YYLinkedMapNode *node = [_lru removeTailNode];</span><br><span class="line"> <span class="keyword">if</span> (node) [holder addObject:node];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> usleep(<span class="number">10</span> * <span class="number">1000</span>); <span class="comment">//10 ms</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (holder.count) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> [holder count]; <span class="comment">// release in queue</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程梳理: - <code>_trimToCost:</code>: - 非空判断 - 若有缓存,判断<code>_lru->_totalCost > costLimit</code>,从链表尾部依次取出nodes - 根据 <code>_lru->_releaseOnMainThread</code>判断释放线程,释放 nodes - <code>_trimToCount:</code>: - 非空判断 -若有缓存,判断<code>_lru->_totalCount > countLimit</code>,从链表尾部依次取出nodes - 根据 <code>_lru->_releaseOnMainThread</code>判断释放线程,释放 nodes - <code>_trimToAge:</code>: - 非空判断 -若有缓存,判断<code>_lru->_tail && (now - _lru->_tail->_time) > ageLimit</code>,从尾节点依次向头节点判断,若尾节点的时间不满足,则取出加入nodes;直到第一个满足时间要求的节点为止。(本身链表的排序也是按照 time来的,越旧未访问的内容,越靠近尾部) - 根据<code>_lru->_releaseOnMainThread</code> 判断释放线程,释放 nodes</p><p>自己的思考: - <code>_trimToAge:</code> 中的 age指的是最近访问时间,而不是创建时间。带着这个认知,再看<code>_trimToAge:</code> 的实现,思路就会很清楚了。</p><h3 id="缓存清理策略小结">缓存清理策略小结</h3><p>缓存清理策略的维度: - cost:缓存消耗 - count:缓存数量 -age:缓存的最近访问时间</p><p>缓存清理的方式: - 默认支持自动缓存清理 - 也支持手动清理</p><h2 id="yydiskcache">YYDiskCache</h2><p>简化后的 <code>YYDiskCache.h</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><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">YYDiskCache</span> : <span class="title">NSObject</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="built_in">NSString</span> *name;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">readonly</span>) <span class="built_in">NSString</span> *path;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">readonly</span>) <span class="built_in">NSUInteger</span> inlineThreshold;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="built_in">NSData</span> *(^customArchiveBlock)(<span class="type">id</span> object);</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="type">id</span> (^customUnarchiveBlock)(<span class="built_in">NSData</span> *data);</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">copy</span>) <span class="built_in">NSString</span> *(^customFileNameBlock)(<span class="built_in">NSString</span> *key);</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSUInteger</span> countLimit;</span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSUInteger</span> costLimit;</span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSTimeInterval</span> ageLimit;</span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSUInteger</span> freeDiskSpaceLimit;</span><br><span class="line"><span class="keyword">@property</span> <span class="built_in">NSTimeInterval</span> autoTrimInterval;</span><br><span class="line"><span class="keyword">@property</span> <span class="type">BOOL</span> errorLogsEnabled;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Initializer</span></span><br><span class="line">- (<span class="keyword">instancetype</span>)init UNAVAILABLE_ATTRIBUTE;</span><br><span class="line">+ (<span class="keyword">instancetype</span>)new UNAVAILABLE_ATTRIBUTE;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="keyword">instancetype</span>)initWithPath:(<span class="built_in">NSString</span> *)path;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="keyword">instancetype</span>)initWithPath:(<span class="built_in">NSString</span> *)path</span><br><span class="line"> inlineThreshold:(<span class="built_in">NSUInteger</span>)threshold <span class="built_in">NS_DESIGNATED_INITIALIZER</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Access Methods</span></span><br><span class="line">- (<span class="type">BOOL</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span>(^)(<span class="built_in">NSString</span> *key, <span class="type">BOOL</span> contains))block;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="type">id</span><<span class="built_in">NSCoding</span>>)objectForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)objectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span>(^)(<span class="built_in">NSString</span> *key, <span class="type">id</span><<span class="built_in">NSCoding</span>> _Nullable object))block;</span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="keyword">nullable</span> <span class="type">id</span><<span class="built_in">NSCoding</span>>)object forKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="keyword">nullable</span> <span class="type">id</span><<span class="built_in">NSCoding</span>>)object forKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">void</span>)removeObjectForKey:(<span class="built_in">NSString</span> *)key withBlock:(<span class="type">void</span>(^)(<span class="built_in">NSString</span> *key))block;</span><br><span class="line">- (<span class="type">void</span>)removeAllObjects;</span><br><span class="line">- (<span class="type">void</span>)removeAllObjectsWithBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line">- (<span class="type">void</span>)removeAllObjectsWithProgressBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">int</span> removedCount, <span class="type">int</span> totalCount))progress</span><br><span class="line"> endBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">BOOL</span> error))end;</span><br><span class="line"></span><br><span class="line">- (<span class="built_in">NSInteger</span>)totalCount;</span><br><span class="line">- (<span class="type">void</span>)totalCountWithBlock:(<span class="type">void</span>(^)(<span class="built_in">NSInteger</span> totalCount))block;</span><br><span class="line">- (<span class="built_in">NSInteger</span>)totalCost;</span><br><span class="line">- (<span class="type">void</span>)totalCostWithBlock:(<span class="type">void</span>(^)(<span class="built_in">NSInteger</span> totalCost))block;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Trim</span></span><br><span class="line">- (<span class="type">void</span>)trimToCount:(<span class="built_in">NSUInteger</span>)count;</span><br><span class="line">- (<span class="type">void</span>)trimToCount:(<span class="built_in">NSUInteger</span>)count withBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line">- (<span class="type">void</span>)trimToCost:(<span class="built_in">NSUInteger</span>)cost;</span><br><span class="line">- (<span class="type">void</span>)trimToCost:(<span class="built_in">NSUInteger</span>)cost withBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line">- (<span class="type">void</span>)trimToAge:(<span class="built_in">NSTimeInterval</span>)age;</span><br><span class="line">- (<span class="type">void</span>)trimToAge:(<span class="built_in">NSTimeInterval</span>)age withBlock:(<span class="type">void</span>(^)(<span class="type">void</span>))block;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Extended Data</span></span><br><span class="line">+ (<span class="keyword">nullable</span> <span class="built_in">NSData</span> *)getExtendedDataFromObject:(<span class="type">id</span>)object;</span><br><span class="line">+ (<span class="type">void</span>)setExtendedData:(<span class="keyword">nullable</span> <span class="built_in">NSData</span> *)extendedData toObject:(<span class="type">id</span>)object;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>需要注意的点:</p><ol type="1"><li>YYDiskCache 禁用了 init 和 new 的初始化方法。通过<code>UNAVAILABLE_ATTRIBUTE</code>,将这两种初始化方法设为私有。</li><li><strong>NS_DESIGNATED_INITIALIZER</strong><ul><li>为什么用这个宏?<ul><li>为了告诉调用者需要用这个方法来初始化类对象</li></ul></li><li>使用的注意事项:<ul><li>如果子类指定了新的初始化器,在这个初始化器的内部必须调用父类Designated Initializer,并重写父类 DesignatedInitializer,将其指向子类新的初始化器。例如: <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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</span>)initWithName:(<span class="built_in">NSString</span> *)name <span class="built_in">NS_DESIGNATED_INITIALIZER</span>;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)init {</span><br><span class="line"><span class="keyword">return</span> [<span class="keyword">self</span> initWithName:<span class="string">@""</span>];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithName:(<span class="built_in">NSString</span> *)name {</span><br><span class="line"><span class="keyword">self</span> = [<span class="variable language_">super</span> init];</span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">self</span>) {</span><br><span class="line"><span class="comment">// do something</span></span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">self</span>; </span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul></li></ul></li><li>配合使用<ul><li>配合使用 <code>UNAVAILABLE_ATTRIBUTE</code> 和<code>NS_DESIGNATED_INITIALIZER</code>,目的在于指定初始化方法。格式如下:<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">+ (<span class="keyword">instancetype</span>)new <span class="built_in">NS_UNAVAILABLE</span>;</span><br><span class="line">- (<span class="keyword">instancetype</span>)init <span class="built_in">NS_UNAVAILABLE</span>;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithName:(<span class="built_in">NSString</span> *)name <span class="built_in">NS_DESIGNATED_INITIALIZER</span>;</span><br></pre></td></tr></table></figure></li></ul></li></ol><p>接着对应看下实现文件的源码实现。</p><h3 id="初始化-1">初始化</h3><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><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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</span>)initWithPath:(<span class="built_in">NSString</span> *)path {</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> initWithPath:path inlineThreshold:<span class="number">1024</span> * <span class="number">20</span>]; <span class="comment">// 20KB</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithPath:(<span class="built_in">NSString</span> *)path</span><br><span class="line"> inlineThreshold:(<span class="built_in">NSUInteger</span>)threshold {</span><br><span class="line"> <span class="keyword">self</span> = [<span class="variable language_">super</span> init];</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">self</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);</span><br><span class="line"> <span class="keyword">if</span> (globalCache) <span class="keyword">return</span> globalCache;</span><br><span class="line"> </span><br><span class="line"> YYKVStorageType type;</span><br><span class="line"> <span class="keyword">if</span> (threshold == <span class="number">0</span>) {</span><br><span class="line"> type = YYKVStorageTypeFile;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (threshold == <span class="built_in">NSUIntegerMax</span>) {</span><br><span class="line"> type = YYKVStorageTypeSQLite;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> type = YYKVStorageTypeMixed;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];</span><br><span class="line"> <span class="keyword">if</span> (!kv) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> _kv = kv;</span><br><span class="line"> _path = path;</span><br><span class="line"> _lock = dispatch_semaphore_create(<span class="number">1</span>);</span><br><span class="line"> _queue = dispatch_queue_create(<span class="string">"com.ibireme.cache.disk"</span>, DISPATCH_QUEUE_CONCURRENT);</span><br><span class="line"> _inlineThreshold = threshold;</span><br><span class="line"> _countLimit = <span class="built_in">NSUIntegerMax</span>;</span><br><span class="line"> _costLimit = <span class="built_in">NSUIntegerMax</span>;</span><br><span class="line"> _ageLimit = DBL_MAX;</span><br><span class="line"> _freeDiskSpaceLimit = <span class="number">0</span>;</span><br><span class="line"> _autoTrimInterval = <span class="number">60</span>;</span><br><span class="line"> </span><br><span class="line"> [<span class="keyword">self</span> _trimRecursively];</span><br><span class="line"> _YYDiskCacheSetGlobal(<span class="keyword">self</span>);</span><br><span class="line"> </span><br><span class="line"> [[<span class="built_in">NSNotificationCenter</span> defaultCenter] addObserver:<span class="keyword">self</span> selector:<span class="keyword">@selector</span>(_appWillBeTerminated) name:<span class="built_in">UIApplicationWillTerminateNotification</span> object:<span class="literal">nil</span>];</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中 <code>_kv</code> 是 YYMemoryCache 与 <code>YYKVStorage</code>的一个联系点,所有的增删改查操作,都需要通过 <code>_kv</code>来实现。我们看下 <code>YYKVStorage</code> 内部的是怎么操作数据的。</p><p>另一个是 DiskCache 真实的存储分为文件和 SQLite存储,在初始化时,可以指定存储方式。即设置: <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"><span class="keyword">@property</span> (<span class="keyword">readonly</span>) <span class="built_in">NSUInteger</span> inlineThreshold; <span class="comment">// 默认 1024 * 20,20kb</span></span><br></pre></td></tr></table></figure></p><h3 id="kvstorage">KVStorage</h3><h4 id="yykvstoragetype">YYKVStorageType</h4><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="built_in">NS_ENUM</span>(<span class="built_in">NSUInteger</span>, YYKVStorageType) {</span><br><span class="line"> YYKVStorageTypeFile = <span class="number">0</span>,</span><br><span class="line"> YYKVStorageTypeSQLite = <span class="number">1</span>,</span><br><span class="line"> YYKVStorageTypeMixed = <span class="number">2</span>,</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>梳理: - YYKVStorageTypeFile:在 SQLite 中直接存文件的路径,不在SQLite 中存要存储的值 - YYKVStorageTypeSQLite:只在 SQLite中存要存储的值 -YYKVStorageTypeMixed:根据文件大小来确定要存储的值存放形式(File 或SQLite),默认使用它。 - 选用通过 <code>inlineThreshold</code>确定。</p><h4 id="yykvstorageitem-与-yykvstorage">YYKVStorageItem 与YYKVStorage</h4><p>YYKVStorage 类似于 MemoryCache 里面的 node。DiskCache每一项被封装成了 YYKVStorageItem 实例,再通过 YYKVStorage 来管理YYKVStorageItem。</p><p>YYKVStorageItem 结构如下:</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="class"><span class="keyword">@interface</span> <span class="title">YYKVStorageItem</span> : <span class="title">NSObject</span></span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) <span class="built_in">NSString</span> *key; <span class="comment">///< key</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) <span class="built_in">NSData</span> *value; <span class="comment">///< value</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) <span class="built_in">NSString</span> *filename; <span class="comment">///< filename (nil if inline)</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="type">int</span> size; <span class="comment">///< value's size in bytes</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="type">int</span> modTime; <span class="comment">///< 修改时间戳</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="type">int</span> accessTime; <span class="comment">///< 最后访问时间戳</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nullable</span>, <span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) <span class="built_in">NSData</span> *extendedData; <span class="comment">///< extended data (nil if no extended data)</span></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>KVStorage 的接口:</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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Save Items</span></span><br><span class="line">- (<span class="type">BOOL</span>)saveItem:(YYKVStorageItem *)item;</span><br><span class="line">- (<span class="type">BOOL</span>)saveItemWithKey:(<span class="built_in">NSString</span> *)key value:(<span class="built_in">NSData</span> *)value;</span><br><span class="line">- (<span class="type">BOOL</span>)saveItemWithKey:(<span class="built_in">NSString</span> *)key</span><br><span class="line"> value:(<span class="built_in">NSData</span> *)value</span><br><span class="line"> filename:(<span class="keyword">nullable</span> <span class="built_in">NSString</span> *)filename</span><br><span class="line"> extendedData:(<span class="keyword">nullable</span> <span class="built_in">NSData</span> *)extendedData;</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Remove Items</span></span><br><span class="line">- (<span class="type">BOOL</span>)removeItemForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">BOOL</span>)removeItemForKeys:(<span class="built_in">NSArray</span><<span class="built_in">NSString</span> *> *)keys;</span><br><span class="line">- (<span class="type">BOOL</span>)removeItemsLargerThanSize:(<span class="type">int</span>)size;</span><br><span class="line">- (<span class="type">BOOL</span>)removeItemsEarlierThanTime:(<span class="type">int</span>)time;</span><br><span class="line">- (<span class="type">BOOL</span>)removeItemsToFitSize:(<span class="type">int</span>)maxSize;</span><br><span class="line">- (<span class="type">BOOL</span>)removeItemsToFitCount:(<span class="type">int</span>)maxCount;</span><br><span class="line">- (<span class="type">BOOL</span>)removeAllItems;</span><br><span class="line">- (<span class="type">void</span>)removeAllItemsWithProgressBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">int</span> removedCount, <span class="type">int</span> totalCount))progress</span><br><span class="line"> endBlock:(<span class="keyword">nullable</span> <span class="type">void</span>(^)(<span class="type">BOOL</span> error))end;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">pragma</span> mark - Get Items</span></span><br><span class="line">- (<span class="keyword">nullable</span> YYKVStorageItem *)getItemForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="keyword">nullable</span> YYKVStorageItem *)getItemInfoForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="built_in">NSData</span> *)getItemValueForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="built_in">NSArray</span><YYKVStorageItem *> *)getItemForKeys:(<span class="built_in">NSArray</span><<span class="built_in">NSString</span> *> *)keys;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="built_in">NSArray</span><YYKVStorageItem *> *)getItemInfoForKeys:(<span class="built_in">NSArray</span><<span class="built_in">NSString</span> *> *)keys;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="built_in">NSDictionary</span><<span class="built_in">NSString</span> *, <span class="built_in">NSData</span> *> *)getItemValueForKeys:(<span class="built_in">NSArray</span><<span class="built_in">NSString</span> *> *)keys;</span><br></pre></td></tr></table></figure><h4 id="saveupdate-的过程">Save/Update 的过程</h4><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><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">BOOL</span>)saveItem:(YYKVStorageItem *)item {</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)saveItemWithKey:(<span class="built_in">NSString</span> *)key value:(<span class="built_in">NSData</span> *)value {</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> saveItemWithKey:key value:value filename:<span class="literal">nil</span> extendedData:<span class="literal">nil</span>];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)saveItemWithKey:(<span class="built_in">NSString</span> *)key value:(<span class="built_in">NSData</span> *)value filename:(<span class="built_in">NSString</span> *)filename extendedData:(<span class="built_in">NSData</span> *)extendedData {</span><br><span class="line"> <span class="keyword">if</span> (key.length == <span class="number">0</span> || value.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> <span class="keyword">if</span> (_type == YYKVStorageTypeFile && filename.length == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (filename.length) {</span><br><span class="line"> <span class="keyword">if</span> (![<span class="keyword">self</span> _fileWriteWithName:filename data:value]) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (![<span class="keyword">self</span> _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {</span><br><span class="line"> [<span class="keyword">self</span> _fileDeleteWithName:filename];</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (_type != YYKVStorageTypeSQLite) {</span><br><span class="line"> <span class="built_in">NSString</span> *filename = [<span class="keyword">self</span> _dbGetFilenameWithKey:key];</span><br><span class="line"> <span class="keyword">if</span> (filename) {</span><br><span class="line"> [<span class="keyword">self</span> _fileDeleteWithName:filename];</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> _dbSaveWithKey:key value:value fileName:<span class="literal">nil</span> extendedData:extendedData];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>过程描述: - 校验 key value - type 为 YYKVStorageTypeFile 时,校验filename 是否存在 - 判断是否有 filename(**_kv.type !=YYKVStorageTypeSQLite** && **value.length >_inlineThreshold**时创建 filename) - 疑问:如果 YYKVStorageTypeFile的话,依旧会按照上面进行文件大小判断,再创建文件名 - 若有filename,value 以 filename写入文件;文件写入成功后,将相关数据写入数据库 db - 若没有 filename,且type 不为 YYKVStorageTypeSQLite - 从 db 查询filename,若有,清空对应文件 - 将相关数据写入 db,只是不写入文件</p><h4 id="remove-的过程">Remove 的过程</h4><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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="type">BOOL</span>)removeItemForKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> (key.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> <span class="keyword">switch</span> (_type) {</span><br><span class="line"> <span class="keyword">case</span> YYKVStorageTypeSQLite: {</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> _dbDeleteItemWithKey:key];</span><br><span class="line"> } <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> YYKVStorageTypeFile:</span><br><span class="line"> <span class="keyword">case</span> YYKVStorageTypeMixed: {</span><br><span class="line"> <span class="built_in">NSString</span> *filename = [<span class="keyword">self</span> _dbGetFilenameWithKey:key];</span><br><span class="line"> <span class="keyword">if</span> (filename) {</span><br><span class="line"> [<span class="keyword">self</span> _fileDeleteWithName:filename];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> _dbDeleteItemWithKey:key];</span><br><span class="line"> } <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>: <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)removeItemForKeys:(<span class="built_in">NSArray</span> *)keys {</span><br><span class="line"> <span class="keyword">if</span> (keys.count == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> <span class="keyword">switch</span> (_type) {</span><br><span class="line"> <span class="keyword">case</span> YYKVStorageTypeSQLite: {</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> _dbDeleteItemWithKeys:keys];</span><br><span class="line"> } <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> YYKVStorageTypeFile:</span><br><span class="line"> <span class="keyword">case</span> YYKVStorageTypeMixed: {</span><br><span class="line"> <span class="built_in">NSArray</span> *filenames = [<span class="keyword">self</span> _dbGetFilenameWithKeys:keys];</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">NSString</span> *filename <span class="keyword">in</span> filenames) {</span><br><span class="line"> [<span class="keyword">self</span> _fileDeleteWithName:filename];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> [<span class="keyword">self</span> _dbDeleteItemWithKeys:keys];</span><br><span class="line"> } <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>: <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)removeAllItems {</span><br><span class="line"> <span class="keyword">if</span> (![<span class="keyword">self</span> _dbClose]) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> [<span class="keyword">self</span> _reset];</span><br><span class="line"> <span class="keyword">if</span> (![<span class="keyword">self</span> _dbOpen]) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> <span class="keyword">if</span> (![<span class="keyword">self</span> _dbInitialize]) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)removeAllItemsWithProgressBlock:(<span class="type">void</span>(^)(<span class="type">int</span> removedCount, <span class="type">int</span> totalCount))progress</span><br><span class="line"> endBlock:(<span class="type">void</span>(^)(<span class="type">BOOL</span> error))end {</span><br><span class="line"> </span><br><span class="line"> <span class="type">int</span> total = [<span class="keyword">self</span> _dbGetTotalItemCount];</span><br><span class="line"> <span class="keyword">if</span> (total <= <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (end) end(total < <span class="number">0</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="type">int</span> left = total;</span><br><span class="line"> <span class="type">int</span> perCount = <span class="number">32</span>;</span><br><span class="line"> <span class="built_in">NSArray</span> *items = <span class="literal">nil</span>;</span><br><span class="line"> <span class="type">BOOL</span> suc = <span class="literal">NO</span>;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> items = [<span class="keyword">self</span> _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];</span><br><span class="line"> <span class="keyword">for</span> (YYKVStorageItem *item <span class="keyword">in</span> items) {</span><br><span class="line"> <span class="keyword">if</span> (left > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (item.filename) {</span><br><span class="line"> [<span class="keyword">self</span> _fileDeleteWithName:item.filename];</span><br><span class="line"> }</span><br><span class="line"> suc = [<span class="keyword">self</span> _dbDeleteItemWithKey:item.key];</span><br><span class="line"> left--;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!suc) <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (progress) progress(total - left, total);</span><br><span class="line"> } <span class="keyword">while</span> (left > <span class="number">0</span> && items.count > <span class="number">0</span> && suc);</span><br><span class="line"> <span class="keyword">if</span> (suc) [<span class="keyword">self</span> _dbCheckpoint];</span><br><span class="line"> <span class="keyword">if</span> (end) end(!suc);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>部分 Remove 的过程,以<code>removeAllItemsWithProgressBlock:endBlock:</code> 为例: -若无缓存,结束,end block - 若有缓存,每次从 db 最多取出 32个缓存项,遍历移除文件和 db 内容 - 每删除 32 个缓存,回调一次progressBlock(progress = (total - left)/total) - 删除完成 endBlock</p><h4 id="get-的过程">Get 的过程</h4><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></pre></td><td class="code"><pre><span class="line">- (YYKVStorageItem *)getItemForKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> (key.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> YYKVStorageItem *item = [<span class="keyword">self</span> _dbGetItemWithKey:key excludeInlineData:<span class="literal">NO</span>];</span><br><span class="line"> <span class="keyword">if</span> (item) {</span><br><span class="line"> [<span class="keyword">self</span> _dbUpdateAccessTimeWithKey:key];</span><br><span class="line"> <span class="keyword">if</span> (item.filename) {</span><br><span class="line"> item.value = [<span class="keyword">self</span> _fileReadWithName:item.filename];</span><br><span class="line"> <span class="keyword">if</span> (!item.value) {</span><br><span class="line"> [<span class="keyword">self</span> _dbDeleteItemWithKey:key];</span><br><span class="line"> item = <span class="literal">nil</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> item;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>以 <code>getItemForKey:</code> 为例: - 校验 key - 从 db 中根据 key取出 YYKVStorageItem 格式的数据 - 更新 db 中这条数据的时间 - 如果有filename,以 filename 从文件中取出 value 并返回给 item.value - 若item.value 为空,从 db 中移除这条无效缓存 - 返回 item</p><h3 id="yydiskcache-的对应处理">YYDiskCache 的对应处理</h3><p>了解了 YYKVStorage 的存取实现逻辑后,回过头再来看下上层 YYDiskCache是如何实现对应方法的。</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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="type">BOOL</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> Lock();</span><br><span class="line"> <span class="type">BOOL</span> contains = [_kv itemExistsForKey:key];</span><br><span class="line"> Unlock();</span><br><span class="line"> <span class="keyword">return</span> contains;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">id</span><<span class="built_in">NSCoding</span>>)objectForKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> Lock();</span><br><span class="line"> YYKVStorageItem *item = [_kv getItemForKey:key];</span><br><span class="line"> Unlock();</span><br><span class="line"> <span class="keyword">if</span> (!item.value) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="type">id</span> object = <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">if</span> (_customUnarchiveBlock) {</span><br><span class="line"> object = _customUnarchiveBlock(item.value);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">@try</span> {</span><br><span class="line"> object = [<span class="built_in">NSKeyedUnarchiver</span> unarchiveObjectWithData:item.value];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">@catch</span> (<span class="built_in">NSException</span> *exception) {</span><br><span class="line"> <span class="comment">// nothing to do...</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (object && item.extendedData) {</span><br><span class="line"> [YYDiskCache setExtendedData:item.extendedData toObject:object];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> object;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setObject:(<span class="type">id</span><<span class="built_in">NSCoding</span>>)object forKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">if</span> (!object) {</span><br><span class="line"> [<span class="keyword">self</span> removeObjectForKey:key];</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSData</span> *extendedData = [YYDiskCache getExtendedDataFromObject:object];</span><br><span class="line"> <span class="built_in">NSData</span> *value = <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">if</span> (_customArchiveBlock) {</span><br><span class="line"> value = _customArchiveBlock(object);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">@try</span> {</span><br><span class="line"> value = [<span class="built_in">NSKeyedArchiver</span> archivedDataWithRootObject:object];</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">@catch</span> (<span class="built_in">NSException</span> *exception) {</span><br><span class="line"> <span class="comment">// nothing to do...</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!value) <span class="keyword">return</span>;</span><br><span class="line"> <span class="built_in">NSString</span> *filename = <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">if</span> (_kv.type != YYKVStorageTypeSQLite) {</span><br><span class="line"> <span class="keyword">if</span> (value.length > _inlineThreshold) {</span><br><span class="line"> filename = [<span class="keyword">self</span> _filenameForKey:key];</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> Lock();</span><br><span class="line"> [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];</span><br><span class="line"> Unlock();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>列出了<code>-containsObjectForKey:</code>、<code>objectForKey:</code>、<code>setObject:forKey:</code>方法对应的源码。在每次的 <code>_kv</code>操作时,都会进行加锁和解锁。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">define</span> Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> Unlock() dispatch_semaphore_signal(self->_lock)</span></span><br></pre></td></tr></table></figure><p>后面结合 YYMemoryCache,一起看下两种锁的区别。</p><h2 id="nsmaptable">NSMapTable</h2><p>NSMapTable 是类似于字典的集合,但具有更广泛的可用内存语义。NSMapTable是 iOS6 之后引入的类,它基于 NSDictionary 建模,但是具有以下差异: -key-value 可以选择弱持有,以便于在回收其中一个对象时删除对应条目。 -它可以包含任意指针(其内容不被约束为对象)。 - 我们可以将 NSMapTable实例配置为对任意指针进行操作,而不仅仅是对象。</p><p>官方文档:<ahref="https://developer.apple.com/documentation/foundation/nsmaptable?language=objc">NSMapTable官方文档</a></p><p>YYDiskCache 内部是基于一个单例 NSMapTable 管理的。</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/// weak reference for all instances</span></span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSMapTable</span> *_globalInstances; <span class="comment">// _globalInstances 管理所有 YYDiskCache 实例</span></span><br><span class="line"><span class="keyword">static</span> dispatch_semaphore_t _globalInstancesLock; <span class="comment">// 使用 dispatch_semaphore 保障 NSMapTable 线程安全</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="type">void</span> _YYDiskCacheInitGlobal() {</span><br><span class="line"> <span class="keyword">static</span> <span class="built_in">dispatch_once_t</span> onceToken;</span><br><span class="line"> <span class="built_in">dispatch_once</span>(&onceToken, ^{</span><br><span class="line"> _globalInstancesLock = dispatch_semaphore_create(<span class="number">1</span>);</span><br><span class="line"> _globalInstances = [[<span class="built_in">NSMapTable</span> alloc] initWithKeyOptions:<span class="built_in">NSPointerFunctionsStrongMemory</span> valueOptions:<span class="built_in">NSPointerFunctionsWeakMemory</span> capacity:<span class="number">0</span>];</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> YYDiskCache *_YYDiskCacheGetGlobal(<span class="built_in">NSString</span> *path) {</span><br><span class="line"> <span class="keyword">if</span> (path.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> _YYDiskCacheInitGlobal();</span><br><span class="line"> dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);</span><br><span class="line"> <span class="type">id</span> cache = [_globalInstances objectForKey:path];</span><br><span class="line"> dispatch_semaphore_signal(_globalInstancesLock);</span><br><span class="line"> <span class="keyword">return</span> cache;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="type">void</span> _YYDiskCacheSetGlobal(YYDiskCache *cache) {</span><br><span class="line"> <span class="keyword">if</span> (cache.path.length == <span class="number">0</span>) <span class="keyword">return</span>;</span><br><span class="line"> _YYDiskCacheInitGlobal();</span><br><span class="line"> dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);</span><br><span class="line"> [_globalInstances setObject:cache forKey:cache.path];</span><br><span class="line"> dispatch_semaphore_signal(_globalInstancesLock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>每当初始化 YYDiskCache 时,先到 NSMapTable 中获取对应 path 的YYDiskCache 实例。如果获取不到,会重新初始化一个 YYDiskCache实例,并且将其引用在 NSMapTable 中,这样做也会提升不少性能。</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</span>)initWithPath:(<span class="built_in">NSString</span> *)path</span><br><span class="line"> inlineThreshold:(<span class="built_in">NSUInteger</span>)threshold {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 先从 NSMapTable 单例中通过 path 获取 YYDiskCache 实例</span></span><br><span class="line"><span class="comment">// 如果获取到,直接返回该实例</span></span><br><span class="line"> YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);</span><br><span class="line"> <span class="keyword">if</span> (globalCache) <span class="keyword">return</span> globalCache;</span><br><span class="line"> </span><br><span class="line"> YYKVStorageType type;</span><br><span class="line"> <span class="keyword">if</span> (threshold == <span class="number">0</span>) {</span><br><span class="line"> type = YYKVStorageTypeFile;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (threshold == <span class="built_in">NSUIntegerMax</span>) {</span><br><span class="line"> type = YYKVStorageTypeSQLite;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> type = YYKVStorageTypeMixed;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 弱没有获取到,初始化一个新的 YYDiskCache 实例</span></span><br><span class="line"> YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];</span><br><span class="line"> <span class="keyword">if</span> (!kv) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> _kv = kv;</span><br><span class="line"> _path = path;</span><br><span class="line"> _lock = dispatch_semaphore_create(<span class="number">1</span>);</span><br><span class="line"> _queue = dispatch_queue_create(<span class="string">"com.ibireme.cache.disk"</span>, DISPATCH_QUEUE_CONCURRENT);</span><br><span class="line"> _inlineThreshold = threshold;</span><br><span class="line"> _countLimit = <span class="built_in">NSUIntegerMax</span>;</span><br><span class="line"> _costLimit = <span class="built_in">NSUIntegerMax</span>;</span><br><span class="line"> _ageLimit = DBL_MAX;</span><br><span class="line"> _freeDiskSpaceLimit = <span class="number">0</span>;</span><br><span class="line"> _autoTrimInterval = <span class="number">60</span>;</span><br><span class="line"> </span><br><span class="line"> [<span class="keyword">self</span> _trimRecursively];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 向 NSMapTable 单例注册新生成的 YYDiskCache 实例</span></span><br><span class="line"> _YYDiskCacheSetGlobal(<span class="keyword">self</span>);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="如何保证的线程安全">如何保证的线程安全</h2><p>通过前面的代码分析,我们已经知道了 YYCache在增删改查的过程中,会进行加解锁操作。锁的目的也是为了保证线程的安全。分别看下MemoryCache 和 DiskCache 分别是怎么实现线程安全的。</p><h3 id="yymemorycache-线程安全处理">YYMemoryCache 线程安全处理</h3><p>YYMemoryCache 使用了 <code>pthread_mutex</code>线程锁来确保线程安全。ibireme 使用 pthread_mutex 的缘由:<ahref="https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/">不再安全的OSSpinLock</a>。简单言之,处于安全考虑选用了现在的<code>pthread_mutex</code>。</p><p><code>pthread_mutex</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 申明一个互斥锁</span></span><br><span class="line">pthread_mutex_t _lock;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 初始化</span></span><br><span class="line">pthread_mutex_init(&_lock,<span class="literal">NULL</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用 _lock 之前一定要初始化,否则不生效</span></span><br><span class="line"><span class="comment">// 锁定互斥锁</span></span><br><span class="line">pthread_mutex_lock(&_lock);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 解除锁定互斥锁</span></span><br><span class="line">pthread_mutex_unlock(&_lock);</span><br><span class="line"></span><br><span class="line"><span class="comment">// pthread_mutex_lock() 的非阻塞版本</span></span><br><span class="line"><span class="comment">// 如果 _lock 所引用的互斥对象被任何线程(包括当前线程)锁定,将立刻返回该调用</span></span><br><span class="line"><span class="comment">// 否则该互斥锁将被锁定,调用线程为当前线程</span></span><br><span class="line"><span class="comment">// pthread_mutex_lock() 成功锁定后会返回 0,返回`其他任何值`表示`出错`</span></span><br><span class="line">pthread_mutex_trylock(&_lock);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 销毁互斥锁</span></span><br><span class="line">pthread_mutex_destroy(&_lock);</span><br></pre></td></tr></table></figure><p><code>pthread_mutex</code> 表示互斥锁。互斥锁在申请锁时,调用了<code>pthread_mutex_lock</code> 方法,会导致线程休眠。</p><p>YYMemoryCache 中:</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</span>)init {</span><br><span class="line"> <span class="keyword">self</span> = <span class="variable language_">super</span>.init;</span><br><span class="line"> pthread_mutex_init(&_lock, <span class="literal">NULL</span>);</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)dealloc {</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"> pthread_mutex_destroy(&_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)_trimToCount:(<span class="built_in">NSUInteger</span>)countLimit {</span><br><span class="line"> <span class="type">BOOL</span> finish = <span class="literal">NO</span>;</span><br><span class="line"> pthread_mutex_lock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (countLimit == <span class="number">0</span>) {</span><br><span class="line"> [_lru removeAll];</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (_lru->_totalCount <= countLimit) {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> <span class="keyword">if</span> (finish) <span class="keyword">return</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSMutableArray</span> *holder = [<span class="built_in">NSMutableArray</span> new];</span><br><span class="line"> <span class="keyword">while</span> (!finish) {</span><br><span class="line"> <span class="keyword">if</span> (pthread_mutex_trylock(&_lock) == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (_lru->_totalCount > countLimit) {</span><br><span class="line"> _YYLinkedMapNode *node = [_lru removeTailNode];</span><br><span class="line"> <span class="keyword">if</span> (node) [holder addObject:node];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> finish = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> pthread_mutex_unlock(&_lock);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> usleep(<span class="number">10</span> * <span class="number">1000</span>); <span class="comment">//10 ms</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (holder.count) {</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();</span><br><span class="line"> <span class="built_in">dispatch_async</span>(queue, ^{</span><br><span class="line"> [holder count]; <span class="comment">// release in queue</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们看到 YYCache 中每次对 <code>_lru</code>操作时都会进行锁定和解除。也用到了<code>pthread_mutex_trylock(&_lock)</code>。</p><h3 id="yydiskcache-线程安全处理">YYDiskCache 线程安全处理</h3><p>YYDiskCache使用了信号量(<strong>dispatch_semaphore</strong>)来保证线程的安全。DispatchSemaphore 是持有计数的信号,计数为 0 时等待,计数 >= 1 时放行。</p><p><code>dispatch_semaphore</code> 的基本用法: -<code>dispatch_semaphore_create</code> 可以生成信号量,参数 value是信号量计数的初始值; - <code>dispatch_semaphore_wait</code>会让信号量值 -1,当信号量值为 0 时进入等待(直到超时),否则正常执行; -<code>dispatch_semaphore_signal</code> 会让信号量值 +1,如果有通过<code>dispatch_semaphore_wait</code> 函数等待 Dispatch Semaphore的计数值增加的线程,会由系统唤醒最先等待的线程执行。</p><p>YYDiskCache 中:</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><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">YYDiskCache</span> </span>{</span><br><span class="line"> YYKVStorage *_kv;</span><br><span class="line"> dispatch_semaphore_t _lock;</span><br><span class="line"> <span class="built_in">dispatch_queue_t</span> _queue;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithPath:(<span class="built_in">NSString</span> *)path</span><br><span class="line"> inlineThreshold:(<span class="built_in">NSUInteger</span>)threshold {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> _lock = dispatch_semaphore_create(<span class="number">1</span>);</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 通过宏来替换加锁解锁代码</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> Unlock() dispatch_semaphore_signal(self->_lock)</span></span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)containsObjectForKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> (!key) <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> Lock();</span><br><span class="line"> <span class="type">BOOL</span> contains = [_kv itemExistsForKey:key];</span><br><span class="line"> Unlock();</span><br><span class="line"> <span class="keyword">return</span> contains;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看到 YYDiskCache 中每次对 <code>_kv</code>操作时,都会进行加解锁操作。</p><h3id="为什么线程安全的处理方式不同">为什么线程安全的处理方式不同?</h3><p>先了解一个概念,<strong>信号量</strong>和<strong>互斥锁</strong>的区别:- 互斥锁用户用于线程的互斥,信号量用于线程的同步。 -同步和互斥是针对线程的。同步允许多线程按序访问;互斥只允许单个线程访问。-互斥:指某个资源同时只允许一个访问者对其访问,具有排他性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。-同步:指在互斥的基础上,通过其他机制实现访问者对资源的有序访问。即信号量可以让多个线程有序访问某个资源。</p><p>YYDiskCache 在写入较大缓存时,会有较长的等待时间,而dispatch_semaphore 是不消耗 CPU 资源的,所以选用信号量。</p><h2 id="总结">总结</h2><ul><li>YYCache 框架结构<ul><li>YYCache</li><li>YYMemoryCache<ul><li><code>_YYLinkedMap</code></li><li><code>_YYLinkedMapNode</code></li></ul></li><li>YYDiskCache<ul><li>YYKVStorage</li><li>YYKVStorageItem</li></ul></li></ul></li><li>YYCache<ul><li>增删改查方法</li><li>先操作 <code>_memoryCache</code>,再操作<code>_diskCache</code></li><li>回调以 <code>_diskCache</code> 为准</li><li>这一层没有真实的做数据处理</li></ul></li><li>LRU<ul><li>Least Recently Used 最近最少使用</li><li>缓存命中率</li><li>MemoryCache LRU 的实现<ul><li>通过双向链表类<code>_YYLinkedMap</code>,保存和管理链表节点<code>_YYLinkedMapNode</code>。</li><li><code>_YYLinkedMapNode</code><ul><li><code>_prev</code></li><li><code>_next</code></li><li><code>value</code></li></ul></li><li><code>_YYLinedMap</code><ul><li><code>_dic</code>:存放链表节点</li><li><code>_head</code>:头节点</li><li><code>_tail</code>:尾节点</li></ul></li><li>每次数据访问时,将被访问节点调整到 head 位置</li></ul></li></ul></li><li>YYMemoryCache<ul><li>初始化<ul><li>pthread_mutex 互斥锁</li><li>三个缓存管理维度(count、cost、age)</li><li>自动清理时间 5s</li><li>默认内存警告、进入后台时,清空 memoryCache</li></ul></li><li>增删改查<ul><li>每次数据访问</li><li>pthread_mutex_lock 加锁</li><li>访问修改数据,更新相关属性(time 等)</li><li>pthread_mutex_unlock 解锁</li></ul></li><li>缓存清理<ul><li>手动或自动</li><li>三个维度(count、cost、age)</li><li>从双向链表尾部开始清理</li></ul></li></ul></li><li>YYDiskCache<ul><li>初始化<ul><li>内有一单例 NSMapTable</li><li>先通过 path 从 NSMapTable中查是否有对应实例,有则用,无责新建并存</li><li><code>_kv</code> 实例,类似 MemoryCache 的 <code>_dic</code></li><li>dispatch_semaphore 信号量,自旋锁</li><li>三个缓存维度(count、cost、age)</li><li>自动清理</li></ul></li><li>增删改查<ul><li>YYKVStorageType<ul><li>File</li><li>SQLite</li><li>Mixed</li></ul></li><li>YYKVStorage 管理 YYKVStorageItem</li><li>增删改查过程中既要修改文件、又要修改 SQLite 的数据</li><li>dispatch_semaphore_wait 加锁</li><li>处理数据</li><li>dispatch_semaphore_signal 解锁</li></ul></li><li>NSMapTable<ul><li>NSHashTable 更广泛意义的 NSSet、NSMutableSet</li><li>NSMapTable 更广泛意义的 NSDictionary、NSMutableDictionary</li><li>可以存储指针,内容不被约束为对象</li></ul></li><li>缓存清理<ul><li>类似memorycache</li></ul></li></ul></li><li>线程安全处理<ul><li>pthread_mutex</li><li>dispatch_semaphore</li></ul></li></ul>]]></content>
<summary type="html"><p>本篇是对 <a href="https://github.com/ibireme/YYCache">YYCache</a>
源码阅读过程中的梳理。YYCache 是一个线程安全的高性能
<strong>Key-Value</strong> 缓存框架。代码质量很高,值得拿来学习。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="源码阅读" scheme="http://example.com/categories/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="源码阅读" scheme="http://example.com/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
</entry>
<entry>
<title>YYImage 源码梳理</title>
<link href="http://example.com/2017/09/10/2017-09-10-yyimage/"/>
<id>http://example.com/2017/09/10/2017-09-10-yyimage/</id>
<published>2017-09-09T16:00:00.000Z</published>
<updated>2017-09-09T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>图片相关的处理,在移动应用中属于比较重要的一个角色。本篇主要是对 <ahref="https://github.com/ibireme/YYImage">YYImage</a>的源码实现做一个梳理,内容结构:</p><ul><li>UIImage 相关的处理</li><li>YYImage 框架结构</li><li>YYImage</li><li>YYFrameImage</li><li>YYSpriteSheetImage</li><li>YYAnimatedImage</li><li>YYAnimatedImageView</li><li>YYImageCoder</li></ul><span id="more"></span><h2 id="uiimage-相关处理">UIImage 相关处理</h2><p>一张图片从磁盘到显示到屏幕的大致过程为: - 从磁盘加载图片信息 -解码图片二进制数据为位图 - 通过 CoreAnimation框架处理最终绘制到屏幕上</p><p>这一过程中耗时较大的操作是<strong>图片解码</strong>的过程。</p><h3 id="图片的加载和解压">图片的加载和解压</h3><p>日常开发中,我们常会通过 UIImage 的 <code>imageNamed:</code> 或<code>imageWithData:</code> 方法从内存中加载图片生成 UIImage对象。在这一过程中,图片不会进行解压,当 RunLoop准备处理图片显示的处理(CATransaction)时,才进行解压,而这个解压过程是在主线程中执行的,所以大图解压也是导致卡顿的一个重要因素。</p><h4 id="imagenamed">imageNamed:</h4><p>在我们使用 <code>imageNamed:</code> 加载图片信息生成 UIImage对象的同时,图片的信息会被缓存起来。所以在使用该方法第一次加载某图片时,会消耗较多时间,而之后再加载该图会快很多。</p><p>到这里图片还未进行解压操作,当某图片要绘制到屏幕前,会进行解压操作,系统也会将解压信息缓存到内存中。这些缓存是全局的,也就是说,即使当前UIImage对象被释放也不会影响该图片的缓存,只有当应用收到内存警告会应用进入后台才会进行缓存处理。具体的缓存清理策略是由系统决定的。</p><h4 id="imagewithdata">imageWithData:</h4><p>在我们使用 <code>imageWithData:</code> 加载图片生成 UIImage对象时,从加载图片信息到解压图片进行屏幕上绘制的这一过程中。都不会将图片信息及解压信息以全局的形式缓存,在该UIImage 对象释放时,相关的图片信息及解压信息都会被销毁。</p><h4 id="对比">对比</h4><ul><li><strong><code>imageNamed:</code></strong>会产生全局的内存占用,但第二次使用同一张图时,可以直接使用缓存数据,性能更好。</li><li><strong><code>imageWIthData:</code></strong>可以理解为即用即生成,即使是同一张图的重复使用,每次都需要走一遍加载和解压的过程。</li></ul><p>对比来看,<strong><code>imageNamed:</code></strong>方法适合小而高频次使用的图片;<strong><code>imageWithData:</code></strong>方法适合大而低频次的图片。</p><p>基于前面的了解可以对图片的加载和解压过程做一些优化。</p><ul><li>加载的优化:可以在异步线程中通过 <ahref="https://developer.apple.com/documentation/uikit/uiimage/1624123-imagewithcontentsoffile?language=occ">imageWithContentsOfFile:</a>方法加载图片。</li><li>解压的优化:系统默认将解压的耗时操作放到了主线程中执行,比较通用的做法是在异步线程中通过<ahref="https://developer.apple.com/documentation/coregraphics/1455939-cgbitmapcontextcreate">CGBitmapContextCreate</a>方法主动将二进制图片数据解压成位图数据,</li></ul><h4 id="大图处理">大图处理</h4><p>主流 iOS 设备最高支持 4096x4096的纹理尺寸,若图片像素过大,在显示时会额外消耗资源来处理图片。且常会的图片加载过程会占用过多的内存。</p><p>大图加载的出发点是,这张图最终的展示效果是需要怎样的?如果大图展示能够满足展示窗口的展示效果,一定程度上我们可以进行大图的压缩。</p><p>在我们缩小图片是,会按取平均值的方式把多个像素点变为一个像素点,但超大图处理的过程中,老设备很可能会出现OOM的情况。原因是常规图片绘制的过程中,会先解码图片,再生成原始分辨率大小的bitmap,这会很消耗内存。</p><p>解决办法是使用更底层的 <strong><code>ImageIO</code></strong>接口,它可以直接读取图像大小和元数据信息,不带来额外的内存开销。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/5ac66d5b10a17fa72db3.png/EnCVrcjPLiM5Hwg.png" /></p><p>另一种解决办法是,WWDC2018 苹果工程师建议开发者使用<strong><code>UIGraphicsImageRenderer</code></strong> 来代替<strong><code>UIGraphicsBeginImageContextWithOptions</code></strong>。该方法从iOS10 引入,在 iOS12 中自动选择最佳的图片格式,可以减少很多的内存。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/ea37043b9c82f5b187c8.png/m8KDjyH7We2CvQ5.png" /></p><h2 id="yyimage-框架结构">YYImage 框架结构</h2><p>前面铺垫了解了一些 UIImage 相关的处理过程。再来看下 YYImage的框架结构。</p><p>YYImage 特性(来自 README):</p><ul><li>支持以下类型动画图像的播放/编码/解码: WebP, APNG, GIF。</li><li>支持以下类型静态图像的显示/编码/解码: WebP, PNG, GIF, JPEG, JP2,TIFF, BMP, ICO, ICNS。</li><li>支持以下类型图片的渐进式/逐行扫描/隔行扫描解码: PNG, GIF, JPEG,BMP。</li><li>支持多张图片构成的帧动画播放,支持单张图片的 sprite sheet动画。</li><li>高效的动态内存缓存管理,以保证高性能低内存的动画播放。</li><li>完全兼容 UIImage 和 UIImageView,使用方便。</li><li>保留可扩展的接口,以支持自定义动画。</li><li>每个类和方法都有完善的文档注释。</li></ul><p>YYImage 的目录结构:</p><ul><li>YYImage</li><li>YYFrameImage</li><li>YYSpriteSheetImage</li><li>YYAnimatedImageView</li><li>YYImageCoder</li></ul><p>主要分为三个层级,分别为: - 继承自 UIImage 的图像层 - 继承自UIImageView 的视图层 - 编解码层</p><p>其中 YYImage、YYFrameImage、YYSpriteSheetImage 都继承自UIImage。YYAnimatedImageView 继承自UIImageView,用于处理框架自定义的图片类。YYImageCoder负责编码和解码。</p><h2 id="yyimage">YYImage</h2><p>YYImage 是 UIImage 的子类,支持 png、jpeg、jpg、gif、webp、apng格式图片的解码,提供了类似 UIImage 的初始化方法。其中为了避免<strong><code>imageName:</code></strong>方法产生全局缓存,重载了该方法。源码如下:</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><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></pre></td><td class="code"><pre><span class="line">+ (YYImage *)imageNamed:(<span class="built_in">NSString</span> *)name {</span><br><span class="line"> <span class="keyword">if</span> (name.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">if</span> ([name hasSuffix:<span class="string">@"/"</span>]) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSString</span> *res = name.stringByDeletingPathExtension;</span><br><span class="line"> <span class="built_in">NSString</span> *ext = name.pathExtension;</span><br><span class="line"> <span class="built_in">NSString</span> *path = <span class="literal">nil</span>;</span><br><span class="line"> <span class="built_in">CGFloat</span> scale = <span class="number">1</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// If no extension, guess by system supported (same as UIImage).</span></span><br><span class="line"> <span class="built_in">NSArray</span> *exts = ext.length > <span class="number">0</span> ? @[ext] : @[<span class="string">@""</span>, <span class="string">@"png"</span>, <span class="string">@"jpeg"</span>, <span class="string">@"jpg"</span>, <span class="string">@"gif"</span>, <span class="string">@"webp"</span>, <span class="string">@"apng"</span>];</span><br><span class="line"> <span class="built_in">NSArray</span> *scales = _NSBundlePreferredScales();</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> s = <span class="number">0</span>; s < scales.count; s++) {</span><br><span class="line"> scale = ((<span class="built_in">NSNumber</span> *)scales[s]).floatValue;</span><br><span class="line"> <span class="built_in">NSString</span> *scaledName = _NSStringByAppendingNameScale(res, scale);</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">NSString</span> *e <span class="keyword">in</span> exts) {</span><br><span class="line"> path = [[<span class="built_in">NSBundle</span> mainBundle] pathForResource:scaledName ofType:e];</span><br><span class="line"> <span class="keyword">if</span> (path) <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (path) <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (path.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSData</span> *data = [<span class="built_in">NSData</span> dataWithContentsOfFile:path];</span><br><span class="line"> <span class="keyword">if</span> (data.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> [[<span class="keyword">self</span> alloc] initWithData:data scale:scale];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>根据图片名取得拓展名,若未指定拓展名,遍历查询所有支持的类型</li><li>scales 为根据设备拿到的对应分辨率,@<span class="citation"data-cites="1">[@1,@2,@3]</span></li><li>得到有效 path 后,break</li><li>调用 <strong><code>initWithData:scale:</code></strong>方法进行初始化</li></ul><p>初始化方法最终都会调用<strong><code>-initWithData:scale:</code></strong>来进行初始化,源码如下:</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><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">instancetype</span>)initWithData:(<span class="built_in">NSData</span> *)data scale:(<span class="built_in">CGFloat</span>)scale {</span><br><span class="line"> <span class="keyword">if</span> (data.length == <span class="number">0</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">if</span> (scale <= <span class="number">0</span>) scale = [<span class="built_in">UIScreen</span> mainScreen].scale;</span><br><span class="line"> _preloadedLock = dispatch_semaphore_create(<span class="number">1</span>);</span><br><span class="line"> <span class="keyword">@autoreleasepool</span> {</span><br><span class="line"> YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:scale];</span><br><span class="line"> YYImageFrame *frame = [decoder frameAtIndex:<span class="number">0</span> decodeForDisplay:<span class="literal">YES</span>];</span><br><span class="line"> <span class="built_in">UIImage</span> *image = frame.image;</span><br><span class="line"> <span class="keyword">if</span> (!image) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">self</span> = [<span class="keyword">self</span> initWithCGImage:image.CGImage scale:decoder.scale orientation:image.imageOrientation];</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">self</span>) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> _animatedImageType = decoder.type;</span><br><span class="line"> <span class="keyword">if</span> (decoder.frameCount > <span class="number">1</span>) {</span><br><span class="line"> _decoder = decoder;</span><br><span class="line"> _bytesPerFrame = <span class="built_in">CGImageGetBytesPerRow</span>(image.CGImage) * <span class="built_in">CGImageGetHeight</span>(image.CGImage);</span><br><span class="line"> _animatedImageMemorySize = _bytesPerFrame * decoder.frameCount;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">self</span>.yy_isDecodedForDisplay = <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方法实现过程:</p><ul><li>初始化信号量 **_preloadedLock**</li><li>初始化图像解码器 **_decoder**(YYImageDecoder )</li><li>通过解压器获取第一帧解压后的图像</li><li>通过 <code>-initWithCGImage:scale:orientation:</code> 初始化得到YYImage 实例</li><li>若帧数 > 1<ul><li>暂存解码器 **_decoder**</li><li>暂存每帧内存占用大小 **_bytesPerFrame**</li><li>暂存总内存占用大小 **_animatedImageMemorySize**</li></ul></li><li>return 初始化完成的 YYImage</li></ul><p>需要注意的几个点:</p><ol type="1"><li><p><strong><code>_preloadedLock</code></strong>:<strong>dispatch_semaphore_t</strong>信号量锁,的目的是为了保证<strong><code>_preloadedFrames</code></strong>在内存中的读写安全。</p></li><li><p><strong><code>_preloadedFrames</code></strong>:对应<strong>preloadAllAnimatedImageFrames</strong>对外属性。若开启预加载所有帧到内存,<code>_preloadedFrames</code>数组会保存所有帧的图像。</p></li><li><p>为什么锁选信号量:原因是 **_preloadedFrames**的读写本身不会太耗时,不会有长时间的等待,使用信号量这样的自旋锁会比较合适。</p></li></ol><h2 id="yyframeimage">YYFrameImage</h2><p>YYFrameImage是专门用来处理帧动画图片的类,可以配置每一帧的图片信息和显示时长,也是UIImage 的子类,仅支持 png 和 jpeg 格式。</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">nullable</span> <span class="keyword">instancetype</span>)initWithImagePaths:(<span class="built_in">NSArray</span><<span class="built_in">NSString</span> *> *)paths</span><br><span class="line"> frameDurations:(<span class="built_in">NSArray</span><<span class="built_in">NSNumber</span> *> *)frameDurations</span><br><span class="line"> loopCount:(<span class="built_in">NSUInteger</span>)loopCount;</span><br><span class="line">- (<span class="keyword">nullable</span> <span class="keyword">instancetype</span>)initWithImageDataArray:(<span class="built_in">NSArray</span><<span class="built_in">NSData</span> *> *)dataArray</span><br><span class="line"> frameDurations:(<span class="built_in">NSArray</span> *)frameDurations</span><br><span class="line"> loopCount:(<span class="built_in">NSUInteger</span>)loopCount;</span><br></pre></td></tr></table></figure><p>方法实现过程: - 通过 path 拿到 NSData(或直接使用传入的 NSData) -通过 <strong>yy_imageByDecoded</strong> 解压 data,得到 UIImage - 通过UIImage,再带上帧动画相关的私有变量,封装得到 YYFrameImage - returnYYFrameImage</p><h2 id="yyspritesheetimage">YYSpriteSheetImage</h2><p>YYSpriteSheetImage 是用于支持 SpriteSheet 动画显示的图像类,也是UIImage 的子类。SpriteSheet动画可以理解为在一张大图上分布有很多块小图,不同时刻显示不同的小图,已达到动画展示的目的。一张图的加载耗时相对多图会好很多,避免了一些非必要的资源浪费。</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></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">nullable</span> <span class="keyword">instancetype</span>)initWithSpriteSheetImage:(<span class="built_in">UIImage</span> *)image</span><br><span class="line"> contentRects:(<span class="built_in">NSArray</span><<span class="built_in">NSValue</span> *> *)contentRects</span><br><span class="line"> frameDurations:(<span class="built_in">NSArray</span><<span class="built_in">NSNumber</span> *> *)frameDurations</span><br><span class="line"> loopCount:(<span class="built_in">NSUInteger</span>)loopCount;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>) <span class="built_in">NSArray</span><<span class="built_in">NSValue</span> *> *contentRects;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>) <span class="built_in">NSArray</span><<span class="built_in">NSValue</span> *> *frameDurations;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>) <span class="built_in">NSUInteger</span> loopCount;</span><br></pre></td></tr></table></figure></p><p>方法实现过程: - 根据传入数据,初始化 SpriteSheet动画播放过程中需要的参数 - <code>_contentRects</code> -<code>_frameDurations</code> - <code>_loopCount</code> - 封装得到YYSpriteSheetImage,并返回</p><h2 id="yyanimatedimage-协议">YYAnimatedImage 协议</h2><p>YYAnimatedImage 协议将 YYAnimatedImageView 和YYImage、YYFrameImage、YYSpriteSheetImage之间构成了联系。不论是这三种图像类或以后会有拓展的图像类,他们之间虽然存在区别,但最终动画展示的原理是不变的。可以将共性的模块通过YYAnimatedImage 协议来实现。</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@protocol</span> <span class="title">YYAnimatedImage</span> <<span class="title">NSObject</span>></span></span><br><span class="line"><span class="keyword">@required</span></span><br><span class="line"><span class="comment">// 总帧数</span></span><br><span class="line">- (<span class="built_in">NSUInteger</span>)animatedImageFrameCount;</span><br><span class="line"><span class="comment">// 循环次数,0 表示无限循环</span></span><br><span class="line">- (<span class="built_in">NSUInteger</span>)animatedImageLoopCount;</span><br><span class="line"><span class="comment">// 每帧在内存中的占用大小</span></span><br><span class="line">- (<span class="built_in">NSUInteger</span>)animatedImageBytesPerFrame;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取某一帧图像</span></span><br><span class="line">- (<span class="keyword">nullable</span> <span class="built_in">UIImage</span> *)animatedImageFrameAtIndex:(<span class="built_in">NSUInteger</span>)index;</span><br><span class="line"><span class="comment">// 获取某一帧的显示时间</span></span><br><span class="line">- (<span class="built_in">NSTimeInterval</span>)animatedImageDurationAtIndex:(<span class="built_in">NSUInteger</span>)index;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@optional</span></span><br><span class="line"><span class="comment">// 为 SpriteSheet 动画提供,获取某一动画的 contentsRect</span></span><br><span class="line">- (<span class="built_in">CGRect</span>)animatedImageContentsRectAtIndex:(<span class="built_in">NSUInteger</span>)index;</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>共性抽离的思路在我们的日常开发中也常会用到,这里通过协议来统一接口的实现方式,使得逻辑看上去很清晰。</p><h2 id="yyanimatedimageview">YYAnimatedImageView</h2><p>YYAnimatedImageView 是用来展示YYImage、YYFrameImage、YYSpriteSheetImage 的类。由于YYImage、YYFrameImage、YYSpriteSheetImage 都实现了 YYAnimatedImage的协议方法,YYAnimatedImageView可以根据不同的类型来对应展示图片。具体看下内部的实现。</p><p>YYAnimatedImageView可以理解为中间展示层,通过图像层解压的图像解码进行显示。</p><h3 id="初始化">初始化</h3><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="type">void</span>)setImage:(<span class="built_in">UIImage</span> *)image {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">self</span>.image == image) <span class="keyword">return</span>;</span><br><span class="line"> [<span class="keyword">self</span> setImage:image withType:YYAnimatedImageTypeImage];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setHighlightedImage:(<span class="built_in">UIImage</span> *)highlightedImage {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">self</span>.highlightedImage == highlightedImage) <span class="keyword">return</span>;</span><br><span class="line"> [<span class="keyword">self</span> setImage:highlightedImage withType:YYAnimatedImageTypeHighlightedImage];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setAnimationImages:(<span class="built_in">NSArray</span> *)animationImages {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">self</span>.animationImages == animationImages) <span class="keyword">return</span>;</span><br><span class="line"> [<span class="keyword">self</span> setImage:animationImages withType:YYAnimatedImageTypeImages];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)setHighlightedAnimationImages:(<span class="built_in">NSArray</span> *)highlightedAnimationImages {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">self</span>.highlightedAnimationImages == highlightedAnimationImages) <span class="keyword">return</span>;</span><br><span class="line"> [<span class="keyword">self</span> setImage:highlightedAnimationImages withType:YYAnimatedImageTypeHighlightedImages];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方法实现过程: - 在 YYAnimatedImageView 中可以看到四种 setImage的方式 - 先与已有 image 进行对比,如果不同则调用<strong><code>-setImage:withType:</code></strong></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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)setImage:(<span class="type">id</span>)image withType:(YYAnimatedImageType)type {</span><br><span class="line"> [<span class="keyword">self</span> stopAnimating];</span><br><span class="line"> <span class="keyword">if</span> (_link) [<span class="keyword">self</span> resetAnimated];</span><br><span class="line"> _curFrame = <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">switch</span> (type) {</span><br><span class="line"> <span class="keyword">case</span> YYAnimatedImageTypeNone: <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> YYAnimatedImageTypeImage: <span class="variable language_">super</span>.image = image; <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> YYAnimatedImageTypeHighlightedImage: <span class="variable language_">super</span>.highlightedImage = image; <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> YYAnimatedImageTypeImages: <span class="variable language_">super</span>.animationImages = image; <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> YYAnimatedImageTypeHighlightedImages: <span class="variable language_">super</span>.highlightedAnimationImages = image; <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> [<span class="keyword">self</span> imageChanged];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方法实现过程: - 根据不同的 type 重置对应 type 的 image 实例 -最终调用 <strong><code>-imageChanged</code></strong>。</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)imageChanged {</span><br><span class="line"><span class="comment">// 获取当前 type 和 image 实例</span></span><br><span class="line"> YYAnimatedImageType newType = [<span class="keyword">self</span> currentImageType];</span><br><span class="line"> <span class="type">id</span> newVisibleImage = [<span class="keyword">self</span> imageForType:newType];</span><br><span class="line"> <span class="built_in">NSUInteger</span> newImageFrameCount = <span class="number">0</span>;</span><br><span class="line"> <span class="type">BOOL</span> hasContentsRect = <span class="literal">NO</span>;</span><br><span class="line"> <span class="comment">// 特殊处理 SpriteSheet 类型 Image</span></span><br><span class="line"> <span class="comment">// 通过判断 protocol 是否有对应实现来判断,是否是 SpriteSheetImage</span></span><br><span class="line"> <span class="keyword">if</span> ([newVisibleImage isKindOfClass:[<span class="built_in">UIImage</span> <span class="keyword">class</span>]] &&</span><br><span class="line"> [newVisibleImage conformsToProtocol:<span class="class"><span class="keyword">@protocol</span>(<span class="title">YYAnimatedImage</span>)]) </span>{</span><br><span class="line"> newImageFrameCount = ((<span class="built_in">UIImage</span><YYAnimatedImage> *) newVisibleImage).animatedImageFrameCount;</span><br><span class="line"> <span class="keyword">if</span> (newImageFrameCount > <span class="number">1</span>) {</span><br><span class="line"> hasContentsRect = [((<span class="built_in">UIImage</span><YYAnimatedImage> *) newVisibleImage) respondsToSelector:<span class="keyword">@selector</span>(animatedImageContentsRectAtIndex:)];</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 若之前展示过 SpriteSheetImage,这里进行 layer 复位</span></span><br><span class="line"> <span class="keyword">if</span> (!hasContentsRect && _curImageHasContentsRect) {</span><br><span class="line"> <span class="comment">// 复位 rect,且过程中取消隐式动画</span></span><br><span class="line"> <span class="keyword">if</span> (!<span class="built_in">CGRectEqualToRect</span>(<span class="keyword">self</span>.layer.contentsRect, <span class="built_in">CGRectMake</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>, <span class="number">1</span>)) ) {</span><br><span class="line"> [<span class="built_in">CATransaction</span> begin];</span><br><span class="line"> [<span class="built_in">CATransaction</span> setDisableActions:<span class="literal">YES</span>];</span><br><span class="line"> <span class="keyword">self</span>.layer.contentsRect = <span class="built_in">CGRectMake</span>(<span class="number">0</span>, <span class="number">0</span>, <span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line"> [<span class="built_in">CATransaction</span> commit];</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> _curImageHasContentsRect = hasContentsRect;</span><br><span class="line"> <span class="comment">// 若是 SpriteSheetImage,取第一帧的 contentsRect</span></span><br><span class="line"> <span class="comment">// 并定位到 image 中 contentsRect 对应的位置</span></span><br><span class="line"> <span class="keyword">if</span> (hasContentsRect) {</span><br><span class="line"> <span class="built_in">CGRect</span> rect = [((<span class="built_in">UIImage</span><YYAnimatedImage> *) newVisibleImage) animatedImageContentsRectAtIndex:<span class="number">0</span>];</span><br><span class="line"> [<span class="keyword">self</span> setContentsRect:rect forImage:newVisibleImage];</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 多帧图特殊处理</span></span><br><span class="line"> <span class="comment">// 初始化属性 - totalLoop、_totalFrameCount 等</span></span><br><span class="line"> <span class="keyword">if</span> (newImageFrameCount > <span class="number">1</span>) {</span><br><span class="line"> [<span class="keyword">self</span> resetAnimated];</span><br><span class="line"> _curAnimatedImage = newVisibleImage;</span><br><span class="line"> _curFrame = newVisibleImage;</span><br><span class="line"> _totalLoop = _curAnimatedImage.animatedImageLoopCount;</span><br><span class="line"> _totalFrameCount = _curAnimatedImage.animatedImageFrameCount;</span><br><span class="line"> [<span class="keyword">self</span> calcMaxBufferCount];</span><br><span class="line"> }</span><br><span class="line"> [<span class="keyword">self</span> setNeedsDisplay];</span><br><span class="line"> [<span class="keyword">self</span> didMoved];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方法实现过程: - 获取 imageType 和图像实例(image 或 images) - 通过protocol 的实现情况,判断是否是 SpriteSheetImage - 若不是SpriteSheetImage 且之前展示过 SpriteSheetImage,进行 layer复位,且取消过程中的隐式动画 - 若是 SpriteSheetImage,取第一帧的contentsRect,并定位到对应 image 的位置 - 多帧图的一些属性处理,调用<strong><code>-resetAnimated</code></strong> 重置动画的配置项 - 调用didMoved 来执行动画</p><h3 id="动画过程">动画过程</h3><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="type">void</span>)didMoved {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">self</span>.autoPlayAnimatedImage) {</span><br><span class="line"> <span class="keyword">if</span>(<span class="keyword">self</span>.superview && <span class="keyword">self</span>.window) {</span><br><span class="line"> [<span class="keyword">self</span> startAnimating];</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> [<span class="keyword">self</span> stopAnimating];</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>会判断是否有 superView 及 window,都满足的情况下,开启动画。</p><h3 id="解压过程">解压过程</h3><p>前面 <strong><code>-resetAnimated</code></strong>方法执行的过程中,会重置队列<strong><code>_requestQueue</code></strong>。该队列的<strong>maxConcurrentOperationCount</strong> 为1,是一个串行队列。该队列作用是用来处理解压任务。</p><p><strong><code>-resetAnimated</code></strong>方法执行过程中会判断是否创建了定时器<strong><code>_link</code></strong>,若未创建,则创建(关于定时器,放后面单独看):</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></pre></td><td class="code"><pre><span class="line">_link = [<span class="built_in">CADisplayLink</span> displayLinkWithTarget:[_YYImageWeakProxy proxyWithTarget:<span class="keyword">self</span>] selector:<span class="keyword">@selector</span>(step:)];</span><br><span class="line"><span class="comment">// _runloopMode 为 NSRunLoopCommonModes</span></span><br><span class="line"><span class="keyword">if</span> (_runloopMode) {</span><br><span class="line"> [_link addToRunLoop:[<span class="built_in">NSRunLoop</span> mainRunLoop] forMode:_runloopMode];</span><br><span class="line">}</span><br><span class="line">_link.paused = <span class="literal">YES</span>;</span><br></pre></td></tr></table></figure><p>看到会定时执行 <strong>-step:</strong> 方法,其内会判断当前**_requestQueue** 是否无在执行的任务,如果没有加入新的 operation。该operation 为<strong><code>_YYAnimatedImageViewFetchOperation</code></strong>:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!bufferIsFull && _requestQueue.operationCount == <span class="number">0</span>) { <span class="comment">// if some work not finished, wait for next opportunity</span></span><br><span class="line"> _YYAnimatedImageViewFetchOperation *operation = [_YYAnimatedImageViewFetchOperation new];</span><br><span class="line"> operation.view = <span class="keyword">self</span>;</span><br><span class="line"> operation.nextIndex = nextIndex;</span><br><span class="line"> operation.curImage = image;</span><br><span class="line"> [_requestQueue addOperation:operation];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong><code>_YYAnimatedImageViewFetchOperation</code></strong>继承自 NSOperation,重写了 main 来自定义解压任务。源码如下:</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)main {</span><br><span class="line"> __<span class="keyword">strong</span> YYAnimatedImageView *view = _view;</span><br><span class="line"> <span class="keyword">if</span> (!view) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">if</span> ([<span class="keyword">self</span> isCancelled]) <span class="keyword">return</span>;</span><br><span class="line"> view->_incrBufferCount++;</span><br><span class="line"> <span class="keyword">if</span> (view->_incrBufferCount == <span class="number">0</span>) [view calcMaxBufferCount];</span><br><span class="line"> <span class="keyword">if</span> (view->_incrBufferCount > (<span class="built_in">NSInteger</span>)view->_maxBufferCount) {</span><br><span class="line"> view->_incrBufferCount = view->_maxBufferCount;</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">NSUInteger</span> idx = _nextIndex;</span><br><span class="line"> <span class="built_in">NSUInteger</span> max = view->_incrBufferCount < <span class="number">1</span> ? <span class="number">1</span> : view->_incrBufferCount;</span><br><span class="line"> <span class="built_in">NSUInteger</span> total = view->_totalFrameCount;</span><br><span class="line"> view = <span class="literal">nil</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i < max; i++, idx++) {</span><br><span class="line"> <span class="keyword">@autoreleasepool</span> {</span><br><span class="line"> <span class="keyword">if</span> (idx >= total) idx = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">if</span> ([<span class="keyword">self</span> isCancelled]) <span class="keyword">break</span>;</span><br><span class="line"> __<span class="keyword">strong</span> YYAnimatedImageView *view = _view;</span><br><span class="line"> <span class="keyword">if</span> (!view) <span class="keyword">break</span>;</span><br><span class="line"> LOCK_VIEW(<span class="type">BOOL</span> miss = (view->_buffer[@(idx)] == <span class="literal">nil</span>));</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (miss) {</span><br><span class="line"> <span class="built_in">UIImage</span> *img = [_curImage animatedImageFrameAtIndex:idx];</span><br><span class="line"> img = img.yy_imageByDecoded;</span><br><span class="line"> <span class="keyword">if</span> ([<span class="keyword">self</span> isCancelled]) <span class="keyword">break</span>;</span><br><span class="line"> LOCK_VIEW(view->_buffer[@(idx)] = img ? img : [<span class="built_in">NSNull</span> null]);</span><br><span class="line"> view = <span class="literal">nil</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>核心代码为:</p><figure class="highlight objc"><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 class="built_in">UIImage</span> *img = [_curImage animatedImageFrameAtIndex:idx];</span><br><span class="line">img = img.yy_imageByDecoded;</span><br></pre></td></tr></table></figure><p><strong>-animatedImageFrameAtIndex:</strong> 方法调用过程中会触发<strong>yy_imageByDecoded</strong>,会进行解码操作。作者为了保证解码成功,又进行了第二次的解码(yy_imageByDecoded方法内部会判断如果已经解码,不会再进行解码)。</p><p>解码完成后进行缓存。</p><h3 id="缓存机制">缓存机制</h3><p>我们可以看到 YYAnimatedImageView 中声明的关于缓存的私有变量:</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"><span class="built_in">NSMutableDictionary</span> *_buffer; <span class="comment">///< frame buffer</span></span><br><span class="line"><span class="type">BOOL</span> _bufferMiss; <span class="comment">///< whether miss frame on last opportunity</span></span><br><span class="line"><span class="built_in">NSUInteger</span> _maxBufferCount; <span class="comment">///< maximum buffer count</span></span><br><span class="line"><span class="built_in">NSInteger</span> _incrBufferCount; <span class="comment">///< current allowed buffer count (will increase by step)</span></span><br></pre></td></tr></table></figure><h4 id="缓存的节点">缓存的节点</h4><p>在解码的过程中,会将解码的内容赋值给 **_buffer**。</p><h4 id="容量限制">容量限制</h4><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// dynamically adjust buffer size for current memory.</span></span><br><span class="line">- (<span class="type">void</span>)calcMaxBufferCount {</span><br><span class="line"> int64_t bytes = (int64_t)_curAnimatedImage.animatedImageBytesPerFrame;</span><br><span class="line"> <span class="keyword">if</span> (bytes == <span class="number">0</span>) bytes = <span class="number">1024</span>;</span><br><span class="line"> </span><br><span class="line"> int64_t total = _YYDeviceMemoryTotal();</span><br><span class="line"> int64_t free = _YYDeviceMemoryFree();</span><br><span class="line"> int64_t max = MIN(total * <span class="number">0.2</span>, free * <span class="number">0.6</span>);</span><br><span class="line"> max = MAX(max, BUFFER_SIZE); <span class="comment">// BUFFER_SIZE = 10MB</span></span><br><span class="line"> <span class="keyword">if</span> (_maxBufferSize) max = max > _maxBufferSize ? _maxBufferSize : max;</span><br><span class="line"> <span class="type">double</span> maxBufferCount = (<span class="type">double</span>)max / (<span class="type">double</span>)bytes;</span><br><span class="line"> <span class="keyword">if</span> (maxBufferCount < <span class="number">1</span>) maxBufferCount = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (maxBufferCount > <span class="number">512</span>) maxBufferCount = <span class="number">512</span>;</span><br><span class="line"> _maxBufferCount = maxBufferCount;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最大缓存容量是动态计算的:</p><ul><li>设备允许容量 = min(总内存x0.2, 总空闲内存x0.6)</li><li>最大计算容量 = max(设备允许容量, BUFFER_SIZE)//BUFFER_SIZE=10MB</li><li>最大容量 = min(最大计算容量, 自定义最大容量)</li></ul><h4 id="清理机制">清理机制</h4><p>在 resetAnimation 时会注册两个 Notification:内存警告、进入后台。</p><p>在应用进入后台时: - 先取消全部异步的解压操作 - 计算下一帧的下标 -移除不是下一帧的所有缓存,保证进入前台时,能及时显示下一帧</p><h3 id="定时器-_link">定时器 <code>_link</code></h3><p><strong><code>_link</code></strong> 是基于 CADisplayLink创建的定时器,帧率刷新的特性适合用来处理帧率相关的 UI逻辑。单独拿出来梳理,是因为作者使用了 **_YYImageWeakProxy**类进行消息转发来避免循环应用。源码如下:</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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> A proxy used to hold a weak object.</span></span><br><span class="line"><span class="comment"> It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">_YYImageWeakProxy</span> : <span class="title">NSProxy</span></span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">weak</span>, <span class="keyword">readonly</span>) <span class="type">id</span> target;</span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithTarget:(<span class="type">id</span>)target;</span><br><span class="line">+ (<span class="keyword">instancetype</span>)proxyWithTarget:(<span class="type">id</span>)target;</span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">_YYImageWeakProxy</span></span></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithTarget:(<span class="type">id</span>)target {</span><br><span class="line"> _target = target;</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">}</span><br><span class="line">+ (<span class="keyword">instancetype</span>)proxyWithTarget:(<span class="type">id</span>)target {</span><br><span class="line"> <span class="keyword">return</span> [[_YYImageWeakProxy alloc] initWithTarget:target];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">id</span>)forwardingTargetForSelector:(SEL)selector {</span><br><span class="line"> <span class="keyword">return</span> _target;</span><br><span class="line">}</span><br><span class="line">- (<span class="type">void</span>)forwardInvocation:(<span class="built_in">NSInvocation</span> *)invocation {</span><br><span class="line"> <span class="type">void</span> *null = <span class="literal">NULL</span>;</span><br><span class="line"> [invocation setReturnValue:&null];</span><br><span class="line">}</span><br><span class="line">- (<span class="built_in">NSMethodSignature</span> *)methodSignatureForSelector:(SEL)selector {</span><br><span class="line"> <span class="keyword">return</span> [<span class="built_in">NSObject</span> instanceMethodSignatureForSelector:<span class="keyword">@selector</span>(init)];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">BOOL</span>)respondsToSelector:(SEL)aSelector {</span><br><span class="line"> <span class="keyword">return</span> [_target respondsToSelector:aSelector];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">BOOL</span>)isEqual:(<span class="type">id</span>)object {</span><br><span class="line"> <span class="keyword">return</span> [_target isEqual:object];</span><br><span class="line">}</span><br><span class="line">- (<span class="built_in">NSUInteger</span>)hash {</span><br><span class="line"> <span class="keyword">return</span> [_target hash];</span><br><span class="line">}</span><br><span class="line">- (Class)superclass {</span><br><span class="line"> <span class="keyword">return</span> [_target superclass];</span><br><span class="line">}</span><br><span class="line">- (Class)<span class="keyword">class</span> {</span><br><span class="line"> <span class="keyword">return</span> [_target <span class="keyword">class</span>];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">BOOL</span>)isKindOfClass:(Class)aClass {</span><br><span class="line"> <span class="keyword">return</span> [_target isKindOfClass:aClass];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">BOOL</span>)isMemberOfClass:(Class)aClass {</span><br><span class="line"> <span class="keyword">return</span> [_target isMemberOfClass:aClass];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">BOOL</span>)conformsToProtocol:(Protocol *)aProtocol {</span><br><span class="line"> <span class="keyword">return</span> [_target conformsToProtocol:aProtocol];</span><br><span class="line">}</span><br><span class="line">- (<span class="type">BOOL</span>)isProxy {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line">}</span><br><span class="line">- (<span class="built_in">NSString</span> *)description {</span><br><span class="line"> <span class="keyword">return</span> [_target description];</span><br><span class="line">}</span><br><span class="line">- (<span class="built_in">NSString</span> *)debugDescription {</span><br><span class="line"> <span class="keyword">return</span> [_target debugDescription];</span><br><span class="line">}</span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>消息转发过程:</p><ul><li>若 **_target** 存在,向 **_YYImageWeakProxy**实例发送消息,会正常转发给 target</li><li>在 **_target** 释放时,<strong>forwardingTargetForSelector:</strong>重定向失败,会调用 <strong>methodSignatureForSelector:</strong>来获取有效方法。</li><li>若未获取到有效方法,抛出异常</li><li>若获取到有效方法,调用 <strong>forwardInvocation:</strong>进行消息转发。作者认为控制返回 null</li></ul><h2 id="yyimagecoder">YYImageCoder</h2><p>该类主要包含:</p><ul><li>YYImageFrame 类(图片帧信息)</li><li>YYImageDecoder 解码器</li><li>YYImageEncoder 编码器</li></ul><p>解码过程描述:</p><ul><li>将 CGImageRef 转化为位图 bitmap</li><li>先通过 CGBitmapContextCreate() 创建图片上下文</li><li>再通过 CGContextDrawImage() 将图片绘制到上下文</li><li>最后通过 CGBitmapContextCreateImage() 结合上下文生成位图</li></ul><p>也通过 ImageIO 实现了渐进式解码。</p><p>多帧解码的过程中,会出现递归操作,作者选用了互斥锁<strong>pthread_mutex_t</strong>。<strong>pthread_mutex_t</strong>本身不支持递归,可以通过设置打开递归选项。</p><h2 id="总结">总结</h2><p>YYImage 可以理解为是基于 UIImage 的一个拓展。</p><p>功能上。一方面支持了更多的业务需求场景,如 webp格式图片的展示。另一方面把图片的解码缓存,变为用户可控状态。我们可以根据自己的业务需求调整缓存的设定。</p><p>框架结构上。线程安全的处理、重叠模块的解耦都做的很棒。日常开发过程中,可以借鉴一些结构上的思路。</p><ul><li>UIImage<ul><li>加载、解码 bitmap、CA 框架绘制</li><li>imageNamed:</li><li>imageWithData:</li><li>大图<ul><li>4096x4096 最大纹理,老设备 OOM</li><li>ImageIO 取图像元数据</li><li>WWDC18 iOS10 引入 UIGraphicsImageRenderer</li></ul></li></ul></li><li>YYImage 框架<ul><li>图像层</li><li>视图层</li><li>编解码</li></ul></li><li>YYImage<ul><li>重写 imageNamed</li><li>初始化信号量<code>_preloadedLock</code>,保证<code>_preloadedFrames</code>读写安全</li><li><code>_preloadedFrames</code> 保存所有帧图像</li></ul></li><li>YYFrameImage<ul><li>帧动画</li><li>帧图片 + 显示时长</li><li>png、jpeg</li></ul></li><li>YYSpriteSheetImage<ul><li>SpriteSheet 动画</li><li>rect</li><li>duration</li><li>loopCount</li></ul></li><li>YYAnimatedImage 协议<ul><li>图像展示原理相同,展示通过该协议来实现</li><li><span class="citation" data-cites="required">@required</span></li><li><span class="citation" data-cites="optional">@optional</span></li></ul></li><li>YYAnimatedImageView<ul><li>展示上面的 Image 解码的图像</li><li>setImage/setHighlightedImage</li><li>setAnimationImages/setHighlightedAnimationImages</li><li>根据 image type 拿到具体的 image 实例</li><li>拿到实例后,imageChange<ul><li>SpriteSheet 单独处理</li><li>重置 rect,取消隐式动画</li><li>frameCount > 1,进行 resetAnimation</li></ul></li><li>resetAnimation 做的几件事<ul><li>重置定时器(commonModes)</li><li>使用了 NSProxy 避免循环引用</li><li>添加 <code>_YYAnimatedImageViewFetchOperation</code> 进行预解码</li><li>继承自 NSOperation,重写了 main来自定义解压任务(二次解压保险)</li><li>解码 image 时会进行 buffer 缓存</li><li>缓存容量计算公式<ul><li>设备允许容量 = min(总内存x0.2, 总空闲内存x0.6)</li><li>最大计算容量 = max(设备允许容量, BUFFER_SIZE)//BUFFER_SIZE=10MB</li><li>最大容量 = min(最大计算容量, 自定义最大容量)</li></ul></li><li>缓存清理<ul><li>resetAnimation 注册两通知(内存警告、进入后台)</li><li>先取消全部异步解码操作</li><li>计算下一帧的下标</li><li>移除下一帧之外的所有缓存,以保证进入前台,可以立刻显示下一帧</li></ul></li></ul></li></ul></li><li>YYImageCoder<ul><li>YYImageFrame</li><li>YYImageDecoder</li><li>解码过程<ul><li>将 CGImageRef 转为 bitmap</li><li>通过 CGBitmapContextCreate() 创建上下文</li></ul></li><li>多帧解码过程中使用了pthread_mutex_t,因为存在递归操作,这里使用了互斥锁</li></ul></li></ul>]]></content>
<summary type="html"><p>图片相关的处理,在移动应用中属于比较重要的一个角色。本篇主要是对 <a
href="https://github.com/ibireme/YYImage">YYImage</a>
的源码实现做一个梳理,内容结构:</p>
<ul>
<li>UIImage 相关的处理</li>
<li>YYImage 框架结构</li>
<li>YYImage</li>
<li>YYFrameImage</li>
<li>YYSpriteSheetImage</li>
<li>YYAnimatedImage</li>
<li>YYAnimatedImageView</li>
<li>YYImageCoder</li>
</ul></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="源码阅读" scheme="http://example.com/categories/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="源码阅读" scheme="http://example.com/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
</entry>
<entry>
<title>WWDC17 505 - Photos APIs 新特性</title>
<link href="http://example.com/2017/08/30/2017-08-30-wwdc17-505-whats-new-in-photos-apis/"/>
<id>http://example.com/2017/08/30/2017-08-30-wwdc17-505-whats-new-in-photos-apis/</id>
<published>2017-08-29T16:00:00.000Z</published>
<updated>2017-08-29T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>视频链接:<a href="https://developer.apple.com/wwdc17/505">WWDC 2017Session 505 - What's New in Photos APIs</a></p><p>本节要介绍的是 Photos APIs的一些新特性。简单的概括有下面这几点内容:</p><ul><li>UIImagePickerController 的大幅优化</li><li>授权模式的改进</li><li>动图的支持</li><li>iCloud 照片图库的优化</li><li>照片项目的扩展</li></ul><p>后续内容,会对这几个点依次展开。</p><span id="more"></span><h2 id="uiimagepickercontroller-的大幅优化">UIImagePickerController的大幅优化</h2><p><code>UIImagePickerController</code>是系统提供的和相册及相机交互的一个类,通过这个类,你可以在应用中选择照片和视频。在iOS 11 里,图片选择器有了许多的改进和新功能的引入。</p><h3 id="隐私授权的改进">隐私授权的改进</h3><p>一直以来,Apple十分关注用户的隐私安全。所以,之前在任何情况下,如果获取 Photos资源,都需要获取用户的授权才可以进行。正如下面弹窗这样,请求用户的授权。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/758ef482bd5a71d4ec7170c15a1bcc60.jpg" /></p><p>正因为授权过程的存在,使得应用程序与用户之间产生了矛盾。对于用户而言,需要他们打开一级隐私,这不是用户想要的;而对于应用程序来说,应用在未获取权限的情况下,无法执行相应的程序和操作,即便它自身有很多优秀的功能,都会因为未授权而无法使用。</p><p>在 iOS 11 中,如果通过<code>UIImagePickerController</code>访问相册资源,这个<code>警告弹窗</code>不会再出现,会直接进行程序运行。看到这里,你或许会问:那用户的隐私保护怎么办?</p><p>首先需要介绍一下<code>UIImagePickerController</code> 新的授权模式。自iOS 11 开始,<code>UIImagePickerController</code>成为了一个自动授权API。也就是说,当应用程序要显示 API的内容,将会是从一个<code>自动处理的沙盒</code>和<code>安全环境</code>中获取,应用不再访问用户的<code>Photo Library</code>。</p><p>并且,只有用户本人可以和<code>UIImagePickerController UI</code>进行互动。当用户做出一个选择,系统会取出选中的照片或视频,发送到应用中。这样就消除了前面提出的在应用中因为授权而产生的矛盾,同时这也让用户有了更高级别的隐私。因为不存在授权,也就不会再有请求授权。使用起来更为方便了。</p><h3 id="元数据的获取更为方便">元数据的获取更为方便</h3><p>Photos拥有丰富的<code>元数据(metadata)</code>,内容包括创建日期、照片的格式,及一些其他不同类型的元数据。在iOS 11 中,获取这些信息变得容易了很多。系统提供了一个全新的键值<code>UIImagePickerControllerImageURL</code>,会包含所有<code>UIImagePickerController</code>的结果。我们可以使用这里的URL,将对应数据读入应用并按照需要进行处理。该 URL是<code>文件 URL</code>,指向一个应用中的<code>临时文件</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">imagePickerController</span>(<span class="keyword">_</span> <span class="params">picker</span>: <span class="type">UIImagePickerController</span>, <span class="params">didFinishPickingMediaWithInfo</span> <span class="params">info</span>: [<span class="params">String</span> : <span class="keyword">Any</span>]) { </span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> imageURL <span class="operator">=</span> info[<span class="type">UIImagePickerControllerImageURL</span>] <span class="keyword">as?</span> <span class="type">URL</span> { </span><br><span class="line"> <span class="built_in">print</span>(imageURL)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="heif-图片格式的引入">HEIF 图片格式的引入</h3><p>iOS 11 中,Photos引入了一种新的图片格式<code>HEIF</code>。同时,Apple意识到生态系统完全接受<code>HEIF</code>需要一段时间,考虑到新类型图片格式的兼容性。Apple为<code>UIIMagePickerController</code>提供了一个新属性<code>imageExportPresent</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> imageExportPreset: <span class="type">UIImagePickerControllerImportExportPreset</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> imagePicker <span class="operator">=</span> <span class="type">UIImagePickerController</span>() </span><br><span class="line">imagePicker.imageExportPreset <span class="operator">=</span> .compatible </span><br><span class="line"><span class="keyword">self</span>.present(imagePicker, animated: <span class="literal">true</span>, completion: <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> imagePicker <span class="operator">=</span> <span class="type">UIImagePickerController</span>() </span><br><span class="line">imagePicker.imageExportPreset <span class="operator">=</span> .current </span><br><span class="line"><span class="keyword">self</span>.present(imagePicker, animated: <span class="literal">true</span>, completion: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p><code>imageExportPresent</code>拥有两种类型:</p><ul><li>.compatible (兼容模式)</li><li>.current (当前模式)</li></ul><p>在<code>compatible (兼容模式)</code>下,如果用户选中的源图片是<code>HEIF 格式</code>,系统会通过转换,提供一个JPEG 格式的图片。当然,JPEG是该属性的默认值,如果不需要有什么改变,就不用再做更多的事情。</p><p>如果,需要获取的照片格式与拍摄时的格式相同,只需把属性值设为<code>current (当前模式)</code>,这样就会得到与Photo Library 里相同格式的图片,包括<code>HEIF 格式</code>。</p><h3 id="视频文件的获取更为方便">视频文件的获取更为方便</h3><p>iOS 11中,对视频选择的功能,也有了很好的改进。暂时把这部分内容放在一边,先来简单了解下<code>AVFoundation</code>。<code>AVFoundation</code>是Apple提供的框架,用于<code>丰富编辑</code>及<code>照片播放</code>。通过<code>AVFoundation</code>导出的素材可以拥有丰富的格式。</p><p>值得称赞的是,<code>UIImagePickerController</code>现在也有了类似的功能,引入了一个新属性<code>videoExportPreset</code>。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> videoExportPreset: <span class="type">String</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br></pre></td></tr></table></figure><p>你可以通过这个方法来告诉系统,你所选中的视频需要以哪种格式返回。这样,你就可以轻松得到预设格式的资源内容了。</p><p>我们来看一个例子:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> AVFoundation</span><br><span class="line"><span class="keyword">let</span> imagePicker <span class="operator">=</span> <span class="type">UIImagePickerController</span>() </span><br><span class="line">imagePicker.videoExportPreset <span class="operator">=</span> <span class="type">AVAssetExportPresetHighestQuality</span> <span class="keyword">self</span>.present(imagePicker, animated: <span class="literal">true</span>, completion: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p>如上代码中,首先,导入<code>AVFoundation</code>;接着,创建一个<code>UIIMagePickerController</code>实例,并描述我们想要资源文件以哪种格式返回(这里我们请求的是最高品质);之后显示选择器。</p><p>当用户做出选择时,无论是什么格式,系统都对其进行交叉编译,得到匹配格式,之后返回给用户。关于可用预设的完整清单,可以通过接口<code>AVAssetExportSession</code>查看。</p><h3id="照片和视频的保存有了新的隐私模式">照片和视频的保存有了新的隐私模式</h3><p>前面,通过一些巧妙的设计,在保护用户隐私的情况下,已经实现了无缝选取。实际上,iOS11 也对图片和视频的保存做了很多的优化。</p><p>在 iOS 11中,保存一张照片或一段视频到用户的图片库中,系统提供了一个全新的安全模型及权限级别。<code>UIImagePickerController</code>对于保存<code>图片资源</code>及<code>视频资源</code>分别提供了权限级别。一个是<code>UIImageWriteToSavedPhotosAlbum</code>,另一个是<code>UISaveVideoAtPathToSavedPhotosAlbum</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">UIImageWriteToSavedPhotosAlbum</span>(<span class="keyword">_</span> <span class="params">image</span>: <span class="type">UIImage</span>, <span class="keyword">_</span> <span class="params">completionTarget</span>: <span class="keyword">Any</span><span class="operator">?</span>, <span class="keyword">_</span> <span class="params">completionSelector</span>: <span class="type">Selector</span>?, <span class="keyword">_</span> <span class="params">contextInfo</span>: <span class="type">UnsafeMutableRawPointer</span>?)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">UISaveVideoAtPathToSavedPhotosAlbum</span>(<span class="keyword">_</span> <span class="params">videoPath</span>: <span class="type">String</span>, <span class="keyword">_</span> <span class="params">completionTarget</span>: <span class="keyword">Any</span><span class="operator">?</span>, <span class="keyword">_</span> <span class="params">completionSelector</span>: <span class="type">Selector</span>?, <span class="keyword">_</span> <span class="params">contextInfo</span>: <span class="type">UnsafeMutableRawPointer</span>?)</span><br></pre></td></tr></table></figure><figure><img src="./resource/session505/505_add_to_photos.jpg"alt="Add To Your Photos" /><figcaption aria-hidden="true">Add To Your Photos</figcaption></figure><p>这两种方式都只会请求<code>添加授权</code>,对于用户来说<code>添加授权</code>是很小的要求。因为这个权限只允许添加内容到用户的PhotoLibrary,而不涉及到读取权限。所以,很大程度上,用户会愿意给出这个权限。</p><h3 id="phasset-获取的改进">PHAsset 获取的改进</h3><p>我们来看一个例子:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">imagePickerController</span>(<span class="keyword">_</span> <span class="params">picker</span>: <span class="type">UIImagePickerController</span>, <span class="params">didFinishPickingMediaWithInfo</span> <span class="params">info</span>: [<span class="params">String</span> : <span class="keyword">Any</span>]) {</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> asset <span class="operator">=</span> info[<span class="type">UIImagePickerControllerPHAsset</span>] <span class="keyword">as?</span> <span class="type">PHAsset</span> {</span><br><span class="line"> <span class="built_in">print</span>(asset)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述代码中,我们实现了一个代理方法。在获得结果词典时,有了一个全新的键,键名为<code>UIIMagePickerControllerPHAsset</code>。取得该键的值,将会得到对应的资产对象,可以对其进行自由使用。</p><p>通过这些改变,增强了用户的隐私保护,也让<code>UIIMagePickerController</code>成为更强大而功能齐全的API,满足了市面上大部分应用的需求。然而,有时会出现需要和照片框架进行更深入集成的需求,在这些场景下,Apple推荐使用<code>PhotoKit</code>。</p><table style="width:7%;"><colgroup><col style="width: 6%" /></colgroup><thead><tr class="header"><th>## PhotoKit</th></tr></thead><tbody><tr class="odd"><td>## PhotoKit</td></tr><tr class="even"><td>和照片相关的应用,一直以来是 App Store里最受欢迎的一类。这一次,<code>PhotoKit</code>做了一些改进,可以让你写出拥有更棒用户体验的新功能。</td></tr><tr class="odd"><td>### Live Photo 介绍</td></tr><tr class="even"><td>首先一起了解下<code>Live Photo</code>的效果。Live Photo效果包含:</td></tr><tr class="odd"><td>- <strong>循环效果</strong> - <strong>弹跳效果</strong> -<strong>长曝光效果等</strong></td></tr><tr class="even"><td>其中<code>循环效果</code>,是通过仔细分析视频帧,并无缝地和这些视频帧无止境循环缝合在一起;<code>弹跳效果</code>,它的工作原理和<code>循环效果</code>也是相似的;最后是<code>长曝光效果</code>,它将分析Live Photo 的视频帧,创造令人惊艳的静物。</td></tr><tr class="odd"><td>现存的<code>PhotoKit</code>媒体类型有这些:</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">PHAssetMediaType</span> : <span class="title class_">Int</span> {</span><br><span class="line"> <span class="keyword">case</span> unknown</span><br><span class="line"> <span class="keyword">case</span> image</span><br><span class="line"> <span class="keyword">case</span> video</span><br><span class="line"> <span class="keyword">case</span> audio</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">struct</span> <span class="title class_">PHAssetMediaSubtype</span> : <span class="title class_">OptionSet</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoPanorama</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoHDR</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoScreenshot</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoLive</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoDepthEffect</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> videoStreamed</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> videoHighFrameRate</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> videoTimelapse</span><br><span class="line"> }</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>如果用户拍摄了一段视频,会想在应用中进行观看,并将拍摄的内容以<code>视频方式</code>使用。如果用户拍摄了一张<code>Live Photo</code>,同样会想要在应用中看到内容以<code>Live Photo</code>的方式呈现。为此,iOS11 提供了三种媒体类型来实现对应目标:</td></tr><tr class="even"><td>- <strong>image</strong> - <strong>video</strong> -<strong>photoLive</strong></td></tr><tr class="odd"><td><code>Live Photo</code>效果比较复杂。为此,iOS 11中引入了全新的<code>PHAsset</code>属性<code>playbackStyle</code>,让你可以简单实现<code>Live Photo</code>的播放。</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">PHAsset</span> : <span class="title class_">PHObject</span> {</span><br><span class="line"> <span class="keyword">var</span> playbackStyle: <span class="type">PHAssetPlaybackStyle</span> { <span class="keyword">get</span> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">enum</span> <span class="title class_">PHAssetPlaybackStyle</span> : <span class="title class_">Int</span> {</span><br><span class="line"> <span class="keyword">case</span> unsupported</span><br><span class="line"> <span class="keyword">case</span> image</span><br><span class="line"> <span class="keyword">case</span> imageAnimated</span><br><span class="line"> <span class="keyword">case</span> livePhoto</span><br><span class="line"> <span class="keyword">case</span> video</span><br><span class="line"> <span class="keyword">case</span> videoLooping</span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td><code>playbackStyle</code>属性,是<code>唯一</code>可以用来查看和决定要使用什么样的图片管理器API、用什么样的视图来表现、以及为该视图设置什么样的 UI 限制。同时,Apple更新了<ahref="https://developer.apple.com/library/content/samplecode/UsingPhotosFramework/Introduction/Intro.html#//apple_ref/doc/uid/TP40014575">PhotoKit示例应用</a>,包含所有这些新的播放风格。这里介绍下其中的三种,它们和前面提到的<code>Live Photo 效果</code>相关。从<code>imageAnimated</code>开始。</td></tr><tr class="even"><td>#### Animated Image</td></tr><tr class="odd"><td><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></pre></td><td class="code"><pre><span class="line">imageManager.requestImageData(for: asset, options: options) {</span><br><span class="line"> (data, dataUTI, orientation, info) <span class="keyword">in</span> <span class="comment">// 使用示例项目中的 animatedImageView</span></span><br><span class="line"> <span class="keyword">let</span> animatedImage <span class="operator">=</span> <span class="type">AnimatedImage</span>(data: data)</span><br><span class="line"> animatedImageView.animatedImage <span class="operator">=</span> animatedImage</span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>iOS 11有了一个期待已久的新功能。现在,在内置应用“照片”中支持了<code>动画 GIF</code>的播放。如果要在你的应用中播放GIF,只需要从图像管理器请求图像数据,然后使用图像 IO 和 Core Graphics进行播放。接下来是<code>Live Photo</code>。</td></tr><tr class="odd"><td>#### Live Photo</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line">imageManager.requestLivePhoto(for: asset, targetSize: pixelSize, contentMode: .aspectFill, options: options) {</span><br><span class="line"> (livePhoto, info) <span class="keyword">in</span> <span class="comment">// 使用示例项目中的 PHLivePhotoView</span></span><br><span class="line"> livePhotoView.livePhoto <span class="operator">=</span> livePhoto</span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td><code>Live Photos</code>一直很受用户的关注,如何在应用中更好地呈现它们,非常重要,也非常简单。在如上的这个例子里,首先从图像管理器<code>请求</code>一张<code>Live Photo</code>,之后设置<code>PHLivePhotoView</code>。在你的应用里,用户可以通过轻触播放一张<code>Live Photo</code>,正如用户在内置<code>“照片”应用</code>里的操作一样。</td></tr><tr class="even"><td>#### Looping Video</td></tr><tr class="odd"><td><figure class="highlight plaintext"><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">swiftimageManager.requestPlayerItem(forVideo: asset, options: options) { </span><br><span class="line"> playerItem, info in</span><br><span class="line"> DispatchQueue.main.async {</span><br><span class="line"> let player = AVQueuePlayer()</span><br><span class="line"> playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)</span><br><span class="line"> playerLayer.player = player</span><br><span class="line"> player.play()</span><br><span class="line"> }}</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>在今年所推出的视频循环中,既包括<code>弹跳效果</code>,也包括<code>Live Photo</code>的循环效果。现在,在你的应用里播放这些和播放普通视频非常相似。可以请求播放器项目,并使用<code>AVFoundation</code>播放,还可以使用<code>AVPlayerLooper</code>取得循环效果。可见,表现用户的媒体变得更为轻便,以他们真正想表现的方式,你也可以在自己的应用中,对这些全新的媒体类型,更为创新地表现。</td></tr><tr class="odd"><td>## iCloud 照片图库的改进 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/fe83154503f1c3e58ce11fbf317bc544.jpg" /></td></tr><tr class="even"><td>“iCloud 照片图库”可以与“照片”应用完美搭配使用。当用户开启“iCloud照片图库”时,用户的照片和视频会被安全上传到 iCloud中,同时这些更改会同步到用户的其他设备中。自动上传 iCloud操作的触发条件是,设备连接到 Wi-Fi且电量充足。根据用户的网络情况,在所有设备和 iCloud.com上看到同步照片和视频所需的时间可能会不同。</td></tr><tr class="odd"><td>使用 iPhone拍照的用户,也常会使用“照片”相关的第三方应用。这些用户,大致可以分为 3类:轻度用户、中度爱好者和重度专业用户。对于重度用户而言,由于自身图库中有很多内容,在第一次使用应用时,需要加载大批量的照片,这个过程中会十分耗时。而且这种耗时的加载状态,会对应用的用户体验大打折扣。</td></tr><tr class="even"><td>在 iOS 11中,针对如何更快速高效地操作“大型照片图库”这一点做了优化,后面的内容会依次展开描述。</td></tr><tr class="odd"><td>### 创建一个用于测试的“大型照片图库”</td></tr><tr class="even"><td>前面提到,如果图库中有大量内容,在应用加载数据时,会处于长时间的加载状态。而你如果想创建一个拥有大批量图片的图库,还是有些难度的。友好的是,Apple为开发者提供了一个用户创建图库的示例应用:<ahref="https://developer.apple.com/sample-code/wwdc/2017/Creating-Large-Photo-Libraries-for-Testing.zip">PhotoLibraryFiller</a>。下载该应用并安装到测试设备,点击<code>“Add Photos” 按钮</code>,它便会迅速生成一个拥有大批量图片的图库供测试使用。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/bbaf0214e3d8cad2d6e968c56487f529.jpg" /></td></tr><tr class="odd"><td>到这里,你就拥有了一个可用于测试的<code>“大型照片图库”</code>。</td></tr><tr class="even"><td>### 现在,如何从“照片图库”提取图片</td></tr><tr class="odd"><td>看下面这段代码:</td></tr><tr class="even"><td><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> assets <span class="operator">=</span> <span class="type">PHAsset</span>.fetchAssets(with: options)</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>这个方法用于从用户的<code>“照片图库”</code>提取图片,等号右侧用于<code>提取资产</code>,左侧为<code>提取结果</code>。</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> options <span class="operator">=</span> <span class="type">PHFetchOptions</span>()options.predicate <span class="operator">=</span> <span class="type">NSPredicate</span>(format: <span class="operator">&</span>quot;isFavorite <span class="operator">=</span> <span class="operator">%</span>d<span class="operator">&</span>quot;, <span class="literal">true</span>)</span><br><span class="line">options.sortDescriptors <span class="operator">=</span> [<span class="type">NSSortDescriptor</span>(key: <span class="operator">&</span>quot;creationDate<span class="operator">&</span>quot;, ascending: <span class="literal">true</span>)]</span><br><span class="line"><span class="keyword">let</span> assets <span class="operator">=</span> <span class="type">PHAsset</span>.fetchAssets(with: options)</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>在上述代码中,首先提取库里<code>所有的 Asset</code>,并进行筛选,筛选条件为<code>isFavorite = true</code>,之后按照对应的创建日期进行排序。这时,如果在这些自定义提取里<code>发现耗时</code>,那么简化这里的筛选条件会十分必要。不同的筛选方式,可能会意味着查询耗时的巨大差异。造成这种差异的原因是你的操作可能在数据库优化路径之外进行,同时又试图回到优化路径中,这样就产生了不同的耗时差距。</td></tr><tr class="even"><td>比这种自定义提取更好的是,尽可能避免这种操作。例如下面这个例子中,我们实际上提取的是用户<code>最喜欢</code>的<code>智能相册</code>。然后在智能相册里<code>提取 Asset</code>。这样既可以使用已有的<code>关键字</code>和<code>排序描述符</code>,还可以保证操作是在数据库优化路径中进行的。</td></tr><tr class="odd"><td><figure class="highlight swift"><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 class="keyword">let</span> smartAlbums <span class="operator">=</span> <span class="type">PHAssetCollection</span>.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">let</span> assets <span class="operator">=</span> <span class="type">PHAsset</span>.fetchAssets(in: smartAlbums.firstObject, options: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>接着,看下返回结果。返回对象是一个<code>PHFetchResult</code>类型的对象。<code>PHFetchResult 类型</code>非常像一个数列,并且可以像数组一样来使用。但从内部实现来看,<code>PHFetchResult</code>和数列的工作机制还是完全不同的。并且这也是<code>PhotoKit</code>在大型图库操作方面,能够如此快速高效的原因之一。</td></tr><tr class="odd"><td>我们来看看它的内部工作机制。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/b7383b7c7eddae3304c48d283c7d75ee.jpg" /></td></tr><tr class="even"><td>最开始,它只包含一个<code>标识符列表</code>。这意味着可以迅速返回对应的Asset。但开始使用后,有更多工作必须执行。我们以一个枚举作为例子。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/94a0ff75cb87c884023be87268c89b97.jpg" /></td></tr><tr class="odd"><td>在这里,我们从<code>索引 0 开始</code>枚举。目前只有一个标识符,你还需要从数据库里提取元数据。为此创建一个<code>PHAsset 对象</code>,以便将Asset 的元数据返回给你。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/16d78b56086431e4f054daede150d4d2.jpg" /></td></tr><tr class="even"><td>实际上,同时也创建了<code>一个批处理</code>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/5662e0c527a4b13085762d09fa1cc817.jpg" /></td></tr><tr class="odd"><td>当我们继续枚举时,<code>索引 1 和 2</code>实际已经在内存中了。枚举继续,它将访问硬盘,获取后续Asset 的元数据。</td></tr><tr class="even"><td>在提取结果量级较小的情况下,这样的提取,并不会有太大的影响。但如果提取结果包含<code>10万个 Asset</code>。其中,每一批都需要<code>占用几 kb 内存</code>。如果是<code>10w 批</code>,那将会产生<code>几百兆</code>的内存用量。更糟糕的是,每一批都需要<code>几毫秒</code>的提取时间,如果有<code>10w 批</code>,就需要<code>消耗 10s</code>来枚举这样的一个大型提取结果。所以,应该尽量<code>避免枚举操作</code>。</td></tr><tr class="odd"><td>### 在 PHFetchResult 中查询 Asset 更优的方式</td></tr><tr class="even"><td>实际开发过程中,枚举操作总是可能会出现的。这里列举一个例子。现在,你需要从一个提取结果中查询一个<code>Asset 的索引</code>。</td></tr><tr class="odd"><td>第一种方式,可以通过枚举该提取结果,通过<code>“等于”</code>比较返回的对象,来获取对应<code>Asset 的索引</code>。但是,枚举会非常耗时,所以更好的方法是通过另一种方式,使用<code>高端 API</code>。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/2a142436035188537528e1eaef142487.jpg" /></td></tr><tr class="even"><td>如上,通过使用<code>indexOfObject</code>来进行 Asset索引的查询。而<code>indexOfObject</code>方法内部是通过比较“对象标识符”,以找到符合条件的Asset,这样就不会有附加的“硬盘访问”和“数据库提取”。进而避免了第一种方式中,因为枚举出现的耗时操作。同样的,对<code>containtsObject</code>也是如此。</td></tr><tr class="odd"><td>## 照片项目的拓展</td></tr><tr class="even"><td>一直以来,Apple 允许用户围绕照片创建丰富的有创意的项目。</td></tr><tr class="odd"><td>### PHProjectExtensionController 的引入</td></tr><tr class="even"><td>现在,<code>“照片”</code>中添加了一个<code>新的扩展</code>。对应的,<code>Xcode</code>中也添加了一个<code>新模板</code>,开发者在自己的应用里可以<code>轻松创建</code>这些扩展。此外,<code>“照片”应用</code>会自动发现你的拓展,大大提高了扩展应用被用户知道的概率。不仅如此,Apple为了让这些扩展更容易为用户所发现,给这些扩展应用提供了<code>App Store</code>的直接链接。该链接会打开<code>App Store</code>窗口,并自动显示支持该扩展的应用。</td></tr><tr class="odd"><td><imgsrc="https://images.xiaozhuanlan.com/photo/2017/756e797b9eced7d408432f876e21c673.jpg" />扩展只存在于开发者的应用内。对于开发者来说,如果你在 App Store已经有了一个关于 Photos相关的应用。此时,就可以将扩展代码移动到该扩展空间内,并加以利用。之后,添加一个视图控制器,并实现<code>PHProjectExtensionController</code>协议。一切就位,“照片应用”便可以发现你的扩展了。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/e3a53f0348049d39207bc4656a78e8fa.jpg" /></td></tr><tr class="even"><td>在用户选择“扩展应用”,并用它创建一个项目时,<code>&quot;照片应用&quot;</code>会发送一些字节数据(<code>PHProjectExtensionContext</code>、<code>PHProjectInfo</code>)到对应的<code>“扩展应用”</code>。之后<code>“照片应用”</code>得到相应的返回结果,知道可以安装你的视图控制器。</td></tr><tr class="odd"><td>过程中遵循的协议本身,对支持的<code>项目类型</code>有一个可选属性,可用于快速描述想让用户选择的选项。</td></tr><tr class="even"><td><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">optional</span> <span class="keyword">public</span> <span class="keyword">var</span> supportedProjectTypes: [<span class="type">PHProjectTypeDescription</span>] { <span class="keyword">get</span> }</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>在实际使用过程中,用户既可以选择退出,也可以选择直接进入扩展。这些,在视图控制器里也有特定的函数方法。</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">protocol</span> <span class="title class_">PHProjectExtensionController</span> : <span class="title class_">NSObjectProtocol</span> {</span><br><span class="line"> <span class="comment">//第一次使用该扩展创建项目时调用 </span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">beginProject</span>(<span class="params">with</span> <span class="params">extensionContext</span>: <span class="type">PHProjectExtensionContext</span>, </span><br><span class="line"> <span class="params">projectInfo</span>: <span class="type">PHProjectInfo</span>, <span class="params">completion</span>: <span class="keyword">@escaping</span> (<span class="type">Error</span>?) -> <span class="type">Void</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//每次用户回到以前创建的项目时调用</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">resumeProject</span>(<span class="params">with</span> <span class="params">extensionContext</span>: <span class="type">PHProjectExtensionContext</span>, </span><br><span class="line"> <span class="params">completion</span>: <span class="keyword">@escaping</span> (<span class="type">Error</span>?) -> <span class="type">Void</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//用户离开项目时调用 </span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">finishProject</span>(<span class="params">completionHandler</span> <span class="params">completion</span>: <span class="keyword">@escaping</span> () -> <span class="type">Void</span>) </span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>通过第一个函数方法,我们可以得到上下文及项目详细。在第一次使用该扩展创建项目时,会调用该方法。</td></tr><tr class="even"><td>第二个函数方法,我们同样可以获得上下文。在每次用户回到扩展项目时,会调用该方法。</td></tr><tr class="odd"><td>最后一个函数方法。如果用户在扩展项目内,当他们决定切换离开,会调用该函数。通过回调,你可以清理任何正在处理的数据,或是关闭任何让处理器忙碌的任务,或是正在执行的动画。</td></tr><tr class="even"><td>### PHProjectExtensionContext 是什么</td></tr><tr class="odd"><td><imgsrc="https://images.xiaozhuanlan.com/photo/2017/3135a448529f42fa40631795582e9620.jpg" />在<code>PHProjectExtensionContext</code>这个容器里,包含两个非常重要的对象。一个是<code>PHProject</code>,另一个是<code>PHPhotoLibrary</code>。</td></tr><tr class="even"><td>#### PHProject 介绍</td></tr><tr class="odd"><td><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// PHProject.h </span></span><br><span class="line"><span class="comment">// Photos</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PHProject</span> : <span class="title class_">PHAssetCollection</span> { <span class="keyword">var</span> projectExtensionData: <span class="type">Data</span> { <span class="keyword">get</span> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td><code>PHProject</code>本身只是<code>PHAsset</code>的一个<code>子集</code>。在子集中,创建<code>PHProject</code>,只添加了一个非常重要的属性<code>projectExtensionData</code>。可以用于保存任何你需要的数据,它是你正在使用的<code>资产标识符</code>的列表。也许是一些基本的<code>布局信息</code>或<code>配置信息</code>。同时属性<code>projectExtensionData</code>并不是为了照片缓存、缩略图之类的存在。因为这些功能,你可以快速地创建或把它们缓存到其他位置。为了它小而有用,抛开了这些功能,并且<code>projectExtensionData</code>被限制定为<code>1 兆</code>。因为这里面的信息,是一系列的字符串,所以<code>1 兆</code>大小已经足够了。即使用户不断创建项目,也不会增大用户的库。</td></tr><tr class="odd"><td><strong>PHProjectChangeRequest</strong></td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">do</span> {</span><br><span class="line"> <span class="keyword">let</span> changeRequest <span class="operator">=</span> <span class="type">PHProjectChangeRequest</span>(project: <span class="keyword">self</span>.project) </span><br><span class="line"> <span class="keyword">try</span> <span class="keyword">self</span>.library.performChangesAndWait { </span><br><span class="line"> changeRequest.projectExtensionData <span class="operator">=</span> <span class="type">NSKeyedArchiver</span>.archivedData(withRootObject: cloudIdentifiers) </span><br><span class="line"> }} <span class="keyword">catch</span> { </span><br><span class="line"> <span class="built_in">print</span>(<span class="operator">&</span>quot;<span class="type">Failed</span> to save project data: \(error.localizedDescription)<span class="operator">&</span>quot;) </span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>设置数据非常简单。只需按上述实例化,实例化后,可以在<code>Photo Library</code>调用<code>performChangesAndWait</code>函数,在里面将数据设置成任何你想要的样式。</td></tr><tr class="even"><td>### PHProjectInfo 介绍</td></tr><tr class="odd"><td>最高层的<code>ProductInfo</code>分为下面这几个区: <imgsrc="https://images.xiaozhuanlan.com/photo/2017/4780125d766ea2c6d3c52bbf861be9c6.jpg" /></td></tr><tr class="even"><td>当看到这个构造时,可能会问,为什么数组里面还有数组。为什么是这种嵌套结构。但是如果想想“照片”应用里的“回忆”功能,会发现这些是有道理的。</td></tr><tr class="odd"><td>“回忆”本身建立于大量资产之上,允许用户回忆时,可以切换<code>“显示照片摘要”</code>或<code>“显示所有照片”</code>。通过下面的一张图解,可以更清晰地描述为什么使用数组。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/4182042d61242c2f7aca4903592a2f7e.jpg" /></td></tr><tr class="even"><td><code>Section Contents</code>数组是已排序数组。<code>索引为 0</code>的对象是最优内容,是资产集合最精炼的摘要;而<code>数组末端</code>的对象内容是最多的,包含了所有的照片数据。开发过程中,开发者需要根据具体的需求,选择性地使用。</td></tr><tr class="odd"><td>### PHCloudIdentifier 介绍</td></tr><tr class="even"><td><code>PHCloudIdentifier</code>是一个全新的概念。当你想把数据存到用户的<code>Photo Library</code>时,数据可能会被同步到用户其他的设备中。为了确保在保存的数据,合理地同步到其他设备中,iOS11 推出了一个新对象<code>PHCloudIdentifier</code>。</td></tr><tr class="odd"><td><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></pre></td><td class="code"><pre><span class="line"><span class="comment">//获取当前 Cloud Identifiers</span></span><br><span class="line">cloudIdentifiers <span class="operator">+=</span> dataDict.value(forKey: <span class="operator">&</span>quot;contentIdentifiers<span class="operator">&</span>quot;) <span class="keyword">as!</span> [<span class="type">PHCloudIdentifier</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">//转换为 Local Identifiers</span></span><br><span class="line"><span class="keyword">let</span> localIdentifiers <span class="operator">=</span> <span class="keyword">self</span>.library.localIdentifiers(for: cloudIdentifiers)</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>可以将它看做资产的<code>全局标识符</code>,但也并不像全局字符串那么简单,因为还需要处理同步和同步状态等情况。而这些复杂操作,系统已经为我们做了。你必须要做的唯一操作是,在提取之前,确保你的转换总是从<code>全局标识符</code>到<code>本地标识符</code>。可以通过<code>PHPhotoLibrary</code>里的方法来进行双向转换。</td></tr><tr class="odd"><td>### 关于“视图布局”的改进 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/cd2b2461bcd157389c2a743c3f101adc.jpg" /></td></tr><tr class="even"><td>例如上述这种网格布局,对用户来说是很愉快的。如果开发者可以直接访问这个布局,不是会很棒吗?在iOS 11 中,你确实可以进行访问了。</td></tr><tr class="odd"><td>为了支持访问,系统首先确定了一个坐标系。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/38d3b64a70bd944d2fbe210ab2b19bbb.jpg" /></td></tr><tr class="even"><td>如果你查看“回忆”功能,会发现所有内容都被排列在一个由 4x3单元格构成的网络中。但它是一个非正方形尺寸,不利于拓展。所以又有了下面这样的结构。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/b3f9f1c4d4e09f58e24d094f4519abcf.jpg" /></td></tr><tr class="odd"><td>例如上述实例图片的的布局,可以被转化为一个由 20个统一列组成的网络空间。确定了这个坐标系,就可以根据需求任意缩放。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/0ba880f78e54f3f35c44a69ec1b7ec16.png" /></td></tr><tr class="even"><td>并且,通过这个坐标系,也可以和系统互相传递坐标信息。例如上图的坐标为<code>(0, 0, 8 , 9)</code>。</td></tr><tr class="odd"><td>### PHProjectElement 介绍</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// PHProjectElementclass </span></span><br><span class="line"><span class="type">PHProjectElement</span> : <span class="type">NSObject</span>, <span class="type">NSSecureCoding</span> {</span><br><span class="line"> <span class="comment">//权重的范围是 0.0 - 1.0,默认为 0.5 </span></span><br><span class="line"><span class="keyword">var</span> weight: <span class="type">Double</span> { <span class="keyword">get</span> }</span><br><span class="line"> <span class="comment">//元素在网络布局中的坐标 </span></span><br><span class="line"><span class="keyword">var</span> placement: <span class="type">CGRect</span> { <span class="keyword">get</span> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>在<code>Section Content</code>里,系统提供了一组元素。所有元素都是<code>PHProjectElement</code>的子集。这里有两个非常重要的属性:</td></tr><tr class="even"><td>- <code>placement</code>(位置) - <code>weight</code>(权重)</td></tr><tr class="odd"><td><code>“位置”属性</code>,前面已经有所介绍了,这里介绍下<code>“权重”属性</code>。再次回到“回忆”功能,在大量资产中如果想确定最相关的照片,系统需要有自己的评分系统。这里,评分系统通过给每个元素一个权重值,来代表每个元素的重要性。权重值从<code>0.0</code>到<code>1.0</code>,默认值是<code>0.5</code>,也就是说,权重值为<code>0.5</code>的资产代表普通。</td></tr><tr class="even"><td>### RegionsOfInterest 是什么</td></tr><tr class="odd"><td><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> regionsOfInterest: [<span class="type">PHProjectRegionOfInterest</span>] { <span class="keyword">get</span> }</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>这里,有这样的一个概念,称为<code>“兴趣区”</code>。也就是这里的<code>regionsOfInterest</code>属性,这是<code>PHProjectAssetElement</code>所特有的。</td></tr><tr class="odd"><td>在<code>macOS API</code>里,已经有很多方法,可以用来进行<code>面部识别</code>,从而寻找图片中的脸。但从这些方法无法知道这些脸的相关性。来看下面一副示例图:<imgsrc="https://images.xiaozhuanlan.com/photo/2017/477debcd87f3f7339fc7da79b66a2c8b.jpg" /></td></tr><tr class="even"><td>你会注意到,这些脸部有对应的标识符。在不同的照片里相同的脸,会看到被标记为相同的标识符。这样的表示十分有趣。如果你正在处理动画、幻灯片等效果,这将非常有用。因为你现在可以真正把大集合中的图片位置彼此联系起来了。对于体验上的改进来说,这会是一个很棒的属性。</td></tr><tr class="odd"><td>## 总结</td></tr><tr class="even"><td>本节主要介绍了 Photos APIs 的新特性,主要包含了以下几点内容:</td></tr><tr class="odd"><td>- 改进的授权模式 - 大幅优化的 UIImagePickerController -全新的图片格式 HEIF - 大型图片库的创建 - 及为 Photos 创建项目扩展</td></tr><tr class="even"><td>回头来看开篇提出的三点疑问:</td></tr><tr class="odd"><td>- 如何以一种不违反用户信任的方式获取及保存内容到相册? - 是否可以为Photo Library 创建扩展内容? -如何在应用中简单、高效地实现这些操作?</td></tr><tr class="even"><td>这些,在新的 Photos APIs 里都有了相应的解决方案。综上可以看到 Photos也在越来越完善,可扩展性越来越强,功能也在越来越强大。</td></tr><tr class="odd"><td>## 相关资料</td></tr><tr class="even"><td>- <a href="https://developer.apple.com/wwdc17/505">WWDC 2017 Session505 - What's New in Photos APIs</a> - <ahref="https://developer.apple.com/sample-code/wwdc/2017/Creating-Large-Photo-Libraries-for-Testing.zip">WWDC2017 Session 505 - Creating Large Photo Libraries for Testing</a> - <ahref="https://developer.apple.com/documentation/photos">开发者文档 -Photos</a> - <ahref="https://developer.apple.com/documentation/photosui">开发者文档 -PhotosUI</a> - <ahref="https://developer.apple.com/library/content/samplecode/UsingPhotosFramework/Introduction/Intro.html#//apple_ref/doc/uid/TP40014575">PhotoKit示例应用</a></td></tr><tr class="odd"><td>" link_users="{}" data-body="本节要介绍的是 Photos APIs的一些新特性。简单的概括有下面这几点内容:</td></tr><tr class="even"><td>- UIImagePickerController 的大幅优化 - 授权模式的改进 - 动图的支持 -iCloud 照片图库的优化 - 照片项目的扩展</td></tr><tr class="odd"><td>后续内容,会对这几个点依次展开。</td></tr><tr class="even"><td>## UIImagePickerController 的大幅优化</td></tr><tr class="odd"><td><code>UIImagePickerController</code>是系统提供的和相册及相机交互的一个类,通过这个类,你可以在应用中选择照片和视频。在iOS 11 里,图片选择器有了许多的改进和新功能的引入。</td></tr><tr class="even"><td>### 隐私授权的改进</td></tr><tr class="odd"><td>一直以来,Apple十分关注用户的隐私安全。所以,之前在任何情况下,如果获取 Photos资源,都需要获取用户的授权才可以进行。正如下面弹窗这样,请求用户的授权。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/758ef482bd5a71d4ec7170c15a1bcc60.jpg" /></td></tr><tr class="even"><td>正因为授权过程的存在,使得应用程序与用户之间产生了矛盾。对于用户而言,需要他们打开一级隐私,这不是用户想要的;而对于应用程序来说,应用在未获取权限的情况下,无法执行相应的程序和操作,即便它自身有很多优秀的功能,都会因为未授权而无法使用。</td></tr><tr class="odd"><td>在 iOS 11 中,如果通过<code>UIImagePickerController</code>访问相册资源,这个<code>警告弹窗</code>不会再出现,会直接进行程序运行。看到这里,你或许会问:那用户的隐私保护怎么办?</td></tr><tr class="even"><td>首先需要介绍一下<code>UIImagePickerController</code>新的授权模式。自 iOS 11开始,<code>UIImagePickerController</code>成为了一个自动授权API。也就是说,当应用程序要显示 API的内容,将会是从一个<code>自动处理的沙盒</code>和<code>安全环境</code>中获取,应用不再访问用户的<code>Photo Library</code>。</td></tr><tr class="odd"><td>并且,只有用户本人可以和<code>UIImagePickerController UI</code>进行互动。当用户做出一个选择,系统会取出选中的照片或视频,发送到应用中。这样就消除了前面提出的在应用中因为授权而产生的矛盾,同时这也让用户有了更高级别的隐私。因为不存在授权,也就不会再有请求授权。使用起来更为方便了。</td></tr><tr class="even"><td>### 元数据的获取更为方便</td></tr><tr class="odd"><td>Photos拥有丰富的<code>元数据(metadata)</code>,内容包括创建日期、照片的格式,及一些其他不同类型的元数据。在iOS 11 中,获取这些信息变得容易了很多。系统提供了一个全新的键值<code>UIImagePickerControllerImageURL</code>,会包含所有<code>UIImagePickerController</code>的结果。我们可以使用这里的URL,将对应数据读入应用并按照需要进行处理。该 URL是<code>文件 URL</code>,指向一个应用中的<code>临时文件</code>,如果之后想对文件继续操作,建议把文件移动到更永久的文件路径中。</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">imagePickerController</span>(<span class="keyword">_</span> <span class="params">picker</span>: <span class="type">UIImagePickerController</span>, <span class="params">didFinishPickingMediaWithInfo</span> <span class="params">info</span>: [<span class="params">String</span> : <span class="keyword">Any</span>]) { </span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> imageURL <span class="operator">=</span> info[<span class="type">UIImagePickerControllerImageURL</span>] <span class="keyword">as?</span> <span class="type">URL</span> { </span><br><span class="line"> <span class="built_in">print</span>(imageURL)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>### HEIF 图片格式的引入</td></tr><tr class="even"><td>iOS 11 中,Photos引入了一种新的图片格式<code>HEIF</code>。同时,Apple意识到生态系统完全接受<code>HEIF</code>需要一段时间,考虑到新类型图片格式的兼容性。Apple为<code>UIIMagePickerController</code>提供了一个新属性<code>imageExportPresent</code>,让兼容过程变得更为容易。</td></tr><tr class="odd"><td><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="keyword">var</span> imageExportPreset: <span class="type">UIImagePickerControllerImportExportPreset</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> imagePicker <span class="operator">=</span> <span class="type">UIImagePickerController</span>() </span><br><span class="line">imagePicker.imageExportPreset <span class="operator">=</span> .compatible </span><br><span class="line"><span class="keyword">self</span>.present(imagePicker, animated: <span class="literal">true</span>, completion: <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> imagePicker <span class="operator">=</span> <span class="type">UIImagePickerController</span>() </span><br><span class="line">imagePicker.imageExportPreset <span class="operator">=</span> .current </span><br><span class="line"><span class="keyword">self</span>.present(imagePicker, animated: <span class="literal">true</span>, completion: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td><code>imageExportPresent</code>拥有两种类型:</td></tr><tr class="odd"><td>- .compatible (兼容模式) - .current (当前模式)</td></tr><tr class="even"><td>在<code>compatible (兼容模式)</code>下,如果用户选中的源图片是<code>HEIF 格式</code>,系统会通过转换,提供一个JPEG 格式的图片。当然,JPEG是该属性的默认值,如果不需要有什么改变,就不用再做更多的事情。</td></tr><tr class="odd"><td>如果,需要获取的照片格式与拍摄时的格式相同,只需把属性值设为<code>current (当前模式)</code>,这样就会得到与Photo Library 里相同格式的图片,包括<code>HEIF 格式</code>。</td></tr><tr class="even"><td>### 视频文件的获取更为方便</td></tr><tr class="odd"><td>iOS 11中,对视频选择的功能,也有了很好的改进。暂时把这部分内容放在一边,先来简单了解下<code>AVFoundation</code>。<code>AVFoundation</code>是Apple提供的框架,用于<code>丰富编辑</code>及<code>照片播放</code>。通过<code>AVFoundation</code>导出的素材可以拥有丰富的格式。</td></tr><tr class="even"><td>值得称赞的是,<code>UIImagePickerController</code>现在也有了类似的功能,引入了一个新属性<code>videoExportPreset</code>。</td></tr><tr class="odd"><td><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> videoExportPreset: <span class="type">String</span> { <span class="keyword">get</span> <span class="keyword">set</span> }</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>你可以通过这个方法来告诉系统,你所选中的视频需要以哪种格式返回。这样,你就可以轻松得到预设格式的资源内容了。</td></tr><tr class="odd"><td>我们来看一个例子:</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> AVFoundation</span><br><span class="line"><span class="keyword">let</span> imagePicker <span class="operator">=</span> <span class="type">UIImagePickerController</span>() </span><br><span class="line">imagePicker.videoExportPreset <span class="operator">=</span> <span class="type">AVAssetExportPresetHighestQuality</span> <span class="keyword">self</span>.present(imagePicker, animated: <span class="literal">true</span>, completion: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td>如上代码中,首先,导入<code>AVFoundation</code>;接着,创建一个<code>UIIMagePickerController</code>实例,并描述我们想要资源文件以哪种格式返回(这里我们请求的是最高品质);之后显示选择器。</td></tr><tr class="even"><td>当用户做出选择时,无论是什么格式,系统都对其进行交叉编译,得到匹配格式,之后返回给用户。关于可用预设的完整清单,可以通过接口<code>AVAssetExportSession</code>查看。</td></tr><tr class="odd"><td>### 照片和视频的保存有了新的隐私模式</td></tr><tr class="even"><td>前面,通过一些巧妙的设计,在保护用户隐私的情况下,已经实现了无缝选取。实际上,iOS11 也对图片和视频的保存做了很多的优化。</td></tr><tr class="odd"><td>在 iOS 11中,保存一张照片或一段视频到用户的图片库中,系统提供了一个全新的安全模型及权限级别。<code>UIImagePickerController</code>对于保存<code>图片资源</code>及<code>视频资源</code>分别提供了权限级别。一个是<code>UIImageWriteToSavedPhotosAlbum</code>,另一个是<code>UISaveVideoAtPathToSavedPhotosAlbum</code>。</td></tr><tr class="even"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">UIImageWriteToSavedPhotosAlbum</span>(<span class="keyword">_</span> <span class="params">image</span>: <span class="type">UIImage</span>, <span class="keyword">_</span> <span class="params">completionTarget</span>: <span class="keyword">Any</span><span class="operator">?</span>, <span class="keyword">_</span> <span class="params">completionSelector</span>: <span class="type">Selector</span>?, <span class="keyword">_</span> <span class="params">contextInfo</span>: <span class="type">UnsafeMutableRawPointer</span>?)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">UISaveVideoAtPathToSavedPhotosAlbum</span>(<span class="keyword">_</span> <span class="params">videoPath</span>: <span class="type">String</span>, <span class="keyword">_</span> <span class="params">completionTarget</span>: <span class="keyword">Any</span><span class="operator">?</span>, <span class="keyword">_</span> <span class="params">completionSelector</span>: <span class="type">Selector</span>?, <span class="keyword">_</span> <span class="params">contextInfo</span>: <span class="type">UnsafeMutableRawPointer</span>?)</span><br></pre></td></tr></table></figure></td></tr><tr class="odd"><td><img src="./resource/session505/505_add_to_photos.jpg"alt="Add To Your Photos" /></td></tr><tr class="even"><td>这两种方式都只会请求<code>添加授权</code>,对于用户来说<code>添加授权</code>是很小的要求。因为这个权限只允许添加内容到用户的PhotoLibrary,而不涉及到读取权限。所以,很大程度上,用户会愿意给出这个权限。</td></tr><tr class="odd"><td>### PHAsset 获取的改进</td></tr><tr class="even"><td>我们来看一个例子:</td></tr><tr class="odd"><td><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">imagePickerController</span>(<span class="keyword">_</span> <span class="params">picker</span>: <span class="type">UIImagePickerController</span>, <span class="params">didFinishPickingMediaWithInfo</span> <span class="params">info</span>: [<span class="params">String</span> : <span class="keyword">Any</span>]) {</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">let</span> asset <span class="operator">=</span> info[<span class="type">UIImagePickerControllerPHAsset</span>] <span class="keyword">as?</span> <span class="type">PHAsset</span> {</span><br><span class="line"> <span class="built_in">print</span>(asset)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></td></tr><tr class="even"><td>上述代码中,我们实现了一个代理方法。在获得结果词典时,有了一个全新的键,键名为<code>UIIMagePickerControllerPHAsset</code>。取得该键的值,将会得到对应的资产对象,可以对其进行自由使用。</td></tr><tr class="odd"><td>通过这些改变,增强了用户的隐私保护,也让<code>UIIMagePickerController</code>成为更强大而功能齐全的API,满足了市面上大部分应用的需求。然而,有时会出现需要和照片框架进行更深入集成的需求,在这些场景下,Apple推荐使用<code>PhotoKit</code>。</td></tr></tbody></table><h2 id="photokit">PhotoKit</h2><p>和照片相关的应用,一直以来是 App Store里最受欢迎的一类。这一次,<code>PhotoKit</code>做了一些改进,可以让你写出拥有更棒用户体验的新功能。</p><h3 id="live-photo-介绍">Live Photo 介绍</h3><p>首先一起了解下<code>Live Photo</code>的效果。Live Photo效果包含:</p><ul><li><strong>循环效果</strong></li><li><strong>弹跳效果</strong></li><li><strong>长曝光效果等</strong></li></ul><p>其中<code>循环效果</code>,是通过仔细分析视频帧,并无缝地和这些视频帧无止境循环缝合在一起;<code>弹跳效果</code>,它的工作原理和<code>循环效果</code>也是相似的;最后是<code>长曝光效果</code>,它将分析Live Photo 的视频帧,创造令人惊艳的静物。</p><p>现存的<code>PhotoKit</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">PHAssetMediaType</span> : <span class="title class_">Int</span> {</span><br><span class="line"> <span class="keyword">case</span> unknown</span><br><span class="line"> <span class="keyword">case</span> image</span><br><span class="line"> <span class="keyword">case</span> video</span><br><span class="line"> <span class="keyword">case</span> audio</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">struct</span> <span class="title class_">PHAssetMediaSubtype</span> : <span class="title class_">OptionSet</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoPanorama</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoHDR</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoScreenshot</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoLive</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> photoDepthEffect</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> videoStreamed</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> videoHighFrameRate</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">var</span> videoTimelapse</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>如果用户拍摄了一段视频,会想在应用中进行观看,并将拍摄的内容以<code>视频方式</code>使用。如果用户拍摄了一张<code>Live Photo</code>,同样会想要在应用中看到内容以<code>Live Photo</code>的方式呈现。为此,iOS11 提供了三种媒体类型来实现对应目标:</p><ul><li><strong>image</strong></li><li><strong>video</strong></li><li><strong>photoLive</strong></li></ul><p><code>Live Photo</code>效果比较复杂。为此,iOS 11中引入了全新的<code>PHAsset</code>属性<code>playbackStyle</code>,让你可以简单实现<code>Live Photo</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">PHAsset</span> : <span class="title class_">PHObject</span> {</span><br><span class="line"> <span class="keyword">var</span> playbackStyle: <span class="type">PHAssetPlaybackStyle</span> { <span class="keyword">get</span> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">enum</span> <span class="title class_">PHAssetPlaybackStyle</span> : <span class="title class_">Int</span> {</span><br><span class="line"> <span class="keyword">case</span> unsupported</span><br><span class="line"> <span class="keyword">case</span> image</span><br><span class="line"> <span class="keyword">case</span> imageAnimated</span><br><span class="line"> <span class="keyword">case</span> livePhoto</span><br><span class="line"> <span class="keyword">case</span> video</span><br><span class="line"> <span class="keyword">case</span> videoLooping</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>playbackStyle</code>属性,是<code>唯一</code>可以用来查看和决定要使用什么样的图片管理器API、用什么样的视图来表现、以及为该视图设置什么样的 UI 限制。同时,Apple更新了<ahref="https://developer.apple.com/library/content/samplecode/UsingPhotosFramework/Introduction/Intro.html#//apple_ref/doc/uid/TP40014575">PhotoKit示例应用</a>,包含所有这些新的播放风格。这里介绍下其中的三种,它们和前面提到的<code>Live Photo 效果</code>相关。从<code>imageAnimated</code>开始。</p><h4 id="animated-image">Animated Image</h4><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></pre></td><td class="code"><pre><span class="line">imageManager.requestImageData(for: asset, options: options) {</span><br><span class="line"> (data, dataUTI, orientation, info) <span class="keyword">in</span> <span class="comment">// 使用示例项目中的 animatedImageView</span></span><br><span class="line"> <span class="keyword">let</span> animatedImage <span class="operator">=</span> <span class="type">AnimatedImage</span>(data: data)</span><br><span class="line"> animatedImageView.animatedImage <span class="operator">=</span> animatedImage</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>iOS 11有了一个期待已久的新功能。现在,在内置应用“照片”中支持了<code>动画 GIF</code>的播放。如果要在你的应用中播放GIF,只需要从图像管理器请求图像数据,然后使用图像 IO 和 Core Graphics进行播放。接下来是<code>Live Photo</code>。</p><h4 id="live-photo">Live Photo</h4><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></pre></td><td class="code"><pre><span class="line">imageManager.requestLivePhoto(for: asset, targetSize: pixelSize, contentMode: .aspectFill, options: options) {</span><br><span class="line"> (livePhoto, info) <span class="keyword">in</span> <span class="comment">// 使用示例项目中的 PHLivePhotoView</span></span><br><span class="line"> livePhotoView.livePhoto <span class="operator">=</span> livePhoto</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>Live Photos</code>一直很受用户的关注,如何在应用中更好地呈现它们,非常重要,也非常简单。在如上的这个例子里,首先从图像管理器<code>请求</code>一张<code>Live Photo</code>,之后设置<code>PHLivePhotoView</code>。在你的应用里,用户可以通过轻触播放一张<code>Live Photo</code>,正如用户在内置<code>“照片”应用</code>里的操作一样。</p><h4 id="looping-video">Looping Video</h4><figure class="highlight plaintext"><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">swiftimageManager.requestPlayerItem(forVideo: asset, options: options) { </span><br><span class="line"> playerItem, info in</span><br><span class="line"> DispatchQueue.main.async {</span><br><span class="line"> let player = AVQueuePlayer()</span><br><span class="line"> playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)</span><br><span class="line"> playerLayer.player = player</span><br><span class="line"> player.play()</span><br><span class="line"> }}</span><br></pre></td></tr></table></figure><p>在今年所推出的视频循环中,既包括<code>弹跳效果</code>,也包括<code>Live Photo</code>的循环效果。现在,在你的应用里播放这些和播放普通视频非常相似。可以请求播放器项目,并使用<code>AVFoundation</code>播放,还可以使用<code>AVPlayerLooper</code>取得循环效果。可见,表现用户的媒体变得更为轻便,以他们真正想表现的方式,你也可以在自己的应用中,对这些全新的媒体类型,更为创新地表现。</p><h2 id="icloud-照片图库的改进">iCloud 照片图库的改进</h2><p><imgsrc="https://images.xiaozhuanlan.com/photo/2017/fe83154503f1c3e58ce11fbf317bc544.jpg" /></p><p>“iCloud 照片图库”可以与“照片”应用完美搭配使用。当用户开启“iCloud照片图库”时,用户的照片和视频会被安全上传到 iCloud中,同时这些更改会同步到用户的其他设备中。自动上传 iCloud操作的触发条件是,设备连接到 Wi-Fi且电量充足。根据用户的网络情况,在所有设备和 iCloud.com上看到同步照片和视频所需的时间可能会不同。</p><p>使用 iPhone拍照的用户,也常会使用“照片”相关的第三方应用。这些用户,大致可以分为 3类:轻度用户、中度爱好者和重度专业用户。对于重度用户而言,由于自身图库中有很多内容,在第一次使用应用时,需要加载大批量的照片,这个过程中会十分耗时。而且这种耗时的加载状态,会对应用的用户体验大打折扣。</p><p>在 iOS 11中,针对如何更快速高效地操作“大型照片图库”这一点做了优化,后面的内容会依次展开描述。</p><h3id="创建一个用于测试的大型照片图库">创建一个用于测试的“大型照片图库”</h3><p>前面提到,如果图库中有大量内容,在应用加载数据时,会处于长时间的加载状态。而你如果想创建一个拥有大批量图片的图库,还是有些难度的。友好的是,Apple为开发者提供了一个用户创建图库的示例应用:<ahref="https://developer.apple.com/sample-code/wwdc/2017/Creating-Large-Photo-Libraries-for-Testing.zip">PhotoLibraryFiller</a>。下载该应用并安装到测试设备,点击<code>“Add Photos” 按钮</code>,它便会迅速生成一个拥有大批量图片的图库供测试使用。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/bbaf0214e3d8cad2d6e968c56487f529.jpg" /></p><p>到这里,你就拥有了一个可用于测试的<code>“大型照片图库”</code>。</p><h3 id="现在如何从照片图库提取图片">现在,如何从“照片图库”提取图片</h3><p>看下面这段代码:</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> assets <span class="operator">=</span> <span class="type">PHAsset</span>.fetchAssets(with: options)</span><br></pre></td></tr></table></figure><p>这个方法用于从用户的<code>“照片图库”</code>提取图片,等号右侧用于<code>提取资产</code>,左侧为<code>提取结果</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> options <span class="operator">=</span> <span class="type">PHFetchOptions</span>()options.predicate <span class="operator">=</span> <span class="type">NSPredicate</span>(format: <span class="operator">&</span>quot;isFavorite <span class="operator">=</span> <span class="operator">%</span>d<span class="operator">&</span>quot;, <span class="literal">true</span>)</span><br><span class="line">options.sortDescriptors <span class="operator">=</span> [<span class="type">NSSortDescriptor</span>(key: <span class="operator">&</span>quot;creationDate<span class="operator">&</span>quot;, ascending: <span class="literal">true</span>)]</span><br><span class="line"><span class="keyword">let</span> assets <span class="operator">=</span> <span class="type">PHAsset</span>.fetchAssets(with: options)</span><br></pre></td></tr></table></figure><p>在上述代码中,首先提取库里<code>所有的 Asset</code>,并进行筛选,筛选条件为<code>isFavorite = true</code>,之后按照对应的创建日期进行排序。这时,如果在这些自定义提取里<code>发现耗时</code>,那么简化这里的筛选条件会十分必要。不同的筛选方式,可能会意味着查询耗时的巨大差异。造成这种差异的原因是你的操作可能在数据库优化路径之外进行,同时又试图回到优化路径中,这样就产生了不同的耗时差距。</p><p>比这种自定义提取更好的是,尽可能避免这种操作。例如下面这个例子中,我们实际上提取的是用户<code>最喜欢</code>的<code>智能相册</code>。然后在智能相册里<code>提取 Asset</code>。这样既可以使用已有的<code>关键字</code>和<code>排序描述符</code>,还可以保证操作是在数据库优化路径中进行的。</p><figure class="highlight swift"><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 class="keyword">let</span> smartAlbums <span class="operator">=</span> <span class="type">PHAssetCollection</span>.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: <span class="literal">nil</span>)</span><br><span class="line"><span class="keyword">let</span> assets <span class="operator">=</span> <span class="type">PHAsset</span>.fetchAssets(in: smartAlbums.firstObject, options: <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure><p>接着,看下返回结果。返回对象是一个<code>PHFetchResult</code>类型的对象。<code>PHFetchResult 类型</code>非常像一个数列,并且可以像数组一样来使用。但从内部实现来看,<code>PHFetchResult</code>和数列的工作机制还是完全不同的。并且这也是<code>PhotoKit</code>在大型图库操作方面,能够如此快速高效的原因之一。</p><p>我们来看看它的内部工作机制。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/b7383b7c7eddae3304c48d283c7d75ee.jpg" /></p><p>最开始,它只包含一个<code>标识符列表</code>。这意味着可以迅速返回对应的Asset。但开始使用后,有更多工作必须执行。我们以一个枚举作为例子。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/94a0ff75cb87c884023be87268c89b97.jpg" /></p><p>在这里,我们从<code>索引 0 开始</code>枚举。目前只有一个标识符,你还需要从数据库里提取元数据。为此创建一个<code>PHAsset 对象</code>,以便将Asset 的元数据返回给你。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/16d78b56086431e4f054daede150d4d2.jpg" /></p><p>实际上,同时也创建了<code>一个批处理</code>。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/5662e0c527a4b13085762d09fa1cc817.jpg" /></p><p>当我们继续枚举时,<code>索引 1 和 2</code>实际已经在内存中了。枚举继续,它将访问硬盘,获取后续Asset 的元数据。</p><p>在提取结果量级较小的情况下,这样的提取,并不会有太大的影响。但如果提取结果包含<code>10万个 Asset</code>。其中,每一批都需要<code>占用几 kb 内存</code>。如果是<code>10w 批</code>,那将会产生<code>几百兆</code>的内存用量。更糟糕的是,每一批都需要<code>几毫秒</code>的提取时间,如果有<code>10w 批</code>,就需要<code>消耗 10s</code>来枚举这样的一个大型提取结果。所以,应该尽量<code>避免枚举操作</code>。</p><h3 id="在-phfetchresult-中查询-asset-更优的方式">在 PHFetchResult中查询 Asset 更优的方式</h3><p>实际开发过程中,枚举操作总是可能会出现的。这里列举一个例子。现在,你需要从一个提取结果中查询一个<code>Asset 的索引</code>。</p><p>第一种方式,可以通过枚举该提取结果,通过<code>“等于”</code>比较返回的对象,来获取对应<code>Asset 的索引</code>。但是,枚举会非常耗时,所以更好的方法是通过另一种方式,使用<code>高端 API</code>。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/2a142436035188537528e1eaef142487.jpg" /></p><p>如上,通过使用<code>indexOfObject</code>来进行 Asset索引的查询。而<code>indexOfObject</code>方法内部是通过比较“对象标识符”,以找到符合条件的Asset,这样就不会有附加的“硬盘访问”和“数据库提取”。进而避免了第一种方式中,因为枚举出现的耗时操作。同样的,对<code>containtsObject</code>也是如此。</p><h2 id="照片项目的拓展">照片项目的拓展</h2><p>一直以来,Apple 允许用户围绕照片创建丰富的有创意的项目。</p><h3id="phprojectextensioncontroller-的引入">PHProjectExtensionController的引入</h3><p>现在,<code>“照片”</code>中添加了一个<code>新的扩展</code>。对应的,<code>Xcode</code>中也添加了一个<code>新模板</code>,开发者在自己的应用里可以<code>轻松创建</code>这些扩展。此外,<code>“照片”应用</code>会自动发现你的拓展,大大提高了扩展应用被用户知道的概率。不仅如此,Apple为了让这些扩展更容易为用户所发现,给这些扩展应用提供了<code>App Store</code>的直接链接。该链接会打开<code>App Store</code>窗口,并自动显示支持该扩展的应用。</p><p><imgsrc="https://images.xiaozhuanlan.com/photo/2017/756e797b9eced7d408432f876e21c673.jpg" />扩展只存在于开发者的应用内。对于开发者来说,如果你在 App Store已经有了一个关于 Photos相关的应用。此时,就可以将扩展代码移动到该扩展空间内,并加以利用。之后,添加一个视图控制器,并实现<code>PHProjectExtensionController</code>协议。一切就位,“照片应用”便可以发现你的扩展了。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/e3a53f0348049d39207bc4656a78e8fa.jpg" /></p><p>在用户选择“扩展应用”,并用它创建一个项目时,<code>&quot;照片应用&quot;</code>会发送一些字节数据(<code>PHProjectExtensionContext</code>、<code>PHProjectInfo</code>)到对应的<code>“扩展应用”</code>。之后<code>“照片应用”</code>得到相应的返回结果,知道可以安装你的视图控制器。</p><p>过程中遵循的协议本身,对支持的<code>项目类型</code>有一个可选属性,可用于快速描述想让用户选择的选项。</p><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">optional</span> <span class="keyword">public</span> <span class="keyword">var</span> supportedProjectTypes: [<span class="type">PHProjectTypeDescription</span>] { <span class="keyword">get</span> }</span><br></pre></td></tr></table></figure><p>在实际使用过程中,用户既可以选择退出,也可以选择直接进入扩展。这些,在视图控制器里也有特定的函数方法。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">protocol</span> <span class="title class_">PHProjectExtensionController</span> : <span class="title class_">NSObjectProtocol</span> {</span><br><span class="line"> <span class="comment">//第一次使用该扩展创建项目时调用 </span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">beginProject</span>(<span class="params">with</span> <span class="params">extensionContext</span>: <span class="type">PHProjectExtensionContext</span>, </span><br><span class="line"> <span class="params">projectInfo</span>: <span class="type">PHProjectInfo</span>, <span class="params">completion</span>: <span class="keyword">@escaping</span> (<span class="type">Error</span>?) -> <span class="type">Void</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//每次用户回到以前创建的项目时调用</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">resumeProject</span>(<span class="params">with</span> <span class="params">extensionContext</span>: <span class="type">PHProjectExtensionContext</span>, </span><br><span class="line"> <span class="params">completion</span>: <span class="keyword">@escaping</span> (<span class="type">Error</span>?) -> <span class="type">Void</span>)</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//用户离开项目时调用 </span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">func</span> <span class="title function_">finishProject</span>(<span class="params">completionHandler</span> <span class="params">completion</span>: <span class="keyword">@escaping</span> () -> <span class="type">Void</span>) </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过第一个函数方法,我们可以得到上下文及项目详细。在第一次使用该扩展创建项目时,会调用该方法。</p><p>第二个函数方法,我们同样可以获得上下文。在每次用户回到扩展项目时,会调用该方法。</p><p>最后一个函数方法。如果用户在扩展项目内,当他们决定切换离开,会调用该函数。通过回调,你可以清理任何正在处理的数据,或是关闭任何让处理器忙碌的任务,或是正在执行的动画。</p><h3 id="phprojectextensioncontext-是什么">PHProjectExtensionContext是什么</h3><p><imgsrc="https://images.xiaozhuanlan.com/photo/2017/3135a448529f42fa40631795582e9620.jpg" />在<code>PHProjectExtensionContext</code>这个容器里,包含两个非常重要的对象。一个是<code>PHProject</code>,另一个是<code>PHPhotoLibrary</code>。</p><h4 id="phproject-介绍">PHProject 介绍</h4><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// PHProject.h </span></span><br><span class="line"><span class="comment">// Photos</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PHProject</span> : <span class="title class_">PHAssetCollection</span> { <span class="keyword">var</span> projectExtensionData: <span class="type">Data</span> { <span class="keyword">get</span> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>PHProject</code>本身只是<code>PHAsset</code>的一个<code>子集</code>。在子集中,创建<code>PHProject</code>,只添加了一个非常重要的属性<code>projectExtensionData</code>。可以用于保存任何你需要的数据,它是你正在使用的<code>资产标识符</code>的列表。也许是一些基本的<code>布局信息</code>或<code>配置信息</code>。同时属性<code>projectExtensionData</code>并不是为了照片缓存、缩略图之类的存在。因为这些功能,你可以快速地创建或把它们缓存到其他位置。为了它小而有用,抛开了这些功能,并且<code>projectExtensionData</code>被限制定为<code>1 兆</code>。因为这里面的信息,是一系列的字符串,所以<code>1 兆</code>大小已经足够了。即使用户不断创建项目,也不会增大用户的库。</p><p><strong>PHProjectChangeRequest</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">do</span> {</span><br><span class="line"> <span class="keyword">let</span> changeRequest <span class="operator">=</span> <span class="type">PHProjectChangeRequest</span>(project: <span class="keyword">self</span>.project) </span><br><span class="line"> <span class="keyword">try</span> <span class="keyword">self</span>.library.performChangesAndWait { </span><br><span class="line"> changeRequest.projectExtensionData <span class="operator">=</span> <span class="type">NSKeyedArchiver</span>.archivedData(withRootObject: cloudIdentifiers) </span><br><span class="line"> }} <span class="keyword">catch</span> { </span><br><span class="line"> <span class="built_in">print</span>(<span class="operator">&</span>quot;<span class="type">Failed</span> to save project data: \(error.localizedDescription)<span class="operator">&</span>quot;) </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>设置数据非常简单。只需按上述实例化,实例化后,可以在<code>Photo Library</code>调用<code>performChangesAndWait</code>函数,在里面将数据设置成任何你想要的样式。</p><h3 id="phprojectinfo-介绍">PHProjectInfo 介绍</h3><p>最高层的<code>ProductInfo</code>分为下面这几个区: <imgsrc="https://images.xiaozhuanlan.com/photo/2017/4780125d766ea2c6d3c52bbf861be9c6.jpg" /></p><p>当看到这个构造时,可能会问,为什么数组里面还有数组。为什么是这种嵌套结构。但是如果想想“照片”应用里的“回忆”功能,会发现这些是有道理的。</p><p>“回忆”本身建立于大量资产之上,允许用户回忆时,可以切换<code>“显示照片摘要”</code>或<code>“显示所有照片”</code>。通过下面的一张图解,可以更清晰地描述为什么使用数组。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/4182042d61242c2f7aca4903592a2f7e.jpg" /></p><p><code>Section Contents</code>数组是已排序数组。<code>索引为 0</code>的对象是最优内容,是资产集合最精炼的摘要;而<code>数组末端</code>的对象内容是最多的,包含了所有的照片数据。开发过程中,开发者需要根据具体的需求,选择性地使用。</p><h3 id="phcloudidentifier-介绍">PHCloudIdentifier 介绍</h3><p><code>PHCloudIdentifier</code>是一个全新的概念。当你想把数据存到用户的<code>Photo Library</code>时,数据可能会被同步到用户其他的设备中。为了确保在保存的数据,合理地同步到其他设备中,iOS11 推出了一个新对象<code>PHCloudIdentifier</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></pre></td><td class="code"><pre><span class="line"><span class="comment">//获取当前 Cloud Identifiers</span></span><br><span class="line">cloudIdentifiers <span class="operator">+=</span> dataDict.value(forKey: <span class="operator">&</span>quot;contentIdentifiers<span class="operator">&</span>quot;) <span class="keyword">as!</span> [<span class="type">PHCloudIdentifier</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">//转换为 Local Identifiers</span></span><br><span class="line"><span class="keyword">let</span> localIdentifiers <span class="operator">=</span> <span class="keyword">self</span>.library.localIdentifiers(for: cloudIdentifiers)</span><br></pre></td></tr></table></figure><p>可以将它看做资产的<code>全局标识符</code>,但也并不像全局字符串那么简单,因为还需要处理同步和同步状态等情况。而这些复杂操作,系统已经为我们做了。你必须要做的唯一操作是,在提取之前,确保你的转换总是从<code>全局标识符</code>到<code>本地标识符</code>。可以通过<code>PHPhotoLibrary</code>里的方法来进行双向转换。</p><h3 id="关于视图布局的改进">关于“视图布局”的改进</h3><p><imgsrc="https://images.xiaozhuanlan.com/photo/2017/cd2b2461bcd157389c2a743c3f101adc.jpg" /></p><p>例如上述这种网格布局,对用户来说是很愉快的。如果开发者可以直接访问这个布局,不是会很棒吗?在iOS 11 中,你确实可以进行访问了。</p><p>为了支持访问,系统首先确定了一个坐标系。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/38d3b64a70bd944d2fbe210ab2b19bbb.jpg" /></p><p>如果你查看“回忆”功能,会发现所有内容都被排列在一个由 4x3单元格构成的网络中。但它是一个非正方形尺寸,不利于拓展。所以又有了下面这样的结构。<imgsrc="https://images.xiaozhuanlan.com/photo/2017/b3f9f1c4d4e09f58e24d094f4519abcf.jpg" /></p><p>例如上述实例图片的的布局,可以被转化为一个由 20个统一列组成的网络空间。确定了这个坐标系,就可以根据需求任意缩放。 <imgsrc="https://images.xiaozhuanlan.com/photo/2017/0ba880f78e54f3f35c44a69ec1b7ec16.png" /></p><p>并且,通过这个坐标系,也可以和系统互相传递坐标信息。例如上图的坐标为<code>(0, 0, 8 , 9)</code>。</p><h3 id="phprojectelement-介绍">PHProjectElement 介绍</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// PHProjectElementclass </span></span><br><span class="line"><span class="type">PHProjectElement</span> : <span class="type">NSObject</span>, <span class="type">NSSecureCoding</span> {</span><br><span class="line"> <span class="comment">//权重的范围是 0.0 - 1.0,默认为 0.5 </span></span><br><span class="line"><span class="keyword">var</span> weight: <span class="type">Double</span> { <span class="keyword">get</span> }</span><br><span class="line"> <span class="comment">//元素在网络布局中的坐标 </span></span><br><span class="line"><span class="keyword">var</span> placement: <span class="type">CGRect</span> { <span class="keyword">get</span> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在<code>Section Content</code>里,系统提供了一组元素。所有元素都是<code>PHProjectElement</code>的子集。这里有两个非常重要的属性:</p><ul><li><code>placement</code>(位置)</li><li><code>weight</code>(权重)</li></ul><p><code>“位置”属性</code>,前面已经有所介绍了,这里介绍下<code>“权重”属性</code>。再次回到“回忆”功能,在大量资产中如果想确定最相关的照片,系统需要有自己的评分系统。这里,评分系统通过给每个元素一个权重值,来代表每个元素的重要性。权重值从<code>0.0</code>到<code>1.0</code>,默认值是<code>0.5</code>,也就是说,权重值为<code>0.5</code>的资产代表普通。</p><h3 id="regionsofinterest-是什么">RegionsOfInterest 是什么</h3><figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> regionsOfInterest: [<span class="type">PHProjectRegionOfInterest</span>] { <span class="keyword">get</span> }</span><br></pre></td></tr></table></figure><p>这里,有这样的一个概念,称为<code>“兴趣区”</code>。也就是这里的<code>regionsOfInterest</code>属性,这是<code>PHProjectAssetElement</code>所特有的。</p><p>在<code>macOS API</code>里,已经有很多方法,可以用来进行<code>面部识别</code>,从而寻找图片中的脸。但从这些方法无法知道这些脸的相关性。来看下面一副示例图:<imgsrc="https://images.xiaozhuanlan.com/photo/2017/477debcd87f3f7339fc7da79b66a2c8b.jpg" /></p><p>你会注意到,这些脸部有对应的标识符。在不同的照片里相同的脸,会看到被标记为相同的标识符。这样的表示十分有趣。如果你正在处理动画、幻灯片等效果,这将非常有用。因为你现在可以真正把大集合中的图片位置彼此联系起来了。对于体验上的改进来说,这会是一个很棒的属性。</p><h2 id="总结">总结</h2><p>本节主要介绍了 Photos APIs 的新特性,主要包含了以下几点内容:</p><ul><li>改进的授权模式</li><li>大幅优化的 UIImagePickerController</li><li>全新的图片格式 HEIF</li><li>大型图片库的创建</li><li>及为 Photos 创建项目扩展</li></ul><p>回头来看开篇提出的三点疑问:</p><ul><li>如何以一种不违反用户信任的方式获取及保存内容到相册?</li><li>是否可以为 Photo Library 创建扩展内容?</li><li>如何在应用中简单、高效地实现这些操作?</li></ul><p>这些,在新的 Photos APIs 里都有了相应的解决方案。综上可以看到 Photos也在越来越完善,可扩展性越来越强,功能也在越来越强大。</p><h2 id="相关资料">相关资料</h2><ul><li><a href="https://developer.apple.com/wwdc17/505">WWDC 2017 Session505 - What's New in Photos APIs</a></li><li><ahref="https://developer.apple.com/sample-code/wwdc/2017/Creating-Large-Photo-Libraries-for-Testing.zip">WWDC2017 Session 505 - Creating Large Photo Libraries for Testing</a></li><li><ahref="https://developer.apple.com/documentation/photos">开发者文档 -Photos</a></li><li><ahref="https://developer.apple.com/documentation/photosui">开发者文档 -PhotosUI</a></li><li><ahref="https://developer.apple.com/library/content/samplecode/UsingPhotosFramework/Introduction/Intro.html#//apple_ref/doc/uid/TP40014575">PhotoKit示例应用</a></li></ul>]]></content>
<summary type="html"><p>视频链接:<a href="https://developer.apple.com/wwdc17/505">WWDC 2017
Session 505 - What's New in Photos APIs</a></p>
<p>本节要介绍的是 Photos APIs
的一些新特性。简单的概括有下面这几点内容:</p>
<ul>
<li>UIImagePickerController 的大幅优化</li>
<li>授权模式的改进</li>
<li>动图的支持</li>
<li>iCloud 照片图库的优化</li>
<li>照片项目的扩展</li>
</ul>
<p>后续内容,会对这几个点依次展开。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="WWDC" scheme="http://example.com/categories/WWDC/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="WWDC" scheme="http://example.com/tags/WWDC/"/>
</entry>
<entry>
<title>HTTP 和 HTTPS</title>
<link href="http://example.com/2016/05/15/2016-05-15-http-and-https/"/>
<id>http://example.com/2016/05/15/2016-05-15-http-and-https/</id>
<published>2016-05-14T16:00:00.000Z</published>
<updated>2016-05-14T16:00:00.000Z</updated>
<content type="html"><![CDATA[<span id="more"></span><h2 id="osi-七层模型">OSI 七层模型</h2><p>OSI(Open System Interconnection),由底层到高层分别为: - 物理层 -数据链路层 - 网络层 - 传输层:TCP/UDP - 会话层 - 表示层 -应用层:HTTP</p><h2 id="tcpudp">TCP/UDP</h2><h3 id="tcp">TCP</h3><p>TCP中连接的建立需要三次握手,在通信结束时断开连接需要四次挥手。一个连接的建立与断开,正常过程至少需要来回送7 个包才能完成。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/11332a114eb6d7b5c5d4.png/UsurEAoeOC86mJW.png" /></p><p>建立 TCP 连接时的三次握手: 1. 客户端向服务器发送一个SYN(synchronous),客户端进入 SYN_SEND 状态 2. 服务器收到 SYN包后,服务器进入 SYN_RECV 状态,发出 SYN+ACK(Acknowledgement) 3.客户端收到 SYN+ACK 后发出 ACK 确认给服务器,客户端进入 ESTABLISH 状态。4. 服务器收到 ACK 后,服务器进入 ESTABLISH 状态。</p><p>建立连接后,传输数据。</p><p>断开 TCP 连接时的四次挥手: 1. 客户端发送发送一个 FIN,等待服务器返回ACK 和 FIN,客户端进入 FIN_WAIT_1 状态; 2. 服务器接收 FIN,发出一个收到FIN 的 ACK 确认,服务器进入 Close Wait 状态; 3. 客户端收到ACK,继续等待服务器的 FIN,客户端进入 FIN_WAIT_2 状态; 4. 服务器发送FIN,服务器等待客户端收到 FIN 的 ACK,服务器进入 LAST_ACK 状态; 5.客户端收到 FIN,发出 ACK,客户端进入 TIME_WAIT状态(2MSL等待状态);等到 2MSL 后,客户端进入 CLOSE 状态 6. 服务器接收ACK,服务器进入 CLOSE 状态;</p><p>连接断开。</p><h3 id="udp">UDP</h3><p>UDP 不提供复杂的控制机制,利用 IP提供面向无连接的通信服务。并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。</p><p>特点:</p><ul><li>无需建立连接(减少延迟)</li><li>实现简单:无需维护连接状态</li><li>头部开销小</li><li>没有拥塞控制:应用可以更好的控制发送时间和发送速率</li></ul><h3 id="tcp-与-udp-的区别">TCP 与 UDP 的区别</h3><ol type="1"><li>TCP 面向连接;UDP 是无连接的,即发送数据之前不需要建立连接;</li><li>TCP 提供可靠的服务。即通过 TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付</li><li>TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP是面向报文的,没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP 电话,实时视频会议等)</li><li>每一条 TCP 连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信</li><li>TCP首部开销 20 字节;UDP 的首部开销小,只有 8 个字节</li></ol><h2 id="socket">Socket</h2><p>Socket 是对 TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用 TCP/IP 协议。</p><p>在 socket 编程中,客户端执行 connect() 时,将触发三次握手。在 socket编程中,任何一方执行 close() 操作即可产生挥手操作。</p><p>Socket 连接与 HTTP 连接的不同。通常情况下 Socket 连接是 TCP连接,因此 Socket连接一旦建立,通信双方即可开始相互发送数据内容,直到双方连接断开。但实际应用中,客户端到服务器之间的通信防火墙默认会关闭长时间处于非活跃状态的连接而导致Socket 连接断开,因此需要通过轮询告诉网络,该连接处于活跃状态。</p><p>而 HTTP连接使用的是“请求—响应”的方式,不仅在请求时需要先建立连接,而且需要客户端向服务器发出请求后,服务器端才能回复数据。</p><h3 id="长连接">长连接</h3><p>在 TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。</p><ul><li>连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接</li></ul><p>长连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。</p><p>每个 TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完成后都不断开,下次处理时直接发送数据包就可以了,不用建立TCP 连接。</p><h3 id="短连接">短连接</h3><p>指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接</p><ul><li>连接→数据传输→关闭连接;</li></ul><h2 id="http">HTTP</h2><p>一次完整的 HTTP 请求过程:</p><ul><li>从 TCP 三次握手建立连接成功后开始</li><li>客户端按指定格式向服务端发送 HTTP 请求</li><li>服务端接受请求后,解析 HTTP 请求,处理完业务逻辑,返回一个 HTTP响应给客户端</li><li>HTTP 的响应内容有标准的格式</li></ul><h3 id="http-请求报文">HTTP 请求报文</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/bdcb5d58559fad8a24d4.png/DNA76fYjoFuC3el.png" /><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/f2e407c85dd06c836e8c.png/WOGMoCq6tyj71kX.png" /></p><p>一个 HTTP 请求报文组成部分有:</p><ul><li>请求行:请求方法(GET/POST/DELETE/PUT/HEAD)、URL 路径、HTTP的版本号</li><li>请求头部:缓存、客户端信息等</li><li>空行</li><li>请求数据</li></ul><p>请求方法: - HTTP 1.0 定义的方法: -GET:请求指定的页面信息,并返回实体主体。 -HEAD:类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头。 -POST:向指定资源提交数据进行处理请求(修改)。数据被包含在请求体中 -HTTP 1.1 新增的方法: -PUT:从客户端向服务器传送的数据取代指定的文档的内容。 -Delete:请求服务器删除指定的页面。 -TRACE:回显服务器收到的请求,主要用于测试或诊断。 -CONNECT:预留给能够将连接改为管道方式的代理服务器。 -OPTIONS:允许客户端查看服务器的性能。</p><h3 id="http-返回报文">HTTP 返回报文</h3><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/8365a9ab4504be41f575.png/NidcMRQ8HxVKABU.png" /><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/cb38b579473bb8dc288b.png/cdZk9LuOW2fr8Sp.png" /></p><p>一个 HTTP 返回报文组成部分有:</p><ul><li>状态行:有 HTTP 协议版本号,状态码和状态说明</li><li>响应头部</li><li>空行</li><li>响应包体</li></ul><p>状态码: <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/7194f156a6c5fc9bbc3f.png/eKlhyENZY1tCPVb.png" /></p><ul><li>200:请求成功</li><li>301:被请求的资源已永久移动到新位置。服务器返回此响应(GET 或 HEAD请求的响应)时,会自动将请求者转到新位置。</li><li>404:没有找到</li><li>405:方法不允许</li><li>408:请求超时</li><li>500:服务器内部错误</li></ul><h3 id="http-长连接">HTTP 长连接</h3><p>HTTP1.1 开始,默认 TCP保持长连接,即任意一端没有提出断开连接,则会一直保持连接状态。一次长连接可进行多次请求和响应,这样可以减少建议连接和断开连接的开销,减少服务器负载,加快HTTP 请求和响应。</p><p><imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/e074d2c95e74520559c0.png/pf9a1cZ3PlQU2SA.png" /></p><h3 id="cookies">Cookies</h3><p>HTTP 是无状态协议,但有时客户端与服务端需要保持某些状态,于是引入Cookie 技术。Cookie 是通过在请求和响应报文中写入 Cookie信息来控制客户端状态。</p><p>Cookie 根据从服务端发送的响应报文中的一个 Set-Cookie头部信息,通知客户端来保持Cookie。当下次客户端再往服务器发送请求时,客户端会自动在请求报文中加入Cookie 后发送出去。</p><p>服务端发现客户端发送来的 Cookie后,会去检查这是从哪个客户端发来的连接请求,对比服务器记录,得到状态信息。</p><h3 id="session">Session</h3><p>在服务端保持状态的方案。</p><h2 id="https">HTTPS</h2><p>HTTPS 从最终的数据解析角度,与 HTTP 没有任何的区别。HTTPS 是将 HTTP协议数据包放到 SSL/TSL层(应用层)加密后,在 TCP/IP 层组成 IP数据包去传输,以保证传输数据的安全。</p><p>对于接收端,在 SSL/TSL 将接收的数据包解密后,将数据传给 HTTP协议层,就变成了普通的 HTTP 数据。其中 HTTP 和 SSL/TSL 都处于 OSI模型的应用层。</p><h3 id="http-的不足">HTTP 的不足</h3><ul><li>通信使用明文,内容容易被窃听</li><li>不验证通信双方的身份,有可能遭遇伪装</li><li>无法证明报文的完整性,有可能遭到篡改</li></ul><h3 id="对称加密和非对称加密">对称加密和非对称加密</h3><p>对称加密:在加密和解密时使用同一个秘钥。</p><p>非对称加密:需要一对公钥和私钥,如果通过公钥对数据进行加密,只能通过对应的私钥解密;如果通过私钥对数据进行加密,则只能通过对应的公钥来解密。</p><h3 id="ssltsl">SSL/TSL</h3><p>主要交换三个信息:</p><h4 id="数字证书">数字证书</h4><p>该证书包含了公钥等信息,一般由服务器发送给客户端,接收方通过验证这个证书是不是有信赖的CA签发,或与本地的证书相对比来判断证书是否可信。如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证。</p><h4 id="三个随机数">三个随机数</h4><p>这三个随机数构成了后续通信过程中用来对数据进行对称加密解密的<strong>对话秘钥</strong>。</p><ul><li>先从客户端发送第一个随机数 N1</li><li>之后服务器返回第二个N2(同时发送证书给客户端),两个随机数都是明文的;</li><li>而第三个随机数N3,是客户端通过数字证书的公钥进行非对称加密得到的,发送给服务端。</li><li>服务端通过自己的私钥解密得到 N3</li><li>这是服务端和客户端都有个三个随机数N1+N2+N3,两端通过这三个随机数来生成<strong>对话秘钥</strong>,之后的通信使用这个对话密钥来进行对称加密解密</li><li>过程中服务端的私钥始终在服务端,没有经历过网络传输,这样私钥不会被泄露,保证了数据的安全</li></ul><p>完整过程如下图: <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/26a2c65b303afa3cbd0b.png/MRnzO1blgIf3Qx2.png" /></p><h4 id="加密通信协议">加密通信协议</h4><p>客户端和服务端商量使用哪一种加密方式,若两者支持的加密方式不匹配,则无法进行通信。</p><p>为什么随机数要有三个?</p><p>由于 SSL/TSL的设计,假设某个客户端提供的随机数不随机,会大大增加对话密钥被破解的风险,而通过3个随机数得到的对话密钥,很大程度上保证了随机数的随机性,以此来保证生成的对话密钥的安全性。</p><h3 id="https-抓包原理">HTTPS 抓包原理</h3><ol type="1"><li>抓包程序将服务器返回的证书截获</li><li>之后给客户端返回一个抓包程序的证书</li><li>客户端发送的数据使用抓包程序给的证书生成的密钥加密</li><li>抓包程序得到客户端发送的数据,抓包程序用自己的证书解密出来,再用服务器证书加密</li><li>抓包程序再把数据发送给服务器</li></ol><h2 id="总结">总结</h2><ul><li>OSI 七层模型</li><li>TCP 3 次握手(SYN + ACK)</li><li>TCP 4 次挥手(FIN + ACK)</li><li>UDP(一对多、不需要建立连接)</li><li>Socket(对 TCP/IP 的封装、connect()、close()、轮询保持活跃)</li><li>HTTP<ul><li>TCP 握手建立连接</li><li>HTTP 请求</li><li>返回 HTTP 响应</li><li>HTTP 请求报文<ul><li>请求行(请求方法、URL、HTTP 版本等)</li><li>请求头(缓存、客户端信息等)</li><li>空行</li><li>请求体</li></ul></li><li>请求方法:<ul><li>HTTP 1.0<ul><li>GET</li><li>HEAD</li><li>POST</li></ul></li><li>HTTP 1.1<ul><li>PUT</li><li>DELETE</li><li>CONNECT</li><li>OPTIONS</li></ul></li></ul></li><li>HTTP 返回报文<ul><li>状态行(HTTP 版本、状态码、状态说明)</li><li>响应头</li><li>空行</li><li>响应体</li></ul></li><li>状态码:<ul><li>200</li><li>301 新位置</li><li>404 没找到</li><li>405 方法不允许</li><li>408 请求超时</li><li>500 服务器内部错误</li></ul></li><li>cookie & session<ul><li>HTTP 无状态</li><li>Set-Cookie 开启</li><li>客户端发请求带 cookie</li><li>服务器检查 cookie</li></ul></li></ul></li><li>HTTPS<ul><li>HTTP 不足<ul><li>明文,窃听</li><li>不验证身份,伪装</li><li>不保证报文完整,篡改</li></ul></li><li>原理<ul><li>通过 SSL/TSL(应用层)加密 HTTP 协议数据包</li><li>再用 TCP/IP 组成 IP 数据包传输</li><li>除了加密解密过程,其他和 HTTP 一样</li></ul></li><li>SSL/TSL<ul><li>数字证书</li><li>三个随机数 N1、N2(+公钥)、N3</li><li>加密通信协议确定</li></ul></li></ul></li><li>HTTPS 抓包原理<ul><li>抓包程序截获服务器证书</li><li>给客户端自己的证书</li><li>客户端按该证书公钥加密</li><li>抓包软件收到客户端加密数据,自己解密</li><li>再用服务器证书加密</li><li>发给服务器</li></ul></li></ul>]]></content>
<summary type="html"><span id="more"></span>
<h2 id="osi-七层模型">OSI 七层模型</h2>
<p>OSI(Open System Interconnection),由底层到高层分别为: - 物理层 -
数据链路层 - 网络层 - 传输层:TCP/UDP - 会</summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
</entry>
<entry>
<title>Objc Tips</title>
<link href="http://example.com/2016/04/03/2016-04-03-objc-tips/"/>
<id>http://example.com/2016/04/03/2016-04-03-objc-tips/</id>
<published>2016-04-02T16:00:00.000Z</published>
<updated>2016-04-02T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>总结记录 Objective-C 使用过程中一些 Tips。</p><span id="more"></span><h2 id="ns_enum-和-ns_options">NS_ENUM 和 NS_OPTIONS</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="built_in">NS_ENUM</span>(<span class="built_in">NSInteger</span>, <span class="built_in">UIViewAnimationCurve</span>) {</span><br><span class="line"> <span class="built_in">UIViewAnimationCurveEaseInOut</span>, <span class="comment">// slow at beginning and end</span></span><br><span class="line"> <span class="built_in">UIViewAnimationCurveEaseIn</span>, <span class="comment">// slow at beginning</span></span><br><span class="line"> <span class="built_in">UIViewAnimationCurveEaseOut</span>, <span class="comment">// slow at end</span></span><br><span class="line"> <span class="built_in">UIViewAnimationCurveLinear</span>,</span><br><span class="line">};</span><br></pre></td></tr></table></figure><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="keyword">typedef</span> <span class="built_in">NS_OPTIONS</span>(<span class="built_in">NSUInteger</span>, <span class="built_in">UIViewAutoresizing</span>) {</span><br><span class="line"> <span class="built_in">UIViewAutoresizingNone</span> = <span class="number">0</span>,</span><br><span class="line"> <span class="built_in">UIViewAutoresizingFlexibleLeftMargin</span> = <span class="number">1</span> << <span class="number">0</span>,</span><br><span class="line"> <span class="built_in">UIViewAutoresizingFlexibleWidth</span> = <span class="number">1</span> << <span class="number">1</span>,</span><br><span class="line"> <span class="built_in">UIViewAutoresizingFlexibleRightMargin</span> = <span class="number">1</span> << <span class="number">2</span>,</span><br><span class="line"> <span class="built_in">UIViewAutoresizingFlexibleTopMargin</span> = <span class="number">1</span> << <span class="number">3</span>,</span><br><span class="line"> <span class="built_in">UIViewAutoresizingFlexibleHeight</span> = <span class="number">1</span> << <span class="number">4</span>,</span><br><span class="line"> <span class="built_in">UIViewAutoresizingFlexibleBottomMargin</span> = <span class="number">1</span> << <span class="number">5</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="comment">// 用法</span></span><br><span class="line"><span class="built_in">UIViewAutoresizing</span> resizing = <span class="built_in">UIViewAutoresizingFlexibleWidth</span> | <span class="built_in">UIViewAutoresizingFlexibleHeight</span>;</span><br><span class="line"><span class="comment">// 转换为二进制计算</span></span><br><span class="line"><span class="built_in">UIViewAutoresizing</span> resizing = <span class="number">000010</span> | <span class="number">010000</span> = <span class="number">010010</span></span><br><span class="line"><span class="comment">// 通过 & 判断是否满足条件之一</span></span><br><span class="line"><span class="comment">// resizing & UIViewAutoresizingFlexibleWidth </span></span><br><span class="line"><span class="comment">// -> 010010 & 000010 = 000010</span></span><br><span class="line"><span class="keyword">if</span> (resizing & <span class="built_in">UIViewAutoresizingFlexibleWidth</span>) {</span><br><span class="line"> <span class="comment">// UIViewAutoresizingFlexibleWidth is set</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>NS_OPTIONS</code>可以同时选择多个枚举值,使用了移位运算来保证相加结果的唯一性。简而言之,<code>NS_ENUM</code>在互斥环境下使用;<code>NS_OPTIONS</code> 在多选情况下使用。</p><h2 id="简介接口设计模板">简介接口设计模板</h2><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="built_in">NS_ENUM</span>(<span class="built_in">NSInteger</span>, UserSex) {</span><br><span class="line"> UserSexMale,</span><br><span class="line"> UserSexFemale</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">DIYUser</span> : <span class="title">NSObject</span><<span class="title">NSCopying</span>></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>, <span class="keyword">copy</span>) <span class="built_in">NSString</span> *name;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readonly</span>, <span class="keyword">assign</span>) <span class="built_in">NSUInteger</span> age;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">readwrite</span>, <span class="keyword">assign</span>) UserSex sex;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithName:(<span class="built_in">NSString</span> *)name age:(<span class="built_in">NSUInteger</span>)age;</span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithName:(<span class="built_in">NSString</span> *)name age:(<span class="built_in">NSUInteger</span>)age sex:(UserSex)sex;</span><br><span class="line"></span><br><span class="line">+ (<span class="keyword">instancetype</span>)userWithName:(<span class="built_in">NSString</span> *)name age:(<span class="built_in">NSUInteger</span>)age sex:(UserSex)sex;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><h2 id="weak-和-assign-对比">weak 和 assign 对比</h2><h3 id="什么情况下使用-weak">什么情况下使用 weak?</h3><ul><li>用来避免循环引用</li><li>OBOutlet 默认为 weak</li></ul><h3 id="与-assign-的区别">与 assign 的区别</h3><ul><li>weak为该属性定义了一种<strong>非拥有关系</strong>,不影响引用计数,在属性所指对象销毁时,属性值也会自动置为nil</li><li>assign 用来修饰基础类型变量(如 CGFloat、NSInteger)</li><li>assign 可用于非 OC 对象,weak 必须用于 OC 对象</li></ul><h3 id="weak-的实现原理">weak 的实现原理</h3><p>weak 有两种作用: - 被 weak修饰符修饰的<strong>弱引用</strong>除了不会增加对象的<strong>引用计数</strong>外-在<strong>引用对象</strong>被释放后,这个<strong>弱引用</strong>会自动失效并置为nil</p><p>实现原理 - 源码入口 objc_initWeak() - 查看对象是否有效,无效置空 -调用 storeWeak - storeWeak - 核心方法<code>weak_unregister_no_lock</code> 和<code>weak_register_no_lock</code> - 都是对 SideTable 的实例进行操作 -SideTable - 内含有 <code>weak_table_t</code>,是 oc 中 weak的核心数据结构 - weak 表通过哈希表实现 -通过<strong>目标对象的地址</strong>作为 key 检索得到对应弱引用变量地址 -需要注意:一个 key 可能对应多个弱引用变量地址,放存放在<code>weak_entry</code> - weak_register_no_lock -先校验是否满足校验条件(计数方式、是否在析构,是否能弱引用) -通过弱引用对象的地址,在 <code>weak_table</code> 查找<code>weak_entry</code> - 找到,在 <code>weak_entry</code> 中的<code>referrers</code> 添加新的弱引用地址 - 若未找到,新建<code>weak_entry</code> 并添加 - 数组中 <code>referrers</code>起始为静态数组,如果在操作过程中发现静态数组空间不够用切换为动态数组,如果动态数组超过总空间3/4,扩容一倍 - weak_unregister_no_lock - 根据对象地址,找出<code>weak_entry</code> - 删除 <code>weak_entry</code> 中的弱引用地址 -如果最后发现 <code>weak_entry</code> 空了,从 weak_table 移除 - 自动置为nil - 底层触发 <code>clearDeallocating</code> 方法 -先校验对象是否满足弱引用 dealloc - 对象 dealloc 时,通过 this指针(对象指针),找到 SideTable。再通过对象地址在 SideTable上找到所有的弱引用指针,逐个置 nil</p><h2 id="atomic-和-nonatomic">atomic 和 nonatomic</h2><p>atomic 属性使用了互斥锁。一般情况下不适用atomic,因为并不能保证线程安全,若要实现线程安全。</p><p>如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,则即使声明为atomic,也还是会读到不同的属性值。</p><h2 id="copy-和-mutablecopy">copy 和 mutableCopy</h2><p>NSString、NSArray、NSDictionary 常用 copy。因为各自对应了可变类型NSMutableString、NSMutableArray、NSMutableDictionary,防止内容在不知情的情况下被更改。所以使用copy 来复制一份不可变的。</p><h3 id="对-nsmutablearray-使用-copy">对 NSMutableArray 使用 copy</h3><p>添加、删除、修改组内的元素时,程序会因为找不到对应的方法而崩溃,原因是copy 复制了一份不可变的 NSArray 对象。错误代码如下:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">copy</span>) <span class="built_in">NSMutableArray</span> *mutableArray;</span><br><span class="line"></span><br><span class="line"><span class="built_in">NSMutableArray</span> *array = [<span class="built_in">NSMutableArray</span> arrayWithObjects:@<span class="number">1</span>, @<span class="number">2</span>, <span class="literal">nil</span>];</span><br><span class="line"><span class="keyword">self</span>.mutableArray = array;</span><br><span class="line">[<span class="keyword">self</span>.mutableArray removeObjectAtIndex:<span class="number">0</span>];</span><br></pre></td></tr></table></figure><h3 id="strong-修饰-nsarray-时">strong 修饰 NSArray 时</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">strong</span>) <span class="built_in">NSArray</span> *array;</span><br><span class="line"></span><br><span class="line"><span class="built_in">NSArray</span> *array = @[ @<span class="number">1</span>, @<span class="number">2</span>, @<span class="number">3</span>, @<span class="number">4</span> ];</span><br><span class="line"><span class="built_in">NSMutableArray</span> *mutableArray = [<span class="built_in">NSMutableArray</span> arrayWithArray:array];</span><br><span class="line"></span><br><span class="line"><span class="keyword">self</span>.array = mutableArray;</span><br><span class="line">[mutableArray removeAllObjects];;</span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"%@"</span>,<span class="keyword">self</span>.array);</span><br><span class="line"></span><br><span class="line">[mutableArray addObjectsFromArray:array];</span><br><span class="line"><span class="keyword">self</span>.array = [mutableArray <span class="keyword">copy</span>];</span><br><span class="line">[mutableArray removeAllObjects];;</span><br><span class="line"><span class="built_in">NSLog</span>(<span class="string">@"%@"</span>,<span class="keyword">self</span>.array);</span><br></pre></td></tr></table></figure><p>打印结果:</p><figure class="highlight text"><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">2016-04-03 00:10:10.765 DIYArrayCopyDemo[10681:713670] (</span><br><span class="line">)</span><br><span class="line">2016-04-03 00:10:10.766 DIYArrayCopyDemo[10681:713670] (</span><br><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">)</span><br></pre></td></tr></table></figure><p>所以 strong 的问题很明显,数据存在被篡改风险。</p><h3 id="对象拷贝">对象拷贝</h3><ul><li>声明该类遵循 NSCopying 协议</li><li>实现 NSCopying 协议方法 <code>-copyWithZone:</code></li></ul><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="type">id</span>)copyWithZone:(<span class="built_in">NSZone</span> *)zone {</span><br><span class="line"> DIYUser *<span class="keyword">copy</span> = [[[<span class="keyword">self</span> <span class="keyword">class</span>] allocWithZone:zone]</span><br><span class="line"> initWithName:_name</span><br><span class="line"> age:_age</span><br><span class="line"> sex:_sex];</span><br><span class="line"> <span class="keyword">copy</span>->_friends = [_friends mutableCopy];</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">copy</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 对象也进行深拷贝</span></span><br><span class="line"><span class="comment">// 写专门的深拷贝方法</span></span><br><span class="line">- (<span class="type">id</span>)deepCopy {</span><br><span class="line"> DIYUser *<span class="keyword">copy</span> = [[[<span class="keyword">self</span> <span class="keyword">class</span>] alloc]</span><br><span class="line"> initWithName:_name</span><br><span class="line"> age:_age</span><br><span class="line"> sex:_sex];</span><br><span class="line"> <span class="keyword">copy</span>->_friends = [[<span class="built_in">NSMutableSet</span> alloc] initWithSet:_friends</span><br><span class="line"> copyItems:<span class="literal">YES</span>];</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">copy</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="集合非集合类对象-copy">集合/非集合类对象 copy</h4><p>非集合类对象: - 不可变对象: - copy:指针拷贝 -mutableCopy:内容拷贝 - 可变对象: - copy:内容拷贝 -mutableCopy:内容拷贝</p><p>集合类对象:NSArray、NSDictionary、NSSet 等 - 不可变对象: -copy:指针拷贝 - mutableCopy:单层深拷贝。(如对 NSArray,是拷贝 array这个对象,但 Array 内部元素任是指针拷贝) - 可变对象: -copy:单层深拷贝 - mutableCopy:单层深拷贝</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">[array <span class="keyword">copy</span>] <span class="comment">//浅拷贝</span></span><br><span class="line">[array mutableCopy] <span class="comment">//单层深拷贝</span></span><br><span class="line">[mutableArray <span class="keyword">copy</span>] <span class="comment">//单层深拷贝</span></span><br><span class="line">[mutableArray mutableCopy] <span class="comment">//单层深拷贝</span></span><br></pre></td></tr></table></figure><h2 id="property"><span class="citation"data-cites="property">@property</span></h2><p><span class="citation" data-cites="property">@property</span> = ivar+ getter + setter,即属性 = 实例变量 + 存取方法。</p><p>完成属性定义后,编译器在编译期会自动编写访问这些属性所需要的方法,即“自动合成”。除了生成getter、setter 外,编译器还会向类中添加适当类型的实例变量,名称为<code>_propertyName</code>。我们也可以通过 <code>@synthesize</code>来指定实例变量的名称。</p><h3 id="property-大致的实现过程"><span class="citation"data-cites="property">@property</span> 大致的实现过程</h3><p>属性的源码实现结构: - 偏移量 - setter、getter 实现函数 -ivar_list:成员变量列表 - method_list:方法列表 -prop_list:属性列表</p><p>实现过程: - 每增加一个属性,系统就会在 ivar_list中添加一个成员变量;在 method_list 中添加对应的 setter 和 getter方法;在 prop_list 中添加一个属性。 -之后计算该属性在对象中的偏移量,给出 setter、getter方法对应的实现。setter 方法中从偏移量位置开始赋值;getter方法中从偏移量开始取值。</p><h3 id="protocol-和-category-如何使用-property"><span class="citation"data-cites="protocol">@protocol</span> 和 category 如何使用 <spanclass="citation" data-cites="property">@property</span></h3><p>protocol 中使用 property 只会生成 setter 和 getter方法声明。我们使用属性的目的是,希望遵循协议的对象能实现该属性。</p><p>category 使用 <span class="citation"data-cites="property">@property</span> 只会生成 setter 和 getter方法的声明。如果我们需要给 category 增加属性的实现,需要借助 runtime函数:</p><ul><li>objc_setAssociatedObject</li><li>objc_getAssociatedObject</li></ul><h3 id="property-的修饰符归类"><span class="citation"data-cites="property">@property</span> 的修饰符归类</h3><ol type="1"><li>原子性<ul><li>atomic</li><li>nonatomic</li></ul></li><li>读写权限<ul><li>readwrite</li><li>readonly</li></ul></li><li>内存管理语义<ul><li>assign</li><li>strong</li><li>weak</li><li>unsafe_unretained</li><li>copy</li></ul></li><li>方法名<ul><li>getter=<name>,如 getter=isOn</li><li>setter=<name></li></ul></li><li>nullable<ul><li>nonnull</li><li>null_resettable</li><li>nullable</li></ul></li><li>ARC 下不显示关键字是,默认为:<ul><li>基本类型<ul><li>atomic</li><li>readwrite</li><li>assign</li></ul></li><li>普通 OC 对象<ul><li>atomic</li><li>readwrite</li><li>strong</li></ul></li></ul></li></ol><h2 id="synthesize-和-dynamic"><span class="citation"data-cites="synthesize">@synthesize</span> 和 <span class="citation"data-cites="dynamic">@dynamic</span></h2><ol type="1"><li><code>@property</code>如果都没写,默认为:<code>@synthesize = _var;</code></li><li><code>@systhesize</code> 语义是,默认让编译器加上 setter 和 getter方法</li><li><code>@dynamic</code>(动态绑定)作用:告诉编译器,属性的setter、getter 方法不自动生成</li></ol><h3 id="什么时候不会-autosynthesis">什么时候不会 autosynthesis?</h3><ul><li>同时重写了 setter 和 getter</li><li>重写了只读属性的 getter</li><li>使用了 <span class="citation"data-cites="dynamic">@dynamic</span></li><li><span class="citation" data-cites="protocol">@protocol</span>中定义的所有属性</li><li>category 中定义的所有属性</li><li>重写的属性</li></ul><p>注意点: - 若子类重写了父类属性,必须用 <span class="citation"data-cites="synthesize">@synthesize</span> 来手动合成 ivar</p>]]></content>
<summary type="html"><p>总结记录 Objective-C 使用过程中一些 Tips。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="Objective-C" scheme="http://example.com/categories/Objective-C/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="Objective-C" scheme="http://example.com/tags/Objective-C/"/>
</entry>
<entry>
<title>KVO 梳理</title>
<link href="http://example.com/2016/03/26/2016-03-26-objc-kvo/"/>
<id>http://example.com/2016/03/26/2016-03-26-objc-kvo/</id>
<published>2016-03-25T16:00:00.000Z</published>
<updated>2016-03-25T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>本篇是对 KVO(Key-Value Observing) 的梳理。内容结构:KVO 基本使用、KVO实现原理、自己实现 KVO。</p><span id="more"></span><h2 id="kvo-基本使用">KVO 基本使用</h2><p>主要的几个方法: -<strong><code>-addObserver:forKeyPath:options:context:</code></strong>:注册观察者,开始监听。-<strong><code>-observeValueForKeyPath:ofObject:change:context:</code></strong>:回调,按需求添加业务代码。-<strong><code>-removeObserver:forKeyPath:</code></strong>:移除观察者。</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><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)viewDidLoad {</span><br><span class="line"> [<span class="variable language_">super</span> viewDidLoad];</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">self</span>.person = [[Person alloc]init];</span><br><span class="line"> <span class="keyword">self</span>.person.age = <span class="number">15</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 注册监听</span></span><br><span class="line"> [<span class="keyword">self</span>.person addObserver:<span class="keyword">self</span> forKeyPath:<span class="string">@"age"</span> options:<span class="built_in">NSKeyValueObservingOptionNew</span> | <span class="built_in">NSKeyValueObservingOptionOld</span> context:<span class="literal">nil</span>];</span><br><span class="line"> <span class="comment">// 值变更</span></span><br><span class="line"> <span class="keyword">self</span>.person.age = <span class="number">20</span>;</span><br><span class="line"> <span class="comment">// 值变更</span></span><br><span class="line"> <span class="keyword">self</span>.person.age = <span class="number">30</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 监听回调</span></span><br><span class="line">- (<span class="type">void</span>)observeValueForKeyPath:(<span class="built_in">NSString</span> *)keyPath</span><br><span class="line"> ofObject:(<span class="type">id</span>)object</span><br><span class="line"> change:(<span class="built_in">NSDictionary</span><<span class="built_in">NSString</span> *,<span class="type">id</span>> *)change</span><br><span class="line"> context:(<span class="type">void</span> *)context {</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"change = %@, keyPath =%@, object =%@, context=%@"</span>, change, keyPath, object, context);</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"属性新值为:%@"</span>,change[<span class="built_in">NSKeyValueChangeNewKey</span>]);</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"属性旧值为:%@"</span>,change[<span class="built_in">NSKeyValueChangeOldKey</span>]);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)dealloc {</span><br><span class="line"><span class="comment">// 移除观察</span></span><br><span class="line"> [<span class="keyword">self</span>.person removeObserver:<span class="keyword">self</span> forKeyPath:<span class="string">@"age"</span>];</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><h2 id="kvo-实现原理">KVO 实现原理</h2><p>KVO 是 Objective-C 中对观察者模式的实现。KVO 的实现依赖于 Objective-C的 Runtime 机制。当某个类的对象第一次被观察时,系统就会在 runtime动态创建该类的一个派生类,在这个派生类中重写原类中任何被观察对象的setter 方法。派生类在被重写的 setter 方法内实现真正的通知机制。</p><p>如现有一 Person类,它对于的派生类会命名为:<strong><code>NSKVONotifying_Person</code></strong>。每个类对象的isa 指针都指向它所属的类,在一个类对象第一次被观察时,系统会将 isa指针指向动态生成的派生类。在给被监控属性赋值时,执行的是派生类的 setter方法。</p><p>KVO 的通知依赖于 NSObject的两个方法:<strong><code>willChangeValueForKey:</code></strong> 和<strong><code>didChangeValueForKey:</code></strong>。被观察属性发生变化时,对应的调用过程如下:</p><ul><li>被改变前,调用<strong><code>willChangeValueForKey:</code></strong>,记录旧值</li><li>改变被观察属性</li><li>被改变后,调用<strong><code>didChangeValueForKey:</code></strong>,记录新值</li><li>接着调用<strong><code>observeValueForKey:ofObject:change:context:</code></strong></li></ul><p>实现原理的过程图: <imgsrc="https://lc-gluttony.s3.amazonaws.com/9zYt4jSanPYX/6c329a73a2e1252840f7.jpg/iTywFhG5VIrmxaj.jpg" /></p><h2 id="kvo-的不足">KVO 的不足</h2><p>KVO很明显的一个问题是,提供了一个单一回调。所有的属性变化,都会通过同一个方法回调。在内部再通过key 来 ifelse判断分别处理。理想的情况是,我们希望对应的属性变化,只触发对应的回调,而不影响其他属性。</p><p>我们可以通过手动实现 KVO 来解决这个问题。</p><h2 id="自己实现-kvo">自己实现 KVO</h2><p>前面我们已经知道 KVO 的实现原理。大致流程为: -当某个类的对象被观察时 - 系统会动态创建一个该类的派生类,派生类superClass 指向原类 - 修改被观察对象的 isa 指针,使其指向派生类 -在派生类中重写 setter 方法,在方法中加入 observer</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></pre></td><td class="code"><pre><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// NSObject+KVO.h</span></span><br><span class="line"><span class="comment">// Demo</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// Created by JonyFang on 2016/3/25.</span></span><br><span class="line"><span class="comment">// Copyright © 2016 JonyFang. All rights reserved.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#import <span class="string"><Foundation/Foundation.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">typedef</span> <span class="type">void</span>(^FFObserverHandler)(<span class="type">id</span> object, <span class="built_in">NSString</span> *key, <span class="type">id</span> oldValue, <span class="type">id</span> newValue);</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSObject</span> (<span class="title">KVO</span>)</span></span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)ff_addObserver:(<span class="built_in">NSObject</span> *)object forKey:(<span class="built_in">NSString</span> *)key withBlock:(FFObserverHandler)handler;</span><br><span class="line">- (<span class="type">void</span>)ff_removeObserver:(<span class="built_in">NSObject</span> *)object forKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure></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><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><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// NSObject+KVO.m</span></span><br><span class="line"><span class="comment">// Demo</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// Created by JonyFang on 2016/3/25.</span></span><br><span class="line"><span class="comment">// Copyright © 2016 JonyFang. All rights reserved.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#import <span class="string">"NSObject+KVO.h"</span></span></span><br><span class="line"><span class="meta">#import <span class="string"><objc/runtime.h></span></span></span><br><span class="line"><span class="meta">#import <span class="string"><objc/message.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *<span class="keyword">const</span> kFFKVOClassPrefix = <span class="string">@"FFObserver_"</span>;</span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *<span class="keyword">const</span> kFFKVOAssociatedObject = <span class="string">@"FFKVOAssociatedObject"</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">FFKVObserver</span> : <span class="title">NSObject</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">weak</span>) <span class="built_in">NSObject</span> *observer;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">copy</span>) <span class="built_in">NSString</span> *key;</span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>, <span class="keyword">copy</span>) FFObserverHandler handler;</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">FFKVObserver</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="keyword">instancetype</span>)initWithObserver:(<span class="built_in">NSObject</span> *)observer forKey:(<span class="built_in">NSString</span> *)key handler:(FFObserverHandler)handler {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">self</span> = [<span class="variable language_">super</span> init]) {</span><br><span class="line"> _observer = observer;</span><br><span class="line"> _key = key;</span><br><span class="line"> _handler = handler;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">self</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// key = propertyName,以 person 为例</span></span><br><span class="line"><span class="comment">// 过程:person -> setPerson:</span></span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *setterForKey(<span class="built_in">NSString</span> *key) {</span><br><span class="line"> <span class="keyword">if</span> (key.length <= <span class="number">0</span>) { <span class="keyword">return</span> <span class="literal">nil</span>; }</span><br><span class="line"> <span class="built_in">NSString</span> *firstStr = [[key substringToIndex:<span class="number">1</span>] uppercaseString];</span><br><span class="line"> <span class="built_in">NSString</span> *leaveStr = [key substringFromIndex:<span class="number">1</span>];</span><br><span class="line"> <span class="keyword">return</span> [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"set%@%@:"</span>, firstStr, leaveStr];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 过程:setPerson: -> person</span></span><br><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> *getterBySetter(<span class="built_in">NSString</span> *<span class="keyword">setter</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">setter</span>.length <= <span class="number">0</span> || ![<span class="keyword">setter</span> hasPrefix: <span class="string">@"set"</span>] || ![<span class="keyword">setter</span> hasSuffix: <span class="string">@":"</span>]) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">NSRange</span> range = <span class="built_in">NSMakeRange</span>(<span class="number">3</span>, <span class="keyword">setter</span>.length - <span class="number">4</span>);</span><br><span class="line"> <span class="built_in">NSString</span> *<span class="keyword">getter</span> = [<span class="keyword">setter</span> substringWithRange: range];</span><br><span class="line"> <span class="built_in">NSString</span> *firstStr = [[<span class="keyword">getter</span> substringToIndex: <span class="number">1</span>] lowercaseString];</span><br><span class="line"> <span class="keyword">getter</span> = [<span class="keyword">getter</span> stringByReplacingCharactersInRange:<span class="built_in">NSMakeRange</span>(<span class="number">0</span>, <span class="number">1</span>) withString: firstStr];</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">getter</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="type">void</span> KVO_Setter(<span class="type">id</span> <span class="keyword">self</span>, SEL _cmd, <span class="type">id</span> newValue) {</span><br><span class="line"> <span class="built_in">NSString</span> *setterName = <span class="built_in">NSStringFromSelector</span>(_cmd);</span><br><span class="line"> <span class="built_in">NSString</span> *getterName = getterBySetter(setterName);</span><br><span class="line"> <span class="keyword">if</span> (!getterName) {</span><br><span class="line"> <span class="keyword">@throw</span> [<span class="built_in">NSException</span> exceptionWithName: <span class="built_in">NSInvalidArgumentException</span> reason: [<span class="built_in">NSString</span> stringWithFormat: <span class="string">@"unrecognized selector sent to instance %p"</span>, <span class="keyword">self</span>] userInfo: <span class="literal">nil</span>];</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="type">id</span> oldValue = [<span class="keyword">self</span> valueForKey: getterName];</span><br><span class="line"> <span class="keyword">struct</span> objc_super superClass = {</span><br><span class="line"> .receiver = <span class="keyword">self</span>,</span><br><span class="line"> .super_class = class_getSuperclass(object_getClass(<span class="keyword">self</span>))</span><br><span class="line"> };</span><br><span class="line"> </span><br><span class="line"> [<span class="keyword">self</span> willChangeValueForKey: getterName];</span><br><span class="line"> <span class="type">void</span> (*objc_msgSendSuperKVO)(<span class="type">void</span> *, SEL, <span class="type">id</span>) = (<span class="type">void</span> *)objc_msgSendSuper;</span><br><span class="line"> objc_msgSendSuperKVO(&superClass, _cmd, newValue);</span><br><span class="line"> [<span class="keyword">self</span> didChangeValueForKey: getterName];</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//获取所有监听回调对象进行回调</span></span><br><span class="line"> <span class="built_in">NSMutableArray</span> * observers = objc_getAssociatedObject(<span class="keyword">self</span>, (__bridge <span class="keyword">const</span> <span class="type">void</span> *)kFFKVOAssociatedObject);</span><br><span class="line"> <span class="keyword">for</span> (FFKVObserver *ob <span class="keyword">in</span> observers) {</span><br><span class="line"> <span class="keyword">if</span> ([ob.key isEqualToString: getterName]) {</span><br><span class="line"> <span class="built_in">dispatch_async</span>(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, <span class="number">0</span>), ^{</span><br><span class="line"> ob.handler(<span class="keyword">self</span>, getterName, oldValue, newValue);</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="keyword">static</span> Class KVO_Class(<span class="type">id</span> <span class="keyword">self</span>) {</span><br><span class="line"> <span class="keyword">return</span> class_getSuperclass(object_getClass(<span class="keyword">self</span>));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// ==================</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">NSObject</span> (<span class="title">KVO</span>)</span></span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)ff_addObserver:(<span class="built_in">NSObject</span> *)object forKey:(<span class="built_in">NSString</span> *)key withBlock:(FFObserverHandler)handler {</span><br><span class="line"> <span class="comment">//step 1 get setter method, if not, throw exception</span></span><br><span class="line"> SEL setterSelector = <span class="built_in">NSSelectorFromString</span>(setterForKey(key));</span><br><span class="line"> Method setterMethod = class_getInstanceMethod([<span class="keyword">self</span> <span class="keyword">class</span>], setterSelector);</span><br><span class="line"> <span class="keyword">if</span> (!setterMethod) {</span><br><span class="line"> <span class="keyword">@throw</span> [<span class="built_in">NSException</span> exceptionWithName: <span class="built_in">NSInvalidArgumentException</span> reason: [<span class="built_in">NSString</span> stringWithFormat: <span class="string">@"unrecognized selector sent to instance %@"</span>, <span class="keyword">self</span>] userInfo: <span class="literal">nil</span>];</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 自己的类作为被观察者类</span></span><br><span class="line"> Class observedClass = object_getClass(<span class="keyword">self</span>);</span><br><span class="line"> <span class="built_in">NSString</span> *className = <span class="built_in">NSStringFromClass</span>(observedClass);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 如果被监听者没有 `FFObserver_`,创建新类</span></span><br><span class="line"> <span class="keyword">if</span> (![className hasPrefix: kFFKVOClassPrefix]) {</span><br><span class="line"> <span class="comment">// 为被观察对象的类创建一个新的带有 `FFObserver_` 前缀的子类</span></span><br><span class="line"> observedClass = [<span class="keyword">self</span> createKVOClassWithOriginalClassName:className];</span><br><span class="line"> <span class="comment">// 创建新的子类,并添加新的方法</span></span><br><span class="line"> <span class="comment">// object 内部的 isa 变量指向它的 class。这个变量可以被改变,而不需要重新创建</span></span><br><span class="line"> <span class="comment">// 这一步即创建我们需要的新 class</span></span><br><span class="line"> object_setClass(<span class="keyword">self</span>, observedClass);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 判断是否有方法</span></span><br><span class="line"> <span class="keyword">if</span> (![<span class="keyword">self</span> hasSelector:setterSelector]) {</span><br><span class="line"> <span class="keyword">const</span> <span class="type">char</span> * types = method_getTypeEncoding(setterMethod);</span><br><span class="line"> <span class="comment">// 将原来的 setter 方法替换为新的 setter 方法</span></span><br><span class="line"> <span class="comment">// 通过 runtime 的 Method Swizzling</span></span><br><span class="line"> class_addMethod(observedClass, setterSelector, (IMP)KVO_Setter, types);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 新建一个观察者类</span></span><br><span class="line"> <span class="comment">// 这个类的实现写在同一个 class,相当于导入一个类:`FFKVOObserver`</span></span><br><span class="line"> <span class="comment">// 这个类的作用是观察者,负责 Block 回调</span></span><br><span class="line"> FFKVObserver *observer = [[FFKVObserver alloc] initWithObserver:object forKey:key handler:handler];</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 因为观察者实例都有前缀 `FFKVOAssociatedObject`</span></span><br><span class="line"> <span class="comment">// 通过共同前缀,获取`观察者数组`</span></span><br><span class="line"> <span class="comment">// 再将新建的 observer 加入数组</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// 因为 objc_getAssociatedObject 的参数要求,需要转换为 void</span></span><br><span class="line"> <span class="comment">// 在 ARC 有效时,通过 (__bridge void *) 能够实现 id 和 void * 的相互转换</span></span><br><span class="line"> <span class="built_in">NSMutableArray</span> *observers = objc_getAssociatedObject(<span class="keyword">self</span>, (__bridge <span class="type">void</span> *)kFFKVOAssociatedObject);</span><br><span class="line"> <span class="comment">// 若没有新建</span></span><br><span class="line"> <span class="keyword">if</span> (!observers) {</span><br><span class="line"> observers = [<span class="built_in">NSMutableArray</span> array];</span><br><span class="line"> objc_setAssociatedObject(<span class="keyword">self</span>, (__bridge <span class="type">void</span> *)kFFKVOAssociatedObject, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);</span><br><span class="line"> }</span><br><span class="line"> [observers addObject:observer];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)ff_removeObserver:(<span class="built_in">NSObject</span> *)object forKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="built_in">NSMutableArray</span> *observers = objc_getAssociatedObject(<span class="keyword">self</span>, (__bridge <span class="type">void</span> *)kFFKVOAssociatedObject);</span><br><span class="line"> </span><br><span class="line"> FFKVObserver *tmp = <span class="literal">nil</span>;</span><br><span class="line"> <span class="keyword">for</span> (FFKVObserver *ob <span class="keyword">in</span> observers) {</span><br><span class="line"> <span class="keyword">if</span> (ob.observer == object && [ob.key isEqualToString: key]) {</span><br><span class="line"> tmp = ob;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> [observers removeObject:tmp];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (Class)createKVOClassWithOriginalClassName:(<span class="built_in">NSString</span> *)className {</span><br><span class="line"> <span class="built_in">NSString</span> *kvoClassName = [kFFKVOClassPrefix stringByAppendingString: className];</span><br><span class="line"> Class observedClass = <span class="built_in">NSClassFromString</span>(kvoClassName);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (observedClass) { <span class="keyword">return</span> observedClass; }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 创建以 `FFObserver_` 为类名前缀的新类</span></span><br><span class="line"> Class originalClass = object_getClass(<span class="keyword">self</span>);</span><br><span class="line"> Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, <span class="number">0</span>);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 获取监听对象的 class 方法实现代码,然后替换新建类的 class 实现</span></span><br><span class="line"> Method classMethod = class_getInstanceMethod(originalClass, <span class="keyword">@selector</span>(<span class="keyword">class</span>));</span><br><span class="line"> <span class="keyword">const</span> <span class="type">char</span> *types = method_getTypeEncoding(classMethod);</span><br><span class="line"> class_addMethod(kvoClass, <span class="keyword">@selector</span>(<span class="keyword">class</span>), (IMP)KVO_Class, types);</span><br><span class="line"> objc_registerClassPair(kvoClass);</span><br><span class="line"> <span class="keyword">return</span> kvoClass;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">BOOL</span>)hasSelector:(SEL)selector {</span><br><span class="line"> Class observedClass = object_getClass(<span class="keyword">self</span>);</span><br><span class="line"> <span class="type">unsigned</span> <span class="type">int</span> methodCount = <span class="number">0</span>;</span><br><span class="line"> Method *methodList = class_copyMethodList(observedClass, &methodCount);</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i < methodCount; i++) {</span><br><span class="line"> SEL thisSelector = method_getName(methodList[i]);</span><br><span class="line"> <span class="keyword">if</span> (thisSelector == selector) {</span><br><span class="line"> free(methodList);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> free(methodList);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p>本篇是对 KVO(Key-Value Observing) 的梳理。内容结构:KVO 基本使用、KVO
实现原理、自己实现 KVO。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="Objective-C" scheme="http://example.com/categories/Objective-C/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="Objective-C" scheme="http://example.com/tags/Objective-C/"/>
</entry>
<entry>
<title>KVC 梳理</title>
<link href="http://example.com/2016/03/25/2016-03-25-objc-kvc/"/>
<id>http://example.com/2016/03/25/2016-03-25-objc-kvc/</id>
<published>2016-03-24T16:00:00.000Z</published>
<updated>2016-03-24T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>日常开发中常会用到KVC,可能一些细节没有深入了解过,本篇通过官方文档来全面了解下 KVC。</p><span id="more"></span><h2 id="一关于-kvc">一、关于 KVC</h2><h3 id="概述">1.1 概述</h3><p>KVC(Key-value coding - 键值编码)是由 <code>NSKeyValueCoding</code>非正式协议启用的一种机制,对象采用这种机制来提供对其<strong>属性</strong>/<strong>成员变量</strong>的间接访问。当一个对象符合键值编码时,它的属性/成员变量可以通过一个简洁、统一的消息传递接口(<code>setValue:forKey:</code>)借助字符串参数寻址。这种间接访问机制补充了<strong>实例变量</strong>(自动生成的<code>_<name></code>)及其相关<strong>访问器方法</strong>(getter方法)提供的直接访问。</p><p>通常使用<code>访问器方法</code>来访问对象的属性。如,get 访问器(或getter)返回属性的值;set 访问器(或 setter)设置属性的值。在 objc中,还可以直接访问属性的底层实例变量(由编译器生成的对应于属性的<code>_</code>+<code><property_name></code>的实例变量)。以上述任何一种方式访问对象属性都是简单的,但需要调用特定于属性的方法或变量名。随着属性列表的增长或更改,访问这些属性的代码也必须随之增长或更改。相反,<strong>KVC兼容对象</strong>提供了一个简单的消息传递接口,该接口在其所有属性中都是一致的。</p><p>KVC 也是许多其他 Cocoa 技术的基础,例如 KVO(key-valueobserving)、Cocoa bindings、Core Data 和AppleScript-ability。在某些情况下,KVC 还可以帮助简化代码。</p><h3 id="为什么说-nskeyvaluecoding-是非正式协议">1.2 为什么说NSKeyValueCoding 是非正式协议?</h3><p>前面提到 "KVC 是由 <code>NSKeyValueCoding</code>非正式协议启用的一种机制"。为什么说<code>NSKeyValueCoding</code>是非正式协议?</p><p>它不同于我们常见的 NSCopying、NSCoding 等协议是通过<code>@protocol</code>直接来定义的,之后当其他类要遵循此协议时,在其类声明或者类延展后面添加<code><NSCopying, NSCoding></code>表示该类遵循此协议。而<code>NSKeyValueCoding 机制</code>是通过分类来实现的,Foundation框架下有一个 <code>NSKeyValueCoding.h</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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSObject</span>(<span class="title">NSKeyValueCoding</span>)</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSArray</span>(<span class="title">NSKeyValueCoding</span>)</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSDictionary</span><<span class="title">KeyType</span>, <span class="title">ObjectType</span>>(<span class="title">NSKeyValueCoding</span>)</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSMutableDictionary</span><<span class="title">KeyType</span>, <span class="title">ObjectType</span>>(<span class="title">NSKeyValueCoding</span>)</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSOrderedSet</span>(<span class="title">NSKeyValueCoding</span>)</span></span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">NSSet</span>(<span class="title">NSKeyValueCoding</span>)</span></span><br></pre></td></tr></table></figure><p>可以看到 NSObject 基类已经实现了 NSKeyValueCoding机制的所有接口,然后NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet这些子类则是对 <code>setValue:forKey:</code> 和<code>valueForKey:</code> 函数进行重载。</p><p>例如,当对一个 NSArray 对象调用 <code>setValue:forKey:</code>函数时,它内部是对数组中的每个元素调用 <code>setValue:forKey:</code>函数。当对一个 NSArray 对象调用 <code>valueForKey:</code>函数时,它返回一个数组,同时会返回数组每个元素调用<code>valueForKey:</code>的结果。返回的数组若包含<code>NSNull</code>元素,指代的是数组中某些元素调用<code>valueForKey:</code> 函数返回 <code>nil</code> 的情况。</p><h2 id="二kvc-的使用">二、KVC 的使用</h2><h3 id="读">2.1 读</h3><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">id</span>)valueForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line">- (<span class="type">id</span>)valueForKeyPath:(<span class="built_in">NSString</span> *)keyPath;</span><br><span class="line"><span class="comment">/// keys 不能包含 path 类型,如使用 path 且未实现 `valueForUndefinedKey:`,会 crash</span></span><br><span class="line">- (<span class="built_in">NSDictionary</span><<span class="built_in">NSString</span> *,<span class="type">id</span>> *)dictionaryWithValuesForKeys:(<span class="built_in">NSArray</span><<span class="built_in">NSString</span> *> *)keys;</span><br><span class="line"><span class="comment">///由 `valueForKey:` 调用,当找不到与给定键对应的属性时,触发</span></span><br><span class="line"><span class="comment">///子类可以重写该方法以返回未定义键的兜底值。默认实现引发 `NSUndefinedKeyException`</span></span><br><span class="line">- (<span class="type">id</span>)valueForUndefinedKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line"><span class="comment">///可以帮助我们更快速的修改`可变/不可变集合类型的属性`</span></span><br><span class="line">- (<span class="built_in">NSMutableArray</span> *)mutableArrayValueForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line"><span class="comment">///</span></span><br></pre></td></tr></table></figure><p>举例,我们给 Student 添加一个这样的属性: <figure class="highlight plaintext"><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">@property (nonatomic, strong) NSArray<Person *> *personArray;</span><br><span class="line"></span><br><span class="line">NSLog(@"❇️❇️ %@", [self.student valueForKeyPath:@"personArray.name"]);</span><br></pre></td></tr></table></figure></p><p>然后打印的就是 <code>personArray</code> 属性中的每个 Person 对象的name 构成的一个字符串数组。</p><h3 id="写">2.2 写</h3><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></pre></td><td class="code"><pre><span class="line">- (<span class="type">void</span>)setValue:(<span class="type">id</span>)value forKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line"><span class="comment">///此方法的默认实现使用 `valueForKey:` 获取每个相关的目标对象,并向最终对象发送 `setValue:forKey:` 消息</span></span><br><span class="line"><span class="comment">///即先读取最终对象然后为其赋值</span></span><br><span class="line">- (<span class="type">void</span>)setValue:(<span class="type">id</span>)value forKeyPath:(<span class="built_in">NSString</span> *)keyPath;</span><br><span class="line"><span class="comment">///子类可重写此方法,默认会引发 `NSInvalidArgumentException` 的 crash</span></span><br><span class="line"><span class="comment">///基础类型会 crash,对象类型不会</span></span><br><span class="line">- (<span class="type">void</span>)setNilValueForKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line"><span class="comment">///默认实现为 `keyedValues` 中的每个键值对调用 `setValue:forKey:`,用 nil 替换 keyedValues 中的 NSNull 值</span></span><br><span class="line">- (<span class="type">void</span>)setValuesForKeysWithDictionary:(<span class="built_in">NSDictionary</span><<span class="built_in">NSString</span> *,<span class="type">id</span>> *)keyedValues;</span><br><span class="line"><span class="comment">///由 setValue:forKey: 调用,当它找不到给定键的属性时,默认实现引发 `NSUndefinedKeyException`</span></span><br><span class="line">- (<span class="type">void</span>)setValue:(<span class="type">id</span>)value forUndefinedKey:(<span class="built_in">NSString</span> *)key;</span><br><span class="line"><span class="comment">///子类可以覆盖它以返回 NO,在这种情况下,KVC 方法将无法访问实例变量</span></span><br><span class="line"><span class="keyword">@property</span>(<span class="keyword">class</span>, <span class="keyword">readonly</span>) <span class="type">BOOL</span> accessInstanceVariablesDirectly;</span><br><span class="line"><span class="comment">///ioValue 指向由 inKey 标识的属性的新值的指针。该方法可以修改或替换该值以使其有效</span></span><br><span class="line">- (<span class="type">BOOL</span>)validateValue:(<span class="keyword">inout</span> <span class="type">id</span> _Nullable *)ioValue forKey:(<span class="built_in">NSString</span> *)inKey error:(<span class="keyword">out</span> <span class="built_in">NSError</span> * _Nullable *)outError;</span><br><span class="line">- (<span class="type">BOOL</span>)validateValue:(<span class="keyword">inout</span> <span class="type">id</span> _Nullable *)ioValue forKeyPath:(<span class="built_in">NSString</span> *)inKeyPath error:(<span class="keyword">out</span> <span class="built_in">NSError</span> * _Nullable *)outError;</span><br></pre></td></tr></table></figure><p>默认情况下,从 NSObject 或其子类继承的 Swift对象的属性符合键值编码。</p><h3 id="集合运算符">2.3 集合运算符</h3><figure><imgsrc="https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/art/keypath.jpg"alt="Operator key path format" /><figcaption aria-hidden="true">Operator key path format</figcaption></figure><figure class="highlight objc"><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 class="comment">///集合运算符格式</span></span><br><span class="line">[email protected]</span><br></pre></td></tr></table></figure><h4 id="聚合运算符">2.3.1 聚合运算符</h4><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></pre></td><td class="code"><pre><span class="line"><span class="comment">///transactionAverage 是 self.transactions 数组中的每个 Transaction 对象中 amount 属性的平均值</span></span><br><span class="line"><span class="built_in">NSNumber</span> *transactionAverage = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@avg.amount"</span>];</span><br><span class="line"><span class="comment">///numberOfTransactions 是 self.transactions 数组中的元素的个数</span></span><br><span class="line"><span class="built_in">NSNumber</span> *numberOfTransactions = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@count"</span>];</span><br><span class="line"><span class="comment">///当指定 @max 运算符时,valueForKeyPath: 在由右键路径命名的集合条目中搜索并返回最大的条目</span></span><br><span class="line"><span class="comment">///搜索使用 `compare:` 方法进行比较,该方法由许多 Foundation 类(例如 NSNumber 类)定义。因此,由右键路径指示的属性必须包含一个对该消息有意义响应的对象(即集合中的元素必须实现了 compare: 函数)</span></span><br><span class="line"><span class="comment">///搜索将忽略 nil 的集合条目</span></span><br><span class="line"><span class="built_in">NSDate</span> *latestDate = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@max.date"</span>];</span><br><span class="line"><span class="built_in">NSDate</span> *earliestDate = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@min.date"</span>];</span><br><span class="line"><span class="comment">///指定 @sum 运算符时,`valueForKeyPath:` 读取由右键路径为集合的每个元素指定的属性,将其转换为 double(用 0 代替 nil 值),并计算这些值的和</span></span><br><span class="line"><span class="comment">///然后返回存储在 NSNumber 实例中的结果</span></span><br><span class="line"><span class="built_in">NSNumber</span> *amountSum = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@sum.amount"</span>];</span><br></pre></td></tr></table></figure><h4 id="数组运算符">2.3.2 数组运算符</h4><p>数组运算符使 valueForKeyPath:返回一个对象数组,该对象数组与右键路径指示的一组特定对象相对应。如果使用数组运算符时,任何子对象为nil,则 <code>valueForKeyPath:</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></pre></td><td class="code"><pre><span class="line"><span class="comment">///distinctPayees 是 self.transactions 数组中的每个 Transaction 对象的 payee 的值组成的字符串数组(忽略重复的 payee)</span></span><br><span class="line"><span class="built_in">NSArray</span> *distinctPayees = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@distinctUnionOfObjects.payee"</span>];</span><br><span class="line"><span class="comment">///payees 是 self.transactions 数组中的每个 Transaction 对象的 payee 的值组成的字符串数组(不忽略重复的 payee)</span></span><br><span class="line"><span class="built_in">NSArray</span> *payees = [<span class="keyword">self</span>.transactions valueForKeyPath:<span class="string">@"@unionOfObjects.payee"</span>];</span><br></pre></td></tr></table></figure><h4 id="嵌套运算符">2.3.3 嵌套运算符</h4><p>嵌套运算符对嵌套集合进行操作,其中集合本身的每个条目都包含一个集合。使用嵌套运算符时,如果任何子对象为nil,则 <code>valueForKeyPath:</code> 方法将引发异常。</p><figure class="highlight objc"><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 class="built_in">NSArray</span> *moreTransactions = @[<<span class="meta"># transaction data #>];</span></span><br><span class="line"><span class="built_in">NSArray</span> *arrayOfArrays = @[<span class="keyword">self</span>.transactions, moreTransactions];</span><br></pre></td></tr></table></figure><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"><span class="comment">///要在 arrayOfArrays 中的所有数组之间获取 payee 属性的不同值</span></span><br><span class="line"><span class="built_in">NSArray</span> *collectedDistinctPayees = [arrayOfArrays valueForKeyPath:<span class="string">@"@distinctUnionOfArrays.payee"</span>];</span><br><span class="line"><span class="built_in">NSArray</span> *collectedPayees = [arrayOfArrays valueForKeyPath:<span class="string">@"@unionOfArrays.payee"</span>];</span><br><span class="line"><span class="comment">///@distinctUnionOfSets 运算符结果与 @distinctUnionOfArrays 的结果相同</span></span><br></pre></td></tr></table></figure></p><h3 id="包装和展开-struct">2.4 包装和展开 Struct</h3><p>包装和解包常见的 Struct 结构: <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></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="built_in">CGPoint</span> {</span><br><span class="line"> <span class="built_in">CGFloat</span> x;</span><br><span class="line"> <span class="built_in">CGFloat</span> y;</span><br><span class="line">};</span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">struct</span> _NSRange {</span><br><span class="line"> <span class="built_in">NSUInteger</span> location;</span><br><span class="line"> <span class="built_in">NSUInteger</span> length;</span><br><span class="line">} <span class="built_in">NSRange</span>;</span><br><span class="line"><span class="keyword">struct</span> <span class="built_in">CGRect</span> {</span><br><span class="line"> <span class="built_in">CGPoint</span> origin;</span><br><span class="line"> <span class="built_in">CGSize</span> size;</span><br><span class="line">};</span><br><span class="line"><span class="keyword">struct</span> <span class="built_in">CGSize</span> {</span><br><span class="line"> <span class="built_in">CGFloat</span> width;</span><br><span class="line"> <span class="built_in">CGFloat</span> height;</span><br><span class="line">};</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><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"><span class="keyword">typedef</span> <span class="keyword">struct</span> {</span><br><span class="line"> <span class="type">float</span> x, y, z;</span><br><span class="line">} ThreeFloats;</span><br><span class="line"> </span><br><span class="line"><span class="class"><span class="keyword">@interface</span> <span class="title">MyClass</span></span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) ThreeFloats threeFloats;</span><br><span class="line"><span class="keyword">@end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">///取。`valueForKey:` 的默认实现会调用 threeFloats getter,然后返回包装在 NSValue 对象中的结果</span></span><br><span class="line"><span class="built_in">NSValue</span> *result = [myClass valueForKey:<span class="string">@"threeFloats"</span>];</span><br><span class="line"><span class="comment">///存。KVC 设置 threeFloats 值</span></span><br><span class="line">ThreeFloats floats = {<span class="number">1.</span>, <span class="number">2.</span>, <span class="number">3.</span>};</span><br><span class="line"><span class="built_in">NSValue</span>* value = [<span class="built_in">NSValue</span> valueWithBytes:&floats objCType:<span class="keyword">@encode</span>(ThreeFloats)];</span><br><span class="line">[myClass setValue:value forKey:<span class="string">@"threeFloats"</span>];</span><br></pre></td></tr></table></figure></p><h2 id="三kvc-的原理">三、KVC 的原理</h2><h3 id="基本的-getter">3.1 基本的 getter</h3><p>在给定键参数作为输入的情况下,<code>valueForKey:</code>的默认实现执行以下过程。(在接收 <code>valueForKey:</code>调用的类实例内部进行如下操作)</p><ol type="1"><li>按序查找 <code>get<Key></code> <code><Key></code><code>is<Key></code> 的 getter 方法,若找到直接调用;</li></ol><ul><li>若方法的返回结果类型是一个对象指针,则直接返回结果;</li><li>若类型为基本数据类型,则转为 NSNumber 返回;否则转为 NSValue返回;</li></ul><ol start="2" type="1"><li>若上述未找到 getter,则查找<code>countOf<Key></code>、<code>objectIn<Key>AtIndex:</code>、<code><Key>AtIndexes</code>方法;</li></ol><ul><li>若 <code>countOf<Key></code>和另外两方法中一个找到,则返回一个可以响应 NSArray所有方法的集合代理对象,否则执行步骤3。代理对象随后将接收到的任何NSArray 消息转化为<code>countOf<Key></code>、<code>objectIn<Key>AtIndex:</code>、<code><Key>AtIndexes</code>消息组合,并将其转换为创建它的KVC兼容对象;</li></ul><ol start="3" type="1"><li>若上述未找到,继续查找 <code>countOf<Key></code><code>enumeratorOf<Key></code> <code>memberOf<Key></code>方法。若都查到,返回一个可以相应 NSSet所有方法的集合代理对象。否则执行步骤4。代理对象随后将接收到的任何 NSSet消息转换为 <code>countOf<Key></code><code>enumeratorOf<Key></code><code>memberOf<Key></code>消息组合,以创建对象;</li><li>若上述简单的访问器方法或集合访问方法组未找到,且 receiver 的类方法<strong>accessInstanceVariablesDirectly</strong> 返回 YES,则按序查找<code>_<Key></code> <code>_is<Key></code><code><Key></code> <code>is<Key></code>的实例变量。若找到,直接获取实例变量的值并执行步骤5。否则,继续步骤6;</li><li>若检索到的属性值是对象指针,只需返回结果;若该值是 NSNumber支持的基础类型,则返回 NSNumber 实例;若该值 NSNumber 不支持,则转换为NSValue 对象;最终返回该对象;</li><li>如果所有方法均失败,则调用<code>valueForUndefinedKey:</code>,默认情况下会引发一个异常。NSObject子类可通过重写 <code>valueForUndefinedKey:</code> 来避免异常;</li></ol><h3 id="基本的-setter">3.2 基本的 setter</h3><p>在接收到调用的对象内部 <code>setValue:forKey:</code>的默认实现使用了以下过程。</p><ol type="1"><li>按顺序查找访问器 <strong>set<Key></strong> 或**_set<Key>**,如果找到,调用这个方法并传入值,完成操作;</li><li>若未找到对应的 setter,且<strong>accessInstanceVariablesDirectly</strong> 类属性返回YES,按序查找命名规则为<code>_key</code>、<code>_isKey</code>、<code>key</code>、<code>isKey</code>的实例变量。若找到则将 value 赋值给实例变量,完成操作;</li><li>在找不到访问器及实例变量后,调用<strong>setValue:forUndefinedKey:</strong>方法,默认会抛出一个异常。子类可重写<code>setValue:forUndefinedKey:</code> 来避免;</li></ol><h3 id="可变数组">3.3 可变数组</h3><p><code>mutableArrayValueForKey:</code> 的默认实现,给定一个 key参数作为输入,为接收访问器调用的对象内的名为 key的属性返回一个可变的代理数组,具体过程如下:</p><ol type="1"><li>查找方法 <code>insertObject:in<Key>AtIndex:</code> 和<code>removeObjectFrom<Key>AtIndex:</code>(分别对应于NSMutableArray 的原始方法 <code>insertObject:atIndex:</code> 和<code>removeObjectAtIndex:</code>);或方法<code>insert<Key>:atIndexes:</code> 和<code>remove<Key>AtIndexes:</code>(分别对应于 NSMutableArray 的<code>insertObjects:atIndexes:</code> 和<code>removeObjectsAtIndexes:</code>)。如果对象具有至少一种插入方法和至少一种删除方法,通过发送<code>insertObject:in<Key>AtIndex:</code>、<code>removeObjectFrom<Key>AtIndex:</code>、<code>insert<Key>:atIndexes:</code>、<code>remove<Key>AtIndexes:</code>消息的组合,并将消息发送给 <code>mutableArrayValueForKey:</code>的原始接收者,返回一个响应 NSMutableArray 消息的代理对象。当接收到<code>mutableArrayValueForKey:</code> 消息的对象还实现了一个可选的replace 对象方法,方法名类似<code>replaceObjectInAtIndex:withObject:</code> 或<code>replaceAtIndexes:with:</code>,代理对象也会在适当的时候也利用这些对象以获得最佳性能;</li><li>如果对象没有可变数组方法,查找与 <code>set:</code>匹配的访问器方法。这种情况下,通过向<code>mutableArrayValueForKey:</code> 的原始接收者发出 <code>set:</code>消息,返回响应 NSMutableArray 消息的代理对象。</li></ol><ul><li>该步骤中的机制比上一个机制效率低很多,它可能会重复创建新的集合对象而不是修改现有的集合对象。在设计自己的KVC 兼容对象时,通常应避免使用它;</li></ul><ol start="3" type="1"><li>若即没有找到可变数组方法,也未找到访问器,且若 receiver 的类方法<code>accessInstanceVariablesDirectly</code> 返回YES。则按序检索实例变量<code>_<key></code>、<code><key></code>,若找到这些实例变量,返回对应的代理对象;</li><li>如果所有的操作都失败,则发送 <code>mutableArrayValueForKey:</code>消息给原始接收方,只要接收到 NSMutableArray 消息则发出<code>setValue:forUndefinedKey:</code> 消息,该消息会引发<code>NSUndefinedKeyException</code>。子类可通过重写该方法来避免crash;</li></ol><h2 id="四ios13-起遇到的问题">四、iOS13 起遇到的问题</h2><p>2019.07.12 更新,记录 iOS13 系统禁止 KVC 访问的几种解决方案。</p><h3 id="uitextfield">4.1 UITextField</h3><figure class="highlight objc"><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 class="built_in">UITextField</span> *textField = [<span class="built_in">UITextField</span> new];</span><br><span class="line">[textField valueForKey:<span class="string">@"_placeholderLabel"</span>];</span><br></pre></td></tr></table></figure><p>但 iOS13 中 UITextField 重写了<code>valueForKey:</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><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">@implementation</span> <span class="title">UITextField</span></span></span><br><span class="line"></span><br><span class="line">- (<span class="type">id</span>)valueForKey:(<span class="built_in">NSString</span> *)key {</span><br><span class="line"> <span class="keyword">if</span> ([key isEqualToString:<span class="string">@"_placeholderLabel"</span>]) {</span><br><span class="line"> [<span class="built_in">NSException</span> raise:<span class="built_in">NSGenericException</span> format:<span class="string">@"Access to UITextField's _placeholderLabel ivar is prohibited. This is an application bug"</span>];</span><br><span class="line"> }</span><br><span class="line"> [<span class="variable language_">super</span> valueForKey:key];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">@end</span></span><br></pre></td></tr></table></figure><p>解决方案:1.取消下划线; 2.为 UITextField 重写一个方法。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">///方案一</span></span><br><span class="line">[textField valueForKey:<span class="string">@"placeholderLabel"</span>];</span><br><span class="line"></span><br><span class="line"><span class="comment">///方案二</span></span><br><span class="line">- (<span class="type">void</span>)resetTextField: (<span class="built_in">UITextField</span> *)textField {</span><br><span class="line"> Ivar ivar = class_getInstanceVariable([textField <span class="keyword">class</span>], <span class="string">"_placeholderLabel"</span>);</span><br><span class="line"> <span class="built_in">UILabel</span> *placeholderLabel = object_getIvar(textField, ivar);</span><br><span class="line"> placeholderLabel.text = title;</span><br><span class="line"> placeholderLabel.textColor = color;</span><br><span class="line"> placeholderLabel.font = [<span class="built_in">UIFont</span> systemFontOfSize:fontSize];</span><br><span class="line"> placeholderLabel.textAlignment = alignment;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="uisearchbar">4.2 UISearchBar</h3><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="built_in">UISearchBar</span> *bar = [<span class="built_in">UISearchBar</span> new];</span><br><span class="line">[bar setValue:<span class="string">@"test"</span> forKey:<span class="string">@"_cancelButtonText"</span>]</span><br><span class="line"><span class="built_in">UIView</span> *searchField = [bar valueForKey:<span class="string">@"_searchField"</span>];</span><br></pre></td></tr></table></figure><p>iOS13 中重写了 <code>set_cancleButtonText</code>,拦截了 KVC:</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="type">void</span>)set_cancelButtonText:(<span class="built_in">NSString</span> *)text {</span><br><span class="line"> [<span class="built_in">NSException</span> raise:<span class="built_in">NSGenericException</span> format:<span class="string">@"Access to UISearchBar's set_cancelButtonText: ivar is prohibited. This is an application bug"</span>];</span><br><span class="line"> [<span class="keyword">self</span> _setCancelButtonText];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (<span class="type">void</span>)_searchField {</span><br><span class="line"> [<span class="built_in">NSException</span> raise:<span class="built_in">NSGenericException</span> format:<span class="string">@"Access to UISearchBar's _searchField ivar is prohibited. This is an application bug"</span>];</span><br><span class="line"> [<span class="keyword">self</span> searchField];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>解决方案:直接调用<code>_setCancelButtonText</code>、<code>searchField</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="built_in">UISearchBar</span> *bar = [<span class="built_in">UISearchBar</span> new];</span><br><span class="line">[bar setValue:<span class="string">@"test"</span> forKey:<span class="string">@"_setCancelButtonText"</span>]</span><br><span class="line"><span class="built_in">UIView</span> *searchField = [bar valueForKey:<span class="string">@"searchField"</span>];</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p>日常开发中常会用到
KVC,可能一些细节没有深入了解过,本篇通过官方文档来全面了解下 KVC。</p></summary>
<category term="iOS" scheme="http://example.com/categories/iOS/"/>
<category term="Objective-C" scheme="http://example.com/categories/Objective-C/"/>
<category term="iOS" scheme="http://example.com/tags/iOS/"/>
<category term="Objective-C" scheme="http://example.com/tags/Objective-C/"/>
</entry>
</feed>