forked from yulingtianxia/yulingtianxia.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
2419 lines (2249 loc) · 908 KB
/
atom.xml
File metadata and controls
2419 lines (2249 loc) · 908 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>yulingtianxia's blog</title>
<subtitle>玉令天下的博客</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://yulingtianxia.com/"/>
<updated>2018-09-17T15:18:38.377Z</updated>
<id>http://yulingtianxia.com/</id>
<author>
<name>杨萧玉</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>GitHub 虚假 Star 净网行动</title>
<link href="http://yulingtianxia.com/blog/2018/09/16/Fuck-Fake-GitHub-Stars/"/>
<id>http://yulingtianxia.com/blog/2018/09/16/Fuck-Fake-GitHub-Stars/</id>
<published>2018-09-16T08:20:55.000Z</published>
<updated>2018-09-17T15:18:38.377Z</updated>
<content type="html"><p>前一阵子看到一篇文章 <a href="https://juejin.im/post/5b8c9310f265da4361530560" target="_blank" rel="external">《石锤 github 买 star 行为》</a>,第一反应是很震惊。是真的很震惊,因为文章中提到的 CocoaDebug 我也 star 了,没想到竟然涉嫌购买 star 炒作,蒙蔽了好多人的双眼。没错,我就是跟风 star,看别的大神 star 啥就顺手 star。 也有的人看 Trending 上啥火顺手 star,甚至用脚本自动 star。</p>
<p>这条黑产背后到底隐藏着什么?GitHub 上还有哪些大笨蛋也曾靠买 Star 蒙蔽了大神们的双眼呢?我写了个简单的程序用于挖掘基于 Star 的关系链,并进行聚类分析。然后从 CocoaDebug 这个 repo 入手,沿着关系链一层层深挖下去。</p>
<p>用数据说话,结果一定也会让你大开眼界。正义可能会迟到,但绝不会缺席!</p>
<p>项目源码:<a href="https://github.com/yulingtianxia/FuckFakeGitHubStars" target="_blank" rel="external">FuckFakeGitHubStars</a></p>
<a id="more"></a>
<h2 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h2><ol>
<li>用 GitHub 的 API 获取 repo 有哪些用户 star 了,然后再看看这些用户都 star 了哪些 repo。</li>
<li>将 star 行为相似的用户和 repo 聚类</li>
<li>疑似黑产的用户集合一般数量较多,且每个用户 star 的 repo 并不多。将这种集合纳入黑名单。(肯定会有误判,但影响不大)</li>
<li>计算 repo 的 star 中黑名单用户占比。</li>
<li>继续遍历黑名单中的用户,挖掘下一层关系链,揪出更多花钱买 star 的 repo。</li>
</ol>
<h2 id="爆料"><a href="#爆料" class="headerlink" title="爆料"></a>爆料</h2><p><strong>郑重声明</strong>:</p>
<ol>
<li>结果不一定准确,仅做参考,毕竟黑名单有误判。</li>
<li>买 Star 都只是推测,没有交易记录就没有实锤。本文仅是分析 GitHub 社区这一有趣而又奇妙的的现象。</li>
<li>不排除有人恶意给别人的 Repo 买 Star 的情况,也说不定有人注册了一堆账号喜欢没事给别人 Star 呢!</li>
<li>由于脚本是广度优先搜索,每个 batch 跑完结果都会更准确。跑完整个 GitHub 需要巨长的时间。跑的 batch 越多,有些 Repo 就越能露出马脚。</li>
</ol>
<p>由于数据量实在是太大了,而且也受限于 GitHub API 请求频率的限制和 CPU 计算的耗时,在上面思路中的第五步中只运行了一部分。当然,全部深挖都只是时间问题,无奈数据量级的恐怖,先把阶段性成果输出下。</p>
<ol>
<li>从 CocoaDebug 入手挖掘出的疑似黑产账号达到了900左右。</li>
<li>CocoaDebug 有 30% 左右的 Star 可能是买的。</li>
<li><p>在 <a href="https://juejin.im/post/5b8c9310f265da4361530560" target="_blank" rel="external">《石锤 github 买 star 行为》</a> 文章中跟 CocoaDebug 一起被揭露的『难兄难弟』所购买的 Star 更为夸张,超过了半数:</p>
<figure class="highlight groovy"><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">repo owner<span class="regexp">/name: baoleiji/</span>QilinBaoleiji stargazer <span class="string">num:</span> <span class="number">1447</span> black <span class="string">percent:</span><span class="number">0.5770559778852798</span></span><br><span class="line">repo owner<span class="regexp">/name: 3348375016/</span>ITSecrets stargazer <span class="string">num:</span> <span class="number">1589</span> black <span class="string">percent:</span><span class="number">0.5173064820641913</span></span><br></pre></td></tr></table></figure>
<p> 当然,再深挖跑一轮数据可能会发现这个比例更大。</p>
</li>
<li><p>Jinxiansen 的 SwiftServerSide-Vapor 曾在 8 月 5 日登上了 Trending,当日收获 104个 Star。如果我没记错的话,mattt 大神也 star 并 follow 过(现在发现又取关了,果然即便蒙蔽了大神的双眼那也只是暂时的事儿)。神奇的是,这个 repo 中有 105 个 Star 疑似来自黑产。附上<a href="https://www.v2ex.com/t/471479" target="_blank" rel="external">这篇 V 站的贴子更有趣</a>。这哥们写的另外一个 JHUD 也是同理。</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">repo owner<span class="regexp">/name: Jinxiansen/</span>SwiftServerSide-Vapor stargazer <span class="string">num:</span> <span class="number">583</span> black <span class="string">percent:</span><span class="number">0.18010291595197256</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>UCodeUStory 的 S-MVP,你慢慢涨 Star 就能逃得了么?</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">repo owner<span class="regexp">/name: UCodeUStory/</span>S-MVP stargazer <span class="string">num:</span> <span class="number">1103</span> black <span class="string">percent:</span><span class="number">0.28014505893019037</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>买一个 Star 到底要多少钱啊,有的 repo 还不到一百个 Star,占比还不低呢,也不多买点,真抠啊(我甚至怀疑是黑产为了伪装自己的账号,随意 star 了一些没花钱买 star 的库):</p>
<figure class="highlight groovy"><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">repo owner<span class="regexp">/name: jianhaod/</span>Kaggle stargazer <span class="string">num:</span> <span class="number">37</span> black <span class="string">percent:</span><span class="number">0.5945945945945946</span></span><br><span class="line">repo owner<span class="regexp">/name: whsgzcy/</span>DEMOS_TO_MySelf_Android stargazer <span class="string">num:</span> <span class="number">63</span> black <span class="string">percent:</span><span class="number">0.4603174603174603</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>搞区块链的?7月7日那天涨了 246 个 star,一算比例还真差不多:</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">repo owner<span class="regexp">/name: DeuroIO/</span>erc20-ico-onchain-technical-analysis stargazer <span class="string">num:</span> <span class="number">512</span> black <span class="string">percent:</span><span class="number">0.427734375</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>仿豆瓣的、仿知乎的。MelonRice 还有个放虎扑的,我脚本还没扫到它,手动点进去一看 star 的人,还是那尿性,也都 star 了前面那位 Jinxiansen。</p>
<figure class="highlight groovy"><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">repo owner<span class="regexp">/name: jianxiaoBai/</span>douban stargazer <span class="string">num:</span> <span class="number">288</span> black <span class="string">percent:</span><span class="number">0.3715277777777778</span></span><br><span class="line">repo owner<span class="regexp">/name: MelonRice/</span>zhihudaily_flutter stargazer <span class="string">num:</span> <span class="number">163</span> black <span class="string">percent:</span><span class="number">0.2085889570552147</span></span><br></pre></td></tr></table></figure>
</li>
</ol>
<p>因为找出来的数据太多了,这里就不逐个去看了,这里只是随便拎几个出来。</p>
<p>要是 GitHub API 没有请求限频,再搞个云服务器成天跑,再做个前端页面支持查找,就完美了。要是家里有矿,说不定还能上 GPU 搞神经网络在线学习?!</p>
<p>我好担心被这些人报复啊。</p>
<h2 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h2><p>直接看 <a href="https://github.com/yulingtianxia/FuckFakeGitHubStars/blob/master/README.md" target="_blank" rel="external">README.md</a> 吧。</p>
<p>因为 GitHub API 用到的 token 没有上传,所以需要填你自己的 token 才可以抓数据。而且我只上传了部分数据,生成的 json 文件太大了,又懒得用数据库。</p>
<p>最终的可读性比较强的信息输出在 log 里,没有上传。有兴趣的可以自己跑下。</p>
<h2 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h2><p>技术栈就是 python3 + GraphQL。</p>
<p><code>REPOSITORY_STARGAZERS.json</code> 存储了 repo 有哪些用户 star 了。<code>USER_STAR_REPOSITORIES.json</code> 存储了用户 star 了哪些 repo。repo 或用户都是一个 node,都有唯一的 node ID。这样就构成了一张有向图。再根据节点的出度或入度集合将节点使用 Jaccard 相似度进行聚类。节点的详细信息以及与其他节点的相似度信息都保存在 <code>NODE_ID_CONTENT.json</code> 中。整理出的疑似黑产黑名单用户保存在 <code>BLACK_LIST.json</code>。</p>
<p>最初的设想是在这张巨大的有向图中广度优先遍历,层层扒皮。后来迫于面对现实,就只跑了两层,先有个阶段性结论。可以分析单个 repo star 的黑产占比,想把全网数据一网打尽需要耗费更多的时间成本。</p>
<p>本项目用到的技术都是现学现卖,纯粹是玩票性质,代码烂的一逼,求轻喷。某大神都深入 Python 底层实现原理开课赚钱了,我还在这边查语法边写垃圾代码,差距太大了哎!</p>
<h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>愿以后 GitHub 能够清静些,虽然我大清自有国情在,但也别让一些别有用心之人一条臭鱼坏了一坨粥。</p>
<p>写这篇文章的时候,强台风『山竹』还在蹂躏着深圳。</p>
<p>就做了一点微小的工作,谢谢大家。</p>
</content>
<summary type="html">
<p>前一阵子看到一篇文章 <a href="https://juejin.im/post/5b8c9310f265da4361530560">《石锤 github 买 star 行为》</a>,第一反应是很震惊。是真的很震惊,因为文章中提到的 CocoaDebug 我也 star 了,没想到竟然涉嫌购买 star 炒作,蒙蔽了好多人的双眼。没错,我就是跟风 star,看别的大神 star 啥就顺手 star。 也有的人看 Trending 上啥火顺手 star,甚至用脚本自动 star。</p>
<p>这条黑产背后到底隐藏着什么?GitHub 上还有哪些大笨蛋也曾靠买 Star 蒙蔽了大神们的双眼呢?我写了个简单的程序用于挖掘基于 Star 的关系链,并进行聚类分析。然后从 CocoaDebug 这个 repo 入手,沿着关系链一层层深挖下去。</p>
<p>用数据说话,结果一定也会让你大开眼界。正义可能会迟到,但绝不会缺席!</p>
<p>项目源码:<a href="https://github.com/yulingtianxia/FuckFakeGitHubStars">FuckFakeGitHubStars</a></p>
</summary>
<category term="GitHub" scheme="http://yulingtianxia.com/tags/GitHub/"/>
<category term="瞎折腾" scheme="http://yulingtianxia.com/tags/%E7%9E%8E%E6%8A%98%E8%85%BE/"/>
</entry>
<entry>
<title>iOS 自动化测试标签生成工具接入指南</title>
<link href="http://yulingtianxia.com/blog/2018/08/13/TBUIAutoTest-Usage/"/>
<id>http://yulingtianxia.com/blog/2018/08/13/TBUIAutoTest-Usage/</id>
<published>2018-08-12T16:24:45.000Z</published>
<updated>2018-09-15T08:28:13.645Z</updated>
<content type="html"><p><a href="https://github.com/yulingtianxia/TBUIAutoTest" target="_blank" rel="external">TBUIAutoTest</a> 可以帮开发人员生成UI 控件的标签,便于自动化测试。只需一行代码或一个配置,几乎所有的 iOS Native UI 都会在运行时生成一个页面内唯一且不变的标签。不仅节省了开发人员手动为每个 UI 控件加标签的时间,也节省了测试人员与开发人员的沟通成本。</p>
<a id="more"></a>
<h2 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h2><p>目前已经有 QQ、今日头条、兴趣部落、NOW直播等几十款 App 接入 <a href="https://github.com/yulingtianxia/TBUIAutoTest" target="_blank" rel="external">TBUIAutoTest</a>。阿里系自动化测试开源框架 <a href="https://github.com/macacajs/iosHookViewId" target="_blank" rel="external">Macaca</a> 也在使用此方案,服务阿里多款 App。</p>
<p>以往的工作流程是:</p>
<ol>
<li>测试同学梳理出需要加标签的 UI 控件,通过截图标注给开发同学。</li>
<li>开发同学按照标注,给相应的 UI 控件手写代码添加标签。(花时间起唯一的标签名,增加安装包体积)</li>
<li>测试同学使用工具抓取控件树,查看控件标签,编写脚本。</li>
</ol>
<p>如果使用 <a href="https://github.com/yulingtianxia/TBUIAutoTest" target="_blank" rel="external">TBUIAutoTest</a>,只需要简化成一个步骤:</p>
<ol>
<li>测试同学使用工具抓取控件树,查看控件标签,编写脚本。</li>
</ol>
<p>而且几乎是<strong>一劳永逸</strong>的,后续新增的代码也会在运行时生成标签,<strong>以不变应万变</strong>!</p>
<p>近些年苹果的自动化测试框架经历过一些变化,但是始终是使用无障碍化相关的 API 来获取控件树和标签。</p>
<p>实现的思路和原理:<a href="http://yulingtianxia.com/blog/2016/03/28/Add-UITest-Label-for-UIAutomation/">为 UIAutomation 添加自动化测试标签的探索</a></p>
<h2 id="集成方式"><a href="#集成方式" class="headerlink" title="集成方式"></a>集成方式</h2><h3 id="手动拖拽文件"><a href="#手动拖拽文件" class="headerlink" title="手动拖拽文件"></a>手动拖拽文件</h3><p>将 TBUIAutoTest 文件夹内的所有文件加入到工程中即可。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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">TB<span class="built_in">UIAutoTest</span>.h</span><br><span class="line">TB<span class="built_in">UIAutoTest</span>.m</span><br><span class="line"><span class="built_in">UIImage</span>+TB<span class="built_in">UIAutoTest</span>.h</span><br><span class="line"><span class="built_in">UIImage</span>+TB<span class="built_in">UIAutoTest</span>.m</span><br><span class="line"><span class="built_in">UIResponder</span>+TB<span class="built_in">UIAutoTest</span>.h</span><br><span class="line"><span class="built_in">UIResponder</span>+TB<span class="built_in">UIAutoTest</span>.m</span><br><span class="line"><span class="built_in">UIView</span>+TB<span class="built_in">UIAutoTest</span>.h</span><br><span class="line"><span class="built_in">UIView</span>+TB<span class="built_in">UIAutoTest</span>.m</span><br></pre></td></tr></table></figure>
<h3 id="CocoaPods"><a href="#CocoaPods" class="headerlink" title="CocoaPods"></a>CocoaPods</h3><p>首先要安装 CocoaPods:</p>
<figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">gem <span class="keyword">install</span> cocoapods</span><br></pre></td></tr></table></figure>
<p>在 <code>Podfile</code> 中添加 TBUIAutoTest。需要将 “MyApp” 替换成自己的名字:</p>
<figure class="highlight elixir"><table><tr><td class="gutter"><pre><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">source <span class="string">'https://github.com/CocoaPods/Specs.git'</span></span><br><span class="line">platform <span class="symbol">:ios</span>, <span class="string">'6.0'</span></span><br><span class="line">use_frameworks!</span><br><span class="line">target <span class="string">'MyApp'</span> <span class="keyword">do</span></span><br><span class="line"> pod <span class="string">'TBUIAutoTest'</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>
<p>最后需要运行下面的命令行安装下:</p>
<figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="keyword">install</span></span><br></pre></td></tr></table></figure>
<h3 id="Carthage"><a href="#Carthage" class="headerlink" title="Carthage"></a>Carthage</h3><p>首先要通过 homebrew 安装 Carthage:</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">brew </span>update</span><br><span class="line"><span class="keyword">brew </span><span class="keyword">install </span>carthage</span><br></pre></td></tr></table></figure>
<p>然后在 <code>Cartfile</code> 文件中添加</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">github</span> <span class="string">"yulingtianxia/TBUIAutoTest"</span></span><br></pre></td></tr></table></figure>
<p>运行 <code>carthage update</code> 命令来获取 <code>TBUIAutoTest.framework</code>,将其拖拽到工程中使用即可。</p>
<h2 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h2><ul>
<li><code>kAutoTestUITurnOnKey</code> :是否生成 UI 标签</li>
<li><code>kAutoTestUILongPressKey</code> :是否开启长按弹窗显示 UI 标签</li>
</ul>
<p><a href="https://github.com/yulingtianxia/TBUIAutoTest" target="_blank" rel="external">TBUIAutoTest</a> 会在 <code>+ load</code> 方法中从 <code>NSUserDefaults</code> 中读取 <code>kAutoTestUITurnOnKey</code> 和 <code>kAutoTestUILongPressKey</code> 的值。所以在设置这两个 Key 之后,一般需要下次启动 App 才生效。这里推荐拉一条自动化测试专用分支,通过宏控制在 App 启动更早的时机设置这两个 Key。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">extern</span> <span class="built_in">NSString</span> * <span class="keyword">const</span> kAutoTest<span class="built_in">UITurnOnKey</span>;</span><br><span class="line"><span class="keyword">extern</span> <span class="built_in">NSString</span> * <span class="keyword">const</span> kAutoTest<span class="built_in">UILongPressKey</span>;</span><br><span class="line">[<span class="built_in">NSUserDefaults</span>.standardUserDefaults setBool:<span class="literal">YES</span> forKey:kAutoTest<span class="built_in">UITurnOnKey</span>];</span><br><span class="line">[<span class="built_in">NSUserDefaults</span>.standardUserDefaults setBool:<span class="literal">YES</span> forKey:kAutoTest<span class="built_in">UILongPressKey</span>];</span><br></pre></td></tr></table></figure>
<h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>系统一些自带的 UI 控件的 <code>isAccessibilityElement</code> 属性默认是 <code>YES</code>,但是如果想让自定义的 UI 控件能够被捕获,需要手动将其设为 <code>YES</code>。除此之外还有一些容器控件之间的嵌套场景,需要在容器类中实现 <code>UIAccessibilityContainer</code>,这样才能捕获到容器控件内的子视图。最简单的一种实现如下:</p>
<figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-id">#pragma</span> <span class="selector-tag">mark</span> <span class="selector-tag">-</span> <span class="selector-tag">UIAccessibilityContainer</span></span><br><span class="line"></span><br><span class="line"><span class="selector-id">#ifdef</span> <span class="selector-tag">AUTO_TEST_ENV</span></span><br><span class="line"><span class="selector-tag">-</span>(BOOL)<span class="selector-tag">isAccessibilityElement</span></span><br><span class="line">&#123;</span><br><span class="line"> return NO;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">-</span> (NSInteger)<span class="selector-tag">accessibilityElementCount</span></span><br><span class="line">&#123;</span><br><span class="line"> return <span class="selector-attr">[[self subviews]</span> <span class="selector-tag">count</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">-</span> (id)<span class="selector-tag">accessibilityElementAtIndex</span><span class="selector-pseudo">:(NSInteger)index</span></span><br><span class="line">&#123;</span><br><span class="line"> return <span class="selector-attr">[[self subviews]</span> <span class="selector-tag">objectAtIndex</span><span class="selector-pseudo">:index</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="selector-tag">-</span> (NSInteger)<span class="selector-tag">indexOfAccessibilityElement</span><span class="selector-pseudo">:(id)element</span></span><br><span class="line">&#123;</span><br><span class="line"> return <span class="selector-attr">[[self subviews]</span> <span class="selector-tag">indexOfObject</span><span class="selector-pseudo">:element</span>];</span><br><span class="line">&#125;</span><br><span class="line"><span class="selector-id">#endif</span></span><br></pre></td></tr></table></figure>
<p>对于那种绘制上去的 UI,但非使用原生 UI 控件的场景,就需要自己创建 <code>UIAccessibilityElement</code> 对象,并手动设置标签了。这部分其实都是 App 无障碍化的知识,具体细节可以参考苹果官方文档:<a href="https://developer.apple.com/documentation/uikit/accessibility/uiaccessibilitycontainer?language=objc" target="_blank" rel="external">https://developer.apple.com/documentation/uikit/accessibility/uiaccessibilitycontainer?language=objc</a></p>
</content>
<summary type="html">
<p><a href="https://github.com/yulingtianxia/TBUIAutoTest">TBUIAutoTest</a> 可以帮开发人员生成UI 控件的标签,便于自动化测试。只需一行代码或一个配置,几乎所有的 iOS Native UI 都会在运行时生成一个页面内唯一且不变的标签。不仅节省了开发人员手动为每个 UI 控件加标签的时间,也节省了测试人员与开发人员的沟通成本。</p>
</summary>
</entry>
<entry>
<title>MessageThrottle Safety</title>
<link href="http://yulingtianxia.com/blog/2018/07/31/MessageThrottle-Safety/"/>
<id>http://yulingtianxia.com/blog/2018/07/31/MessageThrottle-Safety/</id>
<published>2018-07-30T16:50:24.000Z</published>
<updated>2018-09-15T08:28:13.897Z</updated>
<content type="html"><p><a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 是我开发的Objective-C 节流限频组件,其原理基于 Hook 消息转发流程,在运行时应用了一套节流限频的规则。</p>
<p>新版本再次提升性能的同时,确保了 hook 流程、多线程操作、规则管理的安全性,支持了持久化规则,并对 KVO 等场景进行兼容。<a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 的代码测试覆盖率在 80% 以上,在编写测试用例的同时也发现了一些安全隐患,有些甚至是业界知名开源库都没有发现和解决的。</p>
<p>本文是关于 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 的第四篇文章。前三篇如下:</p>
<ul>
<li><a href="http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/">Objective-C Message Throttle and Debounce</a></li>
<li><a href="http://yulingtianxia.com/blog/2017/12/15/Associated-Object-and-Dealloc/">Associated Object 与 Dealloc</a></li>
<li><a href="http://yulingtianxia.com/blog/2018/05/31/MessageThrottle-Performance-Benchmark-and-Optimization/">MessageThrottle Performance Benchmark and Optimization</a></li>
</ul>
<a id="more"></a>
<p>主要类的关系如下图,虚线为 <code>weak</code> 属性。</p>
<p><img src="http://yulingtianxia.com/resources/MessageThrottle1.png" alt=""></p>
<h2 id="继承链消息转发缺陷"><a href="#继承链消息转发缺陷" class="headerlink" title="继承链消息转发缺陷"></a>继承链消息转发缺陷</h2><p>由于是在消息转发流程搞事情,把所有消息都经由一个统一的路由函数 <code>mt_forwardInvocation</code> 进行处理。子类和父类不能同时 Hook 同一个方法,原因是如果子类的方法调用了父类方法,那么父类的方法调用走到统一路由函数 <code>mt_forwardInvocation</code> 的时候,『调用父类方法』这一信息早已经丢失了,接着会转发给子类的方法实现,从而造成死循环。最后 crash。</p>
<p>解决方法就是在 Hook 之前判断关系链,如果已经有子类或者父类被 Hook 了就报错,无法继续 Hook。</p>
<p>在消息转发流程将所有消息通过统一的路由函数处理并转发这件事的缺陷就是丢失了类的信息,因为全都『统一』到同一个函数处理了,而不是在各自类的内部处理。诸如 Aspects 等业界知名开源库也有此问题。</p>
<p><img src="http://yulingtianxia.com/resources/MessageThrottle2.png" alt=""></p>
<h2 id="兼容-KVO、其他-Hook-框架"><a href="#兼容-KVO、其他-Hook-框架" class="headerlink" title="兼容 KVO、其他 Hook 框架"></a>兼容 KVO、其他 Hook 框架</h2><p>首先先了解下 KVO 的原理:当监听类型为 <code>A</code> 的对象 <code>a</code> 时,会动态创建 <code>A</code> 的子类 <code>NSKVONotifying_A</code>,并把 <code>a</code> 的类型改成 <code>NSKVONotifying_A</code>。<code>NSKVONotifying_A</code> 会覆写监听的属性村粗方法,以及 <code>class</code> 方法,让外部以为 <code>a</code> 的类型依然是 <code>A</code>。</p>
<p>其余开源框架在 hook 一个对象的时候,也是通过加前缀或后缀动态创建子类,然后覆写相关方法。继承链总有先来后到,这时候问题就来了。</p>
<p>使用 <code>class</code> 方法获取到的类型可能是被『篡改过』的类,使用 <code>objc_getClass()</code> 函数获取到的才是真正的类。KVO 的做法是在用 <code>objc_getClass()</code> 获取到真正的类之后,直接创建带 <code>NSKVONotifying_</code> 前缀的子类。</p>
<p><img src="http://yulingtianxia.com/resources/MessageThrottle3.png" alt=""></p>
<p>如图所示,MessageThrottle 在 hook 一个对象的时候也会动态创建带前缀 <code>MTSubclassPrefix</code> 的子类,但是不会像 KVO 那样无脑创建,而是先判断通过 <code>class</code> 与 <code>objc_getClass()</code> 获取到的类是否相同。如果不同,则说明已经有现成的子类了,直接在 <code>objc_getClass()</code> 获取的类中 hook 就行了。这里是借鉴了 Aspects 的做法。</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><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">Class cls<span class="comment">;</span></span><br><span class="line">Class statedClass = [target class]<span class="comment">;</span></span><br><span class="line">Class <span class="keyword">baseClass </span>= object_getClass(target)<span class="comment">;</span></span><br><span class="line">NSString *className = NSStringFromClass(<span class="keyword">baseClass);</span><br><span class="line"></span> </span><br><span class="line">if ([className hasPrefix:MTSubclassPrefix]) &#123;</span><br><span class="line"> cls = <span class="keyword">baseClass;</span><br><span class="line"></span>&#125;</span><br><span class="line">else if (mt_object_isClass(target)) &#123;</span><br><span class="line"> cls = target<span class="comment">;</span></span><br><span class="line">&#125;</span><br><span class="line">else if (statedClass != <span class="keyword">baseClass) </span>&#123;</span><br><span class="line"> cls = <span class="keyword">baseClass;</span><br><span class="line"></span>&#125;</span><br><span class="line">else &#123;</span><br><span class="line"> const char *<span class="keyword">subclassName </span>= [MTSubclassPrefix stringByAppendingString:className].UTF8String<span class="comment">;</span></span><br><span class="line"> Class <span class="keyword">subclass </span>= objc_getClass(<span class="keyword">subclassName);</span><br><span class="line"></span> </span><br><span class="line"> if (<span class="keyword">subclass </span>== nil) &#123;</span><br><span class="line"> <span class="keyword">subclass </span>= objc_allocateClassPair(<span class="keyword">baseClass, </span><span class="keyword">subclassName, </span><span class="number">0</span>)<span class="comment">;</span></span><br><span class="line"> if (<span class="keyword">subclass </span>== nil) &#123;</span><br><span class="line"> NSLog(@<span class="string">"objc_allocateClassPair failed to allocate class %s."</span>, <span class="keyword">subclassName);</span><br><span class="line"></span> return NO<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> mt_hookedGetClass(<span class="keyword">subclass, </span>statedClass)<span class="comment">;</span></span><br><span class="line"> mt_hookedGetClass(object_getClass(<span class="keyword">subclass), </span>statedClass)<span class="comment">;</span></span><br><span class="line"> objc_registerClassPair(<span class="keyword">subclass);</span><br><span class="line"></span> &#125;</span><br><span class="line"> object_setClass(target, <span class="keyword">subclass);</span><br><span class="line"></span> cls = <span class="keyword">subclass;</span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>有来就有回,如果要 remove KVO 或 hook 呢?肯定无法确保各个框架或 KVO add 和 remove『先入后出』的顺序,所以必然要做兼容处理。</p>
<p>在 revert hook 的时候需要判断真实类型的前缀是否是 <code>MTSubclassPrefix</code>。如果是,则将实例对象的类型还原回去。最后会判断是否还有其他相同类型的对象也被 hook 了,如果没有,则可以对这个类 revert hook。</p>
<p>添加 KVO 和应用限频规则有先后顺序,移除 KVO 和废除限频规则也有先后顺序,那么可以排列组合出四种结果:</p>
<table>
<thead>
<tr>
<th>初始类为 A</th>
<th>先添加 KVO</th>
<th>先应用限频规则</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>先移除 KVO</strong></td>
<td><code>A</code></td>
<td><code>A</code></td>
</tr>
<tr>
<td><strong>先废除限频规则</strong></td>
<td><code>A</code></td>
<td><code>_MessageThrottle_A</code></td>
</tr>
</tbody>
</table>
<p>PS: <code>MTSubclassPrefix</code> 常量内容就是 <code>_MessageThrottle_</code>。</p>
<p>因为通过 <code>MTDealloc</code> 记录了 hook 的类,所以 revert hook 的时候使用的是当初 hook 的类,而不是当前实例对象真实的类。这主要是针对上面表格中的 『先应用限频规则,先废除限频规则』的情况。在废除规则的时候,真实的类为 <code>NSKVONotifying__MessageThrottle_A</code>,而当初应用规则时 hook 的类为 <code>_MessageThrottle_A</code>。这里要注意区分处理下。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="built_in">BOOL</span> mt_recoverMethod(<span class="keyword">id</span> target, SEL selector, SEL aliasSelector)</span><br><span class="line">&#123;</span><br><span class="line"> Class cls;</span><br><span class="line"> <span class="keyword">if</span> (mt_object_isClass(target)) &#123;</span><br><span class="line"> cls = target;</span><br><span class="line"> <span class="keyword">if</span> ([MTEngine.defaultEngine containsSelector:selector onTargetsOfClass:cls]) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> MTDealloc *mtDealloc = objc_getAssociatedObject(target, selector);</span><br><span class="line"> <span class="comment">// get class when apply rule on target.</span></span><br><span class="line"> cls = mtDealloc.cls;</span><br><span class="line"> <span class="comment">// target current real class name</span></span><br><span class="line"> <span class="built_in">NSString</span> *className = <span class="built_in">NSStringFromClass</span>(object_getClass(target));</span><br><span class="line"> <span class="keyword">if</span> ([className hasPrefix:MTSubclassPrefix]) &#123;</span><br><span class="line"> Class originalClass = <span class="built_in">NSClassFromString</span>([className stringByReplacingOccurrencesOfString:MTSubclassPrefix withString:<span class="string">@""</span>]);</span><br><span class="line"> <span class="built_in">NSCAssert</span>(originalClass != <span class="literal">nil</span>, <span class="string">@"Original class must exist"</span>);</span><br><span class="line"> <span class="keyword">if</span> (originalClass) &#123;</span><br><span class="line"> object_setClass(target, originalClass);</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">if</span> ([MTEngine.defaultEngine containsSelector:selector onTarget:cls] ||</span><br><span class="line"> [MTEngine.defaultEngine containsSelector:selector onTargetsOfClass:cls]) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> mt_revertHook(cls, selector, aliasSelector);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="Revert-Hook-的缺陷"><a href="#Revert-Hook-的缺陷" class="headerlink" title="Revert Hook 的缺陷"></a>Revert Hook 的缺陷</h2><p>前提:子类和父类都实现了同一个方法,并且子类的方法会调用 <code>super</code> 的方法。</p>
<p>在 Aspects 中有两种异常场景:</p>
<h3 id="先-Hook-父类,然后-revert,接着-Hook-子类。最后调用子类实例对象方法。"><a href="#先-Hook-父类,然后-revert,接着-Hook-子类。最后调用子类实例对象方法。" class="headerlink" title="先 Hook 父类,然后 revert,接着 Hook 子类。最后调用子类实例对象方法。"></a>先 Hook 父类,然后 revert,接着 Hook 子类。最后调用子类实例对象方法。</h3><p>结果是只执行了父类的方法,子类的方法没执行到。</p>
<p>原因是当子类没有对应的方法和实现时,<code>instancesRespondToSelector:</code> 会判断在继承链上查找是否有父类实现了方法。在 hook 某个方法前如果只通过 <code>instancesRespondToSelector:</code> 来判断是否已经添加过 <code>aliasSelector</code> 的话,是不够严谨的。父类 hook 后会添加 <code>aliasSelector</code> 方法,revert 后这个方法还在。hook 子类的时候因为判断出已经有 <code>aliasSelector</code> 方法了就没给子类添加该方法,实际上子类是继承的父类的实现,结果就是只执行了父类的方法实现。</p>
<p>MessageThrottle 的解决方案是通过比较父类和子类的 <code>Method</code> 是否相同。如果 <code>instancesRespondToSelector:</code> 方法返回 <code>YES</code> 并且父类和子类的 <code>Method</code> 相同,那么就说明子类的 Method 是来自父类的,仍然需要为子类添加 <code>aliasSelector</code> 对应的方法。</p>
<figure class="highlight oxygene"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> char *typeEncoding = method_getTypeEncoding(targetMethod);</span><br><span class="line"><span class="function"><span class="keyword">Method</span> <span class="title">targetAliasMethod</span> = <span class="title">class_getInstanceMethod</span><span class="params">(cls, aliasSelector)</span>;</span></span><br><span class="line"><span class="function"><span class="keyword">Method</span> <span class="title">targetAliasMethodSuper</span> = <span class="title">class_getInstanceMethod</span><span class="params">(superCls, aliasSelector)</span>;</span></span><br><span class="line"><span class="keyword">if</span> (![cls instancesRespondToSelector:aliasSelector] || targetAliasMethod == targetAliasMethodSuper) <span class="comment">&#123;</span><br><span class="line"> __unused BOOL addedAlias = class_addMethod(cls, aliasSelector, method_getImplementation(targetMethod), typeEncoding);</span><br><span class="line"> NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), cls);</span><br><span class="line">&#125;</span></span><br><span class="line">class_replaceMethod(cls, <span class="keyword">selector</span>, mt_getMsgForwardIMP(statedClass, <span class="keyword">selector</span>), typeEncoding);</span><br></pre></td></tr></table></figure>
<p>测试了下 Aspects 的表现,果然是只调用了父类的实现,这是一个很大的漏洞。</p>
<h3 id="先-Hook-子类,然后-revert,接着-Hook-父类。最后调用子类实例对象方法。"><a href="#先-Hook-子类,然后-revert,接着-Hook-父类。最后调用子类实例对象方法。" class="headerlink" title="先 Hook 子类,然后 revert,接着 Hook 父类。最后调用子类实例对象方法。"></a>先 Hook 子类,然后 revert,接着 Hook 父类。最后调用子类实例对象方法。</h3><p>结果是 crash。</p>
<p>因为 Objective-C Runtime 没有提供移除方法的 API,所以在 revert hook 的时候,无法将 hook 过的 <code>forwardInvocation:</code> 方法彻底复原,只能塞入 <code>NSObject</code> 的默认实现(IMP)。</p>
<ol>
<li><p>当子类的方法调用 <code>super</code> 方法时,因为父类的方法被 hook 了(通过替换 <code>IMP</code> 为 <code>_objc_msgForward</code>),会触发调用 <code>forwardInvocation:</code> 方法。但是子类的 <code>forwardInvocation:</code> 方法曾经被 Hook 过,所以此时不再是直接调用父类的实现,而是调用子类自己的实现,那么结果就是找不到方法,抛异常。</p>
</li>
<li><p>如果子类没被 hook 过,子类是没有 <code>forwardInvocation:</code> 方法的,会调用父类的方法实现。因为父类被 hook 了,所以会走 MessageThrottle 的消息转发流程,所以是不会出问题的。</p>
</li>
</ol>
<p>两者差别在于,调用 <code>forwardInvocation:</code> 方法时,已经 Hook 过的类会调用自己的实现,而不会调用父类的实现。</p>
<p>MessageThrottle 解决方案是记录所有 Hook 过的类,在 Hook 其他类之前先判断下是否已经有子类被 Hook 过。如果有,则作降级处理,打 Log 报错,不能继续 Hook。</p>
<p>这个方案虽然不完美,但总比抛异常 crash 好。连 Aspects 也没有注意到这点,亲测会 crash。</p>
<h2 id="规则持久化"><a href="#规则持久化" class="headerlink" title="规则持久化"></a>规则持久化</h2><p>如果限频规则只存在于内存中,那么其实是很不安全的。</p>
<p>有些场景下限频的周期很长,比如为了减少某条协议请求后台的次数,要求客户端一小时内最多请求一次。如果在一小时内 App 进程杀掉了然后又打开 App,这样就需要限频规则信息能够持久化存储,下次打开 App 读取并应用上次保存的规则。</p>
<p>对于 <code>target</code> 为类或元类、<code>MTPerformModeFirstly</code> 模式下且限频周期大于 5 秒的规则,MessageThrottle 会自动将其标记为持久化规则。</p>
<p>可以通过设置 <code>MTRule</code> 的 <code>persistent</code> 属性为 <code>YES</code>,来标记规则为需要持久化。对于 <code>target</code> 为实例对象的规则持久化是无意义的,因为进程杀掉后,实例对象的生命周期也就结束了,规则也就自动失效了。</p>
<p><code>MTRule</code> 中只有一部分数据能够持久化,一些动态的内容无法持久化,比如队列、block 等。需要注意区分类对象和元类。</p>
<p>可以使用 <code>savePersistentRules</code> 方法来保存持久化规则。对于 iOS、macOS 和 tvOS,会在收到 Terminate 通知时自动调用 <code>savePersistentRules</code> 方法。</p>
<h2 id="线程安全"><a href="#线程安全" class="headerlink" title="线程安全"></a>线程安全</h2><p>每个 <code>MTRule</code> 都对应着一个递归锁,保证了此规则上的方法调用是线程安全的。<br>存储所有 target-selector 映射关系的 <code>MTEngine</code> 添加和废除规则涉及到对 <code>NSMapTable</code> 和 <code>NSMutableSet</code> 的操作,使用一个互斥锁来保证 <code>apply</code>、<code>discard</code> 和 <code>allRules</code> 等方法的线程安全。当一个 <code>MTRule</code> 在多个线程被频繁 <code>apply</code> 和 <code>discard</code> 的同时也可能会有这个 <code>MTRule</code> 的方法在多个线程频繁调用,所以还需要在 <code>apply</code> 和 <code>discard</code> 方法里也加一层 <code>MTRule</code> 的递归锁。</p>
<p>当然,如果在 hook 或 revert 的过程中调用了方法,但是没有走 MessageThrottle 的转发逻辑的话,还是有可能出现多线程问题。但这样的概率很低,我用下面类似的代码进行测试是 OK 的:</p>
<figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> for (<span class="name">int</span> i = <span class="number">0</span><span class="comment">; i &lt; 10000; i ++) &#123;</span></span><br><span class="line"> dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> [rule apply]<span class="comment">;</span></span><br><span class="line"> &#125;)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;)<span class="comment">;</span></span><br><span class="line">dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> for (<span class="name">int</span> i = <span class="number">0</span><span class="comment">; i &lt; 10000; i ++) &#123;</span></span><br><span class="line"> dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> [self.stub foo:[NSDate date]]<span class="comment">;</span></span><br><span class="line"> &#125;)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;)<span class="comment">;</span></span><br><span class="line">dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> for (<span class="name">int</span> i = <span class="number">0</span><span class="comment">; i &lt; 10000; i ++) &#123;</span></span><br><span class="line"> dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> [rule discard]<span class="comment">;</span></span><br><span class="line"> &#125;)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;)<span class="comment">;</span></span><br><span class="line">dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> for (<span class="name">int</span> i = <span class="number">0</span><span class="comment">; i &lt; 10000; i ++) &#123;</span></span><br><span class="line"> dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> [self.stub foo:[NSDate date]]<span class="comment">;</span></span><br><span class="line"> &#125;)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;)<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>因为 <code>MTPerformModeLast</code> 和 <code>MTPerformModeDebounce</code> 都是延时执行模式,所以有可能在延时的过程中,规则已经被废弃了,但是依然会调用到 <code>[invocation invoke]</code>,而此时需要注意 <code>invocation</code> 的 <code>selector</code>。如果规则已经被废弃了,需要使用原始的 <code>selector</code>,而不是 <code>aliasSelector</code>。</p>
</content>
<summary type="html">
<p><a href="https://github.com/yulingtianxia/MessageThrottle">MessageThrottle</a> 是我开发的Objective-C 节流限频组件,其原理基于 Hook 消息转发流程,在运行时应用了一套节流限频的规则。</p>
<p>新版本再次提升性能的同时,确保了 hook 流程、多线程操作、规则管理的安全性,支持了持久化规则,并对 KVO 等场景进行兼容。<a href="https://github.com/yulingtianxia/MessageThrottle">MessageThrottle</a> 的代码测试覆盖率在 80% 以上,在编写测试用例的同时也发现了一些安全隐患,有些甚至是业界知名开源库都没有发现和解决的。</p>
<p>本文是关于 <a href="https://github.com/yulingtianxia/MessageThrottle">MessageThrottle</a> 的第四篇文章。前三篇如下:</p>
<ul>
<li><a href="http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/">Objective-C Message Throttle and Debounce</a></li>
<li><a href="http://yulingtianxia.com/blog/2017/12/15/Associated-Object-and-Dealloc/">Associated Object 与 Dealloc</a></li>
<li><a href="http://yulingtianxia.com/blog/2018/05/31/MessageThrottle-Performance-Benchmark-and-Optimization/">MessageThrottle Performance Benchmark and Optimization</a></li>
</ul>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
</entry>
<entry>
<title>追踪 Objective-C Block 代码定义的位置</title>
<link href="http://yulingtianxia.com/blog/2018/06/24/Objective-C-Block-Mangle-Name/"/>
<id>http://yulingtianxia.com/blog/2018/06/24/Objective-C-Block-Mangle-Name/</id>
<published>2018-06-24T09:19:03.000Z</published>
<updated>2018-09-15T08:28:13.753Z</updated>
<content type="html"><p>之前写了一篇文章<a href="http://yulingtianxia.com/blog/2018/03/31/Track-Block-Arguments-of-Objective-C-Method/">《追踪 Objective-C 方法中的 Block 参数对象》</a>,利用 <a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 和 Objective-C 的动态特性实现对 block 对象执行和销毁的追踪。本文在此基础上,通过 Mach-O 文件格式获取 Mangle Name 并根据 Clang 源码实现对其解析,探寻如何追踪 block 代码定义的位置。</p>
<p>主要代码已经整合到 <a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 1.0.2 版本中。</p>
<a id="more"></a>
<h2 id="解决思路"><a href="#解决思路" class="headerlink" title="解决思路"></a>解决思路</h2><p>能想到的最直接的方法就是获取 block 内部 invoke 函数的内存地址,并找到这个地址对应的 image,然后根据对基地址的偏移量,利用 dYSM 文件存储的符号表查找到对应代码位置。这属于很常规的操作了,即便没有 dYSM 文件,用 Mach-O 反汇编也能知道 block 定义在哪个方法的大概位置。</p>
<p>本文完???</p>
<p>太水了!!!</p>
<p>如果只需要知道 block 定义在哪个方法里的话,其实有更简单的方法呀!在程序运行时就能知道的,不用那么多麻烦的后续操作。解决思路如下:</p>
<ol>
<li>通过读取每个 Mach-O 镜像文件的符号表,建立 block invoke 函数偏移地址到符号名的映射。</li>
<li>获取到的符号名是经过 Clang 处理后的 mangle name,根据生成规则反推出 block invoke 函数实现代码位置。</li>
</ol>
<h2 id="Mach-O-文件格式"><a href="#Mach-O-文件格式" class="headerlink" title="Mach-O 文件格式"></a>Mach-O 文件格式</h2><p>网上关于 Mach-O 文件的介绍一大堆,这里不再赘述。其实就是个二进制文件格式定义,照着文档写代码读二进制内容罢了。苹果也提供了 Mach-O 文件数据结构的定义,直接用就行了。当二进制镜像被加载到虚拟内存中后,就可以通过计算各种偏移量来按图索骥了。下面的代码将 <code>_hunt_blocks_for_image</code> 注册为镜像加载后的回调函数,这行代码执行前已经加载的镜像也会回调此函数:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_dyld_register_func_<span class="keyword">for</span>_add_image(_hunt_blocks_<span class="keyword">for</span>_image);</span><br></pre></td></tr></table></figure>
<p><code>_hunt_blocks_for_image</code> 函数会读取一个 Mach-O 文件中的符号表。具体操作是先从遍历 Load Commands 入手,找到 <code>__LINKEDIT</code> 段的基地址以及符号表数据的偏移量及其字符串表的偏移量。然后遍历符号表,获取到符号地址和符号名的偏移量。如果符号名中包含 <code>_block_invoke</code> 则说明是 block 实现函数,然后用字典保存符号地址到符号名的映射。</p>
<p>遍历 Load Commands 时要注意,不同类型的 Load Command 数据类型也不一样,但是肯定会有 <code>cmd</code> 和 <code>cmdsize</code> 这两个字段。可以凭借 <code>cmd</code> 简单区分其数据结构。比如符号表的 <code>cmd</code> 是 <code>LC_SYMTAB</code>,其数据结构为 <code>symtab_command</code>。比如常见的 <code>cmd</code> 为 <code>LC_SEGMENT</code> 的『段』可以靠 <code>segname</code> 区分,类型有 <code>__PAGEZERO</code> <code>__TEXT</code> <code>__DATA</code> <code>__LINKEDIT</code> <code>__OBJC</code> 等等,有的『段』下面还有很多『节』(Section)。<code>__PAGEZERO</code> 段在可执行文件才有,大小跟架构有关,是虚拟内存基地址。符号表是一个 <code>nlist</code> 数组,保存着每个符号的一些信息,这里只用到了符号地址和符号名。</p>
<p>因为使用 <code>NSMapTable</code> 存储符号地址和符号名的映射,所以需要用 <code>pthread_mutex_t</code> 确保线程安全。</p>
<p><code>_hunt_blocks_for_image</code> 函数实现如下。我觉得我思想解释的够明白了,对于看过 fishhook 源码的人来说应该很简单。实在看不懂就自己对照着 MachOView 和苹果文档,随便找个方法计算一遍偏移量就好了。</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> NSMapTable *block_invoke_mangle_cache;</span><br><span class="line"><span class="keyword">static</span> <span class="keyword">pthread_mutex_t</span> block_invoke_mangle_cache_mutex;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> _hunt_blocks_for_image(<span class="keyword">const</span> <span class="keyword">struct</span> mach_header *header, <span class="keyword">intptr_t</span> slide) &#123;</span><br><span class="line"> Dl_info info;</span><br><span class="line"> <span class="keyword">if</span> (dladdr(header, &amp;info) == <span class="number">0</span>) &#123;</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">segment_command_t</span> *cur_seg_cmd;</span><br><span class="line"> <span class="keyword">segment_command_t</span> *linkedit_segment = <span class="literal">NULL</span>;</span><br><span class="line"> <span class="keyword">segment_command_t</span> *pagezero_segment = <span class="literal">NULL</span>;</span><br><span class="line"> <span class="keyword">struct</span> symtab_command* symtab_cmd = <span class="literal">NULL</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">uintptr_t</span> cur = (<span class="keyword">uintptr_t</span>)header + <span class="keyword">sizeof</span>(<span class="keyword">mach_header_t</span>);</span><br><span class="line"> <span class="keyword">for</span> (uint i = <span class="number">0</span>; i &lt; header-&gt;ncmds; i++, cur += cur_seg_cmd-&gt;cmdsize) &#123;</span><br><span class="line"> cur_seg_cmd = (<span class="keyword">segment_command_t</span> *)cur;</span><br><span class="line"> <span class="keyword">if</span> (cur_seg_cmd-&gt;cmd == LC_SEGMENT_ARCH_DEPENDENT) &#123;</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">strcmp</span>(cur_seg_cmd-&gt;segname, SEG_LINKEDIT) == <span class="number">0</span>) &#123;</span><br><span class="line"> linkedit_segment = cur_seg_cmd;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (strcmp(SEG_PAGEZERO, cur_seg_cmd-&gt;segname) == <span class="number">0</span>) &#123;</span><br><span class="line"> pagezero_segment = (<span class="keyword">segment_command_t</span>*)cur_seg_cmd;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125; <span class="keyword">else</span> <span class="keyword">if</span> (cur_seg_cmd-&gt;cmd == LC_SYMTAB) &#123;</span><br><span class="line"> symtab_cmd = (struct symtab_command*)cur_seg_cmd;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (!symtab_cmd || !linkedit_segment ) &#123;</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">uintptr_t</span> linkedit_base = (<span class="keyword">uintptr_t</span>)slide + linkedit_segment-&gt;vmaddr - linkedit_segment-&gt;fileoff;</span><br><span class="line"> <span class="keyword">nlist_t</span> *symtab = (<span class="keyword">nlist_t</span> *)(linkedit_base + symtab_cmd-&gt;symoff);</span><br><span class="line"> <span class="keyword">char</span> *strtab = (char *)(linkedit_base + symtab_cmd-&gt;stroff);</span><br><span class="line"> </span><br><span class="line"> pthread_mutex_lock(&amp;block_invoke_mangle_cache_mutex);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (!block_invoke_mangle_cache) &#123;</span><br><span class="line"> block_invoke_mangle_cache = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsOpaqueMemory | NSMapTableObjectPointerPersonality valueOptions:NSPointerFunctionsCopyIn];</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">for</span> (uint i = <span class="number">0</span>; i &lt; symtab_cmd-&gt;nsyms; i++) &#123;</span><br><span class="line"> <span class="keyword">uint32_t</span> strtab_offset = symtab[i].n_un.n_strx;</span><br><span class="line"> <span class="keyword">char</span> *symbol_name = strtab + strtab_offset;</span><br><span class="line"> <span class="keyword">bool</span> symbol_name_longer_than_1 = symbol_name[<span class="number">0</span>] &amp;&amp; symbol_name[<span class="number">1</span>];</span><br><span class="line"> <span class="keyword">if</span> (!symbol_name_longer_than_1) &#123;</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">uintptr_t</span> block_addr = (<span class="keyword">uintptr_t</span>)info.dli_fbase + symtab[i].n_value - (pagezero_segment ? pagezero_segment-&gt;vmsize : <span class="number">0</span>);</span><br><span class="line"> NSString *symbolName = [NSString stringWithUTF8String:&amp;symbol_name[<span class="number">1</span>]];</span><br><span class="line"> NSRange range = [symbolName rangeOfString:@<span class="string">"_block_invoke"</span>];</span><br><span class="line"> <span class="keyword">if</span> (range.location != NSNotFound &amp;&amp; range.location &gt; <span class="number">0</span>) &#123;</span><br><span class="line"> [block_invoke_mangle_cache setObject:symbolName forKey:(__bridge id)(<span class="keyword">void</span> *)block_addr];</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> pthread_mutex_unlock(&amp;block_invoke_mangle_cache_mutex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="Block-Mangle-Name"><a href="#Block-Mangle-Name" class="headerlink" title="Block Mangle Name"></a>Block Mangle Name</h2><p>Clang 7.0.0 源码的 <a href="https://clang.llvm.org/doxygen/Mangle_8cpp_source.html#l00060" target="_blank" rel="external">Mangle.cpp</a> 文件实现了 Objective-C 和 block 的 mangle name。只需要看 <code>mangleBlock</code> 和 <code>mangleGlobalBlock</code> 两个函数即可大概了解 block mangle name 的生成规则。</p>
<ol>
<li>全局 block:block 变量名 + <code>_block_invoke</code> + <code>discriminator</code>。详见 <code>mangleGlobalBlock</code> 函数实现。</li>
<li>其他 block:<code>__</code> + block 代码所处的函数或方法的 mangle name + <code>_block_invoke</code> + <code>discriminator</code>。详见 <code>mangleBlock</code> 函数实现。</li>
</ol>
<p>需要注意的是 <code>discriminator</code> 是从第二个才开始显示的。比如在 <code>Foo</code> 类的 <code>bar</code> 方法中定义了两个 block,那么这两个 block 的 mangle name 就是 <code>__10_-[Foo bar]_block_invoke</code> 和 <code>__10_-[Foo bar]_block_invoke_2</code>。在 gcc 里稍有区别,第一个 block 的 mangle name 也会显示 <code>discriminator</code>。前面的 “10” 是方法名 <code>-[Foo bar]</code> 的字符串长度。这部分属于 Objective-C 方法名的 mangle name 规则,C++ 函数也有类似的规则,不仅用数字保存字符串长度,还有其他字母表示方法类型和参数类型等。这里不展开细讲了,看源码都能找到。</p>
<p>于是只要能拿到 mangle name,就能推断出定义 block 代码所处的位置咯。不带 <code>__</code> 的就是全局 block 咯?我并没有打算写代码来解析下 mangle name 的规则,还是交给调用方去使用吧。</p>
<p><a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 的 <code>BHToken</code> 类新增了 <code>mangleName</code> 属性,只需要使用原始的 invoke 函数地址作为 Key 即可从字典里获得这个 block 对应的 <code>mangleName</code>:</p>
<figure class="highlight erlang"><table><tr><td class="gutter"><pre><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="params">(NSString *)</span>mangleName</span><br><span class="line">&#123;</span><br><span class="line"> if <span class="params">(!_mangleName)</span> &#123;</span><br><span class="line"> pthread_mutex_lock<span class="params">(&amp;block_invoke_mangle_cache_mutex)</span>;</span><br><span class="line"> if <span class="params">(_originInvoke)</span> &#123;</span><br><span class="line"> _mangleName = [block_invoke_mangle_cache objectForKey:<span class="params">(__bridge id)</span>_originInvoke];</span><br><span class="line"> &#125;</span><br><span class="line"> pthread_mutex_unlock<span class="params">(&amp;block_invoke_mangle_cache_mutex)</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> return _mangleName;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>这里有个多次 hook 的问题。所谓的 <code>_originInvoke</code> 只是这次 hook 相对的原始实现函数,它可能处在多次 hook 中的一环,而不是最原始的 block 实现,此时是拿不到 <code>mangleName</code>。所以需要用第一次 hook block 的 token 来获取 <code>mangleName</code>。PS:想搞倒是可以搞,把 hook block 产生的 token 都保存起来,然后按照 <code>_originInvoke</code> 和 <code>_replacementInvoke</code> 顺藤摸瓜就行,不难,顺便还能解决 <code>remove</code> 操作的顺序问题。我懒的搞,目前场景太小意义不大。</p>
<p>想了解 <a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 原理的,可以看这篇文章:<a href="http://yulingtianxia.com/blog/2018/02/28/Hook-Objective-C-Block-with-Libffi/">Hook Objective-C Block with Libffi</a>。(继续疯狂炒冷饭。。。)</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>目前 <a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 和 <a href="https://github.com/yulingtianxia/BlockTracker" target="_blank" rel="external">BlockTracker</a> 都已经支持获取 block 的 mangle name 了。</p>
<p>不知道 dyld 3 强制应用后会不会对此有影响,我就是遍历这个二进制文件自己的符号表而已呀,动态重定向的我又不 care。反正 fishhook 到时候估计是 gg 了,因为 dyld 3 会在加载时解析所有符号表,也就是固定下来了,没跳板了。</p>
<p>然而我还是这么菜,赶在月底前写了个篇幅短小全是常识的大水文。。。</p>
<p>本文完。。。</p>
<p>太水了!!!</p>
</content>
<summary type="html">
<p>之前写了一篇文章<a href="http://yulingtianxia.com/blog/2018/03/31/Track-Block-Arguments-of-Objective-C-Method/">《追踪 Objective-C 方法中的 Block 参数对象》</a>,利用 <a href="https://github.com/yulingtianxia/BlockHook">BlockHook</a> 和 Objective-C 的动态特性实现对 block 对象执行和销毁的追踪。本文在此基础上,通过 Mach-O 文件格式获取 Mangle Name 并根据 Clang 源码实现对其解析,探寻如何追踪 block 代码定义的位置。</p>
<p>主要代码已经整合到 <a href="https://github.com/yulingtianxia/BlockHook">BlockHook</a> 1.0.2 版本中。</p>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
</entry>
<entry>
<title>MessageThrottle Performance Benchmark and Optimization</title>
<link href="http://yulingtianxia.com/blog/2018/05/31/MessageThrottle-Performance-Benchmark-and-Optimization/"/>
<id>http://yulingtianxia.com/blog/2018/05/31/MessageThrottle-Performance-Benchmark-and-Optimization/</id>
<published>2018-05-30T18:01:50.000Z</published>
<updated>2018-09-15T08:28:13.562Z</updated>
<content type="html"><p><a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 是我开发的Objective-C 节流限频组件,其原理基于 Hook 消息转发流程,所以相比直接调用方法,会有一些性能上的损耗。本篇文章记录了对其性能进行测试的结果,并通过使用 <code>NSMapTable</code> 改进存储结构和缓存来对性能进行大幅度的优化。</p>
<p>这是你从未体验过的船新版本。</p>
<a id="more"></a>
<p>关于 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 最初的实现原理可以参考 <a href="http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/">Objective-C Message Throttle and Debounce</a>。</p>
<h2 id="Benchmark"><a href="#Benchmark" class="headerlink" title="Benchmark"></a>Benchmark</h2><p>Xcode 自带的单元测试框架可以很方便的测量一个方法的执行效率,<code>measureBlock</code> 里的代码会被执行十次,测试结束后会得到每次执行耗时,以及平均数和方差。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)testPerformanceExample &#123;</span><br><span class="line"> <span class="comment">// This is an example of a performance test case.</span></span><br><span class="line"> <span class="built_in">NSDate</span> *date = [<span class="built_in">NSDate</span> date];</span><br><span class="line"> [<span class="keyword">self</span> measureBlock:^&#123;</span><br><span class="line"> <span class="comment">// Put the code you want to measure the time of here.</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">1000</span>; i ++) &#123;</span><br><span class="line"> <span class="keyword">@autoreleasepool</span> &#123;</span><br><span class="line"> [<span class="keyword">self</span>.sstub foo:date];</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>性能损耗大多发生在消息转发流程上的处理,为了能够校准基线,需要让每次消息发送都执行。MessageThrottle 1.2.0 刚刚支持了让某些条件下消息永远执行的特性:</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)setUp &#123;</span><br><span class="line"> [<span class="keyword">super</span> setUp];</span><br><span class="line"> <span class="comment">// Put setup code here. This method is called before the invocation of each test method in the class.</span></span><br><span class="line"> self.sstub = [SuperStub <span class="keyword">new</span>];</span><br><span class="line"> MTRule *rule = [self.sstub <span class="string">mt_limitSelector:</span><span class="meta">@selector</span>(<span class="string">foo:</span>) <span class="string">oncePerDuration:</span><span class="number">0.01</span> <span class="string">usingMode:</span>MTPerformModeDebounce];</span><br><span class="line"> rule.alwaysInvokeBlock = ^(MTRule *rule, NSDate *date) &#123;</span><br><span class="line"> <span class="keyword">return</span> YES; <span class="comment">// 让消息永远都执行</span></span><br><span class="line"> &#125;;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>通过调整 <code>foo:</code> 方法的耗时来得到调用不同耗时函数的测试结果。</p>
<figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">-</span> (void)<span class="selector-tag">foo</span><span class="selector-pseudo">:(NSDate</span> *)<span class="selector-tag">arg</span> &#123;</span><br><span class="line"> <span class="selector-attr">[NSThread sleepForTimeInterval:0.0001]</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最终得到一组数据,测试机器为 iPhone 8 plus。</p>
<table>
<thead>
<tr>
<th>执行模式\被调用方法耗时</th>
<th>0.0001</th>
<th>0.001</th>
</tr>
</thead>
<tbody>
<tr>
<td>不使用 MT</td>
<td>0.118(baseline)</td>
<td>1.17(baseline)</td>
</tr>
<tr>
<td>MT 立即执行</td>
<td>0.135(14.4%worse)</td>
<td>1.33(13.8%worse)</td>
</tr>
<tr>
<td>MT debounce 0.01s</td>
<td>0.0281(76.2%better)</td>
<td>0.0279(97.6%better)</td>
</tr>
</tbody>
</table>
<ol>
<li>测试的基准数据为不使用 MessageThottle,直接调用方法。</li>
<li>使用 MessageThottle 后,消息转发流程会带来多余的耗时会导致性能下降,而且被调用方法耗时越少,性能下降得越明显(比较两列数据)。</li>
<li>如果加了消息限频,会忽略掉一部分调用,这样当出现大量频繁调用时,方法真正执行的次数很少,性能反而大大提升了(第三行数据)</li>
</ol>
<h2 id="Optimization"><a href="#Optimization" class="headerlink" title="Optimization"></a>Optimization</h2><p>通过性能优化,将消息转发流程产生的耗时降低了将近 50%。并加强了线程安全。</p>
<table>
<thead>
<tr>
<th>执行模式\被调用方法耗时</th>
<th>0.0001</th>
<th>0.001</th>
</tr>
</thead>
<tbody>
<tr>
<td>不使用 MT</td>
<td>0.118(baseline)</td>
<td>1.17(baseline)</td>
</tr>
<tr>
<td>MT 立即执行</td>
<td>0.135(14.4%worse)</td>
<td>1.33(13.8%worse)</td>
</tr>
<tr>
<td>性能优化后</td>
<td>0.126(6.88%worse)</td>
<td>1.25(6.93%worse)</td>
</tr>
</tbody>
</table>
<p>为了方便管理和查看所有的 <code>MTRule</code>,使用了 <code>MTEngine</code> 单例进行中心化的管理。获取一个 <code>MTRule</code> 之前,需要先用 <code>target</code> 和 <code>selector</code> 生成一个描述字符串,然后用这个字符串作为 Key 在 <code>MTEngine</code> 的字典里查询对应的 <code>MTRule</code> 对象。每次应用和废除规则、消息发送时都要频繁从 <code>MTEngine</code> 获取 <code>MTRule</code> 对象,由此也产生了大量开销。这里的性能瓶颈主要有两点:</p>
<ol>
<li>生成描述字符串造成的开销。</li>
<li>从 <code>MTEngine</code> 加锁的字典获取 <code>MTRule</code> 的等待开销。</li>
</ol>
<p>应用和废除规则的时候,这两点开销并不明显。但当所有应用规则的消息发送都要经过这两步的时候,这俨然成了拥堵的重灾区。当然治理方案也是相对的:</p>
<ol>
<li><p>改进 <code>MTEngine</code> 中字典的存储结构,使用 <code>NSMapTable</code> 替换 <code>NSMutableDictionary</code>。因为 <code>NSMapTable</code> 支持将任意指针作为 Key 且无需持有,可以将 <code>target</code> 作为 Key,Value 为这个 <code>target</code> 对应的 <code>selector</code> 集合。<code>MTEngine</code> 不再持有 <code>MTRule</code> 对象,而只是存储了所有应用规则的 <code>target</code> 及其 <code>selector</code>。而 <code>MTRule</code> 对象改为由其 <code>target</code> 通过 AssociatedObject 的方式持有,可以很方便通过 <code>selector</code> 存取。当 <code>target</code> 销毁后,它关联的 <code>MTRule</code> 对象也会被销毁,<code>NSMapTable</code> 也会自动移除那些键或值为 <code>nil</code> 的数据。下面是 <code>MTEngine</code> 封装了 <code>NSMapTable</code> 字典对应的便捷方法。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初始化</span></span><br><span class="line">_targetSELs = [<span class="built_in">NSMapTable</span> weakToStrongObjectsMapTable];</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">//添加 target-selector 记录</span></span><br><span class="line">- (<span class="keyword">void</span>)addSelector:(SEL)selector onTarget:(<span class="keyword">id</span>)target</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">if</span> (!target) &#123;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="built_in">NSMutableSet</span> *selectors = [<span class="keyword">self</span>.targetSELs objectForKey:target];</span><br><span class="line"> <span class="keyword">if</span> (!selectors) &#123;</span><br><span class="line"> selectors = [<span class="built_in">NSMutableSet</span> set];</span><br><span class="line"> &#125;</span><br><span class="line"> [selectors addObject:<span class="built_in">NSStringFromSelector</span>(selector)];</span><br><span class="line"> [<span class="keyword">self</span>.targetSELs setObject:selectors forKey:target];</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//移除 target-selector 记录</span></span><br><span class="line">- (<span class="keyword">void</span>)removeSelector:(SEL)selector onTarget:(<span class="keyword">id</span>)target</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">if</span> (!target) &#123;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="built_in">NSMutableSet</span> *selectors = [<span class="keyword">self</span>.targetSELs objectForKey:target];</span><br><span class="line"> <span class="keyword">if</span> (!selectors) &#123;</span><br><span class="line"> selectors = [<span class="built_in">NSMutableSet</span> set];</span><br><span class="line"> &#125;</span><br><span class="line"> [selectors removeObject:<span class="built_in">NSStringFromSelector</span>(selector)];</span><br><span class="line"> [<span class="keyword">self</span>.targetSELs setObject:selectors forKey:target];</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//是否存在 target-selector 记录</span></span><br><span class="line">- (<span class="built_in">BOOL</span>)containsSelector:(SEL)selector onTarget:(<span class="keyword">id</span>)target</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">return</span> [[<span class="keyword">self</span>.targetSELs objectForKey:target] containsObject:<span class="built_in">NSStringFromSelector</span>(selector)];</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//是否存在 target-selector 记录,未指定具体 target,但 target 的类型为 cls 即可</span></span><br><span class="line">- (<span class="built_in">BOOL</span>)containsSelector:(SEL)selector onTargetsOfClass:(Class)cls</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">id</span> target <span class="keyword">in</span> [[<span class="keyword">self</span>.targetSELs keyEnumerator] allObjects]) &#123;</span><br><span class="line"> <span class="keyword">if</span> (!mt_object_isClass(target) &amp;&amp;</span><br><span class="line"> [target isMemberOfClass:cls] &amp;&amp;</span><br><span class="line"> [[<span class="keyword">self</span>.targetSELs objectForKey:target] containsObject:<span class="built_in">NSStringFromSelector</span>(selector)]) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li><p>每个 <code>MTRule</code> 有自己独立的递归锁,这样避免了在 <code>forwardInvocation</code> 里千军万马过独木桥的拥堵,且不妨碍递归调用的场景。存取 <code>MTEngine</code> 的字典依然使用普通的互斥锁。这两个锁都使用性能较好的 <code>pthread_mutex_t</code> 实现。</p>
<figure class="highlight dns"><table><tr><td class="gutter"><pre><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"> // 初始化递归锁</span><br><span class="line"> pthread_mutexattr_t attr<span class="comment">;</span></span><br><span class="line"> pthread_mutexattr_init(&amp;attr)<span class="comment">;</span></span><br><span class="line"> pthread_mutexattr_settype(&amp;attr, PTHREAD_MUTEX_RECURSIVE)<span class="comment">;</span></span><br><span class="line"> pthread_mutex_t mutex = mtDealloc.invokeLock<span class="comment">;</span></span><br><span class="line"> pthread_mutex_init(&amp;mutex, &amp;attr)<span class="comment">;</span></span><br><span class="line"> objc_setAssociatedObject(rule.target, rule.selector, mtDealloc, OBJC_ASSOCIATION_RETAIN)<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"> // 消息转发时保证线程安全</span><br><span class="line"> static void mt_forwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation)</span><br><span class="line"> &#123;</span><br><span class="line"> SEL originalSelector = invocation.selector<span class="comment">;</span></span><br><span class="line"> SEL fixedOriginalSelector = mt_aliasForSelector(originalSelector)<span class="comment">;</span></span><br><span class="line"> if (![assignSlf respondsToSelector:fixedOriginalSelector]) &#123;</span><br><span class="line"> mt_executeOrigForwardInvocation(assignSlf, selector, invocation)<span class="comment">;</span></span><br><span class="line"> return<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> MTDealloc *mtDealloc = objc_getAssociatedObject(invocation.target, selector)<span class="comment">;</span></span><br><span class="line"> pthread_mutex_t mutex = mtDealloc.invokeLock<span class="comment">;</span></span><br><span class="line"> pthread_mutex_lock(&amp;mutex)<span class="comment">;</span></span><br><span class="line"> mt_handleInvocation(invocation, fixedOriginalSelector)<span class="comment">;</span></span><br><span class="line"> pthread_mutex_unlock(&amp;mutex)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure>
</li>
</ol>
<p><code>MTEngine</code> 中字典的存储结构的改进不仅提高了性能,还让设计思路更清晰。在添加或废除规则的时候,旧方案需要遍历所有的 <code>MTRule</code> 对象,然后通过检查 <code>target</code> 和 <code>selector</code> 来判断规则是否相互干扰;新方案直接存储了 <code>target</code> 和对应的 <code>selector</code> 数组,声明如下:</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">NSMapTable</span>&lt;<span class="keyword">id</span>, <span class="built_in">NSMutableSet</span>&lt;<span class="built_in">NSString</span> *&gt; *&gt; *targetSELs;</span><br></pre></td></tr></table></figure>
<p>这样的存储方式可以更高效地找到某个对象或类的某个方法是否被限频了,增删规则也更快。</p>
<p>在 Hook 某个方法的时候,会给它生成一个新的方法名,这就又涉及到字符串拼接的开销。解决方案是使用缓存来映射两个 <code>SEL</code> 指针,又要用到 <code>NSMapTable</code> 大显神威了。这又将节省 6% 左右的 CPU 耗时!需要注意的是创建 <code>NSMapTable</code> 时的选项,以及存取时的类型强转:</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 初始化 NSMapTable 缓存</span></span><br><span class="line">_aliasSelectorCache = [<span class="built_in">NSMapTable</span> mapTableWithKeyOptions:<span class="built_in">NSPointerFunctionsOpaqueMemory</span> | <span class="built_in">NSMapTableObjectPointerPersonality</span> valueOptions:<span class="built_in">NSPointerFunctionsOpaqueMemory</span> | <span class="built_in">NSMapTableObjectPointerPersonality</span>];</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在方法内部使用缓存优化性能</span></span><br><span class="line"><span class="keyword">static</span> SEL mt_aliasForSelector(SEL selector)</span><br><span class="line">&#123;</span><br><span class="line"> pthread_mutex_lock(&amp;alias_selector_mutex);</span><br><span class="line"> SEL aliasSelector = (__bridge <span class="keyword">void</span> *)[MTEngine.defaultEngine.aliasSelectorCache objectForKey:(__bridge <span class="keyword">id</span>)(<span class="keyword">void</span> *)selector];</span><br><span class="line"> <span class="keyword">if</span> (!aliasSelector) &#123;</span><br><span class="line"> <span class="built_in">NSString</span> *selectorName = <span class="built_in">NSStringFromSelector</span>(selector);</span><br><span class="line"> aliasSelector = <span class="built_in">NSSelectorFromString</span>([<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"__mt_%@"</span>, selectorName]);</span><br><span class="line"> [MTEngine.defaultEngine.aliasSelectorCache setObject:(__bridge <span class="keyword">id</span>)(<span class="keyword">void</span> *)aliasSelector forKey:(__bridge <span class="keyword">id</span>)(<span class="keyword">void</span> *)selector];</span><br><span class="line"> &#125;</span><br><span class="line"> pthread_mutex_unlock(&amp;alias_selector_mutex);</span><br><span class="line"> <span class="keyword">return</span> aliasSelector;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>可能有人会担心直接缓存 <code>SEL</code> 指针会不会命中率很低。因为所有名字相同的方法都拥有同一个唯一的 <code>SEL</code>,所以可以很快速地用直接指针地址判等。可以参考<a href="https://stackoverflow.com/questions/11051528/understanding-uniqueness-of-selectors-in-objective-c?utm_medium=organic&amp;utm_source=google_rich_qa&amp;utm_campaign=google_rich_qa" target="_blank" rel="external">这里</a>。</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>更新 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 到最新版即可获取到更快更强更安全的 Objective 消息节流限频功能,一行代码搞定频繁调用的问题。</p>
<p>新版本在废除消息的时候,也增强了对合法性和安全性的检查。(说白了就是改 bug)</p>
<p>理论上我的另一个组件 <a href="https://github.com/yulingtianxia/BlockTracker" target="_blank" rel="external">BlockTracker</a> 也可以按照本文的方案优化性能了,嘿嘿,有时间搞下。</p>
</content>
<summary type="html">
<p><a href="https://github.com/yulingtianxia/MessageThrottle">MessageThrottle</a> 是我开发的Objective-C 节流限频组件,其原理基于 Hook 消息转发流程,所以相比直接调用方法,会有一些性能上的损耗。本篇文章记录了对其性能进行测试的结果,并通过使用 <code>NSMapTable</code> 改进存储结构和缓存来对性能进行大幅度的优化。</p>
<p>这是你从未体验过的船新版本。</p>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
</entry>
<entry>
<title>Colorful Rounded Rect Dash Border</title>
<link href="http://yulingtianxia.com/blog/2018/04/30/Colorful-Rounded-Rect-Dash-Border/"/>
<id>http://yulingtianxia.com/blog/2018/04/30/Colorful-Rounded-Rect-Dash-Border/</id>
<published>2018-04-30T09:25:44.000Z</published>
<updated>2018-09-15T08:28:13.549Z</updated>
<content type="html"><p>产品经理要求做个能展示进度的分段彩色外环,大概长这样:</p>
<p><img src="https://github.com/yulingtianxia/YXYDashLayer/blob/master/Assets/YXYDashLayer.gif?raw=true" alt=""></p>
<p>花了两天左右来实现和优化,记录下踩坑经历。</p>
<p>组件已经开源,取个名字叫 <code>YXYDashLayer</code> 吧:<a href="https://github.com/yulingtianxia/YXYDashLayer" target="_blank" rel="external">https://github.com/yulingtianxia/YXYDashLayer</a></p>
<a id="more"></a>
<h2 id="接口设计"><a href="#接口设计" class="headerlink" title="接口设计"></a>接口设计</h2><p>因为考虑到要做成稍微通用一些的组件,最底层的 <code>YXYMaskDashLayer</code> 接口设计如下。其他类的属性也都是对它的封装。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span><br><span class="line"> 分段的间隙</span><br><span class="line"> */</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="built_in">CGFloat</span> dashGap;</span><br><span class="line"><span class="comment">/**</span><br><span class="line"> 线宽</span><br><span class="line"> */</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="built_in">CGFloat</span> dashWidth;</span><br><span class="line"><span class="comment">/**</span><br><span class="line"> 矩形的圆角半径</span><br><span class="line"> */</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="built_in">CGFloat</span> dashCornerRadius;</span><br><span class="line"><span class="comment">/**</span><br><span class="line"> 分段总数</span><br><span class="line"> */</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="built_in">NSUInteger</span> totalCount;</span><br><span class="line"><span class="comment">/**</span><br><span class="line"> 需要显示哪些分段的 index</span><br><span class="line"> */</span></span><br><span class="line"><span class="keyword">@property</span> (<span class="keyword">nonatomic</span>) <span class="built_in">NSArray</span>&lt;<span class="built_in">NSNumber</span> *&gt; *showIndexes;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span><br><span class="line"> 刷新整个Layer</span><br><span class="line"> */</span></span><br><span class="line">- (<span class="keyword">void</span>)refresh;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span><br><span class="line"> 仅刷新 Dash 的 totalCount、dashGap 和 showIndexes</span><br><span class="line"> */</span></span><br><span class="line">- (<span class="keyword">void</span>)refreshDash;</span><br></pre></td></tr></table></figure>
<p>因为有些属性改变后并不需要重新绘制 path,为了实现更好的性能,所以还提供了一个只刷新 dash 数据的接口 <code>refreshDash</code>。</p>
<p>具体使用的例子可以运行 Demo 程序。</p>
<h2 id="思路很重要"><a href="#思路很重要" class="headerlink" title="思路很重要"></a>思路很重要</h2><p>之前的样式是个圆形的分段外环,而且是纯色的,看了下以前的代码,是按照弧度均分后,从顶部开始按顺时针一段一段 path 组合起来的。用 <code>UIBezierPath</code> 的 <code>+ bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise:</code> 方法即可画出来。</p>
<p>然而现在改成了圆角矩形的,要按照周长均分来画分段,实现方式完全不同。因为圆形只是圆角矩形的一种特殊情况,所以需要另一种更通用的实现方式。因为借鉴了圆形分段一段段画的思想,最开始想到的也是一段段画圆角矩形,需要把整个圆角矩形划分成 9 个区域(四个四分之一圆弧,四条直线,顶部直线需要分成两块),还要对圆角和直线部分的边界处理,涉及到大量的计算。我刚开始要这么干的时候,觉得这么做有点笨,肯定有更简单的方案。</p>
<p><code>CAShapeLayer</code> 的 <code>lineDashPattern</code> 和 <code>lineDashPhase</code> 属性就可以实现这个需求了,之前一直被旧代码的方案限制了思路。真是退一步海阔天空啊。原本跟产品说这有 5 天工作量,结果半个小时就写出个 demo,哈哈。然后用剩下的时间继续完善打磨,做成通用组件。</p>
<h2 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h2><ol>
<li>先用贝塞尔曲线画一个圆角矩形(就叫 <code>path</code> 吧)</li>
<li><code>path.CGPath</code> 赋值给 <code>CAShapeLayer</code> 实例(就叫 <code>maskLayer</code> 吧)</li>
<li>根据线宽、分段间隙、<code>path</code> 周长、总分段数、要展示的分段 index,可计算出 <code>lineDashPattern</code> 和 <code>lineDashPhase</code> 的值,刷新 <code>maskLayer</code></li>
<li>将 <code>maskLayer</code> 赋值给 <code>CAGradientLayer</code> 实例的 <code>mask</code>。调整 <code>colors</code> 等属性即可实现一个彩色渐变分段圆角矩形外圈。</li>
<li>将多个这样的 <code>CAGradientLayer</code> 实例重叠在一起,即可实现个别分段『高亮』效果。比如一个 layer 当做底色,另一个放上面当做灰色进度条。(PS:本文最开始的 gif 就是这样)</li>
</ol>
<p>这里面踩坑最多的就是前 3 个步骤,计算时需要考虑到一些边界条件。</p>
<h3 id="画圆角矩形的坑"><a href="#画圆角矩形的坑" class="headerlink" title="画圆角矩形的坑"></a>画圆角矩形的坑</h3><p><code>+ bezierPathWithRoundedRect:cornerRadius:</code> 方法是可以直接画出一个圆角矩形的,但是路径的起始点并没确定。表面上看上去是从顶部直线左端开始顺时针画,然而会有向右的一些偏差。这样就无法精确计算出 <code>lineDashPhase</code> 的值,导致画出来的效果不对称了。</p>
<p>于是我这里干脆自己画个圆角矩形,代码也很简单。由于要考虑到线宽,所以需要计算下真正的圆角半径和外接矩形尺寸,顺时针画四段直线四段四分之一圆弧即可。下面的代码是写在 <code>CAShapeLayer</code> 子类里的:</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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"><span class="built_in">UIBezierPath</span> *path = [<span class="built_in">UIBezierPath</span> bezierPath];</span><br><span class="line"><span class="keyword">self</span>.dashRect = <span class="built_in">CGRectInset</span>(<span class="keyword">self</span>.bounds, <span class="keyword">self</span>.dashWidth / <span class="number">2</span>, <span class="keyword">self</span>.dashWidth / <span class="number">2</span>);</span><br><span class="line"><span class="built_in">CGFloat</span> width = <span class="keyword">self</span>.dashRect.size.width;</span><br><span class="line"><span class="built_in">CGFloat</span> height = <span class="keyword">self</span>.dashRect.size.height;</span><br><span class="line"><span class="keyword">self</span>.realDashCornerRadius = MIN(<span class="keyword">self</span>.dashCornerRadius - <span class="keyword">self</span>.dashWidth / <span class="number">2</span>, width / <span class="number">2</span>);</span><br><span class="line"><span class="keyword">self</span>.realDashCornerRadius = MAX(<span class="number">0</span>, <span class="keyword">self</span>.realDashCornerRadius);</span><br><span class="line"><span class="built_in">CGPoint</span> center = <span class="built_in">CGPointMake</span>(<span class="keyword">self</span>.frame.size.width / <span class="number">2</span>, <span class="keyword">self</span>.frame.size.height / <span class="number">2</span>);</span><br><span class="line"> </span><br><span class="line">[path moveToPoint:<span class="built_in">CGPointMake</span>(center.x - width / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius, center.y - height / <span class="number">2</span>)];</span><br><span class="line"> </span><br><span class="line">[path addLineToPoint:<span class="built_in">CGPointMake</span>(center.x + width / <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius, center.y - height / <span class="number">2</span>)];</span><br><span class="line"> </span><br><span class="line">[path addArcWithCenter:<span class="built_in">CGPointMake</span>(center.x + width / <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius, center.y - height / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius) radius:<span class="keyword">self</span>.realDashCornerRadius startAngle:M_PI_2 * <span class="number">3</span> endAngle:<span class="number">0</span> clockwise:<span class="literal">YES</span>];</span><br><span class="line"> </span><br><span class="line">[path addLineToPoint:<span class="built_in">CGPointMake</span>(center.x + width / <span class="number">2</span>, center.y + height / <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius)];</span><br><span class="line"> </span><br><span class="line">[path addArcWithCenter:<span class="built_in">CGPointMake</span>(center.x + width / <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius, center.y + height / <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius) radius:<span class="keyword">self</span>.realDashCornerRadius startAngle:<span class="number">0</span> endAngle:M_PI_2 clockwise:<span class="literal">YES</span>];</span><br><span class="line"> </span><br><span class="line">[path addLineToPoint:<span class="built_in">CGPointMake</span>(center.x - width / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius, center.y + height / <span class="number">2</span>)];</span><br><span class="line"> </span><br><span class="line">[path addArcWithCenter:<span class="built_in">CGPointMake</span>(center.x - width / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius, center.y + height / <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius) radius:<span class="keyword">self</span>.realDashCornerRadius startAngle:M_PI_2 endAngle:M_PI clockwise:<span class="literal">YES</span>];</span><br><span class="line"> </span><br><span class="line">[path addLineToPoint:<span class="built_in">CGPointMake</span>(center.x - width / <span class="number">2</span>, center.y - height / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius)];</span><br><span class="line"> </span><br><span class="line">[path addArcWithCenter:<span class="built_in">CGPointMake</span>(center.x - width / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius, center.y - height / <span class="number">2</span> + <span class="keyword">self</span>.realDashCornerRadius) radius:<span class="keyword">self</span>.realDashCornerRadius startAngle:M_PI endAngle:M_PI_2 * <span class="number">3</span> clockwise:<span class="literal">YES</span>];</span><br><span class="line"></span><br><span class="line"><span class="keyword">self</span>.totalLength = (width + height) * <span class="number">2</span> - <span class="keyword">self</span>.realDashCornerRadius * <span class="number">8</span> + M_PI * <span class="keyword">self</span>.realDashCornerRadius * <span class="number">2</span>;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">self</span>.lineWidth = <span class="keyword">self</span>.dashWidth;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">self</span>.path = path.CGPath;</span><br></pre></td></tr></table></figure>
<p>上面的代码也计算出了周长,用于下一步的分段长度计算。</p>
<h3 id="处理边界值"><a href="#处理边界值" class="headerlink" title="处理边界值"></a>处理边界值</h3><p>圆角矩形的周长已经算出来了,外部提供了 <code>dashGap</code>,但是绘制时真正的分段间隙是需要考虑到线宽和分段总数的。因为线的边缘会有个半圆,半径为二分之一线宽。当只有一个分段的时候画一个完整的圆角矩形,不需要有间隙了。如果分段总数过多导致计算的分段长度 <code>pieceLength</code> 小于 0,需要计算能展示出来分段数的最大值 <code>realTotalCount</code>,并重新计算分段长度 <code>pieceLength</code>。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">CGFloat</span> realDashGap = (<span class="keyword">self</span>.totalCount == <span class="number">1</span>) ? <span class="number">0</span> : <span class="keyword">self</span>.dashGap + <span class="keyword">self</span>.dashWidth;</span><br><span class="line"><span class="built_in">NSUInteger</span> realTotalCount = <span class="keyword">self</span>.totalCount;</span><br><span class="line"><span class="built_in">CGFloat</span> pieceLength = <span class="keyword">self</span>.totalLength / <span class="keyword">self</span>.totalCount - realDashGap;</span><br><span class="line"><span class="keyword">if</span> (pieceLength &lt; <span class="number">0</span>) &#123;</span><br><span class="line"> pieceLength = <span class="number">0</span>;</span><br><span class="line"> realTotalCount = <span class="keyword">self</span>.totalLength / realDashGap;</span><br><span class="line"> pieceLength = <span class="keyword">self</span>.totalLength / realTotalCount - realDashGap;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Can't show! Reduce total count or dash gap! Real Total Count: %lu, Real Dash Gap:%ff"</span>, (<span class="keyword">unsigned</span> <span class="keyword">long</span>)realTotalCount, realDashGap);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="Dash-策略"><a href="#Dash-策略" class="headerlink" title="Dash 策略"></a>Dash 策略</h3><p><code>lineDashPhase</code> 可以理解为 dash 距离 path 起始点的距离,想让 dash 从顶部中间开始,需要设置初始值:二分之一外接矩形宽度的减去圆角半径,再加上二分之一 <code>realDashGap</code>。</p>
<figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">self.lineDashPhase </span>= - (<span class="keyword">self.dashRect.size.width </span>/ <span class="number">2</span> - <span class="keyword">self.realDashCornerRadius </span>+ realDashGap / <span class="number">2</span>)<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>然后就是顺时针画需要展示的分段。输入是一个 <code>showIndexes</code> 数组,比如一共有 10 个分段,想展示的是前两个和最后一个分段,那么 <code>showIndexes</code> 的内容就是 <code>@[@0, @1, @9]</code>。此时 <code>lineDashPattern</code> 的值就应该是(<code>pieceLength</code> 就是每个分段的长度):</p>
<figure class="highlight gherkin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@[</span><span class="meta">@pieceLength,</span> <span class="meta">@realDashGap,</span> <span class="meta">@pieceLength,</span> <span class="meta">@(realDashGap</span> <span class="symbol">*</span> 8 + pieceLength <span class="symbol">*</span> 7), <span class="meta">@pieceLength,</span> <span class="meta">@realDashGap]</span></span><br></pre></td></tr></table></figure>
<p>如果 <code>showIndexes</code> 的内容是 <code>@[@1, @2, @9]</code>,可不可以让 <code>lineDashPattern</code> 数组前面填 <code>@0</code> 呢?</p>
<figure class="highlight gherkin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@[</span><span class="meta">@0,</span> <span class="meta">@(pieceLength</span> + realDashGap), <span class="meta">@pieceLength,</span> <span class="meta">@realDashGap,</span> <span class="meta">@pieceLength,</span> <span class="meta">@(realDashGap</span> <span class="symbol">*</span> 7 + pieceLength <span class="symbol">*</span> 6), <span class="meta">@pieceLength,</span> <span class="meta">@realDashGap]</span></span><br></pre></td></tr></table></figure>
<p>因为把 <code>lineCap</code> 设为了 <code>kCALineCapRound</code>,即便长度为 0 路径也会展示成为一个圆点,半径就是线宽。然而安卓系统对应的 API 在这种情况就不会绘制出圆点。为此 iOS 更麻烦一点,需要再次调整<code>lineDashPhase</code> 的值来『越过』前面几个分段。具体的实现代码如下:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">NSMutableArray</span>&lt;<span class="type">NSNumber</span> *&gt; *dashPattern = [<span class="type">NSMutableArray</span> arrayWithCapacity:<span class="number">2</span> * realTotalCount];</span><br><span class="line"><span class="type">NSInteger</span> needsMovePhaseCount = <span class="number">0</span>;</span><br><span class="line"><span class="keyword">for</span> (int i = <span class="number">0</span>; i &lt; realTotalCount; i ++) &#123;</span><br><span class="line"> <span class="keyword">if</span> ([<span class="keyword">self</span>.showIndexes containsObject:@(i)]) &#123;</span><br><span class="line"> [dashPattern addObject:@(pieceLength)];</span><br><span class="line"> [dashPattern addObject:@(realDashGap)];</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> <span class="keyword">if</span> (dashPattern.<span class="built_in">count</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line"> dashPattern[dashPattern.<span class="built_in">count</span> - <span class="number">1</span>] = @(dashPattern[dashPattern.<span class="built_in">count</span> - <span class="number">1</span>].doubleValue + pieceLength + realDashGap);</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> <span class="keyword">self</span>.lineDashPhase -= (pieceLength + realDashGap);</span><br><span class="line"> needsMovePhaseCount ++;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> (needsMovePhaseCount &gt; <span class="number">0</span> &amp;&amp; dashPattern.<span class="built_in">count</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line"> dashPattern[dashPattern.<span class="built_in">count</span> - <span class="number">1</span>] = @(dashPattern[dashPattern.<span class="built_in">count</span> - <span class="number">1</span>].doubleValue + (pieceLength + realDashGap) * needsMovePhaseCount);</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="keyword">if</span> (<span class="keyword">self</span>.showIndexes.<span class="built_in">count</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line"> <span class="keyword">self</span>.lineDashPattern = dashPattern;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>一开始做这种需求我是拒绝的,交互有点复杂啊,用户看不懂啊,说白了还是不知道咋实现心里没底啊!然而要是没有这种需求,也就没有这篇月末大水文了。</p>
<p>我真是越来越水了,只会写 UI 了,还是搞底层的逆向大佬们牛逼啊!Hank 老师教教我!</p>
</content>
<summary type="html">
<p>产品经理要求做个能展示进度的分段彩色外环,大概长这样:</p>
<p><img src="https://github.com/yulingtianxia/YXYDashLayer/blob/master/Assets/YXYDashLayer.gif?raw=true" alt=""></p>
<p>花了两天左右来实现和优化,记录下踩坑经历。</p>
<p>组件已经开源,取个名字叫 <code>YXYDashLayer</code> 吧:<a href="https://github.com/yulingtianxia/YXYDashLayer">https://github.com/yulingtianxia/YXYDashLayer</a></p>
</summary>
<category term="iOS" scheme="http://yulingtianxia.com/tags/iOS/"/>
</entry>
<entry>
<title>追踪 Objective-C 方法中的 Block 参数对象</title>
<link href="http://yulingtianxia.com/blog/2018/03/31/Track-Block-Arguments-of-Objective-C-Method/"/>
<id>http://yulingtianxia.com/blog/2018/03/31/Track-Block-Arguments-of-Objective-C-Method/</id>
<published>2018-03-31T15:44:39.000Z</published>
<updated>2018-09-15T08:28:13.796Z</updated>
<content type="html"><p>很多方法最后一个参数是类似于 <code>completionBlock</code> 这种回调,然而有些 API 实现一些异常逻辑时会忘记调用传入的 Block 参数(当然这肯定是 bug 啦),或者存在多次调用。在调试的时候可能会碰到这种大坑,需要追踪下 Block 参数何时调用了,甚至是否调用过。如果不方便直接在 Block 实现中加代码,或者没有源码的情况下,就需要无侵入式地追踪 Block 参数对象。</p>
<p><a href="https://github.com/yulingtianxia/BlockTracker" target="_blank" rel="external">BlockTracker</a> 可以追踪方法调用时传入的 Block 类型的参数的执行和销毁。基于 <a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 实现。本文讲述了它的使用方法和实现原理。</p>
<a id="more"></a>
<h2 id="使用方法"><a href="#使用方法" class="headerlink" title="使用方法"></a>使用方法</h2><p>只需要调用 <code>bt_trackBlockArgOfSelector:callback:</code> 方法,就能在对应方法执行传入的 block 参数被调用和销毁的时候得到回调。回调中的内容包含了 <code>block</code> 对象,回调类型,<code>block</code> 已经执行的次数,执行 <code>block</code> 的参数、返回值,堆栈信息。</p>
<figure class="highlight objectivec"><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">BTTracker *tracker = [<span class="keyword">self</span> bt_trackBlockArgOfSelector:<span class="keyword">@selector</span>(performBlock:) callback:^(<span class="keyword">id</span> _Nullable block, BlockTrackerCallbackType type, <span class="built_in">NSInteger</span> invokeCount, <span class="keyword">void</span> * _Nullable * _Null_unspecified args, <span class="keyword">void</span> * _Nullable result, <span class="built_in">NSArray</span>&lt;<span class="built_in">NSString</span> *&gt; * _Nonnull callStackSymbols) &#123;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"%@ invoke count = %ld"</span>, BlockTrackerCallbackTypeInvoke == type ? <span class="string">@"BlockTrackerCallBackTypeInvoke"</span> : <span class="string">@"BlockTrackerCallBackTypeDead"</span>, (<span class="keyword">long</span>)invokeCount);</span><br><span class="line">&#125;];</span><br></pre></td></tr></table></figure>
<p>当你不想追踪这个方法执行时传入的 block 参数时,也可以停止追踪:</p>
<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[tracker stop]</span><span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>举个栗子,现在有个方法叫 <code>performBlock:</code>,只是简单地调用了 <code>block</code> 参数:</p>
<figure class="highlight nimrod"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="built_in">void</span>)performBlock:(<span class="built_in">void</span>(^)(<span class="built_in">void</span>))<span class="keyword">block</span> &#123;</span><br><span class="line"> <span class="keyword">block</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>调用两次这个方法,每次都传入不同的 block 实现:</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><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">__block NSString *word = @<span class="string">"I'm a block"</span><span class="comment">;</span></span><br><span class="line">[self performBlock:^&#123;</span><br><span class="line"> NSLog(@<span class="string">"add '!!!' to word"</span>)<span class="comment">;</span></span><br><span class="line"> word = [word stringByAppendingString:@<span class="string">"!!!"</span>]<span class="comment">;</span></span><br><span class="line">&#125;]<span class="comment">;</span></span><br><span class="line">[self performBlock:^&#123;</span><br><span class="line"> NSLog(@<span class="string">"%@"</span>, word)<span class="comment">;</span></span><br><span class="line">&#125;]<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>因为执行两次方法传入的是两个不同的 block 对象,所以会追踪两个 block 对象的执行和销毁,打印的 log 如下:</p>
<figure class="highlight smali"><table><tr><td class="gutter"><pre><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">add '!!!' to word</span><br><span class="line">BlockTrackerCallBackTypeInvoke<span class="built_in"> invoke </span>count = 1</span><br><span class="line">I'm a block!!!</span><br><span class="line">BlockTrackerCallBackTypeInvoke<span class="built_in"> invoke </span>count = 1</span><br><span class="line">BlockTrackerCallBackTypeDead<span class="built_in"> invoke </span>count = 1</span><br><span class="line">BlockTrackerCallBackTypeDead<span class="built_in"> invoke </span>count = 1</span><br></pre></td></tr></table></figure>
<p>在 block 对象销毁的时候<br>你可以尝试着把 <code>performBlock:</code> 的实现改成这样试试:</p>
<figure class="highlight nimrod"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="built_in">void</span>)performBlock:(<span class="built_in">void</span>(^)(<span class="built_in">void</span>))<span class="keyword">block</span> &#123;</span><br><span class="line"> <span class="keyword">block</span>();</span><br><span class="line"> <span class="keyword">block</span>();</span><br><span class="line"> <span class="keyword">block</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h2><p>原理很简单,就是 Hook 方法后再 Hook 下 Block,流程大致如下:</p>
<ol>
<li>利用 Objective-C Runtime 机制 Hook 某个方法,参考 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 的实现原理。</li>
<li>在方法真正执行前,使用 <a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">BlockHook</a> 先 Hook 所有 Block 类型的参数。Hook 模式为 <code>BlockHookModeAfter</code> 和 <code>BlockHookModeDead</code>。</li>
<li>在 Block 执行后更新执行次数,并将相关信息回调给 Tracker。销毁后也会回调给 Tracker。</li>
</ol>
<p>流程大概很简单,复用以前代码。这里主要讲下 Track 的逻辑。</p>
<h3 id="过滤方法的-Block-参数"><a href="#过滤方法的-Block-参数" class="headerlink" title="过滤方法的 Block 参数"></a>过滤方法的 Block 参数</h3><p>在 <code>bt_trackBlockArgOfSelector:callback:</code> 里获取方法的 Type Encoding 后判断是否含有 Block 类型的参数,并将 Block 参数的 Index 保存到 <code>BTTracker</code> 的 <code>blockArgIndex</code> 属性。</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><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">- (nullable <span class="keyword">BTTracker </span>*)<span class="keyword">bt_trackBlockArgOfSelector:(SEL)selector </span>callback:(<span class="keyword">BlockTrackerCallbackBlock)callback</span><br><span class="line"></span>&#123;</span><br><span class="line"> Class cls = <span class="keyword">bt_classOfTarget(self);</span><br><span class="line"></span> </span><br><span class="line"> Method <span class="keyword">originMethod </span>= class_getInstanceMethod(cls, selector)<span class="comment">;</span></span><br><span class="line"> if (!<span class="keyword">originMethod) </span>&#123;</span><br><span class="line"> return nil<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> const char *<span class="keyword">originType </span>= (char *)method_getTypeEncoding(<span class="keyword">originMethod);</span><br><span class="line"></span> if (![[NSString stringWithUTF8String:<span class="keyword">originType] </span>containsString:@<span class="string">"@?"</span>]) &#123;</span><br><span class="line"> return nil<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> NSMutableArray *<span class="keyword">blockArgIndex </span>= [NSMutableArray array]<span class="comment">;</span></span><br><span class="line"> int argIndex = <span class="number">0</span><span class="comment">; // return type is the first one</span></span><br><span class="line"> while(<span class="keyword">originType </span>&amp;&amp; *<span class="keyword">originType)</span><br><span class="line"></span> &#123;</span><br><span class="line"> <span class="keyword">originType </span>= <span class="keyword">BHSizeAndAlignment(originType, </span>NULL, NULL, NULL)<span class="comment">;</span></span><br><span class="line"> if ([[NSString stringWithUTF8String:<span class="keyword">originType] </span>hasPrefix:@<span class="string">"@?"</span>]) &#123;</span><br><span class="line"> [<span class="keyword">blockArgIndex </span><span class="keyword">addObject:@(argIndex)];</span><br><span class="line"></span> &#125;</span><br><span class="line"> argIndex++<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">BTTracker </span>*tracker = <span class="keyword">BTEngine.defaultEngine.trackers[bt_methodDescription(self, </span>selector)]<span class="comment">;</span></span><br><span class="line"> if (!tracker) &#123;</span><br><span class="line"> tracker = [[<span class="keyword">BTTracker </span>alloc] initWithTarget:self selector:selector]<span class="comment">;</span></span><br><span class="line"> tracker.callback = callback<span class="comment">;</span></span><br><span class="line"> tracker.<span class="keyword">blockArgIndex </span>= [<span class="keyword">blockArgIndex </span>copy]<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> return [tracker apply] ? tracker : nil<span class="comment">;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><code>bt_trackBlockArgOfSelector:callback:</code> 方法返回的 <code>BTTracker</code> 对象也保存了 <code>callback</code> 回调。</p>
<h3 id="执行-Callback"><a href="#执行-Callback" class="headerlink" title="执行 Callback"></a>执行 Callback</h3><p>遍历之前保存的 Block 参数 Index 列表 <code>blockArgIndex</code>,从 <code>NSInvocation</code> 中取到 Block 参数后,就可以 Hook 了。Block 的执行次数保存到了 <code>BHToken</code> 上,每次执行都会累加。在 Block 执行或销毁后都会调用 <code>callback</code>,只是传的参数稍有不同。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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"><span class="keyword">for</span> (<span class="built_in">NSNumber</span> *index <span class="keyword">in</span> tracker.blockArgIndex) &#123;</span><br><span class="line"> <span class="keyword">if</span> (index.integerValue &lt; invocation.methodSignature.numberOfArguments) &#123;</span><br><span class="line"> __unsafe_unretained <span class="keyword">id</span> block;</span><br><span class="line"> [invocation getArgument:&amp;block atIndex:index.integerValue];</span><br><span class="line"> __<span class="keyword">weak</span> <span class="keyword">typeof</span>(block) weakBlock = block;</span><br><span class="line"> __<span class="keyword">weak</span> <span class="keyword">typeof</span>(tracker) weakTracker = tracker;</span><br><span class="line"> BHToken *tokenAfter = [block block_hookWithMode:BlockHookModeAfter usingBlock:^(BHToken *token) &#123;</span><br><span class="line"> __<span class="keyword">strong</span> <span class="keyword">typeof</span>(weakBlock) strongBlock = weakBlock;</span><br><span class="line"> __<span class="keyword">strong</span> <span class="keyword">typeof</span>(weakTracker) strongTracker = weakTracker;</span><br><span class="line"> <span class="built_in">NSNumber</span> *invokeCount = objc_getAssociatedObject(token, <span class="built_in">NSSelectorFromString</span>(<span class="string">@"invokeCount"</span>));</span><br><span class="line"> <span class="keyword">if</span> (!invokeCount) &#123;</span><br><span class="line"> invokeCount = @(<span class="number">1</span>);</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> invokeCount = [<span class="built_in">NSNumber</span> numberWithInt:invokeCount.intValue + <span class="number">1</span>];</span><br><span class="line"> &#125;</span><br><span class="line"> objc_setAssociatedObject(token, <span class="built_in">NSSelectorFromString</span>(<span class="string">@"invokeCount"</span>), invokeCount, OBJC_ASSO<span class="built_in">CIATION_RETAIN</span>);</span><br><span class="line"> <span class="keyword">if</span> (strongTracker.callback) &#123;</span><br><span class="line"> strongTracker.callback(strongBlock, BlockTrackerCallbackTypeInvoke, invokeCount.intValue, token.args, token.retValue, [<span class="built_in">NSThread</span> callStackSymbols]);</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;];</span><br><span class="line"></span><br><span class="line"> [block block_hookWithMode:BlockHookModeDead usingBlock:^(BHToken *token) &#123;</span><br><span class="line"> __<span class="keyword">strong</span> <span class="keyword">typeof</span>(weakTracker) strongTracker = weakTracker;</span><br><span class="line"> <span class="built_in">NSNumber</span> *invokeCount = objc_getAssociatedObject(tokenAfter, <span class="built_in">NSSelectorFromString</span>(<span class="string">@"invokeCount"</span>));</span><br><span class="line"> <span class="keyword">if</span> (strongTracker.callback) &#123;</span><br><span class="line"> strongTracker.callback(<span class="literal">nil</span>, BlockTrackerCallbackTypeDead, invokeCount.intValue, <span class="literal">nil</span>, <span class="literal">nil</span>, [<span class="built_in">NSThread</span> callStackSymbols]);</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;];</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="对-NSInvocation-的一点探索"><a href="#对-NSInvocation-的一点探索" class="headerlink" title="对 NSInvocation 的一点探索"></a>对 NSInvocation 的一点探索</h3><p>在从 <code>NSInvocation</code> 对象获取参数时,需要先调用 <code>retainArguments</code> 方法让 <code>NSInvocation</code> 将 Block 参数 <code>copy</code>。因为有些 Block 参数类型是 <code>__NSStackBlock__</code>,需要拷贝到堆上,否则从 <code>NSInvocation</code> 获取的 Block 不会销毁。</p>
<p><code>getArgument:atIndex:</code> 方法只是将第 <code>index</code> 个参数指针的值拷贝到 <code>buffer</code> 中,而 <code>retainArguments</code> 才是真的对 C 字符串和 Block 拷贝。</p>
<p>我还为此做了个小实验。一个类外部声明并调用了 <code>test:</code> 方法,但其实内部实现的是 <code>foo:</code> 方法。通过实现 <code>methodSignatureForSelector:</code> 让消息转发流程走到 <code>forwardInvocation:</code> 方法中。然后向 Block 参数关联 <code>BTDealloc</code> 对象,在 <code>test:</code> 方法执行后,<code>BTDealloc</code> 类的 <code>dealloc</code> 方法并没有执行。也就是说通过 <code>NSInvocation</code> 获取的 Block 参数没销毁;如果先调用了 <code>retainArguments</code> 就会销毁。</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">void</span>)<span class="string">test:</span>(<span class="keyword">void</span>(^)(<span class="keyword">void</span>))block;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)<span class="string">foo:</span> (<span class="keyword">void</span>(^)(<span class="keyword">void</span>)) block &#123;</span><br><span class="line"> block();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (NSMethodSignature *)<span class="string">methodSignatureForSelector:</span>(SEL)aSelector</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">return</span> [NSMethodSignature <span class="string">signatureWithObjCTypes:</span><span class="string">"v@:@?"</span>];</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">- (<span class="keyword">void</span>)<span class="string">forwardInvocation:</span>(NSInvocation *)anInvocation</span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">// [anInvocation retainArguments];</span></span><br><span class="line"> <span class="keyword">void</span> **invocationFrame = ((__bridge struct BTInvocaton *)anInvocation)-&gt;frame;</span><br><span class="line"> <span class="keyword">void</span> *blockFromFrame = invocationFrame[<span class="number">2</span>];</span><br><span class="line"> <span class="keyword">void</span> *block;</span><br><span class="line"> [anInvocation <span class="string">getArgument:</span>&amp;block <span class="string">atIndex:</span><span class="number">2</span>];</span><br><span class="line"> BTDealloc *btDealloc = [BTDealloc <span class="keyword">new</span>];</span><br><span class="line"> objc_setAssociatedObject((__bridge id)block, <span class="meta">@selector</span>(<span class="string">foo:</span>), btDealloc, OBJC_ASSOCIATION_RETAIN);</span><br><span class="line"> anInvocation.selector = <span class="meta">@selector</span>(<span class="string">foo:</span>);</span><br><span class="line"> [anInvocation invoke];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>通过对 <code>NSInvocation</code> 对象的解析,我发现 <code>NSInvocation</code> 的参数存储于一个私有成员变量 <code>_frame</code> 中,试着将其强转为二级指针,也就是指针数组。拿到对应 index 的值 <code>blockFromFrame</code> 跟 <code>block</code> 作比较,发现是一样的。这里获取 <code>_frame</code> 需要强转下,<code>NSInvocation</code> 的内存模型如下:</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><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">struct <span class="keyword">BTInvocaton </span>&#123;</span><br><span class="line"> void *isa<span class="comment">;</span></span><br><span class="line"> void *frame<span class="comment">;</span></span><br><span class="line"> void *retdata<span class="comment">;</span></span><br><span class="line"> void *signature<span class="comment">;</span></span><br><span class="line"> void *container<span class="comment">;</span></span><br><span class="line"> uint8_t retainedArgs<span class="comment">;</span></span><br><span class="line"> uint8_t reserved[<span class="number">15</span>]<span class="comment">;</span></span><br><span class="line">&#125;<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>由于 Hook Method 的逻辑是在消息转发流程搞事情,所以跟 Aspects 一样不能同时 Hook 父类和子类类相同方法。因为如果子类调用父类的实现,就会死循环。如果 Hook 方法这部分使用 <a href="http://yulingtianxia.com/blog/2017/04/17/Objective-C-Method-Swizzling/">Method Swizzling</a> 等交换 IMP 的方式实现,也会有着严重依赖 Hook 顺序导致调用错乱的问题。还是基于桥的 Hook 牛逼,汇编跳板,我这辈子是看不懂了。</p>
<p>老子终于在这个月最后一天快结束的时候憋出来一篇大水文!搬砖累死了没时间研究技术,你们尽管喷!</p>
</content>
<summary type="html">
<p>很多方法最后一个参数是类似于 <code>completionBlock</code> 这种回调,然而有些 API 实现一些异常逻辑时会忘记调用传入的 Block 参数(当然这肯定是 bug 啦),或者存在多次调用。在调试的时候可能会碰到这种大坑,需要追踪下 Block 参数何时调用了,甚至是否调用过。如果不方便直接在 Block 实现中加代码,或者没有源码的情况下,就需要无侵入式地追踪 Block 参数对象。</p>
<p><a href="https://github.com/yulingtianxia/BlockTracker">BlockTracker</a> 可以追踪方法调用时传入的 Block 类型的参数的执行和销毁。基于 <a href="https://github.com/yulingtianxia/BlockHook">BlockHook</a> 实现。本文讲述了它的使用方法和实现原理。</p>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
</entry>
<entry>
<title>Hook Objective-C Block with Libffi</title>
<link href="http://yulingtianxia.com/blog/2018/02/28/Hook-Objective-C-Block-with-Libffi/"/>
<id>http://yulingtianxia.com/blog/2018/02/28/Hook-Objective-C-Block-with-Libffi/</id>
<published>2018-02-28T11:05:24.000Z</published>
<updated>2018-09-15T08:28:13.567Z</updated>
<content type="html"><p>本文通过参照 <code>MABlockClosure</code> 的实现和 <code>Aspects</code> 的 API 设计,基于 libffi 实现了对 Objective-C Block 的 hook。GitHub 地址:<a href="https://github.com/yulingtianxia/BlockHook" target="_blank" rel="external">https://github.com/yulingtianxia/BlockHook</a></p>
<p>什么场景下需要 hook block 呢?在有源码的情况下,大部分程序员会选择直接在 block 中插代码。假如方法 A 的入参是个 block 对象,方法 A 将 block 传给方法 B,C…等。如果只有方法 A 的源码,上层传入的 block 和下层方法实现都是黑盒的话,想追踪 block 调用的时机,打印些 log,就得 hook 这个 block 对象了。</p>
<a id="more"></a>
<h2 id="如何使用"><a href="#如何使用" class="headerlink" title="如何使用"></a>如何使用</h2><p>虽然 Github 上已经给了例子,用过 Aspects 的人一看就懂,但为了凑篇幅,还是多 BB 几句吧。</p>
<p>API 虽然清奇,但是需要在 block 对象上用哦,在其他类型的对象上用是无效的!</p>
<figure class="highlight erlang"><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="params">(BHToken *)</span>block_hookWithMode:<span class="params">(BlockHookMode)</span>mode</span><br><span class="line"> usingBlock:<span class="params">(id)</span>block</span><br></pre></td></tr></table></figure>
<p>四种 hook 模式任你选择,可以对同一个 block 对象 hook 多次,但是要注意自己控制好顺序问题!hook 后会返回一个 <code>BHToken</code> 对象,可以调用它的 <code>remove</code> 方法来让 hook 失效。切记 <code>remove</code> 的时候要按照 hook 时的逆序!(以后可以搞个栈优化下用户体验,暂时懒的弄)</p>
<figure class="highlight scheme"><table><tr><td class="gutter"><pre><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></pre></td><td class="code"><pre><span class="line">[<span class="name">super</span> viewDidLoad]<span class="comment">;</span></span><br><span class="line">// Do any additional setup after loading the view, typically from a nib.</span><br><span class="line">NSObject *z = NSObject.new<span class="comment">;</span></span><br><span class="line">int (<span class="name">^block</span>)(<span class="name">int</span>, int) = ^(<span class="name">int</span> x, int y) &#123;</span><br><span class="line"> int result = x + y<span class="comment">;</span></span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"%d + %d = %d, z is a NSObject: %p"</span>, x, y, result, z)<span class="comment">;</span></span><br><span class="line"> return result<span class="comment">;</span></span><br><span class="line">&#125;<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line">BHToken *tokenInstead = [<span class="name">block</span> block_hookWithMode:BlockHookModeInstead usingBlock:^(<span class="name">BHToken</span> *token, int x, int y)&#123;</span><br><span class="line"> [<span class="name">token</span> invokeOriginalBlock]<span class="comment">;</span></span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"let me see original result: %d"</span>, *(<span class="name">int</span> *)(<span class="name">token.retValue</span>))<span class="comment">;</span></span><br><span class="line"> // change the block imp and result</span><br><span class="line"> *(<span class="name">int</span> *)(<span class="name">token.retValue</span>) = x * y<span class="comment">;</span></span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"hook instead: '+' -&gt; '*'"</span>)<span class="comment">;</span></span><br><span class="line">&#125;]<span class="comment">;</span></span><br><span class="line"></span><br><span class="line">BHToken *tokenAfter = [<span class="name">block</span> block_hookWithMode:BlockHookModeAfter usingBlock:^(<span class="name">BHToken</span> *token, int x, int y)&#123;</span><br><span class="line"> // print args and result</span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"hook after block! %d * %d = %d"</span>, x, y, *(<span class="name">int</span> *)(<span class="name">token.retValue</span>))<span class="comment">;</span></span><br><span class="line">&#125;]<span class="comment">;</span></span><br><span class="line"></span><br><span class="line">BHToken *tokenBefore = [<span class="name">block</span> block_hookWithMode:BlockHookModeBefore usingBlock:^(<span class="name">id</span> token)&#123;</span><br><span class="line"> // BHToken has to be the first arg.</span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"hook before block! token:%@"</span>, token)<span class="comment">;</span></span><br><span class="line">&#125;]<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">BHToken *tokenDead = [<span class="name">block</span> block_hookWithMode:BlockHookModeDead usingBlock:^(<span class="name">id</span> token)&#123;</span><br><span class="line"> // BHToken is the only arg.</span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"block dead! token:%@"</span>, token)<span class="comment">;</span></span><br><span class="line">&#125;]<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">dispatch_async(<span class="name">dispatch_get_global_queue</span>(<span class="name">DISPATCH_QUEUE_PRIORITY_DEFAULT</span>, <span class="number">0</span>), ^&#123;</span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"hooked block"</span>)<span class="comment">;</span></span><br><span class="line"> int ret = block(<span class="name">3</span>, <span class="number">5</span>)<span class="comment">;</span></span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"hooked result:%d"</span>, ret)<span class="comment">;</span></span><br><span class="line"> // remove all tokens when you don<span class="symbol">'t</span> need.</span><br><span class="line"> // reversed order of hook.</span><br><span class="line"> [<span class="name">tokenBefore</span> remove]<span class="comment">;</span></span><br><span class="line"> [<span class="name">tokenAfter</span> remove]<span class="comment">;</span></span><br><span class="line"> [<span class="name">tokenInstead</span> remove]<span class="comment">;</span></span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"remove tokens, original block"</span>)<span class="comment">;</span></span><br><span class="line"> ret = block(<span class="name">3</span>, <span class="number">5</span>)<span class="comment">;</span></span><br><span class="line"> NSLog(<span class="name">@</span><span class="string">"original result:%d"</span>, ret)<span class="comment">;</span></span><br><span class="line">// [<span class="name">tokenDead</span> remove]<span class="comment">;</span></span><br><span class="line">&#125;)<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>可以通过设置 <code>BHToken</code> 的 <code>retValue</code> 属性来修改 block 的返回值。<code>usingBlock:</code> 的参数内容是自定义的,跟 Aspects 一样,用户自己填上对应的参数列表。完整参数列表的内容就是 <code>BHToken</code>(第一个参数)+ 原始 block 参数列表。看上面的例子应该很容易看懂。可以在 hook 的 block 中获取参数和修改返回值,打log,做些有(wei)趣(suo)的事情。</p>
<p>上面代码执行后的 log 结果如下:</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><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">hooked block</span><br><span class="line">hook before block! <span class="string">token:</span>&lt;<span class="string">BHToken:</span> <span class="number">0x1d00f0d80</span>&gt;</span><br><span class="line"><span class="number">3</span> + <span class="number">5</span> = <span class="number">8</span>, z is a <span class="string">NSObject:</span> <span class="number">0x1d00172b0</span></span><br><span class="line">let me see original <span class="string">result:</span> <span class="number">8</span></span><br><span class="line">hook <span class="string">instead:</span> <span class="string">'+'</span> -&gt; <span class="string">'*'</span></span><br><span class="line">hook after block! <span class="number">3</span> * <span class="number">5</span> = <span class="number">15</span></span><br><span class="line">hooked <span class="string">result:</span><span class="number">15</span></span><br><span class="line">remove tokens, original block</span><br><span class="line"><span class="number">3</span> + <span class="number">5</span> = <span class="number">8</span>, z is a <span class="string">NSObject:</span> <span class="number">0x1d00172b0</span></span><br><span class="line">original <span class="string">result:</span><span class="number">8</span></span><br><span class="line">block dead! <span class="string">token:</span>&lt;<span class="string">BHToken:</span> <span class="number">0x1d00f9900</span>&gt;</span><br></pre></td></tr></table></figure>
<p>老铁稳。</p>
<p>因为需要动态定义和运行函数,用到了 libffi,所以还需要引入对应架构的静态库,自己去官网下个编译好,在工程中引入 libffi.a 和包含头文件的 include 文件夹就行。示例程序 BlockHookSample 使用的是 arm64 架构。具体做法是在 Build Settings 中的 Other Link Flags 加入 libffi.a 的路径,在 Header Search Paths 加入 include 文件夹路径。</p>
<h2 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h2><p>先说下大致思路:</p>
<ol>
<li>根据 block 对象的签名,使用 <code>ffi_prep_cif</code> 构建 block-&gt;invoke 函数的模板 <code>cif</code></li>
<li>使用 <code>ffi_closure</code>,根据 cif 动态定义函数 <code>replacementInvoke</code>,并指定通用的实现函数为 <code>ClosureFunc</code></li>
<li>将 block-&gt;invoke 替换为 <code>replacementInvoke</code>,原始的 block-&gt;invoke 存放在 <code>originInvoke</code></li>
<li>在 <code>ClosureFunc</code> 中动态调用 <code>originInvoke</code> 函数和执行 hook 的逻辑。</li>
</ol>
<p>对 libffi 的介绍和用法有很多文章可以参考,这里不再赘述。</p>
<p>再整理下代码设计思路:</p>
<ul>
<li><code>BHToken</code>: 它实现了 hook 的逻辑,存储了相关的上下文。是最主要的类。</li>
<li><code>NSObject (BlockHook)</code>: 提供 hook 的接口,每次 hook block 对象都会创建一个 <code>BHToken</code>,并将其返回给用户。</li>
<li><code>BHCenter</code> 管理 <code>BHToken</code> 对象的中心,以后可以拓展更多玩法。</li>
</ul>
<p>下面列举下 <code>BHToken</code> 中几个比较重要的逻辑。</p>
<h3 id="通过-Block-创建函数模板"><a href="#通过-Block-创建函数模板" class="headerlink" title="通过 Block 创建函数模板"></a>通过 Block 创建函数模板</h3><p>有关 Objective-C Block 内存模型这里不再赘述,Block ABI 可以在 <a href="https://clang.llvm.org/docs/Block-ABI-Apple.html" target="_blank" rel="external">Clang 文档</a> 查到。根据 block 的 flag 位掩码计算偏移拿到 Type Encoding 签名 signature。<code>BHBlockTypeEncodeString()</code> 函数实现了这些逻辑,代码不贴了。一个 block 的签名格式是:[返回值类型和偏移][@?0][参数0类型和偏移][参数1类型和偏移]…,比如 arm64 下 <code>int (^block)(int, int)</code> 的签名是 <code>i16@?0i8i12</code>。block 指针占 8 字节,参数和返回值 <code>int</code> 都是 4 字节。</p>
<p>然后需要把 signature 字符串处理分拆成参数类型列表,在 libffi 中使用 <code>ffi_type</code> 表示各种类型。<code>_argsWithEncodeString:getCount:</code> 方法会根据 Type Encoding 规则,将 signature 逐个字符处理,可以获取 <code>ffi_type *</code> 参数(返回值)数组和参数个数。<code>_ffiArgForEncode:</code> 方法负责将 Type Encoding 字符映射到对应的 <code>ffi_type</code> 上,这是个很长的方法。</p>
<p>有了参数类型列表,返回值类型,参数个数后,就可以调用 <code>ffi_prep_cif()</code> 函数创建 <code>ffi_cif</code> 了,也就是函数模板。<code>_prepCIF:withEncodeString:</code> 方法实现了这个逻辑。</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">int</span>)<span class="string">_prepCIF:</span>(ffi_cif *)cif <span class="string">withEncodeString:</span>(const <span class="keyword">char</span> *)str</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">int</span> argCount;</span><br><span class="line"> ffi_type **argTypes = [self <span class="string">_argsWithEncodeString:</span>str <span class="string">getCount:</span>&amp;argCount];</span><br><span class="line"> </span><br><span class="line"> ffi_status status = ffi_prep_cif(cif, FFI_DEFAULT_ABI, argCount, [self <span class="string">_ffiArgForEncode:</span> str], argTypes);</span><br><span class="line"> <span class="keyword">if</span>(status != FFI_OK)</span><br><span class="line"> &#123;</span><br><span class="line"> NSLog(@<span class="string">"Got result %ld from ffi_prep_cif"</span>, (<span class="keyword">long</span>)status);</span><br><span class="line"> abort();</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span> argCount;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="创建闭包,替换-Block-的-invoke"><a href="#创建闭包,替换-Block-的-invoke" class="headerlink" title="创建闭包,替换 Block 的 invoke"></a>创建闭包,替换 Block 的 <code>invoke</code></h3><p>可以使用函数模板(<code>ffi_cif</code>)和一个函数指针(<code>replacementInvoke</code>)创建闭包(<code>ffi_closure</code>)。</p>
<figure class="highlight lisp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">_closure = ffi_closure_alloc(<span class="name">sizeof</span>(<span class="name">ffi_closure</span>), <span class="symbol">&amp;_replacementInvoke</span>)<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>当 <code>replacementInvoke()</code> 函数被调用时,绑定到闭包上的函数 <code>void BHFFIClosureFunc(ffi_cif *cif, void *ret, void **args, void *userdata)</code> 会被调用。传给 <code>replacementInvoke()</code> 的参数及其返回值都会被传给 <code>BHFFIClosureFunc()</code>。<code>ffi_prep_closure_loc</code> 函数的倒数第二个参数是 <code>user_data</code>,也会被传给 <code>BHFFIClosureFunc()</code> 方法。</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">- (void)_prepClosure</span><br><span class="line">&#123;</span><br><span class="line"> ffi_status status = ffi_prep_closure_loc(_closure, &amp;_cif, BHFFIClosureFunc, (__bridge void *)(<span class="keyword">self</span>), _replacementInvoke);</span><br><span class="line"> <span class="keyword">if</span>(status != FFI_OK)</span><br><span class="line"> &#123;</span><br><span class="line"> NSLog(@<span class="string">"ffi_prep_closure returned %d"</span>, (<span class="keyword">int</span>)status);</span><br><span class="line"> abort();</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="comment">// exchange invoke func imp</span></span><br><span class="line"> _originInvoke = ((__bridge <span class="class"><span class="keyword">struct</span> <span class="title">_BHBlock</span></span> *)<span class="keyword">self</span>.block)-&gt;invoke;</span><br><span class="line"> ((__bridge <span class="class"><span class="keyword">struct</span> <span class="title">_BHBlock</span></span> *)<span class="keyword">self</span>.block)-&gt;invoke = _replacementInvoke;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>将函数指针 <code>_replacementInvoke</code> 和函数模板 <code>_cif</code> 绑定函数闭包之后,需要将 block 的 <code>invoke</code> 替换成 <code>_replacementInvoke</code>,并把原始的实现存到 <code>_originInvoke</code>。<code>invoke</code> 函数的模板跟 block 的签名内容是一致的。</p>
<p>这样当 block 调用时,实际上会调用 <code>_replacementInvoke</code> 函数,进而调用 <code>BHFFIClosureFunc</code> 通用函数。在这里面会实现 hook 的逻辑。</p>
<p>还原 Hook 的 <code>remove</code> 逻辑也很简单,将 <code>_originInvoke</code> 恢复到 <code>invoke</code> 即可:</p>
<figure class="highlight livescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="params">((__bridge struct _BHBlock *)self.block)</span>-&gt;</span>invoke = _originInvoke;</span><br></pre></td></tr></table></figure>
<h3 id="实现通用-Hook-函数"><a href="#实现通用-Hook-函数" class="headerlink" title="实现通用 Hook 函数"></a>实现通用 Hook 函数</h3><p>所有被 hook 的 block 调用时都会走到 <code>BHFFIClosureFunc</code> 这里,可以拿到 block-&gt;invoke 的函数模板,返回值指针,参数列表。还有自定义的 <code>userdata</code>,传入的是 <code>BHToken</code> 对象。</p>
<p>使用 <code>ffi_call()</code> 动态调用 block 的原始实现 <code>_originInvoke</code>,并将参数列表和返回值指针传入。还需要传入函数模板,满足 Calling Convention。</p>
<figure class="highlight stata"><table><tr><td class="gutter"><pre><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">static void BHFFIClosureFunc(ffi_cif *cif, void *<span class="keyword">ret</span>, void **<span class="keyword">args</span>, void *userdata)</span><br><span class="line">&#123;</span><br><span class="line"> BHToken *<span class="keyword">token</span> = (__bridge BHToken *)(userdata);</span><br><span class="line"> <span class="keyword">token</span>.retValue = <span class="keyword">ret</span>;</span><br><span class="line"> <span class="keyword">if</span> (BlockHookModeBefore == <span class="keyword">token</span>.mode) &#123;</span><br><span class="line"> [<span class="keyword">token</span> invokeHookBlockWithArgs:<span class="keyword">args</span>];</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">if</span> (!(BlockHookModeInstead == <span class="keyword">token</span>.mode &amp;&amp; [<span class="keyword">token</span> invokeHookBlockWithArgs:<span class="keyword">args</span>])) &#123;</span><br><span class="line"> ffi_call(&amp;<span class="keyword">token</span>-&gt;_cif, <span class="keyword">token</span>-&gt;_originInvoke, <span class="keyword">ret</span>, <span class="keyword">args</span>);</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">if</span> (BlockHookModeAfter == <span class="keyword">token</span>.mode) &#123;</span><br><span class="line"> [<span class="keyword">token</span> invokeHookBlockWithArgs:<span class="keyword">args</span>];</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>根据 Hook mode,会在不同的时机调用 <code>invokeHookBlockWithArgs:</code> 方法执行 hook 的逻辑。</p>
<h3 id="组装-NSInvocation-执行-Hook-逻辑"><a href="#组装-NSInvocation-执行-Hook-逻辑" class="headerlink" title="组装 NSInvocation 执行 Hook 逻辑"></a>组装 <code>NSInvocation</code> 执行 Hook 逻辑</h3><p>Hook 逻辑实现在 <code>self.hookBlock</code> 中,被 Hook 的 block 是 <code>self.block</code>,分别获取两者的签名,并拷贝后者的参数传给前者构造的 <code>blockInvocation</code>。这里要注意 <code>self.hookBlock</code> 的参数比 <code>self.block</code> 多一个 <code>token</code>,所以在二者参数比对和传递时需要特殊处理下。最后执行 <code>blockInvocation</code>,即调用了 <code>usingBlock:</code> 的参数。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="built_in">BOOL</span>)invokeHookBlockWithArgs:(<span class="keyword">void</span> **)args</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">if</span> (!<span class="keyword">self</span>.block || !<span class="keyword">self</span>.hookBlock) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="built_in">NSMethodSignature</span> *hookBlockSignature = [<span class="built_in">NSMethodSignature</span> signatureWithObjCTypes:BHBlockTypeEncodeString(<span class="keyword">self</span>.hookBlock)];</span><br><span class="line"> <span class="built_in">NSMethodSignature</span> *originalBlockSignature = [<span class="built_in">NSMethodSignature</span> signatureWithObjCTypes:BHBlockTypeEncodeString(<span class="keyword">self</span>.block)];</span><br><span class="line"> <span class="built_in">NSInvocation</span> *blockInvocation = [<span class="built_in">NSInvocation</span> invocationWithMethodSignature:hookBlockSignature];</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// origin block invoke func arguments: block(self), ...</span></span><br><span class="line"> <span class="comment">// hook block signature arguments: block(self), token, ...</span></span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (hookBlockSignature.numberOfArguments &gt; <span class="keyword">self</span>.numberOfArguments + <span class="number">1</span>) &#123;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Block has too many arguments. Not calling %@"</span>, <span class="keyword">self</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (hookBlockSignature.numberOfArguments &gt; <span class="number">1</span>) &#123;</span><br><span class="line"> [blockInvocation setArgument:(<span class="keyword">void</span> *)&amp;<span class="keyword">self</span> atIndex:<span class="number">1</span>];</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">void</span> *argBuf = <span class="literal">NULL</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="built_in">NSUInteger</span> idx = <span class="number">2</span>; idx &lt; hookBlockSignature.numberOfArguments; idx++) &#123;</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span> *type = [originalBlockSignature getArgumentTypeAtIndex:idx - <span class="number">1</span>];</span><br><span class="line"> <span class="built_in">NSUInteger</span> argSize;</span><br><span class="line"> <span class="built_in">NSGetSizeAndAlignment</span>(type, &amp;argSize, <span class="literal">NULL</span>);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (!(argBuf = reallocf(argBuf, argSize))) &#123;</span><br><span class="line"> <span class="built_in">NSLog</span>(<span class="string">@"Failed to allocate memory for block invocation."</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> memcpy(argBuf, args[idx - <span class="number">1</span>], argSize);</span><br><span class="line"> [blockInvocation setArgument:argBuf atIndex:idx];</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> [blockInvocation invokeWithTarget:<span class="keyword">self</span>.hookBlock];</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (argBuf != <span class="literal">NULL</span>) &#123;</span><br><span class="line"> free(argBuf);</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>因为用户传入的 <code>hookBlock</code> 签名是不确定的,所以需要针对参数数量判断临界条件。</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>又是大水文一篇,总算是在月末憋出来了。因为只花了一天时间写代码,实在太仓促,肯定还有一堆 bug。目前不建议用到生产环境上,辅助 debug 还是可以的,以后会慢慢优化。也欢迎各位老铁们提 PR:<a href="https://github.com/yulingtianxia/BlockHook/pulls" target="_blank" rel="external">https://github.com/yulingtianxia/BlockHook/pulls</a></p>
<p>代码大量参考了 <a href="https://github.com/mikeash/MABlockClosure" target="_blank" rel="external">MABlockClosure</a> 的一些工具函数,API 设计上致敬 <a href="https://github.com/steipete/Aspects" target="_blank" rel="external">Aspects</a>。技术上如有疏漏,还请各位大佬们多多指教。</p>
</content>
<summary type="html">
<p>本文通过参照 <code>MABlockClosure</code> 的实现和 <code>Aspects</code> 的 API 设计,基于 libffi 实现了对 Objective-C Block 的 hook。GitHub 地址:<a href="https://github.com/yulingtianxia/BlockHook">https://github.com/yulingtianxia/BlockHook</a></p>
<p>什么场景下需要 hook block 呢?在有源码的情况下,大部分程序员会选择直接在 block 中插代码。假如方法 A 的入参是个 block 对象,方法 A 将 block 传给方法 B,C…等。如果只有方法 A 的源码,上层传入的 block 和下层方法实现都是黑盒的话,想追踪 block 调用的时机,打印些 log,就得 hook 这个 block 对象了。</p>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
</entry>
<entry>
<title>How to make a Pebble watchface</title>
<link href="http://yulingtianxia.com/blog/2018/01/15/How-to-make-a-Pebble-watchface/"/>
<id>http://yulingtianxia.com/blog/2018/01/15/How-to-make-a-Pebble-watchface/</id>
<published>2018-01-14T16:00:17.000Z</published>
<updated>2018-09-15T08:28:13.892Z</updated>
<content type="html"><p>之前的 leader 送了我一块 Pebble 智能手表,俗话说『穷玩车,富玩表』,希望自己能在 2018 年里『变有钱』,那就多玩玩表吧!</p>
<a id="more"></a>
<p>首先简单介绍下 pebble。这个系列的智能手表虽然没有触屏,甚至有的型号是黑白背光屏,但提供了四个硕大的按钮用于上滚、下滚,进入和退出操作。它的数据传输需要依赖于手机的蓝牙连接,相当于一块副屏。但能在手表上独立运行 app,不像 watchOS 1 那样必须依赖手机上的 host app。其优势是续航性和性价比。pebble 曾一度跟安卓和 iOS 系统有三分天下之势,国外很多 geek 都喜欢搞搞 pebble。它甚至提供了云端编程环境,开发者很容易上手,查文档也十分便捷。经历一些固件升级后,开发语言也从 C 语言拓展到了 JS。 发布 app 的流程也很简单,geek 们可以在上面搞些有趣的事情了。</p>
<p>你可以在 pebble 上安装各种 app,比如查看 evernote,玩一些小游戏。用户输入只有四个按钮,以及重力感应传感器、健康相关传感器等。功能上略差,但能续航一周。它也解决了大部分关于时间的刚需,比如定制表盘,接收通知,计时器等。</p>
<p>出于对 pebble 的好奇以及感叹时间从自己身边流逝,时刻提醒自己把握当下珍惜每一秒,我做了一个简单有趣的 watchface。</p>
<p><img src="https://github.com/yulingtianxia/Pebble-MoHa/blob/master/watchface.gif?raw=true" alt=""></p>
<p>戴着黑框眼镜的程序员哥哥在手表中仿佛看到了自己,并时刻提醒着我们珍惜每一秒的光阴。pebble 以超长续航能力著称,于是我索性将电池电量一直显示满格,满足一切强迫症患者!同样以『超长待机』著称的英国女王伊丽莎白二世出生于 1926 年,有了来自女王的 buff 加持,你的 pebble 将会更持久更耐用!</p>
<p>为了展现上面 GIF 的效果,我设置了个定时器每秒回调下面的函数:</p>
<figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> void update_time() &#123;</span><br><span class="line"><span class="comment">// 获取当地时间戳</span></span><br><span class="line"> time_t temp = time(NULL);</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">tm</span></span> *tick_time = localtime(&amp;temp);</span><br><span class="line"><span class="comment">// 时间戳转字符串:时,分,显示到 label 上</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">char</span> s_hour_buffer[<span class="number">4</span>];</span><br><span class="line"> strftime(s_hour_buffer, <span class="keyword">sizeof</span>(s_hour_buffer), clock_is_24h_style() ?</span><br><span class="line"> <span class="string">"%H"</span> : <span class="string">"%I"</span>, tick_time);</span><br><span class="line"> text_layer_set_text(s_left_time_layer, s_hour_buffer);</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">char</span> s_minute_buffer[<span class="number">4</span>];</span><br><span class="line"> strftime(s_minute_buffer, <span class="keyword">sizeof</span>(s_minute_buffer), <span class="string">"%M"</span>, tick_time);</span><br><span class="line"> text_layer_set_text(s_right_time_layer, s_minute_buffer);</span><br><span class="line"><span class="comment">// 每隔一秒切换下显示状态</span></span><br><span class="line"> <span class="keyword">if</span> (tick_time-&gt;tm_sec % <span class="number">2</span> == <span class="number">0</span>) &#123;</span><br><span class="line"> layer_set_hidden((Layer *)s_eye_layer, <span class="literal">true</span>);</span><br><span class="line"> layer_set_hidden((Layer *)s_left_time_layer, <span class="literal">false</span>);</span><br><span class="line"> layer_set_hidden((Layer *)s_right_time_layer, <span class="literal">false</span>);</span><br><span class="line"> bitmap_layer_set_bitmap(s_nose_layer, s_nose_empty_bitmap);</span><br><span class="line"> &#125; <span class="keyword">else</span> &#123;</span><br><span class="line"> layer_set_hidden((Layer *)s_eye_layer, <span class="literal">false</span>);</span><br><span class="line"> layer_set_hidden((Layer *)s_left_time_layer, <span class="literal">true</span>);</span><br><span class="line"> layer_set_hidden((Layer *)s_right_time_layer, <span class="literal">true</span>);</span><br><span class="line"> bitmap_layer_set_bitmap(s_nose_layer, s_nose_bitmap);</span><br><span class="line"> &#125;</span><br><span class="line"><span class="comment">// 显示每日金句</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">char</span> *s_words[] = &#123;<span class="string">"You Naive!"</span>, <span class="string">"I'm angry!"</span>, <span class="string">"2 young 2 simple"</span>, <span class="string">"Wearing 3 watch"</span>, <span class="string">"Apply for Professor"</span>, <span class="string">"Excited!"</span>, <span class="string">"Sometimes naive!"</span>&#125;;</span><br><span class="line"> text_layer_set_text(s_word_layer, s_words[tick_time-&gt;tm_wday]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最终实现的代码只有一百多行,GitHub 地址: <a href="https://github.com/yulingtianxia/Pebble-MoHa" target="_blank" rel="external">https://github.com/yulingtianxia/Pebble-MoHa</a></p>
<p>这里说几个开发时需要注意的地方:</p>
<ol>
<li>显示带 alpha 通道的 png 图片时需要参照下<a href="https://developer.pebble.com/blog/2015/05/13/tips-and-tricks-transparent-images/" target="_blank" rel="external">这篇指引</a>,我 P 图的时候干脆搞成不透明的了。</li>
<li>位图无法缩放,但可以设置其在 <code>BitmapLayer</code> 中的对齐策略。</li>
<li>加载资源时需要加上 <code>RESOURCE_ID_</code> 前缀。</li>
<li>系统自带的字体并不是所有字号都有的,种类很有限。</li>
<li>圆形手表的 <code>Window</code> 的 <code>bounds.size</code> 是外接矩形,有内建方法判断是否是圆形手表。</li>
<li>真机调试需要打开手机上 Pebble 官方 App,打开开发者模式,开启开发者连接,保持蓝牙连接,让电脑与手机在同一个子网内。</li>
</ol>
<p>我选择使用云端开发工具 <a href="https://cloudpebble.net/ide/" target="_blank" rel="external">CloudPebble</a> 而不是本地 sdk,主要是因为 CloudPebble 集成了一套创建和管理工程、托管代码和资源、在真机或模拟器编译运行、持续集成以及支持同步 GitHub 的开发环境。很适合初学者快速上手,敏捷开发。</p>
<p>开发语言选择 C 语言,并不是为了装逼,也不是因为我不会 JS,而是因为 leader 送我的手表所支持固件最新版本目前为 v3.12.3,而使用 JS 开发需要依赖 Rocky.js,要求固件版本 v4.x。</p>
<p>强烈建议看这篇<a href="https://developer.pebble.com/tutorials/watchface-tutorial/" target="_blank" rel="external">官方开发者教程</a>来快速入门。不得不说 pebble 的开发者博客里无论是文档还是教程都很赞,就是有些 demo 的 github 连接失效了。不过按照教程一步步去做终归还是很容易搞定的。</p>
<p>使用 <a href="https://developer.pebble.com/docs/c/" target="_blank" rel="external">Pebble C SDK</a> 的 API 时会用到各种功能的函数,监听回调也都是传入自定义函数指针。跟 UI 相关的 API 跟移动开发很类似,也会提供 <code>Window</code>,<code>Layer</code>,<code>GBitmap</code> 等类型。因为不涉及到 UI 的点击,所以会简单很多。但要注意的是对象的生命周期,每次调用 xxx_create 方法一定要对应调用 xxx_destroy 方法。<code>Layer</code> 有很多子类,比如 <code>TextLayer</code>,<code>BitmapLayer</code> 等。这些子类可以很方便地显示文字和图片等内容。对于这次我做的 watchface 来说,图片和文字已经够用了。构建 <code>Layer</code> 的层级关系也很简单,比如用 <code>layer_add_child()</code> 就能往一个 <code>Layer</code> 上添加其他 <code>Layer</code>。</p>
<p>目前 pebble 项目所支持的图片资源种类很少,且对二进制和资源大小均有限制。毕竟手表上能发挥的空间有限,所以将大部分逻辑放在手机上。通过蓝牙将数据传输给手表,手表上只负责展示一些比较及时的数据,做一些简单的操作同步数据给手机。所以 pebble 也提供了 iOS 和 Android 对应的 sdk。</p>
<p>pebble 推出了好几款手表,所以一个项目对应的 target 也有五种之多。所幸的是 CloudPebble 的模拟器提供了这五种 target 的模拟器,在网页中编程也有较好的编程体验,支持高亮和查看文档。管理资源更是简单,每种 target 都提供预览。项目配置也都是可视化操作,十分容易上手。</p>
<p>以上内容就是开发一款 pebble watchface 的基本法则,喜欢的话可以去主页点个赞:<a href="https://apps.getpebble.com/applications/5a4b9bfc0dfc329496001b60" target="_blank" rel="external">https://apps.getpebble.com/applications/5a4b9bfc0dfc329496001b60</a></p>
</content>
<summary type="html">
<p>之前的 leader 送了我一块 Pebble 智能手表,俗话说『穷玩车,富玩表』,希望自己能在 2018 年里『变有钱』,那就多玩玩表吧!</p>
</summary>
<category term="瞎折腾" scheme="http://yulingtianxia.com/tags/%E7%9E%8E%E6%8A%98%E8%85%BE/"/>
</entry>
<entry>
<title>Associated Object 与 Dealloc</title>
<link href="http://yulingtianxia.com/blog/2017/12/15/Associated-Object-and-Dealloc/"/>
<id>http://yulingtianxia.com/blog/2017/12/15/Associated-Object-and-Dealloc/</id>
<published>2017-12-14T16:19:21.000Z</published>
<updated>2018-09-15T08:28:13.669Z</updated>
<content type="html"><p>我的 Objective-C 消息节流防抖库 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 需要实现一个特性:当 <code>MTRule</code> 的 <code>target</code> 释放后,自动调用 <code>MTRule</code> 的 <code>discard</code> 方法。后来使用了业界很早就已有的方案:Associated Object,在这里整理下相关的知识点。</p>
<a id="more"></a>
<h2 id="问题的由来"><a href="#问题的由来" class="headerlink" title="问题的由来"></a>问题的由来</h2><p>起初的思路是考虑到 <code>MTRule</code> 的 <code>target</code> 属性是 <code>weak</code> 的,想在其释放之前,也就是 <code>target</code> 变成 <code>nil</code> 之前调用 <code>MTRule</code> 对象的 <code>discard</code> 方法。然而 <code>target</code> 被释放赋值为 <code>nil</code> 的操作并不能通过 KVO 之类来监听,因为其并不是在外部通过 set 方法,这涉及到 <code>weak</code> 的实现原理(PS: 可以查看源码中 <code>weak_clear_no_lock()</code> 函数的实现)。于是问题转而变成了『在对象销毁前得到通知』。</p>
<p>接着我在 MacRumors 上找到了一篇 2005 年的<a href="https://forums.macrumors.com/threads/getting-notified-when-an-object-instance-is-deallocated.976309/" target="_blank" rel="external">贴子</a>。大概内容就是讲通过 KVO 监听 <code>retainCount</code> 属性纯属失了智,众所周知 <code>retainCount</code> 不能真实反映对象内存管理的情况,即便 <code>retainCount</code> 为 <code>1</code> 的时候收到了 <code>release</code> 消息,也会直接 <code>dealloc</code> 掉,并不会变成 <code>0</code>。接着又有人说干脆 hook 下 <code>dealloc</code> 方法,然后抛通知,但是这样不安全。直到 2008 年 DenNukem 回帖说他直到咋办啦,用 Associated Object!</p>
<h2 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h2><p>当一个对象(Host)释放后,其关联的对象(Associated Object)也会被解除。可以在 Host 对象上添加 Associated Object,策略用 <code>OBJC_ASSOCIATION_RETAIN</code>。由于只有 Host 持有了这个 Associated Object,当 Host 释放后 Associated Object 也会被释放。在 Associated Object 的 <code>dealloc</code> 方法中告知外界其 Host 对象已经释放。Perfect!</p>
<p><strong><code>dealloc</code> 方法的调用顺序是从子类到父类直至 <code>NSObject</code> 的,<code>NSObject</code> 的 <code>dealloc</code> 会调用 <code>object_dispose()</code> 函数,进而移除 Associated Object。</strong>具体的实现如下:</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">id</span> </span><br><span class="line">object_dispose(<span class="keyword">id</span> obj)</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">if</span> (!obj) <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line"> <span class="comment">// 销毁对象</span></span><br><span class="line"> objc_destructInstance(obj); </span><br><span class="line"> <span class="comment">// 释放内存</span></span><br><span class="line"> free(obj);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> *objc_destructInstance(<span class="keyword">id</span> obj) </span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">if</span> (obj) &#123;</span><br><span class="line"> <span class="comment">// Read all of the flags at once for performance.</span></span><br><span class="line"> <span class="keyword">bool</span> cxx = obj-&gt;hasCxxDtor();</span><br><span class="line"> <span class="keyword">bool</span> assoc = obj-&gt;hasAssociatedObjects();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// This order is important.</span></span><br><span class="line"> <span class="comment">// C++ 析构</span></span><br><span class="line"> <span class="keyword">if</span> (cxx) object_cxxDestruct(obj);</span><br><span class="line"> <span class="comment">// 移除 Associated Object</span></span><br><span class="line"> <span class="keyword">if</span> (assoc) _object_remove_assocations(obj);</span><br><span class="line"> <span class="comment">// ARC 下调用实例变量的 release 方法,移除 weak 引用</span></span><br><span class="line"> obj-&gt;clearDeallocating();</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> obj;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><strong>也就是说 Associated Object 的 <code>dealloc</code> 调用时 Host 已经释放了,无法拿到关于 Host 对象的任何信息了。但这其实对于大部分场景已经足够了,给外界一个 block/delegate callback,甚至是抛通知都 OK 的。</strong>实现起来很简单,代码很少,网上也可以找到一些 <a href="https://blog.slaunchaman.com/2011/04/11/fun-with-the-objective-c-runtime-run-code-at-deallocation-of-any-object/" target="_blank" rel="external">MRC</a> 或 <a href="https://github.com/ChenYilong/CYLDeallocBlockExecutor" target="_blank" rel="external">ARC</a> 下实现的示例代码。</p>
<p>虽说 Runtime 帮我们自动移除了 Associated Object,但对我这种平常几乎一直写 MRC 代码的人来说还真有点不适应,毕竟脑子里时刻警惕着:每一次 <code>retain</code> 都要配套来一次 <code>release</code> 或 <code>autorelease</code>。</p>
<h2 id="MessageThrottle-的特殊定制"><a href="#MessageThrottle-的特殊定制" class="headerlink" title="MessageThrottle 的特殊定制"></a>MessageThrottle 的特殊定制</h2><p>好,又回归到文章最开头的问题。现在解决了『<code>MTRule</code> 的 <code>target</code> 释放后,自动调用 <code>MTRule</code> 的 <code>discard</code> 方法』的问题。但是,要注意到此时 <code>target</code> 属性都释放了,于是就无法提供 <code>discard</code> 方法正确执行做需要的信息。所以需要在 Associated Object 中加入一些属性来保存一些执行 <code>discard</code> 时所需必要的信息。</p>
<figure class="highlight less"><table><tr><td class="gutter"><pre><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="variable">@interface</span> <span class="attribute">MTDealloc </span>: NSObject</span><br><span class="line"><span class="comment">// 这三个属性就是 discardRule:whenTargetDealloc: 方法将要用到的信息。这个方法会把 rule 从 MTEngine 列表中移除,并按需要还原之前的 hook 操作。这些细节不是重点,重点就是调用 discard 需要这仨属性。</span></span><br><span class="line"><span class="variable">@property</span> (nonatomic, weak) MTRule *rule;</span><br><span class="line"><span class="variable">@property</span> (nonatomic, copy) NSString *methodDescription;</span><br><span class="line"><span class="variable">@property</span> (nonatomic) Class cls;</span><br><span class="line"></span><br><span class="line"><span class="variable">@end</span></span><br><span class="line"></span><br><span class="line"><span class="variable">@implementation</span> MTDealloc</span><br><span class="line"></span><br><span class="line">- (void)dealloc</span><br><span class="line">&#123;</span><br><span class="line"><span class="comment">// 我只是觉得这样写代码就没警告了,而且还骚</span></span><br><span class="line"> SEL selector = NSSelectorFromString(@"<span class="attribute">discardRule</span>:<span class="attribute">whenTargetDealloc</span>:");</span><br><span class="line"> ((void (*)(id, SEL, MTRule *, MTDealloc *))<span class="selector-attr">[MTEngine.defaultEngine methodForSelector:selector]</span>)(MTEngine.defaultEngine, selector, self.rule, self);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">@end</span></span><br></pre></td></tr></table></figure>
<p>然后只需要在 <code>applyRule</code> 的时候初始化和配置好 <code>MTDealloc</code> 对象,并将其关联到 <code>target</code> 上即可:</p>
<figure class="highlight puppet"><table><tr><td class="gutter"><pre><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">// applyRule 时调用这个方法</span><br><span class="line">static void mt_configureTargetDealloc(MTRule *rule)</span><br><span class="line">&#123;</span><br><span class="line"> if (mt_object_isClass(rule.target)) &#123;</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> Class cls = object_getClass(<span class="literal">rule</span>.<span class="literal">target</span>);</span><br><span class="line"> MTDealloc *mtDealloc = objc_getAssociatedObject(<span class="literal">rule</span>.<span class="literal">target</span>, <span class="literal">rule</span>.selector);</span><br><span class="line"> <span class="keyword">if</span> (!mtDealloc) &#123;</span><br><span class="line"> mtDealloc = [MTDealloc new];</span><br><span class="line"> mtDealloc.<span class="literal">rule</span> = <span class="literal">rule</span>;</span><br><span class="line"> mtDealloc.methodDescription = mt_methodDescription(<span class="literal">rule</span>.<span class="literal">target</span>, <span class="literal">rule</span>.selector);</span><br><span class="line"> mtDealloc.cls = cls;</span><br><span class="line"> objc_setAssociatedObject(<span class="literal">rule</span>.<span class="literal">target</span>, <span class="literal">rule</span>.selector, mtDealloc, OBJC_ASSOCIATION_RETAIN);</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>使用 <code>rule.selector</code> 作为 Key 的目的就是让 <code>target</code> 对象上的每一个方法都对应一个关联对象,不会搞混。</p>
<p>感兴趣的可以查看 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 的源码,或者阅读我的上一篇文章 <a href="http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/">Objective-C Message Throttle and Debounce</a>,更详细地讲述了 Objective-C 消息节流防抖的实现原理。这里只是对其实现自动 <code>discard</code> 原理的补充。</p>
<h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul>
<li><a href="https://forums.macrumors.com/threads/getting-notified-when-an-object-instance-is-deallocated.976309/" target="_blank" rel="external">Getting notified when an object instance is deallocated</a></li>
<li><a href="https://blog.slaunchaman.com/2011/04/11/fun-with-the-objective-c-runtime-run-code-at-deallocation-of-any-object/" target="_blank" rel="external">Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object</a></li>
<li><a href="https://stackoverflow.com/questions/10842829/will-an-associated-object-be-released-automatically/10843510#10843510" target="_blank" rel="external">Will An Associated Object Be Released Automatically?</a></li>
<li><a href="https://github.com/ChenYilong/CYLDeallocBlockExecutor" target="_blank" rel="external">CYLDeallocBlockExecutor</a></li>
<li><a href="https://opensource.apple.com/source/objc4/objc4-723/" target="_blank" rel="external">objc4-723</a></li>
</ul>
</content>
<summary type="html">
<p>我的 Objective-C 消息节流防抖库 <a href="https://github.com/yulingtianxia/MessageThrottle">MessageThrottle</a> 需要实现一个特性:当 <code>MTRule</code> 的 <code>target</code> 释放后,自动调用 <code>MTRule</code> 的 <code>discard</code> 方法。后来使用了业界很早就已有的方案:Associated Object,在这里整理下相关的知识点。</p>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
<category term="Runtime" scheme="http://yulingtianxia.com/tags/Runtime/"/>
</entry>
<entry>
<title>Objective-C Message Throttle and Debounce</title>
<link href="http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/"/>
<id>http://yulingtianxia.com/blog/2017/11/05/Objective-C-Message-Throttle-and-Debounce/</id>
<published>2017-11-04T16:39:44.000Z</published>
<updated>2018-09-15T08:28:13.802Z</updated>
<content type="html"><p>在实际项目中经常会遇到因方法调用频繁而导致的 UI 闪动问题和性能问题,这时用某种策略需要控制调用频率,以达到节流和防抖的效果。<a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 是我实现的一个 Objective-C 消息节流和防抖的轻量级工具库,使用便捷且业务无关。</p>
<a id="more"></a>
<p>读懂本文的前提是对 <a href="http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/">Objective-C Runtime</a> 和 <a href="http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/">Objective-C 消息发送与转发机制原理</a>有一定了解。</p>
<h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><p>函数节流(throttle)是一个很基础的概念,常常跟函数防抖(debounce)作比较。在处理连续事件时比较常用,可以通过<a href="http://demo.nimius.net/debounce_throttle/" target="_blank" rel="external">这个 Demo</a> 感受下二者区别。在 JS 中有较多的实现和应用案例,可以查看<a href="https://blog.coding.net/blog/the-difference-between-throttle-and-debounce-in-underscorejs" target="_blank" rel="external">这篇文章</a> 更直接地了解下。</p>
<p>虽然在开发 iOS 和 macOS 的时候不用过多关心连续事件的采样问题,但有时也需要避免某个方法被频繁调用。比如一个很复杂的页面可能会频繁请求网络,每次回包都需更新界面,这时就需要防抖,控制刷新频率。</p>
<p>在 Objective-C 中,方法调用其实就是消息发送,所以我改了个名字,叫消息节流和防抖。</p>
<h2 id="使用姿势"><a href="#使用姿势" class="headerlink" title="使用姿势"></a>使用姿势</h2><p>假如我创建了一个 <code>Stub</code> 类的实例 <code>s</code>,我想限制它调用 <code>foo:</code> 方法的频率。先要创建并配置一个 <code>MTRule</code>,并将规则应用到 <code>MTEngine</code> 单例中:</p>
<figure class="highlight coffeescript"><table><tr><td class="gutter"><pre><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">Stub *s = [Stub <span class="keyword">new</span>];</span><br><span class="line">MTRule *rule = [MTRule <span class="keyword">new</span>];</span><br><span class="line">rule.target = s; <span class="regexp">//</span> You can also assign `<span class="javascript">Stub.class</span>` <span class="keyword">or</span> `<span class="javascript">mt_metaClass(Stub.class)</span>`</span><br><span class="line">rule.selector = @selector(foo:);</span><br><span class="line">rule.durationThreshold = <span class="number">0.01</span>;</span><br><span class="line">[MTEngine.defaultEngine applyRule:rule]; <span class="regexp">//</span> <span class="keyword">or</span> use `<span class="javascript">[rule apply]</span>`</span><br></pre></td></tr></table></figure>
<p><code>target</code> 可以是一个实例对象,也可以是一个类或元类。这样可以更灵活地控制限制策略,既可以只控制某个对象的消息发送频率,也可以控制某个类的实例方法和类方法的频率。当然,规则的 <code>target</code> 为实例对象的优先级比类更高,也不会发生冲突。</p>
<p>当然还有更简单的用法,跟上面那段代码作用相同:</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[s <span class="string">limitSelector:</span><span class="meta">@selector</span>(<span class="string">foo:</span>) <span class="string">oncePerDuration:</span><span class="number">0.01</span>]; <span class="comment">// returns MTRule instance</span></span><br></pre></td></tr></table></figure>
<p>无论是节流还是防抖,都需要设定一个时间 <code>durationThreshold</code> 阈值来限制频率,都意味着方法在最后会延迟调用。<code>MTRule</code> 默认的模式是 <code>MTPerformModeDebounce</code>,也就是防抖模式,需要等消息不再连续频繁发送后才执行。<code>MTPerformModeLast</code> 和 <code>MTPerformModeFirstly</code> 对应着节流模式,也就是控制一定时间内只执行一次。区别在于前者执行的是这段时间内最后发送的消息,后者执行第一次发送的消息。</p>
<p>比如我想要控制界面上某个 Label 内容的更新频率,给用户更好的体验,这时候很适合使用 <code>MTPerformModeLast</code> 模式:</p>
<figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">rule</span>.mode = MTPerformModeLast;</span><br></pre></td></tr></table></figure>
<p>当然所有规则都是可以动态调整的,也就是在应用规则以后,依然可以改变 <code>MTRule</code> 对象中各项配置,并会在下次消息发送时生效。如果调皮地将 <code>durationThreshold</code> 改成非正数,那么等同于立即执行方法,不会限制频率。</p>
<p>当使用 <code>MTPerformModeDebounce</code> 和 <code>MTPerformModeLast</code> 模式的时候,因为执行消息会有延迟,可以指定执行消息的队列 <code>messageQueue</code>,默认为主队列。</p>
<p>当想要废除某条规则时,使用一行代码即可:</p>
<figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[MTEngine.defaultEngine discardRule:<span class="keyword">rule</span>]; // <span class="keyword">or</span> use `[<span class="keyword">rule</span> discard]`</span><br></pre></td></tr></table></figure>
<p>应用和废除规则都是线程安全的。</p>
<h2 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h2><p>参照 <a href="https://github.com/steipete/Aspects" target="_blank" rel="external">Aspects</a> 和 <a href="https://github.com/bang590/JSPatch" target="_blank" rel="external">JSPatch</a> 中 Hook 的原理,将限制频率逻辑嵌入消息转发流程中:</p>
<ol>
<li>给类添加一个新的方法 <code>fixed_selector</code>,对应实现为 <code>rule.selector</code> 的 <code>IMP</code>。</li>
<li>利用 <a href="http://yulingtianxia.com/blog/2016/06/15/Objective-C-Message-Sending-and-Forwarding/">Objective-C runtime 消息转发机制</a>,将 <code>rule.selector</code> 对应的 <code>IMP</code> 改成 <code>_objc_msgForward</code> 从而触发调用 <code>forwardInvocation:</code> 方法。</li>
<li>将 <code>forwardInvocation:</code> 的实现替换为自己实现的 <code>IMP</code>,并在自己实现的逻辑中将 <code>invocation.selector</code> 设为 <code>fixed_selector</code>。并限制 <code>[invocation invoke]</code> 的调用频率。</li>
</ol>
<p>这种做法的缺陷是如果同时 hook 了基类和子类的同一个方法,且子类调用了基类的方法,就会导致循环调用。因为调用 <code>super</code> 方法时,传入的 <code>target</code> 还是 <code>self</code> 对象,导致调用了子类的方法。好在这里并不允许同时 hook 一条继承链上的两个类,因为子类和基类限制频率的规则会相互干扰,导致不易发现的 bug。</p>
<p><a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 从设计上使用 <code>MTEngine</code> 单例这种中心化的的方式来管理所有规则。Aspects 是将 hook 的上下文插入到对应的 <code>target</code> 中,这样的好处是需要暴露的接口较少。而 <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">MessageThrottle</a> 需要提供当前所有的规则给使用方。因为方法调用频率的限制会影响其上游代码和下游代码的运行频率,所以中心化管理的做法很有必要。</p>
<p>由于配置规则的内容较多,如果使用逐个传参的方式,方法名会很长。所以这里用 <code>MTRule</code> 类封装了规则的上下文,并使用 <code>applyRule:</code> 和 <code>discardRule:</code> 方法应用和废除规则。</p>
<h3 id="管理-MTRule"><a href="#管理-MTRule" class="headerlink" title="管理 MTRule"></a>管理 <code>MTRule</code></h3><p><code>MTEngine</code> 内部使用键值对存取 <code>MTRule</code>,这里使用 <code>target</code> 和 <code>selector</code> 的组合值作为 key。这里只要保证唯一性即可区分不同的规则,格式不固定:</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="built_in">NSString</span> * mt_methodDescription(<span class="keyword">id</span> target, SEL selector)</span><br><span class="line">&#123;</span><br><span class="line"> <span class="built_in">NSString</span> *selectorName = <span class="built_in">NSStringFromSelector</span>(selector);</span><br><span class="line"> <span class="keyword">if</span> (object_isClass(target)) &#123;</span><br><span class="line"> <span class="built_in">NSString</span> *className = <span class="built_in">NSStringFromClass</span>(target);</span><br><span class="line"> <span class="keyword">return</span> [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"%@ [%@ %@]"</span>, class_isMetaClass(target) ? <span class="string">@"+"</span> : <span class="string">@"-"</span>, className, selectorName];</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> <span class="keyword">return</span> [<span class="built_in">NSString</span> stringWithFormat:<span class="string">@"[%p %@]"</span>, target, selectorName];</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>在应用和废除规则的时候,需要检查规则合法性。这里只是简单检查下库中涉及的类和方法,一些内存管理和runtime 的方法并没有做限制,毕竟用户想作死我也管不着:</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="built_in">BOOL</span> mt_checkRuleValid(MTRule *rule)</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">if</span> (rule.target &amp;&amp; rule.selector &amp;&amp; rule.durationThreshold &gt; <span class="number">0</span>) &#123;</span><br><span class="line"> <span class="built_in">NSString</span> *selectorName = <span class="built_in">NSStringFromSelector</span>(rule.selector);</span><br><span class="line"> <span class="keyword">if</span> ([selectorName isEqualToString:<span class="string">@"forwardInvocation:"</span>]) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> Class cls;</span><br><span class="line"> <span class="keyword">if</span> (object_isClass(rule.target)) &#123;</span><br><span class="line"> cls = rule.target;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">else</span> &#123;</span><br><span class="line"> cls = object_getClass(rule.target);</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="built_in">NSString</span> *className = <span class="built_in">NSStringFromClass</span>(cls);</span><br><span class="line"> <span class="keyword">if</span> ([className isEqualToString:<span class="string">@"MTRule"</span>] || [className isEqualToString:<span class="string">@"MTEngine"</span>]) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">YES</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">NO</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h3 id="处理-NSInvocation"><a href="#处理-NSInvocation" class="headerlink" title="处理 NSInvocation"></a>处理 <code>NSInvocation</code></h3><p>在进入到消息转发流程调用 <code>forwardInvocation:</code> 方法时会进入到自定义的处理逻辑中,然后决定是否执行 <code>[invocation invoke]</code>。之前已经将原始 <code>selector</code> 的 IMP 替换成了 <code>fixedSelector</code>,所以调用 <code>[invocation invoke]</code> 之前需要调用 <code>invocation.selector = fixedSelector</code>。</p>
<p>下面的函数就是处理 <code>NSInvocation</code> 对象的逻辑。先用 <code>target</code> 和 <code>selector</code> 获取 <code>MTRule</code> 对象,进而根据不同的 <code>mode</code> 采取不同的策略。如果 <code>durationThreshold</code> 非正数就立即执行方法。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> mt_handleInvocation(<span class="built_in">NSInvocation</span> *invocation, SEL fixedSelector)</span><br><span class="line">&#123;</span><br><span class="line"> <span class="built_in">NSString</span> *methodDescriptionForInstance = mt_methodDescription(invocation.target, invocation.selector);</span><br><span class="line"> <span class="built_in">NSString</span> *methodDescriptionForClass = mt_methodDescription(object_getClass(invocation.target), invocation.selector);</span><br><span class="line"> </span><br><span class="line"> MTRule *rule = MTEngine.defaultEngine.rules[methodDescriptionForInstance];</span><br><span class="line"> <span class="keyword">if</span> (!rule) &#123;</span><br><span class="line"> rule = MTEngine.defaultEngine.rules[methodDescriptionForClass];</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (rule.durationThreshold &lt;= <span class="number">0</span>) &#123;</span><br><span class="line"> [invocation setSelector:fixedSelector];</span><br><span class="line"> [invocation invoke];</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="built_in">NSTimeInterval</span> now = [[<span class="built_in">NSDate</span> date] timeIntervalSince1970];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">switch</span> (rule.mode) &#123;</span><br><span class="line"> <span class="keyword">case</span> MTPerformModeFirstly:</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> MTPerformModeLast:</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> MTPerformModeDebounce:</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>上面代码省略了不同 <code>mode</code> 的处理逻辑,下面会逐个讲解。</p>
<h4 id="MTPerformModeFirstly"><a href="#MTPerformModeFirstly" class="headerlink" title="MTPerformModeFirstly"></a><code>MTPerformModeFirstly</code></h4><figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">MTModePerformFirstly</span>:</span><br><span class="line">start end</span><br><span class="line">| durationThreshold |</span><br><span class="line"><span class="variable">@-------------------------</span><span class="variable">@----------</span><span class="variable">@---------------</span><span class="variable">@----------------</span>&gt;&gt;</span><br><span class="line">| | | | </span><br><span class="line">perform immediately ignore ignore ignore</span><br></pre></td></tr></table></figure>
<p>最简单粗暴的实现方式,忽略第一次发送消息之后 <code>durationThreshold</code> 时间段内的所有消息。</p>
<figure class="highlight inform7"><table><tr><td class="gutter"><pre><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">if (now - <span class="keyword">rule</span>.lastTimeRequest &gt; <span class="keyword">rule</span>.durationThreshold) &#123;</span><br><span class="line"> <span class="keyword">rule</span>.lastTimeRequest = now;</span><br><span class="line"> invocation.selector = fixedSelector;</span><br><span class="line"> <span class="comment">[invocation invoke]</span>;</span><br><span class="line"> <span class="keyword">rule</span>.lastInvocation = nil;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="MTPerformModeLast"><a href="#MTPerformModeLast" class="headerlink" title="MTPerformModeLast"></a><code>MTPerformModeLast</code></h4><figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">MTModePerformLast</span>:</span><br><span class="line">start end</span><br><span class="line">| durationThreshold |</span><br><span class="line"><span class="variable">@-------------------------</span><span class="variable">@----------</span><span class="variable">@---------------</span><span class="variable">@----------------</span>&gt;&gt;</span><br><span class="line">| | | | </span><br><span class="line">ignore ignore ignore will perform at end</span><br></pre></td></tr></table></figure>
<p>在 <code>durationThreshold</code> 时间内不断更新 <code>lastInvocation</code> 的值,并在达到阈值 <code>durationThreshold</code> 后执行 <code>[lastInvocation invoke]</code>。这样保证了执行的是最后一次发送的消息。需要注意的是,<code>NSInvocation</code> 对象默认不会持有参数,在异步延迟执行 <code>invoke</code> 的时候参数可能已经被释放了,进而野指针 crash。所以需要调用 <code>retainArguments</code> 方法提前持有参数,防止之后被释放掉。如果实际传入的参数与参数类型不符,可能导致 <code>retainArguments</code> 方法 crash。我曾想过将参数列表保存到一个 <code>NSArray</code> 里,然后放到 <code>MTRule</code> 中,这样可以对参数类型做判断,避免 crash,也顺便持有了参数列表。但发现需要覆盖的类型太多,工作量和风险更多。我把这个半成品代码放在了 GitHubGist 上: <a href="https://gist.github.com/yulingtianxia/1518fc7604ed65aa4ca98abdeee974e1" target="_blank" rel="external">ConvertInvocationArguments.m</a></p>
<figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">if (now - <span class="keyword">rule</span>.lastTimeRequest &gt; <span class="keyword">rule</span>.durationThreshold) &#123;</span><br><span class="line"> <span class="keyword">rule</span>.lastTimeRequest = now;</span><br><span class="line"> dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="keyword">rule</span>.durationThreshold * NSEC_PER_SEC)), <span class="keyword">rule</span>.messageQueue, ^&#123;</span><br><span class="line"> [<span class="keyword">rule</span>.lastInvocation invoke];</span><br><span class="line"> <span class="keyword">rule</span>.lastInvocation = nil;</span><br><span class="line"> &#125;);</span><br><span class="line">&#125;</span><br><span class="line">else &#123;</span><br><span class="line"> invocation.selector = fixedSelector;</span><br><span class="line"> <span class="keyword">rule</span>.lastInvocation = invocation;</span><br><span class="line"> [<span class="keyword">rule</span>.lastInvocation retainArguments];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="MTPerformModeDebounce"><a href="#MTPerformModeDebounce" class="headerlink" title="MTPerformModeDebounce"></a><code>MTPerformModeDebounce</code></h4><figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">MTModePerformDebounce</span>:</span><br><span class="line">start end</span><br><span class="line">| durationThreshold(old) |</span><br><span class="line"><span class="variable">@----------------------</span><span class="variable">@----------------------</span>&gt;&gt;</span><br><span class="line">| | </span><br><span class="line">ignore will perform at end of new duration</span><br><span class="line"> |---------------------------------------------&gt;&gt;</span><br><span class="line"> | durationThreshold(new) |</span><br><span class="line"> start end</span><br></pre></td></tr></table></figure>
<p>虽然流程看上去复杂但其实实现起来也很简单。每次发送消息完再过 <code>durationThreshold</code> 时间后,检查下 <code>lastInvocation</code> 有没有变化。如果无变化,则说明这段时间内没有新的消息发送,则可以执行 <code>lastInvocation</code>。</p>
<figure class="highlight crmsh"><table><tr><td class="gutter"><pre><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">invocation.selector = fixedSelector;</span><br><span class="line"><span class="keyword">rule</span>.lastInvocation = invocation;</span><br><span class="line">[<span class="keyword">rule</span>.lastInvocation retainArguments];</span><br><span class="line">dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(<span class="keyword">rule</span>.durationThreshold * NSEC_PER_SEC)), <span class="keyword">rule</span>.messageQueue, ^&#123;</span><br><span class="line"> if (<span class="keyword">rule</span>.lastInvocation == invocation) &#123;</span><br><span class="line"> [<span class="keyword">rule</span>.lastInvocation invoke];</span><br><span class="line"> <span class="keyword">rule</span>.lastInvocation = nil;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h3 id="规则的应用与废除"><a href="#规则的应用与废除" class="headerlink" title="规则的应用与废除"></a>规则的应用与废除</h3><p>在真正应用规则之前,需要检查下规则合法性,然后检查继承链上是否已经应用过规则了。如果有,则需要输出错误信息;否则应用规则。这里使用 POSIX 的互斥锁保证线程安全。<code>mt_overrideMethod()</code> 函数所作的事情就是开始提到的利用消息转发流程 hook 的三个步骤。</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><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">- (<span class="keyword">BOOL)applyRule:(MTRule </span>*)rule</span><br><span class="line">&#123;</span><br><span class="line"> pthread_mutex_lock(&amp;mutex)<span class="comment">;</span></span><br><span class="line"> __block <span class="keyword">BOOL </span><span class="keyword">shouldApply </span>= YES<span class="comment">;</span></span><br><span class="line"> if (mt_checkRuleValid(rule)) &#123;</span><br><span class="line"> [self.rules enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, MTRule * _Nonnull obj, <span class="keyword">BOOL </span>* _Nonnull stop) &#123;</span><br><span class="line"> if (rule.selector == obj.selector</span><br><span class="line"> &amp;&amp; object_isClass(rule.target)</span><br><span class="line"> &amp;&amp; object_isClass(obj.target)) &#123;</span><br><span class="line"> Class clsA = rule.target<span class="comment">;</span></span><br><span class="line"> Class clsB = obj.target<span class="comment">;</span></span><br><span class="line"> <span class="keyword">shouldApply </span>= !([clsA isSubclassOfClass:clsB] <span class="title">||</span> [clsB isSubclassOfClass:clsA])<span class="comment">;</span></span><br><span class="line"> *stop = <span class="keyword">shouldApply;</span><br><span class="line"></span> NSString *errorDescription = [NSString stringWithFormat:@<span class="string">"Error: %@ already apply rule in %@. A message can only have one throttle per class hierarchy."</span>, NSStringFromSelector(obj.selector), NSStringFromClass(clsB)]<span class="comment">;</span></span><br><span class="line"> NSLog(@<span class="string">"%@"</span>, errorDescription)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> &#125;]<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line"> if (<span class="keyword">shouldApply) </span>&#123;</span><br><span class="line"> self.rules[mt_methodDescription(rule.target, rule.selector)] = rule<span class="comment">;</span></span><br><span class="line"> mt_overrideMethod(rule.target, rule.selector)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> pthread_mutex_unlock(&amp;mutex)<span class="comment">;</span></span><br><span class="line"> return <span class="keyword">shouldApply;</span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<p>废除规则是执行相反的操作。如果 <code>target</code> 是个实例对象,<code>mt_recoverMethod()</code> 函数会判断是否有相同 <code>selector</code> 且 <code>target</code> 为这个实例对象的类的其他规则。如果有,那将不会移除 hook。</p>
<figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">- (<span class="keyword">BOOL)discardRule:(MTRule </span>*)rule</span><br><span class="line">&#123;</span><br><span class="line"> pthread_mutex_lock(&amp;mutex)<span class="comment">;</span></span><br><span class="line"> <span class="keyword">BOOL </span><span class="keyword">shouldDiscard </span>= NO<span class="comment">;</span></span><br><span class="line"> if (mt_checkRuleValid(rule)) &#123;</span><br><span class="line"> NSString *description = mt_methodDescription(rule.target, rule.selector)<span class="comment">;</span></span><br><span class="line"> <span class="keyword">shouldDiscard </span>= self.rules[description] != nil<span class="comment">;</span></span><br><span class="line"> if (<span class="keyword">shouldDiscard) </span>&#123;</span><br><span class="line"> self.rules[description] = nil<span class="comment">;</span></span><br><span class="line"> mt_recoverMethod(rule.target, rule.selector)<span class="comment">;</span></span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> pthread_mutex_unlock(&amp;mutex)<span class="comment">;</span></span><br><span class="line"> return <span class="keyword">shouldDiscard;</span><br><span class="line"></span>&#125;</span><br></pre></td></tr></table></figure>
<h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>其实在开发过程中遇到需要限制方法调用频率的场景并不多,只是最近恰巧连续碰到几个刷新 UI 过频繁的问题,才想到应该去造个轮子。因为时间仓促,肯定还有考虑不周和一些 bug,待投入使用后慢慢完善和修复。</p>
<p>其实想在某个特定函数做节流很简单,但每次都需要做重复劳动,写脏代码,还不如抽象出一个工具类出来。尽量造与业务无关的轮子,锻炼技术,也受益整个业务发展。</p>
<p>好,装逼到此为止。Github : <a href="https://github.com/yulingtianxia/MessageThrottle" target="_blank" rel="external">https://github.com/yulingtianxia/MessageThrottle</a></p>
</content>
<summary type="html">
<p>在实际项目中经常会遇到因方法调用频繁而导致的 UI 闪动问题和性能问题,这时用某种策略需要控制调用频率,以达到节流和防抖的效果。<a href="https://github.com/yulingtianxia/MessageThrottle">MessageThrottle</a> 是我实现的一个 Objective-C 消息节流和防抖的轻量级工具库,使用便捷且业务无关。</p>
</summary>
<category term="Objective-C" scheme="http://yulingtianxia.com/tags/Objective-C/"/>
<category term="Runtime" scheme="http://yulingtianxia.com/tags/Runtime/"/>
<category term="Message Forwarding" scheme="http://yulingtianxia.com/tags/Message-Forwarding/"/>
</entry>
<entry>
<title>Threading Programming Guide(3)</title>
<link href="http://yulingtianxia.com/blog/2017/10/08/Threading-Programming-Guide-3/"/>
<id>http://yulingtianxia.com/blog/2017/10/08/Threading-Programming-Guide-3/</id>
<published>2017-10-08T09:03:54.000Z</published>
<updated>2018-09-15T08:28:13.573Z</updated>
<content type="html"><p><a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW1" target="_blank" rel="external">Threading Programming Guide</a> 的学习笔记&amp;翻译,第三部分。关于同步的内容。</p>
<ul>
<li><a href="http://yulingtianxia.com/blog/2017/08/28/Threading-Programming-Guide-1/">Threading Programming Guide(1)</a></li>
<li><a href="http://yulingtianxia.com/blog/2017/09/17/Threading-Programming-Guide-2/">Threading Programming Guide(2)</a></li>
</ul>
<a id="more"></a>
<h2 id="同步"><a href="#同步" class="headerlink" title="同步"></a>同步</h2><p>两个线程同时修改同一个资源可能会不小心干扰到对方,多线程存取资源带来了潜在的线程安全问题。比如,一个线程可能会覆盖了另一个线程的修改,或者使应用置于未知混乱状态。如果幸运的话,错乱的资源可能会导致明显的性能问题或 crash 这类相对容易复现和解决的 bug;如果不太走运,面临的就是短时间难以重现的 bug 和对代码的全面排查。</p>
<p>为了线程安全,要尽量避免共享资源并减少线程间交互。即便有必须交互的地方,也需要使用同步工具来确保安全。</p>
<p>macOS 和 iOS 提供了许多同步工具,范围涵盖互斥操作工具到应用中的序列化事件。下面会介绍这些工具的使用方法。</p>
<h3 id="同步工具"><a href="#同步工具" class="headerlink" title="同步工具"></a>同步工具</h3><p>完全避免同步问题是理想方案,但并不现实。下面介绍几类基本的同步工具。</p>
<h4 id="原子操作"><a href="#原子操作" class="headerlink" title="原子操作"></a>原子操作</h4><p>原子操作是对简单数据类型同步的一种简易形式。优点是不会阻塞竞争的线程。对于简单的操作,比如增加计数器变量的值,原子操作比锁拥有更好的性能。</p>
<p>macOS 和 iOS 包含许多基本的数学和逻辑运算的操作,可以在 32 位和 64 位上执行。其中就有 compare-and-swap, test-and-set 和 test-and-clear 操作的原子版本。 详见 <code>/usr/include/libkern/OSAtomic.h</code> 头文件,或 <code>atomic</code> man page。</p>
<h4 id="内存屏障和-Volatile-变量"><a href="#内存屏障和-Volatile-变量" class="headerlink" title="内存屏障和 Volatile 变量"></a>内存屏障和 <code>Volatile</code> 变量</h4><p>编译器为了让性能达到最佳,会经常重排序汇编指令,这就有可能导致存取内存的顺序跟着变化,进而产生错误数据,影响到一些看似各自独立的变量。由于编译器优化造成对变量错误的更新顺序,产生了潜在的错误结果。</p>
<p>内存屏障(Memory Barrier)是一种非阻塞的同步工具,用来确保以正确的顺序操作内存。Memory Barrier 就像栅栏一样,强制处理器在栅栏之前的所有读写操作都执行后才可以开始执行栅栏之后的操作。内存屏障知识详见维基百科 <a href="https://en.wikipedia.org/wiki/Memory_barrier" target="_blank" rel="external">Memory Barrier</a>。可以在代码中调用 <code>OSMemoryBarrier</code> 函数添加内存屏障,详见 <code>OSMemoryBarrier</code> man page。</p>
<p>Volatile 关键字对单独的变量应用了另一种内存约束。编译器为了优化代码,会将变量的值加载到寄存器中。对于局部变量这没毛病,如果这个变量由别的线程更新了的话,将出现不一致的现象。Volatile 关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。 如果一个变量可能在任何时候被外部资源修改,而编译器可能无法察觉,那么可以将其声明为 <a href="https://zh.wikipedia.org/wiki/Volatile变量" target="_blank" rel="external">volatile 变量</a>。</p>
<p>由于内存屏障和 volatile 变量都降低了编译器对代码的优化,除非万不得已,需慎用。</p>
<h4 id="锁"><a href="#锁" class="headerlink" title="锁"></a>锁</h4><p>锁是最常用的同步工具之一,可以用它保护代码中的临界区域(critical section)。临界区域中的代码只允许同时被一个线程访问。其他线程对这块代码的修改都会被拒绝,因为会影响其正确性。</p>
<p>下表列出了程序员最常用的一些锁。macOS 和 iOS 提供了大部分锁的实现,那些没实现的锁也会有说明。</p>
<table>
<thead>
<tr>
<th>锁</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mutex</td>
<td>互斥锁(Mutual exclusion,缩写Mutex)是多线程编程中一种对资源的保护机制,避免多个线程同时访问。如果互斥锁正在使用,其他线程需要等到它被释放后才能获取到。在同一时间只能有一个线程使用互斥锁。</td>
</tr>
<tr>
<td>Recursive lock</td>
<td>也叫重入锁或递归锁,是互斥锁的变种。重入锁允许同一条线程多次获得同一个锁,但也释放锁时也要释放对应的次数。重入锁主要用于递归程序或者多个方法都需要获得锁的场景。</td>
</tr>
<tr>
<td>Read-write lock</td>
<td>读写锁可以认为是一种共享版的互斥锁。如果对一个临界区大部分是读操作而只有少量的写操作,在大规模操作上应用读写锁可以显著降低线程互斥产生的代价。正常操作数据时,可以同时有多个读操作。线程想要做写操作时,需要等到所有的读操作完成并释放锁之后,然后写操作会获取锁并更新数据。在写操作线程阻塞等待锁被释放时,新来的读操作线程在写操作完成前会一直阻塞。系统只支持 POSIX 线程中使用读写锁。关于如何使用这些锁,详见 <code>pthread</code> man page。</td>
</tr>
<tr>
<td>Distributed lock</td>
<td>提供进程级别的互斥锁,但并不会真的阻塞进程,只是简单地向进程汇报锁正被占用,并让进程自己决定如何处理。</td>
</tr>
<tr>
<td>Spin lock</td>
<td>自旋锁会重复查询锁的条件,直到为 true。因为自旋锁属于在『死等』,它最常用在多核处理器系统上,且锁的等待时间很短,时间短到轮询比阻塞线程的开销还小(因为需要阻塞线程切换上下文和更新线程数据结构)。因为它的轮询性质,系统没有提供自旋锁的任何实现,在特定场景下可以自己实现。内核中实现自旋锁详见 <a href="https://developer.apple.com/library/content/documentation/Darwin/Conceptual/KernelProgramming/About/About.html#//apple_ref/doc/uid/TP30000905" target="_blank" rel="external">Kernel Programming Guide</a></td>
</tr>
<tr>
<td>Double-checked lock</td>
<td>双重检查锁试图减少并发系统中竞争和同步的开销。由于双重检查锁潜在地不安全性,系统不提供直接支持,不鼓励使用。</td>
</tr>
</tbody>
</table>
<p>注意:大部分的锁也会纳入内存屏障来确保进入临界区域前的加载和存储指令已经完成。</p>
<h4 id="条件变量"><a href="#条件变量" class="headerlink" title="条件变量"></a>条件变量</h4><p>Condition 是信号量的另一种类型,它允许线程在某个条件为 true 的时候,向其他线程发信号(signal)。通常用于标示资源的可用性或确保任务以特定的顺序执行。在进入临界区域时如果检查条件变量不为 true,线程会一直阻塞,直到某个其他线程 signal。与互斥锁的不同点在于 condition 允许被多个线程同时访问,它更像是个用某个特定标准筛查线程的门卫。</p>
<p>一种使用场景是管理即将发生的事件池。当队列中有事件到来时,使用条件变量对发信号(signal)。于是一个被唤醒的线程就可以从队列中获取并处理事件。如果有两个事件大致同时到达队列,会对 condition 发两次信号唤醒两个线程。</p>
<p>系统用几种不同的技术对 condition 提供支持。写这块代码需要谨慎,后面会给出示例。</p>
<h4 id="Perform-Selector"><a href="#Perform-Selector" class="headerlink" title="Perform Selector"></a>Perform Selector</h4><p>Cocoa 提供了向一个活跃线程异步分发消息的便捷方式,也就是 <code>NSObject</code> 类的 <code>performSelector...</code> 系列方法。使用这些方法向线程发送的执行 <code>selector</code> 的请求会被目标线程的 run loop 按接收顺序执行。</p>
<p>详见 <a href="http://yulingtianxia.com/blog/2017/09/17/Threading-Programming-Guide-2/#Cocoa-Perform-Selector-Sources">Cocoa Perform Selector Sources</a>。</p>
<h3 id="同步开销与性能"><a href="#同步开销与性能" class="headerlink" title="同步开销与性能"></a>同步开销与性能</h3><p>同步机制在帮助确保代码正确性的同时也造成了性能代价。即便没有竞争,使用同步工具也会引发延迟。锁和原子操作为了确保充分保护代码,通常会需要使用内存屏障和内核级同步。如果存在对锁的竞争,线程会阻塞,经历甚至更久的延迟。</p>
<p>下表列出了非竞争场景下互斥锁和原子操作的一些大致开销。这些测量值取自几千个样本的均值。时间开销会随着处理器负荷、计算机速度以及系统程序的可用内存数量产生巨幅波动。</p>
<table>
<thead>
<tr>
<th>项目</th>
<th>大约的开销</th>
<th>注释</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mutex acquisition time</td>
<td>大约 0.2 ms</td>
<td>无竞争场景下获取锁的时间。如果锁已经被其他线程持有,获取锁的耗时还会更长。结果取自对均值和中位数的分析,运行系统为 macOS 10.5,配备基于Intel 2 GHz Core Duo 处理器和 1 GB RAM 的 iMac。</td>
</tr>
<tr>
<td>Atomic compare-and-swap</td>
<td>大约 0.05 ms</td>
<td>无竞争场景下的 compare-and-swap 时间。运行环境同上。</td>
</tr>
</tbody>
</table>
<p>设计并发任务的时候,最重要的因素永远是正确性,但是也应该考虑到性能因素。总不能一味追求正确性而导致多线程执行的代码比单线程还慢吧。</p>
<p>如果是在单线程应用的基础上进行多线程的改装,应该分别测量下关键任务在单线程和多线程下执行的性能,比对结果后再决定是否使用多线程。</p>
<p>关于性能和指标采集工具详见 <a href="https://developer.apple.com/library/content/documentation/Performance/Conceptual/PerformanceOverview/Introduction/Introduction.html#//apple_ref/doc/uid/TP40001410" target="_blank" rel="external">Performance Overview</a></p>
<h3 id="线程安全和信号"><a href="#线程安全和信号" class="headerlink" title="线程安全和信号"></a>线程安全和信号</h3><p>信号(Signal) 是一种底层 BSD 机制,用于向进程传递信息或以某种方式操作进程。有些程序使用信号来监测某些事件,比如子进程终止。系统使用信号来终止失控的进程和传达其他类型的信息。</p>
<p>在多线程应用中,信号可能被发送到任何线程。所以实现 signal handler 的首要原则就是不要假定handler 会运行在某个特定线程。也就是说,假如在 A 线程设置 signal handler,信号被发送到 B 线程,A 和 B 不一定相同。</p>
<p>设置 signal handler 的细节可以查看 <code>signal</code> 和 <code>sigaction</code> man page。</p>
<h3 id="线程安全设计技巧"><a href="#线程安全设计技巧" class="headerlink" title="线程安全设计技巧"></a>线程安全设计技巧</h3><p>同步工具是把双刃剑,能让代码线程安全,但使用过多也会带来性能问题。能平衡好二者利弊靠的是经验,下面会提供一些技巧。</p>
<h4 id="完全避免同步"><a href="#完全避免同步" class="headerlink" title="完全避免同步"></a>完全避免同步</h4><p>最佳解决方案是从代码和数据结构设计上避免需要同步。同步工具很管用但也影响性能,能从设计根源上避免当然是最好的了。比如实现并发的时候要减少任务之间的相互作用和依赖。如果每个任务都在自己的私有数据集上操作,就不需要使用锁保护数据了。即便在两个任务共享一分公共数据集的情况下,可以考虑分割数据集或为每个任务提供一份数据拷贝。当然拷贝数据集也会有成本,这就需要提前权衡下拷贝成本高还是同步成本高。</p>
<h4 id="理解同步的限制"><a href="#理解同步的限制" class="headerlink" title="理解同步的限制"></a>理解同步的限制</h4><p>同步工具只有在应用中所有线程都持续使用才能生效。如果创建了互斥锁来限制对某个资源的存取,那么所有线程在试图操作此资源前都必须获得这个的互斥锁。如果做不到这些,互斥锁提供的保护就会失效,这是程序员的错。</p>
<h4 id="清楚代码正确性的风险"><a href="#清楚代码正确性的风险" class="headerlink" title="清楚代码正确性的风险"></a>清楚代码正确性的风险</h4><p>使用锁和内存屏障时需要认真些,要在代码中加对地方才行。你甚至觉得自己加锁的地方是对的,其实只是错觉。下面一系列例子试图阐述这个问题。表面上看似没毛病的代码,也能挑出瑕疵。基础假设是有一个可变数组,包含了一组不可变的对象。如果想要执行数组中第一个对象的方法,可以用下面代码实现:</p>
<figure class="highlight dns"><table><tr><td class="gutter"><pre><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">NSLock* arrayLock = GetArrayLock()<span class="comment">;</span></span><br><span class="line">NSMutableArray* myArray = GetSharedArray()<span class="comment">;</span></span><br><span class="line">id anObject<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">[arrayLock lock]<span class="comment">;</span></span><br><span class="line">anObject = [myArray objectAtIndex:<span class="number">0</span>]<span class="comment">;</span></span><br><span class="line">[arrayLock unlock]<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">[anObject doSomething]<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>因为数组是可变的,在获取数组第一个元素之前,锁会阻止其他线程修改数组。又因为元素对象本身是不可变的,不用对 <code>doSomething</code> 方法加锁。</p>
<p>不过上面的例子存在问题:在锁释放后 <code>doSomething</code> 执行前,如果另一个线程将数组中所有对象都移除会发生什么呢?<code>anObject</code> 野指针!解决问题的办法也很简单,重新整理下代码顺序,在 <code>doSomething</code> 执行后再释放锁:</p>
<figure class="highlight dns"><table><tr><td class="gutter"><pre><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">NSLock* arrayLock = GetArrayLock()<span class="comment">;</span></span><br><span class="line">NSMutableArray* myArray = GetSharedArray()<span class="comment">;</span></span><br><span class="line">id anObject<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">[arrayLock lock]<span class="comment">;</span></span><br><span class="line">anObject = [myArray objectAtIndex:<span class="number">0</span>]<span class="comment">;</span></span><br><span class="line">[anObject doSomething]<span class="comment">;</span></span><br><span class="line">[arrayLock unlock]<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>把调用 <code>doSomething</code> 的代码挪到锁里面保证了对象依然有效,但如果 <code>doSomething</code> 执行时间过长又会导致锁也会被占用很久,造成性能瓶颈。</p>
<p>代码的毛病不是临界区域不清晰,真正的问题在于其他线程插了一脚触发的内存管理问题。因为其他线程释放了 <code>anObject</code>,更好的解决方案是在锁释放前 <code>retain</code> 它。此解决方案不仅对症下药,而且无潜在的性能问题。</p>
<figure class="highlight dns"><table><tr><td class="gutter"><pre><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">NSLock* arrayLock = GetArrayLock()<span class="comment">;</span></span><br><span class="line">NSMutableArray* myArray = GetSharedArray()<span class="comment">;</span></span><br><span class="line">id anObject<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">[arrayLock lock]<span class="comment">;</span></span><br><span class="line">anObject = [myArray objectAtIndex:<span class="number">0</span>]<span class="comment">;</span></span><br><span class="line">[anObject retain]<span class="comment">;</span></span><br><span class="line">[arrayLock unlock]<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">[anObject doSomething]<span class="comment">;</span></span><br><span class="line">[anObject release]<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>尽管上面的例子非常简单,但抓到重点了。要透过表面看本质。要预先考虑到一些问题,比如内存管理和其他方面的设计可能会受多线程的影响。除此之外,在安全问题上要对编辑器的行为做最坏的打算。小心谨慎方能避灾。</p>
<h4 id="提防死锁和活锁"><a href="#提防死锁和活锁" class="headerlink" title="提防死锁和活锁"></a>提防死锁和活锁</h4><p>如果线程在同一时刻持有不止一个锁,随时都有发生死锁的可能。当两个不同的线程分别持有一个锁,并且尝试获取对方持有的锁,<a href="https://zh.wikipedia.org/wiki/死锁" target="_blank" rel="external">死锁</a>就发生了。因为每个线程永远都获取不到另一个锁,结果就是永久阻塞。</p>
<p>活锁跟死锁很像,死锁是获取不到另一个锁就死等,而活锁是获取不到就释放已经持有的锁和资源,然后重试。活锁把时间都花在释放锁和尝试获取其他锁上面了,并没啥干活儿。</p>
<p>避免死锁和活锁的最好方法就是一次只获取一个锁。如果一次必须获取不止一个锁,那就应该确保其他线程别这么做。</p>
<h4 id="正确地使用-Volatile-变量"><a href="#正确地使用-Volatile-变量" class="headerlink" title="正确地使用 Volatile 变量"></a>正确地使用 <code>Volatile</code> 变量</h4><p>如果已经对一段代码用互斥锁保护了,就不要自动假定需要对这段代码中的重要变量用 <code>volatile</code> 关键字再保护一次。互斥锁包含了内存屏障,确保加载和存储操作的顺序正确。添加 <code>volatile</code> 关键字会强制访问变量时每次都从内存加载。可能在特殊情况下有必要将这两种同步技术混合使用,但也会导致严重降低性能。如果只用互斥锁保护变量就够了,删掉 <code>volatile</code> 关键字吧。</p>
<p>不要用 <code>volatile</code> 变量试图替代使用互斥锁。互斥锁和其他同步机制通常比 <code>volatile</code> 变量能更好地保护数据结构的完整性。<code>volatile</code> 关键字只是确保变量从内存加载而不是存在寄存器中。它无法确保代码可以正确地访问变量。</p>
<h3 id="使用原子操作"><a href="#使用原子操作" class="headerlink" title="使用原子操作"></a>使用原子操作</h3><p>无阻塞同步可以执行一些操作并避免锁的开销。虽然用锁可以有效地同步两个线程,但即便在无竞争情况下获取锁的代价相对较高。相反,许多同步操作花一小部分时间就能完成,而且跟锁一样管用。</p>
<p>可以用原子操作在 32 位或 64 位数值上做些简单的数学和逻辑操作。为了确保在原子操作完成后才可再次访问受影响的内存,这些操作依赖专门的硬件指令(和可选的内存屏障)。在多线程情况下为确保内存被正确地同步,应该始终使用纳入内存屏障版本的原子操作(带有 <code>Barrier</code> 后缀)。</p>
<p>下标列出了可用的数学和逻辑原子操作以及相关函数名。这些函数都声明在 <code>/usr/include/libkern/OSAtomic.h</code> 头文件中,它包含了完整语法。这些函数的 64 位版本仅在 64 位处理器中可用。</p>
<table>
<thead>
<tr>
<th>操作</th>
<th>函数名</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>Add</td>
<td>OSAtomicAdd32 OSAtomicAdd32Barrier OSAtomicAdd64 OSAtomicAdd64Barrier</td>
<td>将两个整数相加并将结果存在其中一个指定的变量中</td>
</tr>
<tr>
<td>Increment</td>
<td>OSAtomicIncrement32 OSAtomicIncrement32Barrier OSAtomicIncrement64 OSAtomicIncrement64Barrier</td>
<td>将指定的整数值加一</td>
</tr>
<tr>
<td>Decrement</td>
<td>OSAtomicDecrement32 OSAtomicDecrement32Barrier OSAtomicDecrement64 OSAtomicDecrement64Barrier</td>
<td>将指定的整数值减一</td>
</tr>
<tr>
<td>Logical OR</td>
<td>OSAtomicOr32 OSAtomicOr32Barrier</td>
<td>在指定的 32 位数值和掩码之间做逻辑或</td>
</tr>
<tr>
<td>Logical AND</td>
<td>OSAtomicAnd32 OSAtomicAnd32Barrier</td>
<td>在指定的 32 位数值和掩码之间做逻辑与</td>
</tr>
<tr>
<td>Logical XOR</td>
<td>OSAtomicXor32 OSAtomicXor32Barrier</td>
<td>在指定的 32 位数值和掩码之间做逻辑异或</td>
</tr>
<tr>
<td>Compare and swap</td>
<td>OSAtomicCompareAndSwap32 OSAtomicCompareAndSwap32Barrier OSAtomicCompareAndSwap64 OSAtomicCompareAndSwap64Barrier OSAtomicCompareAndSwapPtr OSAtomicCompareAndSwapPtrBarrier OSAtomicCompareAndSwapInt OSAtomicCompareAndSwapIntBarrier OSAtomicCompareAndSwapLong OSAtomicCompareAndSwapLongBarrier</td>
<td>函数有三个参数:oldValue, newValue, theValue 指针。如果 oldValue 跟 theValue 指针的内容相等,则把 newValue 赋给 theValue 指针的内容。否则啥都不干。比较和赋值会以一个原子操作完成。返回值表明是否发生了交换。</td>
</tr>
<tr>
<td>Test and set</td>
<td>OSAtomicTestAndSet OSAtomicTestAndSetBarrier</td>
<td>将指定变量第 n 位设为 1,并将旧值以 bool 形式返回。注意这里会将变量按 8 位分块,每块的内容是逆序的。所以如果想要设置 0 位的值,n 需要传入 7。</td>
</tr>
<tr>
<td>Test and clear</td>
<td>OSAtomicTestAndClear OSAtomicTestAndClearBarrier</td>
<td>将指定变量第 n 位设为 0,并将旧值以 bool 形式返回。注意这里会将变量按 8 位分块,每块的内容是逆序的。所以如果想要设置 0 位的值,n 需要传入 7。</td>
</tr>
</tbody>
</table>
<p>大多数原子函数的行为都会是相对简单且能预料到的。下面的示例代码使用了 <code>OSAtomicTestAndSet</code> 和 <code>OSAtomicCompareAndSwap32</code> 函数,结果可能跟你预想的不太一样。这些函数在无竞争情况下被调用,且没有其他线程同时操作。</p>
<figure class="highlight mizar"><table><tr><td class="gutter"><pre><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">int32_t theValue = 0;</span><br><span class="line">OSAtomicTestAndSet(0, &amp;theValue);</span><br><span class="line">// theValue <span class="keyword">is</span> <span class="keyword">now</span> 128.</span><br><span class="line"> </span><br><span class="line">theValue = 0;</span><br><span class="line">OSAtomicTestAndSet(7, &amp;theValue);</span><br><span class="line">// theValue <span class="keyword">is</span> <span class="keyword">now</span> 1.</span><br><span class="line"> </span><br><span class="line">theValue = 0;</span><br><span class="line">OSAtomicTestAndSet(15, &amp;theValue)</span><br><span class="line">// theValue <span class="keyword">is</span> <span class="keyword">now</span> 256.</span><br><span class="line"> </span><br><span class="line">OSAtomicCompareAndSwap32(256, 512, &amp;theValue);</span><br><span class="line">// theValue <span class="keyword">is</span> <span class="keyword">now</span> 512.</span><br><span class="line"> </span><br><span class="line">OSAtomicCompareAndSwap32(256, 1024, &amp;theValue);</span><br><span class="line">// theValue <span class="keyword">is</span> still 512.</span><br></pre></td></tr></table></figure>
<p>关于原子操作可以看看 <code>atomic</code> man page 和 <code>/usr/include/libkern/OSAtomic.h</code> 头文件。</p>
<h3 id="使用锁"><a href="#使用锁" class="headerlink" title="使用锁"></a>使用锁</h3><p>锁是多线程编程中的一个基础同步工具,macOS 和 iOS 都提供了基础的互斥锁。Foundation 框架定义了几种用于特别场景的互斥锁作为补充。</p>
<h4 id="POSIX-互斥锁"><a href="#POSIX-互斥锁" class="headerlink" title="POSIX 互斥锁"></a>POSIX 互斥锁</h4><p>POSIX 互斥锁贼好用。用 <code>pthread_mutex_t</code> 结构体声明一个互斥锁变量,将其传入 <code>pthread_mutex_init</code> 函数初始化,然后用 <code>pthread_mutex_lock</code> 和 <code>pthread_mutex_unlock</code> 函数获取和释放锁就行了。等到不需要用锁了,调用 <code>pthread_mutex_destroy</code> 函数析构锁的数据结构。下面是简化后的代码,实际使用时要考虑到错误处理等细节:</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">pthread_mutex_t</span> mutex;</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">MyInitFunction</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line"> pthread_mutex_init(&amp;mutex, <span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">MyLockingFunction</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line"> pthread_mutex_lock(&amp;mutex);</span><br><span class="line"> <span class="comment">// Do work.</span></span><br><span class="line"> pthread_mutex_unlock(&amp;mutex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="NSLock"><a href="#NSLock" class="headerlink" title="NSLock"></a><code>NSLock</code></h4><p><code>NSLock</code> 实现了 Cocoa 中基本的互斥锁。包括 <code>NSLock</code> 的所有锁的接口实际上都由 <code>NSLocking</code> 协议定义,也就是 <code>lock</code> 和 <code>unlock</code> 这俩方法,对应功能是获取和释放锁。</p>
<p>除此之外,<code>NSLock</code> 类还提供了 <code>tryLock</code> 和 <code>lockBeforeDate:</code> 方法。<code>tryLock</code> 方法尝试获取锁,但如果锁不可用,并不会阻塞,只是返回 <code>NO</code> 而已。<code>lockBeforeDate:</code> 方法尝试获取锁,并一直阻塞线程,直到获取到锁(返回 <code>YES</code>)或达到限定的时间(返回 <code>NO</code>)。</p>
<p>下面的示例代码展示了如何使用 <code>NSLock</code> 在多个线程计算要被显示的数据时,即便获取不到锁的情况下依然可以继续运算。</p>
<figure class="highlight objectivec"><table><tr><td class="gutter"><pre><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="built_in">BOOL</span> moreToDo = <span class="literal">YES</span>;</span><br><span class="line"><span class="built_in">NSLock</span> *theLock = [[<span class="built_in">NSLock</span> alloc] init];</span><br><span class="line">...</span><br><span class="line"><span class="keyword">while</span> (moreToDo) &#123;</span><br><span class="line"> <span class="comment">/* Do another increment of calculation */</span></span><br><span class="line"> <span class="comment">/* until there’s no more to do. */</span></span><br><span class="line"> <span class="keyword">if</span> ([theLock tryLock]) &#123;</span><br><span class="line"> <span class="comment">/* Update display used by all threads. */</span></span><br><span class="line"> [theLock unlock];</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h4 id="synchronized"><a href="#synchronized" class="headerlink" title="@synchronized"></a><code>@synchronized</code></h4><p>可以用 <code>@synchronized</code> 指令很方便地在 Objective-C 代码中飞快地写个互斥锁。它的作用跟互斥锁一样,但不用创建锁,只需要把一个 Objective-C 对象当做锁的 token 即可:</p>
<figure class="highlight less"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="selector-tag">-</span> (void)<span class="selector-tag">myMethod</span><span class="selector-pseudo">:(id)anObj</span></span><br><span class="line">&#123;</span><br><span class="line"> <span class="variable">@synchronized</span>(anObj)</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="comment">// Everything between the braces is protected by the @synchronized directive.</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>传给 <code>@synchronized</code> 的对象是区分被保护的代码块的唯一标识。如果两个线程都执行上面的 <code>myMethod:</code> 方法,传入的对象如果不同,则不会有阻塞;传入的对象相同,则一个线程先获取锁,另一个线程在临界区域执行完成之前会一直阻塞。</p>
<p>使用 <code>@synchronized</code> 的前提是工程开启了 Objective-C exception handling 选项。因为 <code>@synchronized</code> 的 block 为了保护代码,预防措施是隐式加入 exception handler。handler 在异常抛出时会自动释放互斥锁。</p>
<p><a href="http://yulingtianxia.com/blog/2015/11/01/More-than-you-want-to-know-about-synchronized/">关于 @synchronized,这儿比你想知道的还要多</a></p>
<h4 id="使用其他-Cocoa-框架的锁"><a href="#使用其他-Cocoa-框架的锁" class="headerlink" title="使用其他 Cocoa 框架的锁"></a>使用其他 Cocoa 框架的锁</h4><h5 id="NSRecursiveLock"><a href="#NSRecursiveLock" class="headerlink" title="NSRecursiveLock"></a><code>NSRecursiveLock</code></h5><p><code>NSRecursiveLock</code> 类也就是递归锁,可以被同一线程获取多次而不会导致死锁。当然 <code>lock</code> 多少次,也要相应地 <code>unlock</code> 多少次,这样锁才会被真正释放,其他线程才能获取锁。</p>
<p>递归锁通常用于递归函数中来避免死锁线程。也可以用于非递归的场景下。这有个使用 <code>NSRecursiveLock</code> 的例子:</p>
<figure class="highlight cs"><table><tr><td class="gutter"><pre><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">NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init];</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">MyRecursiveFunction</span>(<span class="params"><span class="keyword">int</span> <span class="keyword">value</span></span>)</span><br><span class="line"></span>&#123;</span><br><span class="line"> [theLock <span class="keyword">lock</span>];</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">value</span> != <span class="number">0</span>)</span><br><span class="line"> &#123;</span><br><span class="line"> --<span class="keyword">value</span>;</span><br><span class="line"> MyRecursiveFunction(<span class="keyword">value</span>);</span><br><span class="line"> &#125;</span><br><span class="line"> [theLock unlock];</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line">MyRecursiveFunction(<span class="number">5</span>);</span><br></pre></td></tr></table></figure>
<p>注意:因为递归锁需要 <code>lock</code> 和 <code>unlock</code> 次数相等才能释放,应该小心权衡。可以重写代码来避免递归,或避免使用递归锁,这样可以获取更好的性能。</p>
<h5 id="NSConditionLock"><a href="#NSConditionLock" class="headerlink" title="NSConditionLock"></a><code>NSConditionLock</code></h5><p><code>NSConditionLock</code> 定义了可以用特定值来 <code>lock</code> 和 <code>unlock</code> 的互斥锁,但别跟条件变量搞混了。虽然行为差不多但实现很不一样。</p>
<p><code>NSConditionLock</code> 一般用于线程需要以特定的顺序执行任务时,例如生产者消费者问题。当生产者执行时,消费者需要使用程序中特定的条件变量来获取锁。所谓的条件变量其实就是个程序员定义的整型数。当生产者完成后,它会 <code>unlock</code> 并更新条件变量,进而唤醒了消费者线程。消费者线程继续处理数据。</p>
<p><code>NSConditionLock</code> 的加锁和解锁方法可以任意组合使用。比如可以用 <code>lock</code> 跟 <code>unlockWithCondition:</code> 搭配,或用 <code>lockWhenCondition:</code> 跟 <code>unlock</code> 搭配。当然第二种搭配没有在解锁后设置 <code>condition</code> 属性的值,其他一些等待特定条件变量的线程可能还会阻塞。</p>
<p>下面的例子展示了如何使用条件锁处理生产者-消费者问题。摄像应用含有一个数据队列,生产者线程向队列添加数据,消费者线程从队列取数据。生产者不需要等特定的条件,但是必须等锁可用的时候才能安全地向队列添加数据。</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><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">id condLock = [[NSConditionLock alloc] <span class="string">initWithCondition:</span>NO_DATA];</span><br><span class="line"> </span><br><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>)</span><br><span class="line">&#123;</span><br><span class="line"> [condLock lock];</span><br><span class="line"> <span class="comment">/* Add data to the queue. */</span></span><br><span class="line"> [condLock <span class="string">unlockWithCondition:</span>HAS_DATA];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>由于锁的初始条件被设置成 <code>NO_DATA</code>,生产者线程起初会顺利地获取锁。它向队列填充数据并将条件设置为 <code>HAS_DATA</code>。在接下来的迭代中,不管队列是否为空,生产者线程总能添加新数据。它只有消费者线程从队列中获取数据的时候才会阻塞。</p>
<p>由于消费者线程必须有数据去处理,它会用一个特定的条件等待着队列。当生产者网队列中放数据时,消费者线程会活跃起来并获取锁。然后它可能从队列获取一些数据并更新队列状态。下面的例子展示了消费者线程循环程序的基本结构。</p>
<figure class="highlight less"><table><tr><td class="gutter"><pre><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="selector-tag">while</span> (true)</span><br><span class="line">&#123;</span><br><span class="line"> <span class="selector-attr">[condLock lockWhenCondition:HAS_DATA]</span>;</span><br><span class="line"> <span class="comment">/* Remove data from the queue. */</span></span><br><span class="line"> <span class="selector-attr">[condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)]</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Process the data locally.</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h5 id="NSDistributedLock"><a href="#NSDistributedLock" class="headerlink" title="NSDistributedLock"></a><code>NSDistributedLock</code></h5><p><code>NSDistributedLock</code> 可以用于多个应用拥有并访问某个共享资源(例如文件)的临界区。它实际上是使用文件系统的文件或目录等实现的互斥锁。所有使用 <code>NSDistributedLock</code> 对象的应用必须对其可写入。这通常意味着将其放入一个所有运行此应用的计算机都能访问的文件系统。</p>
<p><code>NSDistributedLock</code> <strong>不</strong>像其他锁一样遵从 <code>NSLocking</code> 协议,没有 <code>lock</code> 方法。<code>lock</code> 方法会阻塞进程的执行并需要系统以预定的速率查询锁。与其在代码上强制损耗性能,不如用 <code>NSDistributedLock</code> 提供的 <code>tryLock</code> 方法来让程序员决定是否去查询锁。</p>
<p>由于 <code>NSDistributedLock</code> 使用文件系统实现,它只有在拥有者显式释放它时才会跟着释放。如果应用 crash 时还持有一个 <code>NSDistributedLock</code> 对象,其他 client 将不能访问被保护的资源。在这种情况下,可以使用 <code>breakLock</code> 方法打破已经存在的锁,这样就能获取到它了。通常应该避免打破锁,除非你确信拥有锁的进程挂了,无法释放锁。</p>
<p><code>NSDistributedLock</code> 跟其他锁一样,调用 <code>unlock</code> 方法释放它。</p>
<h3 id="使用条件(Condition)"><a href="#使用条件(Condition)" class="headerlink" title="使用条件(Condition)"></a>使用条件(Condition)</h3><p>Condition 是一种特殊类型的锁,它可以让操作必须以正确的顺序进行。它跟互斥锁有细微的差别。等待 condition 的线程会保持阻塞,直到 condition 被其他线程显式发信号。</p>
<p>由于牵扯到操作系统实现的细节,条件锁在即使没被发信号的情况下被允许伪造成功返回。为了避免这种站不住脚的发信号导致的问题,应该总是把断言跟条件锁结合起来使用。断言是一个决定线程是否能安全进行的更具体的方式。在发信号的线程设置断言前,condition 会让你的线程保持睡眠。</p>
<p>下面展示如何在代码中使用 condition。</p>
<h4 id="NSCondition"><a href="#NSCondition" class="headerlink" title="NSCondition"></a><code>NSCondition</code></h4><p><code>NSCondition</code> 是对 POSIX Condition 语法的封装,而且将锁和 condition 数据结构包含在一个对象里。这使得可以用一个对象既能当做互斥锁 <code>lock</code>,又能像 Condition 一样继续 <code>wait</code>。</p>
<p>下面的代码中 <code>cocoaCondition</code> 变量是一个 <code>NSCondition</code> 对象,<code>timeToDoWork</code> 变量是一个整型数,用作断言。其他线程会在向 condition 发信号之前立刻增加 <code>timeToDoWork</code> 的值。</p>
<figure class="highlight scheme"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">[<span class="name">cocoaCondition</span> lock]<span class="comment">;</span></span><br><span class="line">while (<span class="name">timeToDoWork</span> &lt;= <span class="number">0</span>)</span><br><span class="line"> [<span class="name">cocoaCondition</span> wait]<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">timeToDoWork--<span class="comment">;</span></span><br><span class="line"> </span><br><span class="line">// Do real work here.</span><br><span class="line"> </span><br><span class="line">[<span class="name">cocoaCondition</span> unlock]<span class="comment">;</span></span><br></pre></td></tr></table></figure>
<p>然后就是增加断言的值,并向 condition 发信号。当然这些操作要加锁:</p>
<figure class="highlight less"><table><tr><td class="gutter"><pre><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="selector-attr">[cocoaCondition lock]</span>;</span><br><span class="line">timeToDoWork++;</span><br><span class="line"><span class="selector-attr">[cocoaCondition signal]</span>;</span><br><span class="line"><span class="selector-attr">[cocoaCondition unlock]</span>;</span><br></pre></td></tr></table></figure>
<h4 id="POSIX-Condition"><a href="#POSIX-Condition" class="headerlink" title="POSIX Condition"></a>POSIX Condition</h4><p>POSIX 线程条件锁需要将 condition 数据结构和互斥锁一起使用。尽管两个锁结构是分开的,但是互斥锁在运行时会被紧紧地捆到 condition 结构上。等待发信号的线程应该始终将相同的互斥锁和 condition 结构一起使用。改变配对会导致错误。</p>
<p>下面的代码展示了 condition 和断言基本的初始化和使用。在初始化 condition 和互斥锁后,线程进入了一个使用 <code>ready_to_go</code> 变量作为断言的 <code>while</code> 循环。只有断言设置好并且 condition 接着被发信号后,等待着的线程才会被唤醒,并开始工作。</p>
<figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">pthread_mutex_t</span> mutex;</span><br><span class="line"><span class="keyword">pthread_cond_t</span> condition;</span><br><span class="line">Boolean ready_to_go = <span class="literal">true</span>;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">MyCondInitFunction</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line"> pthread_mutex_init(&amp;mutex);</span><br><span class="line"> pthread_cond_init(&amp;condition, <span class="literal">NULL</span>);</span><br><span class="line">&#125;</span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">MyWaitOnConditionFunction</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line"> <span class="comment">// Lock the mutex.</span></span><br><span class="line"> pthread_mutex_lock(&amp;mutex);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// If the predicate is already set, then the while loop is bypassed;</span></span><br><span class="line"> <span class="comment">// otherwise, the thread sleeps until the predicate is set.</span></span><br><span class="line"> <span class="keyword">while</span>(ready_to_go == <span class="literal">false</span>)</span><br><span class="line"> &#123;</span><br><span class="line"> pthread_cond_wait(&amp;condition, &amp;mutex);</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Do work. (The mutex should stay locked.)</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Reset the predicate and release the mutex.</span></span><br><span class="line"> ready_to_go = <span class="literal">false</span>;</span><br><span class="line"> pthread_mutex_unlock(&amp;mutex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>发信号的线程负责设置断言并向条件锁发信号。下面的代码展示了它的实现。为了避免线程之间等待 condition 而发生竞态条件,发信号的操作要在互斥锁里面进行。因为是简化过后的例子,代码中没包含错误处理的代码,只展示基础用法。</p>
<figure class="highlight aspectj"><table><tr><td class="gutter"><pre><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="function"><span class="keyword">void</span> <span class="title">SignalThreadUsingCondition</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line"> <span class="comment">// At this point, there should be work for the other thread to do.</span></span><br><span class="line"> pthread_mutex_lock(&amp;mutex);</span><br><span class="line"> ready_to_go = <span class="keyword">true</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Signal the other thread to begin work.</span></span><br><span class="line"> pthread_cond_signal(&amp;condition);</span><br><span class="line"> </span><br><span class="line"> pthread_mutex_unlock(&amp;mutex);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</content>
<summary type="html">
<p><a href="https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/ThreadSafety/ThreadSafety.html#//apple_ref/doc/uid/10000057i-CH8-SW1">Threading Programming Guide</a> 的学习笔记&amp;翻译,第三部分。关于同步的内容。</p>
<ul>
<li><a href="http://yulingtianxia.com/blog/2017/08/28/Threading-Programming-Guide-1/">Threading Programming Guide(1)</a></li>
<li><a href="http://yulingtianxia.com/blog/2017/09/17/Threading-Programming-Guide-2/">Threading Programming Guide(2)</a></li>
</ul>