-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
1440 lines (1168 loc) · 113 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
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><![CDATA[Bupo's Blog]]></title>
<link href="http://bupojung.github.io/atom.xml" rel="self"/>
<link href="http://bupojung.github.io/"/>
<updated>2017-02-27T14:05:43+08:00</updated>
<id>http://bupojung.github.io/</id>
<author>
<name><![CDATA[bupo]]></name>
<email><![CDATA[[email protected]]]></email>
</author>
<generator uri="http://octopress.org/">Octopress</generator>
<entry>
<title type="html"><![CDATA[直播终端优化总结]]></title>
<link href="http://bupojung.github.io/blog/2017/02/16/zhi-bo-zhong-duan-you-hua-zong-jie/"/>
<updated>2017-02-16T23:56:13+08:00</updated>
<id>http://bupojung.github.io/blog/2017/02/16/zhi-bo-zhong-duan-you-hua-zong-jie</id>
<content type="html"><![CDATA[<p>@(工作)[视频, ijkplayer, 编码]</p>
<h4>由来</h4>
<p>从接手做第一个版本到现在我们为加快首屏加载速度做过两次优化,到当前本基本上完成了视频秒开的目标。其实当知道加载速度慢的原因后最终的修改其实没什么复杂的,复杂的是分析原因的过程,这里主要分享一下从一开始的外部表现,逐步分析原因,到最终得到优化方案,验证优化成果的流程。希望这个过程也能给其他项目提供一些借鉴。</p>
<h4>优化方法</h4>
<ul>
<li>明确优化目标
<ul>
<li>1s内实现用户点击房间到首帧渲染完成</li>
</ul>
</li>
<li>自顶向下分析:
<ul>
<li>操作流程分解,找到流程中的问题;</li>
<li>逐步向底层深入,分析性能瓶颈;</li>
<li>得出优化方案</li>
<li>数据验证结果</li>
</ul>
</li>
</ul>
<!-- more -->
<h4>相关概念</h4>
<p>在介绍之前为了更好的理解后面的内容,先普及一下直播流程和视频流相关的术语。</p>
<p>推流流程:</p>
<p><img src="http://bupojung.github.io/static/2017/02/16/pushStream.png" alt="Alt text" /></p>
<p>拉流流程:</p>
<p><img src="http://bupojung.github.io/static/2017/02/16/pullStream.png" alt="Alt text" /></p>
<p>如图所示,从摄像头和麦克风获取到视频和音频数据,对于视频我们可能还需要做一些处理,比如加美颜滤镜或者其他的处理,这些原始数据是不适合直接在网络上实时传输的,第一是因为原始数据体积比较大,传输效率低;其次是因为要在网络上实时传输音视频,需要将音视频流分解成小块数据包,而原始数据需要添加音视频同步信息保证接收端能还原出完整的音视频流。所以获取到视频流和音频流之后分别进行编码,现在视频编码使用比较高效的H.264,而音频则使用AAC编码格式。
经过编码后,压缩了传输输数据,下一步是视频封装,将压缩后的音视频以某种格式封装,再使用RTMP网络协议封包推送到服务器。</p>
<p>拉流流程是推流的逆过程,只是少了视频处理的步骤。</p>
<p>推拉流流程的各个阶段涉及到一些概念:</p>
<ol>
<li><p>FPS:视频每秒包含多少帧,一帧就是一个静态的图像;</p></li>
<li><p>I帧,B帧和P帧:I帧是靠尽可能去除图像空间冗余信息来压缩传输数据量的帧内编码图像; P帧是通过充分降低与图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧; B帧是既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧;一般地,I帧压缩效率最低,P帧较高,B帧最高。I帧是通过帧内预测编码的,在获取到I帧后能解码出相应的图像,而P帧和B帧需要依赖相邻的帧才能解码出图像;</p></li>
<li><p>GOP:( Group of Pictures ) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。</p></li>
<li><p>GOP间隔:是指一个画面序列中相邻两个I帧的间隔时间,如图所示:
<img src="http://bupojung.github.io/static/2017/02/16/GOPInterval.jpg" alt="Alt text" /></p></li>
<li><p>封装格式:音视频文件封装格式有avi、rmvb、flv、mkv、MP4等等,封装格式就是把音视频数据打包成一个文件的规范。一般会包含音视频编码类型,时间戳,采样率等信息,方便接收方解析出原始数据。</p></li>
<li><p>视频码率:码率的单位为kbps,指每秒视频数据量,其值跟每秒视频帧数FPS,图像分辨率以及编码器压缩率有关。</p></li>
<li><p>首屏时间:用户点击一个视频到第一个视频画面展示出来的这段时间,是衡量直播体验的关键数据。</p></li>
</ol>
<p>我们的优化目标就是加快首屏时间,提高直播体验。所以需要先整理出从用户点击到视频出现这个流程有哪些阶段,然后分析各个阶段是否有可优化的空间来达到减少首屏时间的目标。</p>
<h4>顶部流程优化</h4>
<p>原来的进房间流程如下图所示:</p>
<p><img src="http://bupojung.github.io/static/2017/02/16/inroomProcess.png" alt="Alt text" /></p>
<p>左边的流程图问题很明显,其实拉流和拉房间信息不应该相互依赖,所用的数据应该在用户点击房间前准备好,这个问题的来源是刚接手做第一个版本的时候后台人手不足,移动端只能使用PC端原有的接口来做,这是妥协的结果。</p>
<h5>分析</h5>
<p><strong>进房间流程</strong></p>
<p>通过插入监控代码计算各个阶段的耗时</p>
<p>创建VC平均耗时:143.428350ms</p>
<p>拉取房间信息平均耗时:458.756419ms</p>
<p>这两个阶段是可以和拉流并行的,完全可以优化掉,在用户点击进房间时同时发起拉流请求和创建房间VC流程以及拉取房间信息。</p>
<p><strong>拉流流程</strong></p>
<p>通过和CDN提供商了解到,他们能提供获取最优CDN IP的接口,该接口可以根据用户所在IP和CDN分布情况返回给用户最优路线,加快连接和视频加载速度,同时减少了通过域名访问的DNS解析时间。</p>
<h5>优化</h5>
<p>经过分析我们很明显的得到如下的方案:</p>
<ol>
<li><p>在列表加载流地址信息,拉流不依赖与房间信息,可以并行请求;</p></li>
<li><p>用户点击同时发起拉流动作,不依赖与房间VC的创建,减少房间VC创建耗时;</p></li>
<li><p>应用启动预加载对应CDN提供商IP,在发起拉流请求时使用IP拉流,提高连接速度;</p></li>
</ol>
<p><img src="http://bupojung.github.io/static/2017/02/16/inroomProcess-afterTunning.png" alt="Alt text" /></p>
<h5>结果</h5>
<p>经过流程上的优化,在相同网络下多次数据取平均值做数据对比,优化前后平局耗时减少0.6s左右,基本上跟之前分析结果一致。
优化前首屏耗时:2810.096540, 总次数:474,总耗时:1331985.760193
优后后首屏耗时:2207.551701, 总次数:427,总耗时:942624.576327</p>
<p>这个数据离秒开差距还是有点大的,接下来只能从IJKPlayer拉流流程中分析出耗时的原因。</p>
<h4>底层环节优化</h4>
<h5>分析</h5>
<p>通过分析IJKPlayer拉流流程如下:</p>
<p><img src="http://bupojung.github.io/static/2017/02/16/pullStreamDetail.png" alt="Alt text" /></p>
<p>通过打时间戳点发现,有两个阶段耗时比较多:</p>
<ol>
<li><p>avformat_find_stream_info: 主要是读一些包(packets ),然后从中提取流的信息。包括获取流解码器,打开对应编码器,从流信息中得到设置编码器的参数等。具体功能可以看<a href="http://blog.csdn.net/u011913612/article/details/53642355">这里</a></p></li>
<li><p>decode_video: 这个方法会判断当前帧能否解码,要解码的第一帧必须是关键帧,后续的非关键需要依赖第一个关键帧才能解析出来,在测试中发现此方法得到第一个关键帧之前会丢弃最多两百多个数据包,还有从手机直播的视频流丢弃的数据包相比PC直播的少。</p></li>
</ol>
<p><strong>对于第一个问题</strong>,如果提前知道拉取的视频流信息,和解码器,那么是可以跳过这个方法,不过需要所有的参数都设置对,对于推流端不确定的情况很难去跳过这个方法,因为参数设置错误,解码器就解不出视频帧,网上有人给出了<a href="https://jiya.io/archives/vlc_optimize_1.html">优化方案</a>,但是我并没有成功过。还有一种方式是通过设置参数限制这个方法的分析时间。可以通过IJKPlayer的参数设置,局限性是如果设置的时间过小可能分析不出足够的流信息导致解码失败。所以没有从这个点优化;</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='cpp'><span class='line'><span class="p">[</span><span class="n">options</span> <span class="nl">setFormatOptionIntValue</span><span class="p">:</span><span class="mi">4096</span> <span class="nl">forKey</span><span class="p">:</span><span class="err">@</span><span class="s">"probsize"</span><span class="p">];</span> <span class="c1">//一次分析的包大小</span>
</span></code></pre></td></tr></table></div></figure>
<p><strong>针对decode_video获取第一个关键帧之前会丢弃很多数据包的问题分析</strong></p>
<ol>
<li><p>实验表明,不同来源的流丢弃的数据包数量不同,PC端的流最大丢弃200多个数据包,而手机端推流一般最多在40几个数据包,这导致播放PC端的流平局耗时比播放手机端的流大;</p></li>
<li><p>通过使用映客的流地址拉流发现,映客流获取到第一个关键帧之前丢弃的帧数最多在20个左右,使用映客的流播放基本能达到1s-1.5s的首屏时间;</p></li>
</ol>
<p>根据这两个实验结果,我们发现,丢弃的帧的数量其实跟推流端一个GOP间隔大小有关系,如果GOP间隔是2秒,FPS是20,那么如果第一帧刚好读取到关键帧之后的非关键帧,那么两秒后才会有第二个关键帧,也就是需要再等20*2 = 40 个包后才能解析出第一个图像;而PC推流端的GOP是主播自定义的或者使用推流工具默认的值,手机端推流的GOP是3s,映客的GOP应该是2s-1s;</p>
<p><strong>得出的结论是可以通过减小GOP间隔来提高加载速度;</strong></p>
<p>但是GOP并不一定设置的越小越好,GOP越小将导致压缩率越低,同样时间长度的视频体积越大,也可能导致加载速度慢。直播应用GOP间隔一般设置为2s或者1s; 即使设置为1s,从读取到第一帧到解析出第一个图像的耗时期望值为0.5s,加上其他的连接建立等其他的环节,网络速度的影响,也很难达保证稳定的秒开目标。网络正常的情况下平均耗时在1.5s左右。而且我们的PC端推流GOP参数无法控制,不同的主播用的推流工具还不一样,她们也不会去设置这个参数。这个优化只能控制从手机推流的视频流的加载时间。</p>
<p><strong>进一步分析</strong></p>
<p>由于和CDN提供商沟通不畅,我们一直觉得他们没有做关键帧缓存,所以下发的第一个视频帧不是关键帧,导致会读取到很多非关键帧,增加首帧渲染耗时。后来通过进一步了解,他们说他们确实是做了关键帧缓存,并且下发的第一个视频帧一定是关键帧。<strong>然而我并不相信</strong>,因为从终端打出来的log数据,第一个开始解码的帧并不是关键帧。当然我也开始怀疑是不是在解码第一个帧之前丢弃了一些数据,将关键帧丢弃了。</p>
<p><strong>验证是否丢弃了关键帧</strong></p>
<ol>
<li><p>使用应用播放一个本地完整的flv文件,发现仍然会丢弃一部分非关键帧。这就有问题了,一个完整的视频文件第一帧应该是关键帧的,然而它不见了。</p></li>
<li><p>使用flv分析工具确认,文件第一帧确实是关键帧。</p></li>
</ol>
<p>从以上的验证得到的结论是,<strong>IJKPlayer拉流在解码数据包之前的步骤把关键帧丢弃了。</strong></p>
<p><strong>跟踪代码</strong></p>
<p>通过阅读IJKPlayer的代码,发现IJKPlayer并没有丢弃数据包的逻辑,于是只能深入到ffmpeg的源码中找原因</p>
<p>拉流流程主要调用了这几个方法,逐个分析:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
</pre></td><td class='code'><pre><code class='cpp'><span class='line'><span class="n">av_find_input_format</span>
</span><span class='line'><span class="n">avformat_open_input</span>
</span><span class='line'><span class="n">av_format_inject_global_side_data</span>
</span><span class='line'><span class="n">setup_find_stream_info_opts</span>
</span><span class='line'><span class="n">avformat_find_stream_info</span>
</span><span class='line'><span class="n">av_read_frame</span>
</span></code></pre></td></tr></table></div></figure>
<p>最终发现,在avformat_find_stream_info有一个逻辑</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
</pre></td><td class='code'><pre><code class='cpp'><span class='line'><span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="n">ic</span><span class="o">-></span><span class="n">flags</span> <span class="o">&</span> <span class="n">AVFMT_FLAG_NOBUFFER</span><span class="p">))</span> <span class="p">{</span>
</span><span class='line'><span class="n">ret</span> <span class="o">=</span> <span class="n">add_to_pktbuf</span><span class="p">(</span><span class="o">&</span><span class="n">ic</span><span class="o">-></span><span class="n">internal</span><span class="o">-></span><span class="n">packet_buffer</span><span class="p">,</span> <span class="n">pkt</span><span class="p">,</span>
</span><span class='line'><span class="o">&</span><span class="n">ic</span><span class="o">-></span><span class="n">internal</span><span class="o">-></span><span class="n">packet_buffer_end</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</span><span class='line'><span class="k">if</span> <span class="p">(</span><span class="n">ret</span> <span class="o"><</span> <span class="mi">0</span><span class="p">)</span>
</span><span class='line'><span class="k">goto</span> <span class="n">find_stream_info_err</span><span class="p">;</span>
</span><span class='line'><span class="p">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>如果设置了AVFMT_FLAG_NOBUFFER 参数为YES,那么用来分析流信息的数据包将不会存到buffer中。</p>
<p><strong>WTF</strong>我此刻的心情,无以言表,尼玛,我们的代码确实设置了这个参数,而且还写着注释</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="p">[</span><span class="n">options</span> <span class="nl">setFormatOptionValue</span><span class="p">:</span><span class="s">@"nobuffer"</span> <span class="nl">forKey</span><span class="p">:</span><span class="s">@"fflags"</span><span class="p">];</span> <span class="c1">//开启之后,会出现进入页面立马卡顿一下的问题</span>
</span></code></pre></td></tr></table></div></figure>
<p>把这个一句注释之后发现,世界更美好了。</p>
<h5>优化</h5>
<p>经过分析得到的优化方案:</p>
<ol>
<li><p>推流端GOP间隔时间设置为2s;</p></li>
<li><p>拉流端不设置nobuffer参数;</p></li>
</ol>
<p>就两个参数的变化。av_read_frame读取到的第一个数据包就是关键帧。不需要丢弃任何非关键帧数据。达到秒开的目标。</p>
<h5>结果</h5>
<p>下面是现网数据上报统计的结果,基本在一秒内看到首屏画面,实际的体验比之前好了很多。</p>
<p><img src="http://bupojung.github.io/static/2017/02/16/dataAfterTunning.jpeg" alt="Alt text" /></p>
<h5>推流端优化点</h5>
<h5>自适应码率</h5>
<p>自适应码率是只通过网络传输速率的反馈,推流端动态调整编码平局码率,提高带宽利用率。当网络状态比较差时可以传输质量较低的视频,而当网络状态变好时可以传输较清晰的视频。避免由于网络阻塞导致断流卡顿。现在很多推流端都已近做了这种优化,包括VideoCore和LFLivekit。</p>
<h5>编码等级选择</h5>
<p>H.264有四种画质级别,分别是BP、EP、MP、HP:</p>
<ol>
<li><p>BP-Baseline Profile:基本画质。支持I/P 帧,只支持无交错(Progressive)和CAVLC;</p></li>
<li><p>EP-Extended profile:进阶画质。支持I/P/B/SP/SI 帧,只支持无交错(Progressive)和CAVLC;</p></li>
<li><p>MP-Main profile:主流画质。提供I/P/B 帧,支持无交错(Progressive)和交错(Interlaced),也支持CAVLC 和CABAC 的支持;</p></li>
<li><p>HP-High profile:高级画质。在main Profile 的基础上增加了8x8内部预测、自定义量化、无损视频编码和更多的YUV 格式;</p></li>
</ol>
<p>上面解释了各种画质等级支持的特性。具体到推流的选择上,我们考虑的有几点:</p>
<ol>
<li><p>观众端是否支持该种编码等级的解码;</p></li>
<li><p>越高的编码等级编解码越复杂,压缩率越高,同样的网络速率能传输更多的视频信息;</p></li>
<li><p>观众端解码效率是否能流程的解码不出现卡顿。</p></li>
</ol>
<p>首先现在的设备基本上都支持者四个等级的解码。我们要考虑的是在播放段的解码效率选择最高的编码等级。我们没有去验证过各种低端设备对各个编码等级解码效率,但是我们现在用的是main profile 没有发现由于解码效率而出现卡顿的现象。(PS. 映客使用的是Baseline Profile)。</p>
<h4>总结</h4>
<p>优化是一个渐进的过程,从表面显而易见的开始,逐步拆解流程,找到可能的优化点,对这些点逐个分析,找到性能瓶颈,然后想办法解决。当然优化有时也需要权衡代价和效果,重点先解决性价比比较高的优化点。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[实现一个控件的思路]]></title>
<link href="http://bupojung.github.io/blog/2016/06/03/shi-xian-%5B%3F%5D-ge-kong-jian-de-si-lu/"/>
<updated>2016-06-03T19:24:09+08:00</updated>
<id>http://bupojung.github.io/blog/2016/06/03/shi-xian-[?]-ge-kong-jian-de-si-lu</id>
<content type="html"><![CDATA[<p>——易用性和可扩展性</p>
<p>by bupo.</p>
<p>这里通过实现一个弹出菜单控件为例。弹出菜单如图所示,菜单可以有多行,每一行有多个选项,每一行可能有个标题,每行有多个选项,如果选项个数超过屏幕范围可以左右滑动。点击选项执行对应的功能模块。这里把这个控件命名为ScrollActionSheet。</p>
<!-- more -->
<p><img src="http://bupojung.github.io/static/2016/06/03/actionSheet.jpg" alt="Alt text" /></p>
<h4>实现</h4>
<p>通过界面和需求可以分析出,ScrollActionSheet 包含几个部分:
- 选项按钮ActionSheetItemView;
- 包含多个选项和标题的一个可滑动模块HorizontleScrollPannel;
- 取消按钮,就是一个简单的button;</p>
<p>这三个部分是ScrollActionSheet的UI,这几个模块中,我们需要找出哪些东西是会变化的,哪些是不变化的,然后把变化的部分独立出来,把不变的封装起来。用户使用的时候只需要通过继承或者组合的方式修改可变的部分就可以使用这个ScrollActionSheet。可以看出用户可能改变的部分都在ActionSheetItemView中,包括图片,标题,点击后的回调方法,还有这个ActionSheetItemView的大小和布局;而HorizontleScrollPanner和整个ScrollActionSheet不需要去改动。
所以这里ActionSheetItemView 需要负责布局和展示自己的内容,所以我们独立出另一个类ActionSheetItem。大概的关系如下图所示:
<img src="http://bupojung.github.io/static/2016/06/03/ActionSheetClass.png" alt="Alt text" /></p>
<p>使用的时候只需要创建ActionSheetItem添加到ScrollActionSheet中展示就可以了</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
<span class='line-number'>28</span>
<span class='line-number'>29</span>
<span class='line-number'>30</span>
<span class='line-number'>31</span>
<span class='line-number'>32</span>
<span class='line-number'>33</span>
<span class='line-number'>34</span>
<span class='line-number'>35</span>
<span class='line-number'>36</span>
<span class='line-number'>37</span>
<span class='line-number'>38</span>
<span class='line-number'>39</span>
<span class='line-number'>40</span>
<span class='line-number'>41</span>
<span class='line-number'>42</span>
<span class='line-number'>43</span>
<span class='line-number'>44</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">showShareView</span>
</span><span class='line'><span class="p">{</span>
</span><span class='line'> <span class="bp">NSMutableArray</span> <span class="o">*</span><span class="n">shareItems</span> <span class="o">=</span> <span class="p">[</span><span class="bp">NSMutableArray</span> <span class="n">array</span><span class="p">];</span>
</span><span class='line'> <span class="bp">NSMutableArray</span> <span class="o">*</span><span class="n">moreItems</span> <span class="o">=</span> <span class="p">[</span><span class="bp">NSMutableArray</span> <span class="n">array</span><span class="p">];</span>
</span><span class='line'>
</span><span class='line'> <span class="p">[</span><span class="n">shareItems</span> <span class="nl">addObject</span><span class="p">:[</span><span class="n">BPScrollActionSheetItem</span> <span class="nl">itemWithImage</span><span class="p">:</span><span class="n">BPIMAGE</span><span class="p">(</span><span class="s">@"more_icon_share_wx.png"</span><span class="p">)</span> <span class="nl">title</span><span class="p">:</span><span class="s">@"微信好友"</span> <span class="nl">action</span><span class="p">:</span><span class="o">^</span><span class="p">(</span><span class="n">BPBaseActionSheetItem</span> <span class="o">*</span><span class="n">item</span><span class="p">){</span>
</span><span class='line'> <span class="n">NSLog</span><span class="p">(</span><span class="s">@"item click:%@"</span><span class="p">,</span><span class="n">item</span><span class="p">);</span>
</span><span class='line'> <span class="p">}]];</span>
</span><span class='line'> <span class="p">[</span><span class="n">shareItems</span> <span class="nl">addObject</span><span class="p">:[</span><span class="n">BPScrollActionSheetItem</span> <span class="nl">itemWithImage</span><span class="p">:</span><span class="n">BPIMAGE</span><span class="p">(</span><span class="s">@"more_icon_share_friends.png"</span><span class="p">)</span> <span class="nl">title</span><span class="p">:</span><span class="s">@"微信朋友圈"</span> <span class="nl">action</span><span class="p">:</span><span class="o">^</span><span class="p">(</span><span class="n">BPBaseActionSheetItem</span> <span class="o">*</span><span class="n">item</span><span class="p">){</span>
</span><span class='line'> <span class="n">NSLog</span><span class="p">(</span><span class="s">@"item click:%@"</span><span class="p">,</span><span class="n">item</span><span class="p">);</span>
</span><span class='line'> <span class="p">}]];</span>
</span><span class='line'> <span class="p">[</span><span class="n">shareItems</span> <span class="nl">addObject</span><span class="p">:[</span><span class="n">BPScrollActionSheetItem</span> <span class="nl">itemWithImage</span><span class="p">:</span><span class="n">BPIMAGE</span><span class="p">(</span><span class="s">@"more_icon_share_qq.png"</span><span class="p">)</span> <span class="nl">title</span><span class="p">:</span><span class="s">@"QQ好友"</span> <span class="nl">action</span><span class="p">:</span><span class="o">^</span><span class="p">(</span><span class="n">BPBaseActionSheetItem</span> <span class="o">*</span><span class="n">item</span><span class="p">){</span>
</span><span class='line'> <span class="n">NSLog</span><span class="p">(</span><span class="s">@"item click:%@"</span><span class="p">,</span><span class="n">item</span><span class="p">);</span>
</span><span class='line'> <span class="p">}]];</span>
</span><span class='line'> <span class="p">[</span><span class="n">shareItems</span> <span class="nl">addObject</span><span class="p">:[</span><span class="n">BPScrollActionSheetItem</span> <span class="nl">itemWithImage</span><span class="p">:</span><span class="n">BPIMAGE</span><span class="p">(</span><span class="s">@"more_icon_share_qzone.png"</span><span class="p">)</span> <span class="nl">title</span><span class="p">:</span><span class="s">@"QQ空间"</span> <span class="nl">action</span><span class="p">:</span><span class="o">^</span><span class="p">(</span><span class="n">BPBaseActionSheetItem</span> <span class="o">*</span><span class="n">item</span><span class="p">){</span>
</span><span class='line'> <span class="n">NSLog</span><span class="p">(</span><span class="s">@"item click:%@"</span><span class="p">,</span><span class="n">item</span><span class="p">);</span>
</span><span class='line'> <span class="p">}]];</span>
</span><span class='line'>
</span><span class='line'> <span class="n">BPLongActionSheetItem</span> <span class="o">*</span><span class="n">item</span> <span class="o">=</span> <span class="p">[</span><span class="n">BPLongActionSheetItem</span> <span class="nl">itemWithImage</span><span class="p">:</span><span class="n">BPIMAGE</span><span class="p">(</span><span class="s">@"qualifying_icon_set"</span><span class="p">)</span> <span class="nl">title</span><span class="p">:</span><span class="s">@"偏好设置"</span> <span class="nl">subTitle</span><span class="p">:</span><span class="s">@"啦啦啦编辑偏好设置啊"</span> <span class="nl">action</span><span class="p">:</span><span class="o">^</span><span class="p">(</span><span class="n">BPBaseActionSheetItem</span> <span class="o">*</span><span class="n">item</span><span class="p">){</span>
</span><span class='line'> <span class="n">NSLog</span><span class="p">(</span><span class="s">@"item click:%@"</span><span class="p">,</span><span class="n">item</span><span class="p">);</span>
</span><span class='line'> <span class="p">}];</span>
</span><span class='line'>
</span><span class='line'> <span class="p">[</span><span class="n">moreItems</span> <span class="nl">addObject</span><span class="p">:</span><span class="n">item</span><span class="p">];</span>
</span><span class='line'>
</span><span class='line'> <span class="bp">NSDictionary</span> <span class="o">*</span><span class="n">section</span> <span class="o">=</span> <span class="nb">nil</span><span class="p">;</span>
</span><span class='line'> <span class="k">if</span> <span class="p">([</span><span class="n">shareItems</span> <span class="n">count</span><span class="p">]</span> <span class="o">></span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span><span class='line'> <span class="n">section</span> <span class="o">=</span> <span class="l">@{</span>
</span><span class='line'> <span class="s">@"title"</span><span class="o">:</span><span class="s">@"掐指一算,你还缺几个兄弟啊!火速召唤他们"</span><span class="p">,</span>
</span><span class='line'> <span class="s">@"items"</span><span class="o">:</span><span class="n">shareItems</span><span class="p">,</span>
</span><span class='line'> <span class="l">}</span><span class="p">;</span>
</span><span class='line'> <span class="p">}</span>
</span><span class='line'>
</span><span class='line'> <span class="bp">NSDictionary</span> <span class="o">*</span><span class="n">upperSection</span> <span class="o">=</span> <span class="l">@{</span>
</span><span class='line'> <span class="s">@"title"</span><span class="o">:</span><span class="s">@"haha "</span><span class="p">,</span>
</span><span class='line'> <span class="s">@"items"</span><span class="o">:</span><span class="n">moreItems</span><span class="p">,</span>
</span><span class='line'> <span class="l">}</span><span class="p">;</span>
</span><span class='line'>
</span><span class='line'> <span class="bp">NSArray</span> <span class="o">*</span><span class="n">sections</span> <span class="o">=</span> <span class="nb">nil</span><span class="p">;</span>
</span><span class='line'> <span class="n">sections</span> <span class="o">=</span> <span class="l">@[</span><span class="n">upperSection</span><span class="p">,</span><span class="n">section</span><span class="l">]</span><span class="p">;</span>
</span><span class='line'> <span class="n">BPScrollActionSheet</span> <span class="o">*</span><span class="n">actionSheet</span> <span class="o">=</span> <span class="p">[[</span><span class="n">BPScrollActionSheet</span> <span class="n">alloc</span><span class="p">]</span> <span class="nl">initWithItems</span><span class="p">:</span><span class="n">sections</span> <span class="nl">description</span><span class="p">:</span><span class="nb">nil</span> <span class="nl">cancelButtonTitle</span><span class="p">:</span><span class="s">@"取消"</span><span class="p">];</span>
</span><span class='line'> <span class="p">[</span><span class="n">actionSheet</span> <span class="nl">showInView</span><span class="p">:</span><span class="nb">self</span><span class="p">.</span><span class="n">view</span><span class="p">];</span>
</span><span class='line'>
</span><span class='line'> <span class="k">return</span><span class="p">;</span>
</span><span class='line'><span class="p">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>如上面代码所示,LongActionSheetItem继承自ActionSheetItem,比ActionSheetItem多了一个小标题字段,其对应的View为LongActionsSheetItemView负责自定义布局,对于自定义的Item用户只需要继承ActionSheetItem和ActionSheetItemView自定义内容和布局即可。</p>
<p><img src="http://bupojung.github.io/static/2016/06/03/ActionSheetDemo.png" alt="Alt text" /></p>
<p>这里通过组合封装达到了易用性的目的,而可扩展性则通过独立出可变模块,通过组合,继承来实现。具体的代码稍后会上传到github,点击这里<a href="https://github.com/bupojung/BPActionScrollActionSheet">下载</a></p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Javascript原生交互解决方]]></title>
<link href="http://bupojung.github.io/blog/2016/06/01/javascriptyuan-sheng-jiao-hu-jie-jue-fang/"/>
<updated>2016-06-01T20:13:42+08:00</updated>
<id>http://bupojung.github.io/blog/2016/06/01/javascriptyuan-sheng-jiao-hu-jie-jue-fang</id>
<content type="html"><![CDATA[<p>by bupo.</p>
<h4>简介</h4>
<p>在终端应用开发过程中经常需要在H5页面中调用原生接口来使用原生服务,所以就有了Javascript与原生代码交互的需求,这里终结一下以前在项目中使用的一种解决方案。
原理很简单,通过在UIWebView的代理中截获window.location.href 跳转请求来响应Javascript请求,并通过UIWebview 提供的执行JS代码的接口stringByEvaluatingJavaScriptFromString 将原始执行结果回调给H5页面。</p>
<!-- more -->
<h4>具体实施</h4>
<p>首先终端和H5确定好调用协议,这里以获取经纬度为例</p>
<pre><code>jsbridge://getLocation/callback=xx#seq
</code></pre>
<p>其中jsbridge是我们自定义的一个协议名,用来区分是调用原生接口的请求还是网络请求,getLocation是接口名称,"/“和“#”之前是参数对,多个参数用&链接,“#”后的seq是调用回调函数的时候传回给JS代码的序列号,用于在多次调用时区分是那一次调用,如果不需要回调给JS可以不传。
下面是H5代码示例,页面中有一个按钮,点击按钮发起原生调用请求。</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
</pre></td><td class='code'><pre><code class='html'><span class='line'><span class="cp"><!doctype html public "-//w3c//dtd html 4.0 transitional//en"></span>
</span><span class='line'><span class="nt"><html></span>
</span><span class='line'><span class="nt"><head></span>
</span><span class='line'> <span class="nt"><title></span> JS原生交互示例 <span class="nt"></title></span>
</span><span class='line'> <span class="nt"><meta</span> <span class="na">charset=</span><span class="s">"utf-8"</span><span class="nt">></span>
</span><span class='line'> <span class="nt"><meta</span> <span class="na">name=</span><span class="s">"generator"</span> <span class="na">content=</span><span class="s">"editplus"</span><span class="nt">></span>
</span><span class='line'> <span class="nt"><meta</span> <span class="na">name=</span><span class="s">"author"</span> <span class="na">content=</span><span class="s">""</span><span class="nt">></span>
</span><span class='line'> <span class="nt"><meta</span> <span class="na">name=</span><span class="s">"keywords"</span> <span class="na">content=</span><span class="s">""</span><span class="nt">></span>
</span><span class='line'> <span class="nt"><meta</span> <span class="na">name=</span><span class="s">"description"</span> <span class="na">content=</span><span class="s">""</span><span class="nt">></span>
</span><span class='line'> <span class="nt"><script </span><span class="na">language=</span><span class="s">"javascript"</span> <span class="na">type=</span><span class="s">"text/javascript"</span><span class="nt">></span>
</span><span class='line'>
</span><span class='line'><span class="kd">function</span> <span class="nx">call_getLocation</span><span class="p">(){</span>
</span><span class='line'> <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">href</span><span class="o">=</span><span class="s2">"jsbridge://getLocation/callback=showData#1"</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'><span class="kd">function</span> <span class="nx">showData</span><span class="p">(</span><span class="nx">s</span><span class="p">,</span><span class="nx">info</span><span class="p">){</span>
</span><span class='line'> <span class="nx">alert</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">info</span><span class="p">));</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'> <span class="nt"></script></span>
</span><span class='line'><span class="nt"></head></span>
</span><span class='line'><span class="nt"><body></span>
</span><span class='line'><span class="nt"><input</span> <span class="na">type=</span><span class="s">"button"</span> <span class="na">onclick=</span><span class="s">"call_getLocation()"</span> <span class="na">value=</span><span class="s">"location"</span><span class="nt">/></span>
</span><span class='line'><span class="nt"></body></span>
</span><span class='line'><span class="nt"></html></span>
</span></code></pre></td></tr></table></div></figure>
<p>在UIWebviewController的代理中捕获请求,判断是原生请求并调用相应接口。代码如下:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">BOOL</span><span class="p">)</span><span class="nf">webView:</span><span class="p">(</span><span class="bp">UIWebView</span> <span class="o">*</span><span class="p">)</span><span class="nv">webView</span> <span class="nf">shouldStartLoadWithRequest:</span><span class="p">(</span><span class="bp">NSURLRequest</span> <span class="o">*</span><span class="p">)</span><span class="nv">request</span> <span class="nf">navigationType:</span><span class="p">(</span><span class="n">UIWebViewNavigationType</span><span class="p">)</span><span class="nv">navigationType</span> <span class="p">{</span>
</span><span class='line'> <span class="bp">NSURL</span> <span class="o">*</span><span class="n">requestURL</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">URL</span><span class="p">;</span>
</span><span class='line'> <span class="bp">NSString</span> <span class="o">*</span><span class="n">urlString</span> <span class="o">=</span> <span class="p">[</span><span class="n">requestURL</span> <span class="n">absoluteString</span><span class="p">];</span>
</span><span class='line'> <span class="kt">BOOL</span> <span class="n">shouldStartLoad</span> <span class="o">=</span> <span class="nb">YES</span><span class="p">;</span>
</span><span class='line'>
</span><span class='line'> <span class="k">if</span> <span class="p">([</span><span class="n">urlString</span> <span class="nl">hasPrefix</span><span class="p">:</span><span class="s">@"jsbridge://"</span><span class="p">])</span> <span class="p">{</span>
</span><span class='line'> <span class="p">[</span><span class="nb">self</span> <span class="nl">decodeCMDandParams</span><span class="p">:[</span><span class="n">urlString</span> <span class="nl">substringFromIndex</span><span class="p">:</span><span class="n">kJSBridgePre</span><span class="p">.</span><span class="n">length</span><span class="p">]];</span>
</span><span class='line'> <span class="n">shouldStartLoad</span> <span class="o">=</span> <span class="nb">NO</span><span class="p">;</span>
</span><span class='line'> <span class="p">}</span>
</span><span class='line'>
</span><span class='line'> <span class="k">return</span> <span class="n">shouldStartLoad</span><span class="p">;</span>
</span><span class='line'><span class="p">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>decodeCMDandParams 方法解析出接口名和参数,利用OC runtime特性执行对应原生代码。</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
<span class='line-number'>27</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">decodeCMDandParams:</span><span class="p">(</span><span class="bp">NSString</span> <span class="o">*</span><span class="p">)</span><span class="nv">cmdAndParams</span>
</span><span class='line'><span class="p">{</span>
</span><span class='line'> <span class="n">QBLogDebug</span><span class="p">(</span><span class="s">@"request cmd and params:%@"</span><span class="p">,</span><span class="n">cmdAndParams</span><span class="p">);</span>
</span><span class='line'> <span class="bp">NSArray</span> <span class="o">*</span><span class="n">data</span> <span class="o">=</span> <span class="p">[</span><span class="n">cmdAndParams</span> <span class="nl">componentsSeparatedByString</span><span class="p">:</span><span class="s">@"/"</span><span class="p">];</span>
</span><span class='line'> <span class="bp">NSString</span> <span class="o">*</span><span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span><span class="n">data</span> <span class="n">firstObject</span><span class="p">];</span>
</span><span class='line'> <span class="bp">NSDictionary</span> <span class="o">*</span><span class="n">params</span> <span class="o">=</span> <span class="nb">nil</span><span class="p">;</span>
</span><span class='line'> <span class="k">if</span> <span class="p">([</span><span class="n">data</span> <span class="n">count</span><span class="p">]</span> <span class="o">></span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span><span class='line'> <span class="n">params</span> <span class="o">=</span> <span class="p">[</span><span class="nb">self</span> <span class="nl">parseParams</span><span class="p">:</span><span class="n">data</span><span class="p">[</span><span class="mi">1</span><span class="p">]];</span>
</span><span class='line'> <span class="p">}</span>
</span><span class='line'>
</span><span class='line'> <span class="p">[</span><span class="nb">self</span> <span class="nl">invokeWithCMD</span><span class="p">:</span><span class="n">cmd</span> <span class="nl">andParams</span><span class="p">:</span><span class="n">params</span><span class="p">];</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'>
</span><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">invokeWithCMD:</span><span class="p">(</span><span class="bp">NSString</span> <span class="o">*</span><span class="p">)</span><span class="nv">cmd</span> <span class="nf">andParams:</span><span class="p">(</span><span class="bp">NSDictionary</span> <span class="o">*</span><span class="p">)</span><span class="nv">params</span>
</span><span class='line'><span class="p">{</span>
</span><span class='line'> <span class="k">if</span> <span class="p">(</span><span class="n">cmd</span> <span class="o">==</span> <span class="nb">nil</span> <span class="o">||</span> <span class="n">cmd</span><span class="p">.</span><span class="n">length</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span><span class='line'> <span class="k">return</span><span class="p">;</span>
</span><span class='line'> <span class="p">}</span>
</span><span class='line'> <span class="kt">SEL</span> <span class="n">selector</span> <span class="o">=</span> <span class="n">NSSelectorFromString</span><span class="p">([</span><span class="bp">NSString</span> <span class="nl">stringWithFormat</span><span class="p">:</span><span class="s">@"Test_%@:"</span><span class="p">,</span><span class="n">cmd</span><span class="p">]);</span>
</span><span class='line'> <span class="k">if</span> <span class="p">([</span><span class="nb">self</span> <span class="nl">respondsToSelector</span><span class="p">:</span><span class="n">selector</span><span class="p">])</span> <span class="p">{</span>
</span><span class='line'><span class="cp">#pragma clang diagnostic push</span>
</span><span class='line'><span class="cp">#pragma clang diagnostic ignored "-Warc-performSelector-leaks"</span>
</span><span class='line'> <span class="p">[</span><span class="nb">self</span> <span class="nl">performSelector</span><span class="p">:</span><span class="n">selector</span> <span class="nl">withObject</span><span class="p">:</span><span class="n">params</span><span class="p">];</span>
</span><span class='line'><span class="cp">#pragma clang diagnostic pop</span>
</span><span class='line'>
</span><span class='line'> <span class="p">}</span>
</span><span class='line'><span class="p">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>这样只需要和前端定好交互协议,并实现对应的原生接口就可以完成JS调用原生接口的功能。
接下来是将原生接口的执行结果回调给JS代码,这个比较简单,通过在参数中解析得到callback参数,将结果拼接成JS代码调用stringByEvaluatingJavaScriptFromString方法就行了。具体代码:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">excuteCallback:</span><span class="p">(</span><span class="bp">NSString</span> <span class="o">*</span><span class="p">)</span><span class="nv">callback</span> <span class="nf">argments:</span><span class="p">(</span><span class="bp">NSDictionary</span> <span class="o">*</span><span class="p">)</span><span class="nv">dic</span>
</span><span class='line'><span class="p">{</span>
</span><span class='line'> <span class="k">if</span><span class="p">(</span><span class="n">callback</span> <span class="o">==</span> <span class="nb">nil</span> <span class="o">||</span> <span class="n">callback</span><span class="p">.</span><span class="n">length</span> <span class="o"><=</span> <span class="mi">0</span><span class="p">){</span>
</span><span class='line'> <span class="k">return</span><span class="p">;</span>
</span><span class='line'> <span class="p">}</span>
</span><span class='line'> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">dic</span><span class="p">)</span> <span class="n">dic</span> <span class="o">=</span> <span class="l">@{}</span><span class="p">;</span>
</span><span class='line'> <span class="bp">NSInteger</span> <span class="n">s</span> <span class="o">=</span> <span class="p">[[</span><span class="n">dic</span> <span class="nl">objectForKey</span><span class="p">:</span><span class="n">kSequence</span><span class="p">]</span><span class="n">integerValue</span><span class="p">];</span>
</span><span class='line'> <span class="c1">// dic to string</span>
</span><span class='line'> <span class="bp">NSString</span> <span class="o">*</span><span class="n">arguments</span> <span class="o">=</span> <span class="p">[</span><span class="nb">self</span> <span class="nl">serializeCallbackArgumentWithObject</span><span class="p">:</span><span class="n">dic</span><span class="p">];</span>
</span><span class='line'> <span class="bp">NSString</span> <span class="o">*</span><span class="n">jsFunction</span> <span class="o">=</span> <span class="p">[</span><span class="bp">NSString</span> <span class="nl">stringWithFormat</span><span class="p">:</span><span class="s">@"%@(%lu,%@)"</span><span class="p">,</span><span class="n">callback</span><span class="p">,</span><span class="n">s</span><span class="p">,</span><span class="n">arguments</span><span class="p">];</span>
</span><span class='line'>
</span><span class='line'> <span class="p">[</span><span class="nb">self</span><span class="p">.</span><span class="n">webView</span> <span class="nl">stringByEvaluatingJavaScriptFromString</span><span class="p">:</span><span class="n">jsFunction</span><span class="p">];</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'>
</span><span class='line'><span class="p">-</span> <span class="p">(</span><span class="bp">NSString</span> <span class="o">*</span><span class="p">)</span><span class="nf">serializeCallbackArgumentWithObject:</span><span class="p">(</span><span class="bp">NSDictionary</span> <span class="o">*</span><span class="p">)</span><span class="nv">object</span><span class="p">{</span>
</span><span class='line'> <span class="bp">NSData</span> <span class="o">*</span><span class="n">data</span> <span class="o">=</span> <span class="p">[</span><span class="bp">NSJSONSerialization</span> <span class="nl">dataWithJSONObject</span><span class="p">:</span><span class="n">object</span> <span class="nl">options</span><span class="p">:</span><span class="mi">0</span> <span class="nl">error</span><span class="p">:</span><span class="nb">nil</span><span class="p">];</span>
</span><span class='line'> <span class="bp">NSString</span> <span class="o">*</span><span class="n">str</span> <span class="o">=</span> <span class="p">[[</span><span class="bp">NSString</span> <span class="n">alloc</span><span class="p">]</span> <span class="nl">initWithData</span><span class="p">:</span><span class="n">data</span> <span class="nl">encoding</span><span class="p">:</span><span class="n">NSUTF8StringEncoding</span><span class="p">];</span>
</span><span class='line'> <span class="k">return</span> <span class="n">str</span><span class="p">;</span>
</span><span class='line'><span class="p">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>完整的代码可以在这里<a href="https://github.com/bupojung/BPJSNativeInteract">下载</a></p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[iOS无障碍支持总结——VoiceOver]]></title>
<link href="http://bupojung.github.io/blog/2015/12/28/ioswu-zhang-ai-zhi-chi-zong-jie-voiceover/"/>
<updated>2015-12-28T23:08:23+08:00</updated>
<id>http://bupojung.github.io/blog/2015/12/28/ioswu-zhang-ai-zhi-chi-zong-jie-voiceover</id>
<content type="html"><![CDATA[<p>最近参加的一个项目需要完全支持无障碍,在iOS中既为VoiceOver,用于支持视觉有障碍的人士使用iOS上的移动应用,提供语音反馈实现无障碍的操作体验。在iOS中使用UIAccessibility API支持VoiceOver。 在支持无障碍体验的开发中涉及到几种情况:</p>
<ul>
<li>标准控件(StandardControls)即UIButton、UITableView、UISegmentedControld等;</li>
<li>UIView和继承自UIView的自定义控件;</li>
<li>除去前面两种情况的需要相应VoiceOver的界面区域,比如通过绘制在UIView上的图形,或者文字区域,既不是标准控件也不是UIView。</li>
</ul>
<!-- more -->
<p>下面分几种情况介绍:</p>
<h5>1. 标准控件:</h5>
<p>标准控件默认支持VoiceOver,不需要做太多的设置,只需要通过UIAccessibility接口设置对应的属性就可以达到大部分的无障碍支持需求,用的比较多的属性有:
accessibilityLabel:此属性用于描述控件的功能或内容,当用户触摸到控件区域,就会播放相应的描述内 容;
accessibilityHint:用于描述控件激活后的动作,一般是一个动词开头的短语,例如:对于UIButton,当用户双击时将播放此属性的描述内容;
accessibilityElementsHidden:是一个BOOL属性,用于隐藏控件,使控件不响应VoiceOver触摸,设置控件的hidden属性只能隐藏控件的可视区域,但是对VoiceOver是无效的,如果要使控件不响应VoiceOver,将此属性设置为YES. accessibilityTraits:UIAccessibilityTraits,用于描述控件的特性,是一个组合属性,即可以通过多个UIAccessibilityTraits或操作组合属性。对于标准控件,都有其默认值描述对应控件的特征,而自定义控件的默认值为UIAccessibilityTraitNone,举个例子,UIButton该属性默认值为UIAccessibilityTraitButton,当用户触摸时将会播放accessibilityLabel的描述内容+“按钮”,如果按钮被disable,则应该像如下设置此属性:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="n">btn</span><span class="p">.</span> <span class="n">accessibilityTraits</span> <span class="o">=</span> <span class="n">UIAccessibilityTraitButton</span> <span class="o">|</span> <span class="n">UIAccessibilityTraitNotEnabled</span>
</span></code></pre></td></tr></table></div></figure>
<p>当触摸到该按钮是将播放“xxx按钮,变灰”。 另一个例子,比如一个UILabel显示秒表,将每秒更新label显示的内容,则可以如下设置其traits:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="n">label</span><span class="p">.</span><span class="n">accessibilityTraits</span> <span class="o">=</span> <span class="n">UIAccessibilityTraitUpdatesFrequently</span><span class="p">;</span>
</span></code></pre></td></tr></table></div></figure>
<p>这样当lable处于选中状态时,如果更新accessibilityLabel,系统将定时播放label内容。 以上是UIKit标准控件进行无障碍支持时常用到的一些属性,这些属性在自定控件中也同样可用。</p>
<h5>2. 自定义控件(UIView or SubClass of UIView):</h5>
<p>默认情况下自定义控件是不支持VoiceOver的。可以通过两种方式使其支持VoiceOver: 1)实现isAccessibilityElement协议,在UIView子类中实现此协议返回YES;
2)直接设置isAccessibilityElement属性,将其设置为YES; 自定义其他属性和标准控件一样,就不需赘述,以下说说需要注意的几点:
1).父View如果设置isAccessibilityElement为YES,则其子View将都不能相应VoiceOver,如果如果需要两个View都支持VoiceOver,只能改变他们的层次结构,并都设置为isAccessibilityElement.
2).如果想让一个view独占响应voiceover,可以将isAccessibilityViewIsModel设置为YES,则其siblings 将忽略VoiceOver事件。
3).在某些情况下,希望控件在VoiceOver模式下直接处理touch事件,比如,画图功能,画布需要处理触摸事件,而在voiceOver开启的情况下,touch事件会被block,这时就需要设置画布的accessibilityTraits:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="n">paintCanvas</span><span class="p">.</span><span class="n">accessibilityTraits</span> <span class="o">|=</span> <span class="n">UIAccessibilityTraitAllowsDirectInteraction</span><span class="p">;</span>
</span></code></pre></td></tr></table></div></figure>
<h5>3. 其他情况:</h5>
<p>在自定义view中有时包含了一些非标准控件也非UIView子类的可触摸UI原素,比如通过draw方法画出来的区域,则以上的两种情况都不能实现无障碍体验,这种情况下,则需要实现UIAccessibilityContainer Protocol来实现.UIAccessibilityContainer Protocol是非正式协议,只需要实现以下接口:
1)定义一个NSMutableArray属性,用于保存所有的accessible Elements. @property(nonatomic, strong) NSMutableArray *accessibleElements;
2)- (NSInteger)accessibilityElementCount方法,返回Element个数;
3)- (id)accessibilityElementAtIndex:(NSInteger)index;返回对应index下的UIAccessibilityElement;
4)实现- (bool)isAccessibilityElement;返回NO,正如之前提到,父View如果为AccessibilityElement,子element将不响应voiceover,所以这里要返回NO; 在生成每个区域的UIAccessibleElement需要定义element关联的frame,,其它的和标准控件一致,举例子:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="bp">CGRect</span> <span class="n">frame</span> <span class="o">=</span> <span class="p">[</span><span class="nb">self</span> <span class="nl">convertRect</span><span class="p">:</span><span class="nb">self</span><span class="p">.</span><span class="nl">accessibilityFramefromView</span><span class="p">:</span><span class="nb">nil</span><span class="p">];</span>
</span><span class='line'><span class="bp">UIAccessibilityElement</span> <span class="o">*</span><span class="n">counterElement</span> <span class="o">=</span> <span class="p">[[</span><span class="bp">UIAccessibilityElement</span> <span class="n">alloc</span><span class="p">]</span>
</span><span class='line'><span class="bp">CGRect</span> <span class="n">textFrame</span> <span class="o">=</span> <span class="n">CGRectInset</span><span class="p">(</span><span class="n">frame</span><span class="p">,</span> <span class="n">UYLCOUNTERVIEW_MARGIN</span> <span class="o">+</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">startButton</span><span class="p">.</span><span class="n">bounds</span><span class="p">.</span><span class="n">size</span><span class="p">.</span><span class="n">width</span> <span class="o">+</span>
</span><span class='line'><span class="n">UYLCOUNTERVIEW_MARGIN</span><span class="p">,</span>
</span><span class='line'><span class="n">UYLCOUNTERVIEW_MARGIN</span><span class="p">);</span>
</span><span class='line'><span class="n">counterElement</span><span class="p">.</span><span class="n">accessibilityFrame</span> <span class="o">=</span> <span class="p">[</span><span class="nb">self</span> <span class="nl">convertRect</span><span class="p">:</span><span class="n">textFrame</span> <span class="nl">toView</span><span class="p">:</span><span class="nb">nil</span><span class="p">];</span>
</span><span class='line'><span class="n">counterElement</span><span class="p">.</span><span class="n">accessibilityLabel</span> <span class="o">=</span> <span class="n">NSLocalizedString</span><span class="p">(</span><span class="s">@"Duration"</span><span class="p">,</span> <span class="nb">nil</span><span class="p">);</span>
</span><span class='line'><span class="n">counterElement</span><span class="p">.</span><span class="n">accessibilityValue</span> <span class="o">=</span> <span class="p">[[</span><span class="bp">NSNumber</span>
</span><span class='line'><span class="nl">numberWithInteger</span><span class="p">:</span><span class="nb">self</span><span class="p">.</span><span class="n">secondsCounter</span><span class="p">]</span>
</span><span class='line'><span class="n">stringValueAsTime</span><span class="p">];</span>
</span><span class='line'><span class="n">counterElement</span><span class="p">.</span><span class="n">accessibilityTraits</span> <span class="o">=</span> <span class="n">UIAccessibilityTraitUpdatesFrequently</span><span class="p">;</span>
</span><span class='line'><span class="p">[</span><span class="n">_accessibleElements</span> <span class="nl">addObject</span><span class="p">:</span><span class="n">counterElement</span><span class="p">];</span>
</span></code></pre></td></tr></table></div></figure>
<p>在界面发生变化时要更新accessibilityElement对应的frame.</p>
<h5>4. 通知:</h5>
<p>1)在进入界面是将VoiceOver焦点定位到特定控件。可以使用UIAccessibilityPostNotification发送通知给特定控件,改变voiceOver焦点:( call UIAccessibilityPostNotification using both the notification UIAccessibilityScreenChangedNotification (which tells VoiceOver that the contents of the screen has changed) and the element you’d like to give focus to)</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="k">@implementation</span> <span class="nc">MyViewController</span>
</span><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">viewDidAppear:</span><span class="p">(</span><span class="kt">BOOL</span><span class="p">)</span><span class="nv">animated</span>
</span><span class='line'><span class="p">{</span>
</span><span class='line'><span class="p">[</span><span class="nb">super</span> <span class="nl">viewDidAppear</span><span class="p">:</span><span class="n">animated</span><span class="p">];</span>
</span><span class='line'>
</span><span class='line'><span class="n">UIAccessibilityPostNotification</span><span class="p">(</span><span class="n">UIAccessibilityScreenChangedNotification</span><span class="p">,</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">myFirstElement</span><span class="p">);</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'><span class="k">@end</span>
</span></code></pre></td></tr></table></div></figure>
<p>2)直接播放一段语音。</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objectivec'><span class='line'><span class="n">UIAccessibilityPostNotification</span><span class="p">(</span><span class="n">UIAccessibilityAnnouncementNotification</span><span class="p">,</span> <span class="s">@"something to read aloud"</span><span class="p">);</span>
</span></code></pre></td></tr></table></div></figure>
<h5>参考:</h5>
<p>[1] voiceover-accessibility
[2] voiceover官方文档:AccessibilityfromtheViewControllersPerspective
[3] voiceover introduction</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[iBeacon原理和应用]]></title>
<link href="http://bupojung.github.io/blog/2015/12/04/ibeaconyuan-li-he-ying-yong/"/>
<updated>2015-12-04T17:01:05+08:00</updated>
<id>http://bupojung.github.io/blog/2015/12/04/ibeaconyuan-li-he-ying-yong</id>
<content type="html"><![CDATA[<p><img src="http://bupojung.github.io/static/2015/12/05/ibeaconLogo" alt="Alt text" /></p>
<h2>简介</h2>
<blockquote><p>iBeacon是苹果在iOS 7.0 引入的一项新的位置感知特性,其工作方式是,配备有 低功耗蓝牙(<strong>Bluetooth Low Energy </strong>,BLE)通信功能的设备使用BLE技术向周围发送自己特有的ID,接收到该ID的应用软件会根据接收到的ID和信号源产生互动。不同功率的BLE设备的信号范围在10m~100m不等,当手机进入信号范围时,能接收到信号源广播的ID(UUID+Major+Minor), App通过解析接收到的ID向用户推送通知或者其他资讯。</p></blockquote>
<!-- more -->
<h2>原理</h2>
<h4>BLE</h4>
<p>Bluetooth Low Energy(BLE)是2010年发布的蓝牙4.0技术规范的一部分。BLE最重要的特点当然在于它的低能耗。传统蓝牙和LE蓝牙使用的都是相同的波段(2.4GHz-2.4835GHz)。BLE协议的传输速率比较低,因此除了用于发现设备和做一些简单通信之外,不太适合用于传输大量的数据流。在协议条款上,LE和传统蓝牙的信号都能够覆盖到100米的范围。</p>
<h4>iBeacon广播信息UUID+Major+Minor</h4>
<p>每个iBeacon设备都有一个唯一ID(UUID+Major+Minor),在一个区域内广播信号,通过信号中的ID信息标记一个特定区域。
| Filed | size | Description |
| :——– | ——–:| :–: |
| UUID | 16 bytes| 用于将你的iBeacon和别的iBeacon区别的唯一标识 |
| Major | 2 bytes| 用于将相关的iBeacon标记为一组 |
| Minor | 2 bytes| 标记特定的一个iBeacon |
举个例子,如果你有一家全国连锁百货公司,你可以在你的店里部署iBeacon,当用户进入iBeacon范围内的时候提供一定的优惠资讯,那么你所有的 iBeacon 发射器都可有同一个 UUID ,但每个店都有它自己的Major值,而里面的每个部门就会有它自己的Minor值。如下表所示:
<img src="http://bupojung.github.io/static/2015/12/05/ibeaconUUID" alt="Alt text" /></p>
<h2>应用</h2>
<blockquote><p>iBeacon作为一种位置感知技术,现有的应用主要有两个方面:1. 通过检测是否进入iBeacon区域向用户进行消息推送;2. 通过利用信号强度以及部署的基站信息进行室内定位。</p>
<h4>利用 iBeacon 进行消息推送</h4>
<p>苹果在iOS 7 及以上为iBeacon提供了系统级的支持,如果在iOS App中监听了某一个特定的iBeacon,当用户持手机进入该iBeacon信号范围内时,应用会被唤醒。我们所说的利用iBeacon进行消息推送,其实是需要App配合的。</p></blockquote>
<p><strong>目前推送的实现逻辑有几种:</strong></p>
<ul>
<li><p>用户进入 iBeacon 覆盖范围 -> 应用被唤醒 -> 应用请求云端数据 -> 应用发送 Local Notification 向用户推送内容。</p>
<blockquote><p>优点:推送内容实时性,自定义程度高
缺点:网络条件要求高</p></blockquote></li>
<li><p>用户进入 iBeacon 覆盖范围 -> 应用被唤醒 -> 应用查看本地缓存推送内容 -> 应用发送 Local Notification 向用户推送内容。</p>
<blockquote><p>优点: 不需要网络
缺点:推送内容实时性较差</p></blockquote></li>
<li><p>用户进入 iBeacon 覆盖范围 -> 打开应用-> 应用主动搜索周边的iBeacon信号-> 根据收到的iBeacon向用户托送信息。</p>
<blockquote><p>优点:可以对多个iBeacon进行处理
缺点: 需要主动触发</p>
<h4>室内定位</h4>
<p>iBeacon 的信号强度采用 RSSI(Received Signal Strength Indication接收的信号强度指示) 值表示。与其他无线信号一样,随着距离的远近, RSSI 值会产生 变化。我么可以通过 RSSI 值的变化来判断用户距离 iBeacon 设备的远近。而设备距离 iBeacon 的距离,在 iOS SDK 中可直接通过 iBeacon 对应的 Accuracy 值读出,单位为米。</p></blockquote></li>
</ul>
<p>但由于信号的波动,以及物理空间复杂的环境因素。iBeacon 的距离测算并不是十分精准,所 以 Apple 定义了四种范围值:</p>
<ul>
<li>Immediate 很近,小于1米。</li>
<li>Near 附近,约1⽶-3米。</li>
<li>Far 较远。</li>
<li>Unknown 未知,⼀般出现在启动阶段,或者因为某些原因⽆无法判断。
这里的Far,设备不一定距离iBeacon真的很远,很可能距离iBeacon很近(比如1米),但是没有足够的信息证明设备距离iBeacon很近。所以Far只是表示一个范围,设备可能在范围内的任一点。
<img src="http://bupojung.github.io/static/2015/12/05/ibeaconPhone" alt="enter image description here" /></li>
</ul>
<blockquote><p><strong>Tips</strong>:iBeacon是一种位置感知技术,要用于定位需要知道iBeacon基站的物理位置部署信息,并通过复杂的算法计算用户位置。</p></blockquote>
<p><strong>定位方案</strong>
* 单点定位
当时别到有在near(1m内)范围内的iBeacon基站时就是用该基站的位置坐标作为用户当前坐标。可以通过部署密集的iBeacon设备达到较高的定位精度。比如在博物馆内不同展区部署iBeacon达到室内定位的作用。
* 两点定位
根据两点确定一条双曲线的原理,通过计算识别到的两个ibeacon设备的RSSI信号计算用户的位置。一般适合在相对狭长的的物理空间内定位,通过物理空间的局限,配合RSSI值计算可以达到较好的定位效果。
* 多点定位
和两点定位类似,通过RSSI值计算到三个iBeacon设备的距离,计算用户位置。此种方式算法比较复杂,适合在空旷的场所部署。</p>
<h3>iOS API使用</h3>
<p><strong>iOS系统提供两种处理iBeacon的能力:</strong>
* Beacon区域的监控
* 估算到iBeacon的距离</p>
<h4>区域监控</h4>
<p>iOS把对iBeacon的支持集成在CoreLocation框架下,使用CLLocationManager类检测iBeacon信号,CLBeaconRegion描述一个iBeacon基站区域。</p>
<p><img src="http://bupojung.github.io/static/2015/12/05/ibeaconClass" alt="enter image description here" /></p>
<h5>sampleCode: 检测一个Region</h5>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
<span class='line-number'>3</span>
<span class='line-number'>4</span>
<span class='line-number'>5</span>
<span class='line-number'>6</span>
<span class='line-number'>7</span>
<span class='line-number'>8</span>
<span class='line-number'>9</span>
<span class='line-number'>10</span>
<span class='line-number'>11</span>
<span class='line-number'>12</span>
<span class='line-number'>13</span>
<span class='line-number'>14</span>
<span class='line-number'>15</span>
<span class='line-number'>16</span>
<span class='line-number'>17</span>
<span class='line-number'>18</span>
<span class='line-number'>19</span>
<span class='line-number'>20</span>
<span class='line-number'>21</span>
<span class='line-number'>22</span>
<span class='line-number'>23</span>
<span class='line-number'>24</span>
<span class='line-number'>25</span>
<span class='line-number'>26</span>
</pre></td><td class='code'><pre><code class='objc'><span class='line'><span class="c1">//定位权限申请</span>
</span><span class='line'><span class="n">CLAuthorizationStatus</span> <span class="n">stat</span> <span class="o">=</span> <span class="p">[</span><span class="bp">CLLocationManager</span> <span class="n">authorizationStatus</span><span class="p">]</span> <span class="p">;</span>
</span><span class='line'><span class="k">if</span> <span class="p">(</span><span class="n">stat</span> <span class="o">==</span> <span class="n">kCLAuthorizationStatusDenied</span> <span class="o">||</span> <span class="n">stat</span> <span class="o">==</span> <span class="n">kCLAuthorizationStatusRestricted</span><span class="p">)</span> <span class="p">{</span>
</span><span class='line'> <span class="n">NSLog</span><span class="p">(</span><span class="s">@"not allowed!"</span><span class="p">);</span>
</span><span class='line'> <span class="k">return</span><span class="p">;</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'>
</span><span class='line'><span class="k">if</span> <span class="p">(</span><span class="n">stat</span> <span class="o">==</span> <span class="n">kCLAuthorizationStatusNotDetermined</span><span class="p">)</span> <span class="p">{</span>
</span><span class='line'> <span class="p">[</span><span class="nb">self</span><span class="p">.</span><span class="n">locationManager</span> <span class="n">requestAlwaysAuthorization</span><span class="p">];</span>
</span><span class='line'><span class="p">}</span>
</span><span class='line'>
</span><span class='line'><span class="c1">//创建一个beaconRegion</span>
</span><span class='line'><span class="bp">NSUUID</span> <span class="o">*</span><span class="n">uuid</span> <span class="o">=</span> <span class="p">[[</span><span class="bp">NSUUID</span> <span class="n">alloc</span><span class="p">]</span><span class="nl">initWithUUIDString</span><span class="p">:</span><span class="s">@"74278BDA-B644-4520-8F0C-720EAF059935"</span><span class="p">];</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">region</span> <span class="o">=</span> <span class="p">[[</span><span class="bp">CLBeaconRegion</span> <span class="n">alloc</span><span class="p">]</span> <span class="nl">initWithProximityUUID</span><span class="p">:</span><span class="n">uuid</span> <span class="nl">identifier</span><span class="p">:</span><span class="n">BeaconIdentifier</span><span class="p">];</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">region</span><span class="p">.</span><span class="n">notifyEntryStateOnDisplay</span> <span class="o">=</span> <span class="nb">YES</span><span class="p">;</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">region</span><span class="p">.</span><span class="n">notifyOnEntry</span> <span class="o">=</span> <span class="nb">YES</span><span class="p">;</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">region</span><span class="p">.</span><span class="n">notifyOnExit</span> <span class="o">=</span> <span class="nb">NO</span><span class="p">;</span>
</span><span class='line'><span class="c1">//开始监控</span>
</span><span class='line'>
</span><span class='line'>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">locationManager</span> <span class="o">=</span> <span class="p">[[</span><span class="bp">CLLocationManager</span> <span class="n">alloc</span><span class="p">]</span> <span class="n">init</span><span class="p">];</span>
</span><span class='line'><span class="nb">self</span><span class="p">.</span><span class="n">locationManager</span><span class="p">.</span><span class="n">delegate</span> <span class="o">=</span> <span class="nb">self</span><span class="p">;</span>
</span><span class='line'><span class="p">[</span><span class="nb">self</span><span class="p">.</span><span class="n">locationManager</span> <span class="nl">startMonitoringForRegion</span><span class="p">:</span><span class="nb">self</span><span class="p">.</span><span class="n">region</span><span class="p">];</span>
</span><span class='line'><span class="k">if</span> <span class="p">([</span><span class="bp">CLLocationManager</span> <span class="n">isRangingAvailable</span><span class="p">])</span> <span class="p">{</span>
</span><span class='line'> <span class="p">[</span><span class="nb">self</span><span class="p">.</span><span class="n">locationManager</span> <span class="nl">startRangingBeaconsInRegion</span><span class="p">:</span><span class="nb">self</span><span class="p">.</span><span class="n">region</span><span class="p">];</span>
</span><span class='line'><span class="p">}</span>
</span></code></pre></td></tr></table></div></figure>
<p>如果启动成功,locationmanager会开始回调region的状态,调用时机通过<strong>CLBeaconRegion对象的参数设置</strong>:</p>
<blockquote><ul>
<li>notifyEntryStateOnDisplay:当手机屏幕亮着的时候,如果用户在监控区域内会收到回调;</li>
<li>notifyOnExit:离开region收到回调;</li>
<li>notifyOnEntry:进入region收到回调;</li>
</ul>
</blockquote>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objc'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">locationManager:</span><span class="p">(</span><span class="bp">CLLocationManager</span> <span class="o">*</span><span class="p">)</span><span class="nv">manager</span> <span class="nf">didDetermineState:</span><span class="p">(</span><span class="n">CLRegionState</span><span class="p">)</span><span class="nv">state</span> <span class="nf">forRegion:</span><span class="p">(</span><span class="bp">CLRegion</span> <span class="o">*</span><span class="p">)</span><span class="nv">region</span>
</span></code></pre></td></tr></table></div></figure>
<h4>检测iBeacon距离(Ranging Beacons)</h4>
<p>当用户进入iBeacon区域后,设备可以获得更高精度的信号采样,从而可以检查在区域内的iBeacon设备信号强度和估算距离。iOS API提供了如下几个接口处理CLBeaconRegion内的iBeacon设备距离的检测:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
<span class='line-number'>2</span>
</pre></td><td class='code'><pre><code class='objc'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">startRangingBeaconsInRegion:</span><span class="p">(</span><span class="bp">CLBeaconRegion</span> <span class="o">*</span><span class="p">)</span><span class="nv">region</span><span class="err">;</span>
</span><span class='line'><span class="o">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nl">stopRangingBeaconsInRegion</span><span class="p">:(</span><span class="bp">CLBeaconRegion</span> <span class="o">*</span><span class="p">)</span><span class="n">region</span><span class="err">;</span>
</span></code></pre></td></tr></table></div></figure>
<p>推荐在接收到 CLLocationManager:didDetermineState: callback回调并确定进入某个区域后,调用startRangingBeaconInRegion:检测区域内的iBeacon距离,当用户收到离开该区域后调用stopRangingBeaconInRegion:。
调用startRangingBeaconsInRegion:后,系统会定时回调以下方法:</p>
<figure class='code'><figcaption><span></span></figcaption><div class="highlight"><table><tr><td class="gutter"><pre class="line-numbers"><span class='line-number'>1</span>
</pre></td><td class='code'><pre><code class='objc'><span class='line'><span class="p">-</span> <span class="p">(</span><span class="kt">void</span><span class="p">)</span><span class="nf">locationManager:</span><span class="p">(</span><span class="bp">CLLocationManager</span> <span class="o">*</span><span class="p">)</span><span class="nv">manager</span> <span class="nf">didRangeBeacons:</span><span class="p">(</span><span class="bp">NSArray</span> <span class="o">*</span><span class="p">)</span><span class="nv">beacons</span> <span class="nf">inRegion:</span><span class="p">(</span><span class="bp">CLBeaconRegion</span> <span class="o">*</span><span class="p">)</span><span class="nv">region</span>
</span></code></pre></td></tr></table></div></figure>
<p>其中beacons参数是在该区域内检测到得iBeacon设备信息列表,我们可以从中得到区域内多个iBeacon设备的信息。
<img src="http://bupojung.github.io/static/2015/12/05/CLBeaconClass" alt="enter image description here" /></p>
<blockquote><ul>
<li>proximity:初略估算iBeacon的距离,有几种状态,CLProximityUnknown, CLProximityImmediate, CLProximityNear, 和 CLProximityFar;</li>
<li>accuracy:估算距离,单位m;</li>
<li>rssi:信号强度;</li>
</ul>
</blockquote>
<p><img src="http://bupojung.github.io/static/2015/12/05/range" alt="enter image description here" /></p>
<h4>问题</h4>
<ul>
<li><p>如何监控多个Region?
可以监控多个Region,每个region的identifier属性不能一样,如果一样会覆盖掉相同属性的Region,通过调用 tartRangingBeaconsInRegion:注册监听的Region。</p></li>
<li><p>能否监控未提前知道UUID的iBeacon设备?
iOS没有公开的API支持这个功能,监控iBeacon区域需要先知道UUID。MacOSX可以通过解析蓝牙广播报文获得iBeacon UUID+Major+Minor.iOS不支持。</p>
<h2>有趣的应用场景</h2></li>
<li>天气助手
<img src="http://bupojung.github.io/static/2015/12/05/ex1" alt="enter image description here" />
你是否遇到过忘记看天气预报下雨出门没带雨伞?或者看了天气预报,匆匆出门还是忘带了。日本一家公司推出了一个基于iBeacon的使用工具,可以让你轻松甩掉这个烦恼。只需要下载这个APP,并且购买一个iBeacon小装置放在门口,当每天出门时不用打开APP,如果要下雨APP会自动弹出提醒你带好雨伞!</li>
<li><p>追踪行李的小应用
<img src="http://bupojung.github.io/static/2015/12/05/ex2" alt="enter image description here" />
等行李所有人都有过痛苦的经历,伸长脖子盯着自己的行李是否出来了。通过在行李箱防止iBeacon,当行李箱接近是弹出提醒,有了这个iBeacon应用,可以轻松的坐在边上等着,手机会自动感应行李的到来</p></li>
<li><p>意大利动物园部署iBeacon互动导览
<img src="http://bupojung.github.io/static/2015/12/05/ex3" alt="enter image description here" />
就可以用手机根据游览位置解锁相应导览介绍和获得游览奖励了,这完全基于iBeacon技术,它被安装爱金16万平米的园区里,可以为整个游览过程带来寓教于乐的新奇体验。APP提供了一张可以与游览者互动的电子地图,不但起到导览,而且可以在有特色的重点区域给予自动提示。</p></li>
<li><p>iBeacon还可以帮助咖啡馆这样运营
美国的一家创业公司专门针对咖啡馆推出了一款APP,通过安装在咖啡馆里的iBeacon基站提供订位,计时长和自动付款服务。很多咖啡馆会遇到来坐着不消费又蹭网的问题,这个APP建议咖啡馆干脆退出类似场地租用的服务,通过所待时长收取一定费用,消费者可以通过APP远程预定座位,收取一定数额定金,当走入咖啡馆时通过iBeacon自动感应签到提示并开始计时,离开时自动计算所待时长和从关联的信用卡扣除相应费用。</p></li>
<li><p>博物馆,shopping mall</p>
<h4>支持iBeacon的手机</h4>
<p>运行 iOS7 以上版本的手机,包括:</p></li>
<li>iPhone 4S 及以上;</li>
<li>iPad 2 及以上;</li>
<li>iPod 5;</li>
</ul>
<p>运行 Android 4.3 以上,支持蓝牙 4.0 的安卓设备,常见设备有:</p>
<ul>
<li>三星 Galaxy 系列;</li>
<li>华为荣耀3C及以上;</li>
<li>小米 2S 及以上;</li>
</ul>
<p><strong>借鉴微信的接入流程:</strong></p>
<p><img src="http://bupojung.github.io/static/2015/12/05/wechat" alt="enter image description here" /></p>
<p>如上图,实现分如下四个步骤:
第一步. 服务提供者向微信后台申请服务,微信后台生成一个IBeaconId,并将其映射到服务提供者提供的服务,再将IBeaconId告诉服务提供者;
第二步. 服务提供者把第一步拿到的IBeaconId设置到IBeacon设备上,让IBeacon设备广播该IBeaconId;
第三步. 用户在该IBeacon设备的信号范围内打开微信摇一摇周边,微信App拿到该IBeaconId;
第四步. 微信通过第三步拿到的IBeaconId,向微信后台拉取相应的服务,展示在摇出来的结果上。
第五步. 用户点击摇出来结果,在微信内嵌的浏览器上,会带上用户信息跳转到服务提供者在第一步申请服务时填的url,进入应用页面</p>
<h2>总结</h2>