-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
1859 lines (1686 loc) · 378 KB
/
atom.xml
File metadata and controls
1859 lines (1686 loc) · 378 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>Startry Blog</title>
<subtitle>随便乱写, 目前专攻iOS研发~</subtitle>
<link href="/atom.xml" rel="self"/>
<link href="http://startry.com/"/>
<updated>2017-01-10T03:49:20.000Z</updated>
<id>http://startry.com/</id>
<author>
<name>Startry</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>2016年&猴年总结</title>
<link href="http://startry.com/2017/01/10/2016-conclusion/"/>
<id>http://startry.com/2017/01/10/2016-conclusion/</id>
<published>2017-01-10T03:49:20.000Z</published>
<updated>2017-01-10T03:49:20.000Z</updated>
<content type="html"><p>距离去年写总结时间已经过去11个月,在这年关将至的时候,本人写一篇2016年的总结来鞭策督促自己。</p>
<p>和2015年的总结<code>知足常乐</code>一致,找个成语形容呗~</p>
<blockquote>
<p>开花结果</p>
</blockquote>
<p>2016年依旧是本人的家庭年~ 人生的几大事件里其中两件发生在了这一年里。开花结果指的是家庭组建&amp;女儿的诞生哇~</p>
<h3 id="工作">工作</h3><p>在2016年初,我给自己定了一个目标:”<strong>无论在公司的发展前景如何, 至少在公司深耕两年以上</strong>“, 这个目标是为了<strong>克制自己浮躁的内心</strong>。</p>
<p><img src="http://blog.startry.com/img/blog_17_hhzl.jpg" alt="hhzl"></p>
<p>2017年新目标</p>
<blockquote>
<p>心如止水</p>
</blockquote>
<p>17年的目标是对16年目标的一个扩展,不再强制约束自己在单位的工作最短期限,在没有达到职业生涯瓶颈之前,都需要保持<strong>心如止水</strong>的状态,时刻约束提高自己(PS: 现在还远远没有达到瓶颈)。</p>
<p>定这个目标还有一个意义。在过去的几年里,个人的情绪释放还是过于随性,因此在新的一年里,我希望能适当的<strong>提高自己的情绪控制能力</strong>,尽量减少因为自己的任性而伤害别人的次数。</p>
<h3 id="家庭">家庭</h3><p>2015年对本人来说是收获家庭的一年,而且有一颗未来的种子在不断的孕育长大,那2016年自然是开花结果的一年。</p>
<p><strong>2016年,我和老婆为咱们的小家庭购置了一套小房子~</strong></p>
<p><img src="http://blog.startry.com/img/blog_17_lzwhc.jpg" alt="liangzhu"></p>
<p>因为本人收入水平非常的有限,因此在大杭州的郊区良渚文化村购置了一套小户型房产。</p>
<p>良渚文化村的周边景观还是很漂亮的,还是值得推荐~ 适合没钱的&amp;隐居的,本人属于没钱的~ 因为住在这个村庄附近,如果没有一辆汽车,基本是“前不着店,后不着村”的。</p>
<p><strong>2016年,我亲爱的女儿降生了~</strong></p>
<p><img src="http://blog.startry.com/img/blog_17_tt.jpg" alt="daughter"></p>
<p>女儿的降生应该是2016年<strong>最最最重要</strong>的一件事情了~ </p>
<p>2.0出来了,那1.0自然就变成过度版本了,生活的重心开始偏移到女儿的身上啦~</p>
<h3 id="技术&amp;软技能">技术&amp;软技能</h3><p>过去的一年仍然是本人技术深入的瓶颈年,基本在积累业务经验,没有真正意义上的技术深度进步。</p>
<p>技术广度则稍微学习使用了python语言, 处于幼儿班级别的程度。学习python语言也只是为了快速实现自己的2个无聊的小工具想法。</p>
<p>2016年度尝试在<a href="https://stackoverflow.com/users/5238614/startry" target="_blank" rel="external">Stackoverflow</a>回复一些基础性的iOS技术问题,并保持2015年的博客写作; </p>
<p>2016年开源三个库,几乎都没有什么影响力,需要再接再厉~</p>
<ul>
<li><a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a> - 一个支持截取全内容的截图库</li>
<li><a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">RiskBlockScanner</a> - Object-C潜在循环引用文本检查器</li>
<li><a href="https://github.com/startry/SameCodeFinder" target="_blank" rel="external">SameCodeFinder</a> - 相似或相同代码查找器</li>
</ul>
<p>在2016年度接触的一些书籍阅读中,知道了“<strong>软技能</strong>”这个词。</p>
<blockquote>
<p>软技能就是激活人资的能力,即是调动别人的资源和知识的能力以及调动自己知识进行创造性思维的能力。</p>
</blockquote>
<p>个人觉得本人对”软件能”的提高目前存在严重的不足,需要在2017年逐渐重视起来~</p>
<p>PS: 软技能本人是从《软技能-代码之外的生存指南》看来的</p>
<h3 id="2017">2017</h3><p>回顾过去一年,自然需要展望一下未来。2017我给自己的<strong>原则</strong>是</p>
<blockquote>
<p>一段时间,只做一件事</p>
</blockquote>
<p>2016年我想做的事情太多, 但是因为太零散, 没有实际的计划, 几乎都没有成功实施。2017年想要继续做2016年想做的事情, 那必须让自己坚持干完其中一件事情,一件事情一件事情来攻克。</p>
<p><img src="http://blog.startry.com/img/blog_17_wjl.jpg" alt="daughter"></p>
<p>16年间,我们的首富“王健林”先生有一句名言“<em>先定一个能达到的小目标</em>”。 嗯,真正的小目标,不是一个亿哦~ 首富的小目标对我来说是完全不可能在一年可以实现的超级大目标。</p>
<p>回归主题,16年在工作&amp;家庭生活中,最深受打击的是个人的英语水平。因为16年的一次美国行,让本人意识到自己的英语短板的弊端。所以,本人迫切希望进一步提高英语水平。</p>
<p>2017年我不想给自己定一个严格的工作目标,也不想给自己定一个技术提升目标,更不想定一个财富实现目标,我想定的是一个语言技能实现目标:</p>
<blockquote>
<p>提高自己的英语水平,获得TOEFL或TOEIC的良好成绩</p>
</blockquote>
<p>英语能够帮助自己提高视野,便于交流,是一种对未来的投资。但是因为没有目标盲目的学习我担心自己就这样把一年给荒废了,因此需要用TOEFL或TOEIC的成绩作为辅助。</p>
<p><strong>PS: 本人是基层一线开发, 总结不具备广义参考意义的,勿转载</strong></p>
<p><strong>PS: 本篇文章非技术博客, 请勿转载</strong></p>
</content>
<summary type="html">
<p>距离去年写总结时间已经过去11个月,在这年关将至的时候,本人写一篇2016年的总结来鞭策督促自己。</p>
<p>和2015年的总结<code>知足常乐</code>一致,找个成语形容呗~</p>
<blockquote>
<p>开花结果</p>
</blockquote>
</summary>
<category term="随笔" scheme="http://startry.com/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
<entry>
<title>通过全文相似度来寻找相同或相似的代码</title>
<link href="http://startry.com/2016/12/14/find-same-code-by-simhash-and-hamming-distance/"/>
<id>http://startry.com/2016/12/14/find-same-code-by-simhash-and-hamming-distance/</id>
<published>2016-12-14T09:37:37.000Z</published>
<updated>2016-12-14T09:37:37.000Z</updated>
<content type="html"><p>最近笔者在职的公司在不断的做App的包瘦身工作, 身边的同事们也研究出了各种各样实用的工具来辅助加快包瘦身的进程。在这么一个大环境下, 笔者突然又冒出一个很无聊的工具想法</p>
<blockquote>
<p>通过文本匹配来寻找相似的方法函数</p>
</blockquote>
<p>笔者给这个小工具取了一个非常传神且牛逼的名字 - <a href="https://github.com/startry/SameCodeFinder" target="_blank" rel="external">SameCodeFinder</a></p>
<p>和上一个查找Block的无聊的小工具<a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">RiskBlockScanner</a>类似, 这个工具笔者觉得也是一个应用面相对比较小的一个工具, 所以笔者自嘲无聊的小工具哈~</p>
<p>笔者自认为这个是一个无聊的小工具, 但是还是坚持把它开发出来了, 因为笔者坚信:</p>
<blockquote>
<p>任何一个无聊小众的作品, 在合适的时机总是能够帮助到合适的人的! </p>
</blockquote>
<p>笔者开发这个小工具除了因为笔者相信这个工具肯定能够帮助到部分人群以外, 还有另外一个目的是督促自己不要停止学习的步伐哈~</p>
<h2 id="起源-辅助研发自查">起源-辅助研发自查</h2><p>查找相似代码想法的起源是因为笔者在在职的公司项目处于包瘦身的大环境下。在这个大环境下, 笔者身边的一名同事发明了基于<a href="http://blog.163.com/iswing@126/blog/static/166700480201129102259978/" target="_blank" rel="external">otool</a>和<a href="http://blog.cnbang.net/tech/2296/" target="_blank" rel="external">linkmap</a>分析查找无用方法的工具, 该工具在Github上有个类似的开源脚本项目<a href="https://github.com/nst/objc_cover" target="_blank" rel="external">objc_cover</a>。与此同时, 笔者的另外一名同事发表了一种基于Clang来查找无用方法的<a href="http://kangwang1988.github.io/tech/2016/11/01/find-unused-duplicate-code-of-your-app-using-clang-plugin.html" target="_blank" rel="external">博文</a>。</p>
<p>受这两位同事的影响, 笔者就在想自己能搞什么和他们不一样点的工具么。因为笔者之前用文本扫描的方式搞了一个简易的<a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">快速Block检查脚本</a>, 笔者就在想能不能通过类似的手段写一个类似的程序。笔者想借鉴<strong>《基于Clang来查找无用方法》</strong>的思路进行扩展, 因为该文章里提出了一种<strong>将文本内容Hash后进行内容比较, 判断方法是否完全重合的思路</strong>。</p>
<p>笔者基于该思路进行扩展, <strong>设想能不能不止比较“完全相同”的方法, 还能比较相似的方法</strong>。顺着这个思路发现了Google的全文搜索相似度比较的一种算法<a href="http://wwwconference.org/www2007/papers/paper215.pdf" target="_blank" rel="external">simhash</a>[7, 8]。</p>
<h3 id="Simhash">Simhash</h3><p>关于simhash的介绍引用博文<a href="simhash算法原理及实现">《simhash算法原理及实现》</a> 里的介绍</p>
<blockquote>
<p>simhash是google用来处理海量文本去重的算法。 google出品,你懂的。 simhash最牛逼的一点就是将一个文档,最后转换成一个64位的字节,暂且称之为特征字,然后判断重复只需要判断他们的特征字的距离是不是&lt;n(根据经验这个n一般取值为3),就可以判断两个文档是否相似。</p>
</blockquote>
<p>上述引用其实有点不完全正确, simhash貌似并不是Google出品的, 第一作者的邮箱后缀明明是普林斯顿大学好不好~ 不过Google将其应用到了网络爬虫并发表了一篇文章哈~</p>
<p>PS: 想了解详情? 阅读Paper去… =。=</p>
<p><a href="simhash算法原理及实现">《simhash算法原理及实现》</a> 针对simhash梳理了简易的原理介绍以及使用判断距离的汉明距离, 可以便于读者快速了解, 但是如果大家想要了解更深层次的实现, 可以去阅读原版paper<a href="http://wwwconference.org/www2007/papers/paper215.pdf" target="_blank" rel="external">《Detecting Near-Duplicates For Web Crawling》</a>和<a href="http://www.cs.princeton.edu/courses/archive/spr04/cos598B/bib/CharikarEstim.pdf" target="_blank" rel="external">《Similarity estimation techniques from<br>rounding algorithms》</a>。</p>
<h4 id="原理简析">原理简析</h4><p>simhash的生产步骤可以分为如下:</p>
<ol>
<li>提取目标文本的关键字feature和权重weight, 并成对存储<ul>
<li>如果不知道怎么提取的同学, 可能需要稍微了解全文搜索相关的知识</li>
</ul>
</li>
<li>将提取出来的关键字进行传统Hash, 输出二进制的值</li>
<li>将每一个关键字提取的Hash按位进行运算, 如果当前位是1, 则增加对应的权重; 如果当前位是0, 增减少当前对应的权重;</li>
<li>将最后得出来的hash值, 如果大于等于1, 则当做1处理; 负数和0当做0处理, 得出最终的二进制值</li>
</ol>
<p>上述步骤可以简化为下图, 此图引用了<a href="http://grunt1223.iteye.com/blog/964564" target="_blank" rel="external">我的数学之美系列二 —— simhash与重复信息识别</a>中的图</p>
<p><img src="http://blog.startry.com/img/blog_simhash_explaination.jpg" alt="simhash原理示意"></p>
<h4 id="汉明距离">汉明距离</h4><p>simhash是一种<a href="https://en.wikipedia.org/wiki/Locality-sensitive_hashing" target="_blank" rel="external">局部敏感Hash</a>。因此可以利用<a href="https://en.wikipedia.org/wiki/Hamming_distance" target="_blank" rel="external">汉明距离</a>去衡量simhash的相似度。</p>
<p>引入Wikipedia的汉明距离介绍:</p>
<blockquote>
<p>In information theory, the Hamming distance between two strings of equal length is the number of positions at which the corresponding symbols are different.</p>
</blockquote>
<p>字面上意思好像就是两个字符串在不一样字符个数的数量, 在我们现在的应用场景就是统计1或者0的个数, 然后他们的个数差就是距离了。。。一般搜索引擎的历史经验默认是<strong>3</strong> </p>
<p>PS: 别问我怎么知道的3的, 我也是从博客里看来的, 没有数据依据</p>
<h2 id="寻找相似的代码">寻找相似的代码</h2><h3 id="寻找完全相似的文件">寻找完全相似的文件</h3><p>针对上述理论, 只要是一个文档都可以计算出两者的汉明距离, 利用汉明距离来就可以衡量两个文档的相似度了。笔者在这里目前没有做太多的工作, 只不过过滤了文档的后缀, 让相当类型的文档进行相互的比较。</p>
<p>寻找相似的文件和寻找相似的代码文件, 其实本质上差距不大。代码文件有一些特性, 例如前面的声明和引用都有一列类似的地方, 如果在进行simhash计算处理前能够提前对代码文件进行预处理的话, 能够大幅度的提高整个代码文件相似度计算的精度。</p>
<p>PS: 鉴于思路的完善性和时间成本, 笔者还没有针对代码进行预处理</p>
<h3 id="寻找雷同方法函数">寻找雷同方法函数</h3><p>既然利用simhash以及汉明距离可以计算两个文档的相似度, 然自然可以缩小范围计算两个函数方法的相似度。那么问题的关键就在于<strong>怎么样才能提取到合适正确的方法函数内容</strong></p>
<p>笔者目前使用的是<strong>文本扫描匹配</strong>的方式, 但是笔者的同事有提出一种是基于clang插件来提取<a href="https://www.objc.io/issues/6-build-tools/mach-o-executables/#preprocessing" target="_blank" rel="external">编译器预处理</a>之后的内容进行hash比较的可行思路。无奈鉴于实现成本和插件无法独立运行的方面考虑, 暂时采用的<strong>直接扫描匹配文本</strong>的方式进行比较。</p>
<p>目前笔者采取的提取方法体方法是:</p>
<ol>
<li>用正则匹配获取方法起始行</li>
<li>从起始行开始记录左右括号的格式, 并且将起始行开始的所有字符串记录</li>
<li>当左右括号的个数相互抵消的时候默认当做匹配整个方法, 保存整个字符串</li>
</ol>
<p>鉴于方法匹配需要根据语法实现, 所以目前只能根据每个语言的语法特性进行截获, 目前<a href="(https://github.com/startry/SameCodeFinder">SameCodeFinder</a>)仅支持Object-C和Java。</p>
<p>语法特性局限了脚本的可扩展性, 步骤一的正则需要和后缀匹配, 步骤二的左右括号在某些语言下不适用, 只能利用发现下一个方法起始行作为步骤三的结束步骤。</p>
<p>Java目前采用正则:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">ur"(public|private)(.*)\)\s?&#123;"</span></span><br></pre></td></tr></table></figure>
<p>Object-C目前采用正则:</p>
<figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">ur"(\-|\+)\s?\(.*\).*(\:\s?\(.*\).*)?&#123;?"</span></span><br></pre></td></tr></table></figure>
<h3 id="排序">排序</h3><p>无论是寻找雷同的文件还是寻找雷同的方法, 最后计算出的Hash结果都是N * N个的, 那么怎么展示计算的结果呢? 如果把所有的结果都展示出来, 那明显可阅读性太低。</p>
<p>目前采用的逻辑是:</p>
<ol>
<li>N * N 中第一个N只找出距离最小的第一个返回, 这样过滤结果只保留N个</li>
<li>将第一步过滤返回的N个结果按照从小到大的方式进行排序</li>
</ol>
<p>此外,在执行排序的步骤1和步骤2之间, 都可以添加一个最大距离过滤, 默认不超过20, 可以大幅度减少步骤1和步骤2的计算排序过滤时间。</p>
<h3 id="开源实现">开源实现</h3><p>笔者基于上述思路以及现成的工具, 利用python脚本花了2天时间去高速实现了一个简易的python脚本, 并开源到了Github上。</p>
<p>访问地址: <a href="https://github.com/startry/SameCodeFinder" target="_blank" rel="external">SameCodeFinder</a></p>
<p>目前开源的版本可能因为笔者使用不当或者开源python版本的<a href="https://github.com/leonsim/simhash" target="_blank" rel="external">simhash</a>的计算<strong>太过耗时</strong>, 因此在性能上存在一定的性能问题, 计算整个较大的工程需要花费不少的时间(计算一个大型工程是分钟级别的)。</p>
<p>笔者会在之后寻找突破方法来提高这方面的计算性能~ </p>
<h2 id="总结">总结</h2><p><a href="https://github.com/startry/SameCodeFinder" target="_blank" rel="external">SameCodeFinder</a>可以帮助大家寻找相似或者完全重叠的方法以及类, 极大程度上可以辅助大家寻找可以<strong>复用</strong>的代码。SameCodeFinder的实现思路都是借用Google的全文相似度比较的现成实现, 并没有什么创新, 但是脚本化和针对语种设计的方法识别, 能够帮助大家节省不少直接利用simhash去实现的成本。</p>
<p>PS: 个人水平有限, 有错误之处请大家及时指出, 随时交流哈~</p>
<h4 id="参考文献">参考文献</h4><ol>
<li><a href="http://kangwang1988.github.io/tech/2016/11/01/find-unused-duplicate-code-of-your-app-using-clang-plugin.html" target="_blank" rel="external">CLANG技术分享系列四:IOS APP无用代码/重复代码分析</a></li>
<li><a href="http://leons.im/posts/a-python-implementation-of-simhash-algorithm/" target="_blank" rel="external">A Python Implementation of Simhash Algorithm</a></li>
<li><a href="http://blog.163.com/iswing@126/blog/static/166700480201129102259978/" target="_blank" rel="external">otool</a></li>
<li><a href="http://blog.csdn.net/u011581005/article/details/19711831" target="_blank" rel="external">Mac的反编译工具一:otool (objdump工具的OSX对应工具)</a></li>
<li><a href="http://blog.cnbang.net/tech/2296/" target="_blank" rel="external">iOS APP可执行文件的组成</a></li>
<li><a href="http://yanyiwu.com/work/2014/01/30/simhash-shi-xian-xiang-jie.html" target="_blank" rel="external">simhash算法原理及实现</a></li>
<li>GS Manku, A Jain, A Das Sarma. Detecting Near-Duplicates For Web Crawling. International Conference on World Wide Web. International Conference on World Wide Web. 141-149. 2007.</li>
<li>M. Charikar. Similarity estimation techniques from<br>rounding algorithms. In Proc. 34th Annual Symposium<br>on Theory of Computing (STOC 2002), pages<br>380–388, 2002.</li>
<li><a href="http://grunt1223.iteye.com/blog/964564" target="_blank" rel="external">我的数学之美系列二 —— simhash与重复信息识别</a></li>
<li><a href="https://en.wikipedia.org/wiki/Locality-sensitive_hashing" target="_blank" rel="external">Locality-sensitive hashing</a></li>
<li><a href="https://en.wikipedia.org/wiki/Hamming_distance" target="_blank" rel="external">Hamming_distance</a></li>
<li><a href="https://www.objc.io/issues/6-build-tools/mach-o-executables" target="_blank" rel="external">Mach-O Executables</a></li>
</ol>
</content>
<summary type="html">
<p>最近笔者在职的公司在不断的做App的包瘦身工作, 身边的同事们也研究出了各种各样实用的工具来辅助加快包瘦身的进程。在这么一个大环境下, 笔者突然又冒出一个很无聊的工具想法</p>
<blockquote>
<p>通过文本匹配来寻找相似的方法函数</p>
</blockquo
</summary>
<category term="Hamming Distance" scheme="http://startry.com/tags/Hamming-Distance/"/>
<category term="simhash" scheme="http://startry.com/tags/simhash/"/>
<category term="优化" scheme="http://startry.com/tags/%E4%BC%98%E5%8C%96/"/>
</entry>
<entry>
<title>一个奇怪且无聊的检测Block的想法</title>
<link href="http://startry.com/2016/11/02/Weird-idea-for-block-detection/"/>
<id>http://startry.com/2016/11/02/Weird-idea-for-block-detection/</id>
<published>2016-11-02T12:24:09.000Z</published>
<updated>2016-11-02T12:24:09.000Z</updated>
<content type="html"><p>在大多数iOS应用开发过程中, 循环引用一直都是最常见的iOS开发问题之一。通常情况下, 最常见的循环引用问题就是在Block回调中的self指针的不当使用了。在这种最常见的场景中, 对象self本身使用的Block被self对象本身<strong>持有</strong>(临时变量block是不会形成循环引用的), 而在Block回调中又使用了self导致形成了一个循环引用。</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><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">self.myBlock = ^&#123;</span><br><span class="line"> [self doSomething];</span><br><span class="line"> &#125;;</span><br><span class="line"> +-----------+ +-----------+</span><br><span class="line"> | self | <span class="keyword">ref</span> | <span class="type">Block</span> |</span><br><span class="line"> ---&gt; | | --------&gt; | |</span><br><span class="line"> | retain <span class="number">2</span> | &lt;-------- | retain <span class="number">1</span> |</span><br><span class="line"> | | <span class="keyword">ref</span> | |</span><br><span class="line"> +-----------+ +-----------+</span><br></pre></td></tr></table></figure>
<p>针对上述最常见的循环引用的场景, 在市面上最常用的几款循环引用检查工具都<strong>不能特别有效</strong>的检测出是否真正的产生了循环引用。笔者针对这种场景突然脑洞大开, 设想是否能够通过文本扫描的方式提前知道哪些block使用场景可能会产生潜在的循环引用问题。为了支持自己的大脑洞想法, 笔者尝试用python书写一个文本扫描器来提前预知潜在Self Retain Block循环引用风险的代码。哈哈, 至此, 一个无聊又奇怪的检测Block的小工具<a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">RiskBlockScanner</a>就诞生了。</p>
<h3 id="想法起源">想法起源</h3><p>笔者这个奇怪的Block想法的起源是因为笔者在工作中参与过的几个大型iOS项目(大于30人协作开发)中都涉及到很多使用Block的场景, 而且经常会发现代码中有些潜在或者确定的Block引用风险, 并且这些引用的风险多数和<code>self</code>指针和<code>Block</code>的组合使用相关联。同时, XCode自带的Warning只能报出比较简单的循环引用警告, 并且通过XCode自带的静态分析有时候也难以发现循环引用的出处。</p>
<p>虽然这个奇怪的想法是突然萌生的, 但是刺激因素有如下:</p>
<ul>
<li><strong>上百万行的代码量太多, 无法人肉检查</strong>(最主要用途)</li>
<li>ReactiveCocoa使用中大量涉及Self Retain Cycle的场景</li>
<li>大型项目开发参差不齐&amp;参与人数过多, 低级错误无法100%避免</li>
<li>循环引用排查困难</li>
<li>XCode自带循环引用排查功能不太完善</li>
</ul>
<h3 id="实现方案">实现方案</h3><p>有奇怪的想法第一件事情当然是验证想法的可行性。在不追求性价比的前提下, 验证想法可行性最愚蠢也是最有效的方法就是把想法实现出来,那么<a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">RiskBlockScanner</a>就这么诞生了。</p>
<p>市面上大多数的Block循环检查基本都是要编译链接的<strong>(例如XCode自带的Anaylse)</strong>, 有些甚至是在操作过程中分析内存是否释放<strong>(例如facebook开源的<a href="https://github.com/facebook/FBRetainCycleDetector" target="_blank" rel="external">FBRetainCycleDetector</a>)</strong>, 很少有检测工具针对编写代码过程中快速发现风险的方面进行设计。</p>
<p>我的这个奇怪的思想是通过正则表达式去匹配发现所有的Block使用起始行、该Block所在的方法起始&amp;结束行。</p>
<p><a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">RiskBlockScanner</a>的大致思路如下:</p>
<ol>
<li>扫描目录提取代码文件(.m格式)</li>
<li>提取每一行代码行</li>
<li>根据每一行代码行去根据正则表达式匹配出方法起始行(func line)、block起始行(block line)、潜在多行block起始行(potential block line)、block结束行和weak执行行(weak line)</li>
<li>根据”}”和”;”组合判断和潜在多行block起始行过滤不是block的行, 保留真正block行</li>
<li>过滤固定匹配关键字的block行(例如masonry等常见不持有block的场景)</li>
<li>判断weak执行行(weak line)是否包含在方法起始行(func line)和block起始行(block line)之间</li>
<li>根据第7步的判断结果来决定是否该Block存在循环引用风险</li>
</ol>
<p><img src="http://blog.startry.com/img/blog_rbs_code_show.png" alt="RiskBlockScanner简易演示"></p>
<p>上图表述的步骤均是根据存储数据来维护的, 具体的逻辑实现因为比较简单本文就不赘述了。如果大家有感兴趣的话请自行访问<a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">Github</a>地址进行代码阅读~</p>
<p>通过RiskBlockScanner可以扫描出大量的Block Self-Retain的风险代码行, 使用方法相对简单.(PS: 对代码量比较小或者不怎么使用Block的工程几乎没有作用) </p>
<p>下图为简单的使用示例:<br><img src="https://github.com/startry/RiskBlockScanner/blob/master/demo.png?raw=true" alt="RiskBlockScanner使用示例"></p>
<h3 id="优化设想">优化设想</h3><ul>
<li>通过不断的积累实验, 过滤更多的白名单方法</li>
<li>风险检测包含但不限于self的检测</li>
<li>开发一个XCode插件工具(看笔者是否够勤奋)</li>
<li>尝试支持检查Swift语法</li>
</ul>
<h3 id="总结">总结</h3><p><a href="https://github.com/startry/RiskBlockScanner" target="_blank" rel="external">RiskBlockScanner</a>是一个检测简单的Block循环引用风险的灵感实现。鉴于该工具是基于文本扫描实现的, 并且没有检测Block是否被使用对象真正持有(不持有则不产生循环引用), 所以它并不能<strong>真正意义上</strong>的检查出Block循环引用, 只能检查出所有潜在的风险点方便开发者排查定位。</p>
<p>该工具纯属笔者好奇实现, 可能有不少童鞋会觉得这个很无聊, 其实笔者觉得这个实际意义也不是特别大哈, 但是有总比没有好。至少该工具可以<strong>在开发者编写代码过程中快速分析定位block引用风险</strong>的代码位置, 能够帮助开发者高效的定位Block使用风险。</p>
<p>PS: 笔者水平有限, 如果文章有错误之处, 请大家一定要指出, 防止误导大家哈~~ </p>
<h4 id="参考文献">参考文献</h4><ol>
<li><a href="http://www.jianshu.com/p/b79bac09177e" target="_blank" rel="external">避免Block的循环引用</a></li>
<li><a href="http://www.jianshu.com/p/2c7a7c53c91a" target="_blank" rel="external">使用FBRetainCycleDetector检测引用循环</a></li>
</ol>
</content>
<summary type="html">
<p>在大多数iOS应用开发过程中, 循环引用一直都是最常见的iOS开发问题之一。通常情况下, 最常见的循环引用问题就是在Block回调中的self指针的不当使用了。在这种最常见的场景中, 对象self本身使用的Block被self对象本身<strong>持有</strong>(
</summary>
<category term="Block" scheme="http://startry.com/tags/Block/"/>
<category term="iOS" scheme="http://startry.com/tags/iOS/"/>
</entry>
<entry>
<title>矢量图在iOS中的应用细节</title>
<link href="http://startry.com/2016/06/15/vector-apply-to-iOS-Project/"/>
<id>http://startry.com/2016/06/15/vector-apply-to-iOS-Project/</id>
<published>2016-06-15T13:02:44.000Z</published>
<updated>2016-06-15T13:02:44.000Z</updated>
<content type="html"><p>对于矢量图的调研, 最开始是始于对其占用iOS App的空间的好奇。笔者好奇一个问题: <strong>利用矢量图能不能帮助iOS App减少整体空间?</strong></p>
<p>iOS其实在很早的时候就已经支持矢量图的应用(XCode 6时代开始支持), 只不过因为大部分开发者沿用了以前@1x、@2x、@3x格式图的习惯, 并没有一个地方专门普及使用矢量图。当然, 还有另外一个原因就是iOS对复杂的矢量图支持的不是很好。</p>
<p>本文并没有特别深入的技术点, 仅仅只是笔者做的几个实验的总结和矢量图基础使用的教程普及~</p>
<h3 id="矢量图和iOS">矢量图和iOS</h3><p>关于矢量图在iOS中的使用早在15年2月就有一篇博文介绍了它的使用 - <a href="https://www.zhihu.com/question/23180955" target="_blank" rel="external">iOS使用矢量图的总结</a>。笔者按照它的用法操作了一遍, 基本大同小异~ 针对该文章没有涉及到的一些细节, 笔者进行一定程度的补充。</p>
<h4 id="iOS中矢量图的使用方法">iOS中矢量图的使用方法</h4><p>笔者为了做一下简单的矢量图实验, 使用<a href="http://www.sketchapp.com/" target="_blank" rel="external">Sketch</a>随意拖了一个星星出来并导出一个<a href="http://blog.startry.com/img/star_test.pdf" target="_blank" rel="external">PDF</a>。(实际上UI绘制矢量图的工具有很多很多, 这里不赘述。如果读者懒得自己去导出, 可以直接挪用)</p>
<p>XCode支持矢量图一定要放置在xcasset文件中才能够生效, 操作步骤如下:</p>
<ol>
<li>拖拽提前制作的PDF进入<code>XXX.xcassets</code>中。</li>
<li>选择<code>Image Set</code>下的参数选项<code>Scale Factors</code>为<code>Single Vector</code>或<code>Vector With Overrides</code></li>
<li>如果图形不在框框中, 拖入框框中(XCode某些版本不能自动对号入座)</li>
</ol>
<p><img src="http://blog.startry.com/img/blog_vector_xcasset_demo.png" alt="iOS矢量图使用"></p>
<p><code>Vector With Overrides</code>是<code>Single Vector</code>的增强, 可以在放置完矢量图之后继续放置@1x、@2x和@3x的png格式的图片。放置的png会优先覆盖矢量图, 未放置对应倍率图片的设备才会使用矢量图对应生成的图片。</p>
<h4 id="矢量图在iOS中的应用原理">矢量图在iOS中的应用原理</h4><p><strong>iOS对矢量图的支持其实只是一种方便开发者的选择, 本质上在XCode编译的阶段矢量图会自动生成对应Target的@1x,@2x和@3x的png格式图像。在iOS实际运行中使用的图片实际上已经是png格式的图片了~</strong></p>
<p><img src="http://blog.startry.com/img/blog_vector_build_illustrator.png" alt="iOS下矢量图工作原理"></p>
<p>通俗的理解 - 放置在xcassets里的矢量图会自动根据设备编译成对应尺寸的图片, 如果是<code>Generic iOS Device</code>则会自动生成全尺寸的同名图片。 </p>
<p>PS: <strong>自动生成的@1x图会和矢量图的原始尺寸保持一致。</strong></p>
<p>下图为利用<a href="https://github.com/alexzielenski/ThemeEngine" target="_blank" rel="external">ThemeEngine</a>打开的基于<code>Generic iOS Device</code>编译出来的Assets.car文件</p>
<p><img src="http://blog.startry.com/img/blog_vector_themeengine.png" alt="ThemeEngine下car文件"></p>
<h3 id="矢量图能否减少空间">矢量图能否减少空间</h3><p>回归到最初的问题, 到底使用矢量图能不能帮助iOS App减少空间呢? </p>
<p>笔者用简单粗暴的实验来对比说明, 步骤如下:</p>
<ol>
<li>使用pdf原始文件编译生成通用IPA</li>
<li>从生成的IPA文件中提取Asset.car文件</li>
<li>利用<a href="https://github.com/devcxm/iOS-Images-Extractor" target="_blank" rel="external">iOS Image Extractor</a>提取Asset.car文件</li>
<li>将提取出来的@1x、@2x、@3x放置回工程, 并删除原始pdf中重新编译</li>
<li>对比步骤1生成的car文件和步骤4生成的car文件大小</li>
</ol>
<table><br> <tr><br> <th>步骤一编译car大小(仅PDF)</th><br> <th>步骤四编译car大小(仅3张图)</th><br> <th>测试PDF尺寸</th><br> </tr><br> <tr><br> <td>115KB</td><br> <td>86KB</td><br> <td>20KB</td><br> </tr><br></table>
<p><strong>在iOS8.3以下, 相同压缩比例的条件下, 矢量图是无法帮助App减少空间。但是在iOS8.3以上, 利用xcassets可以避免多余的资源图片下载, 只下载对应的倍率的图片。因此, 严格意义下, 利用矢量图并不能帮助App节省空间。</strong></p>
<p>其实笔者使用的简单粗暴的方式在苹果新的瘦身机制下是不成立的, 因为编译生成的最终包不一定就是设备最终安装的包。引用官方文档<a href="https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html" target="_blank" rel="external">App Thinning</a>中<a href="https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html#//apple_ref/doc/uid/TP40012582-CH35-SW1" target="_blank" rel="external">Slicing</a>章节中的一段话:</p>
<blockquote>
<p>In Xcode, specify target devices and provide multiple resolutions of images in the asset catalog.<br>You must use the asset catalog in order for resources to be sliced.</p>
</blockquote>
<p>笔者未能完全参透这句话的意思, 只知道xcasset会根据不同的设备会有不同的解决方案, 但是不知道粒度会达到什么样的程度。笔者尝试过针对不同的模拟器编译, 只会生成对应的倍率图, 但是上传给App Store是通用格式的, 难道下载的过程中进行了一定的处理? 这个就需要一台越狱设备去验证了, 就靠各位读者大大了~</p>
<p>PS: 对iOS如何控制多余资源图片下载感兴趣的童鞋们, 可以找一台越狱设备, 将iOS 8.3以上的App给提取出来分析分析哈~ 记得分享哇~ \(^o^)/</p>
<h4 id="题外探究之提取原理">题外探究之提取原理</h4><p>笔者在对比大小的时候使用的<a href="https://github.com/devcxm/iOS-Images-Extractor" target="_blank" rel="external">iOS Image Extrator</a>。 在使用过程中, 笔者对该工具的提取原理产生了那么一点点的好奇。笔者好奇xcasset的格式应该是封闭不开放的, 该工具是怎么从Asset.car中提取图片的, 难道该工具破解了Asset.car的格式?</p>
<p>既然iOS Image Extrator是开源的, 那么笔者就有必要去看一看究竟了~ iOS Image Extrator其实是基于开源库<a href="https://github.com/Marxon13/iOS-Asset-Extractor" target="_blank" rel="external">iOS Asset Extrator</a>开发实现的, 核心提取的功能是在<strong>iOS Asset Extrator</strong>库下提取的, 笔者通过阅读其源码, 找到两个核心方法<a href="https://github.com/Marxon13/iOS-Asset-Extractor/blob/master/CARExtractor/CARExtractor/CARExporter.m#L33" target="_blank" rel="external">exportToDirectory:</a>和<a href="https://github.com/Marxon13/iOS-Asset-Extractor/blob/master/CARExtractor/CARExtractor/CARExporter.m#L77" target="_blank" rel="external">exportThemeRendition:</a></p>
<p>通过阅读这两个方法的源代码可以了解到这个库的基本实现。<a href="https://github.com/Marxon13/iOS-Asset-Extractor/blob/master/CARExtractor/CARExtractor/CARExporter.m#L33" target="_blank" rel="external">exportToDirectory:</a>方法有该库核心的提取图片的所有逻辑代码。而<a href="https://github.com/Marxon13/iOS-Asset-Extractor/blob/master/CARExtractor/CARExtractor/CARExporter.m#L77" target="_blank" rel="external">exportThemeRendition:</a>可以看出该库支持的所有格式, 并且通过苹果内置的各个格式的<code>Rendition</code>类提取导出。</p>
<p><a href="https://github.com/Marxon13/iOS-Asset-Extractor" target="_blank" rel="external">iOS Asset Extrator</a>库本质上调用的是苹果的私有API。在该系列API中, <code>CUICommonAssetStorage</code>负责存储Asset资源的关键key, <code>CUICatalog</code>是承载了具体资源图片信息的登记目录。</p>
<p><font color="red">回归主题, 开源库底层既然是苹果API, 那么就基本是一个黑盒子了。笔者既不能从暴露的API中分析出car的格式, 又不能判断iOS设备是否在执行中解压, 只好放弃</font>~</p>
<h3 id="总结">总结</h3><p>矢量图严格意义上并不能帮助减少App的空间, 但是却使用起来非常的方便, 建议使用。iOS本质上并不支持矢量图, 但是在编译阶段会将矢量图转化成目标设备对应的尺寸图, 同时会利用xcassets的特性在iOS8.3以上设备下支持部分资源下载, 带到包瘦身的效果。每次都要让UI给多个尺寸的图, 肯定没有给一张方便吧? 当然, 前提是UI的童鞋是基于矢量图工具制作的图片的前提下~</p>
<h4 id="参考文献">参考文献</h4><ol>
<li><a href="https://www.zhihu.com/question/23180955" target="_blank" rel="external">知乎 - iOS对矢量图片的支持如何?</a></li>
<li><a href="http://blog.csdn.net/lengshengren/article/details/43406905" target="_blank" rel="external">iOS使用矢量图的总结</a></li>
<li><a href="https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/AppThinning/AppThinning.html" target="_blank" rel="external">App Thinning</a></li>
<li><a href="https://github.com/alexzielenski/ThemeEngine" target="_blank" rel="external">ThemeEngine</a></li>
<li><a href="https://github.com/devcxm/iOS-Images-Extractor" target="_blank" rel="external">iOS Image Extrator</a></li>
<li><a href="https://github.com/Marxon13/iOS-Asset-Extractor" target="_blank" rel="external">iOS Asset Extrator</a></li>
</ol>
</content>
<summary type="html">
<p>对于矢量图的调研, 最开始是始于对其占用iOS App的空间的好奇。笔者好奇一个问题: <strong>利用矢量图能不能帮助iOS App减少整体空间?</strong></p>
<p>iOS其实在很早的时候就已经支持矢量图的应用(XCode 6时代开始支持), 只不过因为
</summary>
<category term="iOS" scheme="http://startry.com/tags/iOS/"/>
</entry>
<entry>
<title>在Pod库中使用xcasset的拷贝陷阱</title>
<link href="http://startry.com/2016/03/17/the-trap-of-image-resource/"/>
<id>http://startry.com/2016/03/17/the-trap-of-image-resource/</id>
<published>2016-03-17T11:54:10.000Z</published>
<updated>2016-03-17T11:54:10.000Z</updated>
<content type="html"><p>本篇文章来自笔者工作中遇到一个难解的BUG - 在App中用<code>UIImage</code>的<code>imageNamed:</code>方法读取的图片始终是<strong>不正确</strong>的。</p>
<p><img src="http://blog.startry.com/img/blog_image_repeate_intro.png" alt="暴走示意"></p>
<p>场景条件回放:</p>
<ol>
<li>有多张同名图片存在工程下, 比如都叫<code>pic_same_test</code></li>
<li>同名图片有存在被工程引用的子Bundle中, 主Bundle中和xcasset中</li>
<li>同名图片未被工程引用进来, 但是放置在工程物理目录下的某个xcasset中</li>
</ol>
<p>试想一下, 这个时候你如果使用下述代码去读取该图片, 会发生取到哪种图片呢?</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UIImage *image = [UIImage imageNamed:@&#34;pic_same_test&#34;];</span><br></pre></td></tr></table></figure>
<p>在上述的条件场景中, 当我在应用中用UIImage去读取<strong>A</strong>图片的时候, 总是会读取到了错误的<strong>B</strong>图片。因为笔者最初的排查方向只在<strong>条件1</strong>和<strong>条件2</strong>两个方向去查找, 没有去深究未被工程引用的部分, 导致了整个思路方向被引向了错误的方向, 极大的加深了BUG的排查难度。</p>
<p>笔者在这个问题上纠结了很久, 在<a href="http://stackoverflow.com/questions/36008210/uiimage-load-wrong-image-in-main-bundle" target="_blank" rel="external">Stackoverflow</a>和<a href="https://forums.developer.apple.com/message/124162" target="_blank" rel="external">苹果开发者论坛</a>都根据这个场景进行了提问, 最终在开发者论坛中经过昵称为<em>Bob133</em>的高人指点, 将问题的<font color="orange">突破口</font>定位在了<a href="https://developer.apple.com/library/prerelease/ios/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/index.html#//apple_ref/doc/uid/TP40015170-CH18-SW1" target="_blank" rel="external">xcasset</a>上。</p>
<h2 id="神秘的错误图片">神秘的错误图片</h2><p>事情的起因是因为笔者在开发的某App的时候突然爆出了一个图片锯齿的BUG, 可是笔者的代码在线上已经稳定运行了几个月了, 怎么可能会突然抽风呢? </p>
<p>处于笔者对UIImage的了解, 第一反应想到的就是<strong>缓存</strong>。这里的<strong>缓存</strong>不是UIImage加载图片的加速缓存, 而是在打包时候的资源不重复copy的<strong>缓存</strong>。因此, 笔者对这个BUG的存在性持有怀疑的态度, 二话不说自己做起了实验, 执行了下述操作:</p>
<ol>
<li>删除Project对应的<strong>Derived Data</strong>.</li>
<li>对项目执行<strong>clean</strong>操作</li>
<li>删除目标设备的项目App</li>
<li>重新打包编译整个App</li>
</ol>
<p>经过上述四部操作和漫长的打包等待, 结果当然是呵呵哒了~ 如果结果正常就不会出现本篇博文了! 没错, 经过上述四部操作, 图片依旧还是<strong>错误</strong>的! </p>
<p>呵呵, 删除缓存不行, 那就不是缓存问题, 笔者怀疑打出来的包里面有图片串位的可能, 心想根目录下的图是不是就是错误的。不多说, 提取ipa, 显示包内容, 包内容根目录下的图竟然是<strong>正确</strong>的!!! </p>
<p>在包内容目录下, 我想要取的图片名字一样的图片总共就两张, 一张在根目录下, 另外一张在子Bundle下面。既然总共就2张图片, 那我就尝试在工程里<strong>删除掉子Bundle下的另外一张图片</strong>, 然后执行上述四部操作重新来过。结果大家想必还是知道的, 图片照样是错误的, 但是打包出来的文件包根目录下就只有一张<strong>正确</strong>的图片! </p>
<p>这个尼玛不是一张幽灵图片么? 笔者当时脑洞大开, 甚至怀疑到是否iCloud同步下来的, 可是笔者的测试机压根就没有绑定iCloud。</p>
<blockquote>
<p>PS: 当时忽略了Assets.car是因为工程里引用的Image.assets里并没有这张同名的文件, 源文件没有, 那自然就不会怀疑打包后的内容。另外, 笔者比较懒, 懒得去提取car文件。</p>
</blockquote>
<h2 id="产生的原因&amp;解决方案">产生的原因&amp;解决方案</h2><p>针对这个幽灵图片, 笔者在<strong>XCode</strong>全局搜索, 也就搜索到前文提到的两者图片。那么这个图片究竟是从哪里来的呢? </p>
<p>笔者在这个问题上纠结了超过十个小时, 并分别在<a href="https://forums.developer.apple.com/message/124162" target="_blank" rel="external">苹果开发论坛</a>和<a href="http://stackoverflow.com/questions/36008210/uiimage-load-wrong-image-in-main-bundle" target="_blank" rel="external">Stackoverflow</a>提出的疑问, 但是疑问有误导回答者往Bundle排查的嫌疑。</p>
<p>但是世界上开发牛人这么多, 稍微误导下问题也不大, 在<a href="https://forums.developer.apple.com/message/124162" target="_blank" rel="external">苹果开发者论坛</a>中的用户<strong>bob133</strong>说他曾经遇到过类似的场景, 也排查了好久, 让我仔细检查下是不是<code>xcasset</code>捣的鬼。</p>
<p>笔者基于<strong>bob133</strong>的提示, 想到是否真的<code>xcasset</code>有问题。笔者通过<strong>XCode</strong>全局搜索了项目里的<strong>xcasset</strong>, 并没有找到错误的那张显示图片。直到这个时候, 笔者才想到要把加密的<code>Assets.car</code>文件提取出来看看。</p>
<p><img src="http://blog.startry.com/img/blog_asset_car_mogujie.png" alt="ThemeEngine Demo"></p>
<p>图片示例提取的是国内知名女性购物平台某某街的App, 可以从上图看出该App的图片使用也存在非常不规范的地方, 同一名字的图片被打入了这么多张。设想一下, 假如在这里写下述代码, 取到的究竟是上图中四张的哪张呢? =。=</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UIImage *image = [UIImage imageNamed:@&#34;address_icon_location&#34;];</span><br></pre></td></tr></table></figure>
<p>Assets.car的提取工具很多, 笔者使用的是<a href="https://github.com/alexzielenski/ThemeEngine" target="_blank" rel="external">ThemeEngine</a>。通过<a href="https://github.com/alexzielenski/ThemeEngine" target="_blank" rel="external">ThemeEngine</a>提取的<code>Assets.car</code>文件中<font color="orange">果然找到错误的图片</font>! 原来UIImage读取错误图片的根源是在这里啊!</p>
<p>总之, 打包后读取的问题图片已经找到了, 藏在二进制文件<code>Assets.car</code>中。</p>
<h4 id="幽灵图片从哪里来">幽灵图片从哪里来</h4><p>在打包生产的<code>Assets.car</code>竟然会出现错误的图片, 那一定还是工程目录下打包进去的。那究竟是什么地方打包进去的呢? </p>
<p>笔者首先想到的突破点是打包编译过程的<code>Copy Pods Resources</code>过程, 通过编译选项笔者发现有一个物理目录下的<code>Example</code>里的<code>XXX.assets</code>被打包进入了最终的<code>Asset.car</code>。</p>
<p>笔者尝试删除该目录下的<code>Example</code>工程, 果然编译出来的App可以读取到了<strong>正确</strong>的图片。</p>
<p>问题根源已经找到了, 笔者查看<code>Copy Pods Resouces</code>下的核心脚本<code>Pods_resources.sh</code>, 发现一段很牛B的代码段:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Find all other xcassets (this unfortunately includes those of path pods and other targets).</span></span><br><span class="line">OTHER_XCASSETS=$(find <span class="string">"<span class="variable">$PWD</span>"</span> -iname <span class="string">"*.xcassets"</span> -type d)</span><br><span class="line"><span class="keyword">while</span> <span class="built_in">read</span> line; <span class="keyword">do</span></span><br><span class="line"><span class="keyword">if</span> [[ <span class="variable">$line</span> != <span class="string">"`realpath <span class="variable">$PODS_ROOT</span>`*"</span> ]]; <span class="keyword">then</span></span><br><span class="line"> XCASSET_FILES+=(<span class="string">"<span class="variable">$line</span>"</span>)</span><br><span class="line"><span class="keyword">fi</span></span><br><span class="line"><span class="keyword">done</span> &lt;&lt;&lt;<span class="string">"<span class="variable">$OTHER_XCASSETS</span>"</span></span><br></pre></td></tr></table></figure>
<p>我去啊。。。怎么会有这样的代码段, 而且从0.35的CocoaPods版本开始就早已存在。笔者当时使用的<a href="https://github.com/CocoaPods/CocoaPods/tree/0.39-stable" target="_blank" rel="external">0.39.stable</a>的CocoaPods版本。关于这个问题, 笔者顺藤摸瓜, 找到了一个相关的<a href="https://github.com/CocoaPods/CocoaPods/issues/1546" target="_blank" rel="external">CocoaPods issue - Pods copy resource script overrides default xcasset bahaviour</a>。</p>
<p>这个资源覆盖的issue截止笔者发文之前依旧还open着。笔者先回归正题, 为什么笔者的代码在线上跑了几个月后会突然出问题了呢? 关键代码在这里:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> [[ -n <span class="string">"<span class="variable">$&#123;WRAPPER_EXTENSION&#125;</span>"</span> ]] &amp;&amp; [ <span class="string">"`xcrun --find actool`"</span> ] &amp;&amp; [ -n <span class="string">"<span class="variable">$XCASSET_FILES</span>"</span> ]</span><br><span class="line">...</span><br><span class="line"><span class="keyword">fi</span></span><br></pre></td></tr></table></figure>
<p>上述的Copy脚本执行条件是满足这个if语句, 这个条件语句有三个条件:</p>
<ol>
<li>有WRAPPER_EXTENSION, pod库依赖的资源文件默认都是<code>bundle</code></li>
<li>xcode命令行支持actool, actool是用来合并xcasset的官方工具</li>
<li>有添加过任意一个<code>xcasset</code>相关的文件</li>
</ol>
<p>条件1和条件2一直都没有改变过, 那么客观条件只有第3条有改变过的可能, 追朔代码:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="title">install_resource</span></span>()</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">case</span> <span class="variable">$1</span> <span class="keyword">in</span></span><br><span class="line"> ...</span><br><span class="line"> *.xcassets)</span><br><span class="line"> ABSOLUTE_XCASSET_FILE=$(realpath <span class="string">"<span class="variable">$&#123;PODS_ROOT&#125;</span>/<span class="variable">$1</span>"</span>)</span><br><span class="line"> XCASSET_FILES+=(<span class="string">"<span class="variable">$ABSOLUTE_XCASSET_FILE</span>"</span>)</span><br><span class="line"> ;;</span><br><span class="line"> /*)</span><br><span class="line"> ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>原来如此, 笔者所用的工程里依赖的Pod库里只要有任意一个Pod库被添加过一次<code>xcasset</code>文件, 则会触发这个全资源拷贝的脚本语句。这也是为啥之前工程没事, 好端端突然就出问题的原因。</p>
<p>防止大家误解, 这里条件3的添加<code>xcasset</code>需要通过引用库的podspec指定添加, 添加后通过主工程<code>pod_install</code>或<code>pod_update</code>生产的脚本引入产生。</p>
<p>示例语句(写在podspec中):</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">s.resource = <span class="string">'DemoLib/Pod/AnyName.xcassets'</span></span><br></pre></td></tr></table></figure>
<p><strong>总而言之, CocoaPods判断如果任意的Pod库里通过描述文件引入了xcasset文件, 就会触发根目录下所有xcasset文件扫描打包car的执行操作。</strong></p>
<h4 id="解决方案">解决方案</h4><p>针对该问题的解决方案有很多, 熟悉了CocoaPods的特性后怎么样都可以解决这个问题:</p>
<p><strong>方法一:</strong> 删除所有物理目录下多余的xcasset, 本身在源代码根目录下放置没有用到库本身就是非常危险的行为。</p>
<p><strong>方法二:</strong> 通过Podfile Hook去屏蔽Pod库资源的Copy和合成, 替换核心脚本, 定向指定自己需要Copy的资源。</p>
<p><strong>方法三:</strong> 逃避的方法, 不要在Pod库中使用xcasset。本身CocoaPods的初衷并没有打算支持资源文件的, 后续演变成目前的形态。(不适用xcasset默认png压缩不会执行, 可能需要手动执行, 并且图片容易被提取)</p>
<h4 id="追根溯源">追根溯源</h4><p>作为一个极具盛名的开源库, 怎么可能会写这么大的一个BUG呢? 有因必有果, 有一个关键问题还是没有找出来, 为什么两年来没有人给这个问题提Pull Request呢?</p>
<p>笔者本着好奇之心去探索CocoaPods的相关issue和commit记录, 找到了一个关键提交节点:</p>
<blockquote>
<p>0.36.4 (2015-04-16)</p>
<p>Bug Fixes</p>
<p>Fixes various problems with Pods that use xcasset bundles. Pods that use xcassets can now be used with the pod :path option.</p>
<p><a href="https://github.com/kylef" target="_blank" rel="external">Kyle Fuller</a> <a href="https://github.com/CocoaPods/CocoaPods/issues/1549" target="_blank" rel="external">#1549</a> <a href="https://github.com/CocoaPods/CocoaPods/pull/3383" target="_blank" rel="external">#3384</a> <a href="https://github.com/CocoaPods/CocoaPods/pull/3358" target="_blank" rel="external">#3358</a></p>
</blockquote>
<p>该解决BUG对应的Merge issue是<a href="https://github.com/CocoaPods/CocoaPods/pull/3405" target="_blank" rel="external">#3405</a></p>
<p>通过该关键节点引申出了一个BUG Fix的<a href="https://github.com/CocoaPods/CocoaPods/commit/44cde6feb61360bc530d85ea52a418ffe023c7ed" target="_blank" rel="external">commit - Do not discard .xcassets from the main project</a>和<a href="https://github.com/CocoaPods/CocoaPods/pull/2212" target="_blank" rel="external">issue - Only include *.xcassets from Pods</a>。</p>
<p>从提交记录可以看出这两次提交分别是为了解决支持<code>:path</code>属性和打包<code>xcasset</code>时候遗漏了主工程的<code>xcasset</code>的问题。</p>
<p>原来这个暴力的拷贝脚本是用来<font color="orange">将主工程的<code>xcasset</code>和Pod的<code>xcasset</code>一起利用actool合成car用的</font>。因为主工程的<code>xcasset</code>命名不规律和文件存储位置的不规律, 和actool的特性有限。CocoaPods的研发者暂时也没有更好的办法, 所以采用这种暴力的方式!</p>
<font color="orange">广大的网友如果有更好的方法, 可以帮助CocoaPods开发者解决该问题。笔者想了半天, 没有想出什么靠谱的方法。</font>
<p>PS: 如果估计针对主工程的<code>xcasset</code>做标志位的话, 和直接利用hook去屏蔽一些对应的资源文件本质上是没有差距的, 因为都需要在主工程里做额外的操作。</p>
<h2 id="总结">总结</h2><p>UIImage加载重名图片本身就存在问题, 因为图片不应该重名出现在工程里。但是, 在大型App开发中, 因为参与人员流动和数量的问题, 就不可避免的会出现各种各样的复杂情况。本文将笔者遇到的资源图片错误加载梳理了一下, 因为对CocoaPods和<code>xcasset</code>共同使用的不了解, 导致了排查的困难。</p>
<p><strong>CocoaPods在Pod里引用了任意一个xcasset相关的文件后, 就会去根目录搜索所有的xcasset组合成为最终的car</strong>。CocoaPods设定这样脚本的原因是无法精确的将主工程下的<code>xcasset</code>寻找到, 只能采用暴力的方式去解决, 暂时也没有更好的解决方案!</p>
<p>PS: 本人技术水平有限, 如果有错误的地方, 请各位大大及时指出哈~~</p>
<h4 id="参考">参考</h4><ol>
<li><a href="https://developer.apple.com/library/prerelease/ios/documentation/Xcode/Reference/xcode_ref-Asset_Catalog_Format/index.html#//apple_ref/doc/uid/TP40015170-CH18-SW1" target="_blank" rel="external">Apple - Asset Catalog Format Reference</a></li>
<li><a href="http://stackoverflow.com/questions/36008210/uiimage-load-wrong-image-in-main-bundle" target="_blank" rel="external">Stackoverflow - UIImage load wrong image in main bundle</a></li>
<li><a href="https://forums.developer.apple.com/message/124162" target="_blank" rel="external">Apple Developer Forum</a></li>
<li><a href="https://github.com/alexzielenski/ThemeEngine" target="_blank" rel="external">Github - ThemeEngine</a></li>
<li><a href="https://github.com/CocoaPods/CocoaPods" target="_blank" rel="external">GitHub - CoocaPods</a></li>
</ol>
</content>
<summary type="html">
<p>本篇文章来自笔者工作中遇到一个难解的BUG - 在App中用<code>UIImage</code>的<code>imageNamed:</code>方法读取的图片始终是<strong>不正确</strong>的。</p>
<p><img src="http://blog.
</summary>
<category term="CocoaPods" scheme="http://startry.com/tags/CocoaPods/"/>
<category term="xcasset" scheme="http://startry.com/tags/xcasset/"/>
</entry>
<entry>
<title>我只是想要截个屏(续)</title>
<link href="http://startry.com/2016/02/26/Screenshots-WKWebView/"/>
<id>http://startry.com/2016/02/26/Screenshots-WKWebView/</id>
<published>2016-02-26T08:26:23.000Z</published>
<updated>2016-02-26T08:26:23.000Z</updated>
<content type="html"><p>上两天写了一篇《<a href="http://blog.startry.com/2016/02/24/Screenshots-With-SwViewCapture/" target="_blank" rel="external">我只是想要截个屏</a>》的博文, 来描述了在书写<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>中遇到的一些坎坷和解决方案。在《我只是想要截个屏》中并没有找到针对WKWebView的全内容截图的相对完美的解决方案, 只是用一种滚动的暴力的方式去截图然后组装临时解决。</p>
<p>本文主要在上篇文章中做一些粗略的补充, 来描述SwViewCapture中是怎么更好的解决<a href="https://developer.apple.com/library/prerelease/ios/documentation/WebKit/Reference/WKWebView_Ref/" target="_blank" rel="external">WKWebView</a>的截屏问题, 还有怎么找到这种取巧的解决方案的~ </p>
<p>PS: 如果大家想直接看实现原理, 请跳过<strong>几次失败尝试</strong>章节~</p>
<h2 id="几次失败尝试">几次失败尝试</h2><p>阅读过《<a href="blog.startry.com/2016/02/24/Screenshots-With-SwViewCapture/">我只是想要截个屏</a>》的童鞋们可能都知道, 对于WKWebView的截图, 只能使用View的<code>drawViewHierarchyInRect:afterScreenUpdates:</code>方法去获取截图。在此前我曾尝试用如下几种方案去截图, 均以<strong>失败</strong>收尾。</p>
<ol>
<li>将WKWebView的frame拉长和ContentSize的高度保持一致, 然后截图</li>
<li>将WKWebView的frame拉长和ContentSize的高度一致, 然后通过WKWebView的<code>snapshotViewAfterScreenUpdates</code>获取的view进行截图</li>
<li>对WKWebView内部的WKContentView直接截图</li>
<li>将WKScrollView对应的Screen进行拉伸, 然后对WKWebView进行等价拉伸, 再截图</li>
<li>使用私有API<code>_snapshotRect:intoImageOfWidth:completionHandler</code></li>
</ol>
<p>上述第一、二、三种方法是笔者自己脑洞尝试, 可是截图要么完全是空白, 要么就只能显示屏幕区域的图。</p>
<p>第四种和第五种是对WKWebView源码不了解的窥看后, 进行一种投机取巧尝试。</p>
<p>既然已经实在找不到解决方案了, 笔者就去官网下载<a href="http://www.opensource.apple.com/source/WebKit2/WebKit2-7601.1.46.9/" target="_blank" rel="external">源码</a>, 希望能够找到突破口。WKWebView是开源的, 其源码放置在苹果官方开源网站<a href="opensource.apple.com">http://opensource.apple.com</a>中, 项目名字为WebKit2。</p>
<p>笔者以为下载到源码了, 至少能够找到一个突破口, 在打开工程项目后, 笔者就发现自己错了, 这个工程<strong>太庞大</strong>了。。。</p>
<p>WKWebView的组成笔者尚不熟悉, iOS的WKWebView底层更多的是WebKit的底层实现, 如果彻底从理解去阅读代码, 估计半个月甚至大半年都不一定读的完~ 有这个心思去阅读代码, 还不如先去阅读<a href="https://book.douban.com/subject/25910556/" target="_blank" rel="external">《WebKit技术内幕》</a>这本书~ </p>
<p>笔者自知不可能从阅读理解源码进行着手, 那就只能直奔要点: 关键字跟踪!笔者一开从<code>WKWebView.mm</code>文件进行突破, 去寻找遮盖关键字<code>unobscured</code>, 从这个关键字中<a href="http://www.opensource.apple.com/source/WebKit2/WebKit2-7601.1.46.9/UIProcess/API/Cocoa/WKWebView.mm" target="_blank" rel="external">发现遮盖区域和scrollView的window</a>相关, 因此尝试第四种方法, 修改window的大小~ 失败的结果唯一能够告诉笔者的就是: <strong>没有找到遮盖视图不渲染的根源!</strong></p>
<p>笔者在第一次寻找关键字失败后尝试从<code>snapshot</code>这个关键字去突破, 结果发现了私有API<code>_snapshotRect:intoImageOfWidth:completionHandler</code>。这也是第五种方法的尝试来源。通过<code>snapshot</code>关键字其实还发现了隐藏在<code>WKWebView.mm</code>底下的<a href="http://www.opensource.apple.com/source/WebKit2/WebKit2-7601.1.46.9/UIProcess/API/Cocoa/WKWebView.mm" target="_blank" rel="external">_takeViewSnapshot</a>方法, 可是该方法返回的对象是C++对象, 笔者就没有从Object-C层级对方法进行调用尝试。</p>
<p>结合<code>snapshot</code>和<code>unobscured</code>两个关键字的搜索, 笔者在底层一串跟踪, 发现了WebPage、DrawingArea等一系列概念, 笔者偶然间在WebPage的初始化方法中发现有个<code>WebPageCreationParameters</code>参数作为构造WebPage的初始参数, 其中包含了如下几个参数</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="preprocessor">#<span class="keyword">if</span> PLATFORM(IOS)</span></span><br><span class="line"> WebCore::FloatSize screenSize;</span><br><span class="line"> WebCore::FloatSize availableScreenSize;</span><br><span class="line"> <span class="keyword">float</span> textAutosizingWidth;</span><br><span class="line"><span class="preprocessor">#<span class="keyword">endif</span></span></span><br></pre></td></tr></table></figure>
<p>通过全局搜索<code>availableScreenSize</code>, 在<a href="http://www.opensource.apple.com/source/WebKit2/WebKit2-7601.1.46.9/UIProcess/ios/WebPageProxyIOS.mm" target="_blank" rel="external">WebPageProxyIOS.mm</a>源码中发现, WebPage的屏幕尺寸是根据<code>WKGetAvailableScreenSize()</code>和<code>WKGetScreenSize()</code>获取的, 核心代码如下:</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">FloatSize WebPageProxy::screenSize()</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">return</span> FloatSize(WKGetScreenSize());</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">FloatSize WebPageProxy::availableScreenSize()</span><br><span class="line">&#123;</span><br><span class="line"> <span class="keyword">return</span> FloatSize(WKGetAvailableScreenSize());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>终于有些眉目了, 一全局搜索<code>WKGetAvailableScreenSize</code>就<strong>崩溃</strong>了~ 在WebKit2开源中并没有这个方法的定义, 并且无法通过<code>Google</code>和<code>Apple Developer</code>搜索到相关信息… T_T</p>
<p><strong>最鸡血的来了, 跟踪了几个小时, 笔者放弃了…</strong> 没错, 笔者直到最后都没有从源码中找到解决方案~ =。=</p>
<h2 id="WKWebView截图方案">WKWebView截图方案</h2><p>虽然没有通过源码找到解决方案, 但是通过改变Window的尝试让我的脑洞打开, 想到了另外一种和滚动截图很相似的暴力的解决方式。</p>
<p>PS: 滚动截图是笔者在<a href="blog.startry.com/2016/02/24/Screenshots-With-SwViewCapture/">我只想要截个屏</a>中所描述的暴力解决截图的方式。实现方式就是滚动一页截取一页, 最后组装成一张长图。</p>
<p>笔者想: <em>既然WKWebView的渲染区域是屏幕范围固定的, 那我不滚动视图, 不断的往上推视图呢?</em> </p>
<p>不断往上推视图的意思就是改变View的origin的y轴, 每截取一张图片后去上移View的高度(高度等价于该WKWebView在界面中的显示范围)和拉长WKWebView的总高度, 直到截取到了最后一张图并组装。</p>
<p>这个思路有个小小的问题, 就是笔者曾经尝试通过放大WKWebView本身去截图, 但是却截出一片空白的情况。透过这个问题可以假设, 我不断上移y轴并放大高度的最后一张情况和上述有问题的情况完全一致, 可以猜测这个方法是无法正确的截取WKWebView的图的。</p>
<p>笔者用了一种很巧妙的方法去躲避了这个问题, 就是去截取WKWebView的父视图, 因为无论WKWebView怎么改变, 通过WKWebView父视图截图是可以正确获取对应的界面的(笔者实验的)。 </p>
<p>通过优化后大致的流程如下: </p>
<ol>
<li>基于WKWebView的尺寸伪造一个UIView, 并拉长至ContentSize高度</li>
<li>将伪造的UIView作为WKWebView的父视图</li>
<li>放置一张大画布长度和WKWebView的ContentSize高度一致</li>
<li>对父视图进行普通截图并放置在大画布中</li>
<li>将WKWebView的高度上移一个父视图的高度</li>
<li>循环执行步骤3和步骤4直到总高度和WKWebView的ContentSize高度一致</li>
<li>读取画布中的图像并返回</li>
</ol>
<p>大致思路如图:</p>
<p><img src="http://blog.startry.com/img/blog_swvc_wkwebview_solution.png" alt="WKWebView合成示意"></p>
<p>思路核心代码如下:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">let</span> containerView = <span class="type">UIView</span>(frame: <span class="keyword">self</span>.bounds)</span><br><span class="line"></span><br><span class="line"><span class="keyword">self</span>.removeFromSuperview()</span><br><span class="line">containerView.addSubview(<span class="keyword">self</span>)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> totalSize = <span class="keyword">self</span>.scrollView.contentSize</span><br><span class="line"><span class="keyword">let</span> page = floorf(<span class="type">Float</span>( totalSize.height / containerView.bounds.height))</span><br><span class="line"></span><br><span class="line"><span class="type">UIGraphicsBeginImageContextWithOptions</span>(totalSize, <span class="literal">false</span>, <span class="type">UIScreen</span>.mainScreen().scale)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> index <span class="keyword">in</span> <span class="number">0</span>...<span class="type">Int</span>(page) &#123;</span><br><span class="line"> <span class="comment">// async for, action need package a method</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> splitFrame = <span class="type">CGRectMake</span>(<span class="number">0</span>, <span class="type">CGFloat</span>(index) * containerView.frame.size.height, containerView.bounds.size.width, containerView.frame.size.height) </span><br><span class="line"> <span class="keyword">var</span> myFrame = <span class="keyword">self</span>.frame</span><br><span class="line"> myFrame.origin.y = -(<span class="type">CGFloat</span>(index) * containerView.frame.size.height)</span><br><span class="line"> <span class="keyword">self</span>.frame = myFrame </span><br><span class="line"> containerView.drawViewHierarchyInRect(splitFrame, afterScreenUpdates: <span class="literal">true</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> capturedImage = <span class="type">UIGraphicsGetImageFromCurrentImageContext</span>()</span><br><span class="line"><span class="type">UIGraphicsEndImageContext</span>()</span><br></pre></td></tr></table></figure>
<p>通过这种方式果然可以截取完整的WKWebView, 并且不存在<code>position: fixed;</code>的标签重复的问题。</p>
<font color="orange">上述代码起示意作用, 实际循环部分需要等待延迟, 因为需要等待WKWebView在改变frame之后准备完毕执行下一次循环。</font>
<h2 id="总结">总结</h2><p>因为WKWebView只能渲染屏幕范围大小左右的视图范围, 因此笔者就利用这个点, 不断的去改变WKWebView的frame去截图, 然后组装成为一张内容截图。通过这种方式可以巧妙的躲避过因为滚动视图产生的部分页面元素重复的问题。</p>
<p>其实WKWebView现在在iOS开发应用中并没有UIWebView广泛, 做截图相关功能的开发者也可能会优先采用UIWebView最为搭载容器, 但是多多少少本篇文章应该还是会帮助到一些使用WKWebView的先驱者的~</p>
<p>另: 本文提供的解决方案可能只是众多解决方案的其中一种, 并且相当的耗时也消耗内存, 希望大家可以一起想想能否有更优的解决方案~ 希望多多交流~ 如果有<strong>更好的方案</strong>, 跪求<strong>Pull Request</strong>或者提交<strong>issue</strong>到<a href="https://github.com/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>。</p>
<p>PS: 鉴于个人水平有限, 有错误之处, 请大家及时指出~ 谢谢~</p>
<h4 id="参考文献">参考文献</h4><ol>
<li><a href="http://www.opensource.apple.com/source/WebKit2/" target="_blank" rel="external">Apple Open Source - WebKit2</a></li>
<li><a href="https://developer.apple.com/library/prerelease/ios/documentation/WebKit/Reference/WKWebView_Ref/" target="_blank" rel="external">iOS Developer library - WKWebView</a></li>
<li><a href="blog.startry.com/2016/02/24/Screenshots-With-SwViewCapture/">我只是想要截个屏</a></li>
</ol>
</content>
<summary type="html">
<p>上两天写了一篇《<a href="http://blog.startry.com/2016/02/24/Screenshots-With-SwViewCapture/" target="_blank" rel="external">我只是想要截个屏</a>》的博文, 来描述
</summary>
<category term="Screenshots" scheme="http://startry.com/tags/Screenshots/"/>
<category term="WKWebView" scheme="http://startry.com/tags/WKWebView/"/>
<category term="iOS" scheme="http://startry.com/tags/iOS/"/>
</entry>
<entry>
<title>我只是想要截个屏</title>
<link href="http://startry.com/2016/02/24/Screenshots-With-SwViewCapture/"/>
<id>http://startry.com/2016/02/24/Screenshots-With-SwViewCapture/</id>
<published>2016-02-24T13:03:08.000Z</published>
<updated>2016-02-24T13:03:08.000Z</updated>
<content type="html"><p>想必使用iPhone的用户, 大家都知道按照Home键+电源键就可以截屏了。 截屏对于产品经理、工程师、设计师都比较重要。那么在iOS中用代码截屏也是再常用不过的功能了~ 那么在iOS研发中, 怎么样才能有效的截屏呢? 笔者在上周用了2天时间去写了一个Swift版本的截图开源库 - <a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>。</p>
<p>起初笔者有一个小小的想法, 怎么样去截取整个网页甚至整个滚动视图的内容呢? 造一个支持该功能的开源库会不会受欢迎呢? 基于CocoaChina+ App的分享思路以及笔者自己的一点小想法, 笔者决定写一个方便Swift开发者使用的截屏库, 支持截取页面载体所有内容的库。</p>
<ul>
<li>该想法的起源来自于<a href="http://weibo.com/u/1746852880" target="_blank" rel="external">@子循</a>的一个开源App - <a href="https://github.com/zixun/CocoaChinaPlus" target="_blank" rel="external">CocoaChina+</a>, 在该App中, 用户可以分享用户正在浏览的页面内容, 也就是WebView的内容。</li>
</ul>
<p>大家可能好奇, 就这么一个截屏, 需要写2天么? 一开始笔者的想法很简单: 无非就写一个截屏库。笔者真正实际写起来的时候, 才发现原来光光一个截屏也有这么多的坎等着我去踩。笔者代码截屏中遇到的困难在此处梳理了一下, 防止大家也重复采坑。</p>
<h2 id="简化截屏API">简化截屏API</h2><p>在刚开始写<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>的时候, 笔者想实现的简单点, 先实现基础的截屏功能, 可以将任意的View直接转化成一张UIImageView。</p>
<h4 id="截屏基础实现">截屏基础实现</h4><p>这个功能估计大部分iOS研发者都涉及到过, 在iOS7以前就一直用CoreGraphic的方式去Draw出对应的图。关键代码如下:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">UIGraphicsBeginImageContextWithOptions</span>(bounds.size, <span class="literal">false</span>, <span class="type">UIScreen</span>.mainScreen().scale)</span><br><span class="line"><span class="keyword">self</span>.layer.renderInContext(context!)</span><br><span class="line"><span class="keyword">let</span> capturedImage = <span class="type">UIGraphicsGetImageFromCurrentImageContext</span>()</span><br><span class="line"><span class="type">UIGraphicsEndImageContext</span>()</span><br></pre></td></tr></table></figure>
<p>上图代码的关键代码<code>renderInContext</code>是<code>CALayer</code>的方法, <code>CALayer</code>是CoreGraphic底层的图层, 组成UIView。UIGraphic等相关操作Context是Quartz 2D框架中的API, 而Quartz 2D是CoreGraphic的其中一个组成。</p>
<p>仅仅4行代码基本已经能够满足大部分的需求了~ 大部分是因为笔者在目前除了在WKWebView上此截图方法截图失败, 暂时还有在其他的View上截图失败, 有待继续检查。那么WKWebView又有什么问题呢?</p>
<h4 id="截屏遇上WKWebView">截屏遇上WKWebView</h4><p>笔者在写<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>的时候, 尝试去截取WKWebView的图。截图的结果返回给我的就仅仅只是一张背景图, 显然<strong>截图失败</strong>。通过搜索StackOverflow和Google, 我发现WKWebView并不能简单的使用<code>layer.renderInContext</code>的方法去绘制图形。</p>
<p>如果直接调用<code>layer.renderInContext</code>需要获取对应的Context, 但是在WKWebView中执行<code>UIGraphicsGetCurrentContext()</code>的返回结果是nil <em>(具体的原理暂时还不明, 待笔者知晓之后会补充)</em></p>
<p><a href="http://stackoverflow.com/questions/24727499/wkwebview-screenshots" target="_blank" rel="external">StackOverflow</a>提供了一种解决思路是使用<code>UIView</code>的<code>drawViewHierarchyInRect</code>方法去截取屏幕视图。</p>
<p>通过直接调用WKWebView的<code>drawViewHierarchyInRect</code>方法(<code>afterScreenUpdates</code>参数必须为<code>true</code>), 可以成功的截取WKWebView的屏幕内容。</p>
<h4 id="页面嵌套的问题">页面嵌套的问题</h4><p>在查找资料设法解决WKWebView截屏问题的时候, 无意中搜索到Chrome开源项目<a href="https://www.chromium.org/" target="_blank" rel="external">chromium</a>的截屏源码<a href="https://chromium.googlesource.com/chromium/src.git/+/46.0.2478.0/ios/chrome/browser/snapshots/snapshot_manager.mm" target="_blank" rel="external">SnapshotManager</a>。笔者在阅读源码的时候发现自己漏考虑了一个大场景:</p>
<ul>
<li>基础的UIView包含WKWebView场景下的截屏</li>
</ul>
<p>参考<a href="https://chromium.googlesource.com/chromium/src.git/+/46.0.2478.0/ios/chrome/browser/snapshots/snapshot_manager.mm" target="_blank" rel="external">SnapshotManager</a>中的解决方案, 定义一个递归函数去判断是否包含了WKWebView:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="func"><span class="keyword">func</span> <span class="title">swContainsWKWebView</span><span class="params">()</span></span> -&gt; <span class="type">Bool</span> &#123;</span><br><span class="line"> <span class="keyword">if</span> <span class="keyword">self</span>.isKindOfClass(<span class="type">WKWebView</span>) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">for</span> subView <span class="keyword">in</span> <span class="keyword">self</span>.subviews &#123;</span><br><span class="line"> <span class="keyword">if</span> (subView.swContainsWKWebView()) &#123;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</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">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><strong>最终普通截屏</strong>的方案为:</p>
<ul>
<li>view中任意一个子View包含WKWebView, 则采用<code>drawViewHierarchyInRect</code>的方式去截取视图</li>
<li>view中任意一个子View都不包含WKWebView, 则采用<code>renderInContext</code>的方式去截图</li>
</ul>
<p>大家可能好奇为啥不全部采用<code>drawViewHierarchyInRect</code>的方式好了, 还多此一举来个判断, 引用chromium源码<a href="https://chromium.googlesource.com/chromium/src.git/+/46.0.2478.0/ios/chrome/browser/snapshots/snapshot_manager.mm" target="_blank" rel="external">SnapshotManager</a>中的注释来解释为什么</p>
<blockquote>
<p> -drawViewHierarchyInRect:afterScreenUpdates:YES is buggy as of iOS 8.3.</p>
<p> Using it afterScreenUpdates:YES creates unexpected GPU glitches, screen</p>
<p> redraws during animations, broken pinch to dismiss on tablet, etc. For now</p>
<p> only using this with WKWebView, which depends on -drawViewHierarchyInRect.</p>
<p> TODO(justincohen): Remove this (and always use drawViewHierarchyInRect)</p>
<p> once the iOS 8 bugs have been fixed.</p>
</blockquote>
<p>PS: 写iOS的这些年来, 多多少少已经碰到不少的iOS系统原生BUG, 也是醉了</p>
<p>截止到笔者写本篇博客的时候, <a href="https://www.chromium.org/" target="_blank" rel="external">chromium</a>项目master上仍旧还存在该段注释。</p>
<p>笔者将上述基础截屏功能封装了一下, 在<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>库中, 仅仅需要一行代码即可实现截图功能:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">view.swCapture &#123; (capturedImage) -&gt; <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> <span class="comment">// Do something with capturedImage(UIImage)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="截取内容实现">截取内容实现</h2><p>普通截屏实现了, 那么就开始想怎么去实现全内容的截屏。开发一个复杂的功能, 第一步就是先把功能简单化实现, 那么笔者一开始就拿<code>UIWebView</code>作为实验对象去实现内容的截取功能。那么问题来了, 怎么实现呢? </p>
<p>通过打印UIWebView内部的UIScrollView的尺寸, 可以初步了解到UIWebView的内容本质上其实是承载在内部的UIScrollView中的。那么一个简单的耗内存的实现思路就冒出来了:</p>
<blockquote>
<p>将UIWebView的长宽修改为UIScrollView的内容尺寸大小, 然后将UIWebView用普通截图的方式截取出来。</p>
</blockquote>
<p>基于上述这个简单的想法, 笔者立马想到是否可以<strong>直接对UIWebView内部的UIScrollView进行长宽修改操作并截屏</strong>, 如果可行的话, 则可以直接引申使用在UITableView以及基础的UIScrollView上了。</p>
<p>基本实现代码如下:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">UIGraphicsBeginImageContext</span>(scrollView.contentSize)</span><br><span class="line"><span class="keyword">let</span> savedContentOffset = scrollView.contentOffset</span><br><span class="line"><span class="keyword">let</span> savedFrame = scrollView.frame</span><br><span class="line"></span><br><span class="line">scrollView.contentOffset = <span class="type">CGPointZero</span></span><br><span class="line">scrollView.frame = <span class="type">CGRectMake</span>(<span class="number">0</span>, <span class="number">0</span>, scrollView.contentSize.width, scrollView.contentSize.height)</span><br><span class="line"></span><br><span class="line">scrollView.layer.renderInContext(<span class="type">UIGraphicsGetCurrentContext</span>()!)</span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> image = <span class="type">UIGraphicsGetImageFromCurrentImageContext</span>();</span><br><span class="line"></span><br><span class="line">scrollView.contentOffset = savedContentOffset;</span><br><span class="line">scrollView.frame = savedFrame;</span><br><span class="line"></span><br><span class="line"><span class="type">UIGraphicsEndImageContext</span>()</span><br></pre></td></tr></table></figure>
<p>这个场景下, UIScrollView可以被正常的截图, 那么引申修改应用在UIWebView上应该不是什么难事吧? </p>
<p>PS: UIWebView有个get方法可以获取对应的UIScrollView</p>
<h4 id="iOS8的诡异BUG">iOS8的诡异BUG</h4><p>在基本的场景下, 该方法都可以正常的截取UIScrollView, 但是在iOS8环境下会出现尾部可视区域为黑色的异常BUG。(这个BUG可能和UIScrollView在被UINavigationController托管下的VC下产生的)</p>
<p>通过一阵搜索Stackoverflow和CocoaChina+的源码提示, 有一个比较合适的<a href="http://stackoverflow.com/questions/3539717/getting-a-screenshot-of-a-uiscrollview-including-offscreen-parts/3539944#28343200" target="_blank" rel="external">解决方法</a>:</p>
<blockquote>
<p> add scrollview to another temp view and render it. </p>
</blockquote>
<p>把UIScrollView单独拎出来, 放在其他临时的UIView里单独渲染。通过该方法果然可以将iOS8的渲染问题给屏蔽掉。</p>
<p><img src="http://blog.startry.com/img/blog_swvc_bug_solution.png" alt="改进方案合成示意"></p>
<h4 id="异步渲染粗暴解决方案">异步渲染粗暴解决方案</h4><p>将之前所描述的截图应用到实际场景中, 笔者发现有些网页的元素是异步加载的, 即只有页面滚动到对应的部分, 才会执行渲染加载(笔者的博客首页主题就是这种场景)。另外, UIScrollView和UITableView中也不缺乏这种场景。</p>
<p>对于这种异步的方式, 没有一种完美的解决方案, 笔者只能解决一种暴力的方式解决部分案例:</p>
<ul>
<li>截屏前滚动ScrollView至底部, 再滚动回首部</li>
</ul>
<h4 id="截图闪屏问题">截图闪屏问题</h4><p>通过<a href="http://stackoverflow.com/questions/3539717/getting-a-screenshot-of-a-uiscrollview-including-offscreen-parts/3539944#28343200" target="_blank" rel="external">Stackovetflow - Getting a Screenshot of a UIScrollView including offscreen parts</a>中的方式修复了<code>iOS8的截图多个黑色区域的诡异BUG</code>后, 在实际截图中发现了另外一个问题:</p>
<ul>
<li>将屏幕中正在显示的View拎出来放置在其他View中渲染, 渲染完毕后再恢复, 可能会出现一闪而过的情况。 </li>
</ul>
<p>上述现象产生的原因大家估计都知道: <strong>因为View离开当前视图的时候, 触发了界面渲染, 显示界面中的视图已经不在显示界面中, 自然就变成了背景色。</strong></p>
<p>既然要做一个截屏库, 那么这个问题也是需要解决的, 总不能让人家调用截屏API的时候闪一下吧?</p>
<p>笔者思考了一下, <font color="orange">决定引入一张当前view的截图并遮盖在此view的父view上, 让大家视觉产生一种幻觉, 来掩盖真正的视图的操作。</font></p>
<p>基于iOS7中View提供的API - <code>snapshotViewAfterScreenUpdates</code>, 可以直接生产一个截屏视图View, 剩下的工作如下:</p>
<ol>
<li>通过<code>snapshotViewAfterScreenUpdates</code>产生假的遮盖View</li>
<li>选择目标视图的parentView, 和目标视图在parentView的层级</li>
<li>将遮盖view添加到目标视图的parentView中的相同层级中</li>
<li>执行真正的截图逻辑</li>
<li>将假的遮盖View从视图中移除</li>
</ol>
<p><img src="http://blog.startry.com/img/blog_swvc_fake_cover.png" alt="伪图遮盖示意"></p>
<p>通过上述方法, 截图闪屏的问题就完美解决了~ </p>
<h4 id="setFrame破坏">setFrame破坏</h4><p>大家注意到在执行全内容截屏的时候, 会动态的去修改UIScrollView的frame, 然后执行相应的逻辑内容。在执行截图逻辑功能的时候, 往往会涉及异步的操作, 那么在下述场景下截图可能会出现异常:</p>
<ol>
<li>用户在对应的<code>layoutSubView</code>中设置修改了需要截图的view的<code>frame</code>。</li>
<li>用户在截图过程中对需要截图的view的<code>frame</code>进行了操作</li>
</ol>
<p>其实场景2包含了场景1, 总结下就是<strong>在截图过程中, 任意的frame操作都会对截图行为造成破坏, 但是frame操作可能是由layoutSubView等系统函数触发的</strong></p>
<p>既然出现这个问题, 那么笔者就要解决这个问题, 笔者能够想到的就是<strong>在截图过程中无效化对该View任意的frame操作</strong></p>
<p>既然已经想到了解决方案, 那么就设计代码实现。笔者一开始尝试的方法是<strong>通过运行时对UIView绑定一个<code>isCapturing</code>的属性, 然后override目标视图的<code>frame</code>的<code>set</code>和<code>get</code>方法, 在<code>set</code>方法中通过判断是否截图中去实现是否调用super的frame操作</strong>。但是在实际操作中发现, 目标视图的<code>frame</code>中<code>set</code>和<code>get</code>的操作并单纯的做了读取和写入的操作, 还有系统的对该视图的操作逻辑存在, 因此<font color="red">不能通过该方式去禁用frame</font>。</p>
<p>在override frame的方案失败后, 笔者又尝试了下述方案:</p>
<ul>
<li>利用<a href="https://developer.apple.com/library/ios/documentation/Cocoa/Reference/ObjCRuntimeRef/" target="_blank" rel="external">Runtime</a>在截图前替换目标view的setFrame方法, 然后在截图结束后, 用运行时将其复原</li>
</ul>
<p>技术实现如下:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> method: <span class="type">Method</span> = class_getInstanceMethod(object_getClass(<span class="keyword">self</span>), <span class="type">Selector</span>(<span class="string">"setFrame:"</span>))</span><br><span class="line"> <span class="keyword">let</span> swizzledMethod: <span class="type">Method</span> = class_getInstanceMethod(object_getClass(<span class="keyword">self</span>), <span class="type">Selector</span>(<span class="string">"swSetFrame:"</span>))</span><br><span class="line"> method_exchangeImplementations(method, swizzledMethod)</span><br><span class="line"><span class="comment">// capturing</span></span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="comment">// capturing</span></span><br><span class="line">method_exchangeImplementations(swizzledMethod, method)</span><br></pre></td></tr></table></figure>
<p>PS: <code>swSetFrame</code>方法是一个空方法, 用来无效化设置frame</p>
<p>这里抛给大家一个问题: 约束是否也有相同的问题呢? </p>
<p><em>关于SwViewCapture库对约束的处理是笔者将来的优化点之一</em></p>
<h4 id="截取内容又遇见WKWebView">截取内容又遇见WKWebView</h4><p>基础的截图功能和BUG都已经解决的差不多了, 那么来试试最麻烦的WKWebView吧。经过简单测试, 通过前面封装UIScrollView的截图方式果然<font color="red">不能对WKWebView进行全内容截图</font>。</p>
<p>笔者好奇WKWebView的组成结构, 就通过扫描subview的方式打印了WKWebView:</p>
<p>打印方式:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">for</span> subView <span class="keyword">in</span> (webView?.subviews)! &#123;</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"v name: <span class="subst">\(subView.<span class="keyword">dynamicType</span>)</span>"</span>)</span><br><span class="line"> <span class="keyword">if</span> <span class="type">String</span>(subView.<span class="keyword">dynamicType</span>) == <span class="string">"WKScrollView"</span> &#123;</span><br><span class="line"> <span class="comment">// Do something</span></span><br><span class="line"> &#125;</span><br><span class="line"> <span class="keyword">for</span> subSubView <span class="keyword">in</span> subView.subviews &#123;</span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"sub name: <span class="subst">\(subSubView.<span class="keyword">dynamicType</span>)</span>"</span>)</span><br><span class="line"> <span class="keyword">if</span> <span class="type">String</span>(subSubView.<span class="keyword">dynamicType</span>) == <span class="string">"WKContentView"</span> &#123;</span><br><span class="line"> <span class="comment">// Do something</span></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>打印结果:</p>
<ul>
<li>WKWebView<ul>
<li>WKScrollView<ul>
<li>WKContentView</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><code>WKScrollView</code>的基类是<code>UIWebScrollView</code>, <code>UIWebScrollView</code>的基类是<code>UIScrollView</code>, 并且WKContentView下包含了和实际网页内容一样的宽和高。那么是否可以通过获取WKWebView下面的<code>WKScrollView</code>进行截图呢? </p>
<p>笔者开心的以为找到了突破口, 赶紧尝试截图。结果无论是对WKWebView本身截图或者对其任意一个子类截图, 结果截图的结果仍旧还是空白一片。</p>
<p>在上述两个方式都失败的无奈情况下, 笔者暂时没有特别好的解决方案, 绝对先临时采用暴力的渲染方式去合成一张大截图:</p>
<ul>
<li>截取屏幕显示范围大小的视图, 滚动, 按页截图, 滚动, 按页截图, 循环操作直到滚动到最后一页, 最后将所有截取的图片合成为一张大图。</li>
</ul>
<p><img src="http://blog.startry.com/img/blog_swvc_wkwebview.png" alt="截图合成示意"></p>
<p>通过上述截图方案截取的图片仍存在<font color="red">不完美</font>的部分, 尤其针对标签为<code>position: fixed;</code>的<code>div</code>元素。标签为<code>position: fixed;</code>的<code>div</code>元素会循环出现在生产的大图上。</p>
<p>针对这种场景, <strong>笔者暂时不计划处理</strong>, 因为WKWebView的这种截图方式暂时还不是笔者理想的截图方式, 需要急用的童鞋们可以暂时使用业务逻辑方式去处理:</p>
<ol>
<li>扫描<code>position: fixed;</code>的元素</li>
<li>计算<code>postion: fixed;</code> 的元素距离顶部和尾部的高度</li>
<li>根据顶部和尾部的距离分别进行对应的显示和隐藏操作</li>
</ol>
<font color="orange">关于WKWebView截图, 如果大伙计大家谁有更好一些的截图方案, 请告知, 相互学习提高; 本人也会不断尝试新的方式, 看看能否和UIWebView一样完美的截取WKWebView; 笔者认为WKWebView绝对是有办法完美截取所有内容的, 只是暂时没有找到</font>
<p>基于前面内容截图的实现描述, 笔者将API简化封装, 只需要使用<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>的时候, 调用如下代码即可使用:</p>
<figure class="highlight swift"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">view.swContentCapture &#123; (capturedImage) -&gt; <span class="type">Void</span> <span class="keyword">in</span></span><br><span class="line"> <span class="comment">// Do something with capturedImage(UIImage)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="关于性能">关于性能</h2><h4 id="绘图性能问题">绘图性能问题</h4><p>编写客户端程序的大家可能都知道, 只有主线程可以操作视图。因为iOS和Android系统在底下做了保护, 非主线程操作视图的行为都可能产生不可预计的后果, 因此系统发现在非主线程进行视图操作的时候, 往往会主动抛出异常。CoreGraphic(draw)的方式绘制视图是可以支持多线程操作, 绘制过程也不会被视图展现出来。</p>
<p>在写<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>的时候, 我曾尝试着用<strong>异步(其他线程)</strong>的方式去操作绘图过程, 在<code>renderInContext</code>和<code>drawViewHierarchyInRect</code>下均作过尝试。</p>
<ul>
<li><code>renderInContext</code>对异步绘制支持的挺好, 并没有出现截图失败以及系统闪退的情况。</li>
<li><code>drawViewHierarchyInRect</code>对多线程并不能友好支持, 可能因为<code>drawViewHierarchyInRect</code>是UIView的方法的缘故, 在GCD异步线程中用<code>drawViewHierarchyInRect</code>绘制的图像会出现绘制丢失和失败的情况。</li>
</ul>
<p>鉴于<code>drawViewHierarchyInRect</code>方法对线程支持的友好性不够, 笔者在第一版本的<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>中并没有考虑去使用多线程的方式去优化性能, 是需要进一步改进和尝试的地方。</p>
<h4 id="内存过大问题">内存过大问题</h4><p>笔者在第一版本的<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>中没有去考虑内存问题, 因此如果非常长的UIScrollView去截取图片的时候可能会出现卡顿甚至闪退的现象。 </p>
<p>笔者认为在截图之前, 用户自己应该大概知道自己要截图的视图的大小, 需要进行一定的预先处理。笔者计划在以后引入区域块截图的方式让用户自定义的去控制截取视图的大小, 这样来规避可能存在的截图导致内存过大的问题。</p>
<h2 id="总结">总结</h2><p>笔者一开始只是想实现一个截取网页内容的一个小功能, 在实现过程中发现截图竟然也可以踩出这么的坑, 便将采坑的过程总结成这篇文章, 供大家参考。同时, 将采坑实现的产物以Swift库<a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a>的形式开源在Github上。</p>
<p>笔者将题目取为<code>我只是想截个屏</code>, 是一种自嘲的方式来描述程序员实现一个功能的艰辛啊~</p>
<p>PS: 本人水平有限, 有错误之处请大家及时指出~~ 如果觉得文章有用, 可以<a href="http://blog.startry.com" target="_blank" rel="external">多多来访</a>哇~</p>
<h4 id="参考链接">参考链接</h4><ol>
<li><a href="http://stackoverflow.com/questions/24727499/wkwebview-screenshots" target="_blank" rel="external">StackOverflow - WKWebView Screenshots</a></li>
<li><a href="http://stackoverflow.com/questions/3539717/getting-a-screenshot-of-a-uiscrollview-including-offscreen-parts" target="_blank" rel="external">StackOvetflow - Getting a Screenshot of a UIScrollView including offscreen parts</a></li>
<li><a href="https://github.com/startry/SwViewCapture" target="_blank" rel="external">SwViewCapture</a></li>
<li><a href="https://github.com/zixun/CocoaChinaPlus" target="_blank" rel="external">CocoaChina+</a></li>
<li><a href="https://chromium.googlesource.com/chromium/src.git/+/46.0.2478.0/ios/chrome/browser/snapshots/snapshot_manager.mm" target="_blank" rel="external">chromium - SnapshotManager</a></li>
<li><a href="https://developer.apple.com/library/ios/documentation/Cocoa/Reference/ObjCRuntimeRef/" target="_blank" rel="external">Object-C Runtime Reference</a></li>
</ol>
</content>
<summary type="html">
<p>想必使用iPhone的用户, 大家都知道按照Home键+电源键就可以截屏了。 截屏对于产品经理、工程师、设计师都比较重要。那么在iOS中用代码截屏也是再常用不过的功能了~ 那么在iOS研发中, 怎么样才能有效的截屏呢? 笔者在上周用了2天时间去写了一个Swift版本的截图开
</summary>
<category term="Screenshots" scheme="http://startry.com/tags/Screenshots/"/>
<category term="iOS" scheme="http://startry.com/tags/iOS/"/>
</entry>
<entry>
<title>iOS界面跳转的一些优化方案</title>
<link href="http://startry.com/2016/02/14/Think-Of-UIViewController-Switch/"/>
<id>http://startry.com/2016/02/14/Think-Of-UIViewController-Switch/</id>
<published>2016-02-14T07:39:11.000Z</published>
<updated>2016-02-14T07:39:11.000Z</updated>
<content type="html"><p>App应用程序开发, 界面跳转是基础中的基础, 几乎没有一个App是用不到界面跳转的, 那么怎么样去书写界面跳转代码才是比较合理的呢?</p>
<p>大家可能在想跳转无非就2种方式, 能有什么内容? 其实并不是这样子的, 对于研发老手来说, 大型应用几乎都是利用URLScheme进行全方位的解决方案; 对于研发新手来说, 他们可能并没有遇到多路口界面跳转的瓶颈, 只会使用一些常用跳转, 并不会意识到界面跳转潜在的一些问题, 甚至无法严格区分Present和Push的操作区别~</p>
<p>本文将针对界面跳转提出一些优化解决方案~</p>
<h3 id="常用跳转方式">常用跳转方式</h3><p>iOS常用的跳转方式只有两种<a href="https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/doc/uid/TP40006926-CH3-SW96" target="_blank" rel="external">Present</a>和<a href="https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UINavigationController_Class/index.html#//apple_ref/doc/uid/TP40006934-CH3-SW13" target="_blank" rel="external">Push</a>。Push和Present最直观的区别是默认的转场效果, Present的默认转场效果是自下而上的, Push的转场效果是自右到左的。Push往往需要搭配<a href="https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UINavigationController_Class/index.html" target="_blank" rel="external">UINavigationController</a>来使用。</p>
<p>Push跳转使用示意:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UIViewController *nextViewController = [[UIViewController alloc] init];&#10;nextViewController.title = @&#34;&#31532;&#20108;&#20010;&#30028;&#38754;&#34;;&#10;&#10;[self.navigationController pushViewController:nextViewController animated:YES];</span><br></pre></td></tr></table></figure>
<p>Present跳转使用示意:</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UIViewController *nextViewController = [[UIViewController alloc] init];&#10;nextViewController.title = @&#34;&#31532;&#20108;&#20010;&#30028;&#38754;&#34;;&#10;&#10;[self presentViewController:nextViewContrller animated:YES completed:nil];</span><br></pre></td></tr></table></figure>
<p>在大部分情况下, iOS研发者都在滥用Push的跳转方式, 往往一个App仅仅包含一个UINavigationController。产生滥用的原因是因为Present的跳转太过难于定制转场效果, 仅仅只能使用系统提供的4种打开方式。</p>
<p>在满足产品要求的前提下, 其实可以基于<strong>模块内跳转</strong>和<strong>模块外跳转</strong>的思量去考虑采用Present的方式还是Push的方式去跳转界面。</p>
<p>PS: 还有一种界面切换方式是利用<a href="https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/doc/uid/TP40006926-CH3-SW86" target="_blank" rel="external">ChildViewController</a>, 进行独立界面的控制。需要高度定制的界面可以采用这种方式, 例如基于地图或者相机的应用。基于ChildViewController的方式内容篇幅太长, 在本文就暂时不介绍了, 以后再补充~</p>
<h3 id="常用跳转方式瓶颈">常用跳转方式瓶颈</h3><p>常用的跳转方式其实已经几乎可以满足我们所有的跳转, 但是<strong>欠缺一层业务层次的高层级封装</strong>。</p>
<p>举一个实际使用场景, 某App支持4种页面打开方式:</p>
<ol>
<li>Push推送</li>
<li>App外部网页打开</li>
<li>App内部网页打开</li>
<li>应用内点击打开</li>
</ol>
<p><img src="http://blog.startry.com/img/blog_sheme_directly.png" alt="多入口示意"></p>
<p>这四种方式均跳转到DetailViewController界面。普通的跳转依然可以满足该场景, 最简单的解决方案是在四个不同的地方都写一个独立的界面打开逻辑。</p>
<p>作为一名业务模块人才, 如此不复用代码合理么? 作为一名App架构师, 如此冗余四份入口代码合适么? </p>
<p>如果您觉得不合适, 那自然需要去抽离代码到统一的地方去书写一套统一的管理逻辑; 如果您觉得合适, 那么您会设想什么方案去根据外部网页以及Push内容去转化代码至普通跳转代码呢?</p>
<h3 id="URLScheme解决方案">URLScheme解决方案</h3><p>我们先根据前面提到的四种场景进行场景分析:</p>
<ul>
<li>外部网页场景:<ul>
<li>只能使用iOS自带的URLScheme的方式去打开App, 然后通过AppDelegate中事件去获取对应的URL进行匹配</li>
</ul>
</li>
<li>Push推送场景:<ul>
<li>在extra字段中定义个链接字段, 链接字段是个字符串或者数字代号, 用于在AppDelegate中事件去获取对应的代号进行匹配; 既然代码是自定义的, 那自然可以定义成一个URL了</li>
</ul>
</li>
<li>内部网页场景<ul>
<li>熟悉iOS WebView开发的童鞋们都知道, UIWebView的JS交互本质上是通过截获URL请求去实现的, 那么既然是传递URL地址, 就可以和外部网页使用相关的方式, 只不过在不同的位置进行对应的URL匹配</li>
</ul>
</li>
<li>应用内点击打开<ul>
<li>可以采用普通打开方式, 也可以通过一个抽离的URLScheme匹配器去匹配打开</li>
</ul>
</li>
</ul>
<p>根据上述四个场景, 我们可以发现, 解决上述四个应用场景, 我们需要的是引入一个抽离的URLScheme匹配器去匹配打开轮转界面~ </p>
<p>利用URLScheme的方式进行一层封装, 几乎可以完美解决多入口打开App的逻辑复用问题。</p>
<p>PS: 一般情况下, URLScheme的抽离器不需要自己封装, 可以使用开源现成的, 很少场景需要高度定制。</p>
<p>开源URLScheme解决方案:</p>
<ol>
<li><a href="https://github.com/clayallsopp/routable-ios" target="_blank" rel="external">Routable</a> Android和iOS均支持的一款权威的应用内URL跳转路由, 几乎可以满足所有需求<ul>
<li>代码切入性比较低, 没有冗余的继承封装。</li>
<li>可以指定NavigationController, 方便定制ChildController的跳转</li>
<li>可以多个Router组合使用, 灵活性高</li>
</ul>
</li>
<li><a href="https://github.com/gaosboy/urlmanager" target="_blank" rel="external">urlmananger</a> 国内技术问题网站<a href="https://segmentfault.com.com" target="_blank" rel="external">segmentfault.com</a>开发者抽离的一个跳转器<ul>
<li>需要嵌入继承和绑定使用NavigationController, 架构设计层级嵌入性很高, 没有Routable合理</li>
<li>无法多个组合使用, 灵活性没有Routable高</li>
<li>封装层次高, 快速使用可以采用</li>
</ul>
</li>
</ol>
<p>个人比较倾向使用Routable, 因为并没有在架构上对代码进行嵌入, 比较符合开发者口味~</p>
<p>以Routable作为示例, 本文可以通过如下代码在App启动的时候就提前注册(PS: Push点击打开执行Optional参数之前注册)</p>
<figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[[Routable sharedRouter] map:@&#34;detail/:id&#34; toController:[DetailController class]];</span><br></pre></td></tr></table></figure>
<p>假设您的App URLScheme前缀为<code>demo123</code>, 您只需要在上述四个场景分别传递<code>demo123://detail/88</code>过来即可。<code>88</code>只是示例的一个随意乱写的id编号, 作为Restful分格的参数进行处理。</p>
<p><img src="http://blog.startry.com/img/blog_scheme_routable.png" alt="Routable改造示意"></p>
<h3 id="URLScheme些许问题">URLScheme些许问题</h3><p>URLScheme进行界面跳转的解决方案也不是完美的, 个人开发时候遇到最大的问题就是传值问题</p>
<ol>
<li>怎么传递对象值<ul>
<li>URLScheme原则上不支持传递复杂的对象, 通过URLScheme方式打开的界面理论上每个界面都相对保持逻辑独立(逻辑独立的代价往往是牺牲细微的用户体验), 逻辑独立的界面可以有更好的架构设计</li>
<li>通过外部URL或者Push的方式是无法传递对象的, 可以不用考虑传递对象的场景</li>
<li>应用内界面跳转可以根据实际场景去区分使用URLScheme的方式还是普通的方式进行界面跳转控制</li>
</ul>
</li>
<li>怎么传递URL值<ul>
<li>URLScheme打开的界面有时候也需要传递URL值用于对应的界面, 最常见的是打开图片管理器以及打开WebView的界面, 这种场景可以采用约定加密的方式进行处理, 对传递的URL进行URIEncode和取值时候的URIDecode。</li>
</ul>
</li>
<li>Push长度限制<ul>
<li>坑爹的APNs规定了Push内容的总长度不能大于255字节, 那么URLScheme的参数传递就收到限制。</li>
<li>最常用的解决方案是压缩字段名字和内容, 传递的字段劲量用一个单词表示, 值字段可以隐藏掉URLScheme的前缀, 只保留后缀以及参数。</li>
<li>还有一种通过的Push长度解决方案是, push只是触发器, 触发App请求去获取真正的内容来绕过长度限制。</li>
</ul>
</li>
</ol>
<h4 id="传值对象">传值对象</h4><p>此处针对应用内跳转使用url scheme还是普通方式进行一些议论, 个人觉得一套应用里如果有两种方式跳转, 虽然灵活性高, 但是比较难以控制, 因此最好都采用一套方式跳转, 那自然是使用url scheme。那复杂传值的问题依旧无法得到解决。</p>
<p>有些开发者为了省时间, 直接通过类似Notification的方式或者用单例对象去维护进行值传递, 也不失为一个方法。</p>
<p>但是我在思考, 有没有一种相对完美的解决方案, 能够将传值问题彻底用url scheme进行传递呢?</p>
<p>结合本人喜欢使用的库<a href="https://github.com/icanzilb/JSONModel" target="_blank" rel="external">JSONModel</a>, 我想到了一种暴力且耗时的解决方案, 但是至少不产生耦合哈~</p>
<p>暴力解决方案步骤:</p>
<ol>
<li>JSON化对象</li>
<li>将对象JSON字符串Base64加密</li>
<li>将Base64加密后的字符串作为url参数传递</li>
<li>接受者处理参数的时候反Base64解密</li>
<li>将解密后的JSON对象用模型实例化</li>
</ol>
<p>针对暴力解决方案, 本人设想了<em>四个自问自答</em>:</p>
<p><strong>为什么不直接Base64对象而需要将其JSON字符串话呢?</strong></p>
<p>答: 为了接受者解密后的直观性。在大型App开发过程中, 解密者解析后不一定知道用哪个模型去实例化JSON字符串, 通过这种方式, 解密者可以不关心接受数据后的模型实例而自由发挥。</p>
<p><strong>利用这种方式暴力解决后, url会不会很长?</strong></p>
<p>答: 这种方式传递的url会超级长, 但是在应用内进行页面处理的场景, 不需要可以的去考虑url的长度。但是url的长度可能会影响解析的性能。</p>
<p><strong>为什么不直接通过广播或者单例维护的方式传值?</strong></p>
<p>答: 为了解耦。 大型App维护的时候, 如果一个内存对象是公用的, 是十分难以维护的, 应该尽量减少传递对象之间的耦合。</p>
<p><strong>为什么需要base64加密而不直接采用uriencode的方式?</strong></p>
<p>答: 为了解决模型嵌套的问题。因为一个模型里可能会嵌套另外一个模型, 当然通过JSON字符串本身可以实现模型嵌套的解决, 那可以有更加节省性能的解决方案。</p>
<p>本人水平有限, 此处的暴力的方式是目前本人觉得<strong>相对耦合度低并且比较好的一种解决方案</strong>。对于目前的iOS手机设备来说, 这点JSON以及解密的性能并不能影响整个App的运行, 因此采用这种方式进行暴力解决。</p>
<h3 id="总结">总结</h3><p>页面轮转在小型的App并不需要针对单独进行优化设计, 但是对于上20w行代码的大型App来说, 往往都是需要针对优化的。</p>
<p>本文引用了市面上最通用的URLScheme的解决方案来进行页面跳转的设计优化, 并建议采用开源库<a href="https://github.com/clayallsopp/routable-ios" target="_blank" rel="external">Routable</a>来进行统一管理。</p>
<p>根据个人在界面跳转开发中遇到的困难, 提出了几个相应的瓶颈和对应的解决方案。针对传递对象值提出了一种暴力但是耦合度比较低的解决方案。</p>
<p>PS: 本人水平有限, 有错误的地方还望大家及时指出~ 谢谢!</p>
<h4 id="参考文献">参考文献</h4><ol>
<li><a href="https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewController_Class/index.html" target="_blank" rel="external">Apple官方文档 - UIViewController</a></li>
<li><a href="https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UINavigationController_Class/index.html" target="_blank" rel="external">Apple官方文档 - UINavigationController</a></li>
<li><a href="https://developer.apple.com/library/ios/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html" target="_blank" rel="external">Apple官方文档 - URLScheme</a></li>
<li><a href="https://developer.apple.com/library/prerelease/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/Introduction.html" target="_blank" rel="external">Apple官方文档 - APNs</a></li>
</ol>
</content>
<summary type="html">
<p>App应用程序开发, 界面跳转是基础中的基础, 几乎没有一个App是用不到界面跳转的, 那么怎么样去书写界面跳转代码才是比较合理的呢?</p>
<p>大家可能在想跳转无非就2种方式, 能有什么内容? 其实并不是这样子的, 对于研发老手来说, 大型应用几乎都是利用URLSch
</summary>
<category term="iOS" scheme="http://startry.com/tags/iOS/"/>
</entry>
<entry>
<title>2015年&羊年总结</title>
<link href="http://startry.com/2016/02/05/2015_conclusion/"/>
<id>http://startry.com/2016/02/05/2015_conclusion/</id>
<published>2016-02-05T06:24:07.000Z</published>
<updated>2016-02-05T06:25:17.000Z</updated>
<content type="html"><p>既然决定要长期坚持写博客, 那么产出水帖也是必然的了~ 本文是个人今年的总结, 并没有啥技术含量哈~ 整年总结是以下四个字:</p>
<blockquote>
<p>知足常乐</p>
</blockquote>
<p>在某种程度上也算是自我安慰, 因为今年并没有实现重大的自我突破~ 在工作、家庭都没有取得什么实质性的进展, 但是也并没有过的不好~ </p>
<h4 id="工作">工作</h4><p>2015年更换了一份工作, 心有不舍的离开了一起成长的<a href="http://d.souche.com" target="_blank" rel="external">车牛</a>, 加入了滴滴快的代驾团队, 成为了滴滴大家庭的一只小桔子。。。</p>
<p>在快速的体验了一次从0到1的过程之后, 收获了很多成长(包括但是不限于技术), 但是在经济和家庭上却陷入了困境, 让我自己重新思考自己当前的阶段。在仔细斟酌和思考后定下来了一个大致的目标, 放弃了所有创业公司的机会, 目标加入一个相对稳定高速成长的公司。</p>
<p>今年的工作目标就是深耕, 磨练心智优先于拓展技能。在重新出发寻找工作的时候就给自己制定了目标: </p>
<blockquote>
<p>无论在公司的发展前景如何, 至少在公司深耕两年以上 </p>
</blockquote>
<p>2015年在工作上并没有取得实质性的突破, 并没有达到自己心里的预期, 但是欲速则不达, 需要慢慢积累~ </p>
<p>做好自己是最优的~ ^_^ 知足常乐但是还是要不忘自我突破~</p>
<h4 id="家庭">家庭</h4><p>2015年对于我个人算是收获家庭的一年~ 但是我说没有实质性的进展, 是因为暂时还没有让老婆有个安定的家, 也暂时没法给予她一个再普通不过的婚礼~</p>
<p>2015年3月家里多了一个成员, 帅气的阿拉斯加犬<strong>“加多宝”</strong>。做研发职位的工程师陪伴家人的时间没法和朝9晚5的工作一样那么多, 老婆大人就买了一个大狗狗陪她, 本人家庭地位直线下降塞~ 其实加多宝刚来的时候萌萌嗒~ </p>
<p><img src="http://blog.startry.com/img/blog_jiaduobao.png" alt="预编译Setting示例"></p>
<p>2015年9月正式和老婆领了证, 但是因为种种原因还没有正式的对外进行一次酒宴, 这也是未来2年内的努力目标~ 家庭方面看上去过的非常圆满, 实际上让老婆背负了很多很多~ 知足常乐但是不能让老婆一直背负压力, 所以需要更加努力~</p>
<p>2015年还有一颗未来的种子在不断的孕育长大~ 是今年最大的收获之一~ 也是人生最大的转折之一</p>
<h4 id="技术">技术</h4><p>2015年是本人的一个技术深入的瓶颈年, 在这一整年并没有在iOS领域深入突破, 基本还是保持在原有的水平的基础上进行了一些细节的补充~ </p>
<ul>
<li>技术上开始写博客进行记录和还原, 但是因为刚开始写, 写的也不是很好。</li>
<li>接触了上亿用户用户的App, 参与协同开发也是一种成长</li>
<li>尝试在<a href="stackoverflow.com/users/5238614/startry">Stackoverflow</a>回答一些iOS技术问题, 扩散影响力, 因为回复的不多, 效果也不佳</li>
</ul>
<p>遗憾的是整年并没有产出什么开源库, 也并没有什么开源库的维护~ 这块短板需要突破改进~</p>
<h4 id="2016">2016</h4><p>年终总结已完毕, 展望一下未来吧~ 2016年努力能够更加的突破自己~ 无论技术、家庭还是工作; 技术需要成长和寻找深入点, 工作需要更加挑战自己, 家庭希望自己能够多帮老婆减少点负担~ 关键还是要坚持, 无论做什么事情, 坚持做重要! 另外还要一直保持一颗”知足常乐”的心态~</p>
</content>
<summary type="html">
<p>既然决定要长期坚持写博客, 那么产出水帖也是必然的了~ 本文是个人今年的总结, 并没有啥技术含量哈~ 整年总结是以下四个字:</p>
<blockquote>
<p>知足常乐</p>
</blockquote>
<p>在某种程度上也算是自我安慰, 因为今年并没有实现重大的自
</summary>
<category term="随笔" scheme="http://startry.com/tags/%E9%9A%8F%E7%AC%94/"/>
</entry>
<entry>
<title>Pods依赖库快速开发入门</title>
<link href="http://startry.com/2015/12/22/pod-repo-dev-quick-start/"/>
<id>http://startry.com/2015/12/22/pod-repo-dev-quick-start/</id>
<published>2015-12-22T11:27:51.000Z</published>
<updated>2015-12-22T11:27:51.000Z</updated>
<content type="html"><p>CocoaPods是所有iOS开发熟知的一个第三方类库依赖管理工具。只要稍微有些经验的iOS开发者都会使用三方依赖库管理工具来管理工程依赖, CocoaPods是目前最火热权威的管理工具。</p>
<p>CocoaPods的基本使用现在网站上遍历都是的教程, 官方文档的简明教程也足够清晰明朗。本篇文章主要告诉大家<strong>如何去开发一个CocoaPods依赖库</strong>, 重点内容分三块:</p>
<ol>
<li>如何创建一个Pod Repo</li>
<li>如何将库提交到中央Spec库或私有Spec库中供大家使用</li>
<li>如何在开发中添加resource、framework以及其它依赖</li>
</ol>
<h3 id="创建一个Pod_Repo">创建一个Pod Repo</h3><h4 id="通过模板创建">通过模板创建</h4><p>关于如何创建一个Pod Repo, <a href="http://weibo.com/wtlucky" target="_blank" rel="external">@wtlucky_星魂丨飘渺灬</a>的一篇博文<a href="http://blog.wtlucky.com/blog/2015/02/26/create-private-podspec/" target="_blank" rel="external">使用Cocoapods创建私有podspec</a>里有介绍如何使用<code>pod lib create (libName)</code>去创建一个Pod Repo。简单示例如下:</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">pod lib create STDemoKit</span><br></pre></td></tr></table></figure>
<p>执行如上命令, 根据lib需要选择对应的语言(Objc/Swift)、是否需要生产示例项目以及是否需要基础测试target等选项, 就会生产一个默认的名叫<code>STDemoKit</code>的Pod库。</p>
<p>就这么一行命令, 自己动手敲一把, 绝对印象会加深很多的~</p>
<hr>
<h4 id="手动创建">手动创建</h4><p>CocoaPods会利用自带的模板去创建, 非常简单方便使用, 本文就不再赘述, 只是描述下如何手动去创建一个Pod Repo。</p>
<p>手动创建CocoaPods库的关键在于描述文件<strong>podspec</strong>, 手动创建私有库的第一步就是复制或新建一个podspec。(podspec格式描述固定, 直接从<a href="https://guides.cocoapods.org/making/specs-and-specs-repo.html" target="_blank" rel="external">官方网站</a>复制来的快)</p>
<p>手动创建私有库的关键两行代码在于:</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="constant">Pod::Spec</span>.new <span class="keyword">do</span> |spec|</span><br><span class="line"> spec.name = <span class="string">'STDemoKit'</span></span><br><span class="line"> spec.source_files = <span class="string">'MyLib/Classes/**/*.&#123;h,m,c&#125;'</span></span><br><span class="line"><span class="comment">## 补充必要的一些描述, 用于通过lint校验</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>
<p>在podspec所在文件夹下创建一个名为<strong>MyLib</strong>的文件夹, 并在<strong>MyLib</strong>文件夹下创建一个名为<strong>Classes*</strong>的文件夹, 放置自己实现的示例代码在该文件夹下。</p>
<p><img src="http://blog.startry.com/img/blog_create_pod_simple_demo.png" alt="自己动手创建podspec示例"></p>
<p>通过上述的步骤, 已经创建了自己的pod库, 然后需要的是本地测试, 测试自己的创建的本地库需要再创建一个Example工程, 同时在Podfile中指定本地库所在位置, 示例如下:</p>
<figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod <span class="string">"STDemoKit"</span>, :path =&gt; <span class="string">"~/目标库地址"</span></span><br></pre></td></tr></table></figure>
<p>在Podfile中指定本地的库地址后, 执行下<code>pod install</code>即可测试自己创建的库了。</p>
<h3 id="如何提交库到Spec">如何提交库到Spec</h3><p>在告诉读者怎么提交库到Spec的时候, 需要问大家一个问题: <strong>什么是Spec?</strong></p>
<p>我引用和翻译一下<a href="https://guides.cocoapods.org/making/specs-and-specs-repo.html" target="_blank" rel="external">官方文档的描述</a>: </p>
<p>PodSpec(Spec)是一个用来描述一个固定版本的Pod库的文件, 根据版本推移, 一个库会有多个PodSpec(Spec)文件去描述它。该描文件描述了该版本库的引用地址、需要引用的文件、应用编译配置项以及类似库名字、库版本和描述相关的其它元数据。</p>
<blockquote>
<p>A Podspec, or Spec, describes a version of a Pod library. One Pod, over the course of time, will have many Specs. It includes details about where the source should be fetched from, what files to use, the build settings to apply, and other general metadata such as its name, version, and description.</p>
</blockquote>
<p>利用CocoaPods模板创建的库的podspec描述文件如下:</p>
<p><img src="http://blog.startry.com/img/blog_podspec_demo.png" alt="PodSepc文件示例"></p>
<p>回归主题: <strong>怎么提交Spec</strong></p>
<p><a href="http://blog.wtlucky.com/blog/2015/02/26/create-private-podspec/" target="_blank" rel="external">使用Cocoapods创建私有podspec</a>中有一个章节<strong>《向Spec Repo提交podspec》</strong>, 该章节有<font color="red">详细</font>的说明介绍如果向Pod库提交podspec, 本文只是列出两个命令, 方便大家快速查阅, 分别是<strong>提交podspec</strong>和<strong>验证podspec有效性</strong>的命令。</p>
<p>一般情况下, 都需要先执行下有效性验证才会去向Spec库提交自己的podspec, 这个是开发者的基本素质吧~</p>
<p><strong>验证podspec有效性命令</strong>:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 需要在podspec文件所在目录下执行</span></span><br><span class="line">pod lib lint</span><br></pre></td></tr></table></figure>
<p><strong>提交podspec命令</strong>:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pod repo push 索引库名 STDebugConsole.podspec</span><br></pre></td></tr></table></figure>
<p><strong>补充</strong>:<br>关于索引库, 有区分官方库和非官方库。一般大公司都会维护一个<strong>官方索引库</strong>和<strong>自有索引库</strong>。<strong>官方索引库</strong>一般是<a href="https://github.com/CocoaPods/Specs" target="_blank" rel="external">git官方库</a>的一个镜像, 为了加快公司内对库索引更新的速度。<strong>自由索引库</strong>一般维护了公司业务产品自有依赖的基础组件和业务组件。</p>
<h3 id="Repo添加文件依赖">Repo添加文件依赖</h3><p>在实际工程开发过程中, 我发现很多童鞋都不了解怎么去添加资源文件到工程中, 其实我自己一开始也不太了解哈, 后面在工程应用中逐渐熟悉起来的。我结合自己的使用经验以及参考<a href="https://guides.cocoapods.org/making/specs-and-specs-repo.html" target="_blank" rel="external">CocoaPods官方描述文档</a>, 在这里简单描述一下。</p>
<p>我先举几个常用的实际场景:</p>
<h4 id="添加framework">添加framework</h4><p>假设我们已经通过前面说的模板去创建了一个私有库<strong>STShareKit</strong>, 然后我需要往<strong>STShareKit</strong>库添加分享库<strong>ShareSDK.framework</strong>, 我是不是直接和开发普通工程一样直接往文件目录一拖就好了呢? <font color="orange">实践证明这种方式是不可取的</font>。</p>
<p>因为STShareKit本身是一个库, 如果直接网里面拖framework, 只会把对应的target引用写进项目的pbproj下, 只是一次性的被根项目引用, 并且不能被库本身引用, 因此通过纯粹的拖动是不可取的。(其实本人觉得如果Xcode做的智能点, 应该是可以解决这个问题的, 吐槽下)</p>
<p>参考前面<strong>PodSepc文件示例</strong>的podspec文件描述, 核心解决关键在于下述代码:</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">s.vendored_frameworks = [</span><br><span class="line"> <span class="string">'Pod/Frameworks/*.framework'</span></span><br><span class="line">]</span><br><span class="line">s.frameworks = <span class="string">'UIKit'</span>, <span class="string">'Foundation'</span></span><br></pre></td></tr></table></figure>
<p>通过在podspec中添加上述两个文件描述并执行一次<code>pod install</code>, 一个可以解决第三方动态库, 一个可以添加本身库依赖的系统库。</p>
<p>大家会不会好奇通过podspec描述的文件是怎么添加上去的, 是怎么被主工程和库给引用的。对Xcode环境配置熟悉的童鞋肯定能够猜到, 环境配置不是在pbproj描述下就是通过xcconfig进行注入的。如果大家对xcconfig想要进一步了解, 请大家移步我之前写的文章<a href="http://blog.startry.com/2015/07/24/iOS_EnvWithXcconfig/" target="_blank" rel="external">《iOS开发必备 - 环境变量配置(Debug &amp; Release)》</a>。</p>
<p>那么我们打开STShareKit.xcconfig文件进行一探究竟。(需要在引用工程执行完<code>pod install</code>命令才会生成)。</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">FRAMEWORK_SEARCH_PATHS = $(inherited) <span class="string">"<span class="variable">$&#123;PODS_ROOT&#125;</span>/../Pod/Frameworks"</span></span><br><span class="line">GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=<span class="number">1</span></span><br><span class="line">HEADER_SEARCH_PATHS = <span class="string">"<span class="variable">$&#123;PODS_ROOT&#125;</span>/Headers/Private"</span> <span class="comment"># ...此处有省略</span></span><br></pre></td></tr></table></figure>
<p>第一行<strong>FRAMEWORK_SEARCH_PATHS</strong>基本已经解除了大家的困惑了吧~</p>
<h4 id="添加资源文件">添加资源文件</h4><p>添加资源文件的方式和添加第三方framework的方式相同, 核心解决关键在下述代码:</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">s.resource_bundles = &#123;</span><br><span class="line"> <span class="string">'STShareKit'</span> =&gt; [<span class="string">'Pod/Assets/*.png'</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>这里有个特殊的地方, 通过上述写法书写的方式在<code>pod</code>命令执行过程中会创建一个名为<strong>ONESDriver.bundle</strong>的bundle来包含所有防止在物理目录<code>Pod/Assets</code>下的资源文件。</p>
<p>添加资源文件还有另外一种方式(<strong>不推荐</strong>):</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">s.resources = <span class="string">'Pod/Assets/**/*'</span></span><br></pre></td></tr></table></figure>
<p>通过这种方式写法会被所有的资源文件不添加bundle直接copy进入主工程, 很容易发生重名冲突等问题, 不建议用这个写法~</p>
<p><strong>补充:</strong> 大家如果把上述描述文件修改和下面一样, 大家猜猜会发生什么事情呢? (库名为STShareKit)</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">s.resource_bundles = &#123;</span><br><span class="line"> <span class="string">'STShareUI'</span> =&gt; [<span class="string">'Pod/Assets/*.png'</span>]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>接下来抛给大家一个问题, 资源文件是怎么添加到主工程里的, 是通过xcconfig吗?<br>我一开始以为是通过xcconfig进行配置, 后面并没有找到对应的配置项目, 在工程下面的Build Phases, CocoaPods会默认添加一个执行脚本:</p>
<p><img src="http://blog.startry.com/img/blog_repo_copy_build_phases.png" alt="Build Phases拷贝脚本"></p>
<p>打开Target Support Files目录下Pods文件夹下的Pods-resources.sh脚本文件, 全局搜索STShareKit.bundle, 可以发现在脚本里有如下代码:</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">install_resource <span class="string">"$&#123;BUILT_PRODUCTS_DIR&#125;/STShareKit.bundle"</span></span><br></pre></td></tr></table></figure>
<p>看到这行代码基本已经解释了整个bundle是怎么进去到主工程里的了, 具体的细节请大家自行研究<strong>install_resource</strong>的实现。</p>
<h4 id="源代码分目录(区分group)">源代码分目录(区分group)</h4><p>做个私有库开发或者细心的童鞋会发现, 本身在开发库时候明明已经区分了物理目录的, 但是在引用工程里的Pods文件夹却是平铺展开的。对于<strong>处女座</strong>的开发者来说, 都是一怔灾难, 平铺看上怎么都是别扭的。以知名库<a href="https://github.com/icanzilb/JSONModel" target="_blank" rel="external">JSONModel</a>为例子:</p>
<p>开发库时候的目录分级:</p>
<p><img src="http://blog.startry.com/img/blog_menu_level_sub_jsonmodel.png" alt="JSONModel源码中目录"></p>
<p>引用库时候的目录分级:</p>
<p><img src="http://blog.startry.com/img/blog_menu_level_flat_jsonmodel.png" alt="JSONModel引入后目录"></p>
<p>是不是看到这个平级目录很抓狂~ <a href="https://guides.cocoapods.org/making/specs-and-specs-repo.html" target="_blank" rel="external">官方Guide</a>来解救有代码整齐强迫症的童鞋们了, 在<strong>A specification with subspecs</strong>章节, 有如下的一个样本podspec</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="constant">Pod::Spec</span>.new <span class="keyword">do</span> |spec|</span><br><span class="line"> spec.name = <span class="string">'ShareKit'</span></span><br><span class="line"> spec.source_files = <span class="string">'Classes/ShareKit/&#123;Configuration,Core,Customize UI,UI&#125;/**/*.&#123;h,m,c&#125;'</span></span><br><span class="line"> <span class="comment"># ...</span></span><br><span class="line"></span><br><span class="line"> spec.subspec <span class="string">'Evernote'</span> <span class="keyword">do</span> |evernote|</span><br><span class="line"> evernote.source_files = <span class="string">'Classes/ShareKit/Sharers/Services/Evernote/**/*.&#123;h,m&#125;'</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">spec.subspec <span class="string">'Facebook'</span> <span class="keyword">do</span> |facebook|</span><br><span class="line"> facebook.source_files = <span class="string">'Classes/ShareKit/Sharers/Services/Facebook/**/*.&#123;h,m&#125;'</span></span><br><span class="line"> facebook.compiler_flags = <span class="string">'-Wno-incomplete-implementation -Wno-missing-prototypes'</span></span><br><span class="line"> facebook.dependency <span class="string">'Facebook-iOS-SDK'</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># ...</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>
<p>呵呵, 有强迫症的童鞋是否已经找到了救星了呢? 通过描述subspec来对代码进行不同的层级区分, 这样使得引用的库能够有一定的层次感, 阅读和逻辑结构更加清晰。</p>
<p>想要了解的更加清晰, 可以参考知名库<a href="https://github.com/AFNetworking/AFNetworking" target="_blank" rel="external">AFNetworking</a>的<a href="https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking.podspec" target="_blank" rel="external">podspec</a>实现以及引用时候其生产的目录结构!</p>
<h3 id="总结">总结</h3><p>关于如何使用如何使用CocoaPods创建私有库, 个人感觉<a href="http://blog.wtlucky.com/blog/2015/02/26/create-private-podspec/" target="_blank" rel="external">使用Cocoapods创建私有podspec</a>已经写的非常的详细, 本篇文章的意义不大。本篇文章对于开发私有库时候的文件等资源文件添加的细节进行了一些补充, 方便大家更加快速的入门, 对于各路高手来说, 这篇文章只有带人引路的作用。</p>
<p>PS: 水平有限, 有错误的地方请及时指出~</p>
<h4 id="参考文献">参考文献</h4><ol>
<li><a href="http://blog.wtlucky.com/blog/2015/02/26/create-private-podspec/" target="_blank" rel="external">使用Cocoapods创建私有podspec</a></li>
<li><a href="https://guides.cocoapods.org" target="_blank" rel="external">CocoaPods官方指南</a></li>
</ol>
</content>
<summary type="html">
<p>CocoaPods是所有iOS开发熟知的一个第三方类库依赖管理工具。只要稍微有些经验的iOS开发者都会使用三方依赖库管理工具来管理工程依赖, CocoaPods是目前最火热权威的管理工具。</p>
<p>CocoaPods的基本使用现在网站上遍历都是的教程, 官方文档的简明
</summary>
<category term="CocoaPods" scheme="http://startry.com/tags/CocoaPods/"/>
<category term="framework" scheme="http://startry.com/tags/framework/"/>
</entry>
<entry>
<title>搭建博客的简单自述</title>
<link href="http://startry.com/2015/12/03/Power-source-of-writing/"/>
<id>http://startry.com/2015/12/03/Power-source-of-writing/</id>
<published>2015-12-03T02:40:16.000Z</published>
<updated>2015-12-03T02:40:16.000Z</updated>
<content type="html"><p>这篇不是技术博客哇~ 只是想记录下自己搭建博客的初衷和想法, 同时分享下过程中的平台和工具~ (典型的充数节奏)</p>
<p>搭建博客这件事情从大学的时候就想开始做了, 但是一直拖延到毕业后1年多才开始搭建, 中间的想法变化了好几次, 想把思想变化的过程记录在本篇文章中和会访问该地址的朋友们分享下哇~</p>
<p>搭建博客的核心观点引用<a href="http://book.douban.com/subject/6021440/" target="_blank" rel="external">《黑客与画家》</a>中的一句话:</p>
<blockquote>
<p>创造优美事物的方式往往不是从头做起, 而是在现有成果的基础上做一些小小的调整, 或者将已有的观点用比较新的方式组合起来。</p>
</blockquote>
<p>所以说, 我的博客内容80%属于重新思考总结, 20%属于偏门内容原创哈~ 在<strong>搭建的想法</strong>中我给自己列举了5个搭建博客的理由哈~</p>