-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
571 lines (311 loc) · 406 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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>南华coder的空间</title>
<link href="/atom.xml" rel="self"/>
<link href="http://buaa0300/nanhuacoder.com/"/>
<updated>2020-01-04T14:24:24.818Z</updated>
<id>http://buaa0300/nanhuacoder.com/</id>
<author>
<name>南华coder</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>漫谈0x02:研发视角下的DNS</title>
<link href="http://buaa0300/nanhuacoder.com/2020/01/04/Architect-002/"/>
<id>http://buaa0300/nanhuacoder.com/2020/01/04/Architect-002/</id>
<published>2020-01-04T02:31:24.000Z</published>
<updated>2020-01-04T14:24:24.818Z</updated>
<content type="html"><![CDATA[<h3 id="一、DNS基础"><a href="#一、DNS基础" class="headerlink" title="一、DNS基础"></a>一、DNS基础</h3><h4 id="1、Domain-Name-和-DNS"><a href="#1、Domain-Name-和-DNS" class="headerlink" title="1、Domain Name 和 DNS"></a>1、Domain Name 和 DNS</h4><ul><li><p><strong>域名</strong>(Domain Name)是由一串用点分隔的名字组成的Internet上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识(有时也指地理位置);eg: www.baidu.com</p></li><li><p><strong>DNS</strong>(Domain Name System, 域名系统) 是Internet上作为将域名和IP地址相互映射的一个分布式数据库,能够根据域名找到对应的IP地址。</p></li><li><p><strong>根域、顶级域、二级域、子域</strong> 域名采用层次化的方式进行组织,每一个点代表一个层级。一个域名完整的格式为<code>www.baidu.com.</code> 最末尾的点(<code>.</code>)代表根域,常常省略;<code>com</code>即顶级域(TLD, Top-Level Domain);<code>baidu.com</code>即二级域。依次类推,还有三级域、四级域等等。子域是一个相对的概念,<code>baidu.com</code>是<code>com</code>的子域,<code>www.baidu.com</code>是<code>baidu.com</code>的子域。</p></li><li><p>根域名通常使用”.”来表示,其实际上也是由域名组成,全世界目前有13组域名根节点,由少数几个国家进行管理,而国内仅有几台根节点镜像。</p></li></ul><h4 id="2、DNS解析相关"><a href="#2、DNS解析相关" class="headerlink" title="2、DNS解析相关"></a>2、DNS解析相关</h4><ul><li><p><strong>正向解析</strong>:查找域名对应IP的过程;<strong>反向解析</strong>: 查找IP对应域名的过程;<strong>解析器</strong>: 即resolver,处于DNS客户端的一套系统,用于实现正向解析或者反向解析。</p></li><li><p><strong>权威DNS</strong>:处于DNS服务端的一套系统,该系统保存了相应域名的权威信息。权威DNS即通俗上“这个域名我说了算”的服务器。</p></li><li><strong>递归DNS</strong>:又叫Local DNS,递归DNS可以理解为是一种功能复杂些的解析器(resolver),其核心是 缓存 和 递归查询。收到域名查询请求后,首先看本地缓存是否有记录,如果没有则一级一级的查询根、顶级域、二级域……直到获取到结果然后返回给用户。<strong>日常上网中运营商分配的DNS即是递归DNS</strong></li><li><strong>转发DNS</strong>:是一种特殊的递归。如果本地的缓存记录中没有相应域名结果时,其将查询请求转发给另外一台DNS服务器,由另外一台DNS服务器来完成查询请求。</li><li><strong>公共DNS</strong>:属于递归DNS。其典型特征为对外一个IP,为所有用户提供公共的递归查询服务。<ul><li>国内主流的公共DNS的有<strong>114DNS</strong>(114.114.114.114)、<strong>阿里DNS</strong>(223.5.5.5)、<strong>百度DNS</strong>(180.76.76.76)、<strong>腾讯DNS</strong>(119.29.29.29)</li><li>国外推荐的公共DNS是<strong>谷歌DNS</strong>(8.8.8.8)</li></ul></li></ul><h4 id="3、DNS常用命令工具"><a href="#3、DNS常用命令工具" class="headerlink" title="3、DNS常用命令工具"></a>3、DNS常用命令工具</h4><ul><li><code>host</code> 命令 or <code>nslookup</code>命令:分析域名查询工具,测试域名系统工作是否正常</li></ul><figure class="highlight shell"><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">#</span> 查询域名的主机信息</span><br><span class="line">host -a www.baidu.com</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> Trying "www.baidu.com"</span><br><span class="line"><span class="meta">#</span> ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 27785</span><br><span class="line"><span class="meta">#</span> ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> ;; QUESTION SECTION:</span><br><span class="line"><span class="meta">#</span> ;www.baidu.com.INANY</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> ;; ANSWER SECTION:</span><br><span class="line"><span class="meta">#</span> www.baidu.com.60INA10.15.63.90</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> 根据IP反查域名</span><br><span class="line">host 114.114.114.114 </span><br><span class="line"><span class="meta">#</span> 114.114.114.114.in-addr.arpa domain name pointer public1.114dns.com.</span><br></pre></td></tr></table></figure><figure class="highlight shell"><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="meta">#</span> 查询域名对应的IP</span><br><span class="line">nslookup www.baidu.com </span><br><span class="line"><span class="meta">#</span> Server:10.8.8.18</span><br><span class="line"><span class="meta">#</span> Address:10.8.8.18#53</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> Non-authoritative answer:</span><br><span class="line"><span class="meta">#</span> Name:www.baidu.com</span><br><span class="line"><span class="meta">#</span> Address: 10.15.63.90</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> 根据IP反查域名</span><br><span class="line">nslookup 114.114.114.114 </span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> Server:10.8.8.18</span><br><span class="line"><span class="meta">#</span> Address:10.8.8.18#53</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span> Non-authoritative answer:</span><br><span class="line"><span class="meta">#</span> 114.114.114.114.in-addr.arpaname = public1.114dns.com.</span><br><span class="line"><span class="meta">#</span> Authoritative answers can be found from:</span><br></pre></td></tr></table></figure><ul><li><code>whois</code>命令:查看域名的注册情况。</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">whois www.baidu.com</span><br></pre></td></tr></table></figure><h3 id="二、DNS查询"><a href="#二、DNS查询" class="headerlink" title="二、DNS查询"></a>二、DNS查询</h3><h4 id="1、DNS查询方式"><a href="#1、DNS查询方式" class="headerlink" title="1、DNS查询方式"></a>1、DNS查询方式</h4><ul><li>DNS查询分两种:递归查询与迭代查询;</li><li><strong>递归查询</strong>:Local DNS Server接收到客户端请求后,如果它本地没有被查询的域名的IP地址,就会代替客户端,去向其他域名服务器继续发出查询,直到查询到结果后,然后层层返回给客户;</li><li><strong>迭代查询</strong>:Local DNS Server接收到客户端请求后,如果它本地没有被查询的域名的IP地址,并不直接回复查询结果,而是告诉客户机另一台DNS 服务器地址,客户机再向这台DNS 服务器提交请求,依次循环直到返回查询的结果。</li></ul><h4 id="2、Local-DNS的设置"><a href="#2、Local-DNS的设置" class="headerlink" title="2、Local DNS的设置"></a>2、Local DNS的设置</h4><ul><li>主机(PC或移动设备)上可以通过<strong>手动配置</strong> DNS的服务器地址 OR 利用DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)<strong>动态获得</strong>DNS的服务器地址。</li><li>Local DNS包括网络服务提供商(ISP)分配给我们的DNS(一般为两个),或者是其他组织提供的公共DNS,比如</li><li>有时,PC或者移动设备上,DNS的地使用路由器的地址,这是因为路由器会将DNS请求转发到ISP的DNS服务器上。</li></ul><h4 id="3、典型DNS查询过程"><a href="#3、典型DNS查询过程" class="headerlink" title="3、典型DNS查询过程"></a>3、典型DNS查询过程</h4><p>当我们在浏览器中输入一个网站如:<code>www.baidu.com</code>时,会发生如下过程: </p><ul><li>操作系统会先依次检查缓存中、本地的hosts中是够有这个域名对应的IP地址;如果有,就先调用这个IP地址,完成域名解析,否则进入下一步;</li><li>如果缓存中、本地的hosts里都没有这个域名的映射,客户端会向Local DNS Server发起查询。Local DNS Server收到查询请求时,如果本地缓存中有记录,则返回IP地址给客户机,完成域名解析,否则进入下一步;</li><li>如果Local DNS Server本地缓存没有记录,则一级一级的查询根、顶级域、二级域……直到获取到IP地址,然后返回给用户。</li><li>最终,客户端可以通过 IP 访问到对应的服务器</li></ul><h4 id="4、扩展"><a href="#4、扩展" class="headerlink" title="4、扩展"></a>4、扩展</h4><ul><li><p>DNS的查询和响应使用的是UDP协议,DNS消息通过UDP数据包发送,如果在一个限定时间内没有收到响应的UDP数据包,那么DNS客户端则会重复查询请求;如果重复一定次数仍然失败,则会尝试域内另一台DNS服务器。</p></li><li><p>TTL(Time To-Live)是一条域名解析记录在DNS服务器中的存留时间;可以通过设置TTL时间来控制DNS缓存的时间;但是客户端也可以域名解析记录,甚至有的客户端忽略DNS Server的TTL时间,而采用自己的固定DNS过期时间。</p></li><li><p>对于大型网站而言,通过Local DNS返回的IP地址,并不是真正应用服务器的地址,而是网站的反向代理(nginx)服务器地址;反向代理(nginx)服务器会根据 负载均衡策略,将请求分配给对应的应用服务器处理;应用服务器处理后,才会返回给原来的客户端。</p></li></ul><h3 id="三、DNS轮询及应用"><a href="#三、DNS轮询及应用" class="headerlink" title="三、DNS轮询及应用"></a>三、DNS轮询及应用</h3><h4 id="1、基本概念"><a href="#1、基本概念" class="headerlink" title="1、基本概念"></a>1、基本概念</h4><ul><li><p>DNS轮询:通过对一个域名设置多个IP解析,来扩充Server性能及实施负载均衡的技术;目前,大部分域名注册商都支持多条A(Address)记录的解析,DNS服务器将解析请求按照A记录的顺序,逐一分配到不同的IP上,这样就完成了简单的负载均衡。</p></li><li><p>A(Address)记录:又称IP指向,用户可以在此设置子域名并指向到自己的目标主机地址上,从而实现通过域名找到服务器。(<em>说明:指向的目标主机地址类型只能使用IP地址</em>)</p></li><li><p><strong>负载均衡</strong>(Load Balance)是分布式系统架构设计中必须考虑的因素之一,它通常是指,将请求/数据均匀分摊到多个操作单元上执行。</p></li></ul><h4 id="2、DNS轮询的优点"><a href="#2、DNS轮询的优点" class="headerlink" title="2、DNS轮询的优点"></a>2、DNS轮询的优点</h4><ul><li>基本上无成本,因为域名注册商的这种解析都是免费的;</li><li>部署方便,除了网络拓扑的简单扩增,新增的Web服务器只要增加一个公网IP即可。</li></ul><h4 id="3、DNS轮询的缺点"><a href="#3、DNS轮询的缺点" class="headerlink" title="3、DNS轮询的缺点"></a>3、DNS轮询的缺点</h4><ul><li><strong>健康检查缺失</strong>:如果某台服务器宕机,DNS服务器是无法知晓的,仍旧会将访问分配到此服务器。修改DNS记录全部生效起码要3-4小时,甚至更久;</li><li><strong>请求/数据分配不均</strong>:如果几台Web服务器之间的配置不同,能够承受的压力也就不同,但是DNS解析分配的访问却是均匀分配的。其实DNS也是有分配算法的,可以根据当前连接较少的分配、可以设置Rate权重分配等等,只是目前绝大多数的DNS服务器都不支持;</li><li><strong>缺少会话保持</strong>:如果是需要身份验证的网站,在不修改软件构架的情况下,这点是比较致命的;因为DNS解析无法将验证用户的访问持久分配到同一服务器。虽然有一定的本地DNS缓存,但是很难保证在用户访问期间,本地DNS不过期,而重新查询服务器并指向新的服务器,那么原服务器保存的用户信息是无法被带到新服务器的,而且可能要求被重新认证身份,来回切换时间长了各台服务器都保存有用户不同的信息,对服务器资源也是一种浪费。</li></ul><h4 id="4、DNS轮询的应用"><a href="#4、DNS轮询的应用" class="headerlink" title="4、DNS轮询的应用"></a>4、DNS轮询的应用</h4><ul><li>常见互联网分布式架构,分为<strong>客户端层、反向代理nginx层、站点层、服务层、数据层</strong>。每一个下游都有多个上游调用,只需要做到,每一个上游都均匀访问每一个下游,就能实现“将请求/数据【均匀】分摊到多个操作单元上执行”。</li><li><p><strong>水平扩展反向代理层</strong>:DNS Server对于一个域名配置了多个解析IP,每次DNS解析请求来访问DNS-Server,会轮询返回这些IP,保证每个IP的解析概率是相同的; 而这些IP就是nginx的外网ip,以做到每台nginx的请求分配也是均衡的。</p></li><li><p>当然,对于比较简单的系统,甚至可以不需要反向代理层,直接利用DNS轮询实施负载均衡,优缺点都非常明显,根据实际情况来吧。</p><ul><li>优点:少了一层网络请求,服务端架构简单</li><li>缺点:<ul><li>DNS只具备解析功能,不能保证对应外网IP的可用性,而nginx与应用Server之间有保活探测机制,当应用Server挂掉时,能够自动迁移流量;</li><li>应用Server需要扩容时,通过DNS扩容生效时间长,而nginx是服务端完全自己可控的部分,应用Server扩容更实时更方便</li></ul></li></ul></li></ul><h4 id="5、总结"><a href="#5、总结" class="headerlink" title="5、总结"></a>5、总结</h4><ul><li>在网络架构的发展过程中,会有利用DNS轮询来实现简单Web Server的负载均衡,但是Web Server的负载均衡主要是通过nginx、LVS和keepalived来实现的。</li><li>使用DNS轮询实现 水平扩展 反向代理层,也是个不错的选择。</li><li>在网站架构中,除了有DNS轮询技术,还有智能DNS技术(<em>GSLB(Global Server load Blance、全局负载均衡</em>)一种应用),可以实现根据用户IP来就近访问服务器。</li></ul><h3 id="四、HTTPDNS方案"><a href="#四、HTTPDNS方案" class="headerlink" title="四、HTTPDNS方案"></a>四、HTTPDNS方案</h3><h4 id="1、简介"><a href="#1、简介" class="headerlink" title="1、简介"></a>1、简介</h4><ul><li><p><strong>HTTPDNS</strong>:基于HTTP协议DNS服务器发送域名解析请求,替代了<em>基于UDP的DNS协议向运营商Local DNS发起解析请求方式</em>,有效避免Local DNS造成的域名劫持和跨网访问问题,提高域名解析效率。</p></li><li><p>DNS劫持是目前常见的攻击手段,通过攻击运营商的解析服务器,返回假IP地址或让请求失去响应,使得通过域名不能访问或访问的是假网址;</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">国内电信运营商 = 中国电信、中国移动、中国联通三大运营商 + 长城宽带等小的运营商</span><br></pre></td></tr></table></figure></li><li><p>HTTPDNS的主要收益是 <strong>防止DNS的劫持,保障了服务高可用</strong>;精准调度,提升用户体验。</p></li></ul><h4 id="2、HTTPDNS设计中关键点"><a href="#2、HTTPDNS设计中关键点" class="headerlink" title="2、HTTPDNS设计中关键点"></a>2、HTTPDNS设计中关键点</h4><ul><li><strong>安全策略</strong>:HTTPDNS查询是基于标准的HTTP协议,但是如果为了保证安全,可以使用HTTPS;</li><li><strong>IP选取策略</strong>:HTTPDNS服务将最优IP按照顺序下发,客户端默认选取第一个,校验连通性OK就使用,如果不OK,选用下一个校验;</li><li><p><strong>缓存过期策略</strong>:后端动态下发域名的TTL时间,当域名的TTL超时后,如果没有新的IP将继续沿用老的IP,也可以降级成Local DNS返回的IP。</p></li><li><p><strong>批量拉取策略</strong>:在应用冷启动 或 网络切换时候,批量拉取域名和IP列表的映射数据,缓存下来;以便在后续请求中使用到,预期提升精准调度能力。</p></li><li><p><strong>降级策略</strong>:当HTTPDNS服务不可用 & 本地也没有缓存或者缓存失效时,降级成运营商的Local DNS方案;当HTTPDNS服务 & Local DNS服务双双不可用的情况下,可以使用<code>BACKUP IP</code>;</p></li></ul><h4 id="3、总结"><a href="#3、总结" class="headerlink" title="3、总结"></a>3、总结</h4><ul><li>复杂的网络环境中,在避免DNS劫持攻击,提升解析的准确性,降低网络时延等方面,HTTPDNS比传统的DNS方案优秀很多;移动互联网发展到今天,HTTPDNS方案已经是保障大前端服务高可用的必备手段;</li><li>国内如阿里云、腾讯云、百度等厂商都提供此类服务,接入也比较简单(推荐使用阿里云HTTPDNS)。</li></ul><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul><li><a href="https://juejin.im/post/5dc14f096fb9a04a6b204c6f" target="_blank" rel="noopener">全面理解DNS及HTTPDNS</a></li><li><a href="https://tech.meituan.com/2017/08/10/checkout-counter-front-end.html" target="_blank" rel="noopener">前端可用性保障实践</a></li><li><p><a href="https://blog.csdn.net/crazw/article/details/8986581" target="_blank" rel="noopener">域名解析中A记录、CNAME、MX记录、NS记录的区别和联系</a></p></li><li><p><a href="https://www.cnblogs.com/data2value/p/6107380.html" target="_blank" rel="noopener">负载均衡手段之DNS轮询</a></p></li><li><p><a href="https://www.w3cschool.cn/architectroad/architectroad-dns-polling.html" target="_blank" rel="noopener">lvs为何不能完全替代DNS轮询</a>、</p></li><li><p><a href="https://www.w3cschool.cn/architectroad/architectroad-dns-in-architectural-design.html" target="_blank" rel="noopener">DNS在架构设计中的巧用</a></p></li><li><a href="https://laod.cn/dns/public-dns.html" target="_blank" rel="noopener">细数免费公共DNS服务器IP地址大全</a></li></ul>]]></content>
<summary type="html">
<h3 id="一、DNS基础"><a href="#一、DNS基础" class="headerlink" title="一、DNS基础"></a>一、DNS基础</h3><h4 id="1、Domain-Name-和-DNS"><a href="#1、Domain-Name-
</summary>
<category term="架构笔记" scheme="http://buaa0300/nanhuacoder.com/categories/%E6%9E%B6%E6%9E%84%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>漫谈0x01:技术演进和思考</title>
<link href="http://buaa0300/nanhuacoder.com/2020/01/01/Architect-001/"/>
<id>http://buaa0300/nanhuacoder.com/2020/01/01/Architect-001/</id>
<published>2020-01-01T02:31:24.000Z</published>
<updated>2020-01-01T09:20:25.354Z</updated>
<content type="html"><![CDATA[<h4 id="一、研发架构的演进"><a href="#一、研发架构的演进" class="headerlink" title="一、研发架构的演进"></a>一、研发架构的演进</h4><h5 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h5><ul><li>互联网PC时代没有明确的前后端概念,这个时期,开发人员没有前后端边界,前端页面和后端逻辑写在一个工程中,如早期的JavaWeb开发,既要写HTML、CSS、JavaScript还要写Java;</li><li>随着业务的发展,开发者同时处理前后端工作,效率越来越低;而移动互联网Android和iOS的爆发,庞大流量的涌入,业务变得越来越复杂,研发团队急速扩张,这些都急速加剧了前后端的分离;</li><li>我们需要一个全新的研发架构,既要保证用户在前端的体验,又要保证在后端服务的高并低延;将团队拆分成前后端团队,让两端的工程师更加专注自己的领域,独立治理,然后构架出一个全栈式、精益求精的团队。</li></ul><h5 id="2、从移动端、前端到大前端"><a href="#2、从移动端、前端到大前端" class="headerlink" title="2、从移动端、前端到大前端"></a>2、从移动端、前端到大前端</h5><ul><li>移动互联网时代,Android和iOS手机操作系统横空出世,他们被定义为客户端;在客户端高歌猛进的同时,前端也非常热闹,Angular、React、Vue … 等各种各样的新技术层出不穷;</li><li>随着业务的急速发展,对业务快速交付的要求越来越高,Hybrid开发模式、跨端技术方案(React Native、Weex、Flutter)也应运而生;</li><li>此后,客户端和Web端的边界越来越模糊,渐渐演化成大前端。对于后端、PM等来说,他们都是前端。</li></ul><h5 id="3、从后端到大后端"><a href="#3、从后端到大后端" class="headerlink" title="3、从后端到大后端"></a>3、从后端到大后端</h5><ul><li>得益于移动互联网的爆发式增长,海量的前端请求都聚集到后端;后端单个接口QPS从0.1k、到1k、1w甚至到10w+;百万、千万PV,百亿级流量已是常态;后端面临越来越多的挑战;</li><li>后端不是简单的BBF(Backend For Frontend,服务于前端的后端),而是在接收诸多挑战:高并发、低延迟、负载均衡、分布式、消息队列、安全等。</li><li>后端的软件架构也在激进演化,从单体架构到分布式应用,到微服务,甚至走向Serverless架构。</li></ul><h4 id="二、后端软件架构的演进"><a href="#二、后端软件架构的演进" class="headerlink" title="二、后端软件架构的演进"></a>二、后端软件架构的演进</h4><p> 后端的软件架构分以下四种(<em>主流的、炙手可热的依然是微服务架构</em>):</p><h5 id="1、单体架构"><a href="#1、单体架构" class="headerlink" title="1、单体架构"></a>1、单体架构</h5><ul><li>初级结构,典型的三级架构,前端(Web端/客户端) + 中间业务逻辑层 + 数据库层,这是一种典型的<code>Java Spring MVC</code>或者<code>Python Drango</code>框架的应用;</li><li>单体架构的应用比较容易部署、测试,但是缺点也比较明显:<ul><li>复杂性高</li><li>技术债务</li><li>部署频率低</li><li>可靠性差:某个应用Bug,例如死循环、内存溢出等,可能会导致整个应用的崩溃。</li><li>扩展能力受限:单体应用只能作为一个整体进行扩展,无法根据业务模块的需要进行伸缩。</li><li>阻碍技术创新:单体应用往往使用统一的技术平台或方案解决所有的问题, 团队中的每个成员都必须使用相同的开发语言和框架,要想引入新框架或新技术平台会非常困难。</li></ul></li></ul><h5 id="2、分布式应用"><a href="#2、分布式应用" class="headerlink" title="2、分布式应用"></a>2、分布式应用</h5><ul><li>核心是:中间业务逻辑层分布式 + 数据库分布式,是对单体架构的并发扩展,将一个大的系统划分为多个业务模块,业务模块分别部署在不同的服务器上,各个业务模块之间通过接口进行数据交互。</li><li>数据库也大量采用分布式数据库,如Redis。通过 LVS/Nginx 代理应用,将用户请求均衡的负载到不同的服务器上。</li><li>分布式应用大大提高了系统负载均衡能力,极大满足高并发的需求,<strong>优点如下:</strong><ul><li>降低了耦合度:把模块拆分,使用接口通信,降低模块之间的耦合度。</li><li>责任清晰:把项目拆分成若干个子项目,不同的团队负责不同的子项目。</li><li>扩展方便:增加功能时只需要再增加一个子项目,调用其他系统的接口就可以。</li><li>部署方便:可以灵活的进行分布式部署。</li><li>提高代码的复用性:比如Web、Android,iOS使用同一个Service层。</li></ul></li><li><strong>分布式应用的缺点:</strong><ul><li>各个系统之间的交互要使用远程通信,接口开发增大工作量,但是利大于弊。</li></ul></li></ul><h5 id="3、微服务架构【当前の主流】"><a href="#3、微服务架构【当前の主流】" class="headerlink" title="3、微服务架构【当前の主流】"></a>3、微服务架构【当前の主流】</h5><ul><li>主要是<strong>中间业务逻辑层</strong>的解耦,将系统拆分成很多小应用(微服务),微服务可以部署在不同的服务器上,也可以部署在相同的服务器上的不同的容器中,单应用的故障和负载都不会影响到其他应用,其代表框架有<code>Spring Cloud</code>、<code>Dubbo</code>等。</li><li><strong>微服务架构的优势:</strong><ul><li>易于开发和维护:一个微服务只会关注一个特定的业务功能,所以它业务清晰、代码量较少</li><li>单个微服务启动较快:单个微服务代码量较少, 所以启动会比较快。</li><li>局部修改容易部署:对某个微服务进行修改,只需要重新部署这个服务即可。</li><li>技术栈不受限:</li></ul></li><li><strong>微服务架构的缺点:</strong><ul><li>运维要求较高:更多的服务意味着更多的运维投入。在单体架构中,只需要保证一个应用的正常运行。而在微服务中,需要保证几十甚至几百个服务服务的正常运行与协作,这给运维带来了很大的挑战。</li><li>分布式固有的复杂性:使用微服务构建的是分布式系统。对于一个分布式系统,系统容错、网络延迟、分布式事务等都会带来巨大的挑战。</li><li>接口调整成本高:微服务之间通过接口进行通信。如果修改某一个微服务的API,可能所有使用了该接口的微服务都需要做调整。</li><li>重复劳动:很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复。尽管可以使用共享库来解决这个问题(例如可以将这个功能封装成公共组件,需要该功能的微服务引用该组件),但共享库在多语言环境下就不一定行得通了。</li></ul></li></ul><h5 id="4、Serverless架构"><a href="#4、Serverless架构" class="headerlink" title="4、Serverless架构"></a>4、Serverless架构</h5><ul><li>Serverless(无服务器架构)是指服务端逻辑由开发者实现,运行在无状态的计算容器中,由事件触发,完全被第三方管理,其业务层面的状态则存储在数据库或其他介质中。</li><li>Serverless 是<strong>云原生技术</strong>发展的高级阶段,可以使开发者更聚焦在业务逻辑,而减少对基础设施的关注。</li><li><p>Serverless分为以下两个领域:</p><ul><li>BaaS(Backend as a Service)后端即服务;一个个API调用后端或已经实现好的程序逻辑,这些API可以使用Baas提供的服务(如数据存储)。</li><li>FaaS(Functions as a Service)函数即服务,FaaS是无服务器计算的一种形式,当前使用最广泛的是AWS的Lambada。</li></ul></li><li><p>Serverless架构能够让开发者在构建应用的过程中无需关注计算资源的获取和运维,由平台来按需分配计算资源并保证应用执行的SLA(服务等级协议),按照调用次数进行计费,有效的节省应用成本。其优点如下所示:</p><ul><li>低运营成本</li><li>简化设备运维</li><li>提升可维护性</li><li>更快的开发速度</li></ul></li><li><p><strong>Serverless架构的缺点:</strong></p><ul><li>厂商平台绑定,对平台依赖非常高,不能随便迁移或迁移成本很高</li><li>目前成功案例比较少,没有行业标准。</li></ul></li></ul><h4 id="三、几点思考"><a href="#三、几点思考" class="headerlink" title="三、几点思考"></a>三、几点思考</h4><h5 id="1、业务-VS-技术"><a href="#1、业务-VS-技术" class="headerlink" title="1、业务 VS 技术"></a>1、业务 VS 技术</h5><ul><li>业务决定技术的上限:技术是为了解决业务问题的,业务能带来真正的价值,技术才有存在的意义。如果没有双11秒杀活动、春节抢火车票这些事情,流量削峰、削峰填谷这类技术就没有存在的意义。在一个业务高歌猛进的同时,技术也在不断演化;</li><li>技术是业务的护城河:没有技术或技术无法支撑业务的快速迭代、快速试错,再美好的业务终究是水中月、镜中花,一纸空谈而已;</li><li>技术和业务是相互促进的,相互依存。对于技术人来说,选择一个好的业务,对技术成长非常重要;业务中成长远快于从书本中学习,甚至是碾压。</li></ul><h5 id="2、个人-VS-团队"><a href="#2、个人-VS-团队" class="headerlink" title="2、个人 VS 团队"></a>2、个人 VS 团队</h5><ul><li>有一定规模的互联网公司,团队职责都会细分,绝大数人在团队中只是做着或负责很小的一部分事情(俗称<em>拧螺丝</em>),但是职责划分是趋势,是团队进化的必然选择;</li><li>在一个团队中,做什么很重要;但是更重要的是能看到什么;团队给你提供了一个非常大的平台,因此可以接触到非常多的人或事;</li><li>团队中的个体通力合作,共同完成团队目标,不仅提高了个人的影响力,也提高了团队整体的竞争力。</li></ul><h5 id="3、技术人的边界"><a href="#3、技术人的边界" class="headerlink" title="3、技术人的边界"></a>3、技术人的边界</h5><ul><li>而今,技术方向细分,每个技术人都在自己的方向深挖,这是非常必要的,因为这样才能组成一个全栈式、精益求精的团队,但是这还远远不够;</li><li>技术人应该不设置边界(不是说没有边界):技术人应该跳出自己的领域,观察其他人、其他技术团队,甚至业务团队在做什么;主动去了解这条业务线上不同角色的职责,然后完善或扩张自己的边界;</li><li>从当今的技术发展趋势来看,每个团队负责”小型”服务,”小型”服务内完成闭环,对外提供服务能力。而”小型”服务内的技术选型,技术栈是灵活的,甚至可以实现在”小型”服务内 技术 + 业务闭环。</li></ul><h5 id="4、技术专家-VS-架构师"><a href="#4、技术专家-VS-架构师" class="headerlink" title="4、技术专家 VS 架构师"></a>4、技术专家 VS 架构师</h5><ul><li><strong>技术专家追求的是深度</strong>,需要专注于某些领域,某一类技术,需要对这方面了解非常透彻,适合做技术专家的领域有:安全、搜索、应用程序框架、缓存、JVM、分布式、RPC框架等,或更加底层的数据库开发,Web服务器开发, 编译器开发等。</li><li><p><strong>架构师追求的是广度</strong>,对深度并没有极致要求, 你可能并不了解一个系统的内部细节实现,但是一定要知道这个系统的特点、能力、性能和适用范围;在工作中,由点及面, 着眼系统整体和业务的全局,培养出设计系统架构的能力:把需求转化成合适的技术方案,然后推动方案的落地。</p></li><li><p>根据自己的优劣势选择合适自己的方向,然后刻意练习,不断完善这方面的能力。</p></li></ul><h4 id="参考文章"><a href="#参考文章" class="headerlink" title="参考文章"></a>参考文章</h4><ul><li><p><a href="https://mp.weixin.qq.com/s?__biz=MzI0MDQ4MTM5NQ==&mid=2247491020&idx=2&sn=19d29c54d39ae9a08118dd3e9e3fedc8&chksm=e91b78d0de6cf1c657651b4f185a254eeee607eae910e90231e010035247376a7febd2140eaf&token=1182852775&lang=zh_CN#rd" target="_blank" rel="noopener">漫谈四种主流软件架构演进史</a></p></li><li><p><a href="https://jimmysong.io/posts/what-is-serverless/" target="_blank" rel="noopener">什么是Serverless(无服务器)架构</a></p></li><li><a href="https://mp.weixin.qq.com/s/W4Slu5F__AQtQTSOEFOsfg" target="_blank" rel="noopener">Flutter+Serverless端到端研发架构实践</a></li></ul>]]></content>
<summary type="html">
<h4 id="一、研发架构的演进"><a href="#一、研发架构的演进" class="headerlink" title="一、研发架构的演进"></a>一、研发架构的演进</h4><h5 id="1、概述"><a href="#1、概述" class="headerli
</summary>
<category term="架构笔记" scheme="http://buaa0300/nanhuacoder.com/categories/%E6%9E%B6%E6%9E%84%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>7分钟了解PNG图片</title>
<link href="http://buaa0300/nanhuacoder.com/2019/08/25/Knowledge-png/"/>
<id>http://buaa0300/nanhuacoder.com/2019/08/25/Knowledge-png/</id>
<published>2019-08-25T02:31:24.000Z</published>
<updated>2020-02-02T09:43:41.637Z</updated>
<content type="html"><![CDATA[<p><em>Question</em>:</p><ul><li><p>PNG图片结构是什么样的</p></li><li><p>PNG规范中定义PNG是无损压缩(Lossless Compression)的图片格式,那么PNG支持有损压缩吗?</p></li></ul><h4 id="一、PNG概述"><a href="#一、PNG概述" class="headerlink" title="一、PNG概述"></a>一、PNG概述</h4><h5 id="1、PNG简介"><a href="#1、PNG简介" class="headerlink" title="1、PNG简介"></a>1、PNG简介</h5><ul><li><p>PNG(Portable Network Graphics,便携式网络图型)是一种无损压缩的图片格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性(<em>iOS的绝大部分切图都是PNG格式</em>)。</p></li><li><p>PNG图片主要有三个类型,分别为 PNG 8、 PNG 24 和 PNG 32;其中 <code>PNG 8</code>支持两种不同的透明形式,索引透明和<code>Alpha</code>透明、 <code>PNG 24</code>不支持透明和 <code>PNG 32</code>在<code>24</code>位基础上增加了<code>8</code>bits透明通道。</p></li><li>PNG规范可见: <a href="https://www.w3.org/TR/2003/REC-PNG-20031110/" target="_blank" rel="noopener">Portable Network Graphics (PNG) Specification (Second Edition)</a></li></ul><h5 id="2、PNG结构"><a href="#2、PNG结构" class="headerlink" title="2、PNG结构"></a>2、PNG结构</h5><ul><li>PNG结构如下: header,chunk,chunk,chunk….chunk; 其中,<code>header</code>代表png图片的头,png文件头一般都是由固定的8个字节组成<code>89 50 4E 47 OD 0A 1A 0A</code>而<code>chunk</code>代表png图片的数据块。</li></ul><p><img src="http://upload.ouliu.net/i/20190816182301odda6.jpeg" alt="PNG编码结构"></p><p><img src="http://upload.ouliu.net/i/20190816181318ythij.jpeg" alt="二进制查看PNG图片"></p><h5 id="3、PNG的数据块"><a href="#3、PNG的数据块" class="headerlink" title="3、PNG的数据块"></a>3、PNG的数据块</h5><ul><li>PNG的数据块由四个数据域组成:长度、数据块类型码、<strong>数据块数据</strong>和循环冗余校验;而数据块分成两种:<strong>关键数据块</strong>和<strong>可选数据块</strong>,关键数据块包括:文件头数据块、调色板数据块、图像数据块和图像结束数据块</li><li><p>对于用户可见的部分,真正和展现有关就是<strong>图像数据块</strong>中的<strong>数据块区域</strong>,因此,我们就需要注意<strong>有没有在别的数据块中引入了不必要的数据</strong>。</p></li><li><p>IHDR数据块:一个PNG图片中只能有一个IHDR数据块,它是PNG图片中第一个数据块,共13个字节(0000 000d),具体信息如下:</p></li></ul><table><thead><tr><th>域名称</th><th>字节数(bytes)</th><th>说明</th></tr></thead><tbody><tr><td>width</td><td>4</td><td>图像宽度,以像素为单位</td></tr><tr><td>height</td><td>4</td><td>图像高度,以像素为单位</td></tr><tr><td>bit depth</td><td>1</td><td>表示一个通道的总位数</td></tr><tr><td>color type</td><td>1</td><td>颜色类型. <code>[0]:灰度图像, 1,2,4,8或16</code> <code>[2]:真彩色图像,8或16</code> <code>[3]:索引彩色图像,1,2,4或8</code> <code>[4]:带α通道数据的灰度图像,8或16</code> <code>[6]:带α通道数据的真彩色图像,8或16</code></td></tr><tr><td>compression method</td><td>1</td><td>压缩方法(LZ77派生算法)</td></tr><tr><td>filter method</td><td>1</td><td>滤波器方法</td></tr><tr><td>Interlace method</td><td>1</td><td>隔行扫描方法. <code>0:非隔行扫描</code> <code>1: Adam7(7遍隔行扫描方法)</code></td></tr></tbody></table><ul><li><code>0000 000d</code>是iHDR数据块的长度,为13,<code>4948 4452</code>是数据块的type,为IHDR,之后紧跟着是data, <code>0000 02bc</code>是图片的宽度,<code>0000 03a5</code>是高度,<code>08</code>是bit depth,也就是一个通道是8位,<code>06</code>是color type,这里表示图片是真彩色,<code>00</code>是压缩方法,png中目前只有一种,也就是LZ77派生的算法,<code>00</code>是滤波器方法,表示不使用,<code>00</code>是隔行扫描方法,代表不扫描。<code>8f 1434 a4</code>是四个字节的CRC校验码。</li><li>IDAT数据块:存放的就是图像的一个个像素,一张图像中可以存在多个IDAT数据块,这里的数据会被<code>隔行扫描方法(Interlace method)</code>、<code>滤波器(filtering)</code>和<code>压缩算法(compression)</code>处理。</li></ul><h5 id="3、总结"><a href="#3、总结" class="headerlink" title="3、总结"></a>3、总结</h5><ul><li>通过了解PNG的结果,可以知道,减少PNG图片大小可以通过去除<code>PNG</code>文件中不需要的信息,或 选择正确的PNG格式(如,如果图片中没有<code>Alpha</code>通道,那么就应当使用<code>PNG 24</code>,而不是使用<code>PNG 32</code>。类似,如果是灰度的图片,那么应当使用<code>PNG 8</code>) </li></ul><h4 id="二、PNG的压缩原理"><a href="#二、PNG的压缩原理" class="headerlink" title="二、PNG的压缩原理"></a>二、PNG的压缩原理</h4><h5 id="1、无损压缩-VS-有损压缩"><a href="#1、无损压缩-VS-有损压缩" class="headerlink" title="1、无损压缩 VS 有损压缩"></a>1、无损压缩 VS 有损压缩</h5><ul><li>无损压缩(Lossless Compression):数据压缩格式方法之一,指数据经过压缩后,信息不受损失,还能完全恢复到压缩前的原样。</li><li>有损压缩(Lossy compression):数据压缩格式方法之一,指数据经过压缩、解压的数据会与原始数据不同但是非常接近。</li><li>无损压缩和有损压缩是相对的,有损压缩将次要的信息数据舍弃,牺牲一些质量来减少数据量、提高压缩比。</li><li>PNG压缩过程分为两个阶段:<strong>Prediction</strong>(预解析) 和 <strong>Compression</strong>(压缩)</li></ul><p><img src="https://s2.ax1x.com/2019/08/22/m0yZSe.png" alt="PNG图片压缩"></p><h5 id="2、PNG压缩之Prediction"><a href="#2、PNG压缩之Prediction" class="headerlink" title="2、PNG压缩之Prediction"></a>2、PNG压缩之Prediction</h5><ul><li><p>简单理解:在这个阶段,对图片做一些预处理,方便后续的压缩处理;</p></li><li><p>每次会处理图片中一行的数据,首先通过<code>Filter</code>处理这一行当中<strong>每一个的像素点中每条通道的值</strong>,然后交<strong>由差分处理器</strong>来重新计算该通道的值。</p></li><li><p><strong>差分处理器</strong>会根据这个像素点上通道和之前或者之上像素点对应通道值之间的差异,进行<strong>差分编码</strong>,也就是说,如果原本相邻像素点之间通道的值之间很接近,那么我们就会获得很多的<code>1,0,-1</code>这种很小的值。(差分编码器比较的是像素点之间对应通道的值,而并不是整个像素点)</p></li><li><p>整个<code>Prediction</code>阶段的目的,也就是<strong>选择合适的差分处理器,让最终的编码结果出现尽可能多的零值和重复值</strong>,这一结果将会影响到<code>Compression</code>阶段的压缩率。</p></li></ul><h5 id="3、PNG压缩之Compression"><a href="#3、PNG压缩之Compression" class="headerlink" title="3、PNG压缩之Compression"></a>3、PNG压缩之Compression</h5><ul><li><p>在<code>Prediction</code>处理完毕之后,再将这一转换的结果输出给<code>Deflate</code>,<code>Deflate</code>执行真正的压缩操作,它会通过<code>LZ77</code>和<code>Huffman</code>对图像进行编码,最后将处理之后的结果保存。在<code>Compression</code>阶段,它最终的压缩率会受到两方面的影响:</p><ul><li><p><strong>Prediction 的处理结果</strong>:对于颜色相近的区域,也就是有很多零值的区域,那么压缩率将会更高,而如果颜色之间差异很大,那么压缩效果将不尽人意。</p></li><li><p><strong>Deflate 每一行的匹配情况</strong>:整个处理过程是按行来处理的。而在处理每一行的数据时,<code>Deflate</code>把处理的符号数限制为<code>3 ~ 258</code>,也就是说,最大的压缩率为<code>1032:1</code>,当出现符号数小于<code>3</code>个时,那么就有可能出现无法匹配的情况,因此,对于图片宽度的改变将有可能影响最终压缩的效果。</p></li></ul></li></ul><h5 id="4、总结"><a href="#4、总结" class="headerlink" title="4、总结"></a>4、总结</h5><ul><li>从上述内容可以知道,减少PNG的图片大小,可以通过PNG压缩优化,压缩优化有两个方向:<ul><li>优化差分编码器,使得经过差分编码后的图像有尽可能多的零值和相同的值</li><li>优化<code>Deflate</code>的算法,获得更高的压缩率</li></ul></li></ul><h4 id="三、PNG的有损压缩方案"><a href="#三、PNG的有损压缩方案" class="headerlink" title="三、PNG的有损压缩方案"></a>三、PNG的有损压缩方案</h4><h5 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h5><ul><li>目前大部分PNG的压缩采用基于LZ77派生算法,使得它压缩比率更高,生成的文件体积更小,并且不损失数据。</li><li>但是有些大神不能接受无损压缩的压缩率,搞了些无损压缩算法,并在工程中得到了应用。</li></ul><h5 id="2、TinyPNG"><a href="#2、TinyPNG" class="headerlink" title="2、TinyPNG"></a>2、TinyPNG</h5><ul><li><p>这是一个提供PNG有损压缩的平台,地址:<a href="https://tinypng.com/" target="_blank" rel="noopener">https://tinypng.com/</a></p></li><li><p>从官网公开的信息来看,TinyPNG主要是使用Quantization的技术,通过合并图片中相似的颜色,通过将 24 位的 PNG 图片压缩成小得多的 8 位色值的图片,并且去掉了图片中不必要的 metadata(元数据,从 Photoshop 等工具中导出的图片都会带有此类信息),这种方式几乎能完美支持原图片的透明度。</p></li><li>很可惜,没有公开PNG的有损压缩算法。</li></ul><h5 id="3、pngquant算法及其应用"><a href="#3、pngquant算法及其应用" class="headerlink" title="3、pngquant算法及其应用"></a>3、<a href="https://github.com/kornelski/pngquant" target="_blank" rel="noopener">pngquant</a>算法及其应用</h5><ul><li>PNG有损压缩算法,详细介绍地址在:<a href="https://pngquant.org/,源码在" target="_blank" rel="noopener">https://pngquant.org/,源码在</a></li></ul><p><img src="https://s2.ax1x.com/2019/08/25/mgod3T.jpg" alt="mgod3T.jpg"></p><ul><li>ImageAlpha也采用了pngquant算法,设计师使用的PNGyu也采用了pngquant算法;甚至有猜测,TinyPNG可能综合利用了pngquant、optipng、advpng,才获得很好的压缩效果。</li><li>这类无损压缩方案在压缩PNG图片,对最终的包大小优化,是有收益的(<em>Xcode构建包的时候,会压缩PNG;如果使用ImageOptim这类无损压缩工具来压缩PNG是没有什么收益的</em>)</li></ul><p><img src="https://s2.ax1x.com/2019/08/25/mgoX28.jpg" alt="mgoX28.jpg"></p><ul><li><strong>pngquant算法的核心</strong>:通过Vector Quantization (矢量量化)减少图片中颜色的种类<ul><li>如果图像中的颜色种类小于<code>256</code>,那么我们可以把它转换为索引<code>PNG</code>格式,而如果图片原本的颜色大于<code>256</code>种,那么可以通过<strong>矢量量化</strong>的方法来创建一个索引<code>PNG</code>格式。</li><li>在矢量量化的过程中,会把所有的像素基于它们之间颜色的相似程度进行分组,一个组内像素的颜色会比较接近。之后根据组内的所有颜色,计算出一个中心点颜色,组内的所有颜色会被替换成为该中心点的颜色。</li><li>矢量量化会通过<strong>将相近颜色替换成同一种颜色的方法,来减少图片中颜色的种类</strong>,因此有可能会使得图片失真。</li></ul></li></ul><p><strong>参考</strong>:<a href="https://www.jianshu.com/p/324744087e24" target="_blank" rel="noopener">图片压缩知识梳理(2) - 减小 PNG 大小</a></p>]]></content>
<summary type="html">
<p><em>Question</em>:</p>
<ul>
<li><p>PNG图片结构是什么样的</p>
</li>
<li><p>PNG规范中定义PNG是无损压缩(Lossless Compression)的图片格式,那么PNG支持有损压缩吗?</p>
</li>
</ul
</summary>
</entry>
<entry>
<title>Cocoapods使用小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/24/iOS-Cocoapods01/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/24/iOS-Cocoapods01/</id>
<published>2019-04-24T11:51:41.000Z</published>
<updated>2020-02-02T09:41:47.991Z</updated>
<content type="html"><![CDATA[<p><em>不识庐山真面目,只缘身在此山中</em></p><h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、CocoaPods是什么?"><a href="#1、CocoaPods是什么?" class="headerlink" title="1、CocoaPods是什么?"></a>1、CocoaPods是什么?</h5><ul><li><p>CocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。</p></li><li><p>iOS程序依赖管理工具还有<strong>Carthage</strong>(Carthage 是由 Swift 语言写的,只支持动态框架,只支持 iOS8+),工程中可以Carthage和CocoaPods一起存在使用,但是建议使用一种,个人推荐CocoaPods。</p></li><li><p>CocoaPods<a href="https://github.com/CocoaPods/CocoaPods" target="_blank" rel="noopener">项目源码</a></p></li></ul><h5 id="2、下载和安装CocoaPods"><a href="#2、下载和安装CocoaPods" class="headerlink" title="2、下载和安装CocoaPods"></a>2、下载和安装CocoaPods</h5><ul><li><p>CocoaPods需要Ruby环境,安装Ruby和修改ruby镜像命令如下:(<em>Mac自带 Ruby</em>)</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><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="comment">#安装ruby </span></span><br><span class="line">gem install ruby </span><br><span class="line"></span><br><span class="line"><span class="comment">#升级ruby </span></span><br><span class="line">sudo gem update --system</span><br><span class="line"></span><br><span class="line"><span class="comment">#检查版本</span></span><br><span class="line">ruby -v</span><br><span class="line"></span><br><span class="line"><span class="comment"># 更新ruby镜像</span></span><br><span class="line">gem sources --remove <span class="symbol">https:</span>/<span class="regexp">/rubygems.org/</span></span><br><span class="line">gem sources -a <span class="symbol">https:</span>/<span class="regexp">/ruby.taobao.org/</span></span><br><span class="line"><span class="comment">#检查镜像是否更新成功</span></span><br><span class="line">gem sources -l</span><br></pre></td></tr></table></figure></li><li><p>下载和卸载命令</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><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="comment">#下载最新版本</span></span><br><span class="line">sudo gem install cocoapods (sudo gem install -n /usr/local/bin cocoapods)</span><br><span class="line"></span><br><span class="line"><span class="comment">#下载指定版本</span></span><br><span class="line">sudo gem install cocoapods -v <span class="number">1.6</span>.<span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#卸载</span></span><br><span class="line">sudo gem uninstall cocoapods </span><br><span class="line"></span><br><span class="line"><span class="comment">#卸载指定版本</span></span><br><span class="line">sudo gem uninstall cocoapods -v <span class="number">1.6</span>.<span class="number">1</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#检查pod版本</span></span><br><span class="line">pod --version</span><br></pre></td></tr></table></figure></li><li><p>还可以在工程目录下,使用<code>Gemfile</code>来指定使用cocoapods的版本。</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gem <span class="string">"cocoapods"</span>, <span class="string">'~> 1.6.1'</span></span><br></pre></td></tr></table></figure></li></ul><h5 id="3、CocoaPods应用"><a href="#3、CocoaPods应用" class="headerlink" title="3、CocoaPods应用"></a>3、CocoaPods应用</h5><ul><li>为项目引入优秀的第三方库,如SDWebImage、AFNetworking、YYCache</li><li>创建私有pods,将项目中子模块拆分、沉库(内部使用)</li><li>创建公有pods,将好的功能模块分享出去</li></ul><h4 id="二、Podfile基础使用"><a href="#二、Podfile基础使用" class="headerlink" title="二、Podfile基础使用"></a>二、Podfile基础使用</h4><h5 id="1、基础"><a href="#1、基础" class="headerlink" title="1、基础"></a>1、基础</h5><ul><li><p>创建Podfile文件 </p><figure class="highlight ruby"><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">#进入项目根路径中执行,生成Podfile文件(如果有Podfile文件不用执行pod init)</span></span><br><span class="line">pod init</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:Podfile文件详细描述了一个或多个工程中targets的依赖关系</p></li><li><p>Podfile添加第三方库依赖</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><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">target <span class="string">'QSAppDemo'</span> <span class="keyword">do</span></span><br><span class="line"> pod <span class="string">'AFNetworking'</span></span><br><span class="line"> pod <span class="string">'YYModel'</span>, <span class="string">'~> 1.0.4'</span></span><br><span class="line">pod <span class="string">'OOMDetector'</span>, <span class="string">'1.3'</span></span><br><span class="line"> <span class="comment"># Debug模式下生效</span></span><br><span class="line"> pod <span class="string">'FLEX'</span>, <span class="string">'~> 2.0'</span>, <span class="symbol">:configurations</span> => [<span class="string">'Debug'</span>]</span><br><span class="line"> pod <span class="string">'WebViewJavascriptBridge'</span>, <span class="symbol">:git</span> => <span class="string">'https://github.com/marcuswestin/WebViewJavascriptBridge.git'</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure></li><li><p>下载和安装第三方库</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod install</span><br></pre></td></tr></table></figure><p><strong>说明1</strong>:下载成功后,使用CocoaPods 生成的 .xcworkspace 文件来打开工程;每次更改了 Podfile 文件,要重新执行一次pod update命令;</p><p><strong>说明2</strong>:发生执行<code>pod install</code>或<code>pod update</code>都卡在<code>Analyzing dependencies</code>的情况,是因为要升级CocoaPods的spec仓库,命令后添加<code>--verbose --no-repo-update</code> 参数可以省略此步。</p></li></ul><h5 id="2、pod指定依赖项版本范围"><a href="#2、pod指定依赖项版本范围" class="headerlink" title="2、pod指定依赖项版本范围"></a>2、pod指定依赖项版本范围</h5><ul><li><p>如果依赖项后不指定版本,默认取最新版本</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod 'OOMDetector'</span><br></pre></td></tr></table></figure></li><li><p>如果依赖项后跟上特定版本,就是使用指定版本 </p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod 'OOMDetector'</span><br></pre></td></tr></table></figure></li><li><p><code>> 0.1</code> 高于0.1版本(不包含0.1版本)的任意一个版本</p></li><li><p><code>>= 0.1</code> 高于0.1版本(包含0.1版本)的任意一个版本</p></li><li><p><code>< 0.1</code> 低于0.1版本(不包含0.1版本)的任意一个</p></li><li><p><code><= 0.1</code>低于0.1版本(包含0.1版本)的任意一个</p></li><li><p><code>~> 0.1.2</code> 版本 0.1.2的版本到0.2 ,不包括0.2。这个基于你指定的版本号的最后一个部分。这个例子等效于>= 0.1.2并且 <0.2.0,并且始终是你指定范围内的最新版本。</p></li></ul><h5 id="3、pod制定依赖库的分支或节点"><a href="#3、pod制定依赖库的分支或节点" class="headerlink" title="3、pod制定依赖库的分支或节点"></a>3、pod制定依赖库的分支或节点</h5><ul><li><p>引入master分支(默认)</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="string">'AFNetworking'</span>, <span class="symbol">:git</span> => <span class="string">'https://github.com/AFNetworking/AFNetworking.git'</span></span><br></pre></td></tr></table></figure></li><li><p>引入指定的分支</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="string">'AFNetworking'</span>, <span class="symbol">:git</span> => <span class="string">'https://github.com/AFNetworking/AFNetworking.git'</span>, <span class="symbol">:branch</span> => <span class="string">'develop'</span></span><br></pre></td></tr></table></figure></li><li><p>引入某个节点的代码</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="string">'AFNetworking'</span>, <span class="symbol">:git</span> => <span class="string">'https://github.com/AFNetworking/AFNetworking.git'</span>, <span class="symbol">:tag</span> => <span class="string">'2.7.0'</span></span><br></pre></td></tr></table></figure></li><li><p>引入某个特殊的提交节点</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="string">'AFNetworking'</span>, <span class="symbol">:git</span> => <span class="string">'https://github.com/gowalla/AFNetworking.git'</span>, <span class="symbol">:commit</span> => <span class="string">'685e31a31bb1ebce3fdb5a752e392dfd8263e169'</span></span><br></pre></td></tr></table></figure></li></ul><h5 id="4、关于Podfile中一些配置说明"><a href="#4、关于Podfile中一些配置说明" class="headerlink" title="4、关于Podfile中一些配置说明"></a>4、关于Podfile中一些配置说明</h5><ul><li><p><strong>Source</strong>:指定pod的来源。如果不指定source,默认是使用CocoaPods官方的source</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># 使用官方默认地址(默认)</span><br><span class="line">source 'https://github.com/CocoaPods/Specs.git'</span><br></pre></td></tr></table></figure></li><li><p><strong>use_frameworks!</strong>:使用此命令会在Pods工程下的Frameworks目录下生成依赖库的framework,如果不使用,会在Pods工程下的Products目录下生成.a的静态库。</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">target <span class="string">'QSAppDemo'</span> <span class="keyword">do</span></span><br><span class="line"> use_frameworks!</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure></li></ul><p>关于Pofile的详细语法可以参考: <a href="https://guides.cocoapods.org/syntax/podfile.html" target="_blank" rel="noopener">Podfile Syntax Reference</a> 或 <a href="https://www.jianshu.com/p/b8b889610b7e" target="_blank" rel="noopener">Podfile文件用法详解</a></p><h4 id="三、Cocoapods建立私有Pod库"><a href="#三、Cocoapods建立私有Pod库" class="headerlink" title="三、Cocoapods建立私有Pod库"></a>三、Cocoapods建立私有Pod库</h4><h5 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h5><ul><li><p>由于业务需要,需要开发某业务SDK,方便公司内业务接入,选择使用创建私有Pod库;</p></li><li><p>本质上,创建公有Pod库或者私有Pod库原理是一样的,不一样的是:两者的版本索引查询方式不一样, 公有库的podspec由<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2FCocoaPods%2FSpecs" target="_blank" rel="noopener">CocoaPods/Specs</a>管理, 而内部私有使用的pod库需要公司内部建立一个仓库来管理podspec。</p></li><li><p>私有spec repo的构建形式有两种, 一种是私有git服务器上面创建,一种是本机创建;一般是在公司内部搭建的git服务器上面创建私有spec repo</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><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#将远程的私有版本仓库添加到本地</span></span><br><span class="line">pod repo add qs_private_pods_specs <span class="symbol">https:</span>/<span class="regexp">/github.com/buaa</span>030<span class="number">0</span>/qs_private_pods_specs.git</span><br><span class="line"></span><br><span class="line"><span class="comment">#添加成功后,打开~/.cocoapods/repos可以看到新增的qs_private_pods_specs</span></span><br></pre></td></tr></table></figure></li></ul><h5 id="2、创建私有Pod库"><a href="#2、创建私有Pod库" class="headerlink" title="2、创建私有Pod库"></a>2、创建私有Pod库</h5><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><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">pod lib create QSBizSDK <span class="comment"># QSBizSDK是SDK的名字</span></span><br><span class="line"></span><br><span class="line">What platform <span class="keyword">do</span> you want to use?? [ iOS / macOS ]</span><br><span class="line"> > iOS</span><br><span class="line"></span><br><span class="line">What language <span class="keyword">do</span> you want to use?? [ Swift / ObjC ]</span><br><span class="line"> > ObjC</span><br><span class="line"></span><br><span class="line">Would you like to <span class="keyword">include</span> a demo application with your library? [ Yes / No ]</span><br><span class="line"> > YES</span><br><span class="line"></span><br><span class="line">Which testing frameworks will you use? [ Specta / Kiwi / None ]</span><br><span class="line"> > None</span><br><span class="line"></span><br><span class="line">Would you like to <span class="keyword">do</span> view based testing? [ Yes / No ]</span><br><span class="line"> > No</span><br><span class="line"></span><br><span class="line">What is your <span class="class"><span class="keyword">class</span> <span class="title">prefix?</span></span></span><br><span class="line"> > QS</span><br><span class="line"></span><br><span class="line">......</span><br></pre></td></tr></table></figure><p><strong>说明1</strong>:<code>pod lib create QSBizSDK</code>其实是下载了一个pod模板,然后在内部通过更改.podspec文件的配置定制化自己的pod,podspec是描述pod的说明信息的。<br><strong>说明2</strong>: <code>pod lib create ProjectName</code>其实使用了默认参数,补全的话<code>pod lib create ProjectName --template-url=https://github.com/CocoaPods/pod-template.git</code></p><p><strong>说明3</strong>: 将项目托管到Git,私有库一般上传到内部的仓库中。</p><h5 id="3、podspec编辑"><a href="#3、podspec编辑" class="headerlink" title="3、podspec编辑"></a>3、podspec编辑</h5><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><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">Pod::Spec.new <span class="keyword">do</span> <span class="params">|s|</span></span><br><span class="line"> s.name = <span class="string">'QSBizSDK'</span></span><br><span class="line"> s.version = <span class="string">'1.0.0'</span></span><br><span class="line"> s.summary = <span class="string">'A short description of QSBizSDK.'</span></span><br><span class="line"> s.description = <span class="string"><<-DESC</span></span><br><span class="line"><span class="string">Add long description of the pod here.</span></span><br><span class="line"><span class="string">DESC</span></span><br><span class="line"> s.homepage = <span class="string">'https://github.com/buaa0300/QSBizSDK'</span></span><br><span class="line"> s.license = { <span class="symbol">:type</span> => <span class="string">'MIT'</span>, <span class="symbol">:file</span> => <span class="string">'LICENSE'</span> }</span><br><span class="line"> s.author = {<span class="string">'南华coder'</span> => <span class="string">'[email protected]'</span>}</span><br><span class="line"> s.source = { <span class="symbol">:git</span> => <span class="string">'https://github.com/buaa0300/QSBizSDK.git'</span>, <span class="symbol">:tag</span> => s.version.to_s }</span><br><span class="line"> </span><br><span class="line"> s.ios.deployment_target = <span class="string">'8.0'</span></span><br><span class="line"> s.default_subspec = <span class="string">'QSCore'</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 各个子模块的个自的源码路径</span></span><br><span class="line"> s.subspec <span class="string">'QSCore'</span> <span class="keyword">do</span> <span class="params">|qscore|</span></span><br><span class="line"> qscore.source_files = <span class="string">'Classes/QSCore/**/*'</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># A a 一定要不一样,是别名,添加好暴露的头文件</span></span><br><span class="line"> s.subspec <span class="string">'A'</span> <span class="keyword">do</span> <span class="params">|a|</span></span><br><span class="line"> a.source_files = <span class="string">'Classes/QSCore/A/**/*'</span></span><br><span class="line"> a.public_header_files=<span class="string">'Classes/QSCore/A/*.h'</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 可以添加内部自己模块,或者外部的依赖</span></span><br><span class="line"> s.subspec <span class="string">'B'</span> <span class="keyword">do</span> <span class="params">|b|</span></span><br><span class="line"> a.source_files = <span class="string">'Classes/QSCore/B/**/*'</span></span><br><span class="line"> a.public_header_files=<span class="string">'Classes/QSCore/B/*.h'</span></span><br><span class="line"> b.dependency <span class="string">'Classes/QSCore/A'</span></span><br><span class="line"> b.dependency <span class="string">'AFNetworking'</span>, <span class="string">'~> 3.0'</span> </span><br><span class="line"> b.frameworks = <span class="string">'SystemConfiguration'</span>, <span class="string">'CFNetwork'</span>, <span class="string">'PassKit'</span></span><br><span class="line"> b.libraries = <span class="string">'c++'</span>, <span class="string">'z'</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"> s.subspec <span class="string">'C'</span> <span class="keyword">do</span> <span class="params">|c|</span></span><br><span class="line"> c.source_files = <span class="string">'Classes/QSCore/C/**/*'</span></span><br><span class="line"> c.dependency <span class="string">'Classes/QSCore/B'</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p><strong>说明</strong>:对于一个功能比较多的SDK,podspec中需要做的事情比较多,如利用<strong>subspec</strong>将代码模块化,方便使用时按需导入;不同的模块可能依赖framework、静态库、内部其他模块,也需要处理好;</p><p><strong>说明2</strong>:可以使用<code>pod spec lint xxx.podspec</code> 来检查podspec的配置有效性</p><h5 id="4、podspec加入私有Sepc-repo"><a href="#4、podspec加入私有Sepc-repo" class="headerlink" title="4、podspec加入私有Sepc repo"></a>4、podspec加入私有Sepc repo</h5><ul><li>公有库使用trunk方式将.podspec文件发布到<a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2FCocoaPods%2FSpecs" target="_blank" rel="noopener">CocoaPods/Specs</a>, 内部的pod组件库则是添加到私有的Spec repo中去, 在终端执行:</li></ul><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod repo push qs_private_pods_specs QSBizSDK.podspec</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:添加成功之后<code>qs_private_pods_specs</code>中会包含QSBizSDK库的podspec信息, 可以前往<code>~/.cocoapods/repos</code>下的<code>qs_private_pods_specs</code>文件夹中查看, 同时git服务器中的远端也更新了.</p><h5 id="5、更多参考"><a href="#5、更多参考" class="headerlink" title="5、更多参考"></a>5、更多参考</h5><ul><li><a href="https://juejin.im/entry/59dd94b06fb9a0451463030b" target="_blank" rel="noopener">Cocoapods原理总结</a></li><li><a href="https://juejin.im/entry/59269b4f0ce46300574f7d86" target="_blank" rel="noopener">教你一步步用 CocoaPods 创建远程公有库</a></li><li><a href="https://juejin.im/entry/5864fd801b69e6006cf76a64" target="_blank" rel="noopener">CocoaPods 创建公有和私有 Pod 库方法总结</a></li><li><a href="https://www.jianshu.com/p/0c640821b36f" target="_blank" rel="noopener">CocoaPods 私有仓库的创建(超详细)</a></li></ul><h4 id="四、Cocoapods重要的”新”特性"><a href="#四、Cocoapods重要的”新”特性" class="headerlink" title="四、Cocoapods重要的”新”特性"></a>四、Cocoapods重要的”新”特性</h4><h5 id="1、cocoapods1-4-0支持static-framework(静态框架)"><a href="#1、cocoapods1-4-0支持static-framework(静态框架)" class="headerlink" title="1、cocoapods1.4.0支持static framework(静态框架)"></a>1、cocoapods1.4.0支持static framework(静态框架)</h5><ul><li><p>1.4.0之前,只能通过<code>use_framework!</code>只能发布动态framework,<strong>新增的特性解决了过去动态framework不能依赖静态库的弊端</strong>,现在的静态framework可以依赖静态库,也可以依赖通过<code>vendored_frameworks</code>发布的第三方框架.(<em>vendored_frameworks和vendored_library是在podspec文件内使用的属性,用法是声明包含的第三方framework和library.</em>)</p></li><li><p>static framework和library区别:framework是对于library、头文件和资源等内容的封装;library可以是动态或者静态的,静态库在构建时期链接,但是动态库是在运行时才进行加载。</p></li><li><p>动态库不能依赖静态库(library)是因为<strong>静态库(library)不需要在运行时再次加载,如果多个动态库依赖同一个静态库,会出现多个静态库的拷贝,而这些拷贝本身只是对于内存空间的消耗</strong>。</p></li><li><p>在1.4.0之前,资源只能通过动态库的方式构建,所以不能依赖vendored_framework的库.而且对于vendored_framework的二进制库,无法在转换成资源pod时仍保持动态性</p></li><li><p>使用静态框架的支持,用法简单,只需要在podspec文件内,声明如下即可</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s.static_framework = <span class="literal">true</span></span><br></pre></td></tr></table></figure></li><li><p>更多参考:<a href="https://www.jianshu.com/p/3d0ae289dee0" target="_blank" rel="noopener">cocoapods的静态库和动态库</a>、<a href="https://www.jianshu.com/p/544df88b6a1e" target="_blank" rel="noopener">CocoaPods 动/静态库混用封装组件化</a></p></li></ul><h5 id="2、CocoaPods-1-7-支持generate-multiple-pod-projects"><a href="#2、CocoaPods-1-7-支持generate-multiple-pod-projects" class="headerlink" title="2、CocoaPods 1.7 支持generate_multiple_pod_projects"></a>2、CocoaPods 1.7 支持generate_multiple_pod_projects</h5><ul><li>引入了一个 generate_multiple_pod_projects 的选项,可以让每个依赖都作为一个单独的项目引入,大大增加了解析速度</li><li>开启的方式很简单,只要在 Podfile 里加入这一行就可以了:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">install! 'cocoapods', generate_multiple_pod_projects: true</span><br></pre></td></tr></table></figure><ul><li>更多参考<a href="https://mp.weixin.qq.com/s/R4KQWpdMgsnx918AhgXxCg" target="_blank" rel="noopener">开启 Cocoapods 新选项,加快项目索引速度</a></li></ul><h5 id="3、CocoaPods-1-8-Beta-将CDN设置为默认值"><a href="#3、CocoaPods-1-8-Beta-将CDN设置为默认值" class="headerlink" title="3、CocoaPods 1.8 Beta 将CDN设置为默认值"></a>3、CocoaPods 1.8 Beta 将CDN设置为默认值</h5><ul><li><p>我们在安装pod时候,会执行<code>pod setup</code>,其本质是<strong>将 <a href="https://github.com/CocoaPods/Specs" target="_blank" rel="noopener">https://github.com/CocoaPods/Specs</a> 上的项目克隆到/Users/用户名/.cocoapods/repos目录下,若此目录下已经有这个项目,使用pod setup命令则会将项目更新到最新的状态。</strong></p></li><li><p><strong>CDN</strong>支持最初是在<strong>1.7</strong>版本中引入的,最终在<strong>1.7.2</strong>中完成。 它旨在大大加快初始设置和依赖性分析。 使用<strong>1.8</strong>,<strong>CocoaPods</strong>不再需要克隆现在巨大的主规格<strong>repo</strong>才能运行,用户几乎可以立即将他们的项目与<strong>CocoaPods</strong>集成。</p></li><li><p>在1.8 Beta之前,需要拷贝现在庞大的 master specs repo 才能运行,但是之后,可以使用如下方式省略master specs repo的clone.</p><ul><li>编辑<strong>Podfile</strong>以将<strong>CDN</strong>设置为主要来源:</li></ul><figure class="highlight ruby"><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">- source <span class="string">'https://github.com/CocoaPods/Specs.git'</span></span><br><span class="line">+ source <span class="string">'https://cdn.cocoapods.org/'</span></span><br></pre></td></tr></table></figure></li></ul><ul><li><p>运行以下命令将其从托管存储库列表中删除:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod repo remove master</span><br></pre></td></tr></table></figure></li></ul><blockquote><p>注意:如果您希望继续使用基于<strong>git</strong>的源,则必须确保通过源<strong>DSL</strong>在<strong>Podfile</strong>中明确指定它,否则<strong>CocoaPods</strong>将自动使用<strong>CDN</strong>进行依赖性解析。</p></blockquote><h5 id="4、更多"><a href="#4、更多" class="headerlink" title="4、更多"></a>4、更多</h5><p><a href="https://juejin.im/post/5e09e382e51d4575a87c9ce6" target="_blank" rel="noopener">CocoaPods 1.9 新特性</a></p><h4 id="五、其他"><a href="#五、其他" class="headerlink" title="五、其他"></a>五、其他</h4><h5 id="1、pod-instal和pod-update的区别"><a href="#1、pod-instal和pod-update的区别" class="headerlink" title="1、pod instal和pod update的区别"></a>1、pod instal和pod update的区别</h5><ul><li>执行pod install时,如果Podfile.lock文件存在, 则下载Podfile.lock文件中指定的版本安装,对于不在Podfile.lock文件中的pod库,pod install命令会搜索这个pod库在Podfile文件中指定的版本来安装;</li><li>当你想要更新pod库的版本时才使用pod update;它不管Podfile.lock是否存在, 都会读取Podfile文件的的框架信息去下载安装,下载好之后, 再根据下载好的框架信息, 生成Podfile.lock文件</li></ul><h5 id="2、第三方库禁止BITCODE"><a href="#2、第三方库禁止BITCODE" class="headerlink" title="2、第三方库禁止BITCODE"></a>2、第三方库禁止BITCODE</h5><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><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">post_install <span class="keyword">do</span> <span class="params">|installer|</span></span><br><span class="line"> installer.pods_project.targets.each <span class="keyword">do</span> <span class="params">|target|</span></span><br><span class="line"> target.build_configurations.each <span class="keyword">do</span> <span class="params">|config|</span></span><br><span class="line"> config.build_setting[<span class="string">"ENABLE_BITCODE"</span>] = <span class="string">'NO'</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h5 id="3、清除相关命令"><a href="#3、清除相关命令" class="headerlink" title="3、清除相关命令"></a>3、清除相关命令</h5><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><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">#查看本地pod缓存</span></span><br><span class="line">pod cache list</span><br><span class="line"></span><br><span class="line"><span class="comment">#清除某个库缓存</span></span><br><span class="line">pod cache clean xxxx</span><br><span class="line"></span><br><span class="line"><span class="comment"># 清除所有pod缓存</span></span><br><span class="line">pod cache clean -all</span><br><span class="line"></span><br><span class="line"><span class="comment">#删除缓存方法</span></span><br><span class="line">rm ~<span class="regexp">/Library/</span>Caches/Cocoapods/Pods/Pods/Release</span><br></pre></td></tr></table></figure><h5 id="4、其他"><a href="#4、其他" class="headerlink" title="4、其他"></a>4、其他</h5><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><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"># 查看当前仓库的信息</span></span><br><span class="line">pod repo</span><br><span class="line"></span><br><span class="line"><span class="comment"># 更新库命令</span></span><br><span class="line">pod repo update</span><br><span class="line"></span><br><span class="line"><span class="comment"># 删除 search_index.json 文件</span></span><br><span class="line">rm ~<span class="regexp">/Library/</span>Caches/CocoaPods/search_index.json</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">
<p><em>不识庐山真面目,只缘身在此山中</em></p>
<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、CocoaPods是什么?"><a href=
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="Cocoapods" scheme="http://buaa0300/nanhuacoder.com/tags/Cocoapods/"/>
<category term="私有Pod" scheme="http://buaa0300/nanhuacoder.com/tags/%E7%A7%81%E6%9C%89Pod/"/>
</entry>
<entry>
<title>OOM问题小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/17/iOS-OOM/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/17/iOS-OOM/</id>
<published>2019-04-17T07:31:41.000Z</published>
<updated>2019-04-24T09:12:46.000Z</updated>
<content type="html"><![CDATA[<p><em>纸上得来终觉浅,绝知此事要躬行</em></p><h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、OOM是什么"><a href="#1、OOM是什么" class="headerlink" title="1、OOM是什么"></a>1、OOM是什么</h5><ul><li><p>OOM,即Out of Memory;是由于iOS的<strong>Jetsam机制</strong>导致的奔溃,不同于常规Crash,通过 Signal等Crash等监控方案是无法捕获到OOM事件。</p></li><li><p>造成OOM的原因可能有两个:系统由于整体内存使用较高,系统基于优先级杀死优先级较低的 App;当前 App 达到了 “high water mark”,也就是达到了系统对单个 App 的内存限制,当前App被系统强杀。</p></li></ul><h5 id="2、Jetsam机制是什么"><a href="#2、Jetsam机制是什么" class="headerlink" title="2、Jetsam机制是什么"></a>2、Jetsam机制是什么</h5><ul><li>Jetsam机制指的是操作系统为了控制内存资源过度使用而采用的一种资源管控机制。</li><li>由于iOS设备不存在交换区导致的内存受限,iOS内核不得不把一些<strong>优先级不高或者占用内存过大的</strong>App杀掉;在杀掉App后会记录一些数据信息并保存到日志。</li><li>Jetsam产生的这些日志可以在<strong>手机设置->隐私->分析</strong>中找到,日志是以<code>JetsamEvent</code>开头,日志中有内存页大小(pageSize),CPU时间(cpuTime)等字段。</li></ul><h4 id="二、获取App的内存使用上限"><a href="#二、获取App的内存使用上限" class="headerlink" title="二、获取App的内存使用上限"></a>二、获取App的内存使用上限</h4><h5 id="1、通过JetsamEvent-日志计算内存限制值"><a href="#1、通过JetsamEvent-日志计算内存限制值" class="headerlink" title="1、通过JetsamEvent 日志计算内存限制值"></a>1、通过JetsamEvent 日志计算内存限制值</h5><ul><li>查看设置->隐私->分析中以JetsamEvent开头的系统日志,关注两个重要的信息</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">//内存页大小(字节数)(16384Byte = 16KB)</span><br><span class="line">"pageSize" : 16384,</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">//内存页达到上限</span><br><span class="line">"rpages" : 948, //App 占用的内存页数量</span><br><span class="line">"reason" : "per-process-limit", //App 占用的内存超过了系统对单个 App 的内存限制。</span><br></pre></td></tr></table></figure><ul><li>该App内存限制上限:pageSize <em> rpages = 16384 </em> 948 /1024/1014 = 14.8MB</li><li>【有人可能会有疑问】<strong>某App内存使用上限只有区区不到15MB,不太可能吧。</strong>其实这是正常的,Jetsam机制会把<strong>优先级不高或内存使用过大</strong>的App强杀掉,这个App属于优先级不高,系统将其强杀,为优先级高的App提供更多内存资源。</li><li>App优先级可以这么理解:前台App > 后台App; 占用内存少 > 占用内存多</li><li>JetsamEvent日志属于系统级别的,是在系统目录下的。App开发者没有权限获得系统目录下内容。只能连接Xcode获取 或 手动进入<strong>手机设置->隐私->分析</strong>中找到日志并分享出来。</li></ul><h5 id="2、收到内存警告通知时获取当前App的内存使用值"><a href="#2、收到内存警告通知时获取当前App的内存使用值" class="headerlink" title="2、收到内存警告通知时获取当前App的内存使用值"></a>2、收到内存警告通知时获取当前App的内存使用值</h5><ul><li>iOS系统会开启优先级最高的线程 <strong>vm_pressure_monitor</strong>来监控系统的内存压力情况,并通过一个堆栈来维护所有App的进程。</li><li>当监控系统内存的线程发现某App内存有压力了,就发出通知,收到通知的App执行对应的处理方法,在这里可以编写释放内存的逻辑,就可能避免App被强杀。</li><li>在收到内存警告通知时,获取当前 App的内存使用值,能获得App内存使用阈值的近似值。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">//获取当前App的内存使用值</span><br><span class="line">uint64_t qs_getAppMemoryBytes() {</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 result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);</span><br><span class="line"> if (result != KERN_SUCCESS)</span><br><span class="line"> return 0;</span><br><span class="line"> return vmInfo.phys_footprint;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">//注册监听内存警告通知</span><br><span class="line">[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(handleReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];</span><br><span class="line"></span><br><span class="line">//收到内存警告通知通知的处理</span><br><span class="line">- (void)handleReceiveMemoryWarning:(NSNotification *)notification {</span><br><span class="line"> //</span><br><span class="line"> NSLog(@"handleReceiveMemoryWarning");</span><br><span class="line"> NSInteger appMemoryBytes = qs_getAppMemoryBytes();</span><br><span class="line"> //.... </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:可以通过模拟器的Debug -> Simulate Memory Warning 模拟内存警告</p><h5 id="3、通过XNU获取内存限制值"><a href="#3、通过XNU获取内存限制值" class="headerlink" title="3、通过XNU获取内存限制值"></a>3、通过XNU获取内存限制值</h5><p>在XNU中,可以通过memorystatus_priority_entry 这个结构体,得到进程的优先级和内存限制值。结构体如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">// 获取进程的 pid、优先级、状态、内存阈值等信息</span><br><span class="line">typedef struct memorystatus_priority_entry {</span><br><span class="line">pid_t pid;</span><br><span class="line">int32_t priority; //线程优先级</span><br><span class="line">uint64_t user_data; </span><br><span class="line">int32_t limit; //进程内存限制值</span><br><span class="line">uint32_t state;</span><br><span class="line">} memorystatus_priority_entry_t;</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:通过XNU获取内存限制值需要root权限,而App的权限不够,所以要获得App的内存限制需要到到越狱设备上。</p><h5 id="4、总结"><a href="#4、总结" class="headerlink" title="4、总结"></a>4、总结</h5><p><strong>总的来说,获取App的内存使用阈值,有三种方式,三种方式的对比如下:</strong></p><ul><li>通过JetsamEvent 日志计算内存限制值 局限大,不适合线上使用;</li><li><strong>收到内存警告通知时获取当前App的内存使用值,这个获取的是上限的近似值,虽然不很准确,但是适合线上使用;</strong></li><li>通过XNU获取内存限制值,局限更大,虽然能知道各进程之前的优先级,内存阈值,但是需要设备越狱,更不适合线上使用。</li></ul><p>——————————————————————————————————————————————————</p><p>很多东西后续完善,现在随便写写, 尴尬</p><h4 id="三、内存分配获取"><a href="#三、内存分配获取" class="headerlink" title="三、内存分配获取"></a>三、内存分配获取</h4><h5 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h5><ul><li>OOM的排查思路:监控App使用内存增长,在收到内存警告通知时,dump 内存信息,获取对象名称、对象个数、各对象的内存值,并在合适的时机上报到服务器。</li></ul><h5 id="2、内存分配函数"><a href="#2、内存分配函数" class="headerlink" title="2、内存分配函数"></a>2、内存分配函数</h5><ul><li>内存分配函数malloc和calloc等默认使用的是nano_zone,<strong>no_zone</strong> 是 256B 以下小内存的分配,大于 256B 的时候会使用 <strong>scalable_zone</strong> 来分配。</li><li>主要针对大内存的分配监控,所以只针对scalable_zone进行分析,可以过滤掉很多小内存分配,比如malloc 函数用的是 malloc_zone_malloc,calloc用的是malloc_zone_calloc。</li><li>使用scalable_zone分配内存的函数都会用到malloc_logger函数,它可以统计并管理内存的分配情况。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">void *malloc_zone_malloc(malloc_zone_t *zone, size_t size)</span><br><span class="line">{</span><br><span class="line">MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);</span><br><span class="line">void *ptr;</span><br><span class="line">if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {</span><br><span class="line">internal_check();</span><br><span class="line">}</span><br><span class="line">if (size > MALLOC_ABSOLUTE_MAX_SIZE) {</span><br><span class="line">return NULL;</span><br><span class="line">}</span><br><span class="line">ptr = zone->malloc(zone, size);</span><br><span class="line">// 在 zone 分配完内存后就开始使用 malloc_logger 进行进行记录</span><br><span class="line">if (malloc_logger) {</span><br><span class="line">malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);</span><br><span class="line">}</span><br><span class="line">MALLOC_TRACE(TRACE_malloc | DBG_FUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);</span><br><span class="line">return ptr;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:利用fishhook去hook malloc_logger函数,可以掌握内存的分配情况,当出现问题时,将内存分配日志捞上来,可以跟踪到内存不合理增大的原因。</p><h5 id="3、阅读"><a href="#3、阅读" class="headerlink" title="3、阅读"></a>3、阅读</h5><p><a href="https://wetest.qq.com/lab/view/367.html" target="_blank" rel="noopener">iOS微信内存监控</a></p><p><a href="https://www.jianshu.com/p/8bc1d2e5e49d" target="_blank" rel="noopener">深入理解内存分配</a></p><p><a href="https://juejin.im/post/5c28646f5188257abf1d947d" target="_blank" rel="noopener">iOS Out-Of-Memory 原理阐述及方案调研</a></p><p><a href="https://github.com/Tencent/OOMDetector" target="_blank" rel="noopener">OOMDetector</a></p><p><a href="https://juejin.im/post/5a58f1a76fb9a01cab283392" target="_blank" rel="noopener">iOS爆内存问题解决方案-OOMDetector组件</a></p>]]></content>
<summary type="html">
<p><em>纸上得来终觉浅,绝知此事要躬行</em></p>
<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、OOM是什么"><a href="#1、OOM
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="iOS" scheme="http://buaa0300/nanhuacoder.com/tags/iOS/"/>
<category term="OOM" scheme="http://buaa0300/nanhuacoder.com/tags/OOM/"/>
</entry>
<entry>
<title>APM基础小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/16/iOS-AppMonitor/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/16/iOS-AppMonitor/</id>
<published>2019-04-16T14:01:22.000Z</published>
<updated>2019-04-24T08:52:47.000Z</updated>
<content type="html"><![CDATA[<p><em>天之道,损有余而补不足</em></p><h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、APM是什么"><a href="#1、APM是什么" class="headerlink" title="1、APM是什么"></a>1、APM是什么</h5><ul><li>我们平时关注更多的是:需求是否delay,线上bug有多少?每个周期(比如2-3周) 关注下App的DAU、DNU、这些产品指标;但是团队中需要有人去关注App的技术质量指标:如Crash率、启动时间、安装包大小、核心页面的FPS、CPU使用率、内存占用、电量使用、卡顿情况等。</li><li>关注App线上质量,从技术维度来判断App是否健康。不健康的App表现为启动时间慢、页面卡顿、耗电量大等,这些App最终会失去用户;</li><li><strong>APM</strong> (Application Performance Manage)旨在建立APP的质量监控接入框架,方便App能快速集成,对性能监控项的异常数据进行采集和分析,输出相应问题的分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。</li></ul><h5 id="2、APM工具"><a href="#2、APM工具" class="headerlink" title="2、APM工具"></a>2、APM工具</h5><ul><li>微信最近开源了微信的APM工具<a href="https://github.com/Tencent/matrix" target="_blank" rel="noopener">Matrix</a>, 提供了针对iOS、Android和macOS系统的性能监控方案。这个方案很全面,可以直接接入App,当然也可以吸收其优秀的技术细节,优化自己的APM工具。</li><li>本文不是介绍如何定制一个APM工具,而是介绍在APM监控中,比较重要的几个监控维度:<strong>CPU使用率、内存使用、FPS和卡顿监控</strong>。</li></ul><h4 id="二、CPU使用率监控"><a href="#二、CPU使用率监控" class="headerlink" title="二、CPU使用率监控"></a>二、CPU使用率监控</h4><h5 id="1、Task和CPU"><a href="#1、Task和CPU" class="headerlink" title="1、Task和CPU"></a>1、Task和CPU</h5><ul><li>任务(Task)是一种容器(Container)对象;虚拟内存空间和其他资源都是通过这个容器对象管理的,这些资源包括设备和其他句柄。</li><li>严格地说,Mach 的任务并不是其他操作系统中所谓的进程,因为 Mach 作为一个微内核的操作系统,并没有提供“进程”的逻辑,而只是提供了最基本的实现。不过在 BSD 的模型中,这两个概念有1:1的简单映射,每一个 BSD 进程(也就是 OS X 进程)都在底层关联了一个 Mach 任务对象。</li><li>而每App运行,会对应一个<code>Mach Task</code>,Task下可能有多条线程同时执行任务,每个线程都是利用CPU的基本单位。<strong>要计算CPU 占用率,就需要获得当前<code>Mach Task</code>下,所有线程占用 CPU 的情况</strong>。</li></ul><h5 id="2、Mach-Task和线程列表"><a href="#2、Mach-Task和线程列表" class="headerlink" title="2、Mach Task和线程列表"></a>2、Mach Task和线程列表</h5><ul><li>一个<code>Mach Task</code>包含它的线程列表。内核提供了<code>task_threads</code> API 调用获取指定 task 的线程列表,然后可以通过<code>thread_info</code> API调用来查询指定线程的信息,</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">kern_return_t task_threads</span><br><span class="line">(</span><br><span class="line"> task_t target_task,</span><br><span class="line"> thread_act_array_t *act_list,</span><br><span class="line"> mach_msg_type_number_t *act_listCnt</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:<code>task_threads</code> 将<code>target_task</code> 任务中的所有线程保存在<code>act_list</code>数组中,<code>act_listCnt</code>表示线程个数:</p><h5 id="3、单个线程信息结构"><a href="#3、单个线程信息结构" class="headerlink" title="3、单个线程信息结构"></a>3、单个线程信息结构</h5><ul><li>iOS 的线程技术与Mac OS X类似,也是基于 Mach 线程技术实现的,可以通过<code>thread_info</code>这个API调用来查询指定线程的信息,thread_info结构如下:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">kern_return_t thread_info</span><br><span class="line">(</span><br><span class="line"> thread_act_t target_act,</span><br><span class="line"> thread_flavor_t flavor, // 传入不同的宏定义获取不同的线程信息</span><br><span class="line"> thread_info_t thread_info_out, // 查询到的线程信息</span><br><span class="line"> mach_msg_type_number_t *thread_info_outCnt // 信息的大小</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul><li>在 Mach 层中<code>thread_basic_info</code> 结构体封装了单个线程的基本信息:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">struct thread_basic_info {</span><br><span class="line"> time_value_t user_time; // 用户运行时长</span><br><span class="line"> time_value_t system_time; // 系统运行时长</span><br><span class="line"> integer_t cpu_usage; // CPU 使用率</span><br><span class="line"> policy_t policy; // 调度策略</span><br><span class="line"> integer_t run_state; // 运行状态</span><br><span class="line"> integer_t flags; // 各种标记</span><br><span class="line"> integer_t suspend_count; // 暂停线程的计数</span><br><span class="line"> integer_t sleep_time; // 休眠的时间</span><br><span class="line">};</span><br></pre></td></tr></table></figure><h5 id="4、CPU-占用率计算"><a href="#4、CPU-占用率计算" class="headerlink" title="4、CPU 占用率计算"></a>4、CPU 占用率计算</h5><ul><li>先获取当前task中的线程总数(<strong>threadCount</strong>)和所有线程数组(<strong>threadList</strong>)</li><li>遍历这个数组来获取单个线程的基本信息。线程基本信息的结构是<strong>thread_basic_info_t</strong>,这里面有CPU的使用率(<strong>cpu_usage</strong>)字段,累计所有线程的CPU使用率就能获得整个APP的CPU使用率(cpuUsage)。</li><li>需要注意的是:cpuUsage是一个整数,想要获得百分比形式,需要除以TH_USAGE_SCALE</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line"> *Scale factor for usage field.</span><br><span class="line"> */</span><br><span class="line">#define TH_USAGE_SCALE1000</span><br></pre></td></tr></table></figure><ul><li>可以定时,比如2s去计算一次CPU的使用率</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">+ (double)getCpuUsage {</span><br><span class="line"> </span><br><span class="line"> kern_return_t kr;</span><br><span class="line"> thread_array_t threadList; // 保存当前Mach task的线程列表</span><br><span class="line"> mach_msg_type_number_t threadCount; // 保存当前Mach task的线程个数</span><br><span class="line"> thread_info_data_t threadInfo; // 保存单个线程的信息列表</span><br><span class="line"> mach_msg_type_number_t threadInfoCount; // 保存当前线程的信息列表大小</span><br><span class="line"> thread_basic_info_t threadBasicInfo; // 线程的基本信息</span><br><span class="line"> </span><br><span class="line"> // 通过“task_threads”API调用获取指定 task 的线程列表</span><br><span class="line"> // mach_task_self_,表示获取当前的 Mach task</span><br><span class="line"> kr = task_threads(mach_task_self(), &threadList, &threadCount);</span><br><span class="line"> if (kr != KERN_SUCCESS) {</span><br><span class="line"> return -1;</span><br><span class="line"> }</span><br><span class="line"> double cpuUsage = 0;</span><br><span class="line"> // 遍历所有线程</span><br><span class="line"> for (int i = 0; i < threadCount; i++) {</span><br><span class="line"> threadInfoCount = THREAD_INFO_MAX;</span><br><span class="line"> // 通过“thread_info”API调用来查询指定线程的信息</span><br><span class="line"> // flavor参数传的是THREAD_BASIC_INFO,使用这个类型会返回线程的基本信息,</span><br><span class="line"> // 定义在 thread_basic_info_t 结构体,包含了用户和系统的运行时间、运行状态和调度优先级等</span><br><span class="line"> kr = thread_info(threadList[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount);</span><br><span class="line"> if (kr != KERN_SUCCESS) {</span><br><span class="line"> return -1;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> threadBasicInfo = (thread_basic_info_t)threadInfo;</span><br><span class="line"> if (!(threadBasicInfo->flags & TH_FLAGS_IDLE)) {</span><br><span class="line"> cpuUsage += threadBasicInfo->cpu_usage;</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"> vm_deallocate(mach_task_self(), (vm_offset_t)threadList, threadCount * sizeof(thread_t));</span><br><span class="line"> </span><br><span class="line"> return cpuUsage / (double)TH_USAGE_SCALE * 100.0;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="4、为什么关注CPU使用率"><a href="#4、为什么关注CPU使用率" class="headerlink" title="4、为什么关注CPU使用率"></a>4、为什么关注CPU使用率</h5><ul><li>CPU的使用率是对APP使用CPU情况的评估,App频繁操作,CPU使用率一般在40%-50%;</li><li>假如CPU使用过高(>90%),可以认为CPU满负载,此种情况大概率发生卡顿,可以选择上报。</li><li>一段时间内CPU的使用率一直超过某个阈值(80%),此种情况大概率发生卡顿,可以选择上报。</li></ul><h4 id="三、内存使用监控"><a href="#三、内存使用监控" class="headerlink" title="三、内存使用监控"></a>三、内存使用监控</h4><h5 id="1、内存"><a href="#1、内存" class="headerlink" title="1、内存"></a>1、内存</h5><ul><li>内存是有限且系统共享的资源,一个App占用地多,系统和其他App所能用的就更少;减少内存占用能不仅仅让自己App,其他App,甚至是整个系统都表现得更好。</li><li>关注App的内存使用情况十分重要</li></ul><h5 id="2、内存信息结构"><a href="#2、内存信息结构" class="headerlink" title="2、内存信息结构"></a>2、内存信息结构</h5><ul><li>Mach task 的内存使用信息存放在<code>mach_task_basic_info</code>结构体中 ,其中<code>resident_size</code> 为驻留内存大小,而phys_footprint表示实际使用的物理内存,iOS 9之后使用phys_footprint来统计App占用的内存大小(和Xcode和Instruments的值显示值接近)。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">struct task_vm_info {</span><br><span class="line"> mach_vm_size_t virtual_size; // 虚拟内存大小</span><br><span class="line"> integer_t region_count; // 内存区域的数量</span><br><span class="line"> integer_t page_size;</span><br><span class="line"> mach_vm_size_t resident_size; // 驻留内存大小</span><br><span class="line"> mach_vm_size_t resident_size_peak; // 驻留内存峰值</span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> /* added for rev1 */</span><br><span class="line"> mach_vm_size_t phys_footprint; // 实际使用的物理内存</span><br><span class="line"></span><br><span class="line"> ...</span><br></pre></td></tr></table></figure><h5 id="3、内存信息获取"><a href="#3、内存信息获取" class="headerlink" title="3、内存信息获取"></a>3、内存信息获取</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">uint64_t qs_getAppMemoryBytes() {</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 result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);</span><br><span class="line"> if (result != KERN_SUCCESS)</span><br><span class="line"> return 0;</span><br><span class="line"> return vmInfo.phys_footprint;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="4、为什么关注内存使用"><a href="#4、为什么关注内存使用" class="headerlink" title="4、为什么关注内存使用"></a>4、为什么关注内存使用</h5><ul><li>内存问题影响最大是OOM,即Out of Memory,指的是 App 占用的内存达到iOS系统对单个App占用内存上限时,而被系统强杀的现象,这是一种由iOS的Jetsam机制导致的奔溃,无法通过信号捕获到。</li><li>对于监控OOM没有很好的办法,目前比较可行的办法是:<strong>定时监控内存使用,当接近内存使用上限时,dump 内存信息,获取对象名称、对象个数、各对象的内存值,并在合适的时机上报到服务器</strong>。</li><li>App中会使用很多单例,这些单例常驻内存,需要关注大单例;大图片解码会造成内存使用飙升,这个也需要关注;还有些取巧的方案,比如预创建webview对象甚至预创建ViewController对象,采用此类做法,需要关注对内存造成的压力。</li></ul><h4 id="四、FPS监控"><a href="#四、FPS监控" class="headerlink" title="四、FPS监控"></a>四、FPS监控</h4><h5 id="1、FPS和CADisplayLink"><a href="#1、FPS和CADisplayLink" class="headerlink" title="1、FPS和CADisplayLink"></a>1、FPS和CADisplayLink</h5><ul><li><code>FPS</code>是<code>Frames Per Second</code> ,意思是每秒帧数,也就是我们常说的“刷新率(单位为Hz)。FPS低(小于50)表示App不流畅,App需要优化,iOS手机屏幕的正常刷新频率是每秒60次,即<code>FPS</code>值为60。</li><li><code>CADisplayLink</code>是和屏幕刷新频率保存一致,它是<code>CoreAnimation</code>提供的另一个类似于<code>NSTimer</code>的类,它总是在屏幕完成一次更新之前启动,<code>CADisplayLink</code>有一个整型的<code>frameInterval</code>属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。</li></ul><h5 id="2、FPS监控实现"><a href="#2、FPS监控实现" class="headerlink" title="2、FPS监控实现"></a>2、FPS监控实现</h5><ul><li>注册CADisplayLink 得到屏幕的同步刷新率,记录1s(<strong>useTime</strong>,可能比1s大一丢丢)时间内刷新的帧数(<strong>total</strong>),计算<strong>total/useTime</strong>得到1s时间内的帧数,即FPS值。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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></pre></td><td class="code"><pre><span class="line">- (void)start {</span><br><span class="line"> //注意CADisplayLink的处理循环引用问题</span><br><span class="line"> self.displayLink = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(updateFPSCount:)];</span><br><span class="line"> [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">// 执行帧率和屏幕刷新率保持一致</span><br><span class="line">- (void)updateFPSCount:(CADisplayLink *)displayLink {</span><br><span class="line"> </span><br><span class="line"> if (self.lastTimeStamp == 0) {</span><br><span class="line"> self.lastTimeStamp = self.displayLink.timestamp;</span><br><span class="line"> } else {</span><br><span class="line"> self.total++;</span><br><span class="line"> // 开始渲染时间与上次渲染时间差值</span><br><span class="line"> NSTimeInterval useTime = self.displayLink.timestamp - self.lastTimeStamp;</span><br><span class="line"> //小于1s立即返回</span><br><span class="line"> if (useTime < 1){</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> self.lastTimeStamp = self.displayLink.timestamp;</span><br><span class="line"> // fps 计算</span><br><span class="line"> NSInteger fps = self.total / useTime;</span><br><span class="line"> NSLog(@"self.total = %@,useTime = %@,fps = %@",@(self.total),@(useTime),@(fps));</span><br><span class="line"> self.total = 0;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:很多团队非常相信(甚至迷信)FPS值,认为FPS值(大于50)就代表不卡顿,这点我是不认可。下面我列举遇到的2个非常典型的Case。</p><h5 id="3、错信FPS值Case1"><a href="#3、错信FPS值Case1" class="headerlink" title="3、错信FPS值Case1"></a>3、错信FPS值Case1</h5><ul><li>同学A在做频繁绘制需求时, 重写UIView的<code>drawRect:</code>方法,在模拟器上频繁调用setNeedsDisplay来触发<code>drawRect:</code>方法,FPS值还稳定在50以上,但是真机上去掉帧很厉害。我认为这里犯了两个错误。</li><li>错误1:<code>drawRect:</code>是利用CPU绘制的,性能并不如GPU绘制,对于频繁绘制的绘制需求,不应该考虑使用重写<code>drawRect:</code>这种方式,推荐<code>CAShapeLayer+UIBezierPath</code>。</li><li>错误2:不应该关注模拟器FPS来观察是否发生卡顿,模拟器使用的是Mac的处理器,比手机的ARM性能要强,所以造成在模拟器上FPS比较理想,真机上比较差。</li></ul><h5 id="4、错信FPS值Case2"><a href="#4、错信FPS值Case2" class="headerlink" title="4、错信FPS值Case2"></a>4、错信FPS值Case2</h5><ul><li>同学B在列表滑动时候,观察iPhone 6 plus真机上FPS的值稳定在52左右,感觉不错,但是肉眼明显感觉到卡顿。</li><li>是FPS错了吗?我认为没错,是我们对FPS的理解错了;因为FPS代表的是<strong>每秒帧数</strong>,这是一个平均值,假如前0.5s播放了2帧,后面0.5s播放了58帧,从结果来看,FPS的值依旧是60。但是实际上,它的确发生了卡顿。</li></ul><h5 id="5、为什么关注FPS"><a href="#5、为什么关注FPS" class="headerlink" title="5、为什么关注FPS"></a>5、为什么关注FPS</h5><ul><li>虽然列举了两个错信FPS的Case,但是FPS依旧是一个很重要的指标,来关注页面的卡顿情况。</li><li>和使用监控RunLoop状态来发现卡顿问题不同,FPS关注的是滑动场景下,FPS偏低的场景。</li><li>而监控RunLoop状态来发现卡顿问题更加关注的是:在一段时间内无法进行用户操作的场景,这类卡顿对用户的伤害非常大,是通过日志很难发现,需要优先解决的问题</li></ul><h4 id="五、卡顿监控"><a href="#五、卡顿监控" class="headerlink" title="五、卡顿监控"></a>五、卡顿监控</h4><h5 id="1、卡顿和RunLoop"><a href="#1、卡顿和RunLoop" class="headerlink" title="1、卡顿和RunLoop"></a>1、卡顿和RunLoop</h5><ul><li>卡顿监控的本质是,监控主线程做了哪些事;线程的消息事件依赖<strong>RunLoop</strong>,通过监听RunLoop的状态,从而判断是否发生卡顿。</li><li>RunLoop在iOS中是由CFRunLoop实现的,它负责监听输入源,进行调度处理的,这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调。RunLoop接收两种输入源:一种是来<strong>自另一个线程或者来自不同应用的异步消息</strong>;另一个事<strong>来自预定时间或重复间隔的同步事件</strong>。</li><li>当有事情处理,Runloop唤起线程去处理,没有事情处理,让线程进入休眠。基于此,我们可以把大量占用CPU的任务(图片加载、数据文件读写等) ,放在空闲的非主线程执行,就可以避免影响主线程滑动过程中的体验(主线程滑动时,RunLoop处在UITrackingRunLoopMode模式)</li></ul><h5 id="2、如何判断卡顿"><a href="#2、如何判断卡顿" class="headerlink" title="2、如何判断卡顿"></a>2、如何判断卡顿</h5><ul><li>已知的RunLoop的7个状态</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">//RunLoop的状态</span><br><span class="line">typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {</span><br><span class="line"> kCFRunLoopEntry = (1UL << 0), // 即将进入Loop</span><br><span class="line"> kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer</span><br><span class="line"> kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source</span><br><span class="line"> kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠</span><br><span class="line"> kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒</span><br><span class="line"> kCFRunLoopExit = (1UL << 7), // 即将退出Loop</span><br><span class="line"> kCFRunLoopAllActivities = 0x0FFFFFFFU // loop 所有状态改变</span><br><span class="line"> };</span><br></pre></td></tr></table></figure><ul><li>由于<code>kCFRunLoopBeforeSources之后</code>需要处理Source0,<code>kCFRunLoopAfterWaiting之后</code>需要处理timer、dispatch 到 main_queue 的 block和Source1,所以可以认为<code>kCFRunLoopBeforeSources</code>和<code>kCFRunLoopAfterWaiting</code>。因为<code>kCFRunLoopBeforeSources</code>之后和<code>kCFRunLoopAfterWaiting</code>之后是事情处理的主要时间段。</li><li><p>dispatch_semaphore_t信号量机制特性:信号量到达、或者 超时会继续向下进行,否则等待;如果超时则返回的结果必定不为0,否则信号量到达结果为0。</p></li><li><p>主线程卡顿发生是因为要处理大量的事情。这就意味着<strong>主线程在消耗时间在处理繁重的事件,导致信号超时了</strong>(dispatch_semaphore_signal不能及时执行),<strong>如果此时发现当前的RunLoop的状态是kCFRunLoopBeforeSources或kCFRunLoopAfterWaiting,就认为主线程长期停留在这两个状态上,此时就判定卡顿发生。</strong></p></li></ul><h5 id="3、卡顿监控的实现"><a href="#3、卡顿监控的实现" class="headerlink" title="3、卡顿监控的实现"></a>3、卡顿监控的实现</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><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></pre></td><td class="code"><pre><span class="line">// QSMainThreadMonitor.h</span><br><span class="line">@interface QSMainThreadMonitor : NSObject</span><br><span class="line"></span><br><span class="line">+ (instancetype)sharedInstance;</span><br><span class="line"></span><br><span class="line">- (void)beginMonitor;</span><br><span class="line"></span><br><span class="line">- (void)stopMonitor;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">// QSMainThreadMonitor.m</span><br><span class="line">@interface QSMainThreadMonitor()</span><br><span class="line"></span><br><span class="line">@property (nonatomic,strong) dispatch_semaphore_t semaphore;</span><br><span class="line">@property (nonatomic,assign) CFRunLoopObserverRef observer;</span><br><span class="line">@property (nonatomic,assign) CFRunLoopActivity runloopActivity;</span><br><span class="line">@property (nonatomic,strong) dispatch_queue_t monitorQueue;</span><br><span class="line"></span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">@implementation QSMainThreadMonitor</span><br><span class="line"></span><br><span class="line">+ (instancetype)sharedInstance {</span><br><span class="line"> static QSMainThreadMonitor *monitor = nil;</span><br><span class="line"> static dispatch_once_t onceToken;</span><br><span class="line"> dispatch_once(&onceToken, ^{</span><br><span class="line"> monitor = [[QSMainThreadMonitor alloc]init];</span><br><span class="line"> });</span><br><span class="line"> return monitor;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (instancetype)init {</span><br><span class="line"> self = [super init];</span><br><span class="line"> if (self) {</span><br><span class="line"> self.monitorQueue = dispatch_queue_create("com.main.thread.monitor.queue", DISPATCH_QUEUE_CONCURRENT);</span><br><span class="line"> }</span><br><span class="line"> return self;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (void)beginMonitor{</span><br><span class="line"> </span><br><span class="line"> if (self.observer) {</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> __block int timeoutCount = 0;</span><br><span class="line"> </span><br><span class="line"> //创建观察者并添加到主线程</span><br><span class="line"> CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL,NULL};</span><br><span class="line"> self.observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);</span><br><span class="line"> //将self.observer添加到主线程RunLoop的Common模式下观察</span><br><span class="line"> CFRunLoopAddObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);</span><br><span class="line"></span><br><span class="line"> self.semaphore = dispatch_semaphore_create(0);</span><br><span class="line"> dispatch_async(self.monitorQueue, ^{</span><br><span class="line"> while (YES) {</span><br><span class="line"> long result = dispatch_semaphore_wait(self.semaphore, dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC));</span><br><span class="line"> if (result != 0 && self.observer) {</span><br><span class="line"> //超时判断</span><br><span class="line"> if (self.runloopActivity == kCFRunLoopBeforeSources || self.runloopActivity == kCFRunLoopAfterWaiting) {</span><br><span class="line"> if (++timeoutCount < 1) {</span><br><span class="line"> NSLog(@"--timeoutCount--%@",@(timeoutCount));</span><br><span class="line"> continue;</span><br><span class="line"> }</span><br><span class="line"> //出现卡顿、进一步处理</span><br><span class="line"> NSLog(@"--timeoutCount 卡顿发生--");</span><br><span class="line"> // todo,eg:获取堆栈信息并上报</span><br><span class="line"> }</span><br><span class="line"> }else {</span><br><span class="line"> timeoutCount = 0;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> </span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (void)stopMonitor{</span><br><span class="line"> </span><br><span class="line"> if (!self.observer) {</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.observer, kCFRunLoopCommonModes);</span><br><span class="line"> CFRelease(self.observer);</span><br><span class="line"> self.observer = NULL;</span><br><span class="line"> </span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">#pragma mark -Private Method</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">static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){</span><br><span class="line"> //每一次监测到Runloop状态变化调用</span><br><span class="line"> QSMainThreadMonitor *monitor = (__bridge QSMainThreadMonitor *)info;</span><br><span class="line"> monitor.runloopActivity = activity;</span><br><span class="line"> if (monitor.semaphore) {</span><br><span class="line"> dispatch_semaphore_signal(monitor.semaphore);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">@end</span><br></pre></td></tr></table></figure><h5 id="4、卡顿时间阈值说明"><a href="#4、卡顿时间阈值说明" class="headerlink" title="4、卡顿时间阈值说明"></a>4、卡顿时间阈值说明</h5><ul><li>这里卡顿时间阈值是2s,连续1次超时且RunLoop的状态处于<code>kCFRunLoopBeforeSources</code>或<code>kCFRunLoopAfterWaiting</code> 状态就认为卡顿。</li><li>利用的RunLoop实现的卡顿方案,主要是针对那些在一段时间内无法进行用户操作的场景,这类卡顿对用户的伤害非常大,是通过日志很难发现,需要优先解决的问题。</li><li><strong>卡顿时间阈值</strong>(<em>timeoutThreshold</em>)和<strong>超时时间次数</strong>(<em>timeoutCount</em>)可以通服务器下发控制,用来控制上报卡顿情况的场景。</li></ul><h4 id="六、电量监控"><a href="#六、电量监控" class="headerlink" title="六、电量监控"></a>六、电量监控</h4><h5 id="1、手动查看电量"><a href="#1、手动查看电量" class="headerlink" title="1、手动查看电量"></a>1、手动查看电量</h5><ul><li>我们可以通过手机的<strong>设置-电池</strong>查看过去一段时间(24小时或2天)查看Top耗电量的App;</li><li>对于用户来说,还有更直接的方式,使用某App时候,手机状态栏右上角电池使用量嗖嗖往下掉或手机发热,那么基本可以判断这个App耗电太快,赶紧卸了。</li><li>对于开发者来说,可以通过Xcode左边栏的Energy Impact查看电量使用,蓝色表示–合理,黄色–表示比较耗电,红色–表示仅仅轻度使用你的程序,就会很耗电。</li><li>还可以使用手机<strong>设置-开发者-Logging-Energy</strong>的startRecording和stopRecording来记录一段时间(3-5minutes)某App的耗电量情况。导入Instrument来分析具体耗电情况。</li></ul><h5 id="2、电量监控方案1"><a href="#2、电量监控方案1" class="headerlink" title="2、电量监控方案1"></a>2、电量监控方案1</h5><ul><li><p>利用<code>UIDevice</code> 提供了获取设备电池的相关信息,包括当前电池的状态以及电量。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">//开启电量监控</span><br><span class="line">[UIDevice currentDevice].batteryMonitoringEnabled = YES;</span><br><span class="line">//监听电量使用情况</span><br><span class="line">[[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryLevelDidChangeNotification object:nil queue:[NSOperationQueue mainQueue]</span><br><span class="line"> usingBlock:^(NSNotification *notification) {</span><br><span class="line"> // Level has changed</span><br><span class="line"> NSLog(@"");</span><br><span class="line"> //UIDevice返回的batteryLevel的范围在0到1之间。</span><br><span class="line"> NSUInteger batteryLevel = [UIDevice currentDevice].batteryLevel * 100;</span><br><span class="line"> NSLog(@"[Battery Level]: %@", @(batteryLevel));</span><br><span class="line"> }];</span><br></pre></td></tr></table></figure></li></ul><p><strong>说明</strong>:使用 <code>UIDevice</code> 可以非常方便获取到电量,但是经测试发现,在 iOS 8.0 之前,<code>batteryLevel</code> 只能精确到5%,而在 <code>iOS 8.0</code> 之后,精确度可以达到1%</p><h5 id="3、电量监控方案2"><a href="#3、电量监控方案2" class="headerlink" title="3、电量监控方案2"></a>3、电量监控方案2</h5><ul><li>利用iOS系统私有框架<code>IOKit</code>, 通过它可以获取设备电量信息,精确度达到1%。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">#import "IOPSKeys.h"</span><br><span class="line">#import "IOPowerSources.h"</span><br><span class="line"></span><br><span class="line">-(double) getBatteryLevel{</span><br><span class="line"> // 返回电量信息</span><br><span class="line"> CFTypeRef blob = IOPSCopyPowerSourcesInfo();</span><br><span class="line"> // 返回电量句柄列表数据</span><br><span class="line"> CFArrayRef sources = IOPSCopyPowerSourcesList(blob);</span><br><span class="line"> CFDictionaryRef pSource = NULL;</span><br><span class="line"> const void *psValue;</span><br><span class="line"> // 返回数组大小</span><br><span class="line"> int numOfSources = CFArrayGetCount(sources);</span><br><span class="line"> // 计算大小出错处理</span><br><span class="line"> if (numOfSources == 0) {</span><br><span class="line"> NSLog(@"Error in CFArrayGetCount");</span><br><span class="line"> return -1.0f;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // 计算所剩电量</span><br><span class="line"> for (int i=0; i<numOfSources; i++) {</span><br><span class="line"> // 返回电源可读信息的字典</span><br><span class="line"> pSource = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(sources, i));</span><br><span class="line"> if (!pSource) {</span><br><span class="line"> NSLog(@"Error in IOPSGetPowerSourceDescription");</span><br><span class="line"> return -1.0f;</span><br><span class="line"> }</span><br><span class="line"> psValue = (CFStringRef) CFDictionaryGetValue(pSource, CFSTR(kIOPSNameKey));</span><br><span class="line"></span><br><span class="line"> int curCapacity = 0;</span><br><span class="line"> int maxCapacity = 0;</span><br><span class="line"> double percentage;</span><br><span class="line"></span><br><span class="line"> psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSCurrentCapacityKey));</span><br><span class="line"> CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &curCapacity);</span><br><span class="line"></span><br><span class="line"> psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSMaxCapacityKey));</span><br><span class="line"> CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &maxCapacity);</span><br><span class="line"></span><br><span class="line"> percentage = ((double) curCapacity / (double) maxCapacity * 100.0f);</span><br><span class="line"> NSLog(@"curCapacity : %d / maxCapacity: %d , percentage: %.1f ", curCapacity, maxCapacity, percentage);</span><br><span class="line"> return percentage;</span><br><span class="line"> }</span><br><span class="line"> return -1.</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:</p><ul><li>因为IOKit.framework是私有类库,使用的时候,需要通过动态引用的方式,没有具体实践,UIDevice获取的方案在iOS 8.0` 之后,精确度可以达到1%, 已经满足项目需要(我们项目最低支持iOS 9)。</li></ul><h5 id="4、耗电量大的操作"><a href="#4、耗电量大的操作" class="headerlink" title="4、耗电量大的操作"></a>4、耗电量大的操作</h5><ul><li><p>CPU使用率高的操作</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">线程过多 (控制合适的线程数)</span><br><span class="line">定位 (按需使用,降低频次)</span><br><span class="line">CPU任务繁重 (使用轻量级对象,缓存计算结果,对象复用等)</span><br><span class="line">频繁网络请求(避免无效冗余的网络请求)</span><br></pre></td></tr></table></figure></li><li><p>I/O操作频繁的操作</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">直接读写磁盘文件 (合理利用内存缓存,碎片化的数据在内存中聚合,合适时机写入磁盘)</span><br></pre></td></tr></table></figure></li></ul><h4 id="七、End"><a href="#七、End" class="headerlink" title="七、End"></a>七、End</h4><h5 id="1、总结"><a href="#1、总结" class="headerlink" title="1、总结"></a>1、总结</h5><ul><li><p>对APP的质量指标的监控,是为了更早地发现问题;发现问题是为了更好地解决问题。所以监控不是终点,是起点。</p></li><li><p>在17年时候,在简书中写了<a href="https://www.jianshu.com/p/3261493e6d9e" target="_blank" rel="noopener">iOS实录14:浅谈iOS Crash(一)</a>和 <a href="https://www.jianshu.com/p/33ee5e7d312c" target="_blank" rel="noopener">iOS实录15:浅谈iOS Crash(二)</a>两篇文章;时隔两年之后,书写此文,是为了纪念过去大半年时候在App质量监控上花的努力。</p></li><li>文章篇幅有限,没有介绍具体的优化办法。</li></ul><h5 id="2、推荐的阅读资料"><a href="#2、推荐的阅读资料" class="headerlink" title="2、推荐的阅读资料"></a>2、推荐的阅读资料</h5><p><a href="https://juejin.im/entry/595356e5f265da6c43670373" target="_blank" rel="noopener">iOS 性能监控方案 Wedjat(上篇)</a></p><p><a href="https://www.jianshu.com/p/f0dc653d04ca" target="_blank" rel="noopener">教你开发省电的 iOS app</a></p>]]></content>
<summary type="html">
<p><em>天之道,损有余而补不足</em></p>
<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、APM是什么"><a href="#1、APM是什么"
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="APM" scheme="http://buaa0300/nanhuacoder.com/tags/APM/"/>
<category term="Monitor" scheme="http://buaa0300/nanhuacoder.com/tags/Monitor/"/>
</entry>
<entry>
<title>捕获NSLog日志小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/15/iOS-NSLog/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/15/iOS-NSLog/</id>
<published>2019-04-15T13:21:40.000Z</published>
<updated>2019-04-18T13:56:32.000Z</updated>
<content type="html"><![CDATA[<p><em>既往不恋,纵情向前</em></p><h4 id="一、NSLog概述"><a href="#一、NSLog概述" class="headerlink" title="一、NSLog概述"></a>一、NSLog概述</h4><h5 id="1、NSLog是什么"><a href="#1、NSLog是什么" class="headerlink" title="1、NSLog是什么"></a>1、NSLog是什么</h5><ul><li>NSLog是一个C函数,函数声明如下:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">//Logs an error message to the Apple System Log facility.</span><br><span class="line">FOUNDATION_EXPORT void NSLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;</span><br></pre></td></tr></table></figure><ul><li>根据苹果的文档介绍,NSLog的作用是输出信息到标准的Error控制台和 苹果的日志系统(ASL,Apple System Log)里面(iOS 10之前)。</li></ul><ul><li>iOS10之后,苹果使用新的统一日志系统(Unified Logging System)来记录日志,全面取代ASL的方式,此种方式,是把日志集中存放在内存和数据库里,并提供单一、高效和高性能的接口去获取系统所有级别的消息传递。</li><li>新的统一日志系统没有ASL那样的接口可以让我们取出全部日志。</li></ul><h5 id="2、NSLog日常使用"><a href="#2、NSLog日常使用" class="headerlink" title="2、NSLog日常使用"></a>2、NSLog日常使用</h5><ul><li><p>NSLog在调试阶段,日志会输出到到Xcode中,而在iOS真机上,它会输出到系统的<code>/var/log/syslog</code>这个文件中。</p></li><li><p>在日常开发中,很多人喜欢使用NSLog来输出调试信息,但是都知道NSLog是比较消耗性能呢,NSLog输出的内容或次数多了之后,甚至会影响App的体验。</p></li><li>于是乎,比较常见的手段是,线上不使用NSLog,DEBUG下才真正使用NSLog。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">#if DEBUG</span><br><span class="line">#define MYLOG(fmt, ...) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);</span><br><span class="line">#else</span><br><span class="line">#define MYLOG(fmt,...) {}</span><br><span class="line">#endif</span><br></pre></td></tr></table></figure><h5 id="3、常见的日记收集框架"><a href="#3、常见的日记收集框架" class="headerlink" title="3、常见的日记收集框架"></a>3、常见的日记收集框架</h5><ul><li>日志收集主要用了两个开源框架来实现:<a href="https://github.com/plausiblelabs/plcrashreporter" target="_blank" rel="noopener">PLCrashReporter</a>与<a href="https://github.com/CocoaLumberjack/CocoaLumberjack" target="_blank" rel="noopener">CocoaLumberjack</a>。<strong>PLCrashReporter</strong>主要用来崩溃日志收集,<strong>CocoaLumberjack</strong>是用来收集非崩溃日志。 </li><li><strong>CocoaLumberjack</strong>中实现了对NSLog日志的捕获。</li></ul><h5 id="4、捕获NSLog日志有三种方式"><a href="#4、捕获NSLog日志有三种方式" class="headerlink" title="4、捕获NSLog日志有三种方式"></a>4、捕获NSLog日志有三种方式</h5><ul><li>iOS 10以前可以通过ASL接口来获取</li><li>通过fishhook库hook NSLog方法重定向NSLog函数</li><li>使用dup2函数和STDERR句柄重定向NSLog函数</li></ul><h4 id="二、获取NSLog的日志输出-iOS-10前"><a href="#二、获取NSLog的日志输出-iOS-10前" class="headerlink" title="二、获取NSLog的日志输出(iOS 10前)"></a>二、获取NSLog的日志输出(iOS 10前)</h4><p><em>参考CocoaLumberjack中的DDASLLogCapture实现</em></p><h5 id="1、流程介绍"><a href="#1、流程介绍" class="headerlink" title="1、流程介绍"></a>1、流程介绍</h5><ul><li>执行<strong>DDASLLogCapture</strong>的<strong>start</strong>方法,启动一个<strong>异步全局队列</strong>去捕获ASL存储的日志;</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">+ (void)start {</span><br><span class="line">//...</span><br><span class="line"> dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){</span><br><span class="line"> [self captureAslLogs];</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>当日志被保存到ASL的数据库时候,<strong>syslogd</strong>(系统里用于接收分发日志消息的日志守护进程)会发出一条通知。因为发过来的这一条通知可能有多条日志,需要先将几条日志进行合并。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">+ (void)captureAslLogs {</span><br><span class="line"> //....</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>将获得到的数据转成char 字符串类型,再转成NSString类型,最后封装成<strong>DDLogMessage</strong>对象,通过<code>[DDLog log: message:]</code> 方法将日志记录下来。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">+ (void)aslMessageReceived:(aslmsg)msg {</span><br><span class="line"> //...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:以上方法不会影响Xcode控制台的输出,无侵入。</p><h5 id="2、注册进程间的系统通知"><a href="#2、注册进程间的系统通知" class="headerlink" title="2、注册进程间的系统通知"></a>2、注册进程间的系统通知</h5><ul><li><code>captureAslLogs</code>中通过<code>notify_register_dispatch</code>来注册监听进程间的系统通知;</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">notify_register_dispatch(kNotifyASLDBUpdate, &notifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token)</span><br><span class="line"> {</span><br><span class="line"> //...</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><ul><li>其中宏<strong>kNotifyASLDBUpdate</strong>表示:日志被保存在ASL数据库发出的跨进程通知;</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line"> * ASL notifications</span><br><span class="line"> * Sent by syslogd to advise clients that new log messages have been</span><br><span class="line"> * added to the ASL database.</span><br><span class="line"> */</span><br><span class="line">#define kNotifyASLDBUpdate "com.apple.system.logger.message"</span><br></pre></td></tr></table></figure><ul><li>将日志保存到ASL数据库时还有很多通知,比如宏<strong>kNotifyVFSLowDiskSpace</strong>表示:系统磁盘空间不足,捕获到这个通知时,可以去清理缓存空间,避免缓存写入磁盘失败的情况。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">#define kNotifyVFSLowDiskSpace "com.apple.system.lowdiskspace"</span><br></pre></td></tr></table></figure><h4 id="三、NSLog重定向"><a href="#三、NSLog重定向" class="headerlink" title="三、NSLog重定向"></a>三、NSLog重定向</h4><h5 id="1、介绍"><a href="#1、介绍" class="headerlink" title="1、介绍"></a>1、介绍</h5><ul><li>在iOS10之后,新的统一日志系统(Unified Logging System)全面取代ASL,没有ASL那样的接口可以让我们取出全部日志,所以为了兼容新的统一日志系统,你就需要对NSLog日志的输出进行重定向。</li><li>NSLog 进行重定向,可以采用 Hook的方式。因为 NSLog 本身就是一个 C 函数,可以使用fishhook进行重定向。</li><li>fishhook是Facebook提供的一个动态修改链接Mach-O文件的工具,能够hook C函数。</li></ul><h5 id="2、fishhook原理"><a href="#2、fishhook原理" class="headerlink" title="2、fishhook原理"></a>2、fishhook原理</h5><ul><li><p>APP运行时,Mach-O文件被<strong>dyld</strong>(动态加载器)加载进内存</p></li><li><p><strong>ASLR</strong>(地址空间布局随机化)让Mach-O被加载时内存地址随机分配</p></li><li>苹果的PIC位置与代码独立技术,让Mach-O调用系统库函数时,先在Mach-O表中的<code>_DATA段</code>建立一个指针指向外部库函数,dyld加载MachO时知道外部库函数的调用地址,会动态的把_DATA段的指针指向外部库函数</li><li>fishhook能够替换NSLog等库函数,这事是因为Mach-O的符号表里有NSLog等,可以通过符号表找到NSLog字符串。</li></ul><p><strong>说明</strong>:具体原理参考<a href="https://www.jianshu.com/p/4d86de908721" target="_blank" rel="noopener">iOS逆向工程 - fishhook原理</a></p><h5 id="3、利用fishhook-hook-NSLog函数"><a href="#3、利用fishhook-hook-NSLog函数" class="headerlink" title="3、利用fishhook hook NSLog函数"></a>3、利用fishhook hook NSLog函数</h5><p>实现代码如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">//申明一个函数指针用于保存原NSLog的真实函数地址</span><br><span class="line">static void (*orig_nslog)(NSString *format, ...);</span><br><span class="line"></span><br><span class="line">//NSLog重定向</span><br><span class="line">void redirect_nslog(NSString *format, ...) {</span><br><span class="line"> </span><br><span class="line"> //可以添加自己的处理,比如输出到自己的持久化存储系统中</span><br><span class="line"></span><br><span class="line"> //继续执行原来的 NSLog</span><br><span class="line"> va_list va;</span><br><span class="line"> format = [NSString stringWithFormat:@"[hook success]%@",format];</span><br><span class="line"> va_start(va, format);</span><br><span class="line"> NSLogv(format, va);</span><br><span class="line"> va_end(va);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">int main(int argc, const char * argv[]) {</span><br><span class="line"> @autoreleasepool {</span><br><span class="line"> struct rebinding nslog_rebinding = {"NSLog",redirect_nslog,(void*)&orig_nslog};</span><br><span class="line"> rebind_symbols((struct rebinding[1]){nslog_rebinding}, 1);</span><br><span class="line"> NSLog(@"%@, hello word!",@"ss");</span><br><span class="line"> }</span><br><span class="line"> return</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">//[hook success]ss, hello word!</span><br></pre></td></tr></table></figure><ul><li>利用fishhook对方法的符号地址进行了重新板顶,从而只要是NDSLog的调用就会转向redirect_nslog方法调用。</li></ul><p><strong>参考</strong>:<a href="https://melodyofnight.github.io/2017/02/24/%E4%BD%BF%E7%94%A8fishhook-hook-NSLog-%E5%87%BD%E6%95%B0/" target="_blank" rel="noopener">使用fishhook hook NSLog 函数</a></p><h4 id="四、dup2重定向"><a href="#四、dup2重定向" class="headerlink" title="四、dup2重定向"></a>四、dup2重定向</h4><h5 id="1、介绍-1"><a href="#1、介绍-1" class="headerlink" title="1、介绍"></a>1、介绍</h5><ul><li>NSLog最后重定向的句柄是STDERR,NSLog输出的日志内容,最终都通过STDERR句柄来记录,而dup2函数式专门进行文件重定向的;</li><li>可以使用dup2重定向STDERR句柄,将内容重定向指定的位置,如写入文件,上传服务器,显示到View上。</li></ul><h5 id="2、核心代码"><a href="#2、核心代码" class="headerlink" title="2、核心代码"></a>2、核心代码</h5><ul><li>实现重定向,需要通过<code>NSPipe</code>创建一个管道,pipe有读端和写端,然后通过<code>dup2</code>将标准输入重定向到pipe的写端。再通过<code>NSFileHandle</code>监听pipe的读端,最后再处理读出的信息。</li><li>之后通过printf或者NSLog写数据,都会写到pipe的写端,同时pipe会将这些数据直接传送到读端,最后通过NSFileHandle的监控函数取出这些数据。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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></pre></td><td class="code"><pre><span class="line">- (void)redirectSTD:(int )fd {</span><br><span class="line"> </span><br><span class="line"> NSPipe * pipe = [NSPipe pipe] ;</span><br><span class="line"> NSFileHandle *pipeReadHandle = [pipe fileHandleForReading] ;</span><br><span class="line"> int pipeFileHandle = [[pipe fileHandleForWriting] fileDescriptor];</span><br><span class="line"> dup2(pipeFileHandle, fd) ;</span><br><span class="line"> </span><br><span class="line"> [[NSNotificationCenter defaultCenter] addObserver:self</span><br><span class="line"> selector:@selector(redirectNotificationHandle:)</span><br><span class="line"> name:NSFileHandleReadCompletionNotification</span><br><span class="line"> object:pipeReadHandle] ;</span><br><span class="line"> [pipeReadHandle readInBackgroundAndNotify];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (void)redirectNotificationHandle:(NSNotification *)nf {</span><br><span class="line"> NSData *data = [[nf userInfo] objectForKey:NSFileHandleNotificationDataItem];</span><br><span class="line"> NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] ;</span><br><span class="line"> //可以添加自己的处理,可以将内容显示到View,或者是存放到另一个文件中等等</span><br><span class="line"> //todo</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> [[nf object] readInBackgroundAndNotify];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">//使用</span><br><span class="line">[self redirectSTD:STDERR_FILENO];</span><br></pre></td></tr></table></figure><ul><li>参考:<a href="https://yohunl.com/iosri-zhi-huo-qu-he-shi-shi-liu-lan-qi-xian-shi-ri-zhi/" target="_blank" rel="noopener">iOS日志获取和实时浏览器显示日志</a></li></ul>]]></content>
<summary type="html">
<p><em>既往不恋,纵情向前</em></p>
<h4 id="一、NSLog概述"><a href="#一、NSLog概述" class="headerlink" title="一、NSLog概述"></a>一、NSLog概述</h4><h5 id="1、NSLog是什么"
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="iOS" scheme="http://buaa0300/nanhuacoder.com/tags/iOS/"/>
<category term="NSLog" scheme="http://buaa0300/nanhuacoder.com/tags/NSLog/"/>
<category term="hook" scheme="http://buaa0300/nanhuacoder.com/tags/hook/"/>
</entry>
<entry>
<title>Webview加载H5优化小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/11/iOS-WKWebView02/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/11/iOS-WKWebView02/</id>
<published>2019-04-11T14:06:05.000Z</published>
<updated>2019-04-26T09:31:00.000Z</updated>
<content type="html"><![CDATA[<p><em>行到水穷处,坐看云起时</em></p><h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、背景"><a href="#1、背景" class="headerlink" title="1、背景"></a>1、背景</h5><ul><li>鉴于H5的优势,客户端的很多业务都由H5来实现,Webview成了App中H5业务的唯一载体。</li><li>WebView组件是iOS组件体系中非常重要的一个,之前的UIWebView 存在严重的性能和内存消耗问题,iOS 8之后推出WKWebView,旨在代替UIWebView;</li><li>WKWebView在性能、稳定性、内存占用上有很大的提升,支持更多的HTML5特性,高达60fps的滚动刷新率以及内置手势;可以通过KVO监控网络加载的进度,获取网页title;</li><li>实践中,大部分App的H5业务将由WKWebview承载。</li></ul><h5 id="2、H5页面的体验问题"><a href="#2、H5页面的体验问题" class="headerlink" title="2、H5页面的体验问题"></a>2、H5页面的体验问题</h5><p>从用户角度,相比Native页面,H5页面的体验问题主要有两点:</p><ul><li><strong>页面打开时间慢</strong>:打开一个 H5 页面需要做一系列处理,会有一段白屏时间,体验糟糕。</li><li><strong>响应流畅度较差</strong>:由于 WebKit 的渲染机制,单线程,历史包袱等原因,页面刷新/交互的性能体验不如原生。</li></ul><p><strong>这里讨论的是:第一点,怎样减少白屏时间。</strong></p><h4 id="二、Webview打开H5"><a href="#二、Webview打开H5" class="headerlink" title="二、Webview打开H5"></a>二、Webview打开H5</h4><p>通过Webview打开H5页面,请求并得到 HTML、CSS 和 JavaScript 等资源并对其进行处理从而渲染出 Web 页面。</p><h5 id="1、加载流程"><a href="#1、加载流程" class="headerlink" title="1、加载流程"></a>1、加载流程</h5><ul><li><code>初始化Webview</code> -> <code>请求页面</code> -> <code>下载数据</code> -> <code>解析HTML</code> -> <code>请求 js/css 资源</code> -><code>DOM 渲染</code> -> <code>解析 JS 执行</code> -> <code>JS 请求数据</code> -> <code>解析渲染</code> -> <code>下载渲染图片</code>-> <code>页面完整展示</code></li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-acf2d12be5ffcbbd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="H5页面加载流程.png"></p><ul><li>DOM渲染之前耗时主要在两部分:<strong>初始化Webview</strong> 和 <strong>数据请求</strong>,一般Webview首次初始化在400ms这个量级,二次加载能少一个量级。</li><li>数据请求依赖网络,网络请求一般经过:DNS查询、TCP 连接、HTTP 请求和响应。数据包括HTML、JS和CSS资源,这些都是在webview在loadRequest:之后做的,这一阶段,用户所见到的都是白屏。(虽然4G已经成为主流,但是4G延迟明显高于Wifi)。</li></ul><h5 id="2、H5页面渲染"><a href="#2、H5页面渲染" class="headerlink" title="2、H5页面渲染"></a>2、H5页面渲染</h5><p>对H5页面的渲染,主要包括:渲染树构建、布局及绘制,具体可分为:</p><ul><li><p>处理 HTML 标记并构建 DOM 树。</p></li><li><p>处理 CSS 标记并构建 CSSOM(CSS Object Model) 树。</p></li><li><p>将 DOM 与 CSSOM 合并成一个渲染树。</p></li><li><p>根据渲染树来布局,以计算每个节点的几何信息。</p></li><li><p>将各个节点绘制到屏幕上。</p></li></ul><p><strong>说明</strong>:这五个步骤并不一定一次性顺序完成。如果 DOM 或 CSSOM 被修改,以上过程需要重复执行,这样才能计算出哪些像素需要在屏幕上进行重新渲染。实际页面中,CSS 与 JavaScript 往往会多次修改 DOM 和 CSSOM。具体参考:<a href="https://www.jianshu.com/p/a2cb1e3a79be" target="_blank" rel="noopener">DOM渲染机制与常见性能优化</a></p><h5 id="3、总结"><a href="#3、总结" class="headerlink" title="3、总结"></a>3、总结</h5><ul><li>分析Webview打开H5打开的过程,我们发现,在H5优化中,前端重任在肩;</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。</span><br><span class="line">加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。</span><br><span class="line">缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存localStorage。</span><br><span class="line">渲染:JS/CSS优化,加载顺序,服务端渲染,pipeline。</span><br></pre></td></tr></table></figure><ul><li>但是客户端也很重要,主要优化DOM渲染之前这些事情,可以做有:<strong>减少DNS时间</strong>、<strong>预初始化WebView</strong> 以及 <strong>HTML、JS、CSS等资源离线下载</strong>。</li><li>列举在某业务中笔者实践过的比较trick的优化方案,然后再引出笔者认为理想的方案。</li></ul><h4 id="二、WebView的客户端优化-trick版"><a href="#二、WebView的客户端优化-trick版" class="headerlink" title="二、WebView的客户端优化(trick版)"></a>二、WebView的客户端优化(trick版)</h4><p><em>由于是接入第三方的H5页面,接入离线包方案,需要比较繁杂的商务沟通和技术挑战(业务逻辑和代码超级诡异),临时采用如下优化方案</em>。</p><h5 id="1、预加载资源"><a href="#1、预加载资源" class="headerlink" title="1、预加载资源"></a>1、预加载资源</h5><ul><li>将首页面需要的<code>JS文件</code>和<code>CSS文件</code>等资源放在一个URL地址(<em>和业务url同域名</em>);</li><li>启动App后,间隔X秒去加载;加载的策略是,检查当前和上一次间隔时间,超时则加载,有效期忽略预加载请求。</li></ul><h5 id="2、预初始化Webview"><a href="#2、预初始化Webview" class="headerlink" title="2、预初始化Webview"></a>2、预初始化Webview</h5><ul><li><p>首次初始化Webview,需要初始化浏览器内核,需要的时间在400ms这个量级;二次初始化时间在几十ms这个量级;</p></li><li><p>根据此特征:选择在APP 启动后X秒,预创建(初始化)一个 Webview 然后释放,这样等使用到 H5 模块,再加载 Webview时,加载时间也少了不少。</p></li><li><p>结合步骤一中预加载公共资源,也需要Webview,所以选择在加载公共资源包时候,首次初始化Webview,加载资源,然后释放。</p></li></ul><h5 id="3、最终方案-迫不得已"><a href="#3、最终方案-迫不得已" class="headerlink" title="3、最终方案(迫不得已)"></a>3、最终方案(迫不得已)</h5><p> 由于第三方业务H5很多问题,和人力上不足;不得不需要客户端强行配合优化,在产品的要求下,不得不采用如下方案,方案的前提是:业务H5尽可能少修改,甚至不修改,客户端还要保证首屏加载快;</p><ul><li>预加载资源</li><li>预创建Webview并加载首页H5,驻留在内存中,需要的时候,立刻显示。</li></ul><h5 id="4、方案的后遗症"><a href="#4、方案的后遗症" class="headerlink" title="4、方案的后遗症"></a>4、方案的后遗症</h5><ul><li>我不建议这种trick做法,因为自从开了这个口子,后续很多H5需求不走之前既定的离线包方案,在内存中预创建多个Webview (最多4个),加载H5时候不用新建Webview,从Webview池中获取;</li><li>此种Webview池方案带来诸多隐患:内存压力、诡异的白屏、JS造成的内存泄露,页面的清空等等问题(<em>填坑填到掉头发</em>)。</li></ul><h4 id="三、离线包方案"><a href="#三、离线包方案" class="headerlink" title="三、离线包方案"></a>三、离线包方案</h4><h5 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h5><ul><li><p>离线包方案才是业务主流的H5加载优化方案,非常建议在客户端团队和前端团队推广,类似预创建Webview加载H5不应该成为主流。</p></li><li><p>将每个独立的H5功能模块,相关HTML、Javascript、CSS 等页面内<strong>静态资源</strong>打包到一个压缩包内,客户端可以下载该离线包到本地,然后打开Webview,直接从本地加载离线包,从而最大程度地摆脱网络环境对 H5 页面的影响。</p></li><li>离线包可以<strong>提升用户体验</strong>(页面加载更快),还可以<strong>实现动态更新</strong>(在推出新版本或是紧急发布的时候,可以把修改的资源放入离线包,通过更新配置让应用自动下载更新)</li></ul><h5 id="2、方案描述"><a href="#2、方案描述" class="headerlink" title="2、方案描述"></a>2、方案描述</h5><p><em>引用bang的离线包方案,简单描述如下</em>:</p><ul><li><p>后端使用构建工具把同一个业务模块相关的页面和资源打包成一个文件,同时对文件加密/签名。</p></li><li><p>客户端根据配置表,在自定义时机去把离线包拉下来,做解压/解密/校验等工作。</p></li><li><p>根据配置表,打开某个业务时转接到打开离线包的入口页面。</p></li><li><p>拦截网络请求,对于离线包已经有的文件,直接读取离线包数据返回,否则走 HTTP 协议缓存逻辑。</p></li><li><p>离线包更新时,根据版本号后台下发两个版本间的 diff 数据,客户端合并,增量更新。</p></li></ul><p><strong>说明</strong>:目前WKWebView已经能成为主流,但是WKWebView在实现离线包方案时,拦截网络请求有坑。</p><h5 id="3、WKWebView拦截网络请求的坑"><a href="#3、WKWebView拦截网络请求的坑" class="headerlink" title="3、WKWebView拦截网络请求的坑"></a>3、WKWebView拦截网络请求的坑</h5><ul><li>虽然NSURLProtocol可以拦截监听每一个<strong>URL Loading System</strong>中发出<strong>request</strong>请求,记住是URL Loading System中那些类发出的请求,也支持AFNetwoking,UIWebView发出的request,NSURLProtocol都可以拦截和监听。</li><li>因为WKWebView 在<strong>独立进程里</strong>执行网络请求。一旦注册 http(s) scheme 后,网络请求将从 Network Process 发送到 App Process,这样 NSURLProtocol 才能拦截网络请求。</li><li>但是在 WebKit2 的设计里使用 MessageQueue 进行进程之间的通信,Network Process 会将请求 encode 成一个 Message,然后通过 IPC(进程间通信) 发送给 App Process。出于性能的原因,encode 的时候 将HTTPBody 和 HTTPBodyStream 这两个字段丢弃掉(<strong>坑</strong>)</li><li>因此,如果通过 <strong>registerSchemeForCustomProtocol</strong> 注册了 http(s) scheme, 那么由 WKWebView 发起的所有 http(s)请求都会通过 IPC 传给主进程 NSURLProtocol 处理,<strong>导致 post 请求 body 被清空</strong>;</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">//苹果开源的 WebKit2 源码暴露了私有API:</span><br><span class="line">+ [WKBrowsingContextController registerSchemeForCustomProtocol:]</span><br><span class="line"></span><br><span class="line">//通过注册 http(s) scheme 后 WKWebView 将可以使用 NSURLProtocol 拦截 http(s) 请求:</span><br><span class="line">Class cls = NSClassFromString(@"WKBrowsingContextController”); </span><br><span class="line">SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); </span><br><span class="line">if ([(id)cls respondsToSelector:sel]) { </span><br><span class="line"> // 注册http(s) scheme, 把 http和https请求交给 NSURLProtocol处理 </span><br><span class="line"> [(id)cls performSelector:sel withObject:@"http"]; </span><br><span class="line"> [(id)cls performSelector:sel withObject:@"https"]; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>说明1:</strong>名目张胆使用私有API,是过不了AppStore审核的,具体使用什么办法,想来你也懂(hun xiao)。</p><p><strong>说明2</strong>:一旦打开ATS开关:<strong>Allow Arbitrary Loads 选项设置为NO</strong>,通过 registerSchemeForCustomProtocol 注册了 http(s) scheme,WKWebView 发起的所有 http(s) 网络请求将被阻塞(即便将<strong>Allow Arbitrary Loads in Web Content 选项设置为YES</strong>);</p><p><strong>说明3</strong>:iOS11之后可以通过<strong>WKURLSchemeHandler</strong>去完成对<code>WKWebView</code>的请求拦截,不需要再调用私有API解决上述问题了。</p><h5 id="4、WKWebView自定义资源scheme"><a href="#4、WKWebView自定义资源scheme" class="headerlink" title="4、WKWebView自定义资源scheme"></a>4、WKWebView自定义资源scheme</h5><ul><li>向WKWebView 注册 customScheme, 比如 <strong>dynamic://</strong>, 而不是https或http,避免对https或http请求的影响</li><li>保证使用离线包功能的请求,没有post方式,遇到customScheme请求,比如<code>dynamic://www.dynamicalbumlocalimage.com/</code>,通过 NSURLProtocol 拦截这个请求并加载离线数据。</li><li>iOS 11上, WebKit 提供的WKURLSchemeHandler可实现拦截,需要注意的只允许开发者拦截自定义 Scheme 的请求,不允许拦截 “http”、“https”、“ftp”、“file” 等的请求,否则会crash。</li></ul><h4 id="四、其他"><a href="#四、其他" class="headerlink" title="四、其他"></a>四、其他</h4><h5 id="1、LocalWebServer"><a href="#1、LocalWebServer" class="headerlink" title="1、LocalWebServer"></a>1、LocalWebServer</h5><ul><li>离线包方案中,除了拦截请求加载资源的方式,还有种在项目中搭建local web server,用以获得本地资源。市面有比较完善的框架</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CocoaHttpServer (支持iOS、macOS及多种网络场景)</span><br><span class="line">GCDWebServer (基于iOS,不支持 https 及 webSocket)</span><br><span class="line">Telegraph (Swift实现,功能较上面两类更完善)</span><br></pre></td></tr></table></figure><ul><li>具体可参考 <a href="https://juejin.im/entry/599e500e6fb9a0249174fa31" target="_blank" rel="noopener">基于 LocalWebServer 实现 WKWebView 离线资源加载</a>, 之前团队有过实践,采用的是GCDWebServer</li></ul><h5 id="2、WKWebView-loadRequest-问题"><a href="#2、WKWebView-loadRequest-问题" class="headerlink" title="2、WKWebView loadRequest 问题"></a>2、WKWebView loadRequest 问题</h5><ul><li>在 WKWebView 上通过 loadRequest 发起的 post 请求 body 数据会丢失:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">//同样是由于进程间通信性能问题,HTTPBody字段被丢弃</span><br><span class="line">[request setHTTPMethod:@"POST"];</span><br><span class="line">[request setHTTPBody:[@"bodyData" dataUsingEncoding:NSUTF8StringEncoding]];</span><br><span class="line">[wkwebview loadRequest: request];</span><br></pre></td></tr></table></figure><p><strong>解决</strong>:假如想通过-[WKWebView loadRequest:]加载 post 请求 (原始请求)request1: <strong>h5.nanhua.com/order/list</strong>,可以通过以下步骤实现:</p><ul><li>替换请求 scheme,生成新的 post 请求 request2: <strong>post://h5.nanhua.com/order/list</strong>, 同时将 request1 的 <strong>body 字段复制到 request2 的 header 中</strong>(WebKit 不会丢弃 header 字段);</li><li>通过-[WKWebView loadRequest:] 加载新的 post 请求 request2;</li><li>并且通过 +[WKBrowsingContextController registerSchemeForCustomProtocol:]注册 scheme: <strong>post://</strong>;</li><li>注册 NSURLProtocol 拦截请求 <strong>post://h5.nanhua.com/order/list</strong> ,替换请求 scheme, 生成新的请求 request3: <strong>h5.nanhua.com/order/list</strong>,将 <strong>request2 header的body 字段复制到 request3 的 body 中</strong>,并使用 NSURLSession 加载 request3,最后将加载结果返回 WKWebView;</li></ul><h5 id="3、推荐资料"><a href="#3、推荐资料" class="headerlink" title="3、推荐资料"></a>3、推荐资料</h5><ul><li><p><a href="https://mp.weixin.qq.com/s/0OR4HJQSDq7nEFUAaX1x5A" target="_blank" rel="noopener">移动端本地 H5 秒开方案探索与实现</a></p></li><li><p><a href="https://developers.google.com/speed/docs/insights/mobile" target="_blank" rel="noopener">使用 PageSpeed Insights 进行移动版分析</a></p></li><li><p><a href="https://tech.meituan.com/2017/06/09/webviewperf.html" target="_blank" rel="noopener">WebView性能、体验分析与优化</a></p></li><li><p><a href="https://juejin.im/post/5c9c664ff265da611624764d" target="_blank" rel="noopener">iOS app秒开H5优化总结</a></p></li><li><a href="http://huyangjie.com/2018/05/02/%E8%B5%8B%E4%BA%88H5%E4%BB%A5Native%E7%9A%84%E7%94%9F%E5%91%BD-%E2%80%94%E2%80%94%E3%80%8AWebView%E4%BC%98%E5%8C%96%E3%80%8B/" target="_blank" rel="noopener">赋予H5以Native的生命 ——《WebView优化》</a></li></ul>]]></content>
<summary type="html">
<p><em>行到水穷处,坐看云起时</em></p>
<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、背景"><a href="#1、背景" class="
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="iOS" scheme="http://buaa0300/nanhuacoder.com/tags/iOS/"/>
<category term="WKWebView优化" scheme="http://buaa0300/nanhuacoder.com/tags/WKWebView%E4%BC%98%E5%8C%96/"/>
<category term="离线包" scheme="http://buaa0300/nanhuacoder.com/tags/%E7%A6%BB%E7%BA%BF%E5%8C%85/"/>
</entry>
<entry>
<title>WKWebView的Cookie问题</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/10/iOS-WKWebView-Cookie/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/10/iOS-WKWebView-Cookie/</id>
<published>2019-04-10T14:31:25.000Z</published>
<updated>2019-04-18T13:58:06.000Z</updated>
<content type="html"><![CDATA[<p><em>往者不可谏,来者犹可追</em></p><h4 id="一、Cookie和Session概述"><a href="#一、Cookie和Session概述" class="headerlink" title="一、Cookie和Session概述"></a>一、Cookie和Session概述</h4><p>Cookie和Session都是为了保存<strong>客户端和服务端之间的交互状态</strong>,实现机制不同,各有优缺点。</p><h5 id="1、Cookie"><a href="#1、Cookie" class="headerlink" title="1、Cookie"></a>1、Cookie</h5><ul><li>Cookie是客户端请求服务端时,服务器会将一些信息以键值对的形式返回给客户端,保存在浏览器中,后续交互的时候可以带上这些Cookie值。用Cookie就可以方便的做一些缓存。</li><li>Cookie的缺点是<strong>大小和数量都有限制</strong>;Cookie是存在客户端的可能被禁用、删除、篡改,是不安全的;Cookie如果很大,每次要请求都要带上,这样就影响了传输效率。</li><li>Cookie的内容主要包括:<strong>名字</strong>,<strong>值</strong>,<strong>过期时间</strong>,<strong>路径</strong>和<strong>域</strong>;<strong>路径</strong>与<strong>域</strong>一起构成Cookie的作用范围。若不设置过期时间,则表示这个Cookie的生命期为浏览器会话期间,关闭浏览器窗口,Cookie就消失。这种生命期为浏览器会话期的Cookie被称为<strong>会话Cookie</strong>。<strong>会话Cookie</strong>一般不存储在硬盘上而是保存在内存里。</li><li>若设置了过期时间,浏览器就会把Cookie保存到硬盘上,关闭后再次打开浏览器,这些Cookie仍然有效直到超过设定的过期时间。<strong>存储在硬盘上的Cookie可以在不同的浏览器进程间共享</strong>,比如两个IE窗口。而对于保存在内存里的Cookie,不同的浏览器有不同的处理方式 。</li></ul><h5 id="2、Session"><a href="#2、Session" class="headerlink" title="2、Session"></a>2、Session</h5><ul><li><p>Session是基于Cookie来实现的,不同的是Session本身存在于<strong>服务端</strong>,但是每次传输的时候不会将数据传输给客户端,只是把代表一个客户端的<strong>sessionid</strong>(jsessionid只是Tomcat中对<strong>sessionid</strong>的叫法)写在客户端的Cookie中,这样每次传输这个ID就可以了。</p></li><li><p>Session的优势就是<strong>传输数据量小,比较安全</strong>。Session有缺点,就是如果Session不做特殊的处理容易<strong>失效、过期、丢失或者Session过多导致服务器内存溢出</strong>,并且要实现一个稳定可用安全的分布式Session框架也是有一定复杂度的。在实际使用中就要结合Cookie和Session的优缺点针对不同的问题来设计解决方案。</p></li></ul><h5 id="3、理解"><a href="#3、理解" class="headerlink" title="3、理解"></a>3、理解</h5><ul><li>Session是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。 </li><li>当Server程序要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session id,如果已包含则说明以前已经为此客户端创建过session,服务器就按照session id把这个session检索出来使用(检索不到,会新建一个);如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个session id将被在本次响应中返回给客户端保存。 </li><li><strong>保存这个session id的方式可以采用Cookie</strong>,这样在交互过程中浏览器可以自动的按照规则把这个标识发送给服务器。一般这个cookie的名字都是类似于SEEESIONID。但<strong>Cookie可以被人为的禁止,则必须有其他机制以便在cookie被禁止时仍然能够把session id传递回服务器</strong>。 </li><li>经常被使用的一种技术叫做<strong>URL重写</strong>,就是把session id直接附加在URL路径的后面。还有一种技术叫做<strong>表单隐藏字段</strong>。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器。</li></ul><h4 id="二、WKWebView和Cookie"><a href="#二、WKWebView和Cookie" class="headerlink" title="二、WKWebView和Cookie"></a>二、WKWebView和Cookie</h4><h5 id="1、起因"><a href="#1、起因" class="headerlink" title="1、起因"></a>1、起因</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie</span><br></pre></td></tr></table></figure><ul><li><p>目前许多 H5 业务都<strong>依赖于 Cookie 作登录态校验</strong>,如果登陆是在 WebView 里做的,不会有什么问题;但是在很多场景下,在Native做登录,需要将登录信息带给WebView;但是在Native做了登录,也获取了Cookie信息,也使用 <strong>NSHTTPCookieStorage</strong> 将Cookie存到了本地;但是WKWebView在打开时候,不会自动去NSHTTPCookieStorage获取Cookie信息,这就是著名的<strong>首次 WKWebView 请求不携带 Cookie 的问题</strong>。</p></li><li><p>WKWebView 实例其实会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中。</p></li><li><p>其实,iOS11 可以解决<em>首次 WKWebView 请求不携带 Cookie 的问题</em>,<strong>只要是存在 WKHTTPCookieStore 里的 cookie,WKWebView 每次请求都会携带</strong>。</p></li></ul><h5 id="2、获取Cookie"><a href="#2、获取Cookie" class="headerlink" title="2、获取Cookie"></a>2、获取Cookie</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">// 方法一</span><br><span class="line"> NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;</span><br><span class="line"> NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];</span><br><span class="line"> NSLog(@"response-cookies = %@",cookies);</span><br><span class="line"> </span><br><span class="line"> //方法二</span><br><span class="line"> NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];</span><br><span class="line"> NSLog(@"cookieString = %@",cookieString);</span><br><span class="line"> </span><br><span class="line"> //方法三(如果有的话)</span><br><span class="line"> NSArray<NSHTTPCookie *> *httpCookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];</span><br><span class="line"> NSLog(@"httpCookies = %@",httpCookies);</span><br><span class="line"> </span><br><span class="line"> //方法四</span><br><span class="line"> if(@available(iOS 11, *)){</span><br><span class="line"> //WKHTTPCookieStore的使用</span><br><span class="line"> WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;</span><br><span class="line"> //获取 cookies</span><br><span class="line"> [cookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {</span><br><span class="line"> [cookies enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {</span><br><span class="line"> NSLog(@"cookieStore-cookies_%@:%@",@(idx),obj);</span><br><span class="line"> }];</span><br><span class="line"> }];</span><br><span class="line"> }</span><br><span class="line"> //将cookie设置到本地</span><br><span class="line"> for (NSHTTPCookie *cookie in cookies) {</span><br><span class="line"> //NSHTTPCookie cookie</span><br><span class="line"> [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> decisionHandler(WKNavigationResponsePolicyAllow);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>查看WKHTTPCookieStore中的某个cookie信息,如下</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">version:1</span><br><span class="line">name:Hm_lvt_0c0e9d9b1e7d617b3e6842e85b9fb068</span><br><span class="line">value:1554970993,1554971029,1554971246,1554971319</span><br><span class="line">expiresDate:'2020-04-10 08:28:38 +0000'</span><br><span class="line">created:'2019-04-11 08:28:38 +0000'</span><br><span class="line">sessionOnly:FALSE</span><br><span class="line">domain:.jianshu.com</span><br><span class="line">partition:none</span><br><span class="line">sameSite:none</span><br><span class="line">path:/</span><br><span class="line">isSecure:FALSE</span><br><span class="line">path:"/" </span><br><span class="line">isSecure:FALSE</span><br></pre></td></tr></table></figure><h5 id="3、未过期Cookie持久化"><a href="#3、未过期Cookie持久化" class="headerlink" title="3、未过期Cookie持久化"></a>3、未过期Cookie持久化</h5><ul><li><strong>未过期的 Cookie</strong>被持久化存储在 <code>NSLibraryDirectory</code> 目录下的 <code>Cookies/</code>文件夹。</li><li><p>Cookie 持久化文件地址在 iOS 9+ 上在<code>NSLibraryDirectory/Cookies</code>,但是在 iOS 8 上 cookie 被保存在两部分,一部分如上所述,还有一部分保存在 App 无法获取的地方,<code>/Users/Mac/Library/Developer/CoreSimulator/Devices/D2F74420-D59B-4A15-A50B-774D3D01FADE/data/Library/Cookies</code>,大概就是后者的 Cookie 是 iOS 的 Safari 使用 。</p></li><li><p>在 Cookies 目录下两个文件比较重要;</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Cookies.binarycookies</span><br><span class="line"><appid>.binarycookies</span><br></pre></td></tr></table></figure><p>两者的区别是 <appid>.binarycookies 是 NSHTTPCookieStorage 文件对象;<appid>.binarycookies 对应 WKWebview 的实例化对象。</appid></appid></p></li></ul><h4 id="三、WKWebView的Cookie注入"><a href="#三、WKWebView的Cookie注入" class="headerlink" title="三、WKWebView的Cookie注入"></a>三、WKWebView的Cookie注入</h4><h5 id="1、Javascript注入Cookie"><a href="#1、Javascript注入Cookie" class="headerlink" title="1、Javascript注入Cookie"></a>1、Javascript注入Cookie</h5><p>在初始化 <strong>WKWebView</strong> 的时候,通过 <strong>WKUserScript</strong> 设置,使用<strong>Javascript</strong> 注入 <strong>Cookie</strong>。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">//js注入</span><br><span class="line">WKUserContentController* userContentController = [[WKUserContentController alloc]init]; </span><br><span class="line">WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie ='CookieKey=CookieValue';"injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; </span><br><span class="line">[userContentController addUserScript:cookieScript]; </span><br><span class="line"></span><br><span class="line">WKWebViewConfiguration* webViewConfig = [[WKWebViewConfiguration alloc]init]; </span><br><span class="line">webViewConfig.userContentController = userContentController; </span><br><span class="line">WKWebView *webView = [[WKWebView alloc] initWithFrame:frame configuration:webViewConfig];</span><br></pre></td></tr></table></figure><ul><li><strong>通过 document.cookie 设置 Cookie (JS注入)解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题</strong>;但是会遇到跨域丢失的问题,</li><li>无法解决302请求的Cookie问题,假设第一个请求是 www.a.com,我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面302跳转到 www.b.com,这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。</li><li>每一次页面跳转前都会调用回调函数decidePolicyForNavigationAction, 在这里拦截302请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{</span><br><span class="line"> //</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>但是这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:]只适合加载 mainFrame 请求。</li></ul><h5 id="2、-NSMutableURLRequest-请求带上-Cookie"><a href="#2、-NSMutableURLRequest-请求带上-Cookie" class="headerlink" title="2、 NSMutableURLRequest 请求带上 Cookie"></a>2、 NSMutableURLRequest 请求带上 Cookie</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//request携带</span><br><span class="line">NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; </span><br><span class="line">//[request setHTTPShouldHandleCookies:YES];</span><br><span class="line">[request setValue:[NSString stringWithFormat:@"%@=%@",@"CookieKey", @"CookieValue"] forHTTPHeaderField:@"Cookie"]; </span><br><span class="line">[webView loadRequest:request];</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:WKWebView loadRequest 前,在 request header 中设置 Cookie,可以解决(首个)请求 Cookie 带不上的问题;</p><h5 id="3、WKHTTPCookieStore-iOS-11-later"><a href="#3、WKHTTPCookieStore-iOS-11-later" class="headerlink" title="3、WKHTTPCookieStore (iOS 11 later)"></a>3、WKHTTPCookieStore (iOS 11 later)</h5><ul><li>利用iOS11 API <strong>WKHTTPCookieStore</strong> 解决 <strong>WKWebView</strong> 首次请求不携带 <strong>Cookie</strong> 的问题;这是因为:WKWebView每次请求都会携带 WKHTTPCookieStore 里的 Cookie。(WKWebView 使用 NSURLProtocol 拦截请求无法获取 Cookie 信息)</li><li>在执行 <em>[WKWebView loadRequest:]</em> 前将 NSHTTPCookieStorage中的Cookie信息复制到 <strong>WKHTTPCookieStore</strong> 中,以此来达到 WKWebView中注入Cookie 的目的。示例代码如下:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">if(@available(iOS 11, *)){</span><br><span class="line"> //发送请求前插入cookie</span><br><span class="line"> NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];</span><br><span class="line"> WKHTTPCookieStore *cookieStore = self.wkWebView.configuration.websiteDataStore.httpCookieStore;</span><br><span class="line"> for (NSHTTPCookie *cookie in cookies) {</span><br><span class="line"> [cookieStore setCookie:cookie completionHandler:^{</span><br><span class="line"> //</span><br><span class="line"> }];</span><br><span class="line"> }</span><br><span class="line"> [self.wkWebView loadRequest:request];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="4、多WKWebView实例共享Cookie"><a href="#4、多WKWebView实例共享Cookie" class="headerlink" title="4、多WKWebView实例共享Cookie"></a>4、多WKWebView实例共享Cookie</h5><ul><li><p>Session 级别的 cookie 是保存在 <code>WKProcessPool</code> 里的,每个 WKWebview 都可以关联一个 <code>WKProcessPool</code> 的实例,如果需要在整个 <strong>App 生命周期里访问 h5 保留 h5 里的登录状态的,可以将使用 <code>WKProcessPool</code> 的单例来共享登录状态。</strong></p></li><li><p>让所有 <strong>WKWebView</strong> 共享同一个 <strong>WKProcessPool</strong> 实例,可以实现多个 <strong>WKWebView</strong> 之间共享 <strong>Cookie(session Cookie and persistent Cookie)</strong> 数据。不过 <strong>WKWebView WKProcessPool</strong> 实例在 App 杀进程重启后会被重置,导致 <strong>WKProcessPool</strong> 中的 <strong>Cookie</strong>、<strong>session Cookie</strong> 数据丢失,目前也无法实现 <strong>WKProcessPool</strong> 实例本地化保存。</p></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">//WKProcessPool+SharedProcessPool.h</span><br><span class="line">@interface WKProcessPool (SharedProcessPool)</span><br><span class="line"></span><br><span class="line">+ (WKProcessPool*)sharedProcessPool;</span><br><span class="line"></span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">//WKProcessPool+SharedProcessPool.m</span><br><span class="line">@implementation WKProcessPool (SharedProcessPool)</span><br><span class="line"></span><br><span class="line">+ (WKProcessPool*)sharedProcessPool {</span><br><span class="line"> static WKProcessPool* shared;</span><br><span class="line"> static dispatch_once_t onceToken;</span><br><span class="line"> dispatch_once(&onceToken, ^{</span><br><span class="line"> shared = [[WKProcessPool alloc] init];</span><br><span class="line"> });</span><br><span class="line"> return shared;</span><br><span class="line">}</span><br><span class="line">@end</span><br><span class="line"> </span><br><span class="line">//use</span><br><span class="line">config.processPool = [WKProcessPool sharedProcessPool]; </span><br><span class="line">self.wkWebView = [[WKWebView alloc]initWithFrame:self.view.bounds configuration:config];</span><br><span class="line">[self.view addSubview:self.wkWebView];</span><br></pre></td></tr></table></figure><h5 id="5、其他"><a href="#5、其他" class="headerlink" title="5、其他"></a>5、其他</h5><ul><li>H5地址是非Https,遇到奇怪的Cookie丢失问题。原因未知。</li></ul><h4 id="四、WKWebView中Cookie的清除"><a href="#四、WKWebView中Cookie的清除" class="headerlink" title="四、WKWebView中Cookie的清除"></a>四、WKWebView中Cookie的清除</h4><h5 id="1、按内容删除"><a href="#1、按内容删除" class="headerlink" title="1、按内容删除"></a>1、按内容删除</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">if (@available(iOS 9, *)){</span><br><span class="line"> // 以www.baidu.com为例,是否包含baidu.com</span><br><span class="line"> NSString *displayName = @"baidu.com";</span><br><span class="line"> WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore];</span><br><span class="line"> [dataStore fetchDataRecordsOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] completionHandler:^(NSArray<WKWebsiteDataRecord *> * __nonnull records) {</span><br><span class="line"> for (WKWebsiteDataRecord *record in records){</span><br><span class="line"> if ([displayName containsString:record.displayName]){</span><br><span class="line"> [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:record.dataTypes forDataRecords:@[record] completionHandler:^{</span><br><span class="line"> NSLog(@"Cookies for %@ deleted successfully",record.displayName);</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></pre></td></tr></table></figure><h5 id="2、按时间删除"><a href="#2、按时间删除" class="headerlink" title="2、按时间删除"></a>2、按时间删除</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">- (void)removeWebViewDataCache:(NSDate *)sinceDate {</span><br><span class="line"> </span><br><span class="line"> if (@available(iOS 11.0, *)) {</span><br><span class="line"> // iOS 9 以后终于可以使用 WKWebsiteDataStore 来清理缓存</span><br><span class="line"> NSSet *websiteDataTypes = [WKWebsiteDataStore allWebsiteDataTypes];</span><br><span class="line"> [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:sinceDate completionHandler:^{</span><br><span class="line"> NSLog(@"clear webView cache");</span><br><span class="line"> }];</span><br><span class="line"> } else {</span><br><span class="line"> // iOS 8 可以通过清理 Library 目录下的 Cookies 目录来清除缓存</span><br><span class="line"> NSString *libraryPath = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES).firstObject;</span><br><span class="line"> NSString *cookiesFolderPath = [libraryPath stringByAppendingString:@"/Cookies"];</span><br><span class="line"> [[NSFileManager defaultManager] removeItemAtPath:cookiesFolderPath error:nil];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="五、IP-直连方案对Cookie的影响"><a href="#五、IP-直连方案对Cookie的影响" class="headerlink" title="五、IP 直连方案对Cookie的影响"></a>五、IP 直连方案对Cookie的影响</h4><h5 id="1、-存在的问题"><a href="#1、-存在的问题" class="headerlink" title="1、 存在的问题"></a>1、 存在的问题</h5><ul><li>采用 IP 直连方案后,服务端返回的 Cookie 里的 Domain 字段也会使用 IP 。如果 IP 是动态的,就有可能导致一些问题:由于许多 H5 业务都依赖于 Cookie 作登录态校验,而 WKWebView 上请求不会自动携带 Cookie。</li></ul><h5 id="2、解决问题办法"><a href="#2、解决问题办法" class="headerlink" title="2、解决问题办法"></a>2、解决问题办法</h5><ul><li>可以参考<a href="https://yq.aliyun.com/articles/64356" target="_blank" rel="noopener">HTTPDNS域名解析场景下如何使用Cookie</a></li></ul><h4 id="六、推荐参考"><a href="#六、推荐参考" class="headerlink" title="六、推荐参考"></a>六、推荐参考</h4><ul><li><p><a href="https://juejin.im/entry/5880ac602f301e006980d1f5" target="_blank" rel="noopener">WKWebView 那些坑</a></p></li><li><p><a href="https://helpcdn.aliyun.com/knowledge_detail/60199.html" target="_blank" rel="noopener">iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明</a></p></li><li><p><a href="https://xibhe.com/2018/02/03/WKWebView-disabuse/" target="_blank" rel="noopener">使用WKWebView进行性能调优</a></p></li><li><p><a href="https://github.com/ChenYilong/iOSBlog/issues/14" target="_blank" rel="noopener">iOS 防 DNS 污染方案调研(四)— Cookie 业务场景</a></p></li></ul>]]></content>
<summary type="html">
<p><em>往者不可谏,来者犹可追</em></p>
<h4 id="一、Cookie和Session概述"><a href="#一、Cookie和Session概述" class="headerlink" title="一、Cookie和Session概述"></a>一、Co
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="iOS" scheme="http://buaa0300/nanhuacoder.com/tags/iOS/"/>
<category term="WKWebView" scheme="http://buaa0300/nanhuacoder.com/tags/WKWebView/"/>
<category term="Cookie" scheme="http://buaa0300/nanhuacoder.com/tags/Cookie/"/>
</entry>
<entry>
<title>计算广告相关术语</title>
<link href="http://buaa0300/nanhuacoder.com/2019/04/08/Ad01/"/>
<id>http://buaa0300/nanhuacoder.com/2019/04/08/Ad01/</id>
<published>2019-04-08T08:53:59.000Z</published>
<updated>2019-04-08T09:38:42.000Z</updated>
<content type="html"><![CDATA[<h4 id="一、广告分类"><a href="#一、广告分类" class="headerlink" title="一、广告分类"></a>一、广告分类</h4><h5 id="1、品牌广告"><a href="#1、品牌广告" class="headerlink" title="1、品牌广告"></a>1、品牌广告</h5><p>主要是为了品牌宣传形象用,主要是为了争取曝光度,提高品牌正面的形象,重在长远。</p><h5 id="2、效果广告"><a href="#2、效果广告" class="headerlink" title="2、效果广告"></a>2、效果广告</h5><ul><li>按效果付费的广告方式,通过广告竞价平台,广告主通过上传广告视频与相关的信息,设定价格,平台动态地以竞价的方式推荐广告;</li><li>通过广告马上带来购买或其他转化行为,重在眼前</li></ul><h4 id="二、重要指标"><a href="#二、重要指标" class="headerlink" title="二、重要指标"></a>二、重要指标</h4><h5 id="1、ROI"><a href="#1、ROI" class="headerlink" title="1、ROI"></a>1、ROI</h5><p>Return On Investment,投入产出比。</p><h5 id="2、CTR"><a href="#2、CTR" class="headerlink" title="2、CTR"></a>2、CTR</h5><p>Click Through Rate,点击通过率,广告点击次量占广告展现量的比例</p><h5 id="3、CVR"><a href="#3、CVR" class="headerlink" title="3、CVR"></a>3、CVR</h5><p>Conversion Rate,转化率,转化次数占点击次数的比例,表示用户点击广告到成为一个有效激活或者注册甚至付费用户的转化率</p><h5 id="4、CPM"><a href="#4、CPM" class="headerlink" title="4、CPM"></a>4、CPM</h5><p>Cost Per Mile,一种计费方式, 广告主为千次展示出价,参与竞价,按<strong>展现结算</strong>。</p><h5 id="5、CPC"><a href="#5、CPC" class="headerlink" title="5、CPC"></a>5、CPC</h5><p>Cost Per Click,一种计费方式,广告主为单次点击出价,参与竞价,按<strong>点击结算</strong>。</p><h5 id="6、CPT"><a href="#6、CPT" class="headerlink" title="6、CPT"></a>6、CPT</h5><p>Cost Per Time,一种计费方式,估计每天的总量,为流量单价定价,然后核算 CPT 价格报价,<strong>按日结算</strong></p><h5 id="7、CPA"><a href="#7、CPA" class="headerlink" title="7、CPA"></a>7、CPA</h5><p>Cost Per Action,一种计费方式,广告主为转化出价,参与竞价,按转化结算 </p><h5 id="8、GD"><a href="#8、GD" class="headerlink" title="8、GD"></a>8、GD</h5><p>Guaranteed Delivery,保量广告,广告主与平台在投放广告时已经确认一定量的广告</p><h5 id="9、RPM"><a href="#9、RPM" class="headerlink" title="9、RPM"></a>9、RPM</h5><p>Revenue Per Mille,千次展示收益</p><h4 id="三、其他术语"><a href="#三、其他术语" class="headerlink" title="三、其他术语"></a>三、其他术语</h4><h5 id="1、RTB"><a href="#1、RTB" class="headerlink" title="1、RTB"></a>1、RTB</h5><p>Real Time Bidding,实时竞价</p><h5 id="2、ADX"><a href="#2、ADX" class="headerlink" title="2、ADX"></a>2、ADX</h5><p>Ad Exchange,广告交易平台</p><h5 id="3、DSP"><a href="#3、DSP" class="headerlink" title="3、DSP"></a>3、DSP</h5><p>Demand Side Platform,需求方平台</p><p><a href="https://blog.csdn.net/avaloon/article/details/78185018" target="_blank" rel="noopener">https://blog.csdn.net/avaloon/article/details/78185018</a></p>]]></content>
<summary type="html">
<h4 id="一、广告分类"><a href="#一、广告分类" class="headerlink" title="一、广告分类"></a>一、广告分类</h4><h5 id="1、品牌广告"><a href="#1、品牌广告" class="headerlink" titl
</summary>
<category term="计算广告" scheme="http://buaa0300/nanhuacoder.com/categories/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/"/>
<category term="计算广告" scheme="http://buaa0300/nanhuacoder.com/tags/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/"/>
</entry>
<entry>
<title>SQLite小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/03/30/iOS-DBSQlite/"/>
<id>http://buaa0300/nanhuacoder.com/2019/03/30/iOS-DBSQlite/</id>
<published>2019-03-30T02:44:32.000Z</published>
<updated>2019-04-24T09:35:36.000Z</updated>
<content type="html"><![CDATA[<p><em>静以修身,俭以养德</em></p><h4 id="一、移动端数据库方案"><a href="#一、移动端数据库方案" class="headerlink" title="一、移动端数据库方案"></a>一、移动端数据库方案</h4><h5 id="1、关系型数据库"><a href="#1、关系型数据库" class="headerlink" title="1、关系型数据库"></a>1、<strong>关系型数据库</strong></h5><ul><li><strong>SQLite</strong>:轻量级的<strong>关系数据库</strong>, 占用资源非常少,目前广泛应用于Android、iOS等手机操作系统。iOS使用时SQLite,只需要加入libSQLite3.0.tbd依赖以及引入SQLite3.h头文件即可。</li><li>Apple内建的CoreData底层的持久化方式可以是<code>SQLite</code>数据库,也可以是XML文件、甚至是内存; 比较流行的第三方框架FMDB是对SQLite操作的封装</li></ul><h5 id="2、非关系数据库"><a href="#2、非关系数据库" class="headerlink" title="2、非关系数据库"></a>2、非关系数据库</h5><ul><li><strong>Realm</strong>:适用于iOS (同样适用于Swift&Objective-C)和Android的跨平台移动数据库,是NoSQL框架,官方定位是取代SQLite。具体可参考<a href="https://zhuanlan.zhihu.com/p/26173366" target="_blank" rel="noopener">Realm(Java)那些事</a></li><li><p>Realm非常的特色是数据变更通知,查询,存储性能比SQLite好,但是体积大、存入Realm的对象必须继承RealmObject,侵入性强,Realm中存储对象不允跨线程访问</p></li><li><p>非关系型数据库还有LevelDB、RocksDB</p></li></ul><h5 id="3、其他"><a href="#3、其他" class="headerlink" title="3、其他"></a>3、其他</h5><ul><li>16年左右,Realm兴起,部分客户端团队开始使用Realm,但是更多的团队还是继续使用SQLite及其衍生方案;使用Realm并非就一定Cool,使用SQLite并非就一定Out,<strong>方案的选择应该是基于业务本身和团队的技术积累。</strong></li><li>在16年时候,微信分享了自己对优化SQLite的源码,具体可见<a href="https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=2649286361&idx=1&sn=78bbcda7f41a14291ad71289e4821f71&scene=4#wechat_redirect" target="_blank" rel="noopener">微信iOS SQLite源码优化实践</a>,随后推出了自己的数据库方案WCDB(<em>基于SQLite</em>)</li></ul><h4 id="二、SQLite的线程模式"><a href="#二、SQLite的线程模式" class="headerlink" title="二、SQLite的线程模式"></a>二、SQLite的线程模式</h4><h5 id="1、三种线程模式"><a href="#1、三种线程模式" class="headerlink" title="1、三种线程模式"></a>1、三种线程模式</h5><ul><li><strong>单线程模式</strong>(Single-thread):所有互斥锁都被禁用,<strong>SQLite连接不能在多个线程中使用</strong>(多线程使用不安全)。</li><li><strong>多线程模式</strong>(Multi-thread):在多线程中使用单个数据库连接是不安全的,否则就是安全的 (不能在多个线程中共享数据库连接)</li><li><strong>串行模式</strong>(Serialized),是线程安全的(即使在多个线程中不加互斥的使用同一个数据库连接)。</li></ul><p><strong>说明</strong>:线程模式可以在<strong>编译时</strong>(通过源码编译SQLite库时)、<strong>启动时</strong>(使用SQLite的应用程序初始化时)或者<strong>运行时</strong>(创建数据库连接时)来指定。一般而言,运行时指定的模式将覆盖启动时的指定模式,启动时指定的模式将覆盖编译时指定的模式。但是,<strong>单线程模式一旦被指定,将无法被覆盖</strong>。默认的线程模式是串行模式。 </p><h5 id="2、编译时选择线程模式"><a href="#2、编译时选择线程模式" class="headerlink" title="2、编译时选择线程模式"></a>2、编译时选择线程模式</h5><ul><li>通过定义<strong>SQLite_THREADSAFE</strong>宏来指定线程模式。如果没有指定,默认为串行模式。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">//0:单线程模式;</span><br><span class="line">//1:串行模式;</span><br><span class="line">//2:多线程模式</span><br></pre></td></tr></table></figure><ul><li><strong>SQLite3_threadsafe</strong>()返回值可以确定编译时指定的线程模式。如果指定了单线程模式,函数返回false。如果指定了<strong>串行或者多线程模式</strong>,函数返回true。</li><li>由于<strong>SQLite3_threadsafe</strong>()函数要早于多线程模式以及启动时和运行时的模式选择,所以它既不能区别多线程模式和串行模式,也不能区别启动时和运行时的模式。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//FMDB 中代码</span><br><span class="line">+ (BOOL)isSQLiteThreadSafe {</span><br><span class="line"> // make sure to read the SQLite headers on this guy!</span><br><span class="line"> return SQLite3_threadsafe() != 0;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li>如果编译时指定了单线程模式,那么临界互斥逻辑在构造时就被省略,因此也就<strong>无法在启动时或运行时指定串行模式或多线程模式。</strong></li></ul><h5 id="3、启动时选择线程模式"><a href="#3、启动时选择线程模式" class="headerlink" title="3、启动时选择线程模式"></a>3、启动时选择线程模式</h5><ul><li><p>假如在编译时没有指定单线程模式,就可以在应用程序初始化时使用SQLite3_config()函数修改线程模式。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SQLite_CONFIG_SINGLETHREAD //单线程模式</span><br><span class="line">SQLite_CONFIG_MULTITHREAD //多线程模式</span><br><span class="line">SQLite_CONFIG_SERIALIZED //串行模式</span><br></pre></td></tr></table></figure></li></ul><h5 id="4、运行时选择线程模式"><a href="#4、运行时选择线程模式" class="headerlink" title="4、运行时选择线程模式"></a>4、运行时选择线程模式</h5><ul><li>如果没有在编译时 和 启动时指定为单线程模式,那么每个数据库连接在创建时,可单独的被指定为多线程模式或者串行模式,<strong>但是不能指定为单线程模式</strong>。</li><li>如果在编译时或启动时指定为单线程模式,就无法在创建连接时指定多线程或者串行模式。</li><li>创建连接时可以用SQLite3_open_v2()函数的第三个参数来指定线程模式。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SQLite_OPEN_NOMUTEX //创建多线程模式的连接(没有指定单线程模式的情况下)</span><br><span class="line">SQLite_OPEN_FULLMUTEX //创建串行模式的连接</span><br></pre></td></tr></table></figure><h5 id="5、模式的选择和处理"><a href="#5、模式的选择和处理" class="headerlink" title="5、模式的选择和处理"></a>5、模式的选择和处理</h5><p>要保证数据库使用安全,一般可以采用如下几种模式</p><ul><li><code>SQLite</code> 采用<strong>单线程模型</strong>,用专门的线程(同时只能有一个任务执行访问) 进行访问</li><li><code>SQLite</code> 采用<strong>多线程模型</strong>,<strong>每个线程都使用各自的数据库连接</strong> (即 <code>SQLite3 *</code>)</li><li><code>SQLite</code> 采用<strong>串行模型</strong>,所有线程都公用同一个数据库连接。</li></ul><h5 id="6、SQLite使用建议"><a href="#6、SQLite使用建议" class="headerlink" title="6、SQLite使用建议"></a>6、SQLite使用建议</h5><p> 写操作的并发性并不好,当多线程进行访问时实际上仍旧需要互相等待,而读操作所需要的 <code>SHARED</code> 锁是可以共享的,所以为了保证最高的并发性,推荐</p><ul><li>使用多线程模式</li><li>使用 <code>WAL</code> 模式</li><li>单线程写,多线程读 (各线程都持有自己对应的数据库连接)</li><li>避免长时间事务</li><li>缓存 <code>SQLite3_prepare</code> 编译结果</li><li>多语句通过 <code>BEGIN</code> 和 <code>COMMIT</code> 做显示事务,减少多次的自动事务消耗</li></ul><h4 id="三、SQLite基础操作"><a href="#三、SQLite基础操作" class="headerlink" title="三、SQLite基础操作"></a>三、SQLite基础操作</h4><h5 id="1、基础概念"><a href="#1、基础概念" class="headerlink" title="1、基础概念"></a>1、基础概念</h5><ul><li><strong>表</strong>:是数据库中一个非常重要的对象,是其他对象的基础。根据信息的分类情况,一个数据库中可能包含若干个数据表</li><li><strong>字段</strong>:表的“列”称为“字段”,每个字段包含某一专题的信息</li><li><strong>记录</strong>:是指对应于数据表中一行信息的一组完整的相关信息</li><li>iOS使用SQLite,需要引入libSQLite3.0.tbd框架,并引入<sqlite3.h>头文件</sqlite3.h></li></ul><h5 id="2、关键API-打开数据库"><a href="#2、关键API-打开数据库" class="headerlink" title="2、关键API-打开数据库"></a>2、关键API-打开数据库</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">//打开数据库连接 定义</span><br><span class="line">SQLite_API int SQLite3_open(</span><br><span class="line"> const char *filename, /* Database filename (UTF-8) */</span><br><span class="line"> SQLite3 **ppDb /* OUT: SQLite db handle */</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">//使用数据库连接</span><br><span class="line">//db是SQLite3对象,SQLite3 *db = nil;</span><br><span class="line"> SQLite3_open([sqlPath UTF8String], &db); </span><br><span class="line"> </span><br><span class="line">//打开</span><br><span class="line">int SQLite3_open_v2(</span><br><span class="line">const char *filename, /* Database filename (UTF-8) */</span><br><span class="line">SQLite3 **ppDb, /* OUT: SQLite db handle */</span><br><span class="line">int flags, /* Flags */</span><br><span class="line">const char *zVfs /* Name of VFS module to use */</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul><li><p>参数1:数据库的路径(因为需要的是C语言的字符串,而不是NSString所以必须进行转换)</p></li><li><p>参数2:SQLite的数据库的操作句柄(指向指针的指针)</p></li></ul><h5 id="3、关键API-执行sql语句"><a href="#3、关键API-执行sql语句" class="headerlink" title="3、关键API - 执行sql语句"></a>3、关键API - 执行sql语句</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">//执行sql语句 定义</span><br><span class="line">SQLite_API int SQLite3_exec(</span><br><span class="line"> SQLite3*, /* An open database */</span><br><span class="line"> const char *sql, /* SQL to be evaluated */</span><br><span class="line"> int (*callback)(void*,int,char**,char**), /* Callback function */</span><br><span class="line"> void *, /* 1st argument to callback */</span><br><span class="line"> char **errmsg /* Error msg written here */</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">//使用</span><br><span class="line"> int result = SQLite3_exec(db, sql.UTF8String, nil, nil, nil); </span><br><span class="line"> if (result == SQLite_OK) {</span><br><span class="line"> //exec ok</span><br><span class="line"> } else {</span><br><span class="line"> //exec failed</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><ul><li>参数1:SQLite3对象</li><li>参数2:sql语句</li><li>参数3:sql执行后回调函数</li><li>参数4:回调函数的参数</li><li>参数5:错误信息</li></ul><h5 id="4、关键API-执行查询语句"><a href="#4、关键API-执行查询语句" class="headerlink" title="4、关键API - 执行查询语句"></a>4、关键API - 执行查询语句</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">//将sql文本转换成一个准备语句(prepared statement)对象,同时返回这个对象的指针,它实际上并不执行(evaluate)这个SQL语句,它仅仅为执行准备这个sql语句。</span><br><span class="line">SQLite_API int SQLite3_prepare_v2(</span><br><span class="line"> SQLite3 *db, /* Database handle */</span><br><span class="line"> const char *zSql, /* SQL statement, UTF-8 encoded */</span><br><span class="line"> int nByte, /* Maximum length of zSql in bytes. */</span><br><span class="line"> SQLite3_stmt **ppStmt, /* OUT: Statement handle */</span><br><span class="line"> const char **pzTail /* OUT: Pointer to unused portion of zSql */</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">//使用</span><br><span class="line">result = SQLite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);</span><br><span class="line">if (result == SQLite_OK) {</span><br><span class="line"> //exec ok</span><br><span class="line">} else {</span><br><span class="line"> //exec failed</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="5、关键API-关闭数据库"><a href="#5、关键API-关闭数据库" class="headerlink" title="5、关键API - 关闭数据库"></a>5、关键API - 关闭数据库</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//关闭数据库 定义</span><br><span class="line">SQLite_API int SQLite3_close(SQLite3*);</span><br><span class="line"></span><br><span class="line">//使用</span><br><span class="line">SQLite3_close(db);</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:具体API参考<a href="https://www.SQLite.org/capi3ref.html" target="_blank" rel="noopener">C-language Interface Specification for SQLite</a>,FMDB中对SQLite3的操作做了很好的封装,具体可参考FMDB的FMDatabase文件</p><ul><li><p>参考 <a href="http://wereadteam.github.io/2016/08/19/SQLite/" target="_blank" rel="noopener">SQLite线程模式探讨</a></p></li></ul><h4 id="四、FMDB"><a href="#四、FMDB" class="headerlink" title="四、FMDB"></a>四、FMDB</h4><p>FMDB是iOS平台的SQLite数据库框架,iOS项目中使用十分广泛。</p><h5 id="1、源码组成"><a href="#1、源码组成" class="headerlink" title="1、源码组成"></a>1、源码组成</h5><ul><li><strong>FMDatabase</strong> : 对SQLite3的封装,可以看做是SQLite3数据库操作实例,通过它可以对SQLite3进行增删改查等等操作。</li><li><strong>FMResultSet</strong> : FMDatabase执行查询之后的结果集。</li><li><strong>FMDatabaseAdditions</strong> : FMDatabase的Extension,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等等功能。</li><li><strong>FMDatabaseQueue</strong> : 使用GCD串行队列保证线程安全,所有的线程共用一个SQLite Handle(<strong>单句柄</strong>),在多线程并发时,能够使各个线程的数据库操作按顺序同步进行,但正是因为各线程同步进行,导致后来的线程会被阻塞较长时间,无论是读操作还是写操作,都必须等待前面的线程执行完毕,使得性能无法得到更好的保障</li><li><strong>FMDatabasePool</strong> : 使用任务池的形式,对多线程的操作提供支持。(不过官方对这种方式并不推荐使用,优先选择FMDatabaseQueue的方式)</li></ul><p><strong>说明</strong>:在FMDB中,SQLite运行在多线程模式,<strong>一个数据库连接在同一个时间只能在一个线程操作</strong> ,应该是在编译时候确定的,当然也可以在打开数据库连接时候,指定线程模式是 多线程或串行。</p><h5 id="2、数据库创建和打开"><a href="#2、数据库创建和打开" class="headerlink" title="2、数据库创建和打开"></a>2、数据库创建和打开</h5><ul><li><p><code>FMDatabase</code>通过一个 SQLite 数据库文件路径创建的,此路径可以是:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">一个文件的系统路径。磁盘中可以不存在此文件,因为如果不存在会自动为你创建。</span><br><span class="line">一个空的字符串 `@""`。会在临时位置创建一个空的数据库,当 `FMDatabase` 连接关闭时,该数据库会被删除。</span><br><span class="line">NULL`。会在内存中创建一个数据库,当 `FMDatabase` 连接关闭时,该数据库会被销毁。</span><br></pre></td></tr></table></figure></li><li><p><code>FMDatabase</code>必须执行open,在这里才能正在创建并打开SQLite3对象。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">FMDatabase *db = [FMDatabase databaseWithPath:dbpath];</span><br><span class="line">[db open];</span><br><span class="line">//...</span><br><span class="line"></span><br><span class="line">//关闭</span><br><span class="line">[db close];</span><br></pre></td></tr></table></figure></li></ul><h5 id="3、数据库查询"><a href="#3、数据库查询" class="headerlink" title="3、数据库查询"></a>3、数据库查询</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">//数据库查询</span><br><span class="line">FMResultSet *rs = [db executeQuery:@"select * from people"];</span><br><span class="line">//利用next函数</span><br><span class="line">while ([rs next]) {</span><br><span class="line"> NSLog(@"%@ %@",[rs stringForColumn:@"name"],[rs stringForColumn:@"age"]);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ul><li><code>FMResultSet</code>通过调用 <code>-executeQuery...</code> 方法之一执行 <code>SELECT</code> 语句返回数据库查询结果<code>FMResultSet</code> 对象,然后就可以遍历查询结果了。</li></ul><h5 id="4、数据库更新"><a href="#4、数据库更新" class="headerlink" title="4、数据库更新"></a>4、数据库更新</h5><ul><li><p>SQL 语句中除过 <code>SELECT</code> 语句都可以称之为更新操作。包括 <code>CREATE</code>,<code>UPDATE</code>,<code>INSERT</code>,<code>ALTER</code>,<code>COMMIT</code>,<code>BEGIN</code>,<code>DETACH</code>,<code>DROP</code>,<code>END</code>,<code>EXPLAIN</code>,<code>VACUUM</code>,<code>REPLACE</code> 等。</p></li><li><p>执行更新语句后会返回一个 <code>BOOL</code> 值,返回 <code>YES</code> 表示执行更新语句成功,返回 <code>NO</code> 表示出现错误,可以通过调用 <code>-lastErrorMessage</code> 和 <code>-lastErrorCode</code> 方法获取更多错误信息。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//创建表</span><br><span class="line">[db executeUpdate:@"CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER DEFAULT 1)"];</span><br><span class="line"></span><br><span class="line">//插入操作</span><br><span class="line">[db executeUpdate:@"INSERT INTO people(name,age) VALUES (?,?)", @"LiLei",[NSNumber numberWithInteger:28]]</span><br></pre></td></tr></table></figure></li></ul><h5 id="5、多线程数据库访问"><a href="#5、多线程数据库访问" class="headerlink" title="5、多线程数据库访问"></a>5、多线程数据库访问</h5><ul><li>FMDatabase 本身不是线程安全的,不要实例化一个 FMDatabase 单例来跨线程使用,但是可以通过FMDatabaseQueue保证跨线程操作是同步的,是线程安全的。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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></pre></td><td class="code"><pre><span class="line">FMDatabaseQueue *databaseQueue = [FMDatabaseQueue databaseQueueWithPath:dbpath];</span><br><span class="line">[databaseQueue inDatabase:^(FMDatabase *db) {</span><br><span class="line"> //</span><br><span class="line"> [db executeUpdate:@"CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, age INTEGER DEFAULT 1)"];</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);</span><br><span class="line"> </span><br><span class="line"> dispatch_async(queue, ^{</span><br><span class="line"> [databaseQueue inDatabase:^(FMDatabase *db) {</span><br><span class="line"> BOOL isSuccess = [db executeUpdate:@"INSERT INTO people(name,age) VALUES (?,?)", @"LiLei",[NSNumber numberWithInteger:28]];</span><br><span class="line"> if (isSuccess) {</span><br><span class="line"> NSLog(@"插入成功1");</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"> dispatch_async(queue, ^{</span><br><span class="line"> [databaseQueue inDatabase:^(FMDatabase *db) {</span><br><span class="line"> BOOL isSuccess = [db executeUpdate:@"INSERT INTO people(name,age) VALUES (?,?)", @"LiLei",[NSNumber numberWithInteger:28]];</span><br><span class="line"> if (isSuccess) {</span><br><span class="line"> NSLog(@"插入成功2");</span><br><span class="line"> }</span><br><span class="line"> }];</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><ul><li>FMDatabaseQueue 将块代码 block 运行在一个串行队列上,即使在多线程同时调用 FMDatabaseQueue 的方法,它们仍然还是顺序执行。这种查询和更新方式不会影响其它,是线程安全的。</li></ul><h4 id="五、其他"><a href="#五、其他" class="headerlink" title="五、其他"></a>五、其他</h4><h5 id="1、GYDataCenter"><a href="#1、GYDataCenter" class="headerlink" title="1、GYDataCenter"></a>1、GYDataCenter</h5><ul><li><p>基于FMDB实现的ORM框架</p></li><li><p>具体参考<a href="https://wereadteam.github.io/2016/07/06/GYDataCenter/" target="_blank" rel="noopener">GYDataCenter:高性能数据库框架</a></p></li></ul><h5 id="2、WCDB"><a href="#2、WCDB" class="headerlink" title="2、WCDB"></a>2、WCDB</h5><ul><li>微信开源的移动端数据库组件,基于SQLite,支持ORM(Object Relational Mapping)(将一个ObjC的类,映射到数据库的表和索引,将类的property,映射到数据库表的字段)</li><li>具体参考 <a href="https://github.com/buaa0300/article/blob/master/%E5%BE%AE%E4%BF%A1%E7%A7%BB%E5%8A%A8%E7%AB%AF%E6%95%B0%E6%8D%AE%E5%BA%93%E7%BB%84%E4%BB%B6WCDB%E7%B3%BB%E5%88%97%EF%BC%88%E4%B8%80%EF%BC%89-iOS%E5%9F%BA%E7%A1%80%E7%AF%87.md" target="_blank" rel="noopener">微信移动端数据库组件WCDB系列(一)-iOS基础篇</a></li></ul><h5 id="3、Realm"><a href="#3、Realm" class="headerlink" title="3、Realm"></a>3、Realm</h5><ul><li>具体参考 <a href="https://www.jianshu.com/p/50e0efb66bdf" target="_blank" rel="noopener">Realm数据库 从入门到“放弃”</a></li></ul>]]></content>
<summary type="html">
<p><em>静以修身,俭以养德</em></p>
<h4 id="一、移动端数据库方案"><a href="#一、移动端数据库方案" class="headerlink" title="一、移动端数据库方案"></a>一、移动端数据库方案</h4><h5 id="1、关系型数据
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="SQLite" scheme="http://buaa0300/nanhuacoder.com/tags/SQLite/"/>
<category term="FMDB" scheme="http://buaa0300/nanhuacoder.com/tags/FMDB/"/>
</entry>
<entry>
<title>ReactiveCocoa-信号基础</title>
<link href="http://buaa0300/nanhuacoder.com/2019/03/24/iOS-RAC01/"/>
<id>http://buaa0300/nanhuacoder.com/2019/03/24/iOS-RAC01/</id>
<published>2019-03-24T02:48:59.000Z</published>
<updated>2019-04-18T13:57:29.000Z</updated>
<content type="html"><![CDATA[<h4 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h4><p>ReactiveCocoa火了一段时间(大概15,16年),在谈及MVVM的双向绑定时候,必然提及ReactiveCocoa,但是这几年,ReactiveCocoa似乎不怎么火了,之前没有用过,最近在一个项目中遇到,简单了解下。</p><h5 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h5><ul><li><p><a href="https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2FReactiveCocoa" target="_blank" rel="noopener">ReactiveCocoa</a>(简称为RAC),是一个开源的,可应用于iOS开发的<strong>函数式</strong> <strong>响应式</strong>编程框架,它提供了一系列用来组合和转换值流的 API</p></li><li><p>函数响应式编程(Functional reactive programming,简称FRP</p></li><li><p>RAC 5.0 对自身项目结构的也进行了大幅度的调整。这个调整就是将 RAC 拆分为四个库:ReactiveCocoa、ReactiveSwift、ReactiveObjC、ReactiveObjCBridge,Swift版本是ReactiveSwift,OC版本是ReactiveObjc。</p></li></ul><h5 id="2、编程思想"><a href="#2、编程思想" class="headerlink" title="2、编程思想"></a>2、编程思想</h5><ul><li><p><strong>面向过程</strong>:处理事情以过程为核心,一步一步的实现。</p></li><li><p><strong>面向对象</strong>: 万物皆对象</p></li><li><p><strong>链式编程</strong>:是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好,代表作 : masonry</p></li><li><p><strong>响应式编程</strong>:不需要考虑调用顺序,只需要知道考虑结果,类似于蝴蝶效应,产生一个事件,会影响很多东西,这些事件像流一样的传播出去,然后影响结果,借用面向对象的一句话,万物皆流。代表KVO</p></li><li><p><strong>函数式编程思想</strong>:是把操作尽量写成一系列嵌套的函数或者方法调用</p></li></ul><p><strong>说明</strong>:<strong>ReactiveCocoa</strong>结合<strong>函数式编程</strong>(Functional Programming)和 <strong>响应式编程</strong>(Reactive Programming)编程思想,简记为 <strong>函数响应式</strong>编程(Functional reactive programming,简称FRP) ; 所以使用RAC时候,不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。</p><h5 id="3、竞品"><a href="#3、竞品" class="headerlink" title="3、竞品"></a>3、竞品</h5><ul><li><p><strong>RxSwift</strong> 是 Rx的 Swift 版本,<strong>Rx</strong>是<strong>ReactiveX</strong>(Reactive Extensions)的简写,它致力于提供一致的编程接口,帮助开发者更方便的处理异步数据流,Rx库支持.NET、JavaScript、C++、Swift, 但是没有支持Objective。</p></li><li><p><a href="https://github.com/meituan/EasyReact" target="_blank" rel="noopener">EasyReact</a> 美团18年开源的响应式编程框架,从官方纰漏信息说,比RAC性能更好 (<a href="https://www.raywenderlich.com/1190-reactivecocoa-vs-rxswift" target="_blank" rel="noopener">ReactiveCocoa vs RxSwift</a>)</p></li></ul><h4 id="二、信号RACSignal"><a href="#二、信号RACSignal" class="headerlink" title="二、信号RACSignal"></a>二、信号RACSignal</h4><p><strong>RACSignal是RAC的核心,RACSignal是RACStream子类。本质是是数据流,可以用来传递和绑定</strong>。</p><h5 id="1、基础操作"><a href="#1、基础操作" class="headerlink" title="1、基础操作"></a>1、基础操作</h5><ul><li><p><strong>创建信号</strong>(createSignal):创建了一个 RACDynamicSignal 类型的信号,并将传入的代码块保存起来,留待以后调用。</p></li><li><p><strong>订阅信号</strong>(subscribeNext):创建了一个 RACPassthroughSubscriber 类型的订阅者,并将传入的代码块保存起来,留待以后调用,同时调用了第一步创建信号中保存的代码块,并传入创建的订阅者。</p></li><li><p><strong>发送信号</strong>(sendNext):执行订阅信号时对应的block</p></li><li><p><strong>取消订阅(disposable):</strong> 把订阅信号获得的disposable进行dispose,即可在调度器调度该部分代码之前禁止调用。</p></li></ul><h5 id="2、Code"><a href="#2、Code" class="headerlink" title="2、Code"></a>2、Code</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">// 1.创建信号</span><br><span class="line"> RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber ) {</span><br><span class="line"> </span><br><span class="line"> //3.发送信号</span><br><span class="line"> [subscriber sendNext:@"123"];</span><br><span class="line"> //3.发送完成并取消订阅</span><br><span class="line"> [subscriber sendCompleted];</span><br><span class="line"> </span><br><span class="line"> //4、用于取消订阅时清理资源用,比如释放资源</span><br><span class="line"> return [RACDisposable disposableWithBlock:^{</span><br><span class="line"> NSLog(@"信号被取消订阅了");</span><br><span class="line"> }];</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> // 2.订阅信号</span><br><span class="line"> [signal subscribeNext:^(id _Nullable x) {</span><br><span class="line"> </span><br><span class="line"> NSLog(@"xxxx-%@",x);</span><br><span class="line"> </span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> // 2.订阅信号</span><br><span class="line"> [signal subscribeNext:^(id x) {</span><br><span class="line"> NSLog(@"yyyy-%@",x);</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> // xxxx-123</span><br><span class="line">// 信号被取消订阅了</span><br><span class="line">// yyyy-123</span><br><span class="line">// 信号被取消订阅了</span><br></pre></td></tr></table></figure><h4 id="三、信号操作源码解读"><a href="#三、信号操作源码解读" class="headerlink" title="三、信号操作源码解读"></a>三、信号操作源码解读</h4><p><img src="https://upload-images.jianshu.io/upload_images/201701-e7bf63b105cdf58a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640" alt="img"> </p><h5 id="1、源码-创建信号"><a href="#1、源码-创建信号" class="headerlink" title="1、源码-创建信号"></a>1、源码-创建信号</h5><ul><li><strong>createSignal</strong>(<strong>创建信号</strong>)<strong>,实际上是创建 RACDynamicSignal 类型的信号,并把定义的</strong>block存储到<strong>didSubscribe</strong>。</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-9c730e8e40ddaf05.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="create.png"> </p><h5 id="2、源码-订阅信号"><a href="#2、源码-订阅信号" class="headerlink" title="2、源码-订阅信号"></a>2、源码-订阅信号</h5><p><strong>subscribeNext (订阅信号)</strong>分为两步:第一步<strong>创建一个订阅者subscriber,存入nextBlock, </strong>第二步<strong>该subscriber订阅信号</strong>。</p><p><img src="https://upload-images.jianshu.io/upload_images/201701-c55b5d564d2439d6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="subscribe.png"> </p><ul><li><strong>第一步代码如下</strong>:</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-881dc25b7d62026e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="subscribe-next.png"> </p><ul><li><strong>第二步:有三个关键点</strong>,一个是<strong>创建一个RACCompoundDisposable</strong>对象,这里存储的是取消订阅后需要做的清除工作;另一个是 <strong>创建RACPassthroughSubscriber对象</strong>,将subscriber、信号和disposable处理存储下来,完成订阅者转换( 可以理解 RACPassthroughSubscriber 是订阅者的装饰器)。最后一个是使用在<strong>RACScheduler.subscriptionScheduler schedule:</strong> 来执行<strong>didSubscribe</strong>(这是创建信号穿过来的block), 最后返回RACCompoundDisposable对象</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-69b14dca5a123e81.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="subscribe-next2.png"></p><h5 id="3、源码-发送信号"><a href="#3、源码-发送信号" class="headerlink" title="3、源码-发送信号"></a>3、源码-<strong>发送信号</strong></h5><ul><li><p>在发送信号里,执行<strong>didSubscribe</strong>时候<strong>,</strong>传入的是<strong>RACPassthroughSubscriber</strong>对象,在执行sendNext: 时候,先执行<strong>RACPassthroughSubscriber对象的sendNext: 方法</strong>,</p></li><li><p>在这里<strong>:先检查disposable状态</strong>,如果已经disposed直接返回;没有disposed才调用内部的innerSubscriber的sendNext:方法(RACSubscriber对象方法)</p></li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-66b346feb1e6c953.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="send.png"> </p><p><img src="https://upload-images.jianshu.io/upload_images/201701-1377630b1862cc71.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="send2.png"> </p><p><strong>说明</strong>:</p><ul><li><p>发送信号就是执行相应block,此处执行的就是第二步中保存的<strong>相应的block</strong>。</p></li><li><p>对于 sendError 和 sendCompleted 都是<strong>先取消订阅,再执行相应的代码块</strong>,而 sendNext 并未<strong>取消</strong>订阅,所以,一般sendNext: 和 sendCompleted组合出现。</p></li></ul><h5 id="4、源码-取消订阅"><a href="#4、源码-取消订阅" class="headerlink" title="4、源码-取消订阅"></a>4、源码-取消订阅</h5><ul><li><p>在 执行sendCompleted后,<strong>RACPassthroughSubscriber</strong>对象执行dispose方法,完成整个过程</p></li><li><p>如果不调用sendCompleted,当订阅者被销毁的时候,<strong>RACPassthroughSubscriber</strong>对象也会执行dispose方法。</p></li></ul><h4 id="四、RACSubscriber-amp-RACDisposable"><a href="#四、RACSubscriber-amp-RACDisposable" class="headerlink" title="四、RACSubscriber & RACDisposable"></a>四、RACSubscriber & RACDisposable</h4><h5 id="1、RACSubscriber"><a href="#1、RACSubscriber" class="headerlink" title="1、RACSubscriber"></a>1、RACSubscriber</h5><ul><li><p><strong>订阅者协议</strong>,主要遵守这个协议,并且实现方法才能成为订阅者。设计者特意设计同名的RACSubscriber类,实现RACSubscriber协议</p></li><li><p><strong>RACSubscriber协议定义如下</strong></p></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">@protocol RACSubscriber <NSObject></span><br><span class="line">@required</span><br><span class="line">/// Sends the next value to subscribers.</span><br><span class="line">- (void)sendNext:(nullable id)value;</span><br><span class="line"></span><br><span class="line">/// Sends the error to subscribers.</span><br><span class="line">- (void)sendError:(nullable NSError *)error;</span><br><span class="line"></span><br><span class="line">/// Sends completed to subscribers.</span><br><span class="line">- (void)sendCompleted;</span><br><span class="line"></span><br><span class="line">/// Sends the subscriber a disposable that represents one of its subscriptions.</span><br><span class="line">- (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable;</span><br><span class="line">@end</span><br></pre></td></tr></table></figure><ul><li><strong>RACSubscriber类定义如下</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">// A simple block-based subscriber.</span><br><span class="line">@interface RACSubscriber : NSObject <RACSubscriber></span><br><span class="line"></span><br><span class="line">// Creates a new subscriber with the given blocks.</span><br><span class="line">+ (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed;</span><br><span class="line"></span><br><span class="line">@end</span><br></pre></td></tr></table></figure><ul><li><strong>RACPassthroughSubscriber</strong>也是实现了RACSubscriber协议,而不是继承RACSubscriber类</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-73824afb1d8ac9db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="RACPassthroughSubscriber.png"> </p><h5 id="2、RACDisposable及其子类"><a href="#2、RACDisposable及其子类" class="headerlink" title="2、RACDisposable及其子类"></a>2、RACDisposable及其子类</h5><ul><li><p>用于<strong>取消订阅</strong>或者<strong>清理资源</strong>,当信号发送完成 或者 发送错误的时候,就会自动触发它。</p></li><li><p><strong>使用场景</strong>:不想监听某个信号时,可以通过它主动取消订阅信号。</p></li><li><p>子类:RACCompoundDisposable、RACScopedDisposable、RACKVOTrampoline、RACSerialDisposable等。</p></li><li><p><strong>RACCompoundDisposable</strong>可以存放<strong>多个RACDisposable</strong> 。当RACCompoundDisposable 执行dispose方法时,它所存放的disposable都会被释放。</p></li></ul><h4 id="五、信号组合"><a href="#五、信号组合" class="headerlink" title="五、信号组合"></a>五、信号组合</h4><h5 id="1、concat"><a href="#1、concat" class="headerlink" title="1、concat"></a>1、concat</h5><p><strong>组合信号,让信号按照顺序去执行。</strong></p><p>模拟发送两个请求,第一个请求返回数据后执行第二个请求</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">//创建一个信号A</span><br><span class="line"> RACSignal *signalA = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {</span><br><span class="line"> NSLog(@"发送请求1...");</span><br><span class="line"> dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{</span><br><span class="line"> [subscriber sendNext:@"请求1返回的数据"];</span><br><span class="line"> [subscriber sendCompleted];</span><br><span class="line"> });</span><br><span class="line"> return [RACDisposable disposableWithBlock:^{</span><br><span class="line"> NSLog(@"取消订阅1...");</span><br><span class="line"> }];</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> //创建一个信号B</span><br><span class="line"> RACSignal *siganlB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {</span><br><span class="line"> </span><br><span class="line"> NSLog(@"发送请求2...");</span><br><span class="line"> dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{</span><br><span class="line"> [subscriber sendNext:@"请求2返回的数据"];</span><br><span class="line"> [subscriber sendCompleted];</span><br><span class="line"> });</span><br><span class="line"> </span><br><span class="line"> return [RACDisposable disposableWithBlock:^{</span><br><span class="line"> NSLog(@"取消订阅2...");</span><br><span class="line"> }];</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> //串联信号A和B</span><br><span class="line"> RACSignal *concatSignal = [signalA concat:siganlB];</span><br><span class="line"> [concatSignal subscribeNext:^(id _Nullable x) {</span><br><span class="line"> NSLog(@"value = %@",x);</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> output:</span><br><span class="line">//发送请求1...</span><br><span class="line">//value = 请求1返回的数据</span><br><span class="line">//取消订阅1...</span><br><span class="line">//发送请求2...</span><br><span class="line">//value = 请求2返回的数据</span><br><span class="line">//取消订阅2...</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:使用concat 连接信号时,第一个信号发送后,一定要执行<strong>sendCompleted</strong>方法,否则不会发送第二个信号。</p><h5 id="2、concat源码实现"><a href="#2、concat源码实现" class="headerlink" title="2、concat源码实现"></a>2、concat源码实现</h5><p><img src="https://upload-images.jianshu.io/upload_images/201701-7a5f43720da0d05f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="concat.png"></p><ul><li><p>当调用concat时,会创建一个<strong>拼接信号(RACDynamicSignal对象)</strong></p></li><li><p>当拼接信号被订阅,就会执行拼接信号的didSubscribe</p></li><li><p>在<strong>didSubscribe</strong>中会<strong>先订阅</strong>第一个信号(signalA),此时会执行第一个信号(signalA)的didSubscribe。</p></li><li><p>第一个信号(signalA)didSubscribe中,发送值,就调用第一个信号(signalA)订阅者的nextBlock,通过拼接信号的订阅者把值发送出来。</p></li><li><p>第一个信号(signalA)didSubscribe中发送完成,<strong>就会调用第一个源信号(signalA)订阅者的completedBlock</strong>,订阅第二个源信号(signalB)这时候才激活(signalB)。</p></li><li><p>订阅第二个信号(signalB),执行第二个源信号(signalB)的didSubscribe</p></li><li><p>第二个信号(signalB)didSubscribe中发送值,就会通过拼接信号的订阅者把值发送出来.</p></li></ul><h5 id="3、then"><a href="#3、then" class="headerlink" title="3、then"></a>3、then</h5><p>用于连接连个信号,<strong>当第一个信号完成,才会连接then返回的信号</strong>,then底层也是使用了concat实现。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">//创建信号A</span><br><span class="line"> RACSignal*signalA = [RACSignal createSignal:^RACDisposable*(id subscriber) {</span><br><span class="line"> NSLog(@"send data A");</span><br><span class="line"> //发送信号</span><br><span class="line"> [subscriber sendNext:@"dataA"];</span><br><span class="line"> [subscriber sendCompleted];</span><br><span class="line"> return nil;</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> //创建信号B</span><br><span class="line"> RACSignal *signalB = [RACSignal createSignal:^RACDisposable*(id subscriber) {</span><br><span class="line"> //发送请求</span><br><span class="line"> NSLog(@"send data B");</span><br><span class="line"> //发送信号</span><br><span class="line"> [subscriber sendNext:@"dataB"];</span><br><span class="line"> [subscriber sendCompleted];</span><br><span class="line"> return nil;</span><br><span class="line"> }];</span><br><span class="line"> //创建组合信号</span><br><span class="line"> //then会忽略点第一个信号的所有值</span><br><span class="line"> RACSignal *signalThen = [signalA then:^RACSignal*{</span><br><span class="line"> //返回的信号就是需要组合的信号,这里回将signalA信号忽略掉</span><br><span class="line"> return signalB;</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> //订阅信号</span><br><span class="line"> [signalThen subscribeNext:^(id x) {</span><br><span class="line"> NSLog(@"%@",x);</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> //send data A</span><br><span class="line"> //send data B</span><br><span class="line"> //dataB</span><br></pre></td></tr></table></figure><h5 id="4、then源码实现"><a href="#4、then源码实现" class="headerlink" title="4、then源码实现"></a>4、then源码实现</h5><p><img src="https://upload-images.jianshu.io/upload_images/201701-64c023e296a0dd9f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="then.png"> </p><ul><li><p>先过滤掉之前的信号发出的值。</p></li><li><p>使用concat连接then返回的信号</p></li></ul><h5 id="5、其他信号组合"><a href="#5、其他信号组合" class="headerlink" title="5、其他信号组合"></a>5、其他信号组合</h5><ul><li><p><strong>merge</strong>:把两个信号合并为一个信号,任何一个信号有新值的时候就会调用。</p></li><li><p><strong>zipWith</strong>:把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发事件。当一个界面有多个网络请求时,要等所有请求完成才更新UI,这时我们就可以用zipWith</p></li><li><strong>combineLatest</strong>:将<strong>多个信号合并起来,拿到各个信号的最新的值</strong>,每个合并的signal至少都有过一次sendNext,才会触发合并的信号,底层也是用 zipWith实现的。 一般拿来跟 reduce 一起使用。</li></ul><h4 id="六、应用"><a href="#六、应用" class="headerlink" title="六、应用"></a>六、应用</h4><p>老说源码比较枯燥,说下信号是怎么让开发变得更有意思。</p><h5 id="1、监听Control事件"><a href="#1、监听Control事件" class="headerlink" title="1、监听Control事件"></a>1、监听Control事件</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">@weakify(self);</span><br><span class="line"> [[self.btn1 rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *btn) {</span><br><span class="line"> @strongify(self);</span><br><span class="line"> NSLog(@"点击事件了");</span><br><span class="line"> }];</span><br></pre></td></tr></table></figure><h5 id="2、监听-textField-的-text-改变"><a href="#2、监听-textField-的-text-改变" class="headerlink" title="2、监听 textField 的 text 改变"></a>2、监听 textField 的 text 改变</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">self.textField.frame = CGRectMake(15, 200, 300, 40);</span><br><span class="line"> [[self.textField rac_textSignal] subscribeNext:^(NSString * _Nullable x) {</span><br><span class="line"> NSLog(@"textField input = [%@]",x);</span><br><span class="line"> }];</span><br></pre></td></tr></table></figure><h5 id="3、把-label-的属性-text-绑定在-UITextField-上"><a href="#3、把-label-的属性-text-绑定在-UITextField-上" class="headerlink" title="3、把 label 的属性 text 绑定在 UITextField 上"></a>3、把 label 的属性 text 绑定在 UITextField 上</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">//self.label1.text 随着 self.textField.text 的改变而改变</span><br><span class="line">RAC(self.label1,text) = self.textField.rac_textSignal;</span><br></pre></td></tr></table></figure><h5 id="4、告别繁琐的KVO"><a href="#4、告别繁琐的KVO" class="headerlink" title="4、告别繁琐的KVO"></a>4、告别繁琐的KVO</h5><ul><li>不需要手动移除了</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">[RACObserve(self.person, name) subscribeNext:^(id _Nullable x) {</span><br><span class="line"> NSLog(@"name = %@",x);</span><br><span class="line"> }];</span><br></pre></td></tr></table></figure><h5 id="5、监听通知"><a href="#5、监听通知" class="headerlink" title="5、监听通知"></a>5、监听通知</h5><ul><li>不需要手动移除了</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">//监听通知</span><br><span class="line"> [[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIKeyboardWillChangeFrameNotification object:nil] subscribeNext:^(NSNotification * _Nullable notification) {</span><br><span class="line"> NSLog(@"-----%@", notification.description);</span><br><span class="line"> }];</span><br></pre></td></tr></table></figure><h5 id="6、数组-amp-字典遍历"><a href="#6、数组-amp-字典遍历" class="headerlink" title="6、数组 & 字典遍历"></a>6、数组 & 字典遍历</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">//数组遍历</span><br><span class="line"> NSLog(@"array filter:");</span><br><span class="line"> NSArray *array = @[@(1), @(2), @(3), @(4), @(5)];</span><br><span class="line"> [array.rac_sequence.signal subscribeNext:^(id _Nullable x) {</span><br><span class="line"> NSLog(@"x = %@",x);</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> NSLog(@"after array filter:");</span><br><span class="line"> //过滤 filter,并获取过滤后的数组</span><br><span class="line"> NSArray *filter = [[array.rac_sequence filter:^BOOL(id _Nullable value) {</span><br><span class="line"> return [value integerValue] > 2;</span><br><span class="line"> }] array];</span><br><span class="line"> </span><br><span class="line"> //</span><br><span class="line"> [filter.rac_sequence.signal subscribeNext:^(id _Nullable x) {</span><br><span class="line"> NSLog(@"y = %@",x);</span><br><span class="line"> }];</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> //匹配、映射 map,变换元素并获取新数组</span><br><span class="line"> NSArray *map = [[array.rac_sequence map:^id _Nullable(id _Nullable value) {</span><br><span class="line"> NSInteger a = [value integerValue] * [value integerValue];</span><br><span class="line"> return [NSNumber numberWithInt:a];</span><br><span class="line"> }] array];</span><br><span class="line"> </span><br><span class="line"> [map.rac_sequence.signal subscribeNext:^(id _Nullable x) {</span><br><span class="line"> NSLog(@"z = %@",x);</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"> NSDictionary *dic = @{@"name": @"lion", @"age": @18};</span><br><span class="line"> [dic.rac_sequence.signal subscribeNext:^(id _Nullable x) {</span><br><span class="line"> </span><br><span class="line"> RACTupleUnpack(NSString *key, NSString *value) = x;</span><br><span class="line"> NSLog(@"\r\nkey: %@\r\nvalue: %@", key, value);</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> }];</span><br></pre></td></tr></table></figure><h5 id="7、按钮是否点击"><a href="#7、按钮是否点击" class="headerlink" title="7、按钮是否点击"></a>7、按钮是否点击</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">// 先联合两个信号,再解析信号结果,最后把结果绑定到信号上</span><br><span class="line"> RAC(self.btn1, enabled) = [RACSignal combineLatest:@[self.textField.rac_textSignal] reduce:^id _Nonnull(NSString *username){</span><br><span class="line"> return @(username.length > 0);</span><br><span class="line"> }];</span><br></pre></td></tr></table></figure><h5 id="8、节流throttle"><a href="#8、节流throttle" class="headerlink" title="8、节流throttle"></a>8、节流throttle</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//节流(0.5 秒内 text 没有改变时,才会进行搜索请求)</span><br><span class="line"> [[[self.textField rac_textSignal] throttle:0.5] subscribeNext:^(NSString * _Nullable x) {</span><br><span class="line"> //发送请求</span><br><span class="line"> NSLog(@"开始搜索请求==%@", x);</span><br><span class="line"> }]</span><br></pre></td></tr></table></figure><h4 id="End"><a href="#End" class="headerlink" title="End"></a>End</h4><h5 id="1、总结"><a href="#1、总结" class="headerlink" title="1、总结"></a>1、总结</h5><ul><li>ReactiveCocoa提供了一个单一的、统一的方法去处理异步的行为,<strong>包括delegate方法,blocks回调,target-action机制,notifications和KVO.</strong></li><li><strong>冷信号和热信号</strong>:冷信号是被动的,只会在被订阅时向订阅者发送通知;热信号是主动的,它会在任意时间发出通知,与订阅者的订阅时间无关;也就是说冷信号所有的订阅者会在订阅时收到完全相同的序列;而订阅热信号之后,只会收到在订阅之后发出的序列。</li></ul><h5 id="2、参考"><a href="#2、参考" class="headerlink" title="2、参考"></a>2、参考</h5><p><a href="https://blog.csdn.net/yutaotst/article/details/58608829" target="_blank" rel="noopener">iOS Reactivecocoa(RAC)知其所以然</a></p><p><a href="https://yyn835314557.github.io/ios/2015/10/07/iOS-ReactiveCocoa%E8%AF%A6%E8%A7%A3.html" target="_blank" rel="noopener">iOS ReactiveCocoa详解</a></p><p><a href="https://www.jianshu.com/p/912c69dfb55a" target="_blank" rel="noopener">ReactiveCocoa信号发送详解</a></p><p><a href="https://juejin.im/entry/58b2f26aac502e0069d9ff42" target="_blank" rel="noopener">RAC 之引起你的兴趣</a></p><p><a href="https://www.jianshu.com/p/912c69dfb55a" target="_blank" rel="noopener">ReactiveCocoa信号发送详解</a></p><p><a href="https://www.jianshu.com/p/a027898612e6" target="_blank" rel="noopener">ReactiveCocoa代码分析之UITextField</a></p>]]></content>
<summary type="html">
<h4 id="一、背景"><a href="#一、背景" class="headerlink" title="一、背景"></a>一、背景</h4><p>ReactiveCocoa火了一段时间(大概15,16年),在谈及MVVM的双向绑定时候,必然提及ReactiveCocoa
</summary>
<category term="iOS第三方库" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E7%AC%AC%E4%B8%89%E6%96%B9%E5%BA%93/"/>
<category term="ReactiveCocoa" scheme="http://buaa0300/nanhuacoder.com/tags/ReactiveCocoa/"/>
<category term="响应式编程" scheme="http://buaa0300/nanhuacoder.com/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B/"/>
</entry>
<entry>
<title>图片解码小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/03/13/iOS-PicDecode/"/>
<id>http://buaa0300/nanhuacoder.com/2019/03/13/iOS-PicDecode/</id>
<published>2019-03-13T04:56:46.000Z</published>
<updated>2019-04-18T13:57:10.000Z</updated>
<content type="html"><![CDATA[<p><strong>KeyPoints</strong></p><ul><li><p>图片如何显示到屏幕上的</p></li><li><p>为什么要图片解码</p></li><li><p>图片解码方案及其对比</p></li></ul><h4 id="一、图像知识"><a href="#一、图像知识" class="headerlink" title="一、图像知识"></a>一、图像知识</h4><h5 id="1、图片"><a href="#1、图片" class="headerlink" title="1、图片"></a>1、图片</h5><ul><li><p>计算机能以<strong>矢量图</strong>(vector)或<strong>位图</strong>(bitmap)格式显示图像,其中<strong>矢量图</strong> 使用线段和曲线描述图像,同时图像还包含了<strong>色彩</strong>和<strong>位置</strong>信息;而<strong>位图</strong> 使用像素点来描述图像,也称为点阵图像,位图图片格式有RGB、CMYK等颜色模式;其中RGB是最常用的颜色模式,它通过红(R)、绿(G)、蓝(B)三个颜色通道的数值表示颜色。手机显示屏使用自带Aphal通道(<strong>RGBA</strong>)的<strong>RGB32</strong>格式。</p></li><li><p>我们平时接触到的JPG或PNG图片格式,他们是<strong>压缩的位图</strong>图形格式,其中 PNG 图片是<strong>无损压缩</strong>,并且支持 alpha 通道,而 JPEG 图片则是<strong>有损压缩</strong>,可以指定 0-100% 的压缩比。JPG或PNG图片显示到屏幕之前,需要将JPG/PNG格式的图片<strong>解码</strong>成<strong>位图图像。</strong></p></li></ul><h5 id="2、16位色、24位色、32位色、真彩色"><a href="#2、16位色、24位色、32位色、真彩色" class="headerlink" title="2、16位色、24位色、32位色、真彩色"></a>2、16位色、24位色、32位色、真彩色</h5><ul><li><p>16位色表示描绘图像时有<strong>2^16种颜色可供选择, 颜色</strong>总数65536。</p></li><li><p>24位色表示描绘图像时有<strong>2^24</strong>种颜色可供选择;颜色总数是16777216。通常也被简称为1600万色或千万色;24位色被称为真彩色,它可以达到人眼分辨的极限</p></li><li><p>32位色在1677万多色基础上,不过它增加了256阶颜色的灰度,为了方便称呼,就规定它为32位色。</p></li><li><p>真彩色指用三个或更多字节描述颜色,24位色、32位色都是真彩色。</p></li></ul><h5 id="3、动图GIF"><a href="#3、动图GIF" class="headerlink" title="3、动图GIF"></a>3、动图GIF</h5><ul><li><p>GIF格式的图像只有256种颜色用以描绘图片,并且只能通过抖动、差值等方式模拟较多丰富的颜色。</p></li><li><p>GIF的alpha通道只有1bit,一个像素要么完全透明,要么完全不透明,而不像现在PNG的RGBA的8bit alpha通道,alpha值也可以和RGB一样都有255个透明值。</p></li><li><p>所有GIF的图片带上透明度以后,边缘会出现明显的锯齿。如果客户端需要展示带透明度的动图,不考虑GIF。</p></li></ul><h4 id="二、iOS背景知识"><a href="#二、iOS背景知识" class="headerlink" title="二、iOS背景知识"></a>二、iOS背景知识</h4><h5 id="1、图像显示到屏幕"><a href="#1、图像显示到屏幕" class="headerlink" title="1、图像显示到屏幕"></a>1、图像显示到屏幕</h5><p><img src="https://upload-images.jianshu.io/upload_images/201701-60465ee3a4d45e3f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640" alt="img"> </p><p>图像显示到屏幕上,是CPU和GPU协作完成渲染的。具体工作如下:</p><ul><li><p><strong>CPU</strong>: 计算视图frame,图片解码,需要绘制纹理图片通过数据总线交给GPU</p></li><li><p><strong>GPU</strong>: 纹理混合,顶点变换与计算,像素点的填充计算,渲染到帧缓冲区。</p></li><li><p><strong>时钟信号</strong>:垂直同步信号V-Sync / 水平同步信号H-Sync。</p></li><li><p><strong>iOS设备双缓冲机制</strong>:显示系统通常会引入两个帧缓冲区,双缓冲机制</p></li></ul><h5 id="2、图片的显示流程"><a href="#2、图片的显示流程" class="headerlink" title="2、图片的显示流程"></a>2、图片的显示流程</h5><ul><li><p>假设本地从本地加载一张图片,将值赋给UIImageView</p></li><li><p>一次Runloop结束后,CATransaction遍历所有layer的contents(寄宿图),发现UIImageView layer的contents的变化(CGImage类型);</p></li><li><p>发现图片不是位图,解码成位图;</p></li><li><p>如果图像数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐;</p></li><li><p>Core Animation提交渲染树CA::render::<strong>commit</strong>,将渲染任务和数据交给Render server线程去处理;</p></li><li><p>Render server调用Open GL、Core Graphics相关程序,最终由GPU完成图像渲染并显示到屏幕。</p></li></ul><p><strong>说明</strong>:图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。</p><h5 id="3、为什么要解压图片"><a href="#3、为什么要解压图片" class="headerlink" title="3、为什么要解压图片"></a>3、为什么要解压图片</h5><ul><li><p>JPEG 和 PNG 图片是位图的压缩格式</p></li><li><p>本质上,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点</p></li><li><p>将磁盘中/网络上获取的 图片渲染到屏幕之前,必须<strong>先要得到图片的原始像素数据</strong>,才能执行后续的绘制操作。</p></li></ul><p><strong>参考</strong>:<a href="https://www.jianshu.com/p/4008ec3cacaa" target="_blank" rel="noopener">https://www.jianshu.com/p/4008ec3cacaa</a></p><h4 id="三、Image-I-O-iOS图片解码方案"><a href="#三、Image-I-O-iOS图片解码方案" class="headerlink" title="三、Image I/O: iOS图片解码方案"></a>三、Image I/O: iOS图片解码方案</h4><h5 id="1、Image-I-O是什么"><a href="#1、Image-I-O是什么" class="headerlink" title="1、Image I/O是什么"></a>1、Image I/O是什么</h5><ul><li><p>Image/IO是Apple提供的一套用于<strong>图片编码解码</strong>的系统库,详细参考 <a href="http://link.zhihu.com/?target=https%3A//developer.apple.com/documentation/imageio" target="_blank" rel="noopener">Apple Image/IO</a></p></li><li><p>Image/IO的解码,支持了常见的图像格式,包括PNG(包括APNG)、JPEG、GIF、BMP、TIFF(具体的,可以通过CGImageSourceCopyTypeIdentifiers来打印出来,不同平台不完全一致)。在iOS 11之后另外支持了HEIC(即使用了HEVC编码的HEIF格式);</p></li><li><p>Image/IO支持的解码和编码格式可通过以下方法查询</p></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">- (void)printImageSupportTypes {</span><br><span class="line">CFArrayRef mySourceTypes = CGImageSourceCopyTypeIdentifiers();</span><br><span class="line"> //支持解码的图片格式</span><br><span class="line"> CFShow(mySourceTypes);</span><br><span class="line"> </span><br><span class="line"> //支持编码的图片格式</span><br><span class="line"> CFArrayRef myDestinationTypes = CGImageDestinationCopyTypeIdentifiers();</span><br><span class="line"> CFShow(myDestinationTypes);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="2、解码流程"><a href="#2、解码流程" class="headerlink" title="2、解码流程"></a>2、解码流程</h5><ul><li><strong>静态图(PNG、JPG)解码流程</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">创建CGImageSource</span><br><span class="line">读取图像格式元数据(可选)</span><br><span class="line">**解码得到CGImage**</span><br><span class="line">CGImage转成UIImage,资源清理</span><br></pre></td></tr></table></figure><ul><li><strong>动态图(GIF、APNG)解码流程</strong></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">静态图的步骤1</span><br><span class="line">遍历所有图像帧,重复静态图的步骤2-4</span><br><span class="line">生成动图UIImage</span><br></pre></td></tr></table></figure><h5 id="3、解码关键API"><a href="#3、解码关键API" class="headerlink" title="3、解码关键API"></a>3、解码关键API</h5><ul><li><p><strong>CGImageSourceCreateWithData</strong>:从一个内存中的二进制数据(CGData)中创建ImageSource。ImageSource代表一个待解码数据,还可以通过CGImageSourceCreateWithURL、CGImageSourceCreateWithDataProvider分别从URL、DataProvide中创建ImageSource,DataProvider提供了很多种输入,包括内存,文件,网络,流等。很多CG的接口会用到这个来避免多个额外的接口。</p></li><li><p><strong>CGImageSourceCreateImageAtIndex</strong>: 获取<strong>CGImage</strong>,对于静态图来说,index始终是0。</p></li></ul><h5 id="4、动态图解码Demo"><a href="#4、动态图解码Demo" class="headerlink" title="4、动态图解码Demo"></a>4、动态图解码Demo</h5><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">CGImageSourceRef source = **CGImageSourceCreateWithData**((__bridge CFDataRef)data, NULL);</span><br><span class="line">if (!source) { // 一般这时候都是输入图像数据的格式不支持</span><br><span class="line"> return nil;</span><br><span class="line">}</span><br><span class="line">NSUInteger frameCount = CGImageSourceGetCount(source); //帧数</span><br><span class="line">NSMutableArray <UIImage *> *images = [NSMutableArray array];</span><br><span class="line">double totalDuration = 0;</span><br><span class="line">for (size_t i = 0; i < frameCount; i++) {</span><br><span class="line"> NSDictionary *frameProperties = (__bridge NSDictionary *) CGImageSourceCopyPropertiesAtIndex(source, i, NULL);</span><br><span class="line"> NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; // GIF属性字典</span><br><span class="line"> double duration = [gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime] doubleValue]; // GIF原始的帧持续时长,秒数</span><br><span class="line"> CGImagePropertyOrientation exifOrientation = [frameProperties[(__bridge NSString *)kCGImagePropertyOrientation] integerValue]; // 方向</span><br><span class="line"> CGImageRef imageRef = **CGImageSourceCreateImageAtIndex**(source, i, NULL); // CGImage</span><br><span class="line"> UIImageOrientation imageOrientation = [self imageOrientationFromExifOrientation:exifOrientation];</span><br><span class="line"> UIImage *image = [[UIImage imageWithCGImage:imageRef scale:[UIScreen mainScreen].scale orientation:imageOrientation];</span><br><span class="line"> totalDuration += duration;</span><br><span class="line"> [images addObject:image];</span><br><span class="line">}</span><br><span class="line">// 最后生成动图</span><br><span class="line">UIImage *animatedImage = [UIImage animatedImageWithImages:images duration:totalDuration];</span><br></pre></td></tr></table></figure><h5 id="5、说明"><a href="#5、说明" class="headerlink" title="5、说明"></a>5、说明</h5><ul><li><p>Image/IO所有的方法都是线程安全的,而且基本上也都是同步的;</p></li><li><p>通过CGImageSourceCreateImageAtIndex生成的CGImage,其实它的Bitmap还没有立即创建,他只是一个包含了一些<strong>元信息的空壳Image</strong>。这个CGImage,在最终需要获取它的Bitmap Buffer的时候(即,通过相应的API,如CGDataProviderCopyData,CGDataProviderRetainBytePtr),<strong>才会触发最后的Bitmap Buffer的创建和内存分配;</strong></p></li><li><p>图片的解码默认发生在主线程,在图片多或图片过大的情况下,第一次加载会导致滚动帧率下滑,后续帧率会好些,解码完成后的Bitmap Buffer会复用;</p></li><li><p>Image/IO是Appple提供的<strong>图片编码解码库,使用简单,性能也有保证,</strong> 但是对于不支持的格式如webp,编解码是无能为力。</p></li></ul><h4 id="四、子线程解码方案"><a href="#四、子线程解码方案" class="headerlink" title="四、子线程解码方案"></a>四、子线程解码方案</h4><h5 id="1、空间换时间"><a href="#1、空间换时间" class="headerlink" title="1、空间换时间"></a>1、空间换时间</h5><ul><li><p>通过CGContext创建一个位图画布 <strong>CGBitmapContextCreate</strong></p></li><li><p>通过<strong>CGContextDrawImage</strong>绘制位图,CGContextDrawImage在执行过程中会触发Image/IO进行解码并分配Bitmap内存。得到的产物用来真正产出一个CGImage-based的UIImage,交由UIImageView渲染。</p></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;</span><br><span class="line"> BOOL hasAlpha = NO;</span><br><span class="line"> if (alphaInfo == kCGImageAlphaPremultipliedLast ||</span><br><span class="line"> alphaInfo == kCGImageAlphaPremultipliedFirst ||</span><br><span class="line"> alphaInfo == kCGImageAlphaLast ||</span><br><span class="line"> alphaInfo == kCGImageAlphaFirst) {</span><br><span class="line"> hasAlpha = YES;</span><br><span class="line"> }</span><br><span class="line"> // BGRA8888 (premultiplied) or BGRX8888</span><br><span class="line"> // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]</span><br><span class="line"> CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;</span><br><span class="line"> bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;</span><br><span class="line"> CGContextRef context = **CGBitmapContextCreate**(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);</span><br><span class="line"> if (!context) return NULL;</span><br><span class="line"> **CGContextDrawImage**(context, CGRectMake(0, 0, width, height), imageRef); // decode</span><br><span class="line"> CGImageRef newImage = **CGBitmapContextCreateImage**(context);</span><br><span class="line"> CFRelease(context);</span><br><span class="line"> return newImage;</span><br></pre></td></tr></table></figure><h5 id="2、特点"><a href="#2、特点" class="headerlink" title="2、特点"></a>2、特点</h5><ul><li><p>可以提升图第一次渲染到屏幕上的性能和滚动帧率; </p></li><li><p>因为解码后的位图要保留在内存中,会给内存带来压力,要注意内存的清理;</p></li><li><p>注意控制处理图片解码的子线程数量,子线程过多同样会影响性能。</p></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">解码后的图像大小和图片的宽高像素有关,宽高像素越大,位图图像就越大。假设一个3MB的图片,其宽高像素为2048 * 2048 的图片,解码后的位图图像大小是16MB(2048 * 2048 * 4)</span><br></pre></td></tr></table></figure><h5 id="3、其他"><a href="#3、其他" class="headerlink" title="3、其他"></a>3、其他</h5><ul><li><p>SDWebImage中有两个解码方法decodedImageWithImage 和 decodedAndScaledDownImageWithImage ,分别对应处理普通图,大图(默认位图大小超过60MB),对于大图,建议使用后者。其主要思路是:将大的原图切块,按块缩放成指定大小的图片填充到目标图片中去。</p></li><li><p>Apple大大没有采用此类方案,猜测原因是:因为早期设备内存有限,UIKit整套渲染机制很多地方采用时间换空间的策略。</p></li><li><p>现在,大部分业务使用的是小图,大图的场景少,导致SDWebImage这类子线程解码方案很欢迎,内存开销比较稳定,性能也能提升。</p></li></ul><h4 id="五、后续"><a href="#五、后续" class="headerlink" title="五、后续"></a>五、后续</h4><ul><li><p>子线程解码最终还是利用Image I/O解码,对于Image I/O不支持的图片格式,第三方解码方案?</p></li><li><p>图片编解中遇到的图像方面的知识</p></li></ul>]]></content>
<summary type="html">
<p><strong>KeyPoints</strong></p>
<ul>
<li><p>图片如何显示到屏幕上的</p>
</li>
<li><p>为什么要图片解码</p>
</li>
<li><p>图片解码方案及其对比</p>
</li>
</ul>
<h4 id="一、图像
</summary>
<category term="iOS基础" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E5%9F%BA%E7%A1%80/"/>
<category term="图片解码" scheme="http://buaa0300/nanhuacoder.com/tags/%E5%9B%BE%E7%89%87%E8%A7%A3%E7%A0%81/"/>
</entry>
<entry>
<title>iOS架构小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/02/21/iOS-iOSDesignMode/"/>
<id>http://buaa0300/nanhuacoder.com/2019/02/21/iOS-iOSDesignMode/</id>
<published>2019-02-21T14:29:49.000Z</published>
<updated>2019-04-18T13:55:34.000Z</updated>
<content type="html"><![CDATA[<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、背景"><a href="#1、背景" class="headerlink" title="1、背景"></a>1、背景</h5><ul><li>谈及iOS的架构,绕不开组件化、MVC、MVVM这些关键词</li><li>对于几个人的小团队,组件化未必是适合的方案,当然遇到业务扩张,新App需要落地,将相同的功能沉库,可以汲取组件化的精髓,提高生产效率。</li><li>组件化是大中型App团队的选择。</li><li>对于百人以上的客户端超级大团队,组件化非常值得拥有,当然实施起来也非常复杂,包括业务划分,业务组件和基础组件的设计和开发、测试、集成和发版。</li></ul><h5 id="2、MVC和MVVM"><a href="#2、MVC和MVVM" class="headerlink" title="2、MVC和MVVM"></a>2、MVC和MVVM</h5><ul><li>好像一说起Controller代码臃肿,就把锅求给MVC,就鼓吹MVVM,其实MVC虽然有明显的缺点,但是合理使用,绝对满足大部分需求,造成代码的臃肿和难以管理,很大原因和开发者认知有关系。</li><li>MVVM可以用,使用MVVM不必要一定引入RAC,</li></ul><h5 id="3、组件化"><a href="#3、组件化" class="headerlink" title="3、组件化"></a>3、组件化</h5><ul><li><strong>业务快速发展、复杂的业务场景、团队的快速扩张等</strong>带来的变革诉求,希望通过<strong>组件化</strong>来提高团队的协作能力、减低开发成本,提高开发质量。</li><li>组件化直接效果:代码解耦,功能模块化;代码的复用性高;代码管理更加科学。</li><li>从<strong>蘑菇街</strong>(<a href="https://www.jianshu.com/p/cdf94a963c27" target="_blank" rel="noopener">蘑菇街 App 的组件化之路</a>)、<strong>支付宝(<a href="http://www.sohu.com/a/129493438_536622" target="_blank" rel="noopener">从支付宝红包揭秘亿级APP的移动开发</a>)</strong>等团队公开的信息来看:业务的增加、开发团队的扩张、快速迭代的要求,催生组件化方案(<em>可能有更好的架构方案</em>)快速落地。</li><li>蘑菇街和支付宝等团队实现组件化方案,一是业务发展的必然选择;二是其技术沉淀深,能为自己和兄弟团队打造出质量上乘的组件化服务。</li></ul><h4 id="二、组件化实施"><a href="#二、组件化实施" class="headerlink" title="二、组件化实施"></a>二、组件化实施</h4><h5 id="1、实施步骤"><a href="#1、实施步骤" class="headerlink" title="1、实施步骤"></a>1、实施步骤</h5><ul><li>剥离产品公共库和基础库</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">将网络请求、数据存储、图片下载和存储、第三方SDK(微信,支付宝)、UI基础组件、自定义相机等、UIKit和Foundation的扩展等,拆分出来,各自沉库,使用cocopods管理。</span><br></pre></td></tr></table></figure><ul><li>独立业务模块单独成库</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">将登录、分享、支付、日志上报等模块封装成组件,也可以将一些通用模块,如资讯详情页、XX模块拆分出来,拆分粒度可以先粗后细,将相对独立的功能封装成组件,统一对外提供服务,保证体验一致性。</span><br></pre></td></tr></table></figure><ul><li>对外服务最小化</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">在前两步都完成的情况下,根据组件被调用的需求来抽象出组件对外的最小化接口(遵循SOLID原则中的接口分离原则)</span><br></pre></td></tr></table></figure><h5 id="2、组件通信"><a href="#2、组件通信" class="headerlink" title="2、组件通信"></a>2、组件通信</h5><p>组件之间的通信,更多是指业务组件之间的通信吧。目前蘑菇街团队公开的方案是:<strong>URLRouter</strong>、<strong>Protocol Class Binding</strong>和<strong>Target-Action</strong>这三类方案。</p><h6 id="1)URLRouter"><a href="#1)URLRouter" class="headerlink" title="1)URLRouter"></a>1)URLRouter</h6><ul><li><p><strong>简介</strong>:蘑菇街团队实现<a href="https://github.com/meili/MGJRouter" target="_blank" rel="noopener">MGJRouter</a>库,可以根据URL处理执行对应的Block;其核心在于,先注册URL 和 <strong>服务Block</strong> & <strong>参数字典</strong>的对应关系(<em>保存在router字典中</em>),然后利用URL找到对应的Block,将参数字典交给Block,唤起对应的服务。</p> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">// 注册</span><br><span class="line">[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {</span><br><span class="line">[self appendLog:[NSString stringWithFormat:@"routerParameters:%@", routerParameters]];</span><br><span class="line"> }];</span><br><span class="line"></span><br><span class="line">//传参</span><br><span class="line">[MGJRouter openURL:@"mgj://foo/bar" withUserInfo:@{@"param1":@"hello world"} completion:nil];</span><br><span class="line"></span><br><span class="line">//同步获取object</span><br><span class="line">NSNumber *orderCount = [MGJRouter objectForURL:@"mgj://cart/ordercount"]</span><br></pre></td></tr></table></figure></li><li><p><strong>优势</strong>:解耦方便;各个组件依赖MGJRouter就可以;打破组件间的相互依赖;</p></li><li><p><strong>不足</strong>:组件本身依赖中间件,但是分散注册又使得<strong>耦合较多</strong>。<br>需要专门维护URL(<em>蘑菇街使用后台维护,自动生成URL短链的方式</em>);</p></li><li><p><strong>补充</strong>:<a href="https://github.com/meili/MGJRouter" target="_blank" rel="noopener">MGJRouter</a>是URL Router的OC版本实现,<a href="https://github.com/devxoul/URLNavigator" target="_blank" rel="noopener">URLNavigator</a>是URL Router的Swift版本实现,核心都是:注册URL和对应的处理,然后根据URL解析去做事情,如页面跳转。</p></li></ul><h6 id="2)Protocol-Class-Binding-协议和类绑定"><a href="#2)Protocol-Class-Binding-协议和类绑定" class="headerlink" title="2)Protocol Class Binding(协议和类绑定)"></a>2)Protocol Class Binding(协议和类绑定)</h6><ul><li><p><strong>简介</strong>:核心在于,为组件定义Protocol,Protocol指定返回的数据,然后在组件中新建Class实现Protocol,如此将Protocol和Class关联起来。</p> <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">//以购物车组件为例</span><br><span class="line">//1、组件定义MGJCart,执行返回订单数方法</span><br><span class="line">@protocol MGJCart <NSObject></span><br><span class="line"></span><br><span class="line">- (NSInteger)orderCount;</span><br><span class="line">@end</span><br><span class="line"></span><br><span class="line">//2、MGJCartImpl 实现MGJCart ,实现略</span><br><span class="line"> </span><br><span class="line">//3、关联</span><br><span class="line">[ModuleManager registerClass:MGJCartImpl forProtocol:@protocol(MGJCart)],</span><br><span class="line"></span><br><span class="line"> //4、获取MGJCartImpl,接下来可以访问到参数了</span><br><span class="line">[ModuleManager classForProtocol:@protocol(MGJCart)]</span><br></pre></td></tr></table></figure></li><li><p><strong>优势</strong>:把公共的协议统一放到同一文件中,组件依赖该文件即可。</p></li><li><p><strong>补充</strong>:阿里的beehive属于此类方案实现,具体参考<a href="https://halfrost.com/beehive/" target="_blank" rel="noopener">BeeHive —— 一个优雅但还在完善中的解耦框架</a>,核心思想涉及:各个模块间从直接调用对应模块,变成以Service的形式,避免了直接依赖;App生命周期的分发,将耦合在AppDelegate中的逻辑拆分,每个模块以微应用的形式独立存在。</p></li></ul><h6 id="3)Target-Action方案"><a href="#3)Target-Action方案" class="headerlink" title="3)Target-Action方案"></a>3)Target-Action方案</h6><ul><li><strong>简介</strong>: <a href="https://github.com/casatwy/CTMediator" target="_blank" rel="noopener">CTMediator</a>方案,在此类中对外提供明确参数类型的接口,接口内部通过performTarget方法调用服务方组件的Target、Action。由于CTMediator类的调用是通过runtime主动发现服务的,所以服务方对此类是完全解耦的。但如果CTMediator类对外提供的方法都放在此类中,将会对CTMediator造成极大的负担和代码量。解决方法就是对每个服务方组件创建一个CTMediator的Category,并将对服务方的performTarget调用放在对应的Category中,这些Category都属于CTMediator中间件,从而实现了感官上的接口分离。</li><li><strong>特点</strong>: 侵入小,但硬编码较多,Runtime编译阶段不检查,运行时才检查对应类或者方法是否存在,对开发要求较高。</li></ul><h5 id="3、其他"><a href="#3、其他" class="headerlink" title="3、其他"></a>3、其他</h5><p>需要一个组件的管理平台,管理这些组件的单元测试、集成和发版</p><p>参考文章:<a href="https://www.jianshu.com/p/48fbcbb36c75" target="_blank" rel="noopener">iOS App组件化开发实践</a>、<a href="http://cocoafei.top/2018/06/iOS%E7%BB%84%E4%BB%B6%E5%8C%96%E6%96%B9%E6%A1%88%E9%80%89%E5%9E%8B/#3_2" target="_blank" rel="noopener">iOS组件化方案选型</a></p><h4 id="三、MVC"><a href="#三、MVC" class="headerlink" title="三、MVC"></a>三、MVC</h4><h5 id="1、苹果推荐的MVC"><a href="#1、苹果推荐的MVC" class="headerlink" title="1、苹果推荐的MVC"></a>1、苹果推荐的MVC</h5><ul><li>我们常说的MVC主要包括以下三部分</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">控制器(Controller)- 数据的加工者</span><br><span class="line">视图(View) - 数据显示</span><br><span class="line">模型(Model)- 数据管理</span><br></pre></td></tr></table></figure><ul><li>在苹果推荐的MVC中,<strong>View和Model是没有通信的</strong>,但是Controller可以Model和View通信;</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-77b1b371629ba8d8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640" alt="MVC"></p><ul><li>Controller是可以直接访问Model,然后Model不知道Controller是谁,当Model发生变化时候,利用通知、代理或KVO等方式通知Controller</li><li><p>Controller也可以直接访问View,Controller可以直接根据Model来决定View的展示。View接收到响应事件, 通过delegate、target-action、block等方式告诉Controller的状态变化。Controller进行业务的处理,然后再控制View的展示。</p></li><li><p>随着Controller和Model、View的交互(通信)越来越多,Controller中的代码就越来越多,造成Controller中代码过于臃肿。</p></li></ul><h5 id="2、MVC的优化实践"><a href="#2、MVC的优化实践" class="headerlink" title="2、MVC的优化实践"></a>2、MVC的优化实践</h5><ul><li>MVC是非常经典的设计模式,虽然有很多问题,但是使用得当的话,其实能搞定大部分项目。很多时候,代码的臃肿很大原因是开发者代码不规范造成的,比如将网络请求逻辑,数据的存储逻辑都放在Controller中,或者Controller中展示聚集太多的UI元素,这如何不造成Controller的代码臃肿呢。</li><li>在使用MVC模式,需要划分好MVC的职责</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">Model应该做的事:</span><br><span class="line">1.给ViewController提供数据</span><br><span class="line">2.给ViewController存储数据提供接口</span><br><span class="line">3.提供经过抽象的业务基本组件,供Controller调度</span><br><span class="line"></span><br><span class="line">Controller应该做的事:</span><br><span class="line">1.管理View Container的生命周期</span><br><span class="line">2.负责生成所有的View实例,并放入View Container</span><br><span class="line">3.监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。</span><br><span class="line"></span><br><span class="line">View应该做的事:</span><br><span class="line">1.响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。</span><br><span class="line">2.界面元素表达</span><br></pre></td></tr></table></figure><ul><li>对软件结构做好分层,将软件分成若干个水平层,每一层都有清晰的角色和分工,不需要知道其他层的细节。层与层之间通过接口通信。</li></ul><h4 id="四、MVVM"><a href="#四、MVVM" class="headerlink" title="四、MVVM"></a>四、MVVM</h4><h5 id="1、介绍"><a href="#1、介绍" class="headerlink" title="1、介绍"></a>1、介绍</h5><p><img src="https://upload-images.jianshu.io/upload_images/201701-7584653944d3e889.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640" alt="MVVM.png"></p><ul><li>在MVVM中,将和视图、数据的交互处理从Controller中移到ViewModel中;VM负责和Model通信,可以直接访问Model,当Model改变时候通知VM;同时,VM负责和视图的通信,直接访问视图,当视图接收到响应事件时,通知VM去处理。而Controller仅仅是协调各个部分的绑定关系以及必要的逻辑处理。</li><li>MVVM的核心是:<strong>实现MVVM的双向绑定</strong>,很多项目组因此引入RAC框架(函数响应式框架),其实使用KVO机制也可以实现双向绑定也可以,直接使用KVO,会有一些问题,建议直接使用facebook的KVOController</li></ul><h5 id="2、MVVM和MVC的对比"><a href="#2、MVVM和MVC的对比" class="headerlink" title="2、MVVM和MVC的对比"></a>2、MVVM和MVC的对比</h5><p><img src="https://upload-images.jianshu.io/upload_images/201701-ea78e2837ecc0db4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640" alt="mvvm和mvc.png"></p><ul><li><strong>MVVM优点</strong>:方便测试,VM可以方便做单元测试,MVC下的Controller里面逻辑太多,无法做单元测试;耦合度低; 复用性高; 层次更清晰; 重构成本低;</li><li><strong>MVVM缺点</strong>:类文件增多(每个VC多一个ViewModel),ViewModel的代码复杂度增大(处理各种交互)</li><li><strong>MVC优点</strong>:通用架构; 处理耦合度高的逻辑方便; </li><li><strong>MVC缺点:</strong> 耦合度高; 复用性差; 测试性差;</li></ul><h5 id="3、补充个MVP"><a href="#3、补充个MVP" class="headerlink" title="3、补充个MVP"></a>3、补充个MVP</h5><ul><li>MVP分别是<strong>Mode</strong>、<strong>View</strong>和<strong>Presenter</strong>,对应数据<strong>、</strong>视图<strong>和</strong>主持者,在Android开发中使用普遍。</li></ul><p><img src="https://upload-images.jianshu.io/upload_images/201701-7fdab1dde3415a38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/640" alt="MVP.png"></p><ul><li>MVC模式下,Android的Activity承担繁杂的业务逻辑,导致代码臃肿,使用MVP模式,将View层和Mode层隔离开,增加Presenter层,作为Mode层和View层通讯的桥梁</li><li>Presenter同时持有Mode层和View层的引用,在需要数据改变 或 视图显示时直接改变数据或者视图的显示状态。同样View层持有Presenter层的引用,这样就能将一些处理事件的逻辑放在Presenter层中进行处理,处理完成后通知View层改变显示状态。</li><li>具体参考 <a href="https://juejin.im/post/5a61559051882573351a5fb6" target="_blank" rel="noopener">MVP模式的经典封装</a></li></ul><h4 id="五、SOLD原则"><a href="#五、SOLD原则" class="headerlink" title="五、SOLD原则"></a>五、SOLD原则</h4><h5 id="1、介绍-1"><a href="#1、介绍-1" class="headerlink" title="1、介绍"></a>1、介绍</h5><ul><li>根据项目的复杂度来决定选择MVC或MVVM,但是无论哪种模式,都做好代码设计,结构分层,类设计遵守SOLID原则</li></ul><table><thead><tr><th>缩写</th><th>全称</th><th>中文</th><th>备注</th></tr></thead><tbody><tr><td>S</td><td>The Single Responsibility Principle</td><td>单一责任原则</td><td>一个类只应承担一种责任</td></tr><tr><td>O</td><td>The Open Closed Principle</td><td>开放封闭原则</td><td>可扩展,不可修改</td></tr><tr><td>L</td><td>Liskov Substitution Principle</td><td>里氏替换原则</td><td>子类可以在任意地方替换基类且软件功能不受影响</td></tr><tr><td>I</td><td>The Interface Segregation Principle</td><td>接口分离原则</td><td>将接口拆分成更小和更具体的接口,有助于解耦</td></tr><tr><td>D</td><td>The Dependency Inversion Principle</td><td>依赖倒置原则</td><td>一个方法应该遵从“依赖于抽象而不是一个实例”。依赖注入 是该原则的一种实现方式。</td></tr></tbody></table><h5 id="2、IOC和DI"><a href="#2、IOC和DI" class="headerlink" title="2、IOC和DI"></a>2、IOC和DI</h5><p>控制反转(IOC)和依赖注入(DI)是Spring中最重要的核心概念之一,而两者实际上是一体两面的。</p><ul><li>依赖注入<ul><li>一个类依赖另一个类的功能,那么就通过注入,如构造器、setter方法等方式将这个类的实例引入。</li><li>侧重于实现。</li></ul></li><li>控制反转<ul><li>创建实例的控制权由一个实例的代码剥离到IOC容器控制,如xml配置中。</li><li>侧重于原理。</li><li>反转了什么:原先是由类本身去创建另一个类,控制反转后变成了被动等待这个类的注入。</li></ul></li></ul>]]></content>
<summary type="html">
<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、背景"><a href="#1、背景" class="headerlink" title="1、背景"></a
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="MVVM" scheme="http://buaa0300/nanhuacoder.com/tags/MVVM/"/>
<category term="MVC" scheme="http://buaa0300/nanhuacoder.com/tags/MVC/"/>
<category term="URLRouter" scheme="http://buaa0300/nanhuacoder.com/tags/URLRouter/"/>
</entry>
<entry>
<title>瘦身优化小记</title>
<link href="http://buaa0300/nanhuacoder.com/2019/01/05/iOS-AppThinning/"/>
<id>http://buaa0300/nanhuacoder.com/2019/01/05/iOS-AppThinning/</id>
<published>2019-01-05T09:39:47.000Z</published>
<updated>2019-04-20T15:49:25.000Z</updated>
<content type="html"><![CDATA[<p><em>闲散心如月,风光好自知</em></p><h4 id="一、安装包组成分析"><a href="#一、安装包组成分析" class="headerlink" title="一、安装包组成分析"></a>一、安装包组成分析</h4><h5 id="1、组成情况"><a href="#1、组成情况" class="headerlink" title="1、组成情况"></a>1、组成情况</h5><p> 将IPA包修改后缀名为ZIP,解压缩后,获取payload中的App文件,查看App文件的内容,你会发现该文件主要包含以下内容</p><ul><li><strong>Exectutable</strong>: 可执行文件</li><li><strong>Resources</strong>:资源文件<ul><li>图片资源:<strong>Assets.car</strong>/bundle/png/jpg 等</li><li>视频/音频资源:mp4/mp3 等</li><li>静态网页资源:html/css/js 等</li><li>视图资源:xib/storyboard 等</li><li>其他:文本/字体/证书 等</li></ul></li><li><strong>Framework</strong>:<ul><li>SwiftSupport: libSwiftxxx 等一系列 Swift 库</li><li>其他依赖库:Embeded Framework</li></ul></li><li><strong>Pulgins</strong>:Application Extensions<ul><li>appex:其组成大致与 ipa 包组成一致</li></ul></li></ul><h5 id="2、组成分析"><a href="#2、组成分析" class="headerlink" title="2、组成分析"></a>2、组成分析</h5><ul><li>一般来说,可执行文件、图片资源(asset.car)和动态库的占比最大,如果是Swift和OC混编,可执行文件比纯OC大很多</li><li>从优化的效果上看,优化图片资源的ROI比较大,如果是首次优化,建议从图片资源的优化开始。</li><li>项目中使用Swift,会增加安装包大小,因为FrameWork中会加入为了支持 Swift 的动态库集合,如果纯Swift项目,不会引入这些东西。</li></ul><h4 id="二、资源文件优化"><a href="#二、资源文件优化" class="headerlink" title="二、资源文件优化"></a>二、资源文件优化</h4><p> 理论上,资源文件包括:图片<strong>、</strong>视频<strong>、</strong>音频和字体等;实际上,视频和音频文件一般不会集成到安装包中,在安装包中的资源文件主要是图片。</p><h5 id="1、优化手段1:App-Slicing"><a href="#1、优化手段1:App-Slicing" class="headerlink" title="1、优化手段1:App Slicing"></a>1、优化手段1:App Slicing</h5><ul><li>iOS 9之后提供了App Thinning三件套:<strong>App Slicing</strong>、<strong>On Demand Resouces</strong>、<strong>Bitcode</strong>;</li></ul><table><thead><tr><th>App Thinning</th><th>理想</th><th>现实</th></tr></thead><tbody><tr><td><strong>App Slicing</strong></td><td>将App Bundle资源根据不同的设备特性分为不同的版本。对于图片资源,会根据设备所需图片分辨率不同分发给对应设备所需对应的图片资源。</td><td>主要是图片资源的Slicing,我们有自己的方案,没有采用</td></tr><tr><td><strong>On Demand Resources</strong></td><td>App的资源只有要使用时才下载,如果其他资源需要空间这些资源可以被移除</td><td>更适合游戏类App,项目没有使用</td></tr><tr><td><strong>Bitcode</strong></td><td>Bitcode可以作为中间产物一起提交AppStore。包含Bitcode配置的程序将会在AppStore上被编译和链接。Bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到AppStore上</td><td>使用BitCode的要求所有代码都支持BitCode,改动项目较大,没有使用</td></tr></tbody></table><p><strong>说明</strong>:可以充分利用App Slicing实现图片资源的瘦身</p><ul><li>在项目中引入图片时候,直接在 <strong>Assets.xcassets</strong>中添加就可以(资源文件用<strong>Asset Catalog</strong>管理),这样能使用到<strong>App Slicing</strong>功能,这样当用户从App Store上下载App时,可以只下载适用于其设备的App架构版本和所需资源,从而减少App所占的空间。</li><li>在实践中发现,有的新同学在<strong>Assets.xcassets</strong>中引入@1x的图片,iPhone手机目前需要的@2x和@3x图片,所以@1x的图片显然是不需要的。</li><li>在实践中还发现,有的图片资源游离在Assets.xcassets之外,这些可以考虑是否可以放入<strong>Assets.xcassets</strong>中(大部分情况下是可以放入的)</li></ul><h5 id="2、优化手段2:Xcode编译项"><a href="#2、优化手段2:Xcode编译项" class="headerlink" title="2、优化手段2:Xcode编译项"></a>2、优化手段2:Xcode编译项</h5><ul><li><p>因为绝大部分引入的图片是PNG格式,Xcode 提供的给我们两个编译选项来帮助压缩 PNG 资源:</p></li><li><p><strong>Compress PNG Files</strong>:设置为YES,打包的时候自动对图片进行无损压缩,使用的工具为 <a href="https://en.wikipedia.org/wiki/Pngcrush" target="_blank" rel="noopener">pngcrush</a>,压缩比还是相当高的,比较流行的压缩软件 <a href="https://imageoptim.com/mac" target="_blank" rel="noopener">ImageOptim</a> 也是使用 pngcrush 进行压缩 PNG 的。</p></li><li><strong>Remove Text Medadata From PNG Files</strong>:设置为YES,能帮助我们移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息。</li><li>引入项目的PNG资源自动被 Xcode 进行压缩了,但是如果是使用Bundle管理的资源,不会被Xcode压缩,可以使用<a href="https://tinypng.com/" target="_blank" rel="noopener">tinypng</a>压缩。</li></ul><h5 id="3、优化手段3:清理无用的资源"><a href="#3、优化手段3:清理无用的资源" class="headerlink" title="3、优化手段3:清理无用的资源"></a>3、优化手段3:清理无用的资源</h5><ul><li><strong>及时清理不使用的图片资源</strong>。使用类似<a href="https://github.com/tinymind/LSUnusedResources" target="_blank" rel="noopener">LSUnusedResources</a> 清理旧的图片文件。</li><li>LSUnusedResources的思路是,先获取图片文件(imageset, jpg, png, gif)集合A,然后搜索代码文件中所有字符串名称得到B,然后从A集合中排除集合B就得到未使用的图片资源。 </li></ul><h5 id="4、优化手段4:图片文件去重"><a href="#4、优化手段4:图片文件去重" class="headerlink" title="4、优化手段4:图片文件去重"></a>4、优化手段4:图片文件去重</h5><ul><li>遍历图片文件,计算每个文件的MD5值,然后以MD5值为key,文件路径存入key对应的数组;</li><li>遍历字典values,将value的数组大小大于1的路径输出,这样就找到重复图片的路径了。</li></ul><h5 id="5、优化手段5:更适合的图片格式"><a href="#5、优化手段5:更适合的图片格式" class="headerlink" title="5、优化手段5:更适合的图片格式"></a>5、优化手段5:更适合的图片格式</h5><ul><li><strong>iconfont</strong>代替项目中纯色小图标,也省去很多@2x和@3x的图片切图。</li><li>PNG切图的替换方案,如<strong>PDF矢量图来代替大部分简单的png切图</strong>;然后在代码中自己解码并展示出来,一套PDF矢量图可以等效大部分2x和3x的png图片;</li><li>网络图片选择压缩比更好的图片格式,如webp</li></ul><p><strong>说明</strong>:PNG切图不可能被完全替换,在表现颜色丰富图片时候,PNG效果很不错,其他详见<a href="https://buaa0300.github.io/2018/01/13/imageOptimize/" target="_blank" rel="noopener">浅谈iOS图片优化</a></p><h4 id="三、可执行文件优化"><a href="#三、可执行文件优化" class="headerlink" title="三、可执行文件优化"></a>三、可执行文件优化</h4><h5 id="1、优化手段1:编译器优化"><a href="#1、优化手段1:编译器优化" class="headerlink" title="1、优化手段1:编译器优化"></a>1、优化手段1:编译器优化</h5><ul><li>Xcode 支持编译器层面的一些优化优化选项,可以让我们介于<strong>更快的编译速度</strong>、<strong>更小的二进制大小</strong>和<strong>更快的执行速度</strong>之间自由选择想要进行的优化粒度;</li></ul><ul><li>在Xcode中,使用<strong>Clang</strong>来编译Objective-C,可以在 Build Setting -> Apple Clang - Code Generation -> <strong>Optimization Level</strong> 设置,Release下为<strong>Fastest Smallest[-Os]</strong>。编译器会开启除了会明显增加包大小以外的所有优化选项;</li></ul><ul><li>在Xcode中,使用<strong>SwiftLang</strong>来编译Swift语言,同样也是基于 LLVM 后端的。Xcode 9.3 版本之后可以在Build Setting -> <strong>Optimization Level</strong> 设置,Release下为<strong>Optimize for Speed[-O]</strong>,这可能会增加安装包大小</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">No optimization[-Onone]:不进行优化,能保证较快的编译速度。</span><br><span class="line">Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。</span><br><span class="line">Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:Xcode 9.3/Swift4.1编译器不是特别稳定,特别是开启 Osize 选项之后,编译器很多情况下会莫名其妙的崩溃(Segmentation fault),目前放弃 [-Osize],选择[-O]</p><h5 id="2、优化手段2:去除符号信息"><a href="#2、优化手段2:去除符号信息" class="headerlink" title="2、优化手段2:去除符号信息"></a>2、优化手段2:去除符号信息</h5><ul><li>可执行文件中的符号:程序中的所有的<strong>变量、类、函数、枚举、变量和地址映射关系</strong>,以及一些在调试的时候使用到的用于定位代码在源码中的位置的调试符号,符号和断点定位以及堆栈符号化有很重要的关系。</li></ul><ul><li><strong>Strip Style</strong>表示的是我们需要去除的符号的类型的选项,可以在Build Setting -> Strip Style设置, Release下为<strong>All Symbols</strong>,其分为三个选择项:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">All Symbols: 去除所有符号,一般是在主工程中开启。</span><br><span class="line"></span><br><span class="line">Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符号,Debug Symbols 同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库/动态库的建议选项。</span><br><span class="line"></span><br><span class="line">Debug Symbols: 去除调试符号,去除之后将无法断点调试。</span><br></pre></td></tr></table></figure><p><strong>说明</strong>:iOS 的调试符号是 <strong>DWARF</strong> 格式的,使用 Xcode 编译打包的时候会先通过可执行文件的 Debug Map 获取到所有对象文件的位置,然后使用 <strong>dysmutil</strong> 来将对象文件中的 DWARF 提取出来生成 dSYM 文件。</p><ul><li><strong>Strip Linked Product</strong>去除不必要的符号信息,去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。Release下为YES。</li></ul><ul><li>Strip Linked Product 选项在 <strong>Deployment Postprocessing 设置为 YES</strong> 的时候才生效,而在 Archive 的时候 Xcode 总是会把 Deployment Postprocessing 设置为 YES,Debug下,Deployment Postprocessing 设置为 NO。</li></ul><ul><li><strong>Strip Debug Symbols During Copy</strong>将那些拷贝进项目包的三方库、资源或者 Extension 的 Debug Symbol 去除掉,在Build Settings -> Strip Debug Symbols During Copy设置,Release下设置为YES。</li></ul><ul><li>Cocoapods 管理的动态库(use_framework!)的情况就相对要特殊一点,因为 Cocoapods 中的的动态库是使用自己实现的脚本 Pods-xxx-frameworks.sh 来实现拷贝的,所以并不会走 Xcode 的流程,当然也就不受 Strip Debug Symbols During Copy 的影响。当然 Cocoapods 是源码管理的,所以只需要将源码 Target 中的 Strip Linked Product 设置为 YES 即可。</li></ul><ul><li><strong>Strip Swift Symbols</strong>能帮助我们移除相应 Target 中的所有的 Swift 符号,这个选项也是默认打开的。Strip Swift symbols需要在打包的发布选项中勾选(默认勾选),在Swift ABI 稳定之前,Swift 标准库是会打进目标文件的。</li></ul><h5 id="3、优化手段3:BitCode"><a href="#3、优化手段3:BitCode" class="headerlink" title="3、优化手段3:BitCode"></a>3、优化手段3:BitCode</h5><ul><li>Bitcode可以作为中间产物一起提交AppStore。包含Bitcode配置的程序将会在AppStore上被编译和链接。Bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到AppStore上。</li></ul><ul><li>开启 BitCode 之后编译器后端(Backend)的工作都由 Apple 接管了。所以假如以后苹果推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化,我们也不再需要为其发布新的安装包了。</li></ul><ul><li>工程开启 BitCode 之后必须要求所有打进 Bundle 的 Binary 都需要支持 BitCode,也就是说我们依赖的静态库和动态库都是含有 BitCode 的,不然就会打包失败。对于 Cocoapods 等源码管理工具来管理的依赖库来说操作会比较简单,我们只需要开启 Pods 工程中的 BitCode 就行。但是对于一些三方的闭源库,我们就无能为力了。</li></ul><ul><li>开启 BitCode 之后,由于最终的可执行文件是 Apple 自动生成的,同时产生新的符号表文件,所以我们使用原本打包生成的 dSYM 符号化文件是无法完成符号化的。所以我们需要在上传至 App Store 时需要勾选 Include app symbols for your application to receive symboilcated crash logs from Apple:勾选之后 Apple 会给我们生成 dSYM,然后就可以在 Xcode -> Organizer 或者 iTunes Connect 中下载对应的 dSYM 来进行符号化了。</li></ul><h5 id="4、优化手段4:清除无用代码"><a href="#4、优化手段4:清除无用代码" class="headerlink" title="4、优化手段4:清除无用代码"></a>4、优化手段4:清除无用代码</h5><ul><li><strong>Dead Code Stripping</strong>:Xcode 默认会开启此选项,C/C++/Swift 等<strong>静态语言</strong>编译器会在 link 的时候<strong>移除未使用的代码</strong>,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。</li></ul><ul><li><strong>扫描查找无用代码</strong>:基本思路都是查找已经使用的方法/类和所有的类/方法,然后从所有的类/方法当中剔除已经使用的方法/类剩下的基本都是无用的类/方法,但是由于 Objective-C 是动态语言,可以使用字符串来调用类和方法,所以检查结果一般都不是特别准确,需要二次确认。目前市面上的扫描的思路大致可以分为 3 种:<ul><li>基于 Clang 扫描</li><li>基于可执行文件扫描</li><li>基于源码扫描</li></ul></li></ul><ul><li><strong>及时下线不需要的功能</strong>,如完成使命的ABTest代码、被产品抛弃的功能代码等。</li></ul><ul><li>移除不需要的系统库和第三方库。</li></ul><h5 id="5、优化手段5:代码重构"><a href="#5、优化手段5:代码重构" class="headerlink" title="5、优化手段5:代码重构"></a>5、优化手段5:代码重构</h5><ul><li>功能合并:相似功能的代码,只需维护一份就可以了。如定制通用UI组件,大家可以有类似需求,可以给通用UI组件的开发提,没必要自己单独实现。</li></ul><h4 id="End"><a href="#End" class="headerlink" title="End"></a>End</h4><h5 id="1、优化之后"><a href="#1、优化之后" class="headerlink" title="1、优化之后"></a>1、优化之后</h5><ul><li><p>保持良好的开发习惯。及时清理无用代码和无效库</p></li><li><p>持续关注安装包大小的变化,</p></li><li>定期Review安装包大小变化</li><li>建议预警机制,监控每个版本的体积大小,体积图片突然变大,要去找原因。</li></ul><h5 id="2、参考资料"><a href="#2、参考资料" class="headerlink" title="2、参考资料"></a>2、参考资料</h5><p><a href="https://juejin.im/post/5800ef71a0bb9f0058736caa" target="_blank" rel="noopener">iOS App 瘦身实践总结</a></p><p><a href="https://sq.163yun.com/blog/article/200385709022117888" target="_blank" rel="noopener">iOS 安装包瘦身(上篇)</a></p><p><a href="https://sq.163yun.com/blog/article/200384401846304768" target="_blank" rel="noopener">iOS 安装包瘦身(下篇)</a></p>]]></content>
<summary type="html">
<p><em>闲散心如月,风光好自知</em></p>
<h4 id="一、安装包组成分析"><a href="#一、安装包组成分析" class="headerlink" title="一、安装包组成分析"></a>一、安装包组成分析</h4><h5 id="1、组成情况"><
</summary>
<category term="iOS进阶" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E8%BF%9B%E9%98%B6/"/>
<category term="瘦身" scheme="http://buaa0300/nanhuacoder.com/tags/%E7%98%A6%E8%BA%AB/"/>
</entry>
<entry>
<title>推荐迷雾(一):话说ABTest实验</title>
<link href="http://buaa0300/nanhuacoder.com/2018/11/30/Recommend02/"/>
<id>http://buaa0300/nanhuacoder.com/2018/11/30/Recommend02/</id>
<published>2018-11-30T15:17:13.000Z</published>
<updated>2019-04-18T14:00:03.000Z</updated>
<content type="html"><![CDATA[<p><em>雄兔脚扑朔,雌兔眼迷离;双兔傍地走,安能辨我是雄雌?</em></p><h3 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h3><h4 id="1、什么是ABTest实验"><a href="#1、什么是ABTest实验" class="headerlink" title="1、什么是ABTest实验"></a>1、什么是ABTest实验</h4><ul><li>今天ABTest已经是一门显学了,主流的产品,无论是算法模型优化,还是UI或体验的调整,上线后都会做ABTest实验,这种线上实验离用户更近,收益大于离线评估方案;</li><li>ABTest实验是为了避免盲目决策带来不确定性和随机性,将各种不同的实验同时放到线上,然后利用数据分析来辅助决策,总之一句话,让数据说话 (data talk)。</li></ul><h4 id="2、ABTest实验的条件"><a href="#2、ABTest实验的条件" class="headerlink" title="2、ABTest实验的条件"></a>2、ABTest实验的条件</h4><ul><li><strong>比较好的两个及以上备选方案</strong>,毕竟 ABTest实验不是银弹,它只是辅助我们做<strong>更好的选择</strong>。</li><li><strong>量化的指标</strong>,比如App中PV、UV、CTR、CVR、CPM等</li><li><strong>用户群体稳定 且 用户量足够</strong></li></ul><h4 id="3、ABTest实验需要注意的问题"><a href="#3、ABTest实验需要注意的问题" class="headerlink" title="3、ABTest实验需要注意的问题"></a>3、ABTest实验需要注意的问题</h4><ul><li><strong>实验流量合理分配</strong>: 保证每组实验流量分配的正交性、均匀性和充足性</li><li><strong>排查实验自身干扰</strong>:实验中可能引入不确定因素,导致结果不可预估</li></ul><p><strong>参考</strong>:<a href="http://uxren.cn/?p=58841" target="_blank" rel="noopener">你的AB测试平台和方案,真的可靠么</a></p><h3 id="二、单层模型和分层模型"><a href="#二、单层模型和分层模型" class="headerlink" title="二、单层模型和分层模型"></a>二、单层模型和分层模型</h3><h4 id="1、单层模型"><a href="#1、单层模型" class="headerlink" title="1、单层模型"></a>1、单层模型</h4><ul><li>不同组实验在同一层拆分流量,不同组的流量是不重叠的</li><li>只能支持少量的实验,不利于迭代</li><li>实验之间不独立,策略之间可能相互影响</li><li>分流方式不灵活</li></ul><h4 id="2、分层模型"><a href="#2、分层模型" class="headerlink" title="2、分层模型"></a>2、分层模型</h4><ul><li>主流的流量分配方案,来自2010年谷歌公布的的《Overlapping Experiment Infrastructure More, Better, Faster Experimentation》论文;</li><li>谷歌提出将实验空间横向、纵向划分,纵向流量可以独占实验区域,可以独享实验流量,不被其他实验影响;横向分若干层,每一个可以做同一组的实验,每个独立实验为一层,层与层之间流量是正交的,一份流量穿越每层实验时,都会再次随机打散,且随机效果离散。</li></ul><p><strong>参考</strong>:<a href="http://www.woshipm.com/pd/1080730.html" target="_blank" rel="noopener">一文搞懂AB Testing的分层分流</a></p><h3 id="三、分层模型方案"><a href="#三、分层模型方案" class="headerlink" title="三、分层模型方案"></a>三、分层模型方案</h3><h4 id="1、技术关键点"><a href="#1、技术关键点" class="headerlink" title="1、技术关键点"></a>1、技术关键点</h4><ul><li><p>分流函数(流量如何在每层被打散)如何设计,如果保证每层流量分配的均匀性和正交性</p></li><li><p>如何处理实验样本的过滤(如 只选取某个地区的用户、只选取新用户)</p></li><li><p>分配多大的流量可以使实验置信</p></li></ul><h4 id="2、设计"><a href="#2、设计" class="headerlink" title="2、设计"></a>2、设计</h4><ul><li>域(domain):划分的一部分流量</li><li>层(layer):系统参数的一个子集</li><li>实验(exp):在一个域上,对一个或者多个参数修改,改变请求路径的过程</li></ul><ul><li>相关联的策略参数位于同一实验层;</li><li>相互独立的策略参数分属于不同的实验层;</li><li>一个实验参数只能在一个实验层中出现;</li><li><p>不同实验层间进行独立的流量划分和独立的实验,互不影响。</p></li><li><p>每一实验层享有 100% 流量,可以避免流量切分过细,保证实验间的可对比性、客观性;</p></li><li>不同实验层之间流量正交,可以避免不同试验间的流量依赖和流量不均匀情况的出现。为了更好地评估实验的效果,每一实验层还引入了基准实验。该基准实验会采用该实验层的默认策略取值,流量配比会设定在一个合适的水平。</li></ul><h3 id="四、业内ABTest实验平台"><a href="#四、业内ABTest实验平台" class="headerlink" title="四、业内ABTest实验平台"></a>四、业内ABTest实验平台</h3><p>基于Google的分层模型,美团和微博的ABTest平台实现</p><h4 id="1、美团点评-的-Gemini"><a href="#1、美团点评-的-Gemini" class="headerlink" title="1、美团点评 的 Gemini"></a>1、美团点评 的 Gemini</h4><p><a href="https://www.csdn.net/article/2015-03-24/2824303" target="_blank" rel="noopener">https://www.csdn.net/article/2015-03-24/2824303</a></p><h4 id="2、微博的-Faraday"><a href="#2、微博的-Faraday" class="headerlink" title="2、微博的 Faraday"></a>2、微博的 Faraday</h4><p>微博广告法拉第(Faraday)全流量分层实验平台。该实验平台支持大规模广告策略并发实验,提供了多种流量均匀分流模式,全面的广告指标跟踪评估,实验效果实时反馈等。</p><p><a href="http://www.yunweipai.com/archives/19535.html" target="_blank" rel="noopener">http://www.yunweipai.com/archives/19535.html</a></p><ul><li><a href="https://zhuanlan.zhihu.com/p/25685006" target="_blank" rel="noopener">携程机票的ABTest实践</a></li><li><a href="https://yq.aliyun.com/articles/5837" target="_blank" rel="noopener">10分钟搞懂分层实验原理</a></li></ul>]]></content>
<summary type="html">
<p><em>雄兔脚扑朔,雌兔眼迷离;双兔傍地走,安能辨我是雄雌?</em></p>
<h3 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h3><h4 id="1、什么是ABTest实验"
</summary>
<category term="推荐" scheme="http://buaa0300/nanhuacoder.com/categories/%E6%8E%A8%E8%8D%90/"/>
<category term="ABTest" scheme="http://buaa0300/nanhuacoder.com/tags/ABTest/"/>
<category term="分层" scheme="http://buaa0300/nanhuacoder.com/tags/%E5%88%86%E5%B1%82/"/>
</entry>
<entry>
<title>推荐迷雾(一):再见推荐系统</title>
<link href="http://buaa0300/nanhuacoder.com/2018/11/21/Recommend01/"/>
<id>http://buaa0300/nanhuacoder.com/2018/11/21/Recommend01/</id>
<published>2018-11-21T14:55:30.000Z</published>
<updated>2019-04-18T13:59:47.000Z</updated>
<content type="html"><![CDATA[<h3 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h3><p><em>花非花,雾非雾。夜半来,天明去。来如春梦不多时,去似朝云无觅处。</em></p><p> 用白居易的诗作为《推荐迷雾》系列的开始,本人非推荐算法工程师,但是工作中也和算法打过些交道,也曾于13年粗读过项亮博士的《推荐系统实战》,这本12年出版的“旧”书如今再读起来,更多一番体会;虽然近些年,机器学习尤其是深度学习给传统推荐学习带来新的变化,但是这些新兴的推荐技术依旧有传统的推荐算法模型的影子。</p><p><img src="/nanhuacoder.com/2018/11/21/Recommend01/jianghu.jpeg" alt="江湖"></p><h3 id="一、开篇"><a href="#一、开篇" class="headerlink" title="一、开篇"></a>一、开篇</h3><ul><li>推荐产生的背景:信息过载,用户需求不确定</li><li>推荐三步骤:召回,预估和排序</li></ul><h3 id="二、推荐系统的评估指标和方法"><a href="#二、推荐系统的评估指标和方法" class="headerlink" title="二、推荐系统的评估指标和方法"></a>二、推荐系统的评估指标和方法</h3><h4 id="1、评估指标"><a href="#1、评估指标" class="headerlink" title="1、评估指标"></a>1、评估指标</h4><h5 id="1-1、评分预测的评估"><a href="#1-1、评分预测的评估" class="headerlink" title="1-1、评分预测的评估"></a>1-1、评分预测的评估</h5><ul><li><p>RMSE (均方根误差,加大预测不准物品评分的惩罚)</p></li><li><p>MAE(平均绝对误差)</p></li></ul><h5 id="1-2、TopN推荐的评估"><a href="#1-2、TopN推荐的评估" class="headerlink" title="1-2、TopN推荐的评估"></a>1-2、TopN推荐的评估</h5><ul><li>在这里用到了<strong>准确率</strong>、<strong>召回率</strong>和<strong>F-1 Score</strong>三个度量值</li><li><strong>召回率</strong>(Recall):用户消费的内容,是由推荐提供的占比</li><li><strong>准确率</strong>(Precision):推荐的内容中,用户消费的占比</li><li><strong>F-1 Score</strong> </li></ul><p>$$<br>F_1 Score = \frac{2 x recall * precision}{recall + precision}<br>$$</p><p><strong>召回率</strong>表示在原始样本的正样本中,最后被正确预测为正样本的概率;</p><figure class="highlight python"><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"><span class="function"><span class="keyword">def</span> <span class="title">PrecisionRecall</span><span class="params">(test,N)</span>:</span></span><br><span class="line">hit = <span class="number">0</span></span><br><span class="line">n_recall = <span class="number">0</span></span><br><span class="line">n_precision = <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> user,item <span class="keyword">in</span> test.items():</span><br><span class="line">rank = Recommend(user,N)</span><br><span class="line">hit += len(rank & items)</span><br><span class="line">n_recall += len(items)</span><br><span class="line">n_precision += N</span><br><span class="line"><span class="keyword">return</span> [hit/(<span class="number">1.0</span> * n_recall),hit/(<span class="number">1.0</span>*n_precision)]</span><br></pre></td></tr></table></figure><ul><li>为了全面评估TopN推荐的准确率和召回率,一般选取不同推荐列表长度N,计算一组准确率/召回率,然后画出准确率/召回率曲线。</li><li>在工程实践中,TopN推荐更合适,因为对于推荐的内容来说,预测用户会不会看,比预测用户看了内容后给多少分更重要。</li></ul><h5 id="1-3、覆盖率"><a href="#1-3、覆盖率" class="headerlink" title="1-3、覆盖率"></a>1-3、覆盖率</h5><p>推荐系统对物品长尾的发掘能力,简单的定义是推荐物品占总物品集合的比例。当然还有更好的指标来定义覆盖率。</p><ul><li>信息熵:</li><li>基尼系数:</li></ul><h5 id="1-3、其他指标"><a href="#1-3、其他指标" class="headerlink" title="1-3、其他指标"></a>1-3、其他指标</h5><ul><li>多样性、新颖性、惊喜度、信任度、实时性、健壮性,商业目标等等,具体参考《推荐系统实战》中的内容</li></ul><h4 id="2、评估方法"><a href="#2、评估方法" class="headerlink" title="2、评估方法"></a>2、评估方法</h4><ul><li><p>离线评估:速度快,不需要用户参与;在用户的历史数据上做评估,和线上真实效果有偏差;只能评估少数指标。</p></li><li><p>用户调查:主要的形式是问卷调查,成本高</p></li><li><p>在线实验:目前最普遍的做法是ABtest,目前主采用多层重叠实验设计,这些在后面的文章重点介绍</p></li></ul><h3 id="三、推荐系统架构"><a href="#三、推荐系统架构" class="headerlink" title="三、推荐系统架构"></a>三、推荐系统架构</h3><ul><li>推荐系统是产品的核心,而推荐算法仅仅是推荐系统中的一部分;以NetFlix的推荐系统架构为例,介绍经典的推荐系统架构。</li><li>在NetFlix推荐系统架构中,分为三层:Offline层(离线层)、Nearline层(近线层)和 Online层(在线层)</li></ul><h4 id="1、Offline层(离线层)"><a href="#1、Offline层(离线层)" class="headerlink" title="1、Offline层(离线层)"></a>1、Offline层(离线层)</h4><ul><li>这一层批量、周期性地<strong>抽取数据</strong>,<strong>训练模型</strong>;训练得到的模型可以用于为用户计算推荐结果。协同过滤、矩阵分解一般在这层做,Hadoop、Spark分布式计算也在这一层。</li><li>Offline阶段的推荐结果或模型在Nearline层被更新,产生最终的推荐结果,呈现给用户。</li></ul><h4 id="2、Nearline层(近线层)"><a href="#2、Nearline层(近线层)" class="headerlink" title="2、Nearline层(近线层)"></a>2、Nearline层(近线层)</h4><ul><li>Nearline要处理处理实时数据流(流计算),执行计算任务:从事件队列中获取最新的一个或少许几个用户反馈行为,将这些用户已经反馈过的物品从离线推荐结果中剔除,然后用这几个反馈行为作为样本,以小批量梯度下降的优化方法去更新融合模型的参数。</li></ul><h4 id="3、Online层(在线层)"><a href="#3、Online层(在线层)" class="headerlink" title="3、Online层(在线层)"></a>3、Online层(在线层)</h4><ul><li>用户使用App/浏览Web,消费展示内容,产生行为事件数据,如页面曝光,按钮点击等,实时被收集走,一边进入分布式文件系统中做存储,给Offline使用,一边流向Nearline的<strong>消息队列</strong>,供Nearline的<strong>流计算</strong>使用。</li><li>用户发出请求,等待推荐结果;Online层必须实施响应用户请求,要快,要有兜底,Online层处理的一般是已经预处理后的推荐结果。</li></ul><h3 id="四、推荐系统中的冷启动和EE问题"><a href="#四、推荐系统中的冷启动和EE问题" class="headerlink" title="四、推荐系统中的冷启动和EE问题"></a>四、推荐系统中的冷启动和EE问题</h3><ul><li>在推荐系统中有两个经典问题:<strong>冷启动</strong>和<strong>EE</strong>(探索和利用问题)。</li><li>Bandit算法提供了一种有效的解决办法;其中<strong>冷启动</strong>的本质是:推荐系统没有历史数据,无法预测用户偏好;可分为用户冷启动,物品冷启动和系统冷启动。<strong>EE问题</strong>是指,是选择现在不确定的一些方案,但未来可能会有高收益的方案;还是选择现在可能最佳的方案;本质是一个选择的问题。</li><li>Bandit 算法来源于历史悠久的赌博学,它要解决这样的问题:一个赌徒去摇老虎机,赌场中有的老虎机一模一样,但是每个老虎机吐钱的概率不一样,他不知道每个老虎机吐钱的概率分布是什么,那么每次该选择哪个老虎机可以做到最大化收益呢?这就是<strong>多臂赌博机问题</strong>(Multi-armed bandit problem, K-armed bandit problem, MAB)</li></ul><p><img src="/nanhuacoder.com/2018/11/21/Recommend01/mab.png" alt="MAB问题"></p><ul><li><p>假设我们已经经过一些试验,得到了当前每个老虎机的吐钱的概率,如果想要获得最大的收益,我们会一直摇哪个吐钱概率最高的老虎机,这就是Exploitation。但是,当前获得的信息并不是老虎机吐钱的真实概率,可能还有更好的老虎机吐钱概率更高,因此还需要进一步探索,这就是Exploration问题。</p></li><li><p>Bandit解决MAB或者EE问题的策略是:有策略地走一步看一步,这些策略就是Bandit算法,经典的Bandit算法分别是:<strong>朴素Bandit、汤普森采样、UCB和Epsilon贪婪算法</strong>。</p></li></ul><h4 id="1、朴素Bandit"><a href="#1、朴素Bandit" class="headerlink" title="1、朴素Bandit"></a>1、朴素Bandit</h4><p><strong>原理</strong>:先随机试若干次,计算每个臂的平均收益,一直选均值最大那个臂。</p><h4 id="2、汤普森采样-Thompson-sampling-算法"><a href="#2、汤普森采样-Thompson-sampling-算法" class="headerlink" title="2、汤普森采样(Thompson sampling)算法"></a>2、汤普森采样(Thompson sampling)算法</h4><p><strong>原理</strong>:</p><ul><li>每个臂维护一个beta(a,b)分布,每次用现有的beta分布产生一个随机数,输出随机数最大的臂(较快,随机性高)</li></ul><p><strong>beta(a,b)分布特点</strong>:</p><ul><li>a+b值越大,分布曲线就越窄,分布就越集中;</li><li>a/(a+b)值越大,分布中心越靠近1,反之越靠近0;</li></ul><p><strong>采样过程</strong>:</p><ul><li><p>假设a是用户的点击次数,b是没有得到用户的点击次数</p></li><li><p>每次取出所有候选的参数a和b,用贝塔分布产生一个随机数</p></li><li><p>随机数排序,输出最大值对应的候选</p></li><li><p>如果用户点击,对应的候选a加1,反之b加1</p></li></ul><p><strong>分析</strong>:</p><ul><li>如果一个候选被选中的次数很多(a+b很大),分布变窄,对应的分布产生的随机数基本在中心位置,接近平均收益。</li><li>如果a+b很大,a/(a+b)也很大,那么分布产生的随机数越接近1,平均收益很好,进入利用阶段;</li><li>如果a+b很小,说明候选的好坏不能确定,分布很宽,可能得到一个较大的随机数,排序时候可能被优先输出,起到了探索的目的。</li></ul><h4 id="3、UCB算法"><a href="#3、UCB算法" class="headerlink" title="3、UCB算法"></a>3、UCB算法</h4><p><strong>原理</strong>:</p><ul><li>以每个候选的平均收益作为基准线进行选择</li><li>对于每次被选择不足的给与照顾</li><li>选择倾向于那些确定收益较好的候选</li></ul><p><strong>简言之</strong>,均值越大,标准差越小,被选中的概率会越来越大 (相对慢一点,确定性高)</p><h4 id="4、Epsilon贪婪-Epsilon-Greedy-算法"><a href="#4、Epsilon贪婪-Epsilon-Greedy-算法" class="headerlink" title="4、Epsilon贪婪(Epsilon-Greedy)算法"></a>4、Epsilon贪婪(Epsilon-Greedy)算法</h4><p><strong>原理:</strong></p><ul><li>先选一个(0,1)之间较小的值,作为Epsilon,然后每次以1-epsilon的概率选取当前收益最大的臂,以epsilon的随机概率选取一个臂。(后期不需要较大探索,epsilon需要衰减)</li><li>Epsilon可以控制探索和利用的程度,Epsilon越接近0,在探索上就越保守;Epsilon越接近1,在探索上就越激进。</li></ul><p><img src="/nanhuacoder.com/2018/11/21/Recommend01/bandit-performance.png" alt="Bandit算法算法模拟试验效果"></p><p><strong>总结</strong>:UCB算法和汤普森采样算法效果更好些。</p><h3 id="五、Bandit算法的工程实现"><a href="#五、Bandit算法的工程实现" class="headerlink" title="五、Bandit算法的工程实现"></a>五、Bandit算法的工程实现</h3><p> 上面介绍的<strong>朴素Bandit、汤普森采样、UCB和Epsilon贪婪算法</strong>都是经典的Bandit算法,在工程中很少使用;实际中,我们采用的是上下文Bandit算法,比较常见的是LinUCB算法和COFIBA算法</p><h4 id="1、LinUCB"><a href="#1、LinUCB" class="headerlink" title="1、LinUCB"></a>1、LinUCB</h4><p><strong>概述</strong>:</p><ul><li><p>传统的 Bandit 算法并没有考虑臂的特征信息,也就是说并没有考虑上下文信息,而yahoo在2010年提出的,一种结合上下文的 Bandit算法——LinUCB (linear UCB)算法。</p></li><li><p>LinUCB 算法可以将当前用户的特征、物品特征构成所有的相关特征,然后根据每个臂维护的特征系数,计算出预估收益。由于加入了特征,所以收敛速度比 UCB 更快。</p></li><li>LinUCB的不足:同时处理的候选臂数量不能太多,不超过几百个最佳。因为每一次要计算每一个候选臂的期望收益和置信区间,一旦候选太多,计算代价将不可接受。其实这也是所有的 Bandit 算法的缺点。</li></ul><p><strong>原理:</strong></p><ul><li><p>LinUCB 假设一个物品推送给用户之后,获得的收益与相关特征呈<strong>线性关系</strong>,这里的相关特征就是指上下文信息。LinUCB 有两个版本:<strong>Disjoint</strong> 和 <strong>Hybrid</strong>,Disjoint 表示不同臂之间的不相关,也就是说参数不共享,Hybrid 表示臂之间共享一些参数。</p></li><li><p>Disjoint 模型:假设每个臂包含一个物品,我们在每一次选择时,用户与物品的的特征构成了上下文信息,表示为 x,维度为 d,每个臂维护了一个 d 维的表示特征系数的向量 θ,使用 c 表示本次选择的收益,如果用户点击了就为 1,否则为 0。我们假定:</p></li></ul><p> 根据p’ + ∆来选择合适的臂。p’的计算基于有监督的学习方法。我们为每个老虎机维护一个特征向量D,同时上下文特征我们写作θ,然后通过收集的反馈进行有监督学习:</p><ul><li><p>加入特征信息,用User和Item的特征预估回报及其置信区间,选择置信区间上界最大的Item推荐,观察回报后更新线性关系的参数,以此达到试验学习的目的。</p></li><li><p>岭回归的求解,岭回归适合样本数少于特征的数据集。</p></li><li><p>参考:<a href="http://www.naodongopen.com/908.html" target="_blank" rel="noopener">结合上下文信息的Bandit算法—LinUCB算法</a></p></li></ul><h4 id="2、COFIBA"><a href="#2、COFIBA" class="headerlink" title="2、COFIBA"></a>2、COFIBA</h4><ul><li>2016年提出,COFIBA算法的不同有两个:<ol><li>基于用户聚类挑选最佳的Item(相似用户集体决策的Bandit)。</li><li>基于用户的反馈情况调整User和Item的聚类(协同过滤部分)。</li></ol></li><li>在时刻t,用户来访问推荐系统,推荐系统需要从已有的候选池子中挑一个最佳的物品推荐给他,然后观察他的反馈,用观察到的反馈来更新挑选策略。 这里的每个物品都有一个特征向量,所以这里的Bandit算法是context相关的。 这里依然是用岭回归去拟合用户的权重向量,用于预测用户对每个物品的可能反馈(payoff),这一点和linUCB算法是一样的</li></ul><ul><li><p>bandit结合协同过滤。</p><p><a href="https://blog.csdn.net/heyc861221/article/details/80129310" target="_blank" rel="noopener">https://blog.csdn.net/heyc861221/article/details/80129310</a></p></li></ul><h4 id="3、其他"><a href="#3、其他" class="headerlink" title="3、其他"></a>3、其他</h4><ul><li>Exploit-Explore这一对矛盾一直客观存在,Bandit算法是公认的一种比较好的解决EE问题的方案。但解决Explore,势必就是要冒险,势必要走向未知,而这显然就是会伤害用户体验的:明知道用户肯定喜欢A,你还偏偏以某个小概率给推荐非A。</li><li>实际上,很少有公司会采用这些理性的办法做Explore,反而更愿意用一些盲目主观的方式。究其原因,可能是因为:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1、互联网产品生命周期短,而Explore又是为了提升长期利益的,所以没有动力做;</span><br><span class="line">2、用户使用互联网产品时间越来越碎片化,Explore的时间长,难以体现出Explore 的价值;</span><br><span class="line">3、同质化互联网产品多,用户选择多,稍有不慎,用户用脚投票,分分钟弃你于不顾;</span><br><span class="line">4、已经成规模的平台,红利杠杠的,其实是没有动力做Explore的。</span><br></pre></td></tr></table></figure><ul><li>所以做Explore要精心设计,必须保证质量。</li></ul><h3 id="TODO"><a href="#TODO" class="headerlink" title="TODO"></a>TODO</h3><ul><li><p>bandit添加对应的源码实现</p></li><li><p>ABTest的分层实验设计</p></li><li>召回中的协同过滤和隐语义模型, 矩阵分解</li><li>CTR预估的进化,特征工程 + LR -> GBDT + LR -> FM ->FFM -> DeepFM/Wide&Deep/…</li><li>排序怎么做</li></ul>]]></content>
<summary type="html">
<h3 id="序言"><a href="#序言" class="headerlink" title="序言"></a>序言</h3><p><em>花非花,雾非雾。夜半来,天明去。来如春梦不多时,去似朝云无觅处。</em></p>
<p> 用白居易的诗作为《推荐迷雾》系列
</summary>
<category term="推荐" scheme="http://buaa0300/nanhuacoder.com/categories/%E6%8E%A8%E8%8D%90/"/>
<category term="推荐" scheme="http://buaa0300/nanhuacoder.com/tags/%E6%8E%A8%E8%8D%90/"/>
<category term="冷启动" scheme="http://buaa0300/nanhuacoder.com/tags/%E5%86%B7%E5%90%AF%E5%8A%A8/"/>
<category term="EE" scheme="http://buaa0300/nanhuacoder.com/tags/EE/"/>
</entry>
<entry>
<title>机器学习中的树模型</title>
<link href="http://buaa0300/nanhuacoder.com/2018/11/20/ML02-DTree/"/>
<id>http://buaa0300/nanhuacoder.com/2018/11/20/ML02-DTree/</id>
<published>2018-11-20T15:33:16.000Z</published>
<updated>2019-04-18T13:58:48.000Z</updated>
<content type="html"><![CDATA[<h3 id="一、开篇"><a href="#一、开篇" class="headerlink" title="一、开篇"></a>一、开篇</h3><h4 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a>1、概述</h4><ul><li>在数据结构中,有树这种结构;在机器学习中,有决策树;咋一看,感觉是一回事,其实不然;</li><li><p>数据结构的树关注的事查找,插入,删除的效率,二叉树,平衡二叉树都是为了解决这些效率而产生的;</p></li><li><p>机器学习中的决策树关注的是,如何找到最佳分解节点,<strong>ID3算法</strong>(按最大信息增益划分),<strong>C4.5</strong>(按信息增益比划分),<strong>CART算法</strong>(按最小基尼指数划分)都是为了达到最佳分裂的效果。</p></li><li>最大信息增益会倾向于<strong>可取值较多</strong>的特征,最大信息增益比会倾向于<strong>可取值较少的特征</strong>。</li></ul><h4 id="2、决策树和集成学习"><a href="#2、决策树和集成学习" class="headerlink" title="2、决策树和集成学习"></a>2、决策树和集成学习</h4><ul><li>决策树模型很简单,简单到即使你不懂机器学习,也能很快了解他,可能有人会说,这么简单的模型有什么用。哈哈,数学的伟大,在于复杂的问题简单化,深度学习的强大在于利用将简单的神经网络不断加深;决策树虽然简单,但是很多个决策树在一起,就足够让很多浅层机器学习算法忘而却步。</li><li><p>这个将许多决策树整合一起的方式就是集成学习,在集成学习中,决策树是个体学习器,当然个体学习器可以是别的弱学习器。</p></li><li><p>而根据个体学习器生成方式的不同,目前集成学习方法大致可分为两大类,即个体学习器间存在<strong>强依赖关系</strong>、必须串行生成的序列化方法,以及个体学习器间<strong>不存在强依赖关系</strong>、可同时生成的并行化方法;前者的代表是<strong>Boosting </strong> 和 <strong>梯度提升树</strong>(GBDT),后者的代表是和<strong>Bagging</strong>和 <strong>随机森林</strong>(Random Forest)</p></li></ul><h3 id="二、决策树"><a href="#二、决策树" class="headerlink" title="二、决策树"></a>二、决策树</h3><h4 id="1、决策树中需关注问题"><a href="#1、决策树中需关注问题" class="headerlink" title="1、决策树中需关注问题"></a>1、决策树中需关注问题</h4><ul><li><p>一棵树是如何构建的?建树过程中,树分裂节点时,如何选出最优的属性作为分裂节点。</p></li><li><p>如何用树的减枝来避免过拟合问题。</p></li><li><p>对于含有空值的数据,如何构建树。</p></li><li><p>构建树可能存在的问题,过拟合问题,如何解决</p></li></ul><h4 id="2、如何构建一棵树"><a href="#2、如何构建一棵树" class="headerlink" title="2、如何构建一棵树"></a>2、如何构建一棵树</h4><p>简言之:<strong>选择最优划分属性作为分裂结点,使得分支结点中所包含的样本尽可能属于同一类</strong>。树的生成算法有三种:ID3、C4.5和CART.</p><h5 id="1、ID3"><a href="#1、ID3" class="headerlink" title="1、ID3"></a>1、ID3</h5><p>选择<strong>最大信息增益(Information Gain)</strong>规则去寻找最优分裂节点</p><h5 id="1-1、基础概念"><a href="#1-1、基础概念" class="headerlink" title="1-1、基础概念"></a>1-1、基础概念</h5><ul><li><p>信息熵(Information entropy):表示随机变量不确定性的度量;<strong>熵越大,随机变量的不确定性就越大</strong>;(我们希望分类后的结果熵越小越好)。</p></li><li><p>信息增益(Information gain):表示因特征X的信息而使得类Y信息不确定性减少的程度。(当然是越大越好);<br>$$<br>g(D,A) = H(D) - H(D|A)<br>$$</p></li></ul><p>在划分过程中,找到信息增益最大的特征将样本根据此特征划分不同的结点中,新的结点中继续划分。</p><h5 id="1-2、不足"><a href="#1-2、不足" class="headerlink" title="1-2、不足"></a>1-2、不足</h5><ul><li>信息增益反应的是:给点条件后,不确定性减少的程度,<strong>特征取值越多,意味着确定性越高,也就是条件熵越小,信息增益越大。</strong>这就造成信息增益对<strong>可取值较多</strong>的属性有所偏好。</li><li>ID3只能处理离散型变量,只能处理分类任务</li><li>对样本特征缺失值比较敏感</li></ul><h5 id="2、C4-5"><a href="#2、C4-5" class="headerlink" title="2、C4.5"></a>2、C4.5</h5><p>选择<strong>最大信息增益比(Information Gain Ratio)</strong>规则去寻找最优分裂节点</p><h5 id="2-1、基础概念"><a href="#2-1、基础概念" class="headerlink" title="2-1、基础概念"></a>2-1、基础概念</h5><ul><li><strong>信息增益比(Information Gain Ratio)</strong>: 特征A对训练数据集D的信息增益比为其 信息增益与训练数据集D关于特征A的值的熵之比。<br>$$<br>g_R(D,A) =\frac{g(D,A)}{H_A(D)}<br>$$</li></ul><p>在划分过程中,找到信息增益比最大的特征将样本根据此特征划分不同的结点中,新的结点中继续划分。</p><h5 id="2-2、不足"><a href="#2-2、不足" class="headerlink" title="2-2、不足"></a>2-2、不足</h5><ul><li>C4.5 对ID3做了优化,使用信息增益比在一定程度上对取值较多的特征进行了惩罚,避免ID3出现的过拟合特性,提升了决策树的泛化能力。</li><li>C4.5 能处理连续型变量和离散型变量,但是只能处理分类任务。</li></ul><h5 id="3、CART"><a href="#3、CART" class="headerlink" title="3、CART"></a>3、CART</h5><p>选择<strong>最小基尼指数(Gini index)</strong>规则去寻找最优分裂节点</p><h5 id="3-1、基础概念"><a href="#3-1、基础概念" class="headerlink" title="3-1、基础概念"></a>3-1、基础概念</h5><ul><li>基尼指数:表示数据的纯度<br>$$<br>Cini(D) = 1 - \sum_{k=1}^n(\frac{|C_k|}{|D|})^2<br>$$</li></ul><p>利用基尼指数最小选择最优分裂点,采用二元切割法</p><h5 id="3-2、特点"><a href="#3-2、特点" class="headerlink" title="3-2、特点"></a>3-2、特点</h5><ul><li>不仅能处理分类任务,还能处理回归任务</li><li>不仅能处理离散型变量,还能处理连续型变量</li><li>能够处理样本特征数据缺失的情况</li></ul><h4 id="3、树的剪枝"><a href="#3、树的剪枝" class="headerlink" title="3、树的剪枝"></a>3、树的剪枝</h4><h5 id="3-1、预剪枝"><a href="#3-1、预剪枝" class="headerlink" title="3-1、预剪枝"></a>3-1、预剪枝</h5><p>阈值、</p><h5 id="3-2、后剪枝"><a href="#3-2、后剪枝" class="headerlink" title="3-2、后剪枝"></a>3-2、后剪枝</h5><h3 id="三、梯度提升树-和-随机森林"><a href="#三、梯度提升树-和-随机森林" class="headerlink" title="三、梯度提升树 和 随机森林"></a>三、梯度提升树 和 随机森林</h3><ul><li><p>梯度提升树,GBDT,特征组合的一把好手</p></li><li><p>随机森林 RF</p></li><li><p>基于决策树实现的GBDT、RF(随机森林)、Xgboost 和 lightGBM都在数据竞赛和工程中都大展身手。</p></li><li><p><a href="https://github.com/e-snail/understanding_machine_learning/tree/master/3_decision_tree" target="_blank" rel="noopener">https://github.com/e-snail/understanding_machine_learning/tree/master/3_decision_tree</a></p></li><li><p><a href="http://sklearn.apachecn.org/cn/0.19.0/modules/tree.html" target="_blank" rel="noopener">决策树API</a></p></li></ul>]]></content>
<summary type="html">
<h3 id="一、开篇"><a href="#一、开篇" class="headerlink" title="一、开篇"></a>一、开篇</h3><h4 id="1、概述"><a href="#1、概述" class="headerlink" title="1、概述"></a
</summary>
<category term="机器学习" scheme="http://buaa0300/nanhuacoder.com/categories/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
<category term="ML" scheme="http://buaa0300/nanhuacoder.com/tags/ML/"/>
<category term="决策树" scheme="http://buaa0300/nanhuacoder.com/tags/%E5%86%B3%E7%AD%96%E6%A0%91/"/>
</entry>
<entry>
<title>机器学习杂谈</title>
<link href="http://buaa0300/nanhuacoder.com/2018/11/17/ML01/"/>
<id>http://buaa0300/nanhuacoder.com/2018/11/17/ML01/</id>
<published>2018-11-17T14:20:28.000Z</published>
<updated>2019-04-18T13:58:32.000Z</updated>
<content type="html"><![CDATA[<p><strong>高能预警</strong>:本人非算法开发,以下是我的一些浅见。</p><h3 id="一、开篇"><a href="#一、开篇" class="headerlink" title="一、开篇"></a>一、开篇</h3><p>自有机器学习以来,算法模型中就有了区分;分为监督学习,无监督学习,半监督学习和强化学习等。</p><h4 id="1、监督学习"><a href="#1、监督学习" class="headerlink" title="1、监督学习"></a>1、监督学习</h4><ul><li><p>机器学习中<strong>大部分任务</strong>都是监督学习任务;在监督学习中,分类和回归是其两大主题。分类中名声最大的是:<strong>逻辑回归</strong>(Logistic regression)、<strong>朴素贝叶斯</strong>(Naive Bayes) 和 <strong>支持向量机</strong>(Support Vector Machine,SVM) 。</p></li><li><p>在工业界曾经有<strong>一招LR打天下</strong>的”传说”,这里的LR就是逻辑回归;早些年做CTR预估时候,<strong>人工特征海洋 + LR</strong>用得非常多,很多算法工程师大部分时间埋头搞特征工程;</p></li><li><strong>朴素贝叶斯</strong>(Naive Bayes)是个开挂的存在,它将<strong>贝叶斯原理</strong>应用到机器学习中,而后机器学习中贝叶斯学派声明鹊起,与旧贵族统计学派在机器学习中鼎足而立;它强假设输入的数据的特征都是独立的,在文本分类中表现出很好的效果,可谓是入门nlp都必须了解的算法之一了。</li><li><strong>支持向量机</strong>(Support Vector Machine,SVM) 当之无愧是监督学习中的无冕之王,自上世纪90年代诞生之后,在机器学习界掀起“腥风血雨”,它利用核技巧,在属于两个不同类别的两组数据之间找到良好的分类边界,将线性不可分问题转为高纬空间的线性可分问题,借此达到了分类的目的。因为SVM的存在,将<strong>多层神经网络</strong>的研究打进冷宫;多层神经网络是深度学习的前身。直到2012年,因为深度学习在ImageSet竞赛中的完胜,神经网络的研究才再次复苏,直至形成今天的野火燎原之势。</li><li>监督学习中另一个主题是回归,最基础的的当然是<strong>线性回归</strong>(Linear regression),模型虽然简单,但是预测个房价,股票价格这种高大上的事情也还是有着不错的表现。</li><li>非监督学习比较尴尬,在工业界,更多起到打辅助的左右;有聚类,降维和关联规则三大主题,聚类中用的较多反而是简单的K-means,PCA和SVD用来做数据降维,关联规则那块好像在推荐系统用的稍微多些。</li></ul><h4 id="2、决策树和集成学习"><a href="#2、决策树和集成学习" class="headerlink" title="2、决策树和集成学习"></a>2、决策树和集成学习</h4><ul><li>决策树是监督学习中比较特殊的模型,说他特殊,是因为它不像LR、SVM和NB直接上手,大家喜欢使用将决策树作为集成学习的基础学习器,比较著名的是<strong>随机森林</strong>(Random Forest)、<strong>梯度提升树</strong>(GBDT),决策树的实现经历了ID3, C4.5,CART三个阶段,在随机森林、梯度提升树里面用的是CART这个分类回归树。从CART的名字也看出来,不仅仅可以做回归,也能做分类。粗暴理解就是,分类时候好多CART投票分类结果,回归时候,取CART预测值得平均数。</li><li>GBDT有个超级经典的实现,XgBoost。在Kaggle比赛中,XgBoost近乎霸主级别的存在;江湖有传言,图片图像等机器学习问题用Keras(深度学习库),浅层机器学习用XgBoost。</li></ul><h4 id="3、神经网络和深度学习的渊源"><a href="#3、神经网络和深度学习的渊源" class="headerlink" title="3、神经网络和深度学习的渊源"></a>3、神经网络和深度学习的渊源</h4><ul><li>我认为,没有神经网络,就没有深度学习;虽然业界有说法,不用神经网络就也能实现深度学习;但是神经网络真的真的很厉害,虽然很长一段时间不被看好,因为两层的神经网络连个异或都处理不了,的确有理由被看不上;但是随着反向传播算法发现,神经网络层数不断加深,在工业界发挥牛逼闪闪的光芒。</li><li>你可能想不到,权重,阈值,激活函数这些简简单单的东西,怎么能解决计算机视觉、语音识别这种非常难的问题,这些问题连当年风光无限的SVM都败下阵来。</li></ul><h4 id="4、特征工程"><a href="#4、特征工程" class="headerlink" title="4、特征工程"></a>4、特征工程</h4><ul><li><p>有句老话:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。在传统机器学习领域,做好特征工程很重要;工业界有句老话,70%多的时间在做特征工程;</p></li><li><p>比较尴尬的是,市面上多是讲解机器学习算法模型的书,讲解算法和模型的奥妙是他们的主题,提供的试验数据也都是已经处理好的,并不需要特征工程处理,但是实际工程中不是这样。特征工程的处理依赖领域知识和经验依旧是主流,但是业界很多项目处于安全、隐私等考虑,不会透露底层的特征工程的处理。</p></li></ul><h4 id="5、特征工程-amp-深度学习"><a href="#5、特征工程-amp-深度学习" class="headerlink" title="5、特征工程 & 深度学习"></a>5、特征工程 & 深度学习</h4><ul><li>深度学习有自动获取特征的能力,可以对输入的低阶特征进行组合、变换,得到高阶特征,但是这个能力只是对于某些领域(如图像、语音)有比较好的效果。</li><li>在其他领域,如自然语言处理中,输入的字或词都是离散、稀疏的值,不像图片一样是连续、稠密的。输入原始数据进行组合、变换得到的高阶特征并不是那么有效。而且有的语义并不来自数据,而来自人们的先验知识,所以利用先验知识构造的特征是很有帮助的。</li><li>总的来说,在深度学习中,特征工程仍然适用;神经网络能对特征自动进行排列组合,所以只要输入一阶特征就行,省去了手动构造高阶特征的工作量。</li></ul><h4 id="6、模型评估"><a href="#6、模型评估" class="headerlink" title="6、模型评估"></a>6、模型评估</h4><ul><li>模型评估很重要,是向别人证明你的模型怎么好的依据,我也就知道过拟合、欠拟合、交差验证、ROC、分类中查全率,查准率这些概念,没有个比较整体的认识,后面抽时间了解后再来补充。</li></ul><h3 id="二、常见的监督学习算法模型"><a href="#二、常见的监督学习算法模型" class="headerlink" title="二、常见的监督学习算法模型"></a>二、常见的监督学习算法模型</h3><p>哈哈,先留坑,后面慢慢填</p><h4 id="1、LR(逻辑回归)"><a href="#1、LR(逻辑回归)" class="headerlink" title="1、LR(逻辑回归)"></a>1、LR(逻辑回归)</h4><ul><li>线性模型的基础上增加了sigmoid函数,处理二分类问题,广义的线性模型,在CTR预估中有大作用</li></ul><h4 id="2、SVM-支持向量机"><a href="#2、SVM-支持向量机" class="headerlink" title="2、SVM(支持向量机)"></a>2、SVM(支持向量机)</h4><ul><li><p>可以处理非线性问题,深度学习未火之前,是学术界和工业界的热点,</p><p>数据规模较小时,能够构建出数据间的非线性关系,</p><p>1、SVM的原始公式是如何由实际问题产生,算法的灵魂</p><p>2、SVM原始问题到对偶问题的数学推导公式</p></li></ul><h4 id="3、NB-朴素贝叶斯"><a href="#3、NB-朴素贝叶斯" class="headerlink" title="3、NB(朴素贝叶斯)"></a>3、NB(朴素贝叶斯)</h4><ul><li>基于贝叶斯定理的一组有监督学习算法,<strong>朴素</strong>:“简单”地假设每对特征之间相互独立;</li><li>在很多实际情况下,朴素贝叶斯工作得很好,特别是<strong>文档分类</strong>和<strong>垃圾邮件过滤</strong>。这些工作都要求 一个小的训练集来估计必需参数。</li></ul><p><a href="http://sklearn.apachecn.org/cn/0.19.0/modules/naive_bayes.html" target="_blank" rel="noopener">http://sklearn.apachecn.org/cn/0.19.0/modules/naive_bayes.html</a></p>]]></content>
<summary type="html">
<p><strong>高能预警</strong>:本人非算法开发,以下是我的一些浅见。</p>
<h3 id="一、开篇"><a href="#一、开篇" class="headerlink" title="一、开篇"></a>一、开篇</h3><p>自有机器学习以来,算法模型中
</summary>
<category term="机器学习" scheme="http://buaa0300/nanhuacoder.com/categories/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
<category term="ML" scheme="http://buaa0300/nanhuacoder.com/tags/ML/"/>
</entry>
<entry>
<title>NSURLProtocol基础</title>
<link href="http://buaa0300/nanhuacoder.com/2018/04/15/iOS-NSURLProtocol/"/>
<id>http://buaa0300/nanhuacoder.com/2018/04/15/iOS-NSURLProtocol/</id>
<published>2018-04-15T07:40:11.000Z</published>
<updated>2019-04-18T13:56:54.000Z</updated>
<content type="html"><![CDATA[<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、URL-Loading-System"><a href="#1、URL-Loading-System" class="headerlink" title="1、URL Loading System"></a>1、URL Loading System</h5><p><img src="https://upload-images.jianshu.io/upload_images/201701-7317567c713d851f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620" alt="URL-loading-system.png"></p><ul><li><strong>URL Loading System</strong> 是一系列用来访问通过 <strong>URL 来定位的资源的类和协议</strong>。这项技术的核心在于基于 <code>NSURL</code> 这个类来访问资源,除了加载 URL 的类 <code>NSURLSession</code> 之外,我们把其他相关辅助类分为 5 类:</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">协议支持(protocol support)</span><br><span class="line">认证和证书(authentication and credentials)</span><br><span class="line">Cookie 存储(Cookie storage)</span><br><span class="line">请求配置(configuration management)</span><br><span class="line">缓存管理(cache management)</span><br></pre></td></tr></table></figure><ul><li>更多可以参考:<a href="https://juejin.im/post/5a69f8366fb9a01cb42c90bc" target="_blank" rel="noopener">URL Loading System 概览</a></li></ul><h5 id="2、NSURLProtocol"><a href="#2、NSURLProtocol" class="headerlink" title="2、NSURLProtocol"></a>2、NSURLProtocol</h5><ul><li><p>NSURLProtocol是<strong>URL Loading System</strong>的重要组成部分;NSURLProtocol能拦截所有基于URL Loading System的网络请求;NSURLProtocol是一个抽象类。使用NSURLProtocol时候,需要创建它的子类。</p></li><li><p>NSURLProtocol可以拦截的网络请求包括NSURLSession,NSURLConnection以及UIWebVIew。</p></li><li><p>基于CFNetwork的网络请求,以及WKWebView的请求是无法拦截的。</p></li><li><p>现在主流的iOS网络库,例如AFNetworking,Alamofire等网络库都是基于NSURLSession的,所以这些网络库的网络请求都可以被NSURLProtocol所拦截。</p></li><li><p>至于还有一些年代比较久远的网络库,例如ASIHTTPRequest,MKNetwokit等网路库都是基于CFNetwork的,所以这些网络库的网络请求无法被NSURLProtocol拦截。</p></li></ul><h4 id="二、使用-NSURLProtocol"><a href="#二、使用-NSURLProtocol" class="headerlink" title="二、使用 NSURLProtocol"></a>二、使用 NSURLProtocol</h4><p>定义NSURLProtocol的子类,然后在子类(MYURLProtocol)中处理好<code>注册—>拦截—>转发—>回调—>结束</code></p><h5 id="1、注册"><a href="#1、注册" class="headerlink" title="1、注册"></a>1、注册</h5><p>对于基于NSURLConnection或者使用[NSURLSession sharedSession]创建的网络请求,调用registerClass方法即可。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[NSURLProtocol registerClass:[NSClassFromString(@"MYURLProtocol") class]];</span><br></pre></td></tr></table></figure><p>对于基于NSURLSession的网络请求,需要通过配置NSURLSessionConfiguration对象的protocolClasses属性。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];</span><br><span class="line">sessionConfiguration.protocolClasses = @[[NSClassFromString(@"MYURLProtocol") class]];</span><br></pre></td></tr></table></figure><h5 id="2、拦截"><a href="#2、拦截" class="headerlink" title="2、拦截"></a>2、拦截</h5><ul><li>NSURLProtocol可以在这个方法中拦截到网络请求,比如过滤掉(不处理)白名单的URL请求,比如只允许<code>http/https</code>请求。</li><li>请求拦截<code>Code Demo</code></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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></pre></td><td class="code"><pre><span class="line">//利用MYHTTPIdentifier避免 canInitWithRequest 和 canonicalRequestForRequest 出现死循环</span><br><span class="line">static NSString * const MYHTTPIdentifier = @"MYHTTPIdentifier";</span><br><span class="line"></span><br><span class="line">//每一个请求的时候都会调用这个方法,在这个方法里面判断这个请求是否需要被处理拦截,</span><br><span class="line">//如果返回YES就代表这个request需要被处理,反之就是不需要被处理。</span><br><span class="line">+ (BOOL)canInitWithRequest:(NSURLRequest *)request {</span><br><span class="line">if ([NSURLProtocol propertyForKey:MYHTTPIdentifier inRequest:request] ) {</span><br><span class="line"> return NO;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> NSString *scheme = [[request.URL scheme] lowercaseString];</span><br><span class="line"> if ([whiteUrlList contains:request.URL.absoluteString];){</span><br><span class="line"> return NO;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> if ([scheme isEqual:@"http"] || [scheme isEqual:@"https"]) {</span><br><span class="line"> return YES;</span><br><span class="line"> }</span><br><span class="line"> return NO;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">//返回规范的request的对象,我们可以对request进行处理。例如修改头部信息等。</span><br><span class="line">+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {</span><br><span class="line"> </span><br><span class="line"> NSMutableURLRequest *mutableReqeust = [request mutableCopy];</span><br><span class="line"> //修改头部信息</span><br><span class="line"> NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy];</span><br><span class="line"> [headers setObject:@"ttf" forKey:@"i am ttf"];</span><br><span class="line"> request.allHTTPHeaderFields = headers;</span><br><span class="line"> //</span><br><span class="line"> [NSURLProtocol setProperty:@YES</span><br><span class="line"> forKey:MYHTTPIdentifier</span><br><span class="line"> inRequest:mutableReqeust];</span><br><span class="line"> return [mutableReqeust copy];</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="3、转发"><a href="#3、转发" class="headerlink" title="3、转发"></a>3、转发</h5><ul><li>在拦截到网络请求,并且对网络请求进行定制处理以后。我们需要将网络请求重新发送出去,核心方法startLoading。</li><li>在该方法中,我们把处理过的request重新发送出去。至于发送的形式,可以是基于NSURLSession。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">- (void)startLoading {</span><br><span class="line"> //</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="4、回调"><a href="#4、回调" class="headerlink" title="4、回调"></a>4、回调</h5><ul><li>在拦截了请求后,不能影响到原来网络请求的逻辑。所以将网络请求转发出去以后,当收到网络请求的返回后,要再将返回值返回给原来发送网络请求的地方。需要用到以下五个方法</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">[self.client URLProtocol:self didFailWithError:error];</span><br><span class="line"></span><br><span class="line">[self.client URLProtocolDidFinishLoading:self];</span><br><span class="line"></span><br><span class="line">[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];</span><br><span class="line"></span><br><span class="line">[self.client URLProtocol:self didLoadData:data];</span><br><span class="line"></span><br><span class="line">[[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];</span><br></pre></td></tr></table></figure><ul><li>假设我们在转发过程中是使用NSURLSession发送的网络请求,那么在NSURLSession的回调方法中,我们做相应的处理即可。并且我们也可以对这些返回,进行定制化处理。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {</span><br><span class="line"> if (error) {</span><br><span class="line"> [self.client URLProtocol:self didFailWithError:error];</span><br><span class="line"> } else {</span><br><span class="line"> [self.client URLProtocolDidFinishLoading:self];</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {</span><br><span class="line"> </span><br><span class="line"> [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];</span><br><span class="line"></span><br><span class="line"> completionHandler(NSURLSessionResponseAllow);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {</span><br><span class="line"> [self.client URLProtocol:self didLoadData:data];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {</span><br><span class="line"> if (response != nil){</span><br><span class="line"> [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="5、结束"><a href="#5、结束" class="headerlink" title="5、结束"></a>5、结束</h5><ul><li>在一个网络请求完全结束以后,NSURLProtocol回调用到stopLoading方法,在此方法中,完成在结束网络请求的操作。</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">- (void)stopLoading {</span><br><span class="line"> //</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="三、NSURLProtocol的应用"><a href="#三、NSURLProtocol的应用" class="headerlink" title="三、NSURLProtocol的应用"></a>三、NSURLProtocol的应用</h4><h5 id="1、Mock网络请求"><a href="#1、Mock网络请求" class="headerlink" title="1、Mock网络请求"></a>1、Mock网络请求</h5><ul><li>可以参考<a href="https://link.jianshu.com/?t=https://github.com/AliSoftware/OHHTTPStubs" target="_blank" rel="noopener">OHHTTPStubs</a></li></ul><h5 id="2、网络监控和相关数据统计"><a href="#2、网络监控和相关数据统计" class="headerlink" title="2、网络监控和相关数据统计"></a>2、网络监控和相关数据统计</h5><ul><li>APM工具,如Bugly</li></ul><h5 id="3、其他"><a href="#3、其他" class="headerlink" title="3、其他"></a>3、其他</h5><ul><li>URL重定向</li><li>实现HTTPDNS</li></ul><h4 id="四、NSURLProtocol的坑"><a href="#四、NSURLProtocol的坑" class="headerlink" title="四、NSURLProtocol的坑"></a>四、NSURLProtocol的坑</h4><h5 id="1、死循环"><a href="#1、死循环" class="headerlink" title="1、死循环"></a>1、死循环</h5><p><strong>原因</strong>:偶尔会出现<strong>canInitWithRequest方法多次调用的</strong>情况,不注意处理会发生死循环的问题,需要在canInitWithRequest方法中会判断是否拦截过请求。</p><p><strong>解决办法</strong>:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">+ (BOOL)canInitWithRequest:(NSURLRequest *)request {</span><br><span class="line"> if ([NSURLProtocol propertyForKey:protocolKey inRequest:request]) { //句1</span><br><span class="line"> return NO;</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">+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {</span><br><span class="line"> </span><br><span class="line"> //...</span><br><span class="line"> [NSURLProtocol setProperty:@(YES) forKey:protocolKey inRequest:request]; //句2</span><br><span class="line"> //...</span><br><span class="line"> return [request copy];</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">//句1和句2是为了防止死循环的,主要用于对request进行标记,如果被标记了,就不再次进行处理了,如果没有标记过就要进行处理。</span><br></pre></td></tr></table></figure><h5 id="2、多NSURLProtocol嵌套使用"><a href="#2、多NSURLProtocol嵌套使用" class="headerlink" title="2、多NSURLProtocol嵌套使用"></a>2、多NSURLProtocol嵌套使用</h5><ul><li><p>若一个项目中存在多个NSURLProtocol,那么NSURLProtocol的拦截顺序跟注册的方式和顺序有关,对于使用registerClass方法注册的情况:<strong>多个NSURLProtocol拦截顺序为注册顺序的反序,即后注册的的NSURLProtocol先拦截。</strong></p></li><li><p>对于通过配置<strong>NSURLSessionConfiguration</strong>对象的protocolClasses属性来注册的情况:<strong>protocolClasses这个数组里只有第一个NSURLProtocol会起作用</strong>,所以为了保证自定义的protocolClass有效,采用的办法是:<strong>把自己的NSURLProtocol插入到protocolClasses的第一个,进行拦截。拦截完成之后,又进行移除</strong>。</p></li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><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">+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig {</span><br><span class="line"> // Runtime check to make sure the API is available on this version</span><br><span class="line"> if ([sessionConfig respondsToSelector:@selector(protocolClasses)]</span><br><span class="line"> && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)]){</span><br><span class="line"> //</span><br><span class="line"> NSMutableArray *urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];</span><br><span class="line"> </span><br><span class="line"> Class protoCls = OHHTTPStubsProtocol.class;</span><br><span class="line"> if (enable && ![urlProtocolClasses containsObject:protoCls]){</span><br><span class="line"> [urlProtocolClasses insertObject:protoCls atIndex:0];</span><br><span class="line"> }else if (!enable && [urlProtocolClasses containsObject:protoCls]){</span><br><span class="line"> [urlProtocolClasses removeObject:protoCls];</span><br><span class="line"> }</span><br><span class="line"> sessionConfig.protocolClasses = urlProtocolClasses;</span><br><span class="line"> }else{</span><br><span class="line">NSLog("")</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h5 id="3、NSURLProtocol不能拦截WKWebView中请求"><a href="#3、NSURLProtocol不能拦截WKWebView中请求" class="headerlink" title="3、NSURLProtocol不能拦截WKWebView中请求"></a>3、NSURLProtocol不能拦截WKWebView中请求</h5><ul><li>WKWebView中的请求走得是WebKit内核,不走在App进程中,一般情况下,在App中不能通过NSURLProtocol拦截请求;</li><li>实践中,通过WebKit2的私有方法,让WKWebView的请求被NSURLProtocol拦截,但是有post请求body数据被清空的坑。</li></ul><h5 id="4、NSURLSession的坑"><a href="#4、NSURLSession的坑" class="headerlink" title="4、NSURLSession的坑"></a>4、NSURLSession的坑</h5><p>在NSURLProtocol中使用NSURLSession有很多莫名其妙的问题,基本上都是系统的bug,比较明显的就是:</p><ul><li>拦截到的Request中的HTTPBody为nil;</li><li>startLoading在某些特殊情况会出现死锁;</li><li>关于注册registerClass方法只适用于sharedSession创建的网络请求;</li></ul><h4 id="五、参考"><a href="#五、参考" class="headerlink" title="五、参考"></a>五、参考</h4><p><a href="https://www.jianshu.com/p/02781c0bbca9" target="_blank" rel="noopener">NSURLProtocol全攻略</a></p><p><a href="https://www.jianshu.com/p/af94f2e8d4b0" target="_blank" rel="noopener">黑魔法NSURLProtocol</a></p><p><a href="https://github.com/draveness/analyze/blob/master/contents/OHHTTPStubs/iOS%20%E5%BC%80%E5%8F%91%E4%B8%AD%E4%BD%BF%E7%94%A8%20NSURLProtocol%20%E6%8B%A6%E6%88%AA%20HTTP%20%E8%AF%B7%E6%B1%82.md" target="_blank" rel="noopener">iOS 开发中使用 NSURLProtocol 拦截 HTTP 请求</a></p><p>#### </p>]]></content>
<summary type="html">
<h4 id="一、概述"><a href="#一、概述" class="headerlink" title="一、概述"></a>一、概述</h4><h5 id="1、URL-Loading-System"><a href="#1、URL-Loading-System" cla
</summary>
<category term="iOS基础" scheme="http://buaa0300/nanhuacoder.com/categories/iOS%E5%9F%BA%E7%A1%80/"/>
<category term="NSURLProtocol" scheme="http://buaa0300/nanhuacoder.com/tags/NSURLProtocol/"/>
</entry>
</feed>