-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.html
924 lines (682 loc) · 184 KB
/
index.html
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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Aichi_B7A</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta property="og:type" content="website">
<meta property="og:title" content="Aichi_B7A">
<meta property="og:url" content="https://tedaliez.github.io/index.html">
<meta property="og:site_name" content="Aichi_B7A">
<meta property="og:locale">
<meta property="article:author" content="Jian Guo">
<meta name="twitter:card" content="summary">
<link rel="alternate" href="/atom.xml" title="Aichi_B7A" type="application/atom+xml">
<link rel="icon" href="/favicon.png">
<link href="//fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="/css/style.css">
<meta name="generator" content="Hexo 5.2.0"></head>
<body>
<div id="container">
<div id="wrap">
<header id="header">
<div id="banner"></div>
<div id="header-outer" class="outer">
<div id="header-title" class="inner">
<h1 id="logo-wrap">
<a href="/" id="logo">Aichi_B7A</a>
</h1>
</div>
<div id="header-inner" class="inner">
<nav id="main-nav">
<a id="main-nav-toggle" class="nav-icon"></a>
<a class="main-nav-link" href="/">Home</a>
<a class="main-nav-link" href="/archives">Archives</a>
</nav>
<nav id="sub-nav">
<a id="nav-rss-link" class="nav-icon" href="/atom.xml" title="RSS Feed"></a>
<a id="nav-search-btn" class="nav-icon" title="Search"></a>
</nav>
<div id="search-form-wrap">
<form action="//google.com/search" method="get" accept-charset="UTF-8" class="search-form"><input type="search" name="q" class="search-form-input" placeholder="Search"><button type="submit" class="search-form-submit"></button><input type="hidden" name="sitesearch" value="https://tedaliez.github.io"></form>
</div>
</div>
</div>
</header>
<div class="outer">
<section id="main">
<article id="post-Android嵌套滚动" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/11/14/Android%E5%B5%8C%E5%A5%97%E6%BB%9A%E5%8A%A8/" class="article-date">
<time datetime="2020-11-14T01:26:07.000Z" itemprop="datePublished">2020-11-14</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/11/14/Android%E5%B5%8C%E5%A5%97%E6%BB%9A%E5%8A%A8/">Android嵌套滚动</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>Android Design包通过提供NestedScrollingChild和NestedScrollingParent来帮助开发实现嵌套滑动效果</p>
<h2 id="NestedScrollingChild与NestedScrollingParent关系简述"><a href="#NestedScrollingChild与NestedScrollingParent关系简述" class="headerlink" title="NestedScrollingChild与NestedScrollingParent关系简述"></a>NestedScrollingChild与NestedScrollingParent关系简述</h2><table>
<thead>
<tr>
<th>NestedScrollingChild</th>
<th>NestedScrollingParent</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>dispatchNestedScroll</td>
<td>onNestedScroll</td>
<td>分发嵌套滑动事件,在子View滑动处理完之后调用, unconsumed表示未被子View滚动消费的距离, consumed表示被子View消费的滚动距离</td>
</tr>
<tr>
<td>startNestedScroll</td>
<td>onStartNestedScroll</td>
<td>前者的调用会触发后者的调用,然后后者的返回值将决定后续的嵌套滑动事件是否能传递给父View,如果返回false,父View将不处理嵌套滑动事件,一般前者的返回值即后者的返回值</td>
</tr>
<tr>
<td></td>
<td>onNestedScrollAccepted</td>
<td>如果onStartNestedScroll返回true,则回调此方法</td>
</tr>
<tr>
<td>stopNestedScroll</td>
<td>onStopNestedScroll</td>
</tr>
<tr>
<td>dispatchNestedPreScroll</td>
<td>onNestedPreScroll</td>
<td>分发预嵌套滑动事件,在子View滑动处理之前调用, 通过consumed数组得到NestedScrollingParent消耗掉的滚动距离</td>
</tr>
<tr>
<td>dispatchNestedFling</td>
<td>onNestedFling</td>
<td></td>
</tr>
<tr>
<td>dispatchNestedPreFling</td>
<td>onNestedPreFling</td>
<td></td>
</tr>
<tr>
<td></td>
<td>getNestedScrollAxes</td>
<td>获得滑动方向,没有回调,为主动调用的方法</td>
</tr>
</tbody>
</table>
<h2 id="举例说明-SwipeRefreshLayout-amp-amp-RecyclerView嵌套"><a href="#举例说明-SwipeRefreshLayout-amp-amp-RecyclerView嵌套" class="headerlink" title="举例说明: SwipeRefreshLayout && RecyclerView嵌套"></a>举例说明: SwipeRefreshLayout && RecyclerView嵌套</h2><p>本例子中,SwipeRefreshLayout为NestedScrollingParent,而RecyclerView为NestedScrollingChild</p>
<p>根据触摸事件分发机制,ACTION_DOWN首先会来到<code>SwipeRefreshLayout#onInterceptTouchEvent</code>, 而SwipeRefreshLayout未做拦截,因此来到RecyclerView#onTouchEvent, 而RecyclerView#onTouchEvent针对ACTION_DOWN返回true;之后的所有事件都会传递到RecyclerView中,之后我们看下RecyclerView中对ACTION_MOVE事件的处理</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">case</span> MotionEvent.ACTION_MOVE: {</span><br><span class="line"> <span class="comment">// ... </span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {</span><br><span class="line"> dx -= mScrollConsumed[<span class="number">0</span>]; <span class="comment">// 得到parent消费滚动后的剩余y距离</span></span><br><span class="line"> dy -= mScrollConsumed[<span class="number">1</span>]; <span class="comment">// 得到parent消费滚动后的剩余x距离</span></span><br><span class="line"> vtev.offsetLocation(mScrollOffset[<span class="number">0</span>], mScrollOffset[<span class="number">1</span>]);</span><br><span class="line"> <span class="comment">// Updated the nested offsets</span></span><br><span class="line"> mNestedOffsets[<span class="number">0</span>] += mScrollOffset[<span class="number">0</span>];</span><br><span class="line"> mNestedOffsets[<span class="number">1</span>] += mScrollOffset[<span class="number">1</span>];</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mScrollState == SCROLL_STATE_DRAGGING) {</span><br><span class="line"> mLastTouchX = x - mScrollOffset[<span class="number">0</span>];</span><br><span class="line"> mLastTouchY = y - mScrollOffset[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (scrollByInternal(</span><br><span class="line"> canScrollHorizontally ? dx : <span class="number">0</span>,</span><br><span class="line"> canScrollVertically ? dy : <span class="number">0</span>,</span><br><span class="line"> vtev)) {</span><br><span class="line"> getParent().requestDisallowInterceptTouchEvent(<span class="keyword">true</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (mGapWorker != <span class="keyword">null</span> && (dx != <span class="number">0</span> || dy != <span class="number">0</span>)) {</span><br><span class="line"> mGapWorker.postFromTraversal(<span class="keyword">this</span>, dx, dy);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">} <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>关注这里的<code>dispatchNestedPreScroll</code>和<code>scrollByInternal</code>方法, <code>dispatchNestedPreScroll</code>最终会触发<code>SwipeRefreshLayout#onNestedPreScroll</code>方法,<code>scrollByInternal</code>会调用<code>dispatchNestedScroll</code>方法,最终也来到<code>SwipeRefreshLayout#onNestedScroll</code></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onNestedScroll</span><span class="params">(<span class="keyword">final</span> View target, <span class="keyword">final</span> <span class="keyword">int</span> dxConsumed, <span class="keyword">final</span> <span class="keyword">int</span> dyConsumed,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">final</span> <span class="keyword">int</span> dxUnconsumed, <span class="keyword">final</span> <span class="keyword">int</span> dyUnconsumed)</span> </span>{</span><br><span class="line"> <span class="comment">//将nestedScroll传递给Parent</span></span><br><span class="line"> dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,</span><br><span class="line"> mParentOffsetInWindow);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span> dy = dyUnconsumed + mParentOffsetInWindow[<span class="number">1</span>];</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (dy < <span class="number">0</span> && !canChildScrollUp()) { <span class="comment">//注意canChildScrollUp方法</span></span><br><span class="line"> mTotalUnconsumed += Math.abs(dy);</span><br><span class="line"></span><br><span class="line"> moveSpinner(mTotalUnconsumed); <span class="comment">//处理refreshView的滑动</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">onNestedPreScroll</span><span class="params">(View target, <span class="keyword">int</span> dx, <span class="keyword">int</span> dy, <span class="keyword">int</span>[] consumed)</span> </span>{</span><br><span class="line"> <span class="comment">//只有当refreshView已经出现在屏幕中,并且手指往上移动才会调用下面的代码</span></span><br><span class="line"> <span class="keyword">if</span> (dy > <span class="number">0</span> && mTotalUnconsumed > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (dy > mTotalUnconsumed) {</span><br><span class="line"> consumed[<span class="number">1</span>] = dy - (<span class="keyword">int</span>) mTotalUnconsumed;</span><br><span class="line"> mTotalUnconsumed = <span class="number">0</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> mTotalUnconsumed -= dy;</span><br><span class="line"> consumed[<span class="number">1</span>] = dy; <span class="comment">//消耗掉的距离, 这里回返回给RecyclerView</span></span><br><span class="line"> }</span><br><span class="line"> moveSpinner(mTotalUnconsumed);<span class="comment">//处理refreshView的滑动</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// refreshView移除屏幕</span></span><br><span class="line"> <span class="keyword">if</span> (mUsingCustomStart && dy > <span class="number">0</span> && mTotalUnconsumed == <span class="number">0</span></span><br><span class="line"> && Math.abs(dy - consumed[<span class="number">1</span>]) > <span class="number">0</span>) {</span><br><span class="line"> mCircleView.setVisibility(View.GONE);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将nestedPreScroll传递到Parent去(本文可以忽略)</span></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span>[] parentConsumed = mParentScrollConsumed;</span><br><span class="line"> <span class="keyword">if</span> (dispatchNestedPreScroll(dx - consumed[<span class="number">0</span>], dy - consumed[<span class="number">1</span>], parentConsumed, <span class="keyword">null</span>)) {</span><br><span class="line"> consumed[<span class="number">0</span>] += parentConsumed[<span class="number">0</span>];</span><br><span class="line"> consumed[<span class="number">1</span>] += parentConsumed[<span class="number">1</span>];</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>嵌套滚动时一个核心问题是当手指滑动时,这个滑动的距离由谁消费?为了解决这个问题,NestedScrollingParent引入了consumed这个概念,通过一个consumed数组的引用,可以告知上层View消费掉了多少距离,而子View则可以根据这个消费掉的距离,以及滑动的总距离,来处理自己的滑动距离</p>
<p>在本例子中,<code>手指移动的距离 = refreshView滑动的距离 + RecyclerView滑动的距离</code></p>
<h2 id="如何在代码中实现嵌套滚动效果"><a href="#如何在代码中实现嵌套滚动效果" class="headerlink" title="如何在代码中实现嵌套滚动效果"></a>如何在代码中实现嵌套滚动效果</h2><p>对一个NestedScrollingChild首先调用startNestedScroll,之后调用dispatchNestedScroll即可</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">recyclerView.startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)</span><br><span class="line"><span class="keyword">val</span> arr = IntArray(<span class="number">2</span>)</span><br><span class="line">recyclerView.dispatchNestedPreScroll(<span class="number">0</span>, offsetY, arr, <span class="literal">null</span>)</span><br><span class="line"><span class="keyword">val</span> dyUnconsumed = offsetY - arr[<span class="number">1</span>]</span><br><span class="line"><span class="keyword">if</span> (dyUnconsumed != <span class="number">0</span>) {</span><br><span class="line"> recyclerView.scrollBy(<span class="number">0</span>, dyUnconsumed)</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/11/14/Android%E5%B5%8C%E5%A5%97%E6%BB%9A%E5%8A%A8/" data-id="ckhh0ji5b0000y1x711wo8311" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/android/" rel="tag">android</a></li></ul>
</footer>
</div>
</article>
<article id="post-FFMpeg-Android开发-简单音视频同步" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/09/13/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5/" class="article-date">
<time datetime="2020-09-13T02:39:03.000Z" itemprop="datePublished">2020-09-13</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/09/13/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5/">FFMpeg-Android开发-简单音视频同步</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>本文代码可参考代码可以参考<a href="https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v2.1" target="_blank" rel="noopener">https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v2.1</a></p>
<p>我们之前实现了视频和音频的播放,但其中最大的问题是我们的音频和视频之间的播放速度没有同步,视频按照解码的速度,以最快速度进行了上屏,那么很有可能会出现视频播放完后音频还在播放的情况。这次我们就来尝试解决这个问题,正式解决问题前,我们先对一些基本概念做出介绍</p>
<h2 id="FFMpeg-中的-dts-和-pts"><a href="#FFMpeg-中的-dts-和-pts" class="headerlink" title="FFMpeg 中的 dts 和 pts"></a>FFMpeg 中的 dts 和 pts</h2><p>FFmpeg 里有两种时间戳:DTS(Decoding Time Stamp)和 PTS(Presentation Time Stamp)。前者是解码的时间,后者是显示的时间。要仔细理解这两个概念,需要先了解 FFmpeg 中的 packet 和 frame 的概念。</p>
<p>FFmpeg 中用 AVPacket 结构体来描述解码前或编码后的压缩包,用 AVFrame 结构体来描述解码后或编码前的信号帧。 对于视频来说,AVFrame 就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的 PTS。DTS 是 AVPacket 里的一个成员,表示这个压缩包应该什么时候被解码。 如果视频里各帧的编码是按输入顺序(也就是显示顺序)依次进行的,那么解码和显示时间应该是一致的。可事实上,在大多数编解码标准(如 H.264 或 HEVC)中,编码顺序和输入顺序并不一致。 于是才会需要 PTS 和 DTS 这两种不同的时间戳。</p>
<p>具体到代码中为</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">AVPacket *packet = av_packet_alloc();</span><br><span class="line">av_init_packet(packet);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// packet->dts == AV_NOPTS_VALUE</span></span><br><span class="line"><span class="keyword">while</span> (av_read_frame(format_context, packet) >= <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// do decoding ...</span></span><br><span class="line"> <span class="comment">// packet->dts != AV_NOPTS_VALUE here</span></span><br><span class="line"></span><br><span class="line"> result = avcodec_send_packet(video_codec_context, packet);</span><br><span class="line"> <span class="keyword">if</span> (result < <span class="number">0</span> && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : codec step 1 fail"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> result = avcodec_receive_frame(video_codec_context, frame);</span><br><span class="line"> <span class="keyword">if</span> (result < <span class="number">0</span> && result != AVERROR_EOF) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : codec step 2 fail"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> LOGI(<span class="string">"Frame %c (%d, size=%d) pts %d dts %d key_frame %d [codec_picture_number %d, display_picture_number %d]"</span>,</span><br><span class="line"> av_get_picture_type_char(frame->pict_type), video_codec_context->frame_number,</span><br><span class="line"> frame->pkt_size,</span><br><span class="line"> frame->pts,</span><br><span class="line"> frame->pkt_dts,</span><br><span class="line"> frame->key_frame, frame->coded_picture_number, frame->display_picture_number);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>那么为什么要区分 DTS 和 PTS 呢?这里就涉及到编解码标准中编码顺序和输入顺序不一致的问题,这里以 H.264 规范为例举一个例子</p>
<p>假设有一个解码帧序列为<code>IBP</code>,当解码器对其解码时,解码第一帧肯定是序列中的第一帧 I, 但我们思考一下第二帧,已知 B 帧的解码需要参考前一个 I 帧或 P 帧以及后面一个 P 帧,那么解码的第二帧显然不能是序列中的第二帧 B 帧,而是第三帧 P 帧,因此我们得到了下面的一个 PTS 和 DTS 关系</p>
<pre><code>PTS: 1 2 3
DTS: 1 3 2
</code></pre><p>这里我们就会发现,DTS 和 PTS 是不同的</p>
<p>DTS 主要用于视频的解码,在解码阶段使用.PTS 主要用于视频的同步和输出.在 display 的时候使用.在没有 B frame 的情况下.DTS 和 PTS 的输出顺序是一样的.</p>
<h2 id="Timebase"><a href="#Timebase" class="headerlink" title="Timebase"></a>Timebase</h2><p>在 FFMpeg 中,同时还引入了 timebase 这个概念。timebase 用来度量时间尺度,假设 timebase={1, 25}, 那么意味着时间尺度就是 1/25 秒,假设 pts=20,那么在 timebase={1, 25}的情况下。这一帧的时间为<code>(20 * 1/25)s</code></p>
<p>FFMpeg 中,不同的数据状态对应的 timebase 也不一致。例如,非压缩时的数据(即 YUV 或者其它),在 ffmpeg 中对应的结构体为 AVFrame,它的 timebase 为 AVCodecContext 的 time_base ,AVRational{1,25}。<br>压缩后的数据(对应的结构体为 AVPacket)对应的 timebase 为 AVStream 的 time_base,AVRational{1,90000}。<br>因为数据状态不同,timebase 不一样,所以我们必须转换,在 1/25 时间刻度下佔 10 格,在 1/90000 下是佔多少格。这就是 pts 的转换。</p>
<h2 id="利用-pts-实现音视频同步"><a href="#利用-pts-实现音视频同步" class="headerlink" title="利用 pts 实现音视频同步"></a>利用 pts 实现音视频同步</h2><p>当我们得到 timebase 和 pts 数据后,我们就可以通过时间换算来同步视频和音频的播放,考虑到音频的播放速度固定,最简单的做法就是将视频的播放向音频同步。</p>
<p>我们需要定义一个<code>audio_clock</code>来记录音频播放的时钟</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (index == player->audio_stream_index) {</span><br><span class="line"> player->audio_clock = packet->pts * av_q2d(stream->time_base);</span><br><span class="line"> LOGD(<span class="string">"SyncPlayer: Playing audio loop"</span>);</span><br><span class="line"> audio_play(player, frame, env);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>然后在视频播放的时候利用这个<code>audio_clock</code>进行一定的 delay</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (index == player->video_stream_index) {</span><br><span class="line"> <span class="keyword">auto</span> audio_clock = player->audio_clock;</span><br><span class="line"> <span class="keyword">double</span> timestamp;</span><br><span class="line"> <span class="keyword">if</span> (packet->pts == AV_NOPTS_VALUE) {</span><br><span class="line"> timestamp = <span class="number">0</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> timestamp = frame->best_effort_timestamp * av_q2d(stream->time_base);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">double</span> frame_rate = av_q2d(stream->avg_frame_rate); <span class="comment">// fps = 1 / stream->avg_frame_rate</span></span><br><span class="line"> frame_rate += frame->repeat_pict * (frame_rate * <span class="number">0.5</span>); <span class="comment">// repeat_dict代表当前frame必须delay的时间, extra_delay = repeat_pict / (2*fps)</span></span><br><span class="line"> <span class="keyword">if</span> (timestamp == <span class="number">0.0</span>) {</span><br><span class="line"> usleep(frame_rate * <span class="number">1000</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">fabs</span>(timestamp - audio_clock) > AV_SYNC_THRESHOLD_MIN</span><br><span class="line"> && <span class="built_in">fabs</span>(timestamp - audio_clock) < AV_NOSYNC_THRESHOLD) {</span><br><span class="line"> <span class="keyword">if</span> (timestamp > audio_clock) {</span><br><span class="line"> usleep((<span class="keyword">unsigned</span> <span class="keyword">long</span>)((timestamp - audio_clock)*<span class="number">1000000</span>));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> video_play(player, frame, env);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>至此,一个简单地音视频播放器就完成了.</p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/09/13/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5/" data-id="ckgx4rr78000ai8x7e4afq3tf" class="article-share-link">Share</a>
</footer>
</div>
</article>
<article id="post-Kotlin协程异常处理" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/09/12/Kotlin%E5%8D%8F%E7%A8%8B%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/" class="article-date">
<time datetime="2020-09-12T01:09:48.000Z" itemprop="datePublished">2020-09-12</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/09/12/Kotlin%E5%8D%8F%E7%A8%8B%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/">Kotlin协程异常处理</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>Kotlin的协程满足结构化语义:</p>
<ol>
<li><p>A parent-Coroutine finishes only after all its child-Coroutines have finished.</p>
</li>
<li><p>When a parent-Coroutine or scope finishes abnormally, either through cancelation or through an exception, all its child-Coroutines, that are still active, are canceled and new child-Coroutines can no longer be launched.</p>
</li>
<li><p>When a child-Coroutine finishes abnormally, its parent-Coroutine or scope finishes abnormally.</p>
</li>
</ol>
<p>针对第三点,无论是<code>CoroutineScope.async</code>或<code>CoroutineScope.launch</code>,以异常结束,只要是在一个scope中,他的父协程也会以异常结束。<code>async</code>和<code>launch</code>唯一不同的地方在于<code>async</code>需要调用await才会执行协程内容。即使我们对子协程进行了try-catch处理异常,父协程仍旧会拿着这个异常作为结果结束</p>
<h2 id="async的异常处理"><a href="#async的异常处理" class="headerlink" title="async的异常处理"></a>async的异常处理</h2><pre><code>不管是哪个启动器(launch, async等),在应用了作用域之后,都会按照**作用域的语义**进行异常扩散,进而触发相应的取消操作,对于 async 来说就算不调用 await 来获取这个异常,它也会在 coroutineScope 当中触发父协程的取消逻辑,这一点请大家注意。
</code></pre><p>以CoroutineScope(Dispatchers.Main)作为根作用域为例,下面展示几个case</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"test"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面的代码即使没有调用<code>foo.await()</code>, 也会扩散到<code>Thread.UncaughtExceptionHandler</code>中。这段代码的执行结果是</p>
<pre><code>D/Foo: [, , 0]:test
</code></pre><p>同时app crash</p>
<p>这里我们应该这么理解,虽然是对<code>await()</code>进行了try-catch,但这里是对执行结果的<code>try-catch</code>, 这不影响协程自己的异常传递规则,在async中的协程scope抛出异常后,此时异常是未捕获状态;因此会向父协程scope<code>coroutineScope</code>转播, <code>coroutineScope</code>继续向<code>viewModelScope</code>传播,最终来到UncaughtExceptionHandler处处理</p>
<p>那么考虑一下直接加入<code>try-catch</code>:</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> foo.await()</span><br><span class="line"> } <span class="keyword">catch</span> (e : IllegalStateException) {</span><br><span class="line"> Log.e(<span class="string">"Foo"</span>, <span class="string">"CoroutineScope caught exception: <span class="variable">$e</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"test"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>输出结果为:</p>
<pre><code>E/Foo: [, , 0]:CoroutineScope caught exception: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
</code></pre><p>同时app crash</p>
<p>这里虽然我们对await进行了try-catch,打印了异常信息,但是根据作用域规则,这里我们的异常行为发生了扩散,从子协程扩散到根协程,最终扩散到<code>Thread.UncaughtExceptionHandler</code>中,对于安卓系统而言,就是引发了crash</p>
<p>那么如果我们在扩散的scope外层进行try-catch能否解决问题呢?尝试如下的代码:</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> coroutineScope {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (e: IllegalStateException) {</span><br><span class="line"> Log.e(<span class="string">"Foo"</span>, <span class="string">"catch expection from outerScope: <span class="variable">$e</span>"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>输出结果为:</p>
<pre><code>E/Foo: [, , 0]:CoroutineScope caught exception: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
</code></pre><p>此时app 不再crash</p>
<p>如果我们在添加await()同时进行try-catch:</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> coroutineScope {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> foo.await()</span><br><span class="line"> } <span class="keyword">catch</span> (e: IllegalStateException) {</span><br><span class="line"> Log.e(<span class="string">"Foo"</span>, <span class="string">"catch expection from innerScope: <span class="variable">$e</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (e: IllegalStateException) {</span><br><span class="line"> Log.e(<span class="string">"Foo"</span>, <span class="string">"catch expection from outerScope: <span class="variable">$e</span>"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>输出结果为:</p>
<pre><code>E/Foo: [, , 0]:catch expection from innerScope: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
E/Foo: [, , 0]:catch expection from outerScope: java.lang.IllegalStateException: test
</code></pre><p>此时app 也不再crash,但我们会发现我们的两处<code>try-catch</code>均被触发</p>
<p>这也说明<strong>在协程中,异常的扩散并不遵循try-catch语法构成的作用域</strong></p>
<p>另一种方式就是换用一个不进行扩散语义的协程作用域,即使用supervisorScope</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> supervisorScope {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>这段代码的输出结果为</p>
<pre><code>D/Foo: [, , 0]:test
</code></pre><p>app 不发生crash, 同时没有IllegalStateException(“test”)的日志打印</p>
<p>这里的核心在于,我们的async是在<code>supervisorScope</code>作用域下,根据文档描述,这个作用域下的协程出现取消情况(异常抛出时)不会向外扩散</p>
<p>假如加上try-catch</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> supervisorScope {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> foo.await()</span><br><span class="line"> } <span class="keyword">catch</span> (e: IllegalStateException) {</span><br><span class="line"> Log.e(<span class="string">"Foo"</span>, <span class="string">"CoroutineScope caught exception: <span class="variable">$e</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"test"</span>)</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>我们得到结果</p>
<pre><code>E/Foo: [, , 0]:CoroutineScope caught exception: java.lang.IllegalStateException: test
D/Foo: [, , 0]:test
</code></pre><p>app 不发生crash</p>
<p>同理,我们的async是在<code>supervisorScope</code>作用域下,根据文档描述,这个作用域下的协程出现取消情况(异常抛出时)不会向外扩散,因此不会向外扩散到<code>Thread.UncaughtExceptionHandler</code>。同时,try-catch对await()发生作用,我们打印出了异常的信息</p>
<h2 id="withContext的异常处理"><a href="#withContext的异常处理" class="headerlink" title="withContext的异常处理"></a>withContext的异常处理</h2><p>那么对比一下withContext的处理我们就会发现不同</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">viewModelScope.launch {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> withContext(Dispatchers.Default) {</span><br><span class="line"> <span class="keyword">throw</span> IllegalArgumentException(<span class="string">"Test"</span>)</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>这里<code>withContext</code>直接就是一个协程scope,我们的try-catch直接作用于整个withContext构造的scope,因此异常被捕获的同时,不再向<code>viewModelScope</code>传播</p>
<h2 id="supervisorScope"><a href="#supervisorScope" class="headerlink" title="supervisorScope"></a>supervisorScope</h2><p>如何能够让子协程抛出异常的情况下,父协程不会终止所以其子协程和自己呢?这就引入了supervisorScope</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">val</span> result = supervisorScope {</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">val</span> supervisedChild1 = <span class="keyword">this</span>.launch { ... }</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">val</span> supervisedChild2 = <span class="keyword">this</span>.async { ... }</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>此时,若child1或child2任意一个抛出异常,也不会使另一个child和<code>supervising parent</code>停止</p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>最终我们得到了协程的Structured Concurrency含义:</p>
<ol>
<li><p>A parent-Coroutine finishes only after all its child-Coroutines have finished.</p>
</li>
<li><p>When a parent-Coroutine or scope finishes abnormally, either through cancelation or through an exception, all its child-Coroutines, that are still active, are canceled and new child-Coroutines can no longer be launched.</p>
</li>
<li><p>When a child-Coroutine finishes abnormally, its parent-Coroutine or scope (a) finishes abnormally if the parent is not a supervisor or (b) keeps running if the parent is a supervisor.</p>
</li>
</ol>
<h2 id="P-S-协程中的异常最佳实践"><a href="#P-S-协程中的异常最佳实践" class="headerlink" title="P.S 协程中的异常最佳实践"></a>P.S 协程中的异常最佳实践</h2><p>通过上文中对await,withContext的异常处理的方式,我们会发现在协程中处理异常其实是一件非常麻烦的事情,其异常的扩散规则并不合<code>try-catch</code>的作用域对应。因此这里简单提一下如何在实际项目中处理这种异常</p>
<p>Kotlin中对异常处理有两种推荐方式:</p>
<ol>
<li>default value</li>
<li>Wrapper Seal class</li>
</ol>
<p>第一种方式就是返回一个跟函数签名同类型的默认值,例如</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">toIntSafely</span><span class="params">(defaultValue: <span class="type">Int</span>)</span></span> : <span class="built_in">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">try</span> {</span><br><span class="line"> parseInt(<span class="keyword">this</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (e: NumberFormatException) {</span><br><span class="line"> <span class="keyword">return</span> defaultValue</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure>
<p>第二种方式则是在API设计上使用一个Wrapper Class来包装出现的异常,使用这个Wrapper Class来保证Type-Check,举例来说:</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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><br><span class="line"><span class="keyword">sealed</span> <span class="class"><span class="keyword">class</span> <span class="title">ParsedDate</span> </span>{</span><br><span class="line"> <span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">Success</span></span>(<span class="keyword">val</span> date: Date) : ParsedDate()</span><br><span class="line"> <span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">Failure</span></span>(<span class="keyword">val</span> errorOffset: <span class="built_in">Int</span>) : ParsedDate()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> DateFormat.<span class="title">tryParse</span><span class="params">(text: <span class="type">String</span>)</span></span>: ParsedDate =</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> ParsedDate.Success(parse(text))</span><br><span class="line"> } <span class="keyword">catch</span> (e: ParseException) {</span><br><span class="line"> ParsedDate.Failure(e.errorOffset)</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>那么对调用者而言,<code>tryParse</code>的结果一定是一个<code>ParsedDate</code>对象,而对异常无感知</p>
<p>结合协程使用来说,我们应该直接在一个作用域内,就返回一个default value或Wrapper Seal class,例如</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">sealed</span> <span class="class"><span class="keyword">class</span> <span class="title">ParsedDate</span> </span>{</span><br><span class="line"> <span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">Success</span></span>(<span class="keyword">val</span> date: Date) : ParsedDate()</span><br><span class="line"> <span class="keyword">data</span> <span class="class"><span class="keyword">class</span> <span class="title">Failure</span></span>(<span class="keyword">val</span> errorOffset: <span class="built_in">Int</span>) : ParsedDate()</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">CoroutineScope(Dispatchers.Main).launch {</span><br><span class="line"> <span class="keyword">val</span> foo = async {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">throw</span> IllegalStateException(<span class="string">"test"</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (e: IllegalStateException) {</span><br><span class="line"> Log.e(<span class="string">"Foo"</span>, <span class="string">"catch exception inside: <span class="variable">$e</span>"</span>)</span><br><span class="line"> ParsedDate.Failure(-<span class="number">1</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">val</span> rst = foo.await()</span><br><span class="line"> Log.d(<span class="string">"Foo"</span>, <span class="string">"return rst: <span class="subst">${rst.errorOffset}</span>"</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>输出结果:</p>
<pre><code>E/Foo: [, , 0]:catch exception inside: java.lang.IllegalStateException: test
D/Foo: [, , 0]:return rst: -1
</code></pre><p>同时app无crash,无异常日志打印</p>
<p>ref: </p>
<p><a target="_blank" rel="noopener" href="https://www.bennyhuo.com/2019/04/23/coroutine-exceptions/#4-%E5%BC%82%E5%B8%B8%E4%BC%A0%E6%92%AD">https://www.bennyhuo.com/2019/04/23/coroutine-exceptions/#4-%E5%BC%82%E5%B8%B8%E4%BC%A0%E6%92%AD</a></p>
<p><a target="_blank" rel="noopener" href="https://johnnyshieh.me/posts/kotlin-coroutine-exception-handling/">https://johnnyshieh.me/posts/kotlin-coroutine-exception-handling/</a></p>
<p><a target="_blank" rel="noopener" href="https://medium.com/@elizarov/structured-concurrency-722d765aa952">https://medium.com/@elizarov/structured-concurrency-722d765aa952</a></p>
<p><a target="_blank" rel="noopener" href="https://medium.com/the-kotlin-chronicle/coroutine-exceptions-supervision-15056802998b">https://medium.com/the-kotlin-chronicle/coroutine-exceptions-supervision-15056802998b</a></p>
<p><a target="_blank" rel="noopener" href="https://medium.com/the-kotlin-chronicle/coroutine-exceptions-3378f51a7d33">https://medium.com/the-kotlin-chronicle/coroutine-exceptions-3378f51a7d33</a></p>
<p><a target="_blank" rel="noopener" href="https://github.com/Kotlin/kotlinx.coroutines/issues/753">https://github.com/Kotlin/kotlinx.coroutines/issues/753</a></p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/09/12/Kotlin%E5%8D%8F%E7%A8%8B%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/" data-id="ckgx4rr7i000mi8x701ovf1ub" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/kotlin-%E5%8D%8F%E7%A8%8B/" rel="tag">kotlin, 协程</a></li></ul>
</footer>
</div>
</article>
<article id="post-FFMpeg-Android开发-简单播放音频" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/08/29/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E9%9F%B3%E9%A2%91/" class="article-date">
<time datetime="2020-08-29T01:04:55.000Z" itemprop="datePublished">2020-08-29</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/08/29/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E9%9F%B3%E9%A2%91/">FFMpeg-Android开发-简单播放音频</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>本文部分对应的源代码可参考<a href="https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.2" target="_blank" rel="noopener">https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.2</a></p>
<h2 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h2><p>音频播放的整个流程和视频非常相似,都经历了下面几个步骤</p>
<ol>
<li>解析 container</li>
<li>根据 container,获得我们关心的媒体数据(例如播放视频,那我们只关心视频媒体)</li>
<li>根据媒体信息获得对应的解码器</li>
<li>将对应的数据送给解码器</li>
<li>解码器解码,输出帧</li>
<li>将帧渲染到目标区域(播放音频)</li>
</ol>
<p>1-5 的步骤几乎可以参考播放视频的流程,这里简单看下不同</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">auto</span> swr_context = swr_alloc();</span><br><span class="line"><span class="keyword">auto</span> out_buffer = (<span class="keyword">uint8_t</span> *) av_malloc(<span class="number">44100</span> * <span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// expected sample output</span></span><br><span class="line"><span class="keyword">uint64_t</span> out_channel_layout = AV_CH_LAYOUT_STEREO;</span><br><span class="line"><span class="keyword">auto</span> out_format = AV_SAMPLE_FMT_S16;</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> out_sample_rate = audio_codec_context->sample_rate;</span><br><span class="line"></span><br><span class="line"><span class="comment">// expected sample out para end</span></span><br><span class="line"></span><br><span class="line">swr_alloc_set_opts(swr_context,</span><br><span class="line"> out_channel_layout, out_format, out_sample_rate,</span><br><span class="line"> audio_codec_context->channel_layout, audio_codec_context->sample_fmt, audio_codec_context->sample_rate,</span><br><span class="line"><span class="number">0</span>, <span class="literal">nullptr</span>);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">swr_init(swr_context);</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> out_channels = av_get_channel_layout_nb_channels(AV_CH_LAYOUT_STEREO);</span><br></pre></td></tr></table></figure>
<p>这一段的主要目的是配置音频的转码格式,跟视频不同,但基本逻辑一致</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> player_class = env->GetObjectClass(instance);</span><br><span class="line"><span class="keyword">auto</span> create_audio_track_method_id = env->GetMethodID(player_class, <span class="string">"createAudioTrack"</span>, <span class="string">"(II)V"</span>);</span><br><span class="line">env->CallVoidMethod(instance, create_audio_track_method_id, <span class="number">44100</span>, out_channels);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> play_audio_track_method_id = env->GetMethodID(player_class, <span class="string">"playAudioTrack"</span>, <span class="string">"([BI)V"</span>);</span><br></pre></td></tr></table></figure>
<p>这里我们通过 JNI 来调用 Java 层的 AudioTrack 相关 API 来播放音频</p>
<p>至此,我们已经完成了音频的播放;接下来就剩下如何同时播放视频和音频,以及音视频同步问题了。下篇文章,我们就会来着手实现一个时间同步的简单播放器</p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/08/29/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E9%9F%B3%E9%A2%91/" data-id="ckgx4rr770009i8x7ktjqzt3v" class="article-share-link">Share</a>
</footer>
</div>
</article>
<article id="post-FFMpeg-Android开发-简单播放视频" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/08/23/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91/" class="article-date">
<time datetime="2020-08-23T02:25:15.000Z" itemprop="datePublished">2020-08-23</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/08/23/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91/">FFMpeg-Android开发-简单播放视频</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>本文部分对应的源代码可参考<a href="https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.1" target="_blank" rel="noopener">https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.1</a></p>
<h2 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h2><p>结合上一篇文章<code>FFMpeg-Android开发-解析视频格式</code>的内容,我们知道视频解码大致分为如下几个步骤</p>
<ol>
<li>解析 container</li>
<li>根据 container,获得我们关心的媒体数据(例如播放视频,那我们只关心视频媒体)</li>
<li>根据媒体信息获得对应的解码器</li>
<li>将对应的数据送给解码器</li>
<li>解码器解码,输出帧</li>
<li>将帧渲染到目标区域</li>
</ol>
<p>那么这次我们就来尝试将视频解码并渲染到设备屏幕上,首先看我们 1-3 步的代码</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// Record result</span></span><br><span class="line"><span class="keyword">int</span> result;</span><br><span class="line"><span class="comment">// R1 Java String -> C String</span></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">char</span> *path = env->GetStringUTFChars(path_, <span class="number">0</span>);</span><br><span class="line"><span class="comment">// Register FFmpeg components</span></span><br><span class="line">av_register_all();</span><br><span class="line"><span class="comment">// R2 initializes the AVFormatContext context</span></span><br><span class="line">AVFormatContext *format_context = avformat_alloc_context();</span><br><span class="line"><span class="comment">// Open Video File</span></span><br><span class="line">result = avformat_open_input(&format_context, path, <span class="literal">NULL</span>, <span class="literal">NULL</span>);</span><br><span class="line"><span class="keyword">if</span> (result < <span class="number">0</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not open video file"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// Finding Stream Information of Video Files</span></span><br><span class="line">result = avformat_find_stream_info(format_context, <span class="literal">NULL</span>);</span><br><span class="line"><span class="keyword">if</span> (result < <span class="number">0</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not find video file stream info"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// Find Video Encoder</span></span><br><span class="line"><span class="keyword">int</span> video_stream_index = <span class="number">-1</span>;</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < format_context->nb_streams; i++) {</span><br><span class="line"> <span class="comment">// Matching Video Stream</span></span><br><span class="line"> <span class="keyword">if</span> (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {</span><br><span class="line"> video_stream_index = i;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="comment">// No video stream found</span></span><br><span class="line"><span class="keyword">if</span> (video_stream_index == <span class="number">-1</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not find video stream"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// Initialization of Video Encoder Context</span></span><br><span class="line">AVCodecContext *video_codec_context = avcodec_alloc_context3(<span class="literal">NULL</span>);</span><br><span class="line">avcodec_parameters_to_context(video_codec_context, format_context->streams[video_stream_index]->codecpar);</span><br><span class="line"><span class="comment">// Initialization of Video Encoder</span></span><br><span class="line">AVCodec *video_codec = avcodec_find_decoder(video_codec_context->codec_id);</span><br><span class="line"><span class="keyword">if</span> (video_codec == <span class="literal">NULL</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not find video codec"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// R3 Opens Video Decoder</span></span><br><span class="line">result = avcodec_open2(video_codec_context, video_codec, <span class="literal">NULL</span>);</span><br><span class="line"><span class="keyword">if</span> (result < <span class="number">0</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not find video stream"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// Getting the Width and Height of Video</span></span><br><span class="line"><span class="keyword">int</span> videoWidth = video_codec_context->width;</span><br><span class="line"><span class="keyword">int</span> videoHeight = video_codec_context->height;</span><br></pre></td></tr></table></figure>
<p>其中 video_codec 代表解码器对象,其他对象在上一篇文章和代码注释中有相关解释。</p>
<p>接下来的步骤就是解码和上屏,这里我们先看下我们的上屏是如何实现的;这里为了方便,我直接使用 SurfaceView 构建我们的上层 View</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> surfaceHolder = surfaceView.holder</span><br><span class="line"> surfaceHolder!!.setFormat(PixelFormat.RGBA_8888)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">playVideo</span><span class="params">(path: <span class="type">String</span>)</span></span> {</span><br><span class="line"> Thread {</span><br><span class="line"> mPlayer.playVideo(path, surfaceHolder!!.surface)</span><br><span class="line"> }.start()</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// R4 Initializes Native Window s for Playing Videos</span></span><br><span class="line">ANativeWindow *native_window = ANativeWindow_fromSurface(env, surface); <span class="comment">// surface对应java层的surface对象</span></span><br><span class="line"><span class="keyword">if</span> (native_window == <span class="literal">NULL</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not create native window"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// Limit the number of pixels in the buffer by setting the width, not the physical display size of the screen.</span></span><br><span class="line"><span class="comment">// If the buffer does not match the display size of the physical screen, the actual display may be stretched or compressed images.</span></span><br><span class="line">result = ANativeWindow_setBuffersGeometry(native_window, videoWidth, videoHeight,WINDOW_FORMAT_RGBA_8888);</span><br><span class="line"><span class="keyword">if</span> (result < <span class="number">0</span>){</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not set native window buffer"</span>);</span><br><span class="line"> ANativeWindow_release(native_window);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// Define drawing buffer</span></span><br><span class="line">ANativeWindow_Buffer window_buffer;</span><br><span class="line"><span class="comment">// There are three declarative data containers</span></span><br><span class="line"><span class="comment">// Data container Packet encoding data before R5 decoding</span></span><br><span class="line">AVPacket *packet = av_packet_alloc();</span><br><span class="line">av_init_packet(packet);</span><br><span class="line"><span class="comment">// Frame Pixel Data of Data Container After R6 Decoding Can't Play Pixel Data Directly and Need Conversion</span></span><br><span class="line">AVFrame *frame = av_frame_alloc();</span><br><span class="line"><span class="comment">// R7 converted data container where the data can be used for playback</span></span><br><span class="line">AVFrame *rgba_frame = av_frame_alloc();</span><br><span class="line"><span class="comment">// Data format conversion preparation</span></span><br><span class="line"><span class="comment">// Output Buffer</span></span><br><span class="line"><span class="keyword">int</span> buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, videoWidth, videoHeight, <span class="number">1</span>);</span><br><span class="line"><span class="comment">// R8 Application for Buffer Memory</span></span><br><span class="line"><span class="keyword">uint8_t</span> *out_buffer = (<span class="keyword">uint8_t</span> *) av_malloc(buffer_size * <span class="keyword">sizeof</span>(<span class="keyword">uint8_t</span>));</span><br><span class="line">LOGI(<span class="string">"outBuffer size: %d, videoWidth: %d, videoHeight: %d, pix_fmt: %d"</span>, buffer_size * <span class="keyword">sizeof</span>(<span class="keyword">uint8_t</span>), videoWidth, videoHeight, video_codec_context->pix_fmt);</span><br><span class="line">av_image_fill_arrays(rgba_frame->data, rgba_frame->linesize, out_buffer, AV_PIX_FMT_RGBA, videoWidth, videoHeight, <span class="number">1</span>);</span><br><span class="line"><span class="comment">// R9 Data Format Conversion Context</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">SwsContext</span> *<span class="title">data_convert_context</span> = <span class="title">sws_getContext</span>(</span></span><br><span class="line"><span class="class"> <span class="title">videoWidth</span>, <span class="title">videoHeight</span>, <span class="title">video_codec_context</span>-><span class="title">pix_fmt</span>,</span></span><br><span class="line"><span class="class"> <span class="title">videoWidth</span>, <span class="title">videoHeight</span>, <span class="title">AV_PIX_FMT_RGBA</span>,</span></span><br><span class="line"><span class="class"> <span class="title">SWS_BICUBIC</span>, <span class="title">NULL</span>, <span class="title">NULL</span>, <span class="title">NULL</span>);</span></span><br><span class="line"><span class="comment">// Start reading frames</span></span><br></pre></td></tr></table></figure>
<p>这里 ANative 相关代码都是 Android NDK 中的 api,可参考文档理解;这里主要看一下这里的 AVPacket 和 AVFrame 的使用</p>
<p>AVPacket 在上一篇文章中有过介绍,它在 FFMpeg 中用来表示未解码时的数据;而 AVFrame 则表示了解码后的帧数据。</p>
<p>但这里有一个小问题是我们解码后的图像格式可能是和我们的 surface 的渲染格式不同,这里我们的 surface 渲染格式是<code>RGBA8888</code>,而视频的图像格式不一定为这个格式。为了实现转换,我们就需要<code>SwsContext</code>对象来帮助我们实现图像格式的转换。</p>
<p>看完上面后,终于来到了我们的解码 runloop</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (av_read_frame(format_context, packet) >= <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// Matching Video Stream</span></span><br><span class="line"> <span class="keyword">if</span> (packet->stream_index == video_stream_index) {</span><br><span class="line"> <span class="comment">// Decode video</span></span><br><span class="line"> result = avcodec_send_packet(video_codec_context, packet);</span><br><span class="line"> <span class="keyword">if</span> (result < <span class="number">0</span> && result != AVERROR(EAGAIN) && result != AVERROR_EOF) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : codec step 1 fail"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> result = avcodec_receive_frame(video_codec_context, frame);</span><br><span class="line"> <span class="keyword">if</span> (result < <span class="number">0</span> && result != AVERROR_EOF) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : codec step 2 fail"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> LOGI(<span class="string">"Frame %c (%d, size=%d) pts %d dts %d key_frame %d [codec_picture_number %d, display_picture_number %d]"</span>,</span><br><span class="line"> av_get_picture_type_char(frame->pict_type), video_codec_context->frame_number,</span><br><span class="line"> frame->pkt_size,</span><br><span class="line"> frame->pts,</span><br><span class="line"> frame->pkt_dts,</span><br><span class="line"> frame->key_frame, frame->coded_picture_number, frame->display_picture_number);</span><br><span class="line"> <span class="comment">// Data Format Conversion</span></span><br><span class="line"> result = sws_scale(</span><br><span class="line"> data_convert_context,</span><br><span class="line"> frame->data, frame->linesize,</span><br><span class="line"> <span class="number">0</span>, videoHeight,</span><br><span class="line"> rgba_frame->data, rgba_frame->linesize);</span><br><span class="line"> <span class="comment">// play</span></span><br><span class="line"> result = ANativeWindow_lock(native_window, &window_buffer, <span class="literal">NULL</span>);</span><br><span class="line"> <span class="keyword">if</span> (result < <span class="number">0</span>) {</span><br><span class="line"> LOGE(<span class="string">"Player Error : Can not lock native window"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Draw the image onto the interface</span></span><br><span class="line"> <span class="comment">// Note: The pixel lengths of rgba_frame row and window_buffer row may not be the same here.</span></span><br><span class="line"> <span class="comment">// Need to convert well or maybe screen</span></span><br><span class="line"> <span class="keyword">uint8_t</span> *bits = (<span class="keyword">uint8_t</span> *) window_buffer.bits;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> h = <span class="number">0</span>; h < videoHeight; h++) {</span><br><span class="line"> <span class="built_in">memcpy</span>(bits + h * window_buffer.stride * <span class="number">4</span>,</span><br><span class="line"> out_buffer + h * rgba_frame->linesize[<span class="number">0</span>],</span><br><span class="line"> rgba_frame->linesize[<span class="number">0</span>]);</span><br><span class="line"> }</span><br><span class="line"> ANativeWindow_unlockAndPost(native_window);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Release package references</span></span><br><span class="line"> av_packet_unref(packet);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>整个解码循环也是大致分为如下几个步骤</p>
<ol>
<li>通过 AVPacket 读取一块数据</li>
<li>将 AVPacket 送给解码器使用</li>
<li>通过 AVFrame 得到 2 中的解码后数据</li>
<li>对 AVFrame 的图像格式进行转换</li>
<li>将 4 中的结果上屏</li>
</ol>
<p>其他的注释在代码中有说明</p>
<p>最后在 run-loop 外完成资源回收</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Release R9</span></span><br><span class="line">sws_freeContext(data_convert_context);</span><br><span class="line"><span class="comment">// Release R8</span></span><br><span class="line">av_free(out_buffer);</span><br><span class="line"><span class="comment">// Release R7</span></span><br><span class="line">av_frame_free(&rgba_frame);</span><br><span class="line"><span class="comment">// Release R6</span></span><br><span class="line">av_frame_free(&frame);</span><br><span class="line"><span class="comment">// Release R5</span></span><br><span class="line">av_packet_free(&packet);</span><br><span class="line"><span class="comment">// Release R4</span></span><br><span class="line">ANativeWindow_release(native_window);</span><br><span class="line"><span class="comment">// Close R3</span></span><br><span class="line">avcodec_close(video_codec_context);</span><br><span class="line"><span class="comment">// Release R2</span></span><br><span class="line">avformat_close_input(&format_context);</span><br><span class="line"><span class="comment">// Release R1</span></span><br><span class="line">env->ReleaseStringUTFChars(path_, path);</span><br></pre></td></tr></table></figure>
<p>此时,我们就完成了整个视频的播放,不过这个播放器显然是处在不可用的状态,主要有下面两个问题</p>
<ol>
<li>没有声音</li>
<li>视频的播放是按照解码器的解码速度播放的;只要解码器足够快,视频播放就有多快,这个显然是不符合播放器播放视频的预期的</li>
</ol>
<p>问题 2 本质就是我们常说的音视频同步问题;我们后面会简单介绍音频的解码播放后,通过引入 pts, dts 等概念后,尝试解决问题 2</p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/08/23/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91/" data-id="ckgx4rr6z0006i8x75s4o09bk" class="article-share-link">Share</a>
</footer>
</div>
</article>
<article id="post-FFMpeg-Android开发-解析视频格式" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/08/22/FFMpeg-Android%E5%BC%80%E5%8F%91-%E8%A7%A3%E6%9E%90%E8%A7%86%E9%A2%91%E6%A0%BC%E5%BC%8F/" class="article-date">
<time datetime="2020-08-22T01:07:51.000Z" itemprop="datePublished">2020-08-22</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/08/22/FFMpeg-Android%E5%BC%80%E5%8F%91-%E8%A7%A3%E6%9E%90%E8%A7%86%E9%A2%91%E6%A0%BC%E5%BC%8F/">FFMpeg-Android开发-解析视频格式</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>在正式使用FFMpeg完成视频的播放之前,我想先写一篇文章,简单介绍一下如何使用FFMpeg去获取一个视频的基本信息。通过这篇文章,来简单介绍一下FFmpeg中的相关概念,以及视频的一些基本概念。</p>
<h2 id="视频基本概念"><a href="#视频基本概念" class="headerlink" title="视频基本概念"></a>视频基本概念</h2><p>当我们提及视频格式的时候,实际上对应的是视频的container(又或者称为format)这个概念,这个概念往往对应mpeg4,mkv, webm等。一个container可以是由多个编码器压缩后的媒体的集合,例如,一个mp4,可以包含视频,音频,字幕等不同媒体</p>
<p>编码器codec,则是按照某种编解码标准下的具体实现,这一层对应的概念往往是av1, h264, vp9等</p>
<p>因此,一个视频文件,从读取IO到播放大致可以分为以下几步:</p>
<ol>
<li>解析container</li>
<li>根据container,获得我们关心的媒体数据(例如播放视频,那我们只关心视频媒体)</li>
<li>根据媒体信息获得对应的解码器</li>
<li>将对应的数据送给解码器</li>
<li>解码器解码,输出帧</li>
<li>将帧渲染到目标区域</li>
</ol>
<h2 id="FFmpeg中对上述概念的定义"><a href="#FFmpeg中对上述概念的定义" class="headerlink" title="FFmpeg中对上述概念的定义"></a>FFmpeg中对上述概念的定义</h2><p>那么在FFmpeg中,是如何定义上述概念的呢?</p>
<p>对于container,FFMpeg使用<code>AVFormatContext</code><br>对于具体的媒体数据,通过遍历<code>AVFormatContext->streams</code>,我们能通过一个<code>AVStream</code>对象表示</p>
<p><code>AVStream</code>中的压缩分片用<code>AVPacket</code>表示,通过解码器解码后我们就能得到帧数据,用<code>AVFrame</code>表示</p>
<p>这篇文章就先简单介绍下1,2,3步的实现</p>
<h2 id="使用FFMpeg解析container,获取基本信息"><a href="#使用FFMpeg解析container,获取基本信息" class="headerlink" title="使用FFMpeg解析container,获取基本信息"></a>使用FFMpeg解析container,获取基本信息</h2><p>代码如下</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">AVFormatContext *avFormatContext = <span class="literal">nullptr</span>;</span><br><span class="line">LOGI(<span class="string">"video_config_create, open file uri: %s"</span>, uri);</span><br><span class="line"><span class="keyword">if</span> (avformat_open_input(&avFormatContext, uri, <span class="literal">nullptr</span>, <span class="literal">nullptr</span>)) { <span class="comment">// open IO, 如果成功, avFormatContext将有值</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (avformat_find_stream_info(avFormatContext, <span class="literal">nullptr</span>) < <span class="number">0</span>) {</span><br><span class="line"> avformat_free_context(avFormatContext);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> pos = <span class="number">0</span>; pos < avFormatContext->nb_streams; pos++) {</span><br><span class="line"> <span class="comment">// Getting the name of a codec of the very first video stream</span></span><br><span class="line"> AVCodecParameters *parameters = avFormatContext->streams[pos]->codecpar;</span><br><span class="line"> <span class="keyword">if</span> (parameters->codec_type == AVMEDIA_TYPE_VIDEO) {</span><br><span class="line"> videoConfig->parameters = parameters;</span><br><span class="line"> videoConfig->avVideoCodec = avcodec_find_decoder(parameters->codec_id);</span><br><span class="line"> videoConfig->videoStreamIndex = pos;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>代码的基本逻辑,和上述介绍的步骤,是一致的;其中,视频的文件格式信息,显然就在<code>AVFormatContext</code>中,视频的宽高信息,就在<code>AVCodecParameters</code>中,这个对象顾名思义,是跟随codec的;而解码器的相关信息,就在<code>avcodec_find_decoder</code>的返回值中</p>
<p>本文对应的源代码请参考<a href="https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.0" target="_blank" rel="noopener">https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.0</a></p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/08/22/FFMpeg-Android%E5%BC%80%E5%8F%91-%E8%A7%A3%E6%9E%90%E8%A7%86%E9%A2%91%E6%A0%BC%E5%BC%8F/" data-id="ckgx4rr7a000ci8x70g2uqaxa" class="article-share-link">Share</a>
</footer>
</div>
</article>
<article id="post-FFMpeg-Android开发101-编译和引入" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/08/15/FFMpeg-Android%E5%BC%80%E5%8F%91101-%E7%BC%96%E8%AF%91%E5%92%8C%E5%BC%95%E5%85%A5/" class="article-date">
<time datetime="2020-08-15T02:15:51.000Z" itemprop="datePublished">2020-08-15</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/08/15/FFMpeg-Android%E5%BC%80%E5%8F%91101-%E7%BC%96%E8%AF%91%E5%92%8C%E5%BC%95%E5%85%A5/">FFMpeg Android开发101-编译和引入</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>本系列会尝试将FFMpeg引入到android项目中,并借助FFMpeg完成一些音视频的简单饮用;FFMpeg作为一个成熟的音视频编解码工具被大量项目使用,但将FFMpeg引入到Android开发的文档并不多,国内有一部分但大量已经过时,这个系列会重新尝试带领大家完成整个过程。</p>
<p>本文对应的源代码请参考<a href="https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.0" target="_blank" rel="noopener">https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.0</a></p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">git clone https://github.com/TedaLIEz/MyFFMpegAndroid/tree/v1.0</span><br><span class="line"></span><br><span class="line">cd MyFFMpegAndroid</span><br><span class="line">git submodule update</span><br><span class="line"></span><br><span class="line">cd ffmpeg-android-maker</span><br><span class="line">export ANDROID_SDK_HOME=${YOUR_SDK_HOME}</span><br><span class="line">export ANDROID_NDK_HOME=${YOUR_NDK_HOME}</span><br><span class="line">./ffmpeg-android-maker.sh</span><br></pre></td></tr></table></figure>
<p>之后用Android Studio打开项目,即可运行</p>
<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>总的来说,将FFMpeg引入到Android项目开发分为下面几个步骤:</p>
<ol>
<li>使用Android NDK编译FFMpeg项目</li>
<li>在自己的项目部署FFMpeg</li>
<li>开始FFMpeg开发,验证效果</li>
</ol>
<h3 id="使用Android-NDK编译FFMpeg"><a href="#使用Android-NDK编译FFMpeg" class="headerlink" title="使用Android NDK编译FFMpeg"></a>使用Android NDK编译FFMpeg</h3><pre><code>TL;DR 参考https://github.com/Javernaut/ffmpeg-android-maker
</code></pre><p>首先需要简单了解些FFMpeg编译产物的一些职责:</p>
<p><code>libavformat</code>: 处理文件container, stream<br><code>libavcodec</code>: 编解码<br><code>libswscale</code>: 图像处理<br><code>libavutil</code>: util库</p>
<p>还有一些其他的库,可以自行参考FFmpeg文档</p>
<p>考虑到我们是编译一个Android的FFMpeg库,本质上这就是一个交叉编译,因此我简单介绍一下FFmpeg编译到Android上的一些配置</p>
<h4 id="编译配置"><a href="#编译配置" class="headerlink" title="编译配置"></a>编译配置</h4><p>由于Android有着ARM, x86的32,64位处理器架构,因此我们需要编译4种二进制文件;这时需要一个交叉编译器(cross-compiler)来帮我们处理问题。</p>
<p>第二,编译过程不只是compile,还包括链接(linker)等其他工具, 我们统称为binutils</p>
<p>第三,我们需要Android系统自己的一些库和头文件,这些文件的存储位置我们成为sysroot</p>
<p>因此,整个编译需要的工具链包括<code>cross-compiler</code>, <code>binutils</code>, <code>sysroot</code></p>
<p>那么我们怎么获得这些工具呢?答案显然就是在Android NDK中,如果之前你有查阅过Android-NDK的目录,那么你就会发现NDK的结构其实已经说明了这三个工具的划分</p>
<p>这里我们看一下configure的相关参数</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">./configure \</span><br><span class="line">--prefix=<span class="variable">${BUILD_DIR}</span>/<span class="variable">${ABI}</span> \</span><br><span class="line">--<span class="built_in">enable</span>-cross-compile \</span><br><span class="line">--target-os=android \</span><br><span class="line">--arch=<span class="variable">${TARGET_TRIPLE_MACHINE_BINUTILS}</span> \</span><br><span class="line">--sysroot=<span class="variable">${SYSROOT}</span> \</span><br><span class="line">--cross-prefix=<span class="variable">${CROSS_PREFIX}</span> \</span><br><span class="line">--cc=<span class="variable">${CC}</span> \</span><br><span class="line">--extra-cflags=<span class="string">"-O3 -fPIC"</span> \</span><br><span class="line">--<span class="built_in">enable</span>-shared \</span><br><span class="line">--<span class="built_in">disable</span>-static \</span><br><span class="line"><span class="variable">${EXTRA_BUILD_CONFIGURATION_FLAGS}</span> \</span><br></pre></td></tr></table></figure>
<p><code>prefix</code>指明了产物的路径</p>
<p><code>target-os=android</code>: 指明我们的编译操作系统是Android</p>
<p><code>--arch=${TARGET_TRIPLE_MACHINE_BINUTILS}</code>: 指明arm, aarch64, i686 and x86_64</p>
<p><code>--sysroot=${SYSROOT}</code>: 指明sysroot的路径,这个路径一定是在Android NDK的路径下的一个子目录</p>
<p><code>--cross-prefix=${CROSS_PREFIX}</code>: 指明binutils的工具名前缀,这个名字会追加到ld前当作linker使用,例如</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">armeabi-v7a: $TOOLCHAIN_PATH/bin/arm-linux-androideabi-</span><br><span class="line">arm64-v8a: $TOOLCHAIN_PATH/bin/aarch64-linux-android-</span><br><span class="line">x86: $TOOLCHAIN_PATH/bin/i686-linux-android-</span><br><span class="line">x86_64: $TOOLCHAIN_PATH/bin/x86_64-linux-android-</span><br></pre></td></tr></table></figure>
<p><code>--cc=${CC}</code>: 指明编译器, 例如</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">armeabi-v7a: $TOOLCHAIN_PATH/bin/armv7a-linux-androideabi16-clang</span><br><span class="line">arm64-v8a: $TOOLCHAIN_PATH/bin/aarch64-linux-android21-clang</span><br><span class="line">x86: $TOOLCHAIN_PATH/bin/i686-linux-android16-clang</span><br><span class="line">x86_64: $TOOLCHAIN_PATH/bin/x86_64-linux-android21-clang</span><br></pre></td></tr></table></figure>
<p><code>--enable-shard</code>和<code>--disable-static</code>表明我们是编译一个动态库</p>
<p><code>--extra-cflags=”-O3 -fPIC”</code>表明了其他的C flag, <code>-O3</code>表明编译器优化级别, <code>-fPIC</code>是编译Android上的动态库必须的参数</p>
<h4 id="FFMpeg自身的相关配置"><a href="#FFMpeg自身的相关配置" class="headerlink" title="FFMpeg自身的相关配置"></a>FFMpeg自身的相关配置</h4><p>除去上面编译工具链需要的参数外,我们还可以定制化我们的FFMpeg编译,例如:</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">--disable-runtime-cpudetect \</span><br><span class="line">--disable-programs \</span><br><span class="line">--disable-muxers \</span><br><span class="line">--disable-encoders \</span><br><span class="line">--disable-avdevice \</span><br><span class="line">--disable-postproc \</span><br><span class="line">--disable-swresample \</span><br><span class="line">--disable-avfilter \</span><br><span class="line">--disable-doc \</span><br><span class="line">--disable-debug \</span><br><span class="line">--disable-pthreads \</span><br><span class="line">--disable-network \</span><br><span class="line">--disable-bsfs \</span><br><span class="line">--disable-decoders \</span><br><span class="line">${DECODERS_TO_ENABLE}</span><br></pre></td></tr></table></figure>
<p>这里<code>--disable</code>-xxx表示不需要FFMpeg的具体模块,这个可以根据自身app的开发来定制</p>
<p>上述工作全部完成后,就可以开始make && make install了</p>
<h3 id="在Android项目中引入FFmpeg"><a href="#在Android项目中引入FFmpeg" class="headerlink" title="在Android项目中引入FFmpeg"></a>在Android项目中引入FFmpeg</h3><p>编译完成后,你应该能获得如下的编译结果</p>
<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">.</span><br><span class="line">├── include</span><br><span class="line">│ ├── arm64-v8a</span><br><span class="line">│ ├── armeabi-v7a</span><br><span class="line">│ ├── x86</span><br><span class="line">│ └── x86_64</span><br><span class="line">└── lib</span><br><span class="line"> ├── arm64-v8a</span><br><span class="line"> ├── armeabi-v7a</span><br><span class="line"> ├── x86</span><br><span class="line"> └── x86_64</span><br></pre></td></tr></table></figure>
<p>下一步就是引入到Android项目中了,首先在你的app/build.gradle中作如下修改</p>
<figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">android {</span><br><span class="line"> ...</span><br><span class="line"> defaultConfig {</span><br><span class="line"> ...</span><br><span class="line"> ndk {</span><br><span class="line"> abiFilters <span class="string">'x86'</span>, <span class="string">'x86_64'</span>, <span class="string">'armeabi-v7a'</span>, <span class="string">'arm64-v8a'</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> sourceSets {</span><br><span class="line"> main {</span><br><span class="line"> <span class="comment">// let gradle pack the shared library into the apk</span></span><br><span class="line"> jniLibs.srcDirs = [<span class="string">'../ffmpeg-android-maker/output/lib'</span>]</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> externalNativeBuild {</span><br><span class="line"> cmake {</span><br><span class="line"> path <span class="string">"CMakeLists.txt"</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>分别解释一下:</p>
<p><code>abiFilters</code>: 指定了app支持的架构</p>
<p><code>jniLibs.srcDirs</code>: 指定了app需要的动态库路径,android编译时会自动将这里指定的动态库打包进apk</p>
<p><code>externalNativeBuild</code>这里指定了我们的<code>CMakeLists.txt</code>路径</p>
<p>下一步就是配置CMakeLists</p>
<figure class="highlight cmake"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">cmake_minimum_required</span>(VERSION <span class="number">3.4</span>.<span class="number">1</span>) <span class="comment"># 指定Cmake最低版本</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">set</span>(ffmpeg_dir <span class="variable">${CMAKE_SOURCE_DIR}</span>/../ffmpeg-android-maker/output) <span class="comment"># 设置ffmpeg_dir变量</span></span><br><span class="line"><span class="keyword">include_directories</span>(<span class="variable">${ffmpeg_dir}</span>/<span class="keyword">include</span>/<span class="variable">${ANDROID_ABI}</span>) <span class="comment"># 设置需要include的头文件路径,注意这里的ANDROID_ABI代表了在gradle中指定的abiFilters的每一个变量</span></span><br><span class="line"><span class="keyword">set</span>(ffmpeg_libs <span class="variable">${ffmpeg_dir}</span>/lib/<span class="variable">${ANDROID_ABI}</span>) <span class="comment"># 设置ffmpeg_libs变量,指明shared library路径</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(avutil SHARED IMPORTED) <span class="comment"># 声明avutil库</span></span><br><span class="line"><span class="keyword">set_target_properties</span>(avutil PROPERTIES IMPORTED_LOCATION <span class="variable">${ffmpeg_libs}</span>/libavutil.so) <span class="comment"># 指定avutil库shared library路径</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(avformat SHARED IMPORTED) <span class="comment"># 类似上面的声明</span></span><br><span class="line"><span class="keyword">set_target_properties</span>(avformat PROPERTIES IMPORTED_LOCATION <span class="variable">${ffmpeg_libs}</span>/libavformat.so)</span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(avfilter SHARED IMPORTED)</span><br><span class="line"><span class="keyword">set_target_properties</span>(avfilter PROPERTIES IMPORTED_LOCATION <span class="variable">${ffmpeg_libs}</span>/libavfilter.so)</span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(avcodec SHARED IMPORTED)</span><br><span class="line"><span class="keyword">set_target_properties</span>(avcodec PROPERTIES IMPORTED_LOCATION <span class="variable">${ffmpeg_libs}</span>/libavcodec.so)</span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(swscale SHARED IMPORTED)</span><br><span class="line"><span class="keyword">set_target_properties</span>(swscale PROPERTIES IMPORTED_LOCATION <span class="variable">${ffmpeg_libs}</span>/libswscale.so)</span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(swresample SHARED IMPORTED)</span><br><span class="line"><span class="keyword">set_target_properties</span>(swresample PROPERTIES IMPORTED_LOCATION <span class="variable">${ffmpeg_libs}</span>/libswresample.so)</span><br><span class="line"></span><br><span class="line"><span class="keyword">find_library</span>(log-lib log) <span class="comment"># 使用Android 的native log库,命名为log-lib</span></span><br><span class="line"><span class="keyword">find_library</span>(jnigraphics-lib jnigraphics) <span class="comment"># 同上,使用jnigraphics</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">add_library</span>(test_ffmpeg</span><br><span class="line"> SHARED</span><br><span class="line"> src/main/cpp/video_config.cpp</span><br><span class="line"> src/main/cpp/video_config_jni.cpp</span><br><span class="line"> src/main/cpp/utils.cpp</span><br><span class="line"> src/main/cpp/main.cpp</span><br><span class="line"> src/main/cpp/player.cpp) <span class="comment"># 添加我们自己项目中的代码,并命名为test_ffmpeg库</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">target_link_libraries</span>(</span><br><span class="line"> test_ffmpeg</span><br><span class="line"> <span class="variable">${log-lib}</span></span><br><span class="line"> <span class="variable">${jnigraphics-lib}</span></span><br><span class="line"> android</span><br><span class="line"> avformat</span><br><span class="line"> avcodec</span><br><span class="line"> swscale</span><br><span class="line"> avutil</span><br><span class="line"> swresample</span><br><span class="line"> avfilter) <span class="comment"># 通知linker将上述所有library链接</span></span><br></pre></td></tr></table></figure>
<p>具体含义已经在注释中</p>
<p>全部配置完成后,可以尝试进行编译;剩下的问题就是JNI相关的知识和Android自身的相关开发知识了,本文就不再赘述。需要注意的一个小点就是在Java层System.loadLibrary时需要注意加载顺序</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">init</span> {</span><br><span class="line"> listOf(<span class="string">"avutil"</span>, <span class="string">"avcodec"</span>, <span class="string">"avformat"</span>, <span class="string">"swscale"</span>, <span class="string">"test_ffmpeg"</span>).forEach {</span><br><span class="line"> System.loadLibrary(it)</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>
<p>之后运行app,如果没有出现crash,就基本证明我们的引入是ok的了。Have Fun!</p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/08/15/FFMpeg-Android%E5%BC%80%E5%8F%91101-%E7%BC%96%E8%AF%91%E5%92%8C%E5%BC%95%E5%85%A5/" data-id="ckgx4rr7d000ei8x7ifypegqv" class="article-share-link">Share</a>
</footer>
</div>
</article>
<article id="post-Jetpack-Compose简介与思考" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/07/25/Jetpack-Compose%E7%AE%80%E4%BB%8B%E4%B8%8E%E6%80%9D%E8%80%83/" class="article-date">
<time datetime="2020-07-25T01:22:01.000Z" itemprop="datePublished">2020-07-25</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/07/25/Jetpack-Compose%E7%AE%80%E4%BB%8B%E4%B8%8E%E6%80%9D%E8%80%83/">Jetpack Compose简介与思考</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>Jetpack Compose可以认为是Android对UI代码架构的演进,核心目的是为了让安卓自身的UI代码能够跟随现代UI开发的步伐</p>
<h2 id="现代UI开发的特性"><a href="#现代UI开发的特性" class="headerlink" title="现代UI开发的特性"></a>现代UI开发的特性</h2><p>现代UI开发的一个重要特点在于其代码为声明式的代码结构,从数学关系上来看,UI的代码构成可以描述为对某一时刻状态的一个函数,即<code>UI=F(n)</code>,其中n表示了当前的交互状态。参考MVVM的设计来说,这个状态可以由ViewModel来管理</p>
<h2 id="安卓的UI开发现状"><a href="#安卓的UI开发现状" class="headerlink" title="安卓的UI开发现状"></a>安卓的UI开发现状</h2><p>安卓的UI开发思路和代码结构已经明显老化,主要体现在下面一些方面:</p>
<ol>
<li>仍旧有xml这样的配置文件,即使出现了ViewBinding也无济于事</li>
<li>构建UI时有大量的命令式代码,这种代码散落在Activity/Fragment中,不利于维护</li>
</ol>
<h2 id="命令式编程与声明式编程的争论"><a href="#命令式编程与声明式编程的争论" class="headerlink" title="命令式编程与声明式编程的争论"></a>命令式编程与声明式编程的争论</h2><p>命令式编程风格的问题在于程序非常依赖过程,换言之,如果代码编写的顺序出错,那么程序也会出错;而声明式编程则是只关心结果,编写代码时,只需要把开发者最终想要的结果直接写入代码,如果需要变化,则直接修改你的声明代码。并且声明式风格的UI代码在状态改变的任意时刻都是<strong>同一套代码</strong>构建UI, 这有助于提升代码质量</p>
<p>声明式风格编码特点在于:</p>
<pre><code>Describe your UI right now. For any value of now.
</code></pre><p>这种风格是一种状态无关代码(status independent)</p>
<h2 id="Jetpack-Compose"><a href="#Jetpack-Compose" class="headerlink" title="Jetpack Compose"></a>Jetpack Compose</h2><p>Jetpack Compose的目的就在于改变现有的UI开发思路,完全向声明式函数演进,同时提供了一些便捷功能,例如一个常规的UI声明如下:</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MainActivity</span> : <span class="type">AppCompatActivity</span></span>() {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> setContentView(R.layout.activity_main)</span><br><span class="line"> findViewById<TextView>(R.id.tv).text = <span class="string">"Android"</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>对于复杂的UI界面,这种UI代码的编码方式是可以极其零散的, 可能一部分写在xml里面,一部分又写在代码里,同时代码里的编码可以任意地乱序(命令式代码的弊端)。而Compose出现后,我们的编码方式就可以变化为</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MainActivity</span> : <span class="type">AppCompatActivity</span></span>() {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> setContent {</span><br><span class="line"> MyApp {</span><br><span class="line"> Greeting(name = <span class="string">"Android"</span>)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@Composable</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">MyApp</span><span class="params">(content: @<span class="type">Composable</span>()</span></span> () -> <span class="built_in">Unit</span>) {</span><br><span class="line"> MaterialTheme {</span><br><span class="line"> content()</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Composable</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">Greeting</span><span class="params">(name: <span class="type">String</span>)</span></span> {</span><br><span class="line"> Surface(color = Color.Yellow) {</span><br><span class="line"> Text(text = <span class="string">"Hello <span class="variable">$name</span>!"</span>, modifier = Modifier.padding(<span class="number">24</span>.dp))</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>乍一看这种编码风格非常像Flutter,React,Vue的UI代码风格,本质上这些前端框架的UI设计都遵循了声明式函数风格,通过将编码方式转化为声明式函数风格后,我们间接地获得了以下收益</p>
<ol>
<li>各个函数独立,复用性提升</li>
<li>UI代码紧凑</li>
</ol>
<p>复用性提升的直接效果之一就是我们可以在IDE里面直接preview UI效果,这个也是Android Studio 4.2后内置的能力,只需要给你的<code>@Composable</code>函数加上一个<code>@Preview</code>标签并进行编译,这个函数自己的UI效果就能直接在IDE上展示</p>
<p>UI代码紧凑则明确了任何对UI的修改都有唯一的入口,例如在上面的例子中,<code>Activity</code>中一定是<code>setContent</code></p>
<h3 id="Jetpack-Compose的状态管理"><a href="#Jetpack-Compose的状态管理" class="headerlink" title="Jetpack Compose的状态管理"></a>Jetpack Compose的状态管理</h3><p>声明式风格的UI都面临一个问题就是如何处理状态改变,Jetpack Compose和其他常见的声明式UI框架一致,采用了state这个概念,通过声明一个state对象,我们根据这个state对象构造自己的UI和其子View,当state改变时,动态的改变这个UI的声明,例如:</p>
<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Composable</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">MyScreenContent</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">val</span> counterState = state { <span class="number">0</span> }</span><br><span class="line"> Counter(</span><br><span class="line"> count = counterState.value,</span><br><span class="line"> updateCount = { newCount -></span><br><span class="line"> counterState.value = newCount</span><br><span class="line"> }</span><br><span class="line"> )</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="meta">@Composable</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">Counter</span><span class="params">(count: <span class="type">Int</span>, updateCount: (<span class="type">Int</span>)</span></span> -> <span class="built_in">Unit</span>) {</span><br><span class="line"> Button(onClick = { updateCount(count+<span class="number">1</span>) }) {</span><br><span class="line"> Text(<span class="string">"I've been clicked <span class="variable">$count</span> times"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这个例子中,我们就能发现<code>MyScreenContent</code>中内含了一个counterState对象,它通过counterState构建了一个Counter,而Counter也是counterState的一个函数,当counterState变化时,Counter也会重新构建,UI因此也发生变化</p>
<h3 id="Jetpack-Compose中Lifecycle"><a href="#Jetpack-Compose中Lifecycle" class="headerlink" title="Jetpack Compose中Lifecycle"></a>Jetpack Compose中Lifecycle</h3><p>Android开发非常强调Lifecycle-aware,compose也是如此。为此Jetpack Compose提出了Efforts这个概念,在这个概念中提供了onCommit, onPreCommit, onActive, onDispose一系列函数,用来监听生命周期变化</p>
<h2 id="感想和思考"><a href="#感想和思考" class="headerlink" title="感想和思考"></a>感想和思考</h2><p>似乎在UI开发上,声明式编程已经成为了主流的方向。随着SwiftUI,Flutter,Jetpack Compose的出现,越来越多的新兴UI开发框架抛弃了现有的MVC,MVP模式,而走向声明式UI方向,随着声明式UI被引入移动端开发,新兴的MVI模式也进入了大众视野,并逐渐被人们接受</p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/07/25/Jetpack-Compose%E7%AE%80%E4%BB%8B%E4%B8%8E%E6%80%9D%E8%80%83/" data-id="ckgx4rr7g000ii8x71igqkp71" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/android-jetpack-compose/" rel="tag">android, jetpack compose</a></li></ul>
</footer>
</div>
</article>
<article id="post-MMKV源码简析-跨进程" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/05/04/MMKV%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90-%E8%B7%A8%E8%BF%9B%E7%A8%8B/" class="article-date">
<time datetime="2020-05-04T07:23:07.000Z" itemprop="datePublished">2020-05-04</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/05/04/MMKV%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90-%E8%B7%A8%E8%BF%9B%E7%A8%8B/">MMKV源码简析---跨进程</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<p>MMKV 一个重要特性就是增加了 android 侧对跨进程读写的支持,我们单独用一篇文章来分析一下 MMKV 对跨进程存储的实现方式<br>典型的初始化调用如下:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><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">MMKV mmkv = MMKV.mmkvWithID(MMKV_ID, MMKV.MULTI_PROCESS_MODE, CryptKey);</span><br><span class="line"></span><br><span class="line">mmkv.encode(...)</span><br><span class="line">mmkv.decode(...)</span><br></pre></td></tr></table></figure>
<p>MMKV 会把每个文件都通过 mmap 到进程的访问空间,因此每个进程本身就可以直接访问存储,但这里没有解决跨进程的并发访问问题,解决并发需要别的手段,我们看看 MMKV.MULTI_PROCESS_MODE 造成了什么影响</p>
<h2 id="MMKV-MULTI-PROCESS-MODE"><a href="#MMKV-MULTI-PROCESS-MODE" class="headerlink" title="MMKV.MULTI_PROCESS_MODE"></a>MMKV.MULTI_PROCESS_MODE</h2><p>参考初始化流程的代码和<code>MMKV.MULTI_PROCESS_MODE=2</code>, 来到 MMKV 构造函数, 可以发现此时 MMKV::m_isInterProcess=true, 这个对代码逻辑有什么影响呢?</p>
<ol>
<li>checkLoadData 的后半段代码会执行</li>
<li>m_sharedProcessLock, m_exclusiveProcessLock 全部 enable</li>
</ol>
<p>先来看看 checkLoadData 的后半段代码是啥</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!m_isInterProcess) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> (!m_metaFile->isFileValid()) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">// <span class="doctag">TODO:</span> atomic lock m_metaFile?</span></span><br><span class="line">MMKVMetaInfo metaInfo;</span><br><span class="line">metaInfo.read(m_metaFile->getMemory()); <span class="comment">// 文件m_metaFile被mmap,每个进程都能读到, 但是m_metaInfo是内存对象,没有被进程共享,因此有可能存储的元数据和文件中的元数据不一致</span></span><br><span class="line"><span class="keyword">if</span> (m_metaInfo->m_sequence != metaInfo.m_sequence) {</span><br><span class="line"> MMKVInfo(<span class="string">"[%s] oldSeq %u, newSeq %u"</span>, m_mmapID.c_str(), m_metaInfo->m_sequence, metaInfo.m_sequence);</span><br><span class="line"> SCOPED_LOCK(m_sharedProcessLock);</span><br><span class="line"></span><br><span class="line"> clearMemoryCache();</span><br><span class="line"> loadFromFile();</span><br><span class="line"> notifyContentChanged();</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> (m_metaInfo->m_crcDigest != metaInfo.m_crcDigest) {</span><br><span class="line"> MMKVDebug(<span class="string">"[%s] oldCrc %u, newCrc %u, new actualSize %u"</span>, m_mmapID.c_str(), m_metaInfo->m_crcDigest,</span><br><span class="line"> metaInfo.m_crcDigest, metaInfo.m_actualSize);</span><br><span class="line"> SCOPED_LOCK(m_sharedProcessLock);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">size_t</span> fileSize = m_file->getActualFileSize();</span><br><span class="line"> <span class="keyword">if</span> (m_file->getFileSize() != fileSize) {</span><br><span class="line"> MMKVInfo(<span class="string">"file size has changed [%s] from %zu to %zu"</span>, m_mmapID.c_str(), m_file->getFileSize(), fileSize);</span><br><span class="line"> clearMemoryCache();</span><br><span class="line"> loadFromFile();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> partialLoadFromFile();</span><br><span class="line"> }</span><br><span class="line"> notifyContentChanged();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>相比较于单进程,这里增加了对内存中的 meta 和文件的 meta 的 seq 和 crc 校验比较,如果不相等,则需要走文件重新加载逻辑,这一块,和<a href="https://github.com/Tencent/MMKV/wiki/android_ipc#%E7%8A%B6%E6%80%81%E5%90%8C%E6%AD%A5" target="_blank" rel="noopener">https://github.com/Tencent/MMKV/wiki/android_ipc#%E7%8A%B6%E6%80%81%E5%90%8C%E6%AD%A5</a>描述是一致的</p>
<p>m_sharedProcessLock, m_exclusiveProcessLock 全部 enable 应该就是使锁生效,那么分析一下这个锁是如何实现跨进程锁的</p>
<h2 id="InterProcessLock"><a href="#InterProcessLock" class="headerlink" title="InterProcessLock"></a>InterProcessLock</h2><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">InterProcessLock</span> {</span></span><br><span class="line"> FileLock *m_fileLock;</span><br><span class="line"> LockType m_lockType;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> InterProcessLock(FileLock *fileLock, LockType lockType)</span><br><span class="line"> : m_fileLock(fileLock), m_lockType(lockType), m_enable(<span class="literal">true</span>) {</span><br><span class="line"> MMKV_ASSERT(m_fileLock);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> m_enable;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">lock</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (m_enable) {</span><br><span class="line"> m_fileLock->lock(m_lockType);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看到,MMKV 的跨进程锁是基于文件锁实现的, <code>InterProcessLock#lock</code>基于<code>FileLock.lock</code>实现, 一路转发来到<code>FileLock::platformLock()</code></p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">bool</span> FileLock::doLock(LockType lockType, <span class="keyword">bool</span> wait) {</span><br><span class="line"> <span class="keyword">if</span> (!isFileLockValid()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">bool</span> unLockFirstIfNeeded = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (lockType == SharedLockType) {</span><br><span class="line"> <span class="comment">// don't want shared-lock to break any existing locks</span></span><br><span class="line"> <span class="keyword">if</span> (m_sharedLockCount > <span class="number">0</span> || m_exclusiveLockCount > <span class="number">0</span>) {</span><br><span class="line"> m_sharedLockCount++;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// don't want exclusive-lock to break existing exclusive-locks</span></span><br><span class="line"> <span class="keyword">if</span> (m_exclusiveLockCount > <span class="number">0</span>) {</span><br><span class="line"> m_exclusiveLockCount++;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// prevent deadlock</span></span><br><span class="line"> <span class="keyword">if</span> (m_sharedLockCount > <span class="number">0</span>) {</span><br><span class="line"> unLockFirstIfNeeded = <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> ret = platformLock(lockType, wait, unLockFirstIfNeeded);</span><br><span class="line"> <span class="keyword">if</span> (ret) {</span><br><span class="line"> <span class="keyword">if</span> (lockType == SharedLockType) {</span><br><span class="line"> m_sharedLockCount++;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> m_exclusiveLockCount++;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ret;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">bool</span> FileLock::platformLock(LockType lockType, <span class="keyword">bool</span> wait, <span class="keyword">bool</span> unLockFirstIfNeeded) {</span><br><span class="line"><span class="meta"># <span class="meta-keyword">ifdef</span> MMKV_ANDROID</span></span><br><span class="line"> <span class="keyword">if</span> (m_isAshmem) {</span><br><span class="line"> <span class="keyword">return</span> ashmemLock(lockType, wait, unLockFirstIfNeeded);</span><br><span class="line"> }</span><br><span class="line"><span class="meta"># <span class="meta-keyword">endif</span></span></span><br><span class="line"> <span class="keyword">auto</span> realLockType = LockType2FlockType(lockType);</span><br><span class="line"> <span class="keyword">auto</span> cmd = wait ? realLockType : (realLockType | LOCK_NB);</span><br><span class="line"> <span class="keyword">if</span> (unLockFirstIfNeeded) {</span><br><span class="line"> <span class="comment">// try lock</span></span><br><span class="line"> <span class="keyword">auto</span> ret = flock(m_fd, realLockType | LOCK_NB);</span><br><span class="line"> <span class="keyword">if</span> (ret == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// let's be gentleman: unlock my shared-lock to prevent deadlock</span></span><br><span class="line"> ret = flock(m_fd, LOCK_UN);</span><br><span class="line"> <span class="keyword">if</span> (ret != <span class="number">0</span>) {</span><br><span class="line"> MMKVError(<span class="string">"fail to try unlock first fd=%d, ret=%d, error:%s"</span>, m_fd, ret, strerror(errno));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> ret = flock(m_fd, cmd);</span><br><span class="line"> <span class="keyword">if</span> (ret != <span class="number">0</span>) {</span><br><span class="line"> MMKVError(<span class="string">"fail to lock fd=%d, ret=%d, error:%s"</span>, m_fd, ret, strerror(errno));</span><br><span class="line"> <span class="comment">// try recover my shared-lock</span></span><br><span class="line"> <span class="keyword">if</span> (unLockFirstIfNeeded) {</span><br><span class="line"> ret = flock(m_fd, LockType2FlockType(SharedLockType));</span><br><span class="line"> <span class="keyword">if</span> (ret != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// let's hope this never happen</span></span><br><span class="line"> MMKVError(<span class="string">"fail to recover shared-lock fd=%d, ret=%d, error:%s"</span>, m_fd, ret, strerror(errno));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看到 MMKV 在文件锁 flock 的基础上,增加了一套计数器机制(代码中的 m_sharedLockCount 和 m_exclusiveLockCount),来确保</p>
<ol>
<li>锁的重入特性</li>
<li>锁的共享和互斥性转换特性</li>
</ol>
<p>关于文件锁的细节,可以参考<a href="https://gavv.github.io/articles/file-locks" target="_blank" rel="noopener">https://gavv.github.io/articles/file-locks</a></p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/05/04/MMKV%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90-%E8%B7%A8%E8%BF%9B%E7%A8%8B/" data-id="ckgx4rr7n000ti8x7m1vin86x" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/android-mmkv/" rel="tag">android, mmkv</a></li></ul>
</footer>
</div>
</article>
<article id="post-MMKV源码简析-读写" class="article article-type-post" itemscope itemprop="blogPost">
<div class="article-meta">
<a href="/2020/05/04/MMKV%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90-%E8%AF%BB%E5%86%99/" class="article-date">
<time datetime="2020-05-04T07:22:29.000Z" itemprop="datePublished">2020-05-04</time>
</a>
</div>
<div class="article-inner">
<header class="article-header">
<h1 itemprop="name">
<a class="article-title" href="/2020/05/04/MMKV%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90-%E8%AF%BB%E5%86%99/">MMKV源码简析---读写</a>
</h1>
</header>
<div class="article-entry" itemprop="articleBody">
<ul>
<li><a href="#mmkv-%e5%86%99">MMKV 写</a><ul>
<li><a href="#checkloaddata">checkLoadData()</a></li>
<li><a href="#appenddatawithkey">appendDataWithKey</a></li>
</ul>
</li>
<li><a href="#mmkv-%e8%af%bb">MMKV 读</a></li>
<li><a href="#%e5%b0%8f%e7%bb%93">小结</a></li>
<li><a href="#mmkv-%e7%a9%ba%e9%97%b4%e4%bc%98%e5%8c%96">MMKV 空间优化</a></li>
</ul>
<p>我们还是从 testcase 出发,一点点看 MMKV 如何实现读写的</p>
<h2 id="MMKV-写"><a href="#MMKV-写" class="headerlink" title="MMKV 写"></a>MMKV 写</h2><figure class="highlight kotlin"><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">val</span> mmkv = MMKV.mmkvWithID(<span class="string">"testKotlin"</span>)</span><br><span class="line">mmkv.encode(<span class="string">"bool"</span>, <span class="literal">true</span>)</span><br><span class="line">mmkv.encode(<span class="string">"int"</span>, Integer.MIN_VALUE)</span><br></pre></td></tr></table></figure>
<p>MMKV#encode 支持所有基本类型, 这里我们挑一个简单的 bool 类型分析一下,根据调用链<code>MMKV#encode</code>-><code>MMKV#encodeBool</code>来到 native-bridge.cpp 中的 JNI 接口,找到其实现<code>MMKV::set(bool value, MMKVKey_t key)</code></p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// MMKVKey_t = const string&</span></span><br><span class="line"><span class="keyword">bool</span> MMKV::<span class="built_in">set</span>(<span class="keyword">bool</span> value, MMKVKey_t key) {</span><br><span class="line"> <span class="keyword">if</span> (isKeyEmpty(key)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">size_t</span> size = pbBoolSize();</span><br><span class="line"> <span class="function">MMBuffer <span class="title">data</span><span class="params">(size)</span></span>;</span><br><span class="line"> <span class="function">CodedOutputData <span class="title">output</span><span class="params">(data.getPtr(), size)</span></span>;</span><br><span class="line"> output.writeBool(value);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> setDataForKey(<span class="built_in">std</span>::move(data), key);</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这里根据 pb 协议的规范,分配了一块和 pb 协议中商定的类型字段长度相同的 buffer MMBuffer, 将这个 buffer 利用 CodedOutputData 这个工具类写入一个 bool,之后调用<code>setDataForKey</code>来记录</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key) {</span><br><span class="line"> <span class="keyword">if</span> (data.length() == <span class="number">0</span> || isKeyEmpty(key)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> SCOPED_LOCK(m_lock);</span><br><span class="line"> SCOPED_LOCK(m_exclusiveProcessLock);</span><br><span class="line"> checkLoadData(); <span class="comment">// #1</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> ret = appendDataWithKey(data, key); <span class="comment">// #2</span></span><br><span class="line"> <span class="keyword">if</span> (ret) {</span><br><span class="line"> m_dic[key] = <span class="built_in">std</span>::move(data);</span><br><span class="line"> m_hasFullWriteback = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ret;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="checkLoadData"><a href="#checkLoadData" class="headerlink" title="checkLoadData()"></a>checkLoadData()</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> MMKV::checkLoadData() {</span><br><span class="line"> <span class="keyword">if</span> (m_needLoadFromFile) { <span class="comment">// m_needLoadFromFile在MMKV构造和clearMemoryCache调用后才会是true</span></span><br><span class="line"> SCOPED_LOCK(m_sharedProcessLock);</span><br><span class="line"></span><br><span class="line"> m_needLoadFromFile = <span class="literal">false</span>;</span><br><span class="line"> loadFromFile(); <span class="comment">// 初始化过程中也调用了这个函数,相当于加载存储中的内容</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!m_isInterProcess) {</span><br><span class="line"> <span class="comment">// 不是跨进程模式的MMKV,直接返回</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!m_metaFile->isFileValid()) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// <span class="doctag">TODO:</span> atomic lock m_metaFile?</span></span><br><span class="line"> MMKVMetaInfo metaInfo;</span><br><span class="line"> metaInfo.read(m_metaFile->getMemory());</span><br><span class="line"> <span class="keyword">if</span> (m_metaInfo->m_sequence != metaInfo.m_sequence) { <span class="comment">// 内存中的seq和文件中的seq不一致,文件有新的改动,清理缓存,重新读文件,通知内容更改</span></span><br><span class="line"> MMKVInfo(<span class="string">"[%s] oldSeq %u, newSeq %u"</span>, m_mmapID.c_str(), m_metaInfo->m_sequence, metaInfo.m_sequence);</span><br><span class="line"> SCOPED_LOCK(m_sharedProcessLock);</span><br><span class="line"></span><br><span class="line"> clearMemoryCache();</span><br><span class="line"> loadFromFile();</span><br><span class="line"> notifyContentChanged();</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (m_metaInfo->m_crcDigest != metaInfo.m_crcDigest) { <span class="comment">// 内存中的crc校验值和文件中crc校验值不一致</span></span><br><span class="line"> MMKVDebug(<span class="string">"[%s] oldCrc %u, newCrc %u, new actualSize %u"</span>, m_mmapID.c_str(), m_metaInfo->m_crcDigest,</span><br><span class="line"> metaInfo.m_crcDigest, metaInfo.m_actualSize);</span><br><span class="line"> SCOPED_LOCK(m_sharedProcessLock);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">size_t</span> fileSize = m_file->getActualFileSize();</span><br><span class="line"> <span class="keyword">if</span> (m_file->getFileSize() != fileSize) {</span><br><span class="line"> <span class="comment">// 有新内容写入,清理memory,重新读文件,通知内容更改</span></span><br><span class="line"> MMKVInfo(<span class="string">"file size has changed [%s] from %zu to %zu"</span>, m_mmapID.c_str(), m_file->getFileSize(), fileSize);</span><br><span class="line"> clearMemoryCache();</span><br><span class="line"> loadFromFile();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 现有内容有变化,存量加载</span></span><br><span class="line"> partialLoadFromFile();</span><br><span class="line"> }</span><br><span class="line"> notifyContentChanged(); <span class="comment">// 通知Java层通过setWantsContentChangeNotify注册的回调</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>逻辑比较清晰,主要看看 partialLoadFromFile 这个函数</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// read from last m_position</span></span><br><span class="line"><span class="keyword">void</span> MMKV::partialLoadFromFile() {</span><br><span class="line"> m_metaInfo->read(m_metaFile->getMemory());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">size_t</span> oldActualSize = m_actualSize;</span><br><span class="line"> m_actualSize = readActualSize();</span><br><span class="line"> <span class="keyword">auto</span> fileSize = m_file->getFileSize();</span><br><span class="line"> MMKVDebug(<span class="string">"loading [%s] with file size %zu, oldActualSize %zu, newActualSize %zu"</span>, m_mmapID.c_str(), fileSize,</span><br><span class="line"> oldActualSize, m_actualSize);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (m_actualSize > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">if</span> (m_actualSize < fileSize && m_actualSize + Fixed32Size <= fileSize) {</span><br><span class="line"> <span class="keyword">if</span> (m_actualSize > oldActualSize) {</span><br><span class="line"> <span class="comment">// 有新内容</span></span><br><span class="line"> <span class="keyword">size_t</span> bufferSize = m_actualSize - oldActualSize;</span><br><span class="line"> <span class="keyword">auto</span> ptr = (<span class="keyword">uint8_t</span> *) m_file->getMemory();</span><br><span class="line"> <span class="function">MMBuffer <span class="title">inputBuffer</span><span class="params">(ptr + Fixed32Size + oldActualSize, bufferSize, MMBufferNoCopy)</span></span>;</span><br><span class="line"> <span class="comment">// incremental update crc digest</span></span><br><span class="line"> m_crcDigest =</span><br><span class="line"> (<span class="keyword">uint32_t</span>) CRC32(m_crcDigest, (<span class="keyword">const</span> <span class="keyword">uint8_t</span> *) inputBuffer.getPtr(), inputBuffer.length());</span><br><span class="line"> <span class="keyword">if</span> (m_crcDigest == m_metaInfo->m_crcDigest) {</span><br><span class="line"> <span class="keyword">if</span> (m_crypter) {</span><br><span class="line"> decryptBuffer(*m_crypter, inputBuffer);</span><br><span class="line"> }</span><br><span class="line"> MiniPBCoder::greedyDecodeMap(m_dic, inputBuffer, bufferSize); <span class="comment">// 解析buffer内容到m_dic, buffer的结构是线性的string-MMBuffer KV结构</span></span><br><span class="line"> m_output->seek(bufferSize);</span><br><span class="line"> m_hasFullWriteback = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> MMKVDebug(<span class="string">"partial loaded [%s] with %zu values"</span>, m_mmapID.c_str(), m_dic.size());</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> MMKVError(<span class="string">"m_crcDigest[%u] != m_metaInfo->m_crcDigest[%u]"</span>, m_crcDigest, m_metaInfo->m_crcDigest);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// something is wrong, do a full load</span></span><br><span class="line"> clearMemoryCache();</span><br><span class="line"> loadFromFile();</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h3 id="appendDataWithKey"><a href="#appendDataWithKey" class="headerlink" title="appendDataWithKey"></a>appendDataWithKey</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> MMKV::appendDataWithKey(<span class="keyword">const</span> MMBuffer &data, MMKVKey_t key) {</span><br><span class="line"> <span class="keyword">size_t</span> keyLength = key.length();</span><br><span class="line"> <span class="comment">// size needed to encode the key</span></span><br><span class="line"> <span class="keyword">size_t</span> size = keyLength + pbRawVarint32Size((<span class="keyword">int32_t</span>) keyLength);</span><br><span class="line"> <span class="comment">// size needed to encode the value</span></span><br><span class="line"> size += data.length() + pbRawVarint32Size((<span class="keyword">int32_t</span>) data.length());</span><br><span class="line"></span><br><span class="line"> SCOPED_LOCK(m_exclusiveProcessLock);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> hasEnoughSize = ensureMemorySize(size);</span><br><span class="line"> <span class="keyword">if</span> (!hasEnoughSize || !isFileValid()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> m_output->writeString(key);</span><br><span class="line"> m_output->writeData(data); <span class="comment">// note: write size of data</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> ptr = (<span class="keyword">uint8_t</span> *) m_file->getMemory() + Fixed32Size + m_actualSize;</span><br><span class="line"> <span class="keyword">if</span> (m_crypter) {</span><br><span class="line"> m_crypter->encrypt(ptr, ptr, size);</span><br><span class="line"> }</span><br><span class="line"> m_actualSize += size;</span><br><span class="line"> updateCRCDigest(ptr, size); <span class="comment">// 更新crc校验和seq</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h2 id="MMKV-读"><a href="#MMKV-读" class="headerlink" title="MMKV 读"></a>MMKV 读</h2><p>同样地,通过 JNI 找到<code>MMKV::getBool</code></p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> MMKV::getBool(MMKVKey_t key, <span class="keyword">bool</span> defaultValue) {</span><br><span class="line"> <span class="keyword">if</span> (isKeyEmpty(key)) {</span><br><span class="line"> <span class="keyword">return</span> defaultValue;</span><br><span class="line"> }</span><br><span class="line"> SCOPED_LOCK(m_lock);</span><br><span class="line"> <span class="keyword">auto</span> &data = getDataForKey(key); <span class="comment">// #1</span></span><br><span class="line"> <span class="keyword">if</span> (data.length() > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="function">CodedInputData <span class="title">input</span><span class="params">(data.getPtr(), data.length())</span></span>;</span><br><span class="line"> <span class="keyword">return</span> input.readBool();</span><br><span class="line"> } <span class="keyword">catch</span> (<span class="built_in">std</span>::exception &exception) {</span><br><span class="line"> MMKVError(<span class="string">"%s"</span>, exception.what());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> defaultValue;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>实现关键在 getDataForKey</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"><span class="keyword">const</span> MMBuffer &MMKV::getDataForKey(MMKVKey_t key) {</span><br><span class="line"> checkLoadData();</span><br><span class="line"> <span class="keyword">auto</span> itr = m_dic.find(key);</span><br><span class="line"> <span class="keyword">if</span> (itr != m_dic.end()) {</span><br><span class="line"> <span class="keyword">return</span> itr->second;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">static</span> MMBuffer nan;</span><br><span class="line"> <span class="keyword">return</span> nan;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>checkLoadData 函数通过调用 loadFromFile 会确保所有的文件数据被读进 m_dic 中,这里直接取就可以了</p>
<h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>MMKV 的读写逻辑比较简单,主要的实现还是依赖了 PB 方式的数据序列化和反序列化,根据代码逻辑,我们可以尝试还原出 MMKV 内部的存储结构如下:</p>
<figure class="highlight plain"><figcaption><span>buffer</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">message KV {</span><br><span class="line"> string key = 1;</span><br><span class="line"> buffer value = 2;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">message MMKV {</span><br><span class="line"> int32 size = 1; // 文件大小,用于校验新数据写入</span><br><span class="line"> repeated KV kv = 2;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>同时我们可以通过<code>MiniPBCoder::decodeOneMap</code>的代码实现可以发现,MMKV 对于新旧数据问题,采用的方式不是写覆盖,而是直接追加,同时读时以最后一次写为最新值。这种方式显然是会带来大量的 key 字段冗余,因此必然存在一套完整的空间优化</p>
<h2 id="MMKV-空间优化"><a href="#MMKV-空间优化" class="headerlink" title="MMKV 空间优化"></a>MMKV 空间优化</h2><p>核心实现在<code>MMKV::ensureMemorySize</code>中</p>
<figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> MMKV::ensureMemorySize(<span class="keyword">size_t</span> newSize) {</span><br><span class="line"> <span class="keyword">if</span> (!isFileValid()) {</span><br><span class="line"> MMKVWarning(<span class="string">"[%s] file not valid"</span>, m_mmapID.c_str());</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// make some room for placeholder</span></span><br><span class="line"> <span class="keyword">constexpr</span> <span class="keyword">size_t</span> ItemSizeHolderSize = <span class="number">4</span>;</span><br><span class="line"> <span class="keyword">if</span> (m_dic.empty()) {</span><br><span class="line"> newSize += ItemSizeHolderSize;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (newSize >= m_output->spaceLeft() || m_dic.empty()) {</span><br><span class="line"> <span class="comment">// 空间不够,进行一次全量写入,这次全量写入会把过长的冗余字段全部覆盖写</span></span><br><span class="line"> <span class="comment">// try a full rewrite to make space</span></span><br><span class="line"> <span class="keyword">auto</span> fileSize = m_file->getFileSize();</span><br><span class="line"> MMBuffer data = MiniPBCoder::encodeDataWithObject(m_dic);</span><br><span class="line"> <span class="keyword">size_t</span> lenNeeded = data.length() + Fixed32Size + newSize;</span><br><span class="line"> <span class="keyword">size_t</span> avgItemSize = lenNeeded / <span class="built_in">std</span>::max<<span class="keyword">size_t</span>>(<span class="number">1</span>, m_dic.size());</span><br><span class="line"> <span class="keyword">size_t</span> futureUsage = avgItemSize * <span class="built_in">std</span>::max<<span class="keyword">size_t</span>>(<span class="number">8</span>, (m_dic.size() + <span class="number">1</span>) / <span class="number">2</span>);</span><br><span class="line"> <span class="comment">// 1. no space for a full rewrite, double it</span></span><br><span class="line"> <span class="comment">// 2. or space is not large enough for future usage, double it to avoid frequently full rewrite</span></span><br><span class="line"> <span class="keyword">if</span> (lenNeeded >= fileSize || (lenNeeded + futureUsage) >= fileSize) {</span><br><span class="line"> <span class="keyword">size_t</span> oldSize = fileSize;</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> fileSize *= <span class="number">2</span>; <span class="comment">// 2倍的扩充策略</span></span><br><span class="line"> } <span class="keyword">while</span> (lenNeeded + futureUsage >= fileSize);</span><br><span class="line"> MMKVInfo(<span class="string">"extending [%s] file size from %zu to %zu, incoming size:%zu, future usage:%zu"</span>, m_mmapID.c_str(),</span><br><span class="line"> oldSize, fileSize, newSize, futureUsage);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// if we can't extend size, rollback to old state</span></span><br><span class="line"> <span class="keyword">if</span> (!m_file->truncate(fileSize)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// check if we fail to make more space</span></span><br><span class="line"> <span class="keyword">if</span> (!isFileValid()) {</span><br><span class="line"> MMKVWarning(<span class="string">"[%s] file not valid"</span>, m_mmapID.c_str());</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> doFullWriteBack(<span class="built_in">std</span>::move(data)); <span class="comment">// 全量写入</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>整个读写过程分析到这里,后面将分析一下 MMKV 对于跨进程存储的实现</p>
</div>
<footer class="article-footer">
<a data-url="https://tedaliez.github.io/2020/05/04/MMKV%E6%BA%90%E7%A0%81%E7%AE%80%E6%9E%90-%E8%AF%BB%E5%86%99/" data-id="ckgx4rr7m000ri8x7sz8yh2kj" class="article-share-link">Share</a>
<ul class="article-tag-list" itemprop="keywords"><li class="article-tag-list-item"><a class="article-tag-list-link" href="/tags/android-mmkv/" rel="tag">android, mmkv</a></li></ul>
</footer>
</div>
</article>
<nav id="page-nav">
<span class="page-number current">1</span><a class="page-number" href="/page/2/">2</a><a class="page-number" href="/page/3/">3</a><a class="extend next" rel="next" href="/page/2/">Next &raquo;</a>
</nav>
</section>
<aside id="sidebar">
<div class="widget-wrap">
<h3 class="widget-title">Tags</h3>
<div class="widget">
<ul class="tag-list" itemprop="keywords"><li class="tag-list-item"><a class="tag-list-link" href="/tags/Android-ROOM/" rel="tag">Android, ROOM</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/NDK/" rel="tag">NDK</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/TypeScript/" rel="tag">TypeScript</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/WebAssembly/" rel="tag">WebAssembly</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/WebView-JSBridge/" rel="tag">WebView, JSBridge</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/android/" rel="tag">android</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/android-jetpack-compose/" rel="tag">android, jetpack compose</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/android-mmkv/" rel="tag">android, mmkv</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/android-textview/" rel="tag">android, textview</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/kotlin/" rel="tag">kotlin</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/kotlin-coroutine/" rel="tag">kotlin, coroutine</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/kotlin-%E5%8D%8F%E7%A8%8B/" rel="tag">kotlin, 协程</a></li><li class="tag-list-item"><a class="tag-list-link" href="/tags/v8-WebAssembly/" rel="tag">v8, WebAssembly</a></li></ul>
</div>
</div>
<div class="widget-wrap">
<h3 class="widget-title">Tag Cloud</h3>
<div class="widget tagcloud">
<a href="/tags/Android-ROOM/" style="font-size: 10px;">Android, ROOM</a> <a href="/tags/NDK/" style="font-size: 10px;">NDK</a> <a href="/tags/TypeScript/" style="font-size: 10px;">TypeScript</a> <a href="/tags/WebAssembly/" style="font-size: 15px;">WebAssembly</a> <a href="/tags/WebView-JSBridge/" style="font-size: 10px;">WebView, JSBridge</a> <a href="/tags/android/" style="font-size: 20px;">android</a> <a href="/tags/android-jetpack-compose/" style="font-size: 10px;">android, jetpack compose</a> <a href="/tags/android-mmkv/" style="font-size: 20px;">android, mmkv</a> <a href="/tags/android-textview/" style="font-size: 10px;">android, textview</a> <a href="/tags/kotlin/" style="font-size: 10px;">kotlin</a> <a href="/tags/kotlin-coroutine/" style="font-size: 10px;">kotlin, coroutine</a> <a href="/tags/kotlin-%E5%8D%8F%E7%A8%8B/" style="font-size: 10px;">kotlin, 协程</a> <a href="/tags/v8-WebAssembly/" style="font-size: 10px;">v8, WebAssembly</a>
</div>
</div>
<div class="widget-wrap">
<h3 class="widget-title">Archives</h3>
<div class="widget">
<ul class="archive-list"><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/11/">November 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/09/">September 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/08/">August 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/07/">July 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/05/">May 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/03/">March 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/02/">February 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2020/01/">January 2020</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/12/">December 2019</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/11/">November 2019</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/09/">September 2019</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/07/">July 2019</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/06/">June 2019</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/05/">May 2019</a></li><li class="archive-list-item"><a class="archive-list-link" href="/archives/2019/03/">March 2019</a></li></ul>
</div>
</div>
<div class="widget-wrap">
<h3 class="widget-title">Recent Posts</h3>
<div class="widget">
<ul>
<li>
<a href="/2020/11/14/Android%E5%B5%8C%E5%A5%97%E6%BB%9A%E5%8A%A8/">Android嵌套滚动</a>
</li>
<li>
<a href="/2020/09/13/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5/">FFMpeg-Android开发-简单音视频同步</a>
</li>
<li>
<a href="/2020/09/12/Kotlin%E5%8D%8F%E7%A8%8B%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86/">Kotlin协程异常处理</a>
</li>
<li>
<a href="/2020/08/29/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E9%9F%B3%E9%A2%91/">FFMpeg-Android开发-简单播放音频</a>
</li>
<li>
<a href="/2020/08/23/FFMpeg-Android%E5%BC%80%E5%8F%91-%E7%AE%80%E5%8D%95%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91/">FFMpeg-Android开发-简单播放视频</a>
</li>
</ul>
</div>
</div>
</aside>
</div>
<footer id="footer">
<div class="outer">
<div id="footer-info" class="inner">
© 2020 Jian Guo<br>
Powered by <a href="http://hexo.io/" target="_blank">Hexo</a>
</div>
</div>
</footer>
</div>
<nav id="mobile-nav">
<a href="/" class="mobile-nav-link">Home</a>
<a href="/archives" class="mobile-nav-link">Archives</a>
</nav>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<link rel="stylesheet" href="/fancybox/jquery.fancybox.css">
<script src="/fancybox/jquery.fancybox.pack.js"></script>
<script src="/js/script.js"></script>
</div>
</body>
</html>