-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathatom.xml
524 lines (255 loc) · 376 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>刘阳</title>
<subtitle>LiuYang's blog</subtitle>
<link href="https://handsomeliuyang.github.io/atom.xml" rel="self"/>
<link href="https://handsomeliuyang.github.io/"/>
<updated>2024-10-14T03:54:12.867Z</updated>
<id>https://handsomeliuyang.github.io/</id>
<author>
<name>[object Object]</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>框架设计的重新思考与 HarmonyOS Next 实践</title>
<link href="https://handsomeliuyang.github.io/2024/10/12/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/"/>
<id>https://handsomeliuyang.github.io/2024/10/12/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/</id>
<published>2024-10-12T06:32:24.000Z</published>
<updated>2024-10-14T03:54:12.867Z</updated>
<content type="html"><![CDATA[<p>在鸿蒙化过程中,我们不仅对框架设计进行了深刻反思,更通过实际应用规避了安卓系统中的历史包袱与遗留问题。高内聚、低耦合等设计原则虽然耳熟能详,但真正落地时依然充满挑战。因此,本文将聚焦实际问题,结合鸿蒙化的设计思考,带大家一探究竟。</p><h1 id="整体架构:组件化"><a href="#整体架构:组件化" class="headerlink" title="整体架构:组件化"></a>整体架构:组件化</h1><h2 id="第一版架构图:组件化设计"><a href="#第一版架构图:组件化设计" class="headerlink" title="第一版架构图:组件化设计"></a>第一版架构图:组件化设计</h2><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/%E7%BB%84%E4%BB%B6%E5%8C%96.drawio.svg"></p><p>架构特点及问题分析:</p><ol><li>基础组件接口暴露过多:<ol><li>问题1:配置接口与使用接口统一暴露,容易导致配置接口被多次调用,出现重复初始化问题。</li><li>问题2:暴露过多的接口,尤其第三方团队维护的组件(如分享库),实际业务中使用的接口寥寥无几,增加了单元测试的难度与成本。</li></ol></li><li>接口与实现高度耦合: <ol><li>问题1:以实现角度设计接口,增加接口的使用成本,开发者难以迅速上手。</li><li>问题2:接口变更成本过低,难以控制变更规则,导致迭代时单元测试无法复用,测试成本大幅上升。</li></ol></li></ol><p>解决方案:提取服务抽象层,严格区分业务接口与实现。</p><h2 id="第二架构图:组件化-服务抽象层"><a href="#第二架构图:组件化-服务抽象层" class="headerlink" title="第二架构图:组件化 + 服务抽象层"></a>第二架构图:组件化 + 服务抽象层</h2><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/%E7%BB%84%E4%BB%B6%E5%8C%96-%E6%9C%8D%E5%8A%A1%E6%8A%BD%E8%B1%A1%E5%B1%82.drawio.svg"></p><h3 id="服务设计规范:"><a href="#服务设计规范:" class="headerlink" title="服务设计规范:"></a>服务设计规范:</h3><h3 id="服务规范1:只暴露必要接口,屏蔽不必要的配置接口"><a href="#服务规范1:只暴露必要接口,屏蔽不必要的配置接口" class="headerlink" title="服务规范1:只暴露必要接口,屏蔽不必要的配置接口"></a>服务规范1:只暴露必要接口,屏蔽不必要的配置接口</h3><p>以网络组件库为例,我们将服务接口控制在业务需求范围内,避免暴露配置接口或未使用的接口:</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/network-api.jpg"></p><h3 id="服务规范2:聚合接口,提升业务使用便捷性"><a href="#服务规范2:聚合接口,提升业务使用便捷性" class="headerlink" title="服务规范2:聚合接口,提升业务使用便捷性"></a>服务规范2:聚合接口,提升业务使用便捷性</h3><p>为了简化业务调用,我们在接口层充当业务的BFF层,聚合接口。例如,使用Proxy实现依赖注入,确保接口调用与直接使用组件库保持一致,避免了依赖注解或容器管理带来的繁琐操作。</p><p>1、使用Proxy来实现依赖注入,而不采用注解或依赖管理容器来提供依赖入,保障与直接组件库的使用方式的一致,在Android中,我们使用的是依赖注入容器管理类,导致每次使用前,都需要先获取相应的服务才能使用</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/city-get-api.jpg"></p><p>2、对数据做聚合,减少Api的调用次数。例如,登录库中获取PPU、UserId、Ticket票据分别属于不同模块,但业务开发者不需要关注这些细节,接口层将这些细节屏蔽。</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/wblogin.jpg"></p><h3 id="服务规范3:异步化操作,解决性能瓶颈"><a href="#服务规范3:异步化操作,解决性能瓶颈" class="headerlink" title="服务规范3:异步化操作,解决性能瓶颈"></a>服务规范3:异步化操作,解决性能瓶颈</h3><p>对于耗时操作(如IO、网络请求等),我们采用Promise异步调用。在安卓中,像SharedPreferences、MMKV等库大多是同步方法,虽然简便,但在大规模应用中,容易出现卡顿现象。通过异步化,避免主线程阻塞。</p><p>ArtTS语言提供了Promise异步调用,以及await同步写法写异步调用过程,在鸿蒙化的服务中,涉及到IO等操作的Api都应返回Promise,用白屏或Loading来解决卡顿问题</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/login-await.jpg"></p><p>当不支持异步时,我们通过状态回调解决问题。例如,在Web设置Cookie时,为了应对异步返回,我们需在onLoadIntercept()方法中拦截URL加载,确保Cookie设置完成后再继续加载URL。</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/web-setcookie.png"></p><h3 id="服务规范4:Impl实现库的依赖模块延迟初始化,提升性能"><a href="#服务规范4:Impl实现库的依赖模块延迟初始化,提升性能" class="headerlink" title="服务规范4:Impl实现库的依赖模块延迟初始化,提升性能"></a>服务规范4:Impl实现库的依赖模块延迟初始化,提升性能</h3><p>为了减少冷启动时间,我们采用延时初始化策略,避免依赖模块在构造函数中初始化,确保逻辑不受始化顺序影响。</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/wmda-connect.png"></p><h3 id="服务规范5:测试覆盖率要求"><a href="#服务规范5:测试覆盖率要求" class="headerlink" title="服务规范5:测试覆盖率要求"></a>服务规范5:测试覆盖率要求</h3><p>服务库的单元测试必须覆盖100%的使用接口,由于不与具体实现库耦合,将会大幅提升测试Case使用率</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/service-api-test.png"></p><h3 id="服务规范6:抽象层变动需严格审核"><a href="#服务规范6:抽象层变动需严格审核" class="headerlink" title="服务规范6:抽象层变动需严格审核"></a>服务规范6:抽象层变动需严格审核</h3><p>由于服务抽象层的变动会影响到所有业务,必须经过严格审核。实现层的迭代则通过完善的测试用例来保障质量,降低线上风险。</p><h1 id="业务功能:模块化拆分"><a href="#业务功能:模块化拆分" class="headerlink" title="业务功能:模块化拆分"></a>业务功能:模块化拆分</h1><p>传统的业务模块划分以BG为单位,颗粒度较大,随着业务规模增大,模块变得臃肿。在鸿蒙化过程中,我们进一步细化拆分,按功能模块进行划分,带来了以下好处:</p><ol><li>便于分析包大小。</li><li>不同场景下实现按需加载或替换,尤其适合厂商定制需求。</li><li>满足元服务对包大小的要求,实现分包加载。</li></ol><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/feature-split.png"></p><h1 id="View(视图层):MVVM架构"><a href="#View(视图层):MVVM架构" class="headerlink" title="View(视图层):MVVM架构"></a>View(视图层):MVVM架构</h1><p>鸿蒙的双向数据绑定大大简化了View层与ViewModel层的通信。构建MVVM框架时,我们仅需关注以下关键点:</p><ol><li>ViewModel设计原则:按相关性创建ViewModel,确保相关属性聚合在同一ViewModel中,而非散落于View层。</li></ol><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/MVVM.jpg"></p><h1 id="Model(数据层):单一数据源,简版Repository架构"><a href="#Model(数据层):单一数据源,简版Repository架构" class="headerlink" title="Model(数据层):单一数据源,简版Repository架构"></a>Model(数据层):单一数据源,简版Repository架构</h1><p>我们采用单一数据源的设计来解耦数据层,以Model层为例,所有数据操作均通过单一接口调用,避免多源数据导致的复杂性。</p><p>以Model举例:</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/model.jpg"></p><p>在实现库中也是如此,如下图所示,简化了实现逻辑,确保各模块的独立性与可维护性。</p><p><img src="/../../hexo-img/2024-10-12-%E9%B8%BF%E8%92%99%E5%8C%96%E7%9A%84%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%80%83/wbloginimpl.png"></p>]]></content>
<summary type="html"><p>在鸿蒙化过程中,我们不仅对框架设计进行了深刻反思,更通过实际应用规避了安卓系统中的历史包袱与遗留问题。高内聚、低耦合等设计原则虽然耳熟能详,但真正落地时依然充满挑战。因此,本文将聚焦实际问题,结合鸿蒙化的设计思考,带大家一探究竟。</p>
<h1 id="整体架构:组件化"</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="HarmonyOS" scheme="https://handsomeliuyang.github.io/tags/HarmonyOS/"/>
</entry>
<entry>
<title>Android内核学习笔记:Android进程\线程管理</title>
<link href="https://handsomeliuyang.github.io/2023/10/08/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-Android%E5%86%85%E6%A0%B8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%9AAndroid%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E7%AE%A1%E7%90%86/"/>
<id>https://handsomeliuyang.github.io/2023/10/08/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-Android%E5%86%85%E6%A0%B8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%9AAndroid%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E7%AE%A1%E7%90%86/</id>
<published>2023-10-08T02:53:20.121Z</published>
<updated>2023-10-08T02:53:20.121Z</updated>
<content type="html"><![CDATA[<h1 id="Android程序启动过程"><a href="#Android程序启动过程" class="headerlink" title="Android程序启动过程"></a>Android程序启动过程</h1><p><img src="/Android%E7%A8%8B%E5%BA%8F%E7%9A%84%E5%90%AF%E5%8A%A8%E6%B5%81%E7%A8%8B.png"></p><ol><li>ActivityManagerService与WindowManagerService在独立的进程里,与程序进度之间的通信通过Bindler进行</li><li>每个应用程序都是运行在独立的进程里的,进程与进程之间无法直接通信,每个进程里都一个JVM虚拟机,不能通过static进行通信</li><li>应用程序的进程是由ActivityManagerService通过Process.start(“android.app.ActivityThread”)创建的,进程创建后,会同时创建一个线程,这个线程就是我们所说的UIThread。</li><li>同一个进程里的Activity, Service等等四大组件都是运行在ActivityThread里,即UI线程里的。所以通常我们要在Service里创建一个Thread来真正执行后台程序</li><li>应用程序启动后,除了创建AcivityThread后,还会创建两个BindlerThread,作用就是用于与AMS,WMS进行交互的。</li></ol><h1 id="什么是线程"><a href="#什么是线程" class="headerlink" title="什么是线程"></a>什么是线程</h1><p><strong>Runnable是不是线程?</strong></p><p>不是,Runnable只是一个接口,用于创建线程的接口类</p><p><strong>Thread是不是线程?</strong></p><p>不是,Thread只有在调用thread.start()方法后,才会创建一个Thread出来,之前的所有的初始化步骤都是在当前线程里执行的。Thread.start()方法如下:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">start</span><span class="params">()</span>{</span><br><span class="line">checkNotStarted();</span><br><span class="line">hasBeanStarted = <span class="literal">true</span>;</span><br><span class="line">VMThread.create(<span class="built_in">this</span>, stackSize); <span class="comment">// 这里才是真正创建一个CPU线程的地方</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>只有当VMThread.create()方法之后,才会创建一个真正的线程。</p><h1 id="Android的UIThread"><a href="#Android的UIThread" class="headerlink" title="Android的UIThread"></a>Android的UIThread</h1><p>Android有四大组件:Activity,Service,ContentProvider,Broadcast。组各自的功能:</p><ol><li>Activity:界面,生命周期:onCreate(), …</li><li>Service:后台服务,生命周期:onCreate(), …</li><li>Broadcast:广播,生命周期:onReceive()</li><li>ContentProvider:用于数据共享,生命周期:onCreate(), …</li></ol><p><strong>四大组件的运行哪个进程,哪个线程里呢?</strong></p><ol><li>默认情况下:四大组件都是运行在以程序的包名命名的进程里,</li><li>四大组件都是运行在UIThread里,但注意:是其生命周期方法是运行在UIThread里。如ContentProvider的query()等等方法的执行线程要依调用方来决定</li><li>Service的生命周期是运行在UIThread里,我们需要执行的后台任务,需要创建一个子线程来执行</li><li>四大组件,Activity,Service,Broadcast都是需要时,系统进行创建,但ContentProvider例外,其是在应用进程启动时,就会开发创建。</li></ol><h1 id="Android的编程框架"><a href="#Android的编程框架" class="headerlink" title="Android的编程框架"></a>Android的编程框架</h1><p>从开始接触Android开始,我们都是面向四大组件及四大组件的生命周期方法来进行编程。<br>但学过C程序开发的都知道,应用程序都是从main()方法开始执行,再执行一个while()循环,不停接收事件,再处理事件的过程。<br>Android的事件驱动流程:<br><img src="/Android%E7%A8%8B%E5%BA%8F%E6%B5%81%E7%A8%8B.png"></p><ol><li>由AMS创建应用程序进程,并创建UIThread,通过Looper.loop(),让UIThread进入事件驱动循环中</li><li>四大组件的生命周期方法,用户交互等等都当作Message,进入MessageQueue里,进入UIThread的事件驱动循环中。</li></ol><h1 id="ANR异常"><a href="#ANR异常" class="headerlink" title="ANR异常"></a>ANR异常</h1><p>概念:ANR(Application No Response)用户点击屏幕后,如果5s没有处理完成此点击Event,就会报ANR异常</p><p>ANR发生的情况:</p><ol><li>在UIThread里执行网络请求,IO操作等等耗时操作</li><li>UI绘制时间过长,也有可能造成ANR异常</li><li>ANR异常很多时候不是由一个耗时操作造成的,很多是由一组操作,如进行10000次SP读写操作。</li></ol>]]></content>
<summary type="html"><h1 id="Android程序启动过程"><a href="#Android程序启动过程" class="headerlink" title="Android程序启动过程"></a>Android程序启动过程</h1><p><img src="/Android%E7%A8%8</summary>
<category term="日常学习" scheme="https://handsomeliuyang.github.io/categories/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0/"/>
<category term="Android内核学习笔记" scheme="https://handsomeliuyang.github.io/tags/Android%E5%86%85%E6%A0%B8%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
</entry>
<entry>
<title>ASM 学习心得</title>
<link href="https://handsomeliuyang.github.io/2023/08/08/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-ASM%E5%AD%A6%E4%B9%A0%E5%BF%83%E5%BE%97/"/>
<id>https://handsomeliuyang.github.io/2023/08/08/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-ASM%E5%AD%A6%E4%B9%A0%E5%BF%83%E5%BE%97/</id>
<published>2023-08-08T12:00:00.000Z</published>
<updated>2023-10-08T02:53:20.128Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>阅读了<a href="https://juejin.cn/post/7042328862872567838">ASM 字节码插桩:实现双击防抖</a>这篇博客之后,激发了实际操作的念头。在动手编程过程中,遇到了一些新问题,记录一下解决方案。</p><h1 id="Aop编程第一步:确定-Hook-点"><a href="#Aop编程第一步:确定-Hook-点" class="headerlink" title="Aop编程第一步:确定 Hook 点"></a>Aop编程第一步:确定 Hook 点</h1><p>ASM字节码插桩的对象不是Java代码或Kotlin代码,需要从字节码中找规律,找到可以定位的Hook点。</p><p>以“实现双击防抖”为例子,说明一下操作步骤:</p><ol><li>枚举出所有给View设置onClickListener的可能写法,包括内部类,Lambda表达式等不同写法</li><li>编译代码后,分析生成的class类文件</li><li>通过<code>javap -v -p xx.class</code>命令,查看字节码的规律,若遇到难以理解的命令,随时向ChatGPT寻求指导。</li></ol><h1 id="Aop编程第二步:编写-ASM-代码"><a href="#Aop编程第二步:编写-ASM-代码" class="headerlink" title="Aop编程第二步:编写 ASM 代码"></a>Aop编程第二步:编写 ASM 代码</h1><p>一般是通过“ASM Bytecode Viewer”插件来看查看字节码所对就的ASM代码,实际操作过程中,我遇到三个问题:</p><ol><li>在升级Android Studio到2022.3.1版本后,运行“ASM Bytecode Viewer”插件总是报错</li><li>Android Studio不显示编译后的内部类文件,只能看到外部类文件</li><li>通过“ASM Bytecode Viewer”生成的ASM代码包括了Debug调试信息和字节码验证信息,需要手动删减代码</li></ol><p>通过ChatGPT4的辅助,编写了一个功能类似“ASM Bytecode Viewer”的Java工具类,完美的解决了上述问题:</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> BytecodeToASM {</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">toASM</span><span class="params">(classPath: <span class="type">String</span>)</span></span>{</span><br><span class="line"> <span class="keyword">val</span> inputStream = FileInputStream(classPath)</span><br><span class="line"> <span class="keyword">val</span> classReader = ClassReader(inputStream)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> printWriter = PrintWriter(System.<span class="keyword">out</span>)</span><br><span class="line"> <span class="keyword">val</span> traceClassVisitor = TraceClassVisitor(<span class="literal">null</span>, ASMifier(), printWriter)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ClassReader.SKIP_DEBUG and ClassReader.SKIP_FRAMES 去掉 Debug调试信息 和 字节码验证信息</span></span><br><span class="line"> classReader.accept(traceClassVisitor, ClassReader.SKIP_DEBUG and ClassReader.SKIP_FRAMES)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">toBytecodeByJavap</span><span class="params">(classPath: <span class="type">String</span>)</span></span> {</span><br><span class="line"> <span class="keyword">val</span> process = Runtime.getRuntime().exec(<span class="string">"javap -v -p <span class="variable">$classPath</span>"</span>)</span><br><span class="line"> <span class="keyword">val</span> reader = BufferedReader(InputStreamReader(process.inputStream))</span><br><span class="line"></span><br><span class="line"> reader.useLines { lines -></span><br><span class="line"> lines.forEach { println(it) }</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="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">(args: <span class="type">Array</span><<span class="type">String</span>>)</span></span> {</span><br><span class="line"> <span class="keyword">val</span> classPath = <span class="string">""</span></span><br><span class="line"></span><br><span class="line"> println(<span class="string">"----------以下是字节码 ----------"</span>)</span><br><span class="line"> BytecodeToASM.toBytecodeByJavap(classPath)</span><br><span class="line"></span><br><span class="line"> println(<span class="string">"----------以下是ASM代码 ----------"</span>)</span><br><span class="line"> BytecodeToASM.toASM(classPath)</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>以实现双击防抖为例:</p><p>Java代码</p><figure class="highlight plaintext"><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">if(!ViewClickMonitor.onClick(view)) {</span><br><span class="line"> return</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>字节码:</p><figure class="highlight plaintext"><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">6: aload_1</span><br><span class="line">7: invokestatic #34 // Method github/leavesczy/track/click/view/ViewClickMonitor.onClick:(Landroid/view/View;)Z</span><br><span class="line">10: ifne 14</span><br><span class="line">13: return</span><br></pre></td></tr></table></figure><p>ASM代码:</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></pre></td><td class="code"><pre><span class="line">methodVisitor.visitVarInsn(ALOAD, <span class="number">1</span>);</span><br><span class="line">methodVisitor.visitMethodInsn(INVOKESTATIC, <span class="string">"github/leavesczy/track/click/view/ViewClickMonitor"</span>, <span class="string">"onClick"</span>, <span class="string">"(Landroid/view/View;)Z"</span>, <span class="literal">false</span>);</span><br><span class="line"><span class="type">Label</span> <span class="variable">label2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Label</span>();</span><br><span class="line">methodVisitor.visitJumpInsn(IFNE, label2);</span><br><span class="line">methodVisitor.visitInsn(RETURN);</span><br><span class="line">methodVisitor.visitLabel(label2);</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>阅读了<a href="https://juejin.cn/post/7042328862872567838">ASM 字节码插桩:实现双击</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
<category term="ASM" scheme="https://handsomeliuyang.github.io/tags/ASM/"/>
<category term="AOP" scheme="https://handsomeliuyang.github.io/tags/AOP/"/>
</entry>
<entry>
<title>App的Repo多仓库管理</title>
<link href="https://handsomeliuyang.github.io/2023/02/22/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-App%E7%9A%84Repo%E5%A4%9A%E4%BB%93%E5%BA%93%E7%AE%A1%E7%90%86/"/>
<id>https://handsomeliuyang.github.io/2023/02/22/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-App%E7%9A%84Repo%E5%A4%9A%E4%BB%93%E5%BA%93%E7%AE%A1%E7%90%86/</id>
<published>2023-02-22T08:17:53.000Z</published>
<updated>2023-10-08T02:53:20.128Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>当项目做了组件化或模块化拆分后,为了方便管理,会分出很多的 <code>git</code> 仓库,这么多仓库的下载与上传的管理成本比较高,经常出现如下问题:</p><ol><li>找不到对应aar的仓库地址</li><li>需要手动clone当前所有的git 仓库</li><li>历史版本的源码分支,有时需要通过aar的版本号反查才能知到</li></ol><p>我们考虑了两种解决方案:<code>Git submodule</code> 和 <code>repo</code>。然而,git submodule的子模组需要固定在某个版本上,并且在多人修改子模块时,子模块的冲突不易解决。综合考虑之后,我们最终选择了 repo。</p><h1 id="Repo-简介"><a href="#Repo-简介" class="headerlink" title="Repo 简介"></a>Repo 简介</h1><blockquote><p>Repo is a tool built on top of Git. Repo helps manage many Git repositories, does the uploads to revision control systems, and automates parts of the development workflow. Repo is not meant to replace Git, only to make it easier to work with Git. The repo command is an executable Python script that you can put anywhere in your path.</p></blockquote><p>简单的理解:</p><ol><li>repo 建立在 git 之上的一个 python 脚本工具集</li><li>用于提升多创库管理效率</li></ol><h1 id="Repo-私域搭建"><a href="#Repo-私域搭建" class="headerlink" title="Repo 私域搭建"></a>Repo 私域搭建</h1><p>受网络影响,Repo 需要科学上网才能使用,为了减少使用的成本,有必要做私域镜像。</p><p>Repo的组成部分:</p><ol><li>Repo launcher (Repo 引导脚本)</li><li>Repo 命令的主体部分</li></ol><p>这样设计旨在使 Repo launcher只包含最基本的init、help两个命令,主体命令可实现自动更新,这将显著改善用户的使用体验。</p><p>私域镜像过程:</p><ol><li>通过科学上网,从<a href="https://code.google.com/archive/p/git-repo/">git-repo</a>官网上,克隆最新代码到本地<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">git 设置代理</span></span><br><span class="line">git config --global http.proxy 'socks5://127.0.0.1:7890'</span><br><span class="line">git config --global https.proxy 'socks5://127.0.0.1:7890'</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">clone</span> 仓库</span></span><br><span class="line">git clone https://gerrit.googlesource.com/git-repo </span><br></pre></td></tr></table></figure></li><li>删除 .git 和 .github 文件夹(原因是私域限制了提交的邮箱后缀)</li><li>私域上创建git-repo项目,再提交到私域</li><li>修改 Repo 的部分代码<figure class="highlight plaintext"><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">REPO_URL = os.environ.get('REPO_URL', None)</span><br><span class="line">if not REPO_URL:</span><br><span class="line"> REPO_URL = 'https://gerrit.googlesource.com/git-repo'</span><br><span class="line">REPO_REV = os.environ.get('REPO_REV')</span><br><span class="line">if not REPO_REV:</span><br><span class="line"> REPO_REV = 'stable'</span><br><span class="line"></span><br><span class="line">// 改为如下:</span><br><span class="line">REPO_URL = 'git@私域域名:git_mirror/git-repo.git'</span><br><span class="line">REPO_REV = 'master'</span><br></pre></td></tr></table></figure></li></ol><h1 id="Repo-私域安装步骤"><a href="#Repo-私域安装步骤" class="headerlink" title="Repo 私域安装步骤"></a>Repo 私域安装步骤</h1><ol><li>下载 Repo launcher(即repo文件):https://私域域名/git_mirror/git-repo/-/raw/master/repo?inline=false</li><li>修改 repo 的权限,配置环境变量<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">chmod 777 xxx/repo</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">~/.zshrc</span></span><br><span class="line">export PATH="xxx:$PATH"</span><br></pre></td></tr></table></figure></li></ol><h1 id="Repo开发流"><a href="#Repo开发流" class="headerlink" title="Repo开发流"></a>Repo开发流</h1><ol><li>创建 manifest 仓库,并创建manifest清单文件<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version=<span class="string">"1.0"</span>?></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">manifest</span>></span></span><br><span class="line"> <span class="comment"><!-- git服务器配置 --></span></span><br><span class="line"> <span class="tag"><<span class="name">remote</span></span></span><br><span class="line"><span class="tag"> <span class="attr">name</span>=<span class="string">"private"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">fetch</span>=<span class="string">"ssh://git@私域域名/"</span>/></span></span><br><span class="line"></span><br><span class="line"> <span class="comment"><!-- 默认配置</span></span><br><span class="line"><span class="comment"> sync-j:指定并发下载数</span></span><br><span class="line"><span class="comment"> revision:分支名,指tag的方式“refs/tags/tag名称”</span></span><br><span class="line"><span class="comment"> remote:指定默认从哪个git服务下载</span></span><br><span class="line"><span class="comment"> --></span></span><br><span class="line"> <span class="tag"><<span class="name">default</span></span></span><br><span class="line"><span class="tag"> <span class="attr">revision</span>=<span class="string">"master"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">remote</span>=<span class="string">"private"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">sync-j</span>=<span class="string">"4"</span>/></span></span><br><span class="line"></span><br><span class="line"> <span class="comment"><!-- project配置</span></span><br><span class="line"><span class="comment"> clone-depth:用于加快clone速度</span></span><br><span class="line"><span class="comment"> name:groupname/projectname</span></span><br><span class="line"><span class="comment"> path: 本地目录路径</span></span><br><span class="line"><span class="comment"> groups:用于分类使用,可用于按group下载</span></span><br><span class="line"><span class="comment"> --></span></span><br><span class="line"> <span class="tag"><<span class="name">project</span></span></span><br><span class="line"><span class="tag"> <span class="attr">clone-depth</span>=<span class="string">"1"</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">name</span>=<span class="string">"group/project1"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">path</span>=<span class="string">"project1"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">groups</span>=<span class="string">"main"</span>/></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">manifest</span>></span></span><br></pre></td></tr></table></figure></li><li>初始化:<code>repo init</code><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">repo init -u git@私域域名:/manifest.git [-b 分支名] [-g group,group]</span><br><span class="line"></span><br><span class="line">-b 指定版本分支,可选,默认all</span><br><span class="line">-g 指定group名称,可选,默认是all</span><br></pre></td></tr></table></figure></li><li>同步代码:<code>repo sync</code></li><li>创建本地分支:<code>repo start f-xxx --all</code></li><li>IDE 开发及代码提交</li><li>同步代码:<code>repo rebase</code></li><li>删除开发分支:<code>repo abandon 分支名</code></li></ol><h1 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a>使用场景</h1><h2 id="场景1:管理58App相关的所有Git仓库"><a href="#场景1:管理58App相关的所有Git仓库" class="headerlink" title="场景1:管理58App相关的所有Git仓库"></a>场景1:管理58App相关的所有Git仓库</h2><p>58App相关的所有Git仓库,不仅仅代码仓库,还有很多的工具仓库,如doc文档、CodeTure、Python比对脚本等等</p><p>统一配置到manifest之后,可带来如下好处:</p><ol><li>快速找到所有相关的仓库</li><li>实现全局搜索,按 Google 内部的调研,发现平均每位工程师每天会执行 12 个代码搜索请求</li><li>代码阅读笔记可实现快速共享</li></ol><p>创建一个manifest仓库,依据58App的发版方式,创建对应的版本分支,每个分支下,配置相应的default.xml文件</p><h2 id="场景2:管理各种厂商预装包的特殊要求"><a href="#场景2:管理各种厂商预装包的特殊要求" class="headerlink" title="场景2:管理各种厂商预装包的特殊要求"></a>场景2:管理各种厂商预装包的特殊要求</h2><p>类似Flipper一样,不用去分散的写各种python脚本,解决工具查找的问题。</p><p>manifest的配置如上一样</p><h2 id="场景3:管理各种有业务差异的马甲包,如早期的58同城与58本地"><a href="#场景3:管理各种有业务差异的马甲包,如早期的58同城与58本地" class="headerlink" title="场景3:管理各种有业务差异的马甲包,如早期的58同城与58本地"></a>场景3:管理各种有业务差异的马甲包,如早期的58同城与58本地</h2><p>一体化项目之前,58本地fork了一些底层库,导致在下载相应源码库时,容易出现混淆。</p>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>当项目做了组件化或模块化拆分后,为了方便管理,会分出很多的 <code>git</code> 仓库,这么多仓库的下载与上传的管理成本比较高,</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
<category term="python" scheme="https://handsomeliuyang.github.io/tags/python/"/>
</entry>
<entry>
<title>从开发者角度思考单元测试的价值</title>
<link href="https://handsomeliuyang.github.io/2022/11/02/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E4%BB%8E%E5%BC%80%E5%8F%91%E8%80%85%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BB%B7%E5%80%BC/"/>
<id>https://handsomeliuyang.github.io/2022/11/02/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E4%BB%8E%E5%BC%80%E5%8F%91%E8%80%85%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BB%B7%E5%80%BC/</id>
<published>2022-11-02T07:13:00.000Z</published>
<updated>2023-10-08T02:53:20.131Z</updated>
<content type="html"><![CDATA[<h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>把 Logseq 上的知识点笔记自动转换为 Anki 的卡片,实现 Logseq 上做知识归类,在 Anki 上做科学复习</p><p>Logseq 上的知识笔记:<br><img src="/../../hexo-img/%E4%BB%8E%E5%BC%80%E5%8F%91%E8%80%85%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BB%B7%E5%80%BC/2022-11-02-15-32-34.png"></p><p>自动转换 Anki 上的复习卡片效果:</p><table><thead><tr><th>卡片正面</th><th>卡片背面</th></tr></thead><tbody><tr><td><img src="/../../hexo-img/%E4%BB%8E%E5%BC%80%E5%8F%91%E8%80%85%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BB%B7%E5%80%BC/2022-11-02-15-35-59.png"></td><td><img src="/../../hexo-img/%E4%BB%8E%E5%BC%80%E5%8F%91%E8%80%85%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BB%B7%E5%80%BC/2022-11-02-15-36-23.png"></td></tr></tbody></table><h1 id="实现分析"><a href="#实现分析" class="headerlink" title="实现分析"></a>实现分析</h1><p>整个功能分为三部分:</p><ol><li>markdown 解析</li><li>标记转换</li><li>Anki 卡片生成</li></ol><p>通过 google ,了解有二个开源的 Python 库:</p><ol><li><a href="https://github.com/lepture/mistune">lepture/mistune</a>:一个快速而强大的Python Markdown解析器,带有渲染器和插件,支持扩展</li><li><a href="https://github.com/kerrickstaley/genanki">kerrickstaley/genanki</a>:通过 Python 3,以编程方式生成 Anki 的 Deck。</li></ol><p>借助这两个开源库,整体的实现思路:</p><ol><li>markdown 解析:通过<code>mistune</code>解析出<code>#anki</code>标记的知识点</li><li>标记转换:通过正则表达式,把 markdown 的高亮标记 <code>==高亮内容==</code> 转换为 Anki 的填空标记 <code>{{c1::填空内容}}</code></li><li>Anki 卡片生成:通过<code>genanki</code>生成 Anki 的 Deck。</li></ol><h1 id="常规开发方式"><a href="#常规开发方式" class="headerlink" title="常规开发方式"></a>常规开发方式</h1><p>有了上面的整体的实现思路,常规开发方式,会采用<code>总分</code>的方式来编写代码,如下所示:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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></pre></td><td class="code"><pre><span class="line">....</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">AutoGenanki</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, package_path</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">add_cloze_notes</span>(<span class="params">self, texts</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">write_to_file</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ParseAnkiNote</span>:</span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">__init__</span>(<span class="params">self, logseq_file</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">start</span>(<span class="params">self</span>):</span><br><span class="line"> <span class="keyword">pass</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> <span class="comment"># 解析 markdown,提取知识点,标记转换</span></span><br><span class="line"> parse_anki_note = ParseAnkiNote(os.path.join(logseq_journals_path, <span class="string">'2022_08_02.md'</span>))</span><br><span class="line"> note_texts = parse_anki_note.start()</span><br><span class="line"></span><br><span class="line"> <span class="comment"># Anki 卡片生成</span></span><br><span class="line"> auto_gen_anki = AutoGenanki(package_path=<span class="string">'./build/output.apkg'</span>)</span><br><span class="line"> auto_gen_anki.add_cloze_notes(note_texts)</span><br><span class="line"> auto_gen_anki.write_to_file()</span><br></pre></td></tr></table></figure><p>整体过程为:</p><ol><li>先写框架,忽略细节,提前做好各个模块的衔接</li><li>逐个填充每个子模块的细节</li></ol><p>这种开发模式存在的问题:</p><ol><li>框架运行成本高:<ol><li>为了让整个框架框架可运行,需要给每个子模块填充假实现</li><li>构造各种输入数据,用于模拟各自测试 Case</li></ol></li><li>子组件的运行成本高:<ol><li>子组件需求依赖整体的框架运行,导致运行速度慢,</li><li>各子组件需要提交完整代码,不能提交影响运行的代码,并行开发时,会相互影响</li></ol></li></ol><h1 id="结合单元测试的开发方式"><a href="#结合单元测试的开发方式" class="headerlink" title="结合单元测试的开发方式"></a>结合单元测试的开发方式</h1><p>通过单元测试来搭建整体的框架,关键是定义好各组件之间的通信接口</p><p>项目结构如下:<br><img src="/../../hexo-img/%E4%BB%8E%E5%BC%80%E5%8F%91%E8%80%85%E8%A7%92%E5%BA%A6%E6%80%9D%E8%80%83%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E7%9A%84%E4%BB%B7%E5%80%BC/2022-11-08-17-00-59.png"></p><p>借助单元测试模块,完美的解决上述开发模式的问题。</p><p><strong>好处1:子组件支持独立运行,且相互不影响</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">TestLogseqMarkdownParser</span>(unittest.TestCase):</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">test_anki_markdown_parser</span>(<span class="params">self</span>):</span><br><span class="line"> md_text = <span class="string">'''</span></span><br><span class="line"><span class="string">- **Android**</span></span><br><span class="line"><span class="string"> collapsed:: true</span></span><br><span class="line"><span class="string"> - Gradle的Task: #anki</span></span><br><span class="line"><span class="string"> - Task的组成:==由一系列Action (动作)组成的==</span></span><br><span class="line"><span class="string"> - 增加Action的方法:==doFirst、doLast、leftShift、<< ==</span></span><br><span class="line"><span class="string"> - 代码识别:</span></span><br><span class="line"><span class="string"> ` ` `</span></span><br><span class="line"><span class="string"> task myTask1 {</span></span><br><span class="line"><span class="string"> println "123" ==配置阶段执行的配置代码==</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"> ` ` `</span></span><br><span class="line"><span class="string">- python</span></span><br><span class="line"><span class="string"> - pyhton字典访问元素: #anki</span></span><br><span class="line"><span class="string"> - dict[key],如果key不存在,==抛出 KeyError 异常==</span></span><br><span class="line"><span class="string"> - dict.get(key),如果key不存在,==返回默认值 None==</span></span><br><span class="line"><span class="string"> - 指定key不存在时的返回值:==dict.get(key, 0.0)==</span></span><br><span class="line"><span class="string"> '''</span></span><br><span class="line"> html_text = AnkiMarkdownParser.parse_md_to_html(md_text)</span><br><span class="line"> <span class="built_in">print</span>(html_text)</span><br><span class="line"> <span class="variable language_">self</span>.assertNotIn(<span class="string">"#anki"</span>, html_text)</span><br><span class="line"> <span class="variable language_">self</span>.assertNotIn(<span class="string">"=="</span>, html_text)</span><br><span class="line"> <span class="variable language_">self</span>.assertIn(<span class="string">"{{c1::"</span>, html_text)</span><br><span class="line"></span><br><span class="line"> cloze_dict = AnkiMarkdownParser.html_to_dict(html_text)</span><br><span class="line"> <span class="built_in">print</span>(cloze_dict)</span><br><span class="line"> <span class="variable language_">self</span>.assertTrue(cloze_dict.__contains__(<span class="string">'Android'</span>))</span><br><span class="line"> <span class="variable language_">self</span>.assertTrue(cloze_dict.__contains__(<span class="string">'python'</span>))</span><br></pre></td></tr></table></figure><p><strong>好处2:正则表达式等需要独立测试的核心逻辑,可单且快速的运行并测试</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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="keyword">class</span> <span class="title class_">TestAnkiGenerate</span>(unittest.TestCase):</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">test_code_one_line</span>(<span class="params">self</span>):</span><br><span class="line"> code = <span class="string">'''</span></span><br><span class="line"><span class="string">task myTask1 {</span></span><br><span class="line"><span class="string"> println "123" ==@1==</span></span><br><span class="line"><span class="string"> pringln "456" ==@2==</span></span><br><span class="line"><span class="string"> pringln "23984" ==@3==</span></span><br><span class="line"><span class="string">}</span></span><br><span class="line"><span class="string"> '''</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">tag_text</span>(<span class="params">matched</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'{{c1::'</span> + matched.group(<span class="number">1</span>) + <span class="string">'}}'</span></span><br><span class="line"></span><br><span class="line"> new_code = re.sub(<span class="string">r'==(.+?)=='</span>, tag_text, code)</span><br><span class="line"> <span class="built_in">print</span>(new_code)</span><br><span class="line"> <span class="variable language_">self</span>.assertNotIn(<span class="string">"=="</span>, new_code)</span><br><span class="line"> <span class="variable language_">self</span>.assertIn(<span class="string">"{{c1::"</span>, new_code)</span><br></pre></td></tr></table></figure><p><strong>好处3:单元测试的用case就相当于使用文档,不用额外编写使用文档</strong></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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="keyword">class</span> <span class="title class_">TestAnkiGenerate</span>(unittest.TestCase):</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">test_cloze_gen</span>(<span class="params">self</span>):</span><br><span class="line"> cloze_gen = ClozeGenerate(<span class="string">'../_build'</span>)</span><br><span class="line"></span><br><span class="line"> cloze_dict = {</span><br><span class="line"> <span class="string">"Android"</span>: [</span><br><span class="line"> <span class="string">"cloze卡片1"</span>,</span><br><span class="line"> <span class="string">"cloze卡片2"</span></span><br><span class="line"> ],</span><br><span class="line"> <span class="string">"阅读"</span>: [</span><br><span class="line"> <span class="string">"cloze卡片1"</span>,</span><br><span class="line"> <span class="string">"cloze卡片2"</span></span><br><span class="line"> ]</span><br><span class="line"> }</span><br><span class="line"> cloze_gen.add_cloze_notes(cloze_dict)</span><br><span class="line"> filepath = cloze_gen.write_to_file()</span><br><span class="line"> <span class="variable language_">self</span>.assertTrue(os.path.exists(filepath))</span><br></pre></td></tr></table></figure><p><font color="#ff0000">注意:</font>尤其是 python 这种弱类型语言,或者函数有字典参数的功能,好处3的意义更大</p><p><strong>好处4:后期功能迭代时,通过阅读单元测试,可以快速了解子组件的各种边界条件,通过运行历史的单元测试,又可以提升代码迭代的质量</strong></p><p>下面的代码是扩展多行填空卡片支持的迭代需求:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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="keyword">class</span> <span class="title class_">TestAnkiGenerate</span>(unittest.TestCase):</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">test_code_one_line</span>(<span class="params">self</span>):</span><br><span class="line"> code = <span class="string">'''</span></span><br><span class="line"><span class="string">task myTask1 {</span></span><br><span class="line"><span class="string"> println "123" ==@1==</span></span><br><span class="line"><span class="string"> pringln "456" ==@2==</span></span><br><span class="line"><span class="string"> pringln "23984" ==@3==</span></span><br><span class="line"><span class="string">}</span></span><br><span class="line"><span class="string"> '''</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">tag_text</span>(<span class="params">matched</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'{{c1::'</span> + matched.group(<span class="number">1</span>) + <span class="string">'}}'</span></span><br><span class="line"></span><br><span class="line"> new_code = re.sub(<span class="string">r'==(.+?)=='</span>, tag_text, code)</span><br><span class="line"> <span class="built_in">print</span>(new_code)</span><br><span class="line"> <span class="variable language_">self</span>.assertNotIn(<span class="string">"=="</span>, new_code)</span><br><span class="line"> <span class="variable language_">self</span>.assertIn(<span class="string">"{{c1::"</span>, new_code)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">test_code_multi_line</span>(<span class="params">self</span>):</span><br><span class="line"> code = <span class="string">'''</span></span><br><span class="line"><span class="string"> task myTask1 {</span></span><br><span class="line"><span class="string"> ====println "123"</span></span><br><span class="line"><span class="string"> pringln "456"</span></span><br><span class="line"><span class="string"> pringln "23984"====</span></span><br><span class="line"><span class="string"> ====println "2335"</span></span><br><span class="line"><span class="string"> println "35894"====</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"> '''</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">def</span> <span class="title function_">tag_text</span>(<span class="params">matched</span>):</span><br><span class="line"> <span class="keyword">return</span> <span class="string">'{{c1::'</span> + matched.group(<span class="number">1</span>) + <span class="string">'}}'</span></span><br><span class="line"></span><br><span class="line"> new_code = re.sub(<span class="string">r'====((?:.|\n)+?)===='</span>, tag_text, code)</span><br><span class="line"> <span class="built_in">print</span>(new_code)</span><br><span class="line"> <span class="variable language_">self</span>.assertNotIn(<span class="string">"===="</span>, new_code)</span><br><span class="line"> <span class="variable language_">self</span>.assertIn(<span class="string">"{{c1::"</span>, new_code)</span><br></pre></td></tr></table></figure><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>之前我一直不愿意写单元测试的原因有两个方面:</p><ol><li>单元测试应该是在功能开发结束后才写的,这时再去写单元测试,就会影响功能交付的时间</li><li>单元测试的代码覆盖率不高,无法做 100% 的覆盖,导致无法节省人工测试</li></ol><p>我们无法做到《Google 软件测试之道》里提到的“开发和测试必须同时开展,写一段代码就立刻测试这段代码”程度,但可以按一个角度来考虑,单元测试能否提升开发效率:</p><ol><li>功能开发阶段:通过单元测试,减少运行成本,提高并行开发效率,提高沟通效率</li><li>Bug修复阶段:通过单元测试,可以不断补全问题模块的测试边界,持续改善后续迭代的代码质量</li><li>功能迭代阶段:通过单元测试,快速了解代码的使用方式,Api如何传参,各种使用场景,通过运行历史的单元测试,减少历史问题重复发生</li></ol>]]></content>
<summary type="html"><h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>把 Logseq 上的知识点笔记自动转换为 Anki 的卡片,实现 Logseq 上做知识归类,在 Anki 上做科学复习</p>
<p>L</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="python" scheme="https://handsomeliuyang.github.io/tags/python/"/>
</entry>
<entry>
<title>Dart的const理解</title>
<link href="https://handsomeliuyang.github.io/2022/05/28/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-Dart%E7%9A%84const%E7%90%86%E8%A7%A3/"/>
<id>https://handsomeliuyang.github.io/2022/05/28/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-Dart%E7%9A%84const%E7%90%86%E8%A7%A3/</id>
<published>2022-05-28T07:46:00.000Z</published>
<updated>2023-10-08T02:53:20.122Z</updated>
<content type="html"><![CDATA[<p>在Flutter的Widget.build()函数里,const使用频率非常的高,但一直没有真正的理解其作用,从而也导致不知道何时应该使用const。</p><p>只有真正做到“知其然知其所以然”,才能真正的写出高质量高性能的程序。</p><blockquote><p>A const variable is a compile-time constant. (Const variables are implicitly final.)</p></blockquote><p>两个关键点:</p><ol><li>初始化时机:编译时</li><li>可变性:完全不可变</li></ol><h1 id="常量变量"><a href="#常量变量" class="headerlink" title="常量变量"></a>常量变量</h1><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 全局常量变量</span></span><br><span class="line"><span class="keyword">const</span> a = <span class="string">'123'</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 类级常量变量</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Demo</span> </span>{</span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">const</span> b = <span class="string">'456'</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>常量变量又可以称为<code>编译时变量</code>,其值要求是<code>编译时常数</code>。</p><p>什么是编译时常数?编译时就要能确定下来,如数值、字符串、常量集合、常量实例等。</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> a = <span class="number">123</span>; <span class="comment">// 数值</span></span><br><span class="line"><span class="keyword">const</span> a = <span class="string">'123'</span>; <span class="comment">// 字符串</span></span><br><span class="line"><span class="keyword">const</span> a = <span class="keyword">const</span> [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>] <span class="comment">// 常量集合</span></span><br><span class="line"><span class="keyword">const</span> a = Constobj(); <span class="comment">// 常量实例</span></span><br></pre></td></tr></table></figure><p><code>常量</code>这个概率在很多的语言里都有,Java里的常量定义为<code>public static final</code>来定义,数值型和字符型的常量好理解,与其他语言的常量类似,但有所差异的是<code>常量集合</code>和<code>常量实例</code>。</p><h1 id="常量集合-与-final变量的区别"><a href="#常量集合-与-final变量的区别" class="headerlink" title="常量集合 与 final变量的区别"></a>常量集合 与 final变量的区别</h1><p>主要区别在于:常量集合是<code>完全不可变</code>,不仅仅只是变量不能再次赋值,包括对象的属性也不能修改</p><figure class="highlight dart"><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="comment">// final 变量</span></span><br><span class="line"><span class="keyword">final</span> a = [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">a[<span class="number">0</span>] = <span class="number">5</span>; <span class="comment">// 可修改对象的属性</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// const常量</span></span><br><span class="line"><span class="keyword">const</span> b = [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>];</span><br><span class="line">b[<span class="number">0</span>] = <span class="number">5</span>; <span class="comment">// 报错</span></span><br><span class="line"><span class="keyword">var</span> c = <span class="keyword">const</span> [<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>];</span><br><span class="line">c[<span class="number">0</span>] = <span class="number">3</span>; <span class="comment">// 报错</span></span><br></pre></td></tr></table></figure><h1 id="常量实例"><a href="#常量实例" class="headerlink" title="常量实例"></a>常量实例</h1><p>创建常量实例的要求:</p><ol><li>const 修饰 构造函数</li><li>所有的成员变量都必须是final</li><li>实例化时,使用常量构造函数,且使用const修饰</li></ol><figure class="highlight dart"><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="class"><span class="keyword">class</span> <span class="title">Point</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> <span class="built_in">int</span> x;</span><br><span class="line"> <span class="keyword">final</span> <span class="built_in">int</span> y;</span><br><span class="line"> <span class="keyword">final</span> <span class="built_in">int</span> z=<span class="number">11</span>;</span><br><span class="line"> <span class="keyword">static</span> <span class="built_in">int</span> m = <span class="number">12</span>; <span class="comment">// 静态变量不需要是final</span></span><br><span class="line"> <span class="keyword">const</span> Point(<span class="keyword">this</span>.x, <span class="keyword">this</span>.y);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">void</span> main() {</span><br><span class="line"> <span class="keyword">const</span> p1 = Point(<span class="number">1</span>, <span class="number">2</span>); <span class="comment">// 常量实例</span></span><br><span class="line"> <span class="keyword">final</span> p2 = <span class="keyword">const</span> Point(<span class="number">1</span>, <span class="number">2</span>); <span class="comment">// 常量实例</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> p3 = Point(<span class="number">1</span>,<span class="number">2</span>);<span class="comment">// 普通实例</span></span><br><span class="line"> <span class="built_in">print</span>(p1 == p2); <span class="comment">// 结果为true</span></span><br><span class="line"> <span class="built_in">print</span>(p1 == p3); <span class="comment">// 结果为false</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>常量实例的特点:</strong></p><ol><li>对应类:需要是Immutable类(不可变类)</li><li>对象的特点:编译时可完全确定,构造参数相同的常量实例复用</li></ol><p><strong>作用</strong></p><ol><li>通过复用,减少内存占用</li><li>通过减少垃圾回收次数,提升性能</li></ol><p><strong>使用场景</strong><br>Flutter里的Widget的build()方法执行次数非常多,为了减少每次重绘时的内存占用和垃圾回收次数,大量使用了const</p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MyApp</span> <span class="keyword">extends</span> <span class="title">StatelessWidget</span> </span>{</span><br><span class="line"> <span class="keyword">const</span> MyApp({Key? key}) : <span class="keyword">super</span>(key: key);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// This widget is the root of your application.</span></span><br><span class="line"> <span class="meta">@override</span></span><br><span class="line"> Widget build(BuildContext context) {</span><br><span class="line"> <span class="keyword">return</span> MaterialApp(</span><br><span class="line"> theme: ThemeData(</span><br><span class="line"> primarySwatch: Colors.blue,</span><br><span class="line"> ),</span><br><span class="line"> home: <span class="keyword">const</span> MyHomePage(title: <span class="string">'Flutter Demo Home Page'</span>),</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>Android里类似的实现:</strong> 对象垃圾池,如Message</p>]]></content>
<summary type="html"><p>在Flutter的Widget.build()函数里,const使用频率非常的高,但一直没有真正的理解其作用,从而也导致不知道何时应该使用const。</p>
<p>只有真正做到“知其然知其所以然”,才能真正的写出高质量高性能的程序。</p>
<blockquote>
<p</summary>
<category term="日常学习" scheme="https://handsomeliuyang.github.io/categories/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0/"/>
<category term="Dart" scheme="https://handsomeliuyang.github.io/tags/Dart/"/>
</entry>
<entry>
<title>Anki助手:解决孩子复习和家长辅导的痛点</title>
<link href="https://handsomeliuyang.github.io/2021/07/26/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-Anki%E5%8A%A9%E6%89%8B/"/>
<id>https://handsomeliuyang.github.io/2021/07/26/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-Anki%E5%8A%A9%E6%89%8B/</id>
<published>2021-07-26T01:00:00.000Z</published>
<updated>2023-10-08T02:53:20.128Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><blockquote><p>「Anki」是一款记忆软件,更确切的说它是一款智能安排我们复习知识点的工具。</p></blockquote><p>Anki 对知识点的记忆上有非常大的帮助,基于遗忘曲线有针对性的适时重复,对于有一定自制力的成人来说,帮助非常的大,但对于缺乏自制力的孩子来说,使用体验上问题比较多:</p><ol><li>为了防止小孩偷懒式的随意点击,需要家长来操作软件,对家长的投入很大</li><li>孩子需要长时间的使用手机/Pad等电子设备</li><li>需要家长了解软件的使用,同时无法远程辅导</li></ol><p>AnkiDroid是一个开源软件,同时提供了对外的Api,适配做扩展插件,选择基于自定一个插件:Anki助手来解决上述痛点。</p><h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>解决上述痛点的方式:</p><ol><li>打印复习Anki卡片:把当前需要复习和学习的问题打印出来,让小孩摆脱电子设备独立完成复习</li><li>家长检查:基于打印复习的结果,由家长把结果录入Anki软件,解决小孩偷懒式的随意点击</li><li>加强记忆:对于完全错误或部分错误的卡片需要加强记忆</li></ol><p>具体的需求:</p><ol><li>基于AnkiDroid的Api开发Anki助手</li><li>Anki助手包括如下功能:<ol><li>首页</li><li>打印复习卡片</li><li>家长检查</li><li>加强记忆</li></ol></li></ol><h1 id="原型图"><a href="#原型图" class="headerlink" title="原型图"></a>原型图</h1><p><strong>打印复习原型图:</strong></p><p><img src="/../../hexo-img/Anki%E5%8A%A9%E6%89%8B/%E6%89%93%E5%8D%B0%E5%A4%8D%E4%B9%A0%E5%8E%9F%E5%9E%8B%E5%9B%BE.drawio.svg"></p><p><strong>家长检查原型图:</strong></p><p><img src="/../../hexo-img/Anki%E5%8A%A9%E6%89%8B/%E5%AE%B6%E9%95%BF%E6%A3%80%E6%9F%A5%E5%8E%9F%E5%9E%8B%E5%9B%BE.drawio.svg"></p><p><strong>记忆加强原型图:</strong></p><p><img src="/../../hexo-img/Anki%E5%8A%A9%E6%89%8B/%E5%8A%A0%E5%BC%BA%E8%AE%B0%E5%BF%86%E5%8E%9F%E5%9E%8B%E5%9B%BE.drawio.svg"></p><h1 id="核心实现"><a href="#核心实现" class="headerlink" title="核心实现"></a>核心实现</h1><h2 id="AnkiDroid的Api"><a href="#AnkiDroid的Api" class="headerlink" title="AnkiDroid的Api"></a>AnkiDroid的Api</h2><p>需要申请<code>Ankidroid</code>的权限:<code>com.ichi2.anki.permission.READ_WRITE_DATABASE</code></p><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><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* 获取到期的卡片列表</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getDueDeckList</span><span class="params">()</span></span>: List<AnkiDeck> {</span><br><span class="line"> <span class="keyword">val</span> deckList = arrayListOf<AnkiDeck>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> decksCursor = App.context.contentResolver.query(</span><br><span class="line"> FlashCardsContract.Deck.CONTENT_ALL_URI,</span><br><span class="line"> <span class="literal">null</span>,</span><br><span class="line"> <span class="literal">null</span>,</span><br><span class="line"> <span class="literal">null</span>,</span><br><span class="line"> <span class="literal">null</span></span><br><span class="line"> ) ?: <span class="keyword">return</span> deckList</span><br><span class="line"></span><br><span class="line"> decksCursor.use { it -></span><br><span class="line"> <span class="keyword">if</span> (it.moveToFirst()) {</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> <span class="keyword">val</span> deckId = it.getLong(it.getColumnIndex(FlashCardsContract.Deck.DECK_ID))</span><br><span class="line"> <span class="keyword">val</span> deckName = it.getString(it.getColumnIndex(FlashCardsContract.Deck.DECK_NAME))</span><br><span class="line"> <span class="keyword">val</span> deckCounts = it.getString(it.getColumnIndex(FlashCardsContract.Deck.DECK_COUNTS))</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 过滤掉 Default 类别</span></span><br><span class="line"> <span class="keyword">if</span> (deckName == <span class="string">"Default"</span>) {</span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> ankiDeck = AnkiDeck.fromString(deckId, deckName, deckCounts)</span><br><span class="line"> <span class="comment">// 过滤掉无复习的类别</span></span><br><span class="line"> <span class="keyword">if</span>(ankiDeck.deckDueCounts.getTotal() <= <span class="number">0</span>){</span><br><span class="line"> <span class="keyword">continue</span></span><br><span class="line"> }</span><br><span class="line"> deckList.add(ankiDeck)</span><br><span class="line"> } <span class="keyword">while</span> (it.moveToNext())</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> deckList</span><br><span class="line">}</span><br></pre></td></tr></table></figure><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><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getDueCards</span><span class="params">(deckId: <span class="type">Long</span>, numCards: <span class="type">Int</span>)</span></span>: List<AnkiCard> {</span><br><span class="line"> <span class="keyword">val</span> ankiCards = arrayListOf<AnkiCard>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> cursor = App.context.contentResolver.query(</span><br><span class="line"> FlashCardsContract.ReviewInfo.CONTENT_URI,</span><br><span class="line"> <span class="literal">null</span>,</span><br><span class="line"> <span class="string">"limit=?, deckID=?"</span>,</span><br><span class="line"> arrayOf(numCards.toString(), deckId.toString()),</span><br><span class="line"> <span class="literal">null</span></span><br><span class="line"> ) ?: <span class="keyword">return</span> ankiCards</span><br><span class="line"></span><br><span class="line"> cursor.use { it -></span><br><span class="line"> <span class="keyword">if</span>(it.moveToFirst()) {</span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> <span class="keyword">val</span> noteId = it.getLong(it.getColumnIndex(FlashCardsContract.ReviewInfo.NOTE_ID))</span><br><span class="line"> <span class="keyword">val</span> cardOrd = it.getInt(it.getColumnIndex(FlashCardsContract.ReviewInfo.CARD_ORD))</span><br><span class="line"> <span class="keyword">val</span> buttonCount = it.getInt(it.getColumnIndex(FlashCardsContract.ReviewInfo.BUTTON_COUNT))</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> nextReviewTimes = it.getString(it.getColumnIndex(FlashCardsContract.ReviewInfo.NEXT_REVIEW_TIMES))</span><br><span class="line"></span><br><span class="line"><span class="comment">// val mediaFiles = it.getString(it.getColumnIndex(FlashCardsContract.ReviewInfo.MEDIA_FILES))</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> card = retrieveCard(noteId, cardOrd)</span><br><span class="line"> card.setReviewData(buttonCount, nextReviewTimes)</span><br><span class="line"> ankiCards.add(card)</span><br><span class="line"> } <span class="keyword">while</span> (it.moveToNext())</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ankiCards</span><br><span class="line">}</span><br></pre></td></tr></table></figure><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><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">answerCard</span><span class="params">(noteId: <span class="type">Long</span>, cardOrd: <span class="type">Int</span>, easy: <span class="type">Int</span>)</span></span>: <span class="built_in">Int</span>{</span><br><span class="line"> <span class="keyword">val</span> values = ContentValues()</span><br><span class="line"> values.put(FlashCardsContract.ReviewInfo.NOTE_ID, noteId)</span><br><span class="line"> values.put(FlashCardsContract.ReviewInfo.CARD_ORD, cardOrd)</span><br><span class="line"> values.put(FlashCardsContract.ReviewInfo.EASE, easy)</span><br><span class="line"> values.put(FlashCardsContract.ReviewInfo.TIME_TAKEN, <span class="number">5000</span>)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> App.context.contentResolver.update(</span><br><span class="line"> FlashCardsContract.ReviewInfo.CONTENT_URI,</span><br><span class="line"> values,</span><br><span class="line"> <span class="literal">null</span>,</span><br><span class="line"> <span class="literal">null</span></span><br><span class="line"> )</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="本地持久化"><a href="#本地持久化" class="headerlink" title="本地持久化"></a>本地持久化</h2><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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Entity(tableName = <span class="string">"print_table"</span>)</span></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">PrintEntity</span>(</span><br><span class="line"> <span class="meta">@PrimaryKey(autoGenerate = true)</span></span><br><span class="line"> <span class="keyword">val</span> id: <span class="built_in">Int</span>,</span><br><span class="line"> <span class="keyword">val</span> name: String,</span><br><span class="line"> <span class="keyword">val</span> time: Date?,</span><br><span class="line"> <span class="keyword">val</span> deckEntitys: List<DeckEntity>,</span><br><span class="line"> <span class="keyword">var</span> hasCheckAndSyncAnki: <span class="built_in">Boolean</span> = <span class="literal">false</span>,</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> strengthenMemoryCounts: <span class="built_in">Int</span> = <span class="number">0</span>,</span><br><span class="line"> <span class="keyword">var</span> hasStrengthenMemory: <span class="built_in">Boolean</span> = <span class="literal">false</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">DeckEntity</span>(</span><br><span class="line"> <span class="keyword">val</span> deckId: <span class="built_in">Long</span>,</span><br><span class="line"> <span class="keyword">val</span> name: String,</span><br><span class="line"> <span class="keyword">val</span> total: <span class="built_in">Int</span>,</span><br><span class="line"> <span class="keyword">var</span> cards: List<CardEntity></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">CardEntity</span>(</span><br><span class="line"> <span class="keyword">val</span> noteId: <span class="built_in">Long</span>,</span><br><span class="line"> <span class="keyword">val</span> cardOrd: <span class="built_in">Int</span>,</span><br><span class="line"> <span class="keyword">val</span> buttonCount: <span class="built_in">Int</span>,</span><br><span class="line"> <span class="keyword">val</span> nextReviewTimes: List<String>,</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> answerEasy: <span class="built_in">Int</span> = -<span class="number">1</span>,</span><br><span class="line"> <span class="keyword">var</span> hasStrengthenMemory: <span class="built_in">Boolean</span> = <span class="literal">false</span></span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>对应的DAO:</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Dao</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">PrintDao</span> {</span><br><span class="line"> <span class="meta">@Query(<span class="string">"SELECT * FROM print_table ORDER BY time DESC"</span>)</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">getPrints</span><span class="params">()</span></span>: List<PrintEntity></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Query(<span class="string">"SELECT * FROM print_table WHERE time BETWEEN :start AND :end ORDER BY time DESC"</span>)</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">getPrintsByDate</span><span class="params">(start: <span class="type">Long</span>, end: <span class="type">Long</span>)</span></span>: List<PrintEntity></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Query(<span class="string">"SELECT * FROM print_table WHERE id=:printId"</span>)</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">getPrintById</span><span class="params">(printId: <span class="type">Int</span>)</span></span>: PrintEntity</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Insert</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">insertAll</span><span class="params">(<span class="keyword">vararg</span> printEntitys: <span class="type">PrintEntity</span>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Update</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">update</span><span class="params">(printEntity: <span class="type">PrintEntity</span>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Delete</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">delete</span><span class="params">(printEntity: <span class="type">PrintEntity</span>)</span></span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Query(<span class="string">"DELETE FROM print_table WHERE time < :date"</span>)</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">deleteBeforeDate</span><span class="params">(date: <span class="type">Long</span>)</span></span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="databinding-LiveData-Coroutine"><a href="#databinding-LiveData-Coroutine" class="headerlink" title="databinding + LiveData + Coroutine"></a>databinding + LiveData + Coroutine</h2><p>框架为:DataBinding,LiveData,Coroutine</p><p>DataBinding做界面绑定,LiveData做数据监听,Coroutine做异步任务</p><h2 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h2><p><a href="https://github.com/handsomeliuyang/anki_assist_app">handsomeliuyang/anki_assist_app</a></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://github.com/android/architecture-samples">android/architecture-samples</a></li><li><a href="https://github.com/ankidroid/Anki-Android">ankidroid/Anki-Android</a></li><li><a href="https://github.com/ankidroid/Anki-Android/wiki/AnkiDroid-API">AnkiDroid API</a></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><blockquote>
<p>「Anki」是一款记忆软件,更确切的说它是一款智能安排我们复习知识点的工具。</p>
</blockquote>
</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>暗黑适配方案比较</title>
<link href="https://handsomeliuyang.github.io/2021/05/17/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E6%9A%97%E9%BB%91%E9%80%82%E9%85%8D%E6%96%B9%E6%A1%88%E7%9A%84%E5%AF%B9%E6%AF%94/"/>
<id>https://handsomeliuyang.github.io/2021/05/17/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E6%9A%97%E9%BB%91%E9%80%82%E9%85%8D%E6%96%B9%E6%A1%88%E7%9A%84%E5%AF%B9%E6%AF%94/</id>
<published>2021-05-16T16:00:00.000Z</published>
<updated>2023-10-08T02:53:20.133Z</updated>
<content type="html"><![CDATA[<p>大部分 App 的开发都没有统一设计规范,有很多的硬编码,导致暗黑适配成本非常高。</p><p>官方也意识到此问题,提供了 <code>Force Dark</code> 功能,自动转换为暗黑样式,但自动转换的方式,会有一些小瑕疵,现在普遍的方案是:混合 <code>Force Dark</code> 和原生适配的方案。</p><blockquote><p>Your themes and styles should avoid hard-coded colors or icons intended for use under a light theme. You should use theme attributes (preferred) or night-qualified resources instead.</p></blockquote><p>由上可知,原生适配的思路是:不要硬编码颜色和图标,而应该使用主题 或 night resources。</p><p>此文主要对比使用主题适配 和 night resources 适配的差异。</p><h1 id="方案一:night-resources-适配暗黑"><a href="#方案一:night-resources-适配暗黑" class="headerlink" title="方案一:night resources 适配暗黑"></a>方案一:night resources 适配暗黑</h1><p><img src="/../../hexo-img/%E6%9A%97%E9%BB%91%E9%80%82%E9%85%8D%E6%96%B9%E6%A1%88%E7%9A%84%E5%AF%B9%E6%AF%94/night-resources.drawio.svg"></p><p>此方案的适配与屏幕大小适配方案一样,主要特点如下:</p><ol><li>系统支持,系统要求 Android 10 及以上,低版本不支持。</li><li>color 不能硬编码,需按功能或作用命名,不能按色值命名</li><li>灵活,但缺乏管理,按资源类别分散</li><li><font color="#ff0000">多Lib库情况下,不适合复用,只能通过依赖复用,违背组件库设计原则</font></li></ol><h1 id="方案二:主题适配暗黑"><a href="#方案二:主题适配暗黑" class="headerlink" title="方案二:主题适配暗黑"></a>方案二:主题适配暗黑</h1><p><img src="/../../hexo-img/%E6%9A%97%E9%BB%91%E9%80%82%E9%85%8D%E6%96%B9%E6%A1%88%E7%9A%84%E5%AF%B9%E6%AF%94/themes.drawio.svg"></p><p>此方案主要特点如下:</p><ol><li>通过自定义属性,定义一套主题样式属性,统一管理,所有 Lib 库方便复用</li><li>全版本支持:<ol><li>Android 10及以上,通过 night-qualified 自动切换</li><li>低版本,通过手动切换主题,context.setTheme(R.style.Theme_LYDemo)</li></ol></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>从统一管理和 Lib 库复用的角度考虑,应该使用方案二,官方推荐的方案也是方案二。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://developer.android.com/guide/topics/ui/look-and-feel/darktheme">Dark theme</a></li></ol>]]></content>
<summary type="html"><p>大部分 App 的开发都没有统一设计规范,有很多的硬编码,导致暗黑适配成本非常高。</p>
<p>官方也意识到此问题,提供了 <code>Force Dark</code> 功能,自动转换为暗黑样式,但自动转换的方式,会有一些小瑕疵,现在普遍的方案是:混合 <code>Fo</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>基于git和markdown的文档平台</title>
<link href="https://handsomeliuyang.github.io/2021/05/11/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E5%9F%BA%E4%BA%8Egit%E5%92%8Cmarkdown%E7%9A%84%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0/"/>
<id>https://handsomeliuyang.github.io/2021/05/11/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E5%9F%BA%E4%BA%8Egit%E5%92%8Cmarkdown%E7%9A%84%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0/</id>
<published>2021-05-10T16:00:00.000Z</published>
<updated>2023-10-08T02:53:20.132Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>对于一个团队而言,文档的重要性不言而喻,为此我们尝试了很多的文档平台,如wiki, confluence,iwiki,ishare等等。实际使用过程中,总会有各种各样的问题,而且无法定制解决。</p><p>对于技术文档,重点关注的是内容,而不是格式,文档中代码比重比较高,平时组内同学写博客大部都是基于 markdown 来写,最终我们决定基于 git + markdown 搭建组内的文档平台。</p><p>搭建后的整体框架:</p><p><img src="/../../hexo-img/%E5%9F%BA%E4%BA%8Egit%E5%92%8Cmarkdown%E7%9A%84%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0/%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0%E6%95%B4%E4%BD%93%E6%A1%86%E6%9E%B6.drawio.svg"></p><h1 id="Markdown编辑器"><a href="#Markdown编辑器" class="headerlink" title="Markdown编辑器"></a>Markdown编辑器</h1><p>Markdown 的编辑器有很多,最终从可扩展性和轻量级上,最终选择了 VSCode</p><p>通过 VSCode 丰富的插件库,补全文档编辑过程中的不足:</p><ol><li>Draw.io Integration:画图工具,而支持直接编辑 svg 和 png 图片</li><li>Favorites:收藏工具,方便收藏常用的文档</li><li>GitLens:Git工具,方便查看每段文档的提交人</li><li>Paste Image:方便粘贴图片</li></ol><p>但还是存在几个关键问题:</p><ol><li>无法快速分享当前文档的GitLab地址</li><li>对于没有GitLab权限的PM,无法查看</li><li>图片存储方式与位置没有统一的规范</li></ol><h2 id="自定义-Plugin-解决快速分享问题"><a href="#自定义-Plugin-解决快速分享问题" class="headerlink" title="自定义 Plugin 解决快速分享问题"></a>自定义 Plugin 解决快速分享问题</h2><p>插件实现非常简单,主要思路:</p><ol><li>获取当前 markdown 文档的相对路径</li><li>通过提前配置的 GitLab 和 动态博客的 baseURL,拼接出完整分享地址</li></ol><p>源码:<a href="https://github.com/handsomeliuyang/vscode-git-markdown-tools">vscode-git-markdown-tools</a></p><p>效果如下:</p><p><img src="/../../hexo-img/%E5%9F%BA%E4%BA%8Egit%E5%92%8Cmarkdown%E7%9A%84%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0/2021-05-10-21-15-03.png"></p><h2 id="动态博客解决无权限无法查看问题"><a href="#动态博客解决无权限无法查看问题" class="headerlink" title="动态博客解决无权限无法查看问题"></a>动态博客解决无权限无法查看问题</h2><p>通过 GitLab 来分享,除了权限问题外,还有对 Markdown 的支持问题,如当前公司的 GitLab 不支持 svg 的渲染。</p><p>gitbook、hexo等静态博客的流程:</p><p><img src="/../../hexo-img/%E5%9F%BA%E4%BA%8Egit%E5%92%8Cmarkdown%E7%9A%84%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0/%E9%9D%99%E6%80%81%E5%8D%9A%E5%AE%A2.drawio.svg"></p><p>此类静态博客的最大问题是本地编译时间较长,不适配团队且更新频繁的文档。</p><p>动态博客docsify的流程:<br><img src="/../../hexo-img/%E5%9F%BA%E4%BA%8Egit%E5%92%8Cmarkdown%E7%9A%84%E6%96%87%E6%A1%A3%E5%B9%B3%E5%8F%B0/docsify.drawio.svg"></p><h1 id="Python搭建web服务"><a href="#Python搭建web服务" class="headerlink" title="Python搭建web服务"></a>Python搭建web服务</h1><p>docsify的两种访问方式:</p><ol><li>访问首页:<a href="https://host:ip/">https://host:ip/</a></li><li>访问子页:<a href="https://host:ip/xxx,如https://host:ip/README">https://host:ip/xxx,如https://host:ip/README</a></li></ol><p>从其原理可知,这两种访问都应该近回 index.html,而不应该访问具体的 xxx.md 文件。</p><p>由 index.html 异步发出的请求:</p><ol><li>请求 *.md、图片等:<a href="https://host:ip/xxx.md,https://host:ip/xxx.png">https://host:ip/xxx.md,https://host:ip/xxx.png</a></li><li>请求目录文件(_sidebar.md):此目录文件是通过相对路径请求的,所以直接打开子页面时,除了<a href="https://host:ip/_sidebar.md">https://host:ip/_sidebar.md</a> 外,还会发出一个请求,如 <a href="https://host:ip/README/_sidebar.md">https://host:ip/README/_sidebar.md</a></li></ol><p>通过 Python 的 Flask Web 框架实现上述需求,代码如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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">from</span> flask <span class="keyword">import</span> Flask, send_from_directory, request</span><br><span class="line"><span class="keyword">import</span> os</span><br><span class="line"></span><br><span class="line">app = Flask(__name__)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">'/'</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">docsify</span>():</span><br><span class="line"> <span class="keyword">return</span> send_from_directory(app.root_path, <span class="string">'index.html'</span>)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">'/<path:filename>'</span></span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">docsify_static</span>(<span class="params">filename</span>):</span><br><span class="line"> file_extension = os.path.splitext(filename)[<span class="number">1</span>]</span><br><span class="line"> <span class="keyword">if</span>(file_extension == <span class="string">''</span>): <span class="comment"># 因为routerMode:'history'模式,不能直接打开子页面,需要先加载index.html页面才行</span></span><br><span class="line"> <span class="keyword">return</span> send_from_directory(app.root_path, <span class="string">'index.html'</span>)</span><br><span class="line"> <span class="keyword">if</span>(filename.endswith(<span class="string">'_sidebar.md'</span>)):</span><br><span class="line"> <span class="keyword">return</span> send_from_directory(app.root_path, <span class="string">'_sidebar.md'</span>)</span><br><span class="line"> <span class="keyword">return</span> send_from_directory(app.root_path + <span class="string">'/..'</span>, filename)</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">'__main__'</span>:</span><br><span class="line"> <span class="comment"># 先更新代码,再启动服务</span></span><br><span class="line"> updateCode()</span><br><span class="line"> app.run(debug=<span class="literal">True</span>, port=<span class="number">8484</span>, host=<span class="string">'0.0.0.0'</span>)</span><br></pre></td></tr></table></figure><p>通过 Webhooks 实现代码的监听,提供一个接口给 GitLab 来调用,代码如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">def</span> <span class="title function_">updateCode</span>():</span><br><span class="line"> working_path = app.root_path + <span class="string">'/..'</span></span><br><span class="line"> <span class="comment"># 先更新代码</span></span><br><span class="line"> process = subprocess.run([<span class="string">"git"</span>,<span class="string">"pull"</span>, <span class="string">"origin"</span>, <span class="string">"master:master"</span>], cwd=working_path, capture_output=<span class="literal">True</span>)</span><br><span class="line"> <span class="comment"># 重新生成目录文件</span></span><br><span class="line"> docsify_generate_sidebar.gen_sidebar()</span><br><span class="line"> <span class="keyword">return</span> process.stdout</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.route(<span class="params"><span class="string">'/igit_webhook'</span>, methods = [<span class="string">'POST'</span>,<span class="string">'GET'</span>]</span>)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">web_hook</span>():</span><br><span class="line"> <span class="keyword">return</span> updateCode()</span><br></pre></td></tr></table></figure><p>由于 docsify 不支持通过扫描文件夹,生成目录的功能,使用 Python 写了一个脚本,在代码更新后,自动遍历文件夹,生成目录文件(_sidebar.md)</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://docsify.js.org/">docsify</a></li><li><a href="https://flask.palletsprojects.com/en/1.1.x/quickstart/">flask</a></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>对于一个团队而言,文档的重要性不言而喻,为此我们尝试了很多的文档平台,如wiki, confluence,iwiki,ishare等等。实际</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>Android端的架构设计的演进和思考</title>
<link href="https://handsomeliuyang.github.io/2021/04/26/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/"/>
<id>https://handsomeliuyang.github.io/2021/04/26/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/</id>
<published>2021-04-25T16:00:00.000Z</published>
<updated>2023-10-08T02:53:20.128Z</updated>
<content type="html"><![CDATA[<h1 id="无架构设计"><a href="#无架构设计" class="headerlink" title="无架构设计"></a>无架构设计</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/%E6%97%A0%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.drawio.svg"></p><p>无架构设计时期,直接基于Activity/Fragment组件开发,分层和特点:</p><ol><li>View层:layout布局<ol><li>基于XML的有状态View</li><li>无法承载所有View,如RecyclerView</li></ol></li><li>Activity/Fragment:大管家<ol><li>初始化View,如Adapter</li><li>直接耦合各种数据源</li><li>与Android平台深度依赖</li></ol></li></ol><h1 id="单元测试分类及意义"><a href="#单元测试分类及意义" class="headerlink" title="单元测试分类及意义"></a>单元测试分类及意义</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/%E9%BB%91%E7%9B%92%E6%B5%8B%E8%AF%95.drawio.svg"></p><p>上述是常规的黑盒测试,从中可以发现,通过黑盒测试,很难模拟各种边界条件。</p><p>好的架构除了可扩展和易维护的特点外,还有一个重大的优点是:方便写单元测试</p><p>单元测试的分类:</p><ol><li>Local tests:JVM上运行,不依赖真机或模拟器,执行时间最快</li><li>Instrumented tests:真机或模拟器上运行,部署麻烦,执行较慢</li></ol><p>单元测试的意义:</p><ol><li>模拟黑盒测试难以模拟的边界条件</li><li>代码重构的保障</li><li>最好的文档</li></ol><p>单元测试的范围:</p><ol><li>正常和异常的Case,如发生过的Bug,Null处理</li><li>性能测试,如方法耗时</li></ol><h1 id="MVC框架"><a href="#MVC框架" class="headerlink" title="MVC框架"></a>MVC框架</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/MVC%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.drawio.svg"></p><p>在 MVC框架里,重点是对 M层的分细,实现低耦合。</p><p>MVC:</p><ol><li>View层:XML 布局文件</li><li>C层:Activity/Fragment<ol><li>初始化 View,即与View层耦合很大</li><li>依据 UseCase 的过程和结果,更新View,如Loading、Error、Success</li></ol></li><li>M层:UseCase + Repository<ol><li>UseCase 不依赖 Android 平台</li><li>通过 RxJava 组装各种数据源</li><li>Repository 的实现依赖 Android 平台,功能单一</li></ol></li></ol><h1 id="MVP框架"><a href="#MVP框架" class="headerlink" title="MVP框架"></a>MVP框架</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/MVP%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.drawio.svg"></p><p>MVP:</p><ol><li>View层:XML + Activity/Fragment</li><li>P层:Presenter<ol><li>与 View 层解藕,接口依赖</li><li>依据 UseCase 的过程和结果,通过View Contract更新View,如Loading、Error、Success</li></ol></li></ol><p>使 Presenter 支持单元测试</p><h1 id="MVVM-RxJava框架"><a href="#MVVM-RxJava框架" class="headerlink" title="MVVM+RxJava框架"></a>MVVM+RxJava框架</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/MVVM%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.drawio.svg"></p><p>MVVM:</p><ol><li>View层:XML + Activity/Fragment</li><li>VM层:ViewModel<ol><li>与 View 层解藕,数据监听,生命周期监听</li><li>Fragment之间共享数据</li><li>屏幕旋转时,界面重建,数据保留</li><li>依据 UseCase 的过程和结果,更新LiveData</li></ol></li></ol><p>使用 RxJava 函数式编译,不仅仅是为了方便线程切换,RxJava 带来最大的好处在于在空间和时间两个维度上,对结果进行重排。</p><p>通过 MediatorLiveData 也能实现对结果进行重排:</p><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/livedata.drawio.svg"></p><h1 id="MVVM-LiveData框架"><a href="#MVVM-LiveData框架" class="headerlink" title="MVVM+LiveData框架"></a>MVVM+LiveData框架</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E7%9A%84%E6%BC%94%E8%BF%9B%E5%92%8C%E6%80%9D%E8%80%83/MVVM_LiveData%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1.drawio.svg"></p><p>LiveData 本身支持多线程,通过 MediatorLiveData 也能实现对结果的重排,即可以把 M层进行纯 LiveData 改造。</p><p>改造成本不大,主要是两个核心点:</p><ol><li>LiveDataTask:run(),LiveData实现线程同步</li><li>UseCase:<ol><li>线程池:io, net, main</li><li>MediatorLiveData 组合各种 LiveData</li></ol></li></ol><h1 id="MVVM-LiveData的代码实现"><a href="#MVVM-LiveData的代码实现" class="headerlink" title="MVVM+LiveData的代码实现"></a>MVVM+LiveData的代码实现</h1><h2 id="M层"><a href="#M层" class="headerlink" title="M层"></a>M层</h2><p>LiveDataTask:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">LiveDataTask</span><T> <span class="keyword">implements</span> <span class="title class_">Runnable</span> {</span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">final</span> MutableLiveData<DataResult<T>> _liveData = <span class="keyword">new</span> <span class="title class_">MutableLiveData</span><>();</span><br><span class="line"> <span class="keyword">public</span> LiveData<DataResult<T>> <span class="title function_">getLiveData</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> _liveData;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>ContentDataRepository:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">ContentDataRepository</span> {</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 从网络请求首页数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> cityDir</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> LiveDataTask<ContentBean> <span class="title function_">contentDataFromNet</span><span class="params">(String cityDir)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 从缓存中获取首页数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> cityDir</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> LiveDataTask<ContentBean> <span class="title function_">contentDataFromCache</span><span class="params">(String cityDir)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 从Assets中获取首页数据</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> cityDir</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> LiveDataTask<ContentBean> <span class="title function_">contentDataFromAssets</span><span class="params">(String cityDir)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 保存数据到本地缓存</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> pairData</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> LiveDataTask<Unit> <span class="title function_">saveContentDataToCache</span><span class="params">(ContentBean pairData)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>GetContentDataUseCase:</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><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GetContentDataUseCase</span> <span class="keyword">extends</span> <span class="title class_">UseCase</span><GetContentDataUseCase.RequestValues, ContentBean> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> ContentDataRepository mContentDataRepository;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">GetContentDataUseCase</span><span class="params">(</span></span><br><span class="line"><span class="params"> AppExecutors appExecutors,</span></span><br><span class="line"><span class="params"> ContentDataRepository contentDataRepository</span></span><br><span class="line"><span class="params"> )</span> {</span><br><span class="line"> <span class="built_in">super</span>(appExecutors);</span><br><span class="line"> <span class="built_in">this</span>.mContentDataRepository = contentDataRepository;</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="keyword">public</span> LiveData<DataResult<ContentBean>> <span class="title function_">execute</span><span class="params">(RequestValues requestValues)</span> {</span><br><span class="line"> LiveDataTask<ContentBean> assetsLiveDataTask, cacheLiveDataTask, networkLiveDataTask;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 三个原始的LiveDataTask</span></span><br><span class="line"> assetsLiveDataTask = mContentDataRepository.contentDataFromAssets(requestValues.cityDir);</span><br><span class="line"> cacheLiveDataTask = mContentDataRepository.contentDataFromCache(requestValues.cityDir);</span><br><span class="line"> networkLiveDataTask = mContentDataRepository.contentDataFromNet(requestValues.cityDir);</span><br><span class="line"></span><br><span class="line"> MediatorLiveData<DataResult<ContentBean>> result = <span class="keyword">new</span> <span class="title class_">MediatorLiveData</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 显示加载中...</span></span><br><span class="line"> result.setValue(DataResult.loading(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 组合本地级存 与 Assets缓存,逻辑为:当cache无数据时,就加载assets的数据</span></span><br><span class="line"> MediatorLiveData<DataResult<ContentBean>> cacheAndAssetsLiveData = <span class="keyword">new</span> <span class="title class_">MediatorLiveData</span><>();</span><br><span class="line"> appExecutors.diskIO().execute(cacheLiveDataTask); <span class="comment">// diskIO线程池里,执行缓存加载</span></span><br><span class="line"> cacheAndAssetsLiveData.addSource(cacheLiveDataTask.getLiveData(), cacheDataResult -> {</span><br><span class="line"> <span class="keyword">switch</span> (cacheDataResult.status) {</span><br><span class="line"> <span class="keyword">case</span> SUCCESS:</span><br><span class="line"> <span class="keyword">case</span> LOADING:</span><br><span class="line"> cacheAndAssetsLiveData.setValue(cacheDataResult);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> ERROR: <span class="comment">// 如果加载出错,加载assets内置数据</span></span><br><span class="line"> cacheAndAssetsLiveData.removeSource(cacheLiveDataTask.getLiveData());</span><br><span class="line"> appExecutors.diskIO().execute(assetsLiveDataTask);</span><br><span class="line"> cacheAndAssetsLiveData.addSource(assetsLiveDataTask.getLiveData(), <span class="keyword">new</span> <span class="title class_">Observer</span><DataResult<ContentBean>>() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onChanged</span><span class="params">(DataResult<ContentBean> assetsDataResult)</span> {</span><br><span class="line"> cacheAndAssetsLiveData.setValue(assetsDataResult);</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 class="comment">// 监听缓存数据,注意:返回的状态都是 加载中...</span></span><br><span class="line"> result.addSource(cacheAndAssetsLiveData, dataResult -> result.setValue(DataResult.loading(dataResult.data)));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// networkIO线程池里,执行网络加载</span></span><br><span class="line"> appExecutors.networkIO().execute(networkLiveDataTask);</span><br><span class="line"> <span class="comment">// 逻辑:</span></span><br><span class="line"> <span class="comment">// 1. 网络请求成功,就不再监听缓存数据了,不管缓存数据是否加载成功,都直接使用网络数据</span></span><br><span class="line"> <span class="comment">// 2. 网络请求失败,就再监听级存数据,可能存在两种情况:</span></span><br><span class="line"> <span class="comment">// 1. 缓存已经加载成功,已经回调首页的Activity了,但状态是Loading,会马上重新回调Activity,但状态为Error,数据一样</span></span><br><span class="line"> <span class="comment">// 2. 缓存还没有加载成功,则等缓存加载成功后,直接回调Activity,但状态为Error,数据为缓存数据</span></span><br><span class="line"> result.addSource(networkLiveDataTask.getLiveData(), networkDataResult -> {</span><br><span class="line"> <span class="keyword">switch</span> (networkDataResult.status) {</span><br><span class="line"> <span class="keyword">case</span> SUCCESS:</span><br><span class="line"> result.removeSource(cacheAndAssetsLiveData);</span><br><span class="line"> result.removeSource(networkLiveDataTask.getLiveData());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 同步更新缓存数据</span></span><br><span class="line"> appExecutors.diskIO().execute(mContentDataRepository.saveContentDataToCache(networkDataResult.data));</span><br><span class="line"> result.setValue(DataResult.success(networkDataResult.data));</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> ERROR:</span><br><span class="line"> result.removeSource(cacheAndAssetsLiveData);</span><br><span class="line"> result.removeSource(networkLiveDataTask.getLiveData());</span><br><span class="line"></span><br><span class="line"> result.addSource(cacheAndAssetsLiveData, dataResult -> result.setValue(DataResult.error(networkDataResult.message, dataResult.data)));</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> LOADING:</span><br><span class="line"> <span class="comment">// 不加调状态,因为已经更新Loading状态了</span></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 class="keyword">return</span> result;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">RequestValues</span> <span class="keyword">implements</span> <span class="title class_">UseCase</span>.RequestValues {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> String cityDir;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">RequestValues</span><span class="params">(String cityDir)</span> {</span><br><span class="line"> <span class="built_in">this</span>.cityDir = cityDir;</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="keyword">public</span> <span class="type">boolean</span> <span class="title function_">equals</span><span class="params">(Object o)</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">this</span> == o) <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">if</span> (o == <span class="literal">null</span> || getClass() != o.getClass()) <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> <span class="type">RequestValues</span> <span class="variable">that</span> <span class="operator">=</span> (RequestValues) o;</span><br><span class="line"> <span class="keyword">return</span> Objects.equals(cityDir, that.cityDir);</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="keyword">public</span> <span class="type">int</span> <span class="title function_">hashCode</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> Objects.hash(cityDir);</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="keyword">public</span> String <span class="title function_">toString</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"RequestValues{"</span> +</span><br><span class="line"> <span class="string">"cityDir='"</span> + cityDir + <span class="string">'\''</span> +</span><br><span class="line"> <span class="string">'}'</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="VM层"><a href="#VM层" class="headerlink" title="VM层"></a>VM层</h2><p>HomePageViewModel: </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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HomePageViewModel</span> <span class="keyword">extends</span> <span class="title class_">ViewModel</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> GetContentDataUseCase getContentDataUseCase;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> MutableLiveData<String> cityDirLiveData = <span class="keyword">new</span> <span class="title class_">MutableLiveData</span><>();</span><br><span class="line"> <span class="comment">// 注意点:此处应该只返回LiveData,而不应该返回MutableLiveData类型</span></span><br><span class="line"> <span class="keyword">public</span> LiveData<String> <span class="title function_">getCityDirLiveData</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> cityDirLiveData;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">retryCityDir</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">String</span> <span class="variable">cityDir</span> <span class="operator">=</span> cityDirLiveData.getValue();</span><br><span class="line"> <span class="keyword">if</span>(TextUtils.isEmpty(cityDir)) {</span><br><span class="line"> <span class="keyword">return</span> ;</span><br><span class="line"> }</span><br><span class="line"> cityDirLiveData.setValue(cityDir);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setCityDir</span><span class="params">(String cityDir)</span> {</span><br><span class="line"> <span class="keyword">if</span> (TextUtils.isEmpty(cityDir) || cityDir.equals(cityDirLiveData.getValue())) {</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"> cityDirLiveData.setValue(cityDir);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 实际的子类为MediatorLiveData,监听了cityDir变化后,获取的contentData</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">final</span> LiveData<DataResult<ContentBean>> contentData = Transformations.switchMap(cityDirLiveData, <span class="keyword">new</span> <span class="title class_">Function</span><String, LiveData<DataResult<ContentBean>>>() {</span><br><span class="line"> MutableLiveData<DataResult<ContentBean>> mSource = <span class="keyword">new</span> <span class="title class_">MutableLiveData</span><>();</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> LiveData<DataResult<ContentBean>> <span class="title function_">apply</span><span class="params">(String cityDir)</span> {</span><br><span class="line"> <span class="keyword">return</span> getContentDataUseCase.execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(cityDir));</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">HomePageViewModel</span><span class="params">(GetContentDataUseCase getContentDataUseCase)</span> {</span><br><span class="line"> <span class="built_in">this</span>.getContentDataUseCase = getContentDataUseCase;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="单元测试"><a href="#单元测试" class="headerlink" title="单元测试"></a>单元测试</h2><p>对 ViewModel 的单元测试:</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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RunWith(JUnit4.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HomePageViewModelTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Rule</span></span><br><span class="line"> <span class="meta">@JvmField</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">InstantTaskExecutorRule</span> <span class="variable">instantExecutorRule</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InstantTaskExecutorRule</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">GetContentDataUseCase</span> <span class="variable">getContentDataUseCase</span> <span class="operator">=</span> mock(GetContentDataUseCase.class);</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">HomePageViewModel</span> <span class="variable">homePageViewModel</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HomePageViewModel</span>(getContentDataUseCase);</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testNull</span><span class="params">()</span>{</span><br><span class="line"> assertThat(homePageViewModel.contentData, notNullValue());</span><br><span class="line"> verify(getContentDataUseCase, never()).execute(any(GetContentDataUseCase.RequestValues.class));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">dontFetchWithoutObservers</span><span class="params">()</span>{</span><br><span class="line"> homePageViewModel.setCityDir(<span class="string">"bj"</span>);</span><br><span class="line"> verify(getContentDataUseCase, never()).execute(any(GetContentDataUseCase.RequestValues.class));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">fetchWhenObserved</span><span class="params">()</span>{</span><br><span class="line"> homePageViewModel.setCityDir(<span class="string">"bj"</span>);</span><br><span class="line"> homePageViewModel.contentData.observeForever(mock(Observer.class));</span><br><span class="line"> verify(getContentDataUseCase).execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">changeWhileObserved</span><span class="params">()</span>{</span><br><span class="line"> homePageViewModel.contentData.observeForever(mock(Observer.class));</span><br><span class="line"></span><br><span class="line"> homePageViewModel.setCityDir(<span class="string">"bj"</span>);</span><br><span class="line"> homePageViewModel.setCityDir(<span class="string">"shanghai"</span>);</span><br><span class="line"></span><br><span class="line"> verify(getContentDataUseCase).execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>));</span><br><span class="line"> verify(getContentDataUseCase).execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"shanghai"</span>));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">retry</span><span class="params">()</span>{</span><br><span class="line"> homePageViewModel.retryCityDir();</span><br><span class="line"> verifyNoMoreInteractions(getContentDataUseCase);</span><br><span class="line"></span><br><span class="line"> homePageViewModel.setCityDir(<span class="string">"bj"</span>);</span><br><span class="line"> verifyNoMoreInteractions(getContentDataUseCase);</span><br><span class="line"></span><br><span class="line"> homePageViewModel.contentData.observeForever(mock(Observer.class));</span><br><span class="line"> verify(getContentDataUseCase).execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>));</span><br><span class="line"></span><br><span class="line"> reset(getContentDataUseCase);</span><br><span class="line"> homePageViewModel.retryCityDir();</span><br><span class="line"> verify(getContentDataUseCase).execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">blankCityDir</span><span class="params">()</span>{</span><br><span class="line"> homePageViewModel.setCityDir(<span class="string">""</span>);</span><br><span class="line"> homePageViewModel.contentData.observeForever(mock(Observer.class));</span><br><span class="line"> verifyNoMoreInteractions(getContentDataUseCase);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对 UseCase 的单元测试:</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><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RunWith(JUnit4.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GetContentDataUseCaseTest</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Rule</span></span><br><span class="line"> <span class="meta">@JvmField</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">InstantTaskExecutorRule</span> <span class="variable">instantExecutorRule</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InstantTaskExecutorRule</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> MutableLiveData<DataResult<ContentBean>> assetsLiveData = <span class="keyword">new</span> <span class="title class_">MutableLiveData</span><>();</span><br><span class="line"> <span class="keyword">private</span> MutableLiveData<DataResult<ContentBean>> cacheLiveData = <span class="keyword">new</span> <span class="title class_">MutableLiveData</span><>();</span><br><span class="line"> <span class="keyword">private</span> MutableLiveData<DataResult<ContentBean>> netLiveData = <span class="keyword">new</span> <span class="title class_">MutableLiveData</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> LiveDataTask<ContentBean> assetsTask = Mockito.mock(LiveDataTask.class);</span><br><span class="line"> <span class="keyword">private</span> LiveDataTask<ContentBean> cacheTask = Mockito.mock(LiveDataTask.class);</span><br><span class="line"> <span class="keyword">private</span> LiveDataTask<ContentBean> netTask = Mockito.mock(LiveDataTask.class);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> ContentDataRepository contentDataRepository;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> GetContentDataUseCase getContentDataUseCase;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Before</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// mock repository实现</span></span><br><span class="line"> contentDataRepository = Mockito.mock(ContentDataRepository.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// mock asset实现</span></span><br><span class="line"> Mockito.when(assetsTask.getLiveData()).thenReturn(assetsLiveData);</span><br><span class="line"> Mockito.when(contentDataRepository.contentDataFromAssets(Mockito.anyString())).thenReturn(assetsTask);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// mock cache实现</span></span><br><span class="line"> Mockito.when(cacheTask.getLiveData()).thenReturn(cacheLiveData);</span><br><span class="line"> Mockito.when(contentDataRepository.contentDataFromCache(Mockito.anyString())).thenReturn(cacheTask);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// mock net实现</span></span><br><span class="line"> Mockito.when(netTask.getLiveData()).thenReturn(netLiveData);</span><br><span class="line"> Mockito.when(contentDataRepository.contentDataFromNet(Mockito.anyString())).thenReturn(netTask);</span><br><span class="line"></span><br><span class="line"> getContentDataUseCase = <span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>(</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">InstantAppExecutors</span>(),</span><br><span class="line"> contentDataRepository</span><br><span class="line"> );</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testBegin</span><span class="params">()</span>{</span><br><span class="line"> getContentDataUseCase.execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>));</span><br><span class="line"> verify(cacheTask).run();</span><br><span class="line"> verify(netTask).run();</span><br><span class="line"> verifyNoMoreInteractions(assetsTask);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">dontHaveCache</span><span class="params">()</span>{</span><br><span class="line"> Observer<DataResult<ContentBean>> observer = mock(Observer.class);</span><br><span class="line"></span><br><span class="line"> getContentDataUseCase</span><br><span class="line"> .execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>))</span><br><span class="line"> .observeForever(observer);</span><br><span class="line"></span><br><span class="line"> verify(observer).onChanged(DataResult.loading(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 本地缓存加载失败</span></span><br><span class="line"> cacheLiveData.postValue(DataResult.error(<span class="string">"not have data"</span>, <span class="literal">null</span>));</span><br><span class="line"> verify(assetsTask).run();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 assets缓存的成功的情况</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">assetsContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> assetsLiveData.postValue(DataResult.success(assetsContentBean));</span><br><span class="line"> verify(observer).onChanged(DataResult.loading(assetsContentBean));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 net缓存请求成功的情况</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">netContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> netLiveData.postValue(DataResult.success(netContentBean));</span><br><span class="line"> verify(observer).onChanged(DataResult.success(netContentBean));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">haveCache</span><span class="params">()</span>{</span><br><span class="line"> Observer<DataResult<ContentBean>> observer = mock(Observer.class);</span><br><span class="line"></span><br><span class="line"> getContentDataUseCase</span><br><span class="line"> .execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>))</span><br><span class="line"> .observeForever(observer);</span><br><span class="line"></span><br><span class="line"> verify(observer).onChanged(DataResult.loading(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 本地缓存加载成功</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">cacheContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> cacheLiveData.postValue(DataResult.success(cacheContentBean));</span><br><span class="line"> verify(observer).onChanged(DataResult.loading(cacheContentBean));</span><br><span class="line"> verify(assetsTask, never()).run();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 net缓存请求成功的情况</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">netContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> netLiveData.postValue(DataResult.success(netContentBean));</span><br><span class="line"> verify(contentDataRepository).saveContentDataToCache(netContentBean); <span class="comment">// 验证缓存方法是否执行</span></span><br><span class="line"> verify(observer).onChanged(DataResult.success(netContentBean));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">netError</span><span class="params">()</span>{</span><br><span class="line"> Observer<DataResult<ContentBean>> observer = mock(Observer.class);</span><br><span class="line"></span><br><span class="line"> getContentDataUseCase</span><br><span class="line"> .execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>))</span><br><span class="line"> .observeForever(observer);</span><br><span class="line"></span><br><span class="line"> verify(observer).onChanged(DataResult.loading(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 本地缓存加载成功</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">cacheContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> cacheLiveData.postValue(DataResult.success(cacheContentBean));</span><br><span class="line"> verify(observer).onChanged(DataResult.loading(cacheContentBean));</span><br><span class="line"> verify(assetsTask, never()).run();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 net请求失败的情况</span></span><br><span class="line"> netLiveData.postValue(DataResult.error(<span class="string">"net error"</span>, <span class="literal">null</span>));</span><br><span class="line"> verify(observer).onChanged(DataResult.error(<span class="string">"net error"</span>, cacheContentBean));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">netErrorButQuick</span><span class="params">()</span>{</span><br><span class="line"> Observer<DataResult<ContentBean>> observer = mock(Observer.class);</span><br><span class="line"></span><br><span class="line"> getContentDataUseCase</span><br><span class="line"> .execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>))</span><br><span class="line"> .observeForever(observer);</span><br><span class="line"></span><br><span class="line"> verify(observer).onChanged(DataResult.loading(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 net缓存失败的情况</span></span><br><span class="line"> netLiveData.postValue(DataResult.error(<span class="string">"net error"</span>, <span class="literal">null</span>));</span><br><span class="line"> verify(observer, never()).onChanged(DataResult.error(<span class="string">"net error"</span>, any(ContentBean.class)));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 本地缓存加载成功</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">cacheContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> cacheLiveData.postValue(DataResult.success(cacheContentBean));</span><br><span class="line"> verify(observer).onChanged(DataResult.error(<span class="string">"net error"</span>, cacheContentBean));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">netSuccessButQuick</span><span class="params">()</span>{</span><br><span class="line"> Observer<DataResult<ContentBean>> observer = mock(Observer.class);</span><br><span class="line"></span><br><span class="line"> getContentDataUseCase</span><br><span class="line"> .execute(<span class="keyword">new</span> <span class="title class_">GetContentDataUseCase</span>.RequestValues(<span class="string">"bj"</span>))</span><br><span class="line"> .observeForever(observer);</span><br><span class="line"></span><br><span class="line"> verify(observer).onChanged(DataResult.loading(<span class="literal">null</span>));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 net缓存请求成功的情况</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">netContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> netLiveData.postValue(DataResult.success(netContentBean));</span><br><span class="line"> verify(observer).onChanged(DataResult.success(netContentBean));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟 本地缓存加载成功</span></span><br><span class="line"> <span class="type">ContentBean</span> <span class="variable">cacheContentBean</span> <span class="operator">=</span> mock(ContentBean.class);</span><br><span class="line"> cacheLiveData.postValue(DataResult.success(cacheContentBean));</span><br><span class="line"> verify(observer, never()).onChanged(DataResult.loading(cacheContentBean));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://www.jianshu.com/p/be63c37b02cb">架构设计的历史·MVC·MVP·MVVM</a></li><li><a href="https://academy.realm.io/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/">https://academy.realm.io/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android/</a></li><li><a href="https://github.com/android/architecture-components-samples/blob/8874799313/GithubBrowserSample/app/src/test/java/com/android/example/github/repository/NetworkBoundResourceTest.kt">NetworkBoundResourceTest.kt</a></li><li><a href="https://developer.android.com/jetpack/guide">Repository图</a></li><li><a href="https://github.com/android/architecture-samples">各种框架代码</a></li></ol>]]></content>
<summary type="html"><h1 id="无架构设计"><a href="#无架构设计" class="headerlink" title="无架构设计"></a>无架构设计</h1><p><img src="/../../hexo-img/Android%E7%AB%AF%E7%9A%84%E6%9E%</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>责任链模式两种典型实现</title>
<link href="https://handsomeliuyang.github.io/2021/01/20/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/"/>
<id>https://handsomeliuyang.github.io/2021/01/20/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/</id>
<published>2021-01-20T02:00:00.000Z</published>
<updated>2023-10-08T02:53:20.127Z</updated>
<content type="html"><![CDATA[<blockquote><p>责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。</p></blockquote><p><img src="/../../hexo-img/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/intercepter-chain.drawio.svg"></p><p><code>责任链模式解决的问题:</code>避免请求的发送者和接收者之间的耦合关系。</p><p>Rxjava的拦截器是典型的责任链例子,同时我们自己实现类似的拦截器功能时,也会用到责任链模式,通过研究Rxjava的源码,总结两种实现方案:</p><ol><li>方案一:List,for循环+返回值(最简单的实现)</li><li>方案二:List,链式+手动调用(Rxjava的实现)</li></ol><h1 id="方案一:for循环-返回值"><a href="#方案一:for循环-返回值" class="headerlink" title="方案一:for循环+返回值"></a>方案一:for循环+返回值</h1><p><strong>示例代码</strong></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 class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Interceptor</span> {</span><br><span class="line"> <span class="comment">// 返回TRUE,拦截此请求</span></span><br><span class="line"> <span class="comment">// 返回null 或 FALSE = 交下一个处理</span></span><br><span class="line"> Boolean <span class="title function_">intercept</span><span class="params">(Request request)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorA</span> <span class="keyword">implements</span> <span class="title class_">Interceptor</span> {</span><br><span class="line"> <span class="keyword">public</span> Boolean <span class="title function_">intercept</span><span class="params">(Request request)</span>{</span><br><span class="line"> <span class="comment">// 此拦截器对request做个性化处理</span></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 拦截此请求</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line"> <span class="comment">// 交下一个拦截器处理</span></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><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorChain</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> List<Interceptor> interceptors = <span class="keyword">new</span> <span class="title class_">ArrayList</span><Interceptor>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptor</span><span class="params">(Interceptor interceptor)</span>{</span><br><span class="line"> <span class="built_in">this</span>.interceptors.add(interceptor);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">process</span><span class="params">(Request request)</span>{</span><br><span class="line"> <span class="keyword">for</span>(Interceptor interceptor : interceptors) {</span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">result</span> <span class="operator">=</span> interceptor.intercept(request);</span><br><span class="line"> <span class="keyword">if</span>(result == <span class="literal">true</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><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">InterceptorChain</span> <span class="variable">chain</span> <span class="operator">=</span> InterceptorChain();</span><br><span class="line"> chain.addInterceptor(<span class="keyword">new</span> <span class="title class_">InterceptorA</span>());</span><br><span class="line"> chain.addInterceptor(<span class="keyword">new</span> <span class="title class_">InterceptorB</span>());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 处理请求</span></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">result</span> <span class="operator">=</span> chain.process(<span class="keyword">new</span> <span class="title class_">Request</span>(<span class="string">"Hello Wrold"</span>));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>类图</strong></p><p><img src="/../../hexo-img/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/intercepter-for-class.drawio.svg"></p><p><strong>时序图</strong></p><p><img src="/../../hexo-img/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/intercepter-for-sequence.drawio.svg"></p><h1 id="方案二:链式-手动调用"><a href="#方案二:链式-手动调用" class="headerlink" title="方案二:链式+手动调用"></a>方案二:链式+手动调用</h1><p><strong>示例代码</strong></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><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 class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Interceptor</span> {</span><br><span class="line"> Response <span class="title function_">intercept</span><span class="params">(Chain chain, Request request)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Chain</span> {</span><br><span class="line"> Response <span class="title function_">process</span><span class="params">(Request request)</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorA</span> <span class="keyword">implements</span> <span class="title class_">Interceptor</span> {</span><br><span class="line"> Response <span class="title function_">intercept</span><span class="params">(Chain chain, Request request)</span>{</span><br><span class="line"> <span class="comment">// 此拦截器对request做个性化处理</span></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 拦截此请求</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> <span class="comment">// 交下一个拦截器处理,chain 可以理解为下一个链路结点,即下一个拦截器</span></span><br><span class="line"> <span class="keyword">return</span> chain.process(request);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorChain</span> <span class="keyword">implements</span> <span class="title class_">Chain</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> List<Interceptor> interceptors = <span class="keyword">new</span> <span class="title class_">ArrayList</span><Interceptor>();</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> index;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">InterceptorChain</span><span class="params">(List<Interceptor> interceptors, <span class="type">int</span> index)</span>{</span><br><span class="line"> <span class="built_in">this</span>.interceptors = interceptors;</span><br><span class="line"> <span class="built_in">this</span>.index = index;</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="keyword">public</span> Response <span class="title function_">process</span><span class="params">(Request request)</span>;</span><br><span class="line"> <span class="keyword">if</span>(index >= interceptors.size()) {</span><br><span class="line"> <span class="comment">// 所有拦截器都已处理完成</span></span><br><span class="line"> <span class="keyword">return</span> processFinal(request);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 构建下一个执行链路节点</span></span><br><span class="line"> <span class="type">InterceptorChain</span> <span class="variable">next</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InterceptorChain</span>(interceptors, index + <span class="number">1</span>);</span><br><span class="line"> <span class="type">Interceptor</span> <span class="variable">interceptor</span> <span class="operator">=</span> interceptors.get(index);</span><br><span class="line"> <span class="keyword">return</span> interceptor.intercept(next, request);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> </span><br><span class="line"> List<Interceptor> interceptors = <span class="keyword">new</span> <span class="title class_">ArrayList</span><Interceptor>();</span><br><span class="line"> interceptors.add(<span class="keyword">new</span> <span class="title class_">InterceptorA</span>());</span><br><span class="line"> interceptors.add(<span class="keyword">new</span> <span class="title class_">InterceptorB</span>());</span><br><span class="line"></span><br><span class="line"> <span class="type">Response</span> <span class="variable">response</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InterceptorChain</span>(interceptors, <span class="number">0</span>).process(<span class="keyword">new</span> <span class="title class_">Request</span>(<span class="string">"Hello Wrold"</span>));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>类图</strong></p><p><img src="/../../hexo-img/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/intercepter-rxjava-class.drawio.svg"></p><p><strong>时序图</strong></p><p><img src="/../../hexo-img/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E4%B8%A4%E7%A7%8D%E5%85%B8%E5%9E%8B%E5%AE%9E%E7%8E%B0/intercepter-rxjava-sequence.drawio.svg"></p><h1 id="对比"><a href="#对比" class="headerlink" title="对比"></a>对比</h1><p>两种方案的最大差别在于:方案二可以实现Request对象处理前的拦截,和处理后的拦截,原理是通过函数的递归调用。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://www.liaoxuefeng.com/wiki/1252599548343744/1281319474561057">廖雪峰-责任链</a></li><li><a href="https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/strategy.html">类图+时序图参考</a></li></ol>]]></content>
<summary type="html"><blockquote>
<p>责任链模式(Chain of Responsibility)是一种处理请求的模式,它让多个处理器都有机会处理该请求,直到其中某个处理成功为止。</p>
</blockquote>
<p><img src="/../../hexo-img/%E8%B</summary>
<category term="日常学习" scheme="https://handsomeliuyang.github.io/categories/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0/"/>
<category term="设计模式" scheme="https://handsomeliuyang.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>群晖:搭建个人和工作的数据中心</title>
<link href="https://handsomeliuyang.github.io/2021/01/03/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E7%BE%A4%E6%99%96%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%92%8C%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83/"/>
<id>https://handsomeliuyang.github.io/2021/01/03/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E7%BE%A4%E6%99%96%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%92%8C%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83/</id>
<published>2021-01-03T07:30:00.000Z</published>
<updated>2023-10-08T02:53:20.133Z</updated>
<content type="html"><![CDATA[<p>经历了多次换手机后,生活中珍贵的照片被分散在多处,如QQ空间、小米云盘、华为云盘、微信收藏等处,加大了管理难度,不仅要管理账号,还要定期给云空间续费,同时查看也非常的不方便,甚至导致一些珍贵的照片的遗失。</p><p>同时受制于网络的原因,批量管理照片也非常的不方便,这些云端相册空间,上传照片都很方便,但批量的编辑都比较麻烦。</p><p>现在的终端也越来越多,如Mac电脑(公司和家里各一台)、iPad/android平板、手机,对于工作的无缝接力就显的非常的重要。像国外的Dropbox的同步软件的作用越来越大,但受国情影响,只能通过科学上网的方式来访问,还有2G容量的限制。</p><p>经过元旦假期对群晖DS720+的摸索,总算搭建了一个让自己基本满意的数据中心,最终的应用场景如下所示:</p><p><img src="/../../hexo-img/%E7%BE%A4%E6%99%96%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%92%8C%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83/%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF.drawio.svg"></p><h1 id="数据中心"><a href="#数据中心" class="headerlink" title="数据中心"></a>数据中心</h1><h2 id="群晖DS720-的硬盘配制及备份"><a href="#群晖DS720-的硬盘配制及备份" class="headerlink" title="群晖DS720+的硬盘配制及备份"></a>群晖DS720+的硬盘配制及备份</h2><p>群晖DS720+只有两个硬盘位,默认不带硬盘,经过几翻折腾后,并没有采用Raid1阵列,基于如下原因:</p><ol><li>两盘位只能采用RAID1模式,硬盘利用率减少一半,写入速度取决于最慢的硬盘</li><li>成本太高,两块HDD硬盘的噪音太大。</li><li>不是所有的数据都要备份,像电影、Android源码等等数据不需要备份。</li></ol><p>硬盘配制情况:</p><ol><li>一盘位:250G的SSD硬盘,basic模式,负责DSM系统加载,套件的安装盘,比起HDD盘的噪音大大减小。</li><li>二盘位:2TB希捷NAS硬盘,basic模式,所有数据的存储盘。</li><li>USB盘位:1TB HDD硬盘,用作关键数据的冷备份。</li></ol><p>数据分为如下几类:</p><ol><li>个人重要数据,如照片,工作的资料等,数据丢失后无法找回。</li><li>可找回的数据,如下载的电影,已经提交到Github上的代码等等,数据丢失后,重新下载就可以。</li><li>整机备份文件(如Mac的TimeMachine),由于体积太大,单独使用一个SSD移动硬盘备份。</li></ol><p>对于个人重要数据采用「3-2-1 原则」备份:</p><blockquote><p>3:存储 3 份完整文件,一份原件加上两份拷贝。<br>2:将文件起码保持在两种不同的介质上。<br>1:将一份拷贝保存在异地。</p></blockquote><p>重要数据的具体备份方式:</p><ol><li>群晖NAS内置2T机械硬盘保存一份原件</li><li>通过USB Copy套件,镜像同步一份到移动硬盘,做冷备份</li><li>通过Cloud Sync套件,同步一份到云盘,最终选择的是百度云盘</li></ol><h2 id="备份与同步的理解"><a href="#备份与同步的理解" class="headerlink" title="备份与同步的理解"></a>备份与同步的理解</h2><p>群晖NAS的备份与同步的区别:</p><ol><li>备份:每次都是完整备份,多份历史完整备份,一般存放的是特定的备份文件格式,特定格式的无法直接查看。</li><li>同步:字面意思,数据保持一致,没有历史版本。</li></ol><p>不同套件对于备份和同步的支持情况:</p><ol><li>Hyper Backup:NAS->各端,只支持完整备份,不支持增量备份,特定格式,不支持直接查看。</li><li>USB Copy:NAS->USB硬盘,同时支持备份和同步,备份是指每次拷贝到不同的文件夹里,同步支持镜像和增量,区别在于是否同步删除。</li><li>Cloud Sync:NAS->云端,支持备份和同步。</li><li>Active Backup for Business:wins->NAS,整机备份,类似Mac的TimeMachine。</li><li>Drive:mac/win->NAS,类拟Dropbox,支持文件的同步和备份,支持按需同步,释放本地空间。</li></ol><p>重要数据的备份最终采用是同步方案,主要是考虑到多份完整备份,会消耗大量的时间及备份盘的容量,但如果需要做回滚,最好采用备份,而不是同步。</p><h2 id="照片备份"><a href="#照片备份" class="headerlink" title="照片备份"></a>照片备份</h2><p>通过DS Photo App,非常方便的将手机/平板里的照片及时上传到NAS,除了手动选择图片上传外,还有自动监控新增图片,自动上传的功能。如下所示:监控DCIM和WeiXin文件夹,同时及时释放手机的空间。<br><img src="/../../hexo-img/%E7%BE%A4%E6%99%96%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%92%8C%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83/2021-01-03-21-04-08.png"></p><p>通过NAS里的Photo Station统一管理照片,做好分类文件夹,再使用Moments套件,实现AI识别,自动对人物、主题和位置分类。</p><p>手机上的照片上传之所以不使用Moments,是因为Moments会以照片日期来新建文件夹进行分类,当照片多了后非常不方便管理,详细可参考:<a href="https://post.smzdm.com/p/av7zn43p/">群晖NAS相册的正确玩法!使用Moments的朋友必看!强烈推荐搭配DS Photo使用!</a></p><p>我已经把多年来各组的照片统一上传到NAS里了,包括QQ空间,小米云盘,华为云盘,Mac的照片App,移动硬盘。再通过321原则进行备份,保证照片的安全性。</p><h2 id="多终端之间的协作"><a href="#多终端之间的协作" class="headerlink" title="多终端之间的协作"></a>多终端之间的协作</h2><p>为了摆脱对于一台电脑的依赖,做到多终端之间的无缝接力,需要实现多终端之间的资料和代码的接力。</p><p>主力Macbook Pro通过Synology Drive时时同步笔记和资料到NAS上,使用上与Dropbox一样,通过监听本地的文件变动来保存同步,不影响正常的工作,网络不佳时,可以随时暂停同步。<br><img src="/../../hexo-img/%E7%BE%A4%E6%99%96%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%92%8C%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83/2021-01-03-21-43-18.png"></p><p>为了减少家用Mac,手机和平板的环境配置上的麻烦,同时也是为了统一生产环境,在NAS里通过Docker搭建了Web版VSCode软件,家用Mac,手机和平台通过浏览器访问Web版VSCode接力,使用体验与VSCode的客户端没有区别,如下所示:<br><img src="/../../hexo-img/%E7%BE%A4%E6%99%96%E6%90%AD%E5%BB%BA%E4%B8%AA%E4%BA%BA%E5%92%8C%E5%B7%A5%E4%BD%9C%E7%9A%84%E6%95%B0%E6%8D%AE%E4%B8%AD%E5%BF%83/2021-01-03-21-52-50.png"></p><p>通过Docker部署code-server(即web版VSCode)的<a href="https://www.zhyong.cn/posts/99b4/">参考教程</a>,具体的备份过程很简单,就不重复了,只讲一个遇到的问题:</p><ol><li>出错信息:Service Workers are not enabled in browser</li><li>原因:要https打开code-server,service workers需要https访问</li><li>解决方案:把code-server容器里的配置文件“~/.config/code-server/config.yaml”里的cert值为true,再重启容器,通过https访问即可。</li></ol><h2 id="构建下载中心"><a href="#构建下载中心" class="headerlink" title="构建下载中心"></a>构建下载中心</h2><p>通过Download Station套件,实现NAS的下载和解压功能,对大文件的下载帮助很大,如Android的源码,即可以不影响工作,同时又减少工作电脑的硬盘容量。</p><h2 id="替换腾讯云,Docker搭建Anki-Server端"><a href="#替换腾讯云,Docker搭建Anki-Server端" class="headerlink" title="替换腾讯云,Docker搭建Anki Server端"></a>替换腾讯云,Docker搭建Anki Server端</h2><p>通过Docker搭建Anki Server端非常的简单,<a href="https://zhuanlan.zhihu.com/p/70269217">搭建参考教程</a>。</p><p>Docker搭建的服务,无法通过QuickConnect实现外网访问。</p><p>QuickConnect实现内网穿透的机制如下:</p><ol><li>获取NAS的内网IP,尝试直联,速度最快</li><li>获取NAS的公网IP,尝试直联,速度次之</li><li>通过中继服务器,网络转发,速度最慢</li></ol><p>默认情况下,NAS是内网的IPv4地址,外网无法直连,只能通过中继服务器,做流量转发实现。</p><p>DDNS的内网穿透的方案过程:</p><ol><li>向三大运营商申请公网IP,默认分配的是内网IP,同时让运营商把光猫的模式改为桥接模式,即把公网IP设置给路由器</li><li>给NAS配置固定的内网IP,同是在路由器里配置端口映射,可以手动配置,也可以把路由器的 UPnP 功能打开,NAS可以自由配置。</li><li>用此公网IP和服务Port,即可访问Docker搭建的服务,但此公网IP是会变的,最好的再配置DDNS,即动态DNS服务,通过域名访问。</li></ol><p>如果分配的是IPv6地址,那本身就是公网IP,不需要额外申请,也是趋势。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://post.smzdm.com/p/av7zn43p/">群晖NAS相册的正确玩法!使用Moments的朋友必看!强烈推荐搭配DS Photo使用!</a></li><li><a href="https://zhuanlan.zhihu.com/p/75164381">群晖 NAS 选购 & 入门指南:动手打造自己的家庭数据中心</a></li></ol>]]></content>
<summary type="html"><p>经历了多次换手机后,生活中珍贵的照片被分散在多处,如QQ空间、小米云盘、华为云盘、微信收藏等处,加大了管理难度,不仅要管理账号,还要定期给云空间续费,同时查看也非常的不方便,甚至导致一些珍贵的照片的遗失。</p>
<p>同时受制于网络的原因,批量管理照片也非常的不方便,这些</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="NAS" scheme="https://handsomeliuyang.github.io/tags/NAS/"/>
</entry>
<entry>
<title>StudyDemos的Demo列表的动态配置实现</title>
<link href="https://handsomeliuyang.github.io/2020/11/05/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-StudyDemos%E7%9A%84Demo%E5%88%97%E8%A1%A8%E7%9A%84%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE%E5%AE%9E%E7%8E%B0/"/>
<id>https://handsomeliuyang.github.io/2020/11/05/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-StudyDemos%E7%9A%84Demo%E5%88%97%E8%A1%A8%E7%9A%84%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE%E5%AE%9E%E7%8E%B0/</id>
<published>2020-11-05T02:00:00.000Z</published>
<updated>2023-10-08T02:53:20.130Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>每次在StudyDemos工程里,添加知识点Demo时,总有一部分工作是重复的,那就是在首页添加入口。省掉这个重复添加的过程就是我的目的。</p><h1 id="实现思路"><a href="#实现思路" class="headerlink" title="实现思路"></a>实现思路</h1><p>与APIDemo App类似,实现思路如下:</p><ol><li>对manifest里的activity添加两个约定,查找规范和目录规范</li><li>首页自动扫描上面特定的intent-filter,获取Demo列表和名称</li><li>通过RecyclerView显示,同时支持多级目录</li></ol><p>效果如下所示:</p><p><img src="/../../hexo-img/StudyDemos%E7%9A%84Demo%E5%88%97%E8%A1%A8%E7%9A%84%E5%8A%A8%E6%80%81%E9%85%8D%E7%BD%AE%E5%AE%9E%E7%8E%B0/2020-11-17-09-46-53.png"></p><p>此Demo的源码:<a href="https://github.com/handsomeliuyang/AndroidStudyDemo">AndroidStudyDemo</a></p><h1 id="实现过程"><a href="#实现过程" class="headerlink" title="实现过程"></a>实现过程</h1><h2 id="查找规范和目录规范"><a href="#查找规范和目录规范" class="headerlink" title="查找规范和目录规范"></a>查找规范和目录规范</h2><p><strong>查找规范:</strong>通过指定特殊的intent-filter实现过滤出真正的Demo Activity,复用Android已有的SAMPLE_CODE类别,如下所示:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.MAIN"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.SAMPLE_CODE"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">intent-filter</span>></span></span><br></pre></td></tr></table></figure><p><strong>目录规范:</strong>通过“/”分割多级目录和名称,如下所示:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">android:label="Binder/AIDL例子"</span><br></pre></td></tr></table></figure><h2 id="首页显示"><a href="#首页显示" class="headerlink" title="首页显示"></a>首页显示</h2><p>由于有目录的概念,不是简单的查找出所有的Demo Activity后直接显示,需要判断类型:</p><ol><li>当前如果是目录,跳转的Intent还首页,但只显示当前目录下的子目录或Demo</li><li>当前如果是Demo,跳转的Intent就是Demo界面</li></ol><h3 id="查询所有的Demo"><a href="#查询所有的Demo" class="headerlink" title="查询所有的Demo"></a>查询所有的Demo</h3><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> intent: Intent = Intent(Intent.ACTION_MAIN)</span><br><span class="line"> .addCategory(Intent.CATEGORY_SAMPLE_CODE)</span><br><span class="line"> <span class="comment">// 为了防止把系统的Demo也查询出来,指定包名查询</span></span><br><span class="line"> .setPackage(<span class="keyword">this</span>.packageName) </span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> list: List<ResolveInfo> =</span><br><span class="line"> <span class="keyword">this</span>.packageManager.queryIntentActivities(intent, <span class="number">0</span>) ?: <span class="keyword">return</span> demos</span><br></pre></td></tr></table></figure><h3 id="对所有的Demo进行过滤处理,只获取当前目录下的子目录或Demo"><a href="#对所有的Demo进行过滤处理,只获取当前目录下的子目录或Demo" class="headerlink" title="对所有的Demo进行过滤处理,只获取当前目录下的子目录或Demo"></a>对所有的Demo进行过滤处理,只获取当前目录下的子目录或Demo</h3><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><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getData</span><span class="params">(prefix: <span class="type">String</span>)</span></span>: MutableList<DemosAdapter.Demo> {</span><br><span class="line"> <span class="keyword">val</span> demos: MutableList<DemosAdapter.Demo> = mutableListOf<DemosAdapter.Demo>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> intent: Intent = Intent(Intent.ACTION_MAIN)</span><br><span class="line"> .addCategory(Intent.CATEGORY_SAMPLE_CODE)</span><br><span class="line"> .setPackage(<span class="keyword">this</span>.packageName)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> list: List<ResolveInfo> =</span><br><span class="line"> <span class="keyword">this</span>.packageManager.queryIntentActivities(intent, <span class="number">0</span>) ?: <span class="keyword">return</span> demos</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> prefixPath: List<String>? = <span class="literal">null</span></span><br><span class="line"> <span class="keyword">var</span> prefixWithSlash: String = prefix</span><br><span class="line"> <span class="keyword">if</span>(prefix != <span class="string">""</span>) {</span><br><span class="line"> prefixPath = prefix.split(<span class="string">"/"</span>)</span><br><span class="line"> prefixWithSlash = <span class="string">"<span class="variable">$prefix</span>/"</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 此Map是用于过滤重复子目录</span></span><br><span class="line"> <span class="keyword">val</span> map:MutableMap<String, <span class="built_in">Boolean</span>> = mutableMapOf<String, <span class="built_in">Boolean</span>>()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (info <span class="keyword">in</span> list) {</span><br><span class="line"> <span class="keyword">val</span> label:String = info.activityInfo.loadLabel(<span class="keyword">this</span>.packageManager).toString()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(prefixPath == <span class="literal">null</span> || label.startsWith(prefixWithSlash)) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> labelPath = label.split(<span class="string">"/"</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 获取当前的显示名称</span></span><br><span class="line"> <span class="keyword">val</span> nextLabel: String = labelPath[prefixPath?.size ?: <span class="number">0</span>]</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 通过长度判断当是否是最后一个path</span></span><br><span class="line"> <span class="keyword">if</span> ((prefixPath?.size ?: <span class="number">0</span>) == (labelPath.size - <span class="number">1</span>)) {</span><br><span class="line"> <span class="keyword">val</span> result = Intent()</span><br><span class="line"> result.setClassName(info.activityInfo.packageName, info.activityInfo.name)</span><br><span class="line"> demos.add(DemosAdapter.Demo(nextLabel, result))</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span>(map.<span class="keyword">get</span>(nextLabel) == <span class="literal">null</span>){</span><br><span class="line"> <span class="keyword">val</span> result = Intent(<span class="keyword">this</span>, StudyDemos::<span class="keyword">class</span>.java)</span><br><span class="line"> result.putExtra(<span class="string">"com.example.android.apis.Path"</span>, <span class="keyword">if</span>(prefix == <span class="string">""</span>) nextLabel <span class="keyword">else</span> <span class="string">"<span class="subst">${prefix}</span>/<span class="subst">${nextLabel}</span>"</span>)</span><br><span class="line"> demos.add(DemosAdapter.Demo(nextLabel, result))</span><br><span class="line"> map.put(nextLabel, <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><br><span class="line"> <span class="keyword">return</span> demos</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="RecyclerView显示"><a href="#RecyclerView显示" class="headerlink" title="RecyclerView显示"></a>RecyclerView显示</h3><p><strong>初始化RecyclerView</strong></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="keyword">val</span> recyclerView: RecyclerView = findViewById<RecyclerView>(R.id.recycler_view)</span><br><span class="line">recyclerView.setHasFixedSize(<span class="literal">true</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">// set LayoutManager</span></span><br><span class="line"><span class="keyword">val</span> linearLayoutManager = LinearLayoutManager(<span class="keyword">this</span>)</span><br><span class="line">linearLayoutManager.orientation = LinearLayoutManager.VERTICAL</span><br><span class="line">recyclerView.layoutManager = linearLayoutManager</span><br><span class="line"></span><br><span class="line"><span class="comment">// set Adapter</span></span><br><span class="line"><span class="keyword">val</span> demos: MutableList<DemosAdapter.Demo> = getData(path?:<span class="string">""</span>)</span><br><span class="line">recyclerView.adapter = DemosAdapter(demos, <span class="keyword">object</span>: DemosAdapter.OnItemClickListener {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onClick</span><span class="params">(holder: <span class="type">DemosAdapter</span>.<span class="type">DemosViewHolder</span>, demo: <span class="type">DemosAdapter</span>.<span class="type">Demo</span>)</span></span> {</span><br><span class="line"> <span class="keyword">this</span><span class="symbol">@StudyDemos</span>.startActivity(demo.intent)</span><br><span class="line"> }</span><br><span class="line">})</span><br><span class="line"></span><br><span class="line"><span class="comment">// set 分隔线</span></span><br><span class="line">recyclerView.addItemDecoration(DividerItemDecoration(<span class="keyword">this</span>, DividerItemDecoration.VERTICAL))</span><br></pre></td></tr></table></figure><p><strong>Adapter:</strong></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><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="keyword">class</span> <span class="title class_">DemosAdapter</span>(<span class="keyword">var</span> demos: MutableList<Demo>, <span class="keyword">var</span> itemClickListener: OnItemClickListener) : RecyclerView.Adapter<DemosAdapter.DemosViewHolder>() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">getItemViewType</span><span class="params">(position: <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="number">0</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreateViewHolder</span><span class="params">(parent: <span class="type">ViewGroup</span>, viewType: <span class="type">Int</span>)</span></span>: DemosViewHolder {</span><br><span class="line"> <span class="keyword">val</span> inflater = LayoutInflater.from(parent.context)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> itemView = inflater.inflate(R.layout.item_demo, parent, <span class="literal">false</span>)</span><br><span class="line"> <span class="keyword">return</span> DemosViewHolder(itemView)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">getItemCount</span><span class="params">()</span></span>: <span class="built_in">Int</span> {</span><br><span class="line"> <span class="keyword">return</span> demos.size</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onBindViewHolder</span><span class="params">(holder: <span class="type">DemosViewHolder</span>, position: <span class="type">Int</span>)</span></span> {</span><br><span class="line"> <span class="keyword">val</span> demo = demos[position]</span><br><span class="line"> holder.titleView.setText(demo.title)</span><br><span class="line"> holder.itemView.setOnClickListener {</span><br><span class="line"> itemClickListener.onClick(holder, demo)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">interface</span> <span class="title class_">OnItemClickListener</span> {</span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">onClick</span><span class="params">(holder: <span class="type">DemosViewHolder</span>, demo: <span class="type">Demo</span>)</span></span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">class</span> <span class="title class_">Demo</span>(<span class="keyword">var</span> title: String, <span class="keyword">var</span> intent: Intent) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">class</span> <span class="title class_">DemosViewHolder</span>(itemView: View) : RecyclerView.ViewHolder(itemView) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> titleView: TextView</span><br><span class="line"></span><br><span class="line"> <span class="keyword">init</span> {</span><br><span class="line"> titleView = itemView.findViewById(R.id.title_view)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://github.com/aosp-mirror/platform_development/tree/master/samples/ApiDemos">ApiDemos</a></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>每次在StudyDemos工程里,添加知识点Demo时,总有一部分工作是重复的,那就是在首页添加入口。省掉这个重复添加的过程就是我的目的。<</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>hexo本地静态搜索实现</title>
<link href="https://handsomeliuyang.github.io/2020/10/31/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-hexo%E6%9C%AC%E5%9C%B0%E9%9D%99%E6%80%81%E6%90%9C%E7%B4%A2%E5%AE%9E%E7%8E%B0/"/>
<id>https://handsomeliuyang.github.io/2020/10/31/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-hexo%E6%9C%AC%E5%9C%B0%E9%9D%99%E6%80%81%E6%90%9C%E7%B4%A2%E5%AE%9E%E7%8E%B0/</id>
<published>2020-10-31T04:40:45.000Z</published>
<updated>2023-10-08T02:53:20.131Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>通过Hexo搭建了静态博客,使用的是hexo-theme-skapp主题,此主题默认支持本地搜索,但由于依赖了lunr库,一直无法安装成本lunr库,导致搜索功能一直无法使用。</p><p>同时为了更加深入的了解Hexo的原理,就想基于hexo-theme-skapp的源码修改,让其支持本地搜索。</p><h1 id="思路分析"><a href="#思路分析" class="headerlink" title="思路分析"></a>思路分析</h1><p>由于Hexo是静态博客,没有后台服务,不能利用常规后端做数据库的存储及查询来实现搜索功能,只能通过前端js来实现查询逻辑。</p><p>静态博客实现搜索的整个流程:</p><ol><li>通过Hexo插件,生成所有文章的索引文件search.json</li><li>前端搜索页面,请求并载入索引文件search.json</li><li>解析检索词,并进行分词处理</li><li>在索引中进行搜索词的匹配</li><li>展示搜索结果</li></ol><p>可复用主题hexo-theme-skapp上的搜索结果展示功能,只需要完成前面4步就行。</p><h1 id="实现过程"><a href="#实现过程" class="headerlink" title="实现过程"></a>实现过程</h1><h2 id="生成索引文件search-json"><a href="#生成索引文件search-json" class="headerlink" title="生成索引文件search.json"></a>生成索引文件search.json</h2><p>刚开始是使用Hexo插件hexo-generator-search来实现此功能,但由于此插件生成的索引文件只有文章的基本数据,缺少很多关键数据,如cover, date, subtitle, author等等信息。</p><p>最终基于插件hexo-generator-search的源码重写索引生成功能,整体实现过程很简单,主要是从locals.posts对象里,读取需要数据存入索引文件里。</p><p>展示所需要的数据结构:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"cover"</span><span class="punctuation">:</span> <span class="string">"页面头部图片地址"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"title"</span><span class="punctuation">:</span> <span class="string">"标题"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"month"</span><span class="punctuation">:</span> <span class="string">"十月"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"day"</span><span class="punctuation">:</span> <span class="number">31</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"date"</span><span class="punctuation">:</span> <span class="string">"2020-10-31"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"url"</span><span class="punctuation">:</span> <span class="string">"地址"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"desc"</span><span class="punctuation">:</span> <span class="string">"字标题"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"authorNick"</span><span class="punctuation">:</span> <span class="string">"作者"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"authorLink"</span><span class="punctuation">:</span> <span class="string">"作者地址"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"tagArr"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"> <span class="punctuation">{</span></span><br><span class="line"> <span class="attr">"name"</span><span class="punctuation">:</span> <span class="string">"标签名"</span><span class="punctuation">,</span></span><br><span class="line"> <span class="attr">"path"</span><span class="punctuation">:</span> <span class="string">"标签地址"</span></span><br><span class="line"> <span class="punctuation">}</span></span><br><span class="line"> <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>上面的大部分都不难,只有一个功能比较麻烦,生成的月份需要转换为中文月份,通过一个数组进行转换,如下所示:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> <span class="title class_">MonthArr</span> = [<span class="string">"一月"</span>, <span class="string">"二月"</span>, <span class="string">"三月"</span>, <span class="string">"四月"</span>, <span class="string">"五月"</span>, <span class="string">"六月"</span>, <span class="string">"七月"</span>, <span class="string">"八月"</span>, <span class="string">"九月"</span>, <span class="string">"十月"</span>, <span class="string">"十一月"</span>, <span class="string">"十二月"</span>]</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> dateObj = <span class="keyword">new</span> <span class="title class_">Date</span>(data.<span class="property">date</span>);</span><br><span class="line"><span class="keyword">var</span> month = <span class="title class_">MonthArr</span>[dateObj.<span class="title function_">getMonth</span>()];</span><br></pre></td></tr></table></figure><p>整体实现如下所示:</p><figure class="highlight javascript"><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> pathFn = <span class="built_in">require</span>(<span class="string">'path'</span>);</span><br><span class="line"><span class="keyword">var</span> fs = <span class="built_in">require</span>(<span class="string">'fs'</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">json_generator</span>(<span class="params">locals</span>){</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> searchName = <span class="string">"search.json"</span>;</span><br><span class="line"> <span class="keyword">var</span> datas = locals.<span class="property">posts</span>.<span class="title function_">sort</span>(<span class="string">'-date'</span>) || [];</span><br><span class="line"> <span class="comment">// var datas = locals.pages;</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> <span class="title class_">MonthArr</span> = [<span class="string">"一月"</span>, <span class="string">"二月"</span>, <span class="string">"三月"</span>, <span class="string">"四月"</span>, <span class="string">"五月"</span>, <span class="string">"六月"</span>, <span class="string">"七月"</span>, <span class="string">"八月"</span>, <span class="string">"九月"</span>, <span class="string">"十月"</span>, <span class="string">"十一月"</span>, <span class="string">"十二月"</span>]</span><br><span class="line"> <span class="keyword">var</span> res = <span class="keyword">new</span> <span class="title class_">Array</span>() </span><br><span class="line"> <span class="keyword">var</span> index = <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> datas.<span class="title function_">each</span>(<span class="keyword">function</span>(<span class="params">data</span>) {</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">indexing</span> != <span class="literal">undefined</span> && !data.<span class="property">indexing</span>) <span class="keyword">return</span>;</span><br><span class="line"> <span class="keyword">var</span> temp_data = <span class="keyword">new</span> <span class="title class_">Object</span>() </span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">cover</span>) { </span><br><span class="line"> temp_data.<span class="property">cover</span> = data.<span class="property">cover</span> </span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">title</span>) { </span><br><span class="line"> temp_data.<span class="property">title</span> = data.<span class="property">title</span> </span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">date</span>) { </span><br><span class="line"> <span class="keyword">var</span> dateObj = <span class="keyword">new</span> <span class="title class_">Date</span>(data.<span class="property">date</span>);</span><br><span class="line"> <span class="keyword">var</span> year = dateObj.<span class="title function_">getFullYear</span>();</span><br><span class="line"> <span class="keyword">var</span> month = dateObj.<span class="title function_">getMonth</span>();</span><br><span class="line"> <span class="keyword">var</span> day = dateObj.<span class="title function_">getDate</span>();</span><br><span class="line"> </span><br><span class="line"> temp_data.<span class="property">month</span> = <span class="title class_">MonthArr</span>[month];</span><br><span class="line"> temp_data.<span class="property">day</span> = day;</span><br><span class="line"> temp_data.<span class="property">date</span> = year + <span class="string">"-"</span> + (month+<span class="number">1</span>) + <span class="string">"-"</span> + day;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">path</span>) { </span><br><span class="line"> temp_data.<span class="property">url</span> = <span class="string">'/'</span> + data.<span class="property">path</span> </span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">subtitle</span>) { </span><br><span class="line"> temp_data.<span class="property">desc</span> = data.<span class="property">subtitle</span> </span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">author</span> && data.<span class="property">author</span>.<span class="property">nick</span>) { </span><br><span class="line"> temp_data.<span class="property">authorNick</span> = data.<span class="property">author</span>.<span class="property">nick</span></span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">author</span> && data.<span class="property">author</span>.<span class="property">link</span>) { </span><br><span class="line"> temp_data.<span class="property">authorLink</span> = data.<span class="property">author</span>.<span class="property">link</span></span><br><span class="line"> } </span><br><span class="line"> <span class="comment">// if (content != false && data._content) { </span></span><br><span class="line"> <span class="comment">// temp_data.content = data._content </span></span><br><span class="line"> <span class="comment">// } </span></span><br><span class="line"> <span class="keyword">if</span> (data.<span class="property">tags</span> && data.<span class="property">tags</span>.<span class="property">length</span> > <span class="number">0</span>) { </span><br><span class="line"> <span class="keyword">var</span> tags = [];</span><br><span class="line"> </span><br><span class="line"> data.<span class="property">tags</span>.<span class="title function_">forEach</span>(<span class="keyword">function</span> (<span class="params">tag</span>) {</span><br><span class="line"> tags.<span class="title function_">push</span>({</span><br><span class="line"> <span class="string">'name'</span>: tag.<span class="property">name</span>,</span><br><span class="line"> <span class="string">'path'</span>: <span class="string">'/'</span> + tag.<span class="property">path</span></span><br><span class="line"> });</span><br><span class="line"> }); </span><br><span class="line"> temp_data.<span class="property">tagArr</span> = tags </span><br><span class="line"> } </span><br><span class="line"> res[index] = temp_data; </span><br><span class="line"> index += <span class="number">1</span>; </span><br><span class="line"> }); </span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> json = <span class="title class_">JSON</span>.<span class="title function_">stringify</span>(res);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> {</span><br><span class="line"> <span class="attr">path</span>: searchName,</span><br><span class="line"> <span class="attr">data</span>: json</span><br><span class="line"> };</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">hexo.<span class="property">extend</span>.<span class="property">generator</span>.<span class="title function_">register</span>(<span class="string">'json'</span>, json_generator);</span><br></pre></td></tr></table></figure><h2 id="前端的检索逻辑"><a href="#前端的检索逻辑" class="headerlink" title="前端的检索逻辑"></a>前端的检索逻辑</h2><p>通过axios库请求索引文件search.json:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">axios</span><br><span class="line"> .<span class="title function_">get</span>(<span class="string">'/search.json'</span>)</span><br><span class="line"> .<span class="title function_">then</span>(<span class="keyword">function</span>(<span class="params">res</span>){</span><br><span class="line"> <span class="keyword">return</span> res.<span class="property">data</span>;</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">then</span>(<span class="keyword">function</span>(<span class="params">data</span>) { </span><br><span class="line"> self.<span class="title function_">initSearch</span>(data);</span><br><span class="line"> })</span><br><span class="line"> .<span class="title function_">catch</span>(<span class="keyword">function</span>(<span class="params">error</span>){</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(error);</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>获取搜索词,在索引中进行搜索词的匹配,如下所示:</p><figure class="highlight javascript"><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">function</span> <span class="title function_">searchFunc</span>(<span class="params">queryString, datas</span>) {</span><br><span class="line"> <span class="keyword">var</span> result = [];</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 对搜索词做简单的分词</span></span><br><span class="line"> <span class="keyword">var</span> keyword = queryString.<span class="title function_">trim</span>();</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">'11111'</span>, keyword);</span><br><span class="line"> <span class="comment">// 本地查找</span></span><br><span class="line"> <span class="keyword">for</span>(<span class="keyword">var</span> dataIndex <span class="keyword">in</span> datas){</span><br><span class="line"> <span class="keyword">var</span> data = datas[dataIndex];</span><br><span class="line"></span><br><span class="line"> data.<span class="property">title</span> = data.<span class="property">title</span> || <span class="string">''</span>;</span><br><span class="line"> data.<span class="property">content</span> = data.<span class="property">content</span> || <span class="string">''</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> index_title = data.<span class="property">title</span>.<span class="title function_">indexOf</span>(keyword);</span><br><span class="line"> <span class="keyword">var</span> index_content = data.<span class="property">content</span>.<span class="title function_">indexOf</span>(keyword);</span><br><span class="line"> <span class="keyword">if</span> (index_title >= <span class="number">0</span> || index_content >= <span class="number">0</span>) {</span><br><span class="line"> result.<span class="title function_">push</span>(data);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后就是展示搜索结果,此处是复用主题hexo-theme-skapp的展示逻辑。</p><h1 id="主题hexo-theme-skapp的其他个性化改造"><a href="#主题hexo-theme-skapp的其他个性化改造" class="headerlink" title="主题hexo-theme-skapp的其他个性化改造"></a>主题hexo-theme-skapp的其他个性化改造</h1><p>很多文章只有标题数据,没有其他信息,导致列表显示很丑,如下所示:</p><p><img src="/../../hexo-img/hexo%E6%9C%AC%E5%9C%B0%E9%9D%99%E6%80%81%E6%90%9C%E7%B4%A2%E5%AE%9E%E7%8E%B0/2020-11-01-15-18-14.png"></p><p>修改方式是给各种信息添加默认值。</p><h2 id="Cover图片添加默认图"><a href="#Cover图片添加默认图" class="headerlink" title="Cover图片添加默认图"></a>Cover图片添加默认图</h2><p>此功能默认已经支持,只需要配置一下就行,如下所示:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># page default cover</span><br><span class="line">default_cover: /hexo-img/default_cover.png</span><br></pre></td></tr></table></figure><h2 id="描述添加默认值"><a href="#描述添加默认值" class="headerlink" title="描述添加默认值"></a>描述添加默认值</h2><p>当文章缺少相应的描述信息时,获取文章正文的前200个字符,但由于正文里有html标签,需要使用相应的函数去掉html标签信息,实现代码如下所示:</p><figure class="highlight plaintext"><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"><p itemprop="articleSection" class="min-article__desc"></span><br><span class="line"> {% if not post.subtitle %}</span><br><span class="line"> {{ truncate(strip_html(post.content), 200) }}</span><br><span class="line"> {% endif %}</span><br><span class="line"> {% if post.subtitle %}</span><br><span class="line"> {{ post.subtitle }}</span><br><span class="line"> {% endif %}</span><br><span class="line"></p></span><br></pre></td></tr></table></figure><h2 id="作者信息添加默认值"><a href="#作者信息添加默认值" class="headerlink" title="作者信息添加默认值"></a>作者信息添加默认值</h2><p>因为此博客是个人博客,作者信息本身可以不用配置,直接使用config里的作者信息就行。</p><figure class="highlight plaintext"><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">{% if post.author %}</span><br><span class="line"> <span itemprop="author" itemscope itemtype="http://schema.org/Person"></span><br><span class="line"> <a itemprop="url" href="{{ post.author.link }}" target="_blank"></span><br><span class="line"> <span itemprop="name">{{ titlecase(post.author.name || post.author.nick) }}</span></span><br><span class="line"> </a></span><br><span class="line"> </span></span><br><span class="line">{% else %}</span><br><span class="line"> <span itemprop="author" itemscope itemtype="http://schema.org/Person"></span><br><span class="line"> <a itemprop="url" href="{{ get_setting('author').link }}" target="_blank"></span><br><span class="line"> <span itemprop="name">{{ titlecase( get_setting('author').name ) }}</span></span><br><span class="line"> </a></span><br><span class="line"> </span></span><br><span class="line">{% endif %}</span><br></pre></td></tr></table></figure><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>类似的方式修改了其他几处问题:</p><ol><li>详情页作者的默认显示</li><li>详情页文章的显示效果调整,字体调大为16px,减少间距到10px</li><li>修改首页的标题标的字体</li></ol><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://liam.page/2017/09/21/local-search-engine-in-Hexo-site/">为 Hexo 博客创建本地搜索引擎</a></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>通过Hexo搭建了静态博客,使用的是hexo-theme-skapp主题,此主题默认支持本地搜索,但由于依赖了lunr库,一直无法安装成本l</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="hexo" scheme="https://handsomeliuyang.github.io/tags/hexo/"/>
</entry>
<entry>
<title>VSCode插件开发之Markdown扩展功能</title>
<link href="https://handsomeliuyang.github.io/2020/10/24/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-VSCode%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E4%B9%8BMarkdown%E6%89%A9%E5%B1%95%E5%8A%9F%E8%83%BD/"/>
<id>https://handsomeliuyang.github.io/2020/10/24/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-VSCode%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E4%B9%8BMarkdown%E6%89%A9%E5%B1%95%E5%8A%9F%E8%83%BD/</id>
<published>2020-10-23T16:00:00.000Z</published>
<updated>2023-10-08T02:53:20.130Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>新的文档采用Git+Markdown实现,本地需要一个好Markdown编辑器,最后选择使用vscode轻量级文本编辑器。</p><p>Markdown是一种轻量级的标记语言,虽然优化很多,但有两个明显的缺点:</p><ol><li>图片添加及管理</li><li>表格添加及管理</li></ol><p>表格可以采用在线文档替代,再分享或截图添加,图片的管理方式主要是有如下几种:</p><ol><li>图片先上传到图床等cdn服务器,通过生成的网络地址引用</li><li>放置统一的目录,如根目录的img文件夹,再通过相对路径添加</li><li>通过文档名称创建文件夹,把markdown文件和图片资源统一放入</li></ol><p>每种方式都各有优缺点,最终考虑到文档的可读性及图片管理,采用了方案2和3的融合方式:</p><figure class="highlight plaintext"><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">├── 文档.md</span><br><span class="line">├── img</span><br><span class="line"> └── 文档</span><br><span class="line"> ├── a.png</span><br><span class="line"> └── b.png</span><br></pre></td></tr></table></figure><p>可以通过vscode的插件Paste Image实现从剪贴板自动复制图片到上述目录,如此插件有两个问题:</p><ol><li>文件目录需要手动创建,不能自动创建</li><li>显示的图片不能修改显示大小,需要手动改为img标签来修改</li></ol><p>通过git来管理文档还有比较严重的问题:无法快速的分享文档链接,需要通过web版git获取文档地址</p><h1 id="需求及效果"><a href="#需求及效果" class="headerlink" title="需求及效果"></a>需求及效果</h1><p>需求如下:</p><ol><li>从剪贴板自动复制图片,同时满足如下要求:</li><li>图片保存到以当前Markdown文件命名的文件里</li><li>通过img标签添加,默认width=500</li><li>获取当前文件的相对路径,并生成gitlab的在线地址</li></ol><p>效果如下:</p><p><img src="/../../hexo-img/VSCode%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E4%B9%8BMarkdown%E6%89%A9%E5%B1%95%E5%8A%9F%E8%83%BD/2020-10-26-11-23-44.png"></p><h1 id="VSCode插件开发流程"><a href="#VSCode插件开发流程" class="headerlink" title="VSCode插件开发流程"></a>VSCode插件开发流程</h1><p>参考官方文档:</p><ol><li><a href="https://code.visualstudio.com/api/get-started/your-first-extension">Your First Extension</a></li><li><a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension">Publishing Extensions</a></li></ol><p>阅读上面官方文档,通过代码脚手架,生成Hello World模板工程。</p><p>关键点:extension要选择typescript类型,方便复用第三方插件源码。</p><p>常用命令:</p><ol><li>F5:运行</li><li>npm run publish:发布部署</li></ol><h1 id="Git-Markdown-Tools插件开发"><a href="#Git-Markdown-Tools插件开发" class="headerlink" title="Git Markdown Tools插件开发"></a>Git Markdown Tools插件开发</h1><p>vscode的插件开发很简单,如下图所示:</p><p><img src="/../../hexo-img/VSCode%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E4%B9%8BMarkdown%E6%89%A9%E5%B1%95%E5%8A%9F%E8%83%BD/2020-10-26-14-16-12.png"></p><h2 id="剪贴版复制图片,并设置默认大小"><a href="#剪贴版复制图片,并设置默认大小" class="headerlink" title="剪贴版复制图片,并设置默认大小"></a>剪贴版复制图片,并设置默认大小</h2><p>此功能是基于<a href="https://github.com/mushanshitiancai/vscode-paste-image">Paste Image</a>插件的源码做的微调,新增img选择,如下所示:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">public <span class="keyword">static</span> <span class="title function_">renderFilePath</span>(<span class="attr">languageId</span>: string, <span class="attr">basePath</span>: string, <span class="attr">imageFilePath</span>: string): string {</span><br><span class="line"> ....</span><br><span class="line"> <span class="keyword">let</span> imageSyntaxPrefix = <span class="string">""</span>;</span><br><span class="line"> <span class="keyword">let</span> imageSyntaxSuffix = <span class="string">""</span>;</span><br><span class="line"> <span class="keyword">switch</span> (languageId) {</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"markdown"</span>:</span><br><span class="line"> imageSyntaxPrefix = <span class="string">`![](`</span>;</span><br><span class="line"> imageSyntaxSuffix = <span class="string">`)`</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"asciidoc"</span>:</span><br><span class="line"> imageSyntaxPrefix = <span class="string">`image::`</span>;</span><br><span class="line"> imageSyntaxSuffix = <span class="string">`[]`</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> <span class="string">"img"</span>: <span class="comment">// 新增标签类型</span></span><br><span class="line"> imageSyntaxPrefix = <span class="string">`<img src="`</span>;</span><br><span class="line"> imageSyntaxSuffix = <span class="string">`" width = "500" />`</span>;</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 class="keyword">return</span> result;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>在右键菜单里添加相应的快捷入口,只需要配置package.json文件即可,如下所示:</p><figure class="highlight json"><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 class="punctuation">{</span></span><br><span class="line">....</span><br><span class="line"><span class="attr">"activationEvents"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="string">"onCommand:git-markdown-tools.MarkdownImagePastePrimary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">"onCommand:git-markdown-tools.MarkdownImagePasteSubsidiary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"contributes"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"><span class="attr">"commands"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.MarkdownImagePastePrimary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"title"</span><span class="punctuation">:</span> <span class="string">"粘贴图片-Primary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"category"</span><span class="punctuation">:</span> <span class="string">"58IGit"</span></span><br><span class="line"><span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.MarkdownImagePasteSubsidiary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"title"</span><span class="punctuation">:</span> <span class="string">"粘贴图片-Subsidiary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"category"</span><span class="punctuation">:</span> <span class="string">"58IGit"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"menus"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"><span class="attr">"editor/context"</span><span class="punctuation">:</span> <span class="punctuation">[</span> <span class="comment">// 编辑器界面</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.MarkdownImagePastePrimary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"group"</span><span class="punctuation">:</span> <span class="string">"navigation"</span></span><br><span class="line"><span class="punctuation">}</span><span class="punctuation">,</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.MarkdownImagePasteSubsidiary"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"group"</span><span class="punctuation">:</span> <span class="string">"navigation"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br></pre></td></tr></table></figure><p>Paste Image插件本身有一个问题:不支持直接复制本地的图片文件。</p><p>通过分析Paste Image的源码了解到,剪贴板的操作不是通过javascript实现的,而是通过调用对应平台的脚本实现:</p><ol><li>mac:AppleScript脚本</li><li>linux:shell脚本</li></ol><p>在macOS里,直接复制图片文件到剪贴板时,剪贴板里只有相应的文件路径,没有相应的图片内容,Paste Image的AppleScript脚本不支持这种类型,这个是后续需要修改点。</p><h2 id="获取当前Markdown的远程Git地址"><a href="#获取当前Markdown的远程Git地址" class="headerlink" title="获取当前Markdown的远程Git地址"></a>获取当前Markdown的远程Git地址</h2><p>这个功能非常简单,主过程是如下:</p><ol><li>先获取当前Markdown文件相对路径</li><li>拼接上相应服务器的地址即可</li></ol><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line">public <span class="keyword">static</span> <span class="title function_">copyRemoteUrlToClipboard</span>(<span class="params"></span>){</span><br><span class="line"> <span class="comment">// get current edit file path</span></span><br><span class="line"> <span class="keyword">let</span> editor = vscode.<span class="property">window</span>.<span class="property">activeTextEditor</span>;</span><br><span class="line"> <span class="keyword">if</span>(!editor) {</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">let</span> fileUri = editor.<span class="property">document</span>.<span class="property">uri</span>;</span><br><span class="line"> <span class="keyword">if</span>(!fileUri) {</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="comment">// 获取文件的相对路径,相对于项目根目录</span></span><br><span class="line"> <span class="keyword">let</span> filePath = fileUri.<span class="property">fsPath</span>;</span><br><span class="line"> <span class="keyword">let</span> projectPath = vscode.<span class="property">workspace</span>.<span class="property">rootPath</span>;</span><br><span class="line"> <span class="keyword">let</span> fileRelativePath = filePath.<span class="title function_">replace</span>(projectPath ?? <span class="string">''</span>, <span class="string">''</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 通过配置获取远程项目的url地址,git-markdown-tools.romoteurl,'http://igit.58corp.com/wuxian-doc/58app'</span></span><br><span class="line"> <span class="keyword">let</span> projectRomoteUrlConfig = vscode.<span class="property">workspace</span>.<span class="title function_">getConfiguration</span>(<span class="string">'git-markdown-tools'</span>)[<span class="string">'romoteurl'</span>];</span><br><span class="line"> <span class="keyword">if</span> (!projectRomoteUrlConfig) {</span><br><span class="line"> <span class="title class_">Logger</span>.<span class="title function_">showErrorMessage</span>(<span class="string">`The config git-markdown-tools.romoteurl = '<span class="subst">${projectRomoteUrlConfig}</span>' is invalid. please check your config.`</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">let</span> remoteUrl = <span class="title class_">GitShare</span>.<span class="title function_">getRemoteUrl</span>(projectRomoteUrlConfig, <span class="string">'master'</span>, fileRelativePath);</span><br><span class="line"></span><br><span class="line"> vscode.<span class="property">env</span>.<span class="title function_">openExternal</span>(vscode.<span class="property">Uri</span>.<span class="title function_">parse</span>(remoteUrl));</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 拼接远程url地址</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> projectRomoteUrl 用户配置,如:http://igit.58corp.com/wuxian-doc/58app</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> branch master分支,还是当前分支</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> fileRelativePath 当前文件的相对路径,如:技术文档/请求的Header过大调研.md</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">private <span class="keyword">static</span> <span class="title function_">getRemoteUrl</span>(<span class="attr">projectRomoteUrl</span>: string, <span class="attr">branch</span>: string, <span class="attr">fileRelativePath</span>: string): string{</span><br><span class="line"> <span class="keyword">return</span> <span class="string">`<span class="subst">${projectRomoteUrl}</span>/blob/<span class="subst">${branch}</span><span class="subst">${fileRelativePath}</span>`</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在右键菜单里添加相应的快捷入口,只需要配置package.json文件即可,如下所示:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">{</span></span><br><span class="line">...</span><br><span class="line"><span class="attr">"activationEvents"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="string">"onCommand:git-markdown-tools.CopyRemoteUrlToClipboard"</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"contributes"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"><span class="attr">"commands"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.CopyRemoteUrlToClipboard"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"title"</span><span class="punctuation">:</span> <span class="string">"打开远程Master地址"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"category"</span><span class="punctuation">:</span> <span class="string">"58IGit"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"menus"</span><span class="punctuation">:</span> <span class="punctuation">{</span></span><br><span class="line"><span class="attr">"editor/title/context"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.CopyRemoteUrlToClipboard"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"group"</span><span class="punctuation">:</span> <span class="string">"navigation"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">]</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"editor/context"</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="punctuation">{</span></span><br><span class="line"><span class="attr">"command"</span><span class="punctuation">:</span> <span class="string">"git-markdown-tools.CopyRemoteUrlToClipboard"</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">"group"</span><span class="punctuation">:</span> <span class="string">"navigation"</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"><span class="punctuation">}</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="GitHub开源地址"><a href="#GitHub开源地址" class="headerlink" title="GitHub开源地址"></a>GitHub开源地址</h1><ol><li>源码地址:<a href="https://github.com/handsomeliuyang/vscode-git-markdown-tools.git">https://github.com/handsomeliuyang/vscode-git-markdown-tools.git</a></li><li>插件安装地址:<a href="https://marketplace.visualstudio.com/items?itemName=handsomeliuyang.git-markdown-tools&ssr=false#overview">https://marketplace.visualstudio.com/items?itemName=handsomeliuyang.git-markdown-tools&ssr=false#overview</a></li></ol><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol start="2"><li><a href="https://github.com/mushanshitiancai/vscode-paste-image">vscode-paste-image</a></li><li><a href="https://code.visualstudio.com/api/get-started/your-first-extension">Your First Extension</a></li><li><a href="https://github.com/eamodio/vscode-gitlens">vscode-gitlens</a></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>新的文档采用Git+Markdown实现,本地需要一个好Markdown编辑器,最后选择使用vscode轻量级文本编辑器。</p>
<p>M</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="nodejs" scheme="https://handsomeliuyang.github.io/tags/nodejs/"/>
</entry>
<entry>
<title>双开系列:AIDL原理简介及动态扩展实现</title>
<link href="https://handsomeliuyang.github.io/2020/08/09/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/"/>
<id>https://handsomeliuyang.github.io/2020/08/09/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/</id>
<published>2020-08-09T04:32:34.000Z</published>
<updated>2023-10-08T02:53:20.132Z</updated>
<content type="html"><![CDATA[<h1 id="AIDL的用法"><a href="#AIDL的用法" class="headerlink" title="AIDL的用法"></a>AIDL的用法</h1><p>定义接口,IRemoteService.aidl</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.ly.studydemo.binder;</span><br><span class="line"><span class="keyword">import</span> com.ly.studydemo.binder.MyData;</span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">IRemoteService</span> {</span><br><span class="line"> <span class="type">int</span> <span class="title function_">getPid</span><span class="params">()</span>;</span><br><span class="line"> MyData <span class="title function_">getMyData</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译器自动生成两个类:</p><ol><li>服务端:IRemoteService.Stub,继承Binder,真正的服务提供者</li><li>客户端:IRemoteService.Stub.Proxy,实现接口IRemoteService,与服务端通信,请求和结果传输</li></ol><p>服务端通过继承IRemoteService.Stub,提供服务:</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 真正的实现类</span></span><br><span class="line"><span class="keyword">private</span> val mBinder: IRemoteService.Stub = object:IRemoteService.Stub() {</span><br><span class="line"> override fun <span class="title function_">getPid</span><span class="params">()</span>: Int {</span><br><span class="line"> Log.i(TAG, <span class="string">"[RemoteService] getPid()=${android.os.Process.myPid()}"</span>)</span><br><span class="line"> <span class="keyword">return</span> android.os.Process.myPid()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> override fun <span class="title function_">getMyData</span><span class="params">()</span>: MyData? {</span><br><span class="line"> Log.i(TAG, <span class="string">"[RemoteService] getMyData()=${[email protected]}"</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span><span class="meta">@RemoteService</span>.mMyData</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>客户端(如Activity)获取到IBinder对象后,转换为接口对象使用:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">IRemoteService</span> <span class="variable">remoteService</span> <span class="operator">=</span> IRemoteService.Stub.asInterface(service);</span><br></pre></td></tr></table></figure><p>客户端要获取IBinder对象有两种方式:</p><ol><li>异步获取:通过绑定Service获取</li><li>同步获取:通过ContentProvider获取</li></ol><p>整体过程如下图所示:<br><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/AIDL%E4%BD%BF%E7%94%A8.png"></p><h1 id="IRemoteService-Stub源码分析"><a href="#IRemoteService-Stub源码分析" class="headerlink" title="IRemoteService.Stub源码分析"></a>IRemoteService.Stub源码分析</h1><p>由于是跨进程通过,不可能真正持有IRemoteService的实现类,客户端持有仅仅只是一个Proxy对象。</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Proxy</span> <span class="keyword">implements</span> <span class="title class_">com</span>.ly.studydemo.binder.IRemoteService {</span><br><span class="line"> <span class="keyword">private</span> android.os.IBinder mRemote;</span><br><span class="line"></span><br><span class="line"> Proxy(android.os.IBinder remote) {</span><br><span class="line"> mRemote = remote;</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="keyword">public</span> android.os.IBinder <span class="title function_">asBinder</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> mRemote;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> java.lang.String <span class="title function_">getInterfaceDescriptor</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> DESCRIPTOR;</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="keyword">public</span> <span class="type">int</span> <span class="title function_">getPid</span><span class="params">()</span> <span class="keyword">throws</span> android.os.RemoteException {</span><br><span class="line"> android.os.<span class="type">Parcel</span> <span class="variable">_data</span> <span class="operator">=</span> android.os.Parcel.obtain();</span><br><span class="line"> android.os.<span class="type">Parcel</span> <span class="variable">_reply</span> <span class="operator">=</span> android.os.Parcel.obtain();</span><br><span class="line"> <span class="type">int</span> _result;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> _data.writeInterfaceToken(DESCRIPTOR);</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">_status</span> <span class="operator">=</span> mRemote.transact(Stub.TRANSACTION_getPid, _data, _reply, <span class="number">0</span>);</span><br><span class="line"> <span class="keyword">if</span> (!_status && getDefaultImpl() != <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> getDefaultImpl().getPid();</span><br><span class="line"> }</span><br><span class="line"> _reply.readException();</span><br><span class="line"> _result = _reply.readInt();</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> _reply.recycle();</span><br><span class="line"> _data.recycle();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> _result;</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="keyword">public</span> com.ly.studydemo.binder.MyData <span class="title function_">getMyData</span><span class="params">()</span> <span class="keyword">throws</span> android.os.RemoteException {</span><br><span class="line"> android.os.<span class="type">Parcel</span> <span class="variable">_data</span> <span class="operator">=</span> android.os.Parcel.obtain();</span><br><span class="line"> android.os.<span class="type">Parcel</span> <span class="variable">_reply</span> <span class="operator">=</span> android.os.Parcel.obtain();</span><br><span class="line"> com.ly.studydemo.binder.MyData _result;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> _data.writeInterfaceToken(DESCRIPTOR);</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">_status</span> <span class="operator">=</span> mRemote.transact(Stub.TRANSACTION_getMyData, _data, _reply, <span class="number">0</span>);</span><br><span class="line"> <span class="keyword">if</span> (!_status && getDefaultImpl() != <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> getDefaultImpl().getMyData();</span><br><span class="line"> }</span><br><span class="line"> _reply.readException();</span><br><span class="line"> <span class="keyword">if</span> ((<span class="number">0</span> != _reply.readInt())) {</span><br><span class="line"> _result = com.ly.studydemo.binder.MyData.CREATOR.createFromParcel(_reply);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> _result = <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> _reply.recycle();</span><br><span class="line"> _data.recycle();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> _result;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> com.ly.studydemo.binder.IRemoteService sDefaultImpl;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过mRemote.transact(),以code的方式传递需要调用的方法名,服务端收到code码后,会调用IRemoteService.Stub的实现类的对应方法,这就是远程过程调用(RPC)。</p><p><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/20200813104040407.png"></p><h1 id="Binder-IPC的内部实现"><a href="#Binder-IPC的内部实现" class="headerlink" title="Binder IPC的内部实现"></a>Binder IPC的内部实现</h1><p>Android的跨进程通信:</p><ol><li>Zygote进程通过Socket机制通信</li><li>应用进程通过Binder IPC通信</li></ol><p>进程之间之所以无法直接通信的原因是虚拟内存管理技术,进程分为两部分(以32位为例):</p><ol><li>用户空间:0~3G,进程独有,即虚拟内存地址映射到的物理地址是独有,即使用不同的页表</li><li>内核空间:3G~4G,进程共享,即虚拟内存地址映射到的物理地址,其他进程也可以访问到,即使用同一个页表</li></ol><blockquote><p><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/%E8%BF%9B%E7%A8%8B%E9%A1%B5%E8%A1%A8%E9%9A%94%E7%A6%BB.png"><br>图片引用自<a href="https://juejin.im/post/6844904113046568973">Binder内存拷贝的本质和变迁</a></p></blockquote><p>用户空间和内核空间都使用虚拟内存管理技术,由于所有的内核空间的地址,使用同一个页表,可访问相同的物理地址。所以进程间通信要借助内核空间,有两种方式:</p><ol><li>共享内存:两个进程通过内核空间,共享同一块物理内存,都具有读写权限。需要处理同步问题。</li><li>内存拷贝:通常是两次拷贝,先从A进程的用户空间拷贝到共享的内核空间,再从共享的内核空间拷贝至B进程的用户空间。不用考虑同步问题,但性能有损失。</li></ol><p>Binder IPC也是采用内存拷贝,但通过mmap(内存映射技术),只需拷贝一次,提升了性能。<br><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/20200814100618132.png"></p><h1 id="AIDL的限制"><a href="#AIDL的限制" class="headerlink" title="AIDL的限制"></a>AIDL的限制</h1><p>AIDL虽然让跨进程通信变得很简单,但无法实现运行时,动态扩展功能。每次AIDL的接口变化,都需要重新编译。</p><p>在VirtualApp里,通过IPC总线,实现了运行时动态的扩展能力。</p><h1 id="IPC总线(IPCBus)"><a href="#IPC总线(IPCBus)" class="headerlink" title="IPC总线(IPCBus)"></a>IPC总线(IPCBus)</h1><p>AIDL自动生成的Stub和Stub.Proxy的主要功能:</p><ol><li>生成code,如TRANSACTION_getPid, TRANSACTION_getMyDatat,用于传输方法名的传输</li><li>传输方法名(即code)和方法参数,调用真正实现类对应的方法</li></ol><h2 id="总线实现"><a href="#总线实现" class="headerlink" title="总线实现"></a>总线实现</h2><p>通过IPC总线实现这两个能力后,就可以实现运行时增加通信能力,整体框架如下:</p><p><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/20200826033225943.png"></p><p><strong>动态生成code:替换AIDL的编译时生成</strong></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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">ServerInterface</span>(<span class="keyword">val</span> interfaceClass: Class<*>) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> codeToInterfaceMethod: SparseArray<IPCMethod></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> methodToIPCMethodMap: Map<Method, IPCMethod></span><br><span class="line"></span><br><span class="line"> <span class="keyword">init</span> {</span><br><span class="line"> <span class="keyword">val</span> methods = interfaceClass.methods</span><br><span class="line"> codeToInterfaceMethod = SparseArray(methods.size)</span><br><span class="line"> methodToIPCMethodMap = HashMap(methods.size)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 遍历接口的所有方法,生成方法对应的code</span></span><br><span class="line"> <span class="keyword">for</span>((index, method) <span class="keyword">in</span> methods.withIndex()){</span><br><span class="line"> <span class="keyword">val</span> code = Binder.FIRST_CALL_TRANSACTION + index</span><br><span class="line"> <span class="keyword">val</span> ipcMethod = IPCMethod(code, method, interfaceClass.name)</span><br><span class="line"> codeToInterfaceMethod.put(code, ipcMethod)</span><br><span class="line"> methodToIPCMethodMap.put(method, ipcMethod)</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><strong>客户端:通过动态代理替换IRemoteService.Stub.Proxy的功能</strong></p><p>IRemoteService.Stub.Proxy 实现</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Proxy</span> <span class="keyword">implements</span> <span class="title class_">com</span>.ly.studydemo.binder.IRemoteService {</span><br><span class="line"> <span class="keyword">private</span> android.os.IBinder mRemote;</span><br><span class="line"> ...</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getPid</span><span class="params">()</span> <span class="keyword">throws</span> android.os.RemoteException {</span><br><span class="line"> android.os.<span class="type">Parcel</span> <span class="variable">_data</span> <span class="operator">=</span> android.os.Parcel.obtain();</span><br><span class="line"> android.os.<span class="type">Parcel</span> <span class="variable">_reply</span> <span class="operator">=</span> android.os.Parcel.obtain();</span><br><span class="line"> <span class="type">int</span> _result;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> _data.writeInterfaceToken(DESCRIPTOR);</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">_status</span> <span class="operator">=</span> mRemote.transact(Stub.TRANSACTION_getPid, _data, _reply, <span class="number">0</span>);</span><br><span class="line"> <span class="keyword">if</span> (!_status && getDefaultImpl() != <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> getDefaultImpl().getPid();</span><br><span class="line"> }</span><br><span class="line"> _reply.readException();</span><br><span class="line"> _result = _reply.readInt();</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> _reply.recycle();</span><br><span class="line"> _data.recycle();</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> _result;</span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>IPCBus的动态代理实现</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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">fun</span> <span class="type"><T></span> <span class="title">get</span><span class="params">(interfaceClass: <span class="type">Class</span><*>)</span></span>: T? {</span><br><span class="line"> <span class="keyword">val</span> serverInterface = ServerInterface(interfaceClass)</span><br><span class="line"> <span class="comment">// 通过AIDL:IServiceFetcher获取服务器注册的Binder对象</span></span><br><span class="line"> <span class="keyword">val</span> binder = getService(interfaceClass.name) ?: <span class="keyword">return</span> <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Proxy.newProxyInstance(</span><br><span class="line"> interfaceClass.classLoader,</span><br><span class="line"> arrayOf(interfaceClass),</span><br><span class="line"> IPCInvocationBridge(serverInterface, binder)</span><br><span class="line"> ) <span class="keyword">as</span> T</span><br><span class="line">}</span><br><span class="line"><span class="keyword">class</span> <span class="title class_">IPCInvocationBridge</span>(<span class="keyword">val</span> serverInterface: ServerInterface, <span class="keyword">val</span> binder: IBinder) : InvocationHandler {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">invoke</span><span class="params">(proxy: <span class="type">Any</span>?, method: <span class="type">Method</span>?, args: <span class="type">Array</span><<span class="type">Any</span>>?)</span></span>: Any? {</span><br><span class="line"> <span class="keyword">val</span> ipcMethod = serverInterface.getIPCMethod(method)</span><br><span class="line"> ?: <span class="keyword">throw</span> IllegalStateException(<span class="string">"Can not found the ipc method : "</span> + method?.declaringClass?.name + <span class="string">"@"</span> + method?.name)</span><br><span class="line"> <span class="keyword">return</span> ipcMethod.callRemote(binder, args)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// IPCMethod.callRemote</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">callRemote</span><span class="params">(server: <span class="type">IBinder</span>, args: <span class="type">Array</span><<span class="type">Any</span>>?)</span></span>: Any? {</span><br><span class="line"> <span class="keyword">val</span> <span class="keyword">data</span> = Parcel.obtain()</span><br><span class="line"> <span class="keyword">val</span> reply = Parcel.obtain()</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">data</span>.writeInterfaceToken(interfaceName)</span><br><span class="line"> <span class="keyword">data</span>.writeArray(args)</span><br><span class="line"> server.transact(code, <span class="keyword">data</span>, reply, <span class="number">0</span>)</span><br><span class="line"> reply.readException()</span><br><span class="line"> <span class="keyword">val</span> result = reply.readValue(<span class="keyword">this</span>.javaClass.classLoader)</span><br><span class="line"> <span class="keyword">return</span> result</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="keyword">data</span>.recycle()</span><br><span class="line"> reply.recycle()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>服务端:通过Binder实现类TransformBinder替换IRemoteService.Stub的功能</strong></p><p>IRemoteService.Stub实现</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">Stub</span> <span class="keyword">extends</span> <span class="title class_">android</span>.os.Binder <span class="keyword">implements</span> <span class="title class_">com</span>.ly.studydemo.binder.IRemoteService {</span><br><span class="line"> ...</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">onTransact</span><span class="params">(<span class="type">int</span> code, android.os.Parcel data, android.os.Parcel reply, <span class="type">int</span> flags)</span> <span class="keyword">throws</span> android.os.RemoteException {</span><br><span class="line"> java.lang.<span class="type">String</span> <span class="variable">descriptor</span> <span class="operator">=</span> DESCRIPTOR;</span><br><span class="line"> <span class="keyword">switch</span> (code) {</span><br><span class="line"> <span class="keyword">case</span> INTERFACE_TRANSACTION: {</span><br><span class="line"> reply.writeString(descriptor);</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">case</span> TRANSACTION_getPid: {</span><br><span class="line"> data.enforceInterface(descriptor);</span><br><span class="line"> <span class="type">int</span> <span class="variable">_result</span> <span class="operator">=</span> <span class="built_in">this</span>.getPid();</span><br><span class="line"> reply.writeNoException();</span><br><span class="line"> reply.writeInt(_result);</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">case</span> TRANSACTION_getMyData: {</span><br><span class="line"> data.enforceInterface(descriptor);</span><br><span class="line"> com.ly.studydemo.binder.<span class="type">MyData</span> <span class="variable">_result</span> <span class="operator">=</span> <span class="built_in">this</span>.getMyData();</span><br><span class="line"> reply.writeNoException();</span><br><span class="line"> <span class="keyword">if</span> ((_result != <span class="literal">null</span>)) {</span><br><span class="line"> reply.writeInt(<span class="number">1</span>);</span><br><span class="line"> _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> reply.writeInt(<span class="number">0</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><span class="line"> <span class="keyword">default</span>: {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">super</span>.onTransact(code, data, reply, flags);</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>IPCBus里的TransformBinder实现</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><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">class</span> <span class="title class_">TransformBinder</span>(<span class="keyword">val</span> serverInterface: ServerInterface, <span class="keyword">val</span> server: Any) : Binder() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onTransact</span><span class="params">(code: <span class="type">Int</span>, <span class="keyword">data</span>: <span class="type">Parcel</span>, reply: <span class="type">Parcel</span>?, flags: <span class="type">Int</span>)</span></span>: <span class="built_in">Boolean</span> {</span><br><span class="line"> <span class="keyword">if</span>(code == Binder.INTERFACE_TRANSACTION) {</span><br><span class="line"> reply?.writeString(serverInterface.getInterfaceName())</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">val</span> ipcMethod = serverInterface.getIPCMethod(code)</span><br><span class="line"> ?: <span class="keyword">return</span> <span class="keyword">super</span>.onTransact(code, <span class="keyword">data</span>, reply, flags)</span><br><span class="line"> ipcMethod.handleTransact(server, <span class="keyword">data</span>, reply)</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><span class="line">}</span><br><span class="line"><span class="comment">// IPCMethod.handleTransact()</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">handleTransact</span><span class="params">(server: <span class="type">Any</span>, <span class="keyword">data</span>: <span class="type">Parcel</span>, reply: <span class="type">Parcel</span>?)</span></span>{</span><br><span class="line"> <span class="keyword">data</span>.enforceInterface(interfaceName)</span><br><span class="line"> <span class="keyword">val</span> parameters = <span class="keyword">data</span>.readArray(<span class="keyword">this</span>.javaClass.classLoader)</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> res: Any?</span><br><span class="line"> <span class="keyword">if</span> (parameters == <span class="literal">null</span>) {</span><br><span class="line"> res = method.invoke(server)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> res = method.invoke(server, parameters)</span><br><span class="line"> }</span><br><span class="line"> reply?.writeNoException()</span><br><span class="line"> reply?.writeValue(res)</span><br><span class="line"> }<span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> reply?.writeException(e)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Binder的单例管理"><a href="#Binder的单例管理" class="headerlink" title="Binder的单例管理"></a>Binder的单例管理</h2><p>因为跨进程通信最终还是通过IBinder实现,每个接口对应的IBinder对象应该复用,全局单例。<br><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/20200826040156423.png"></p><h2 id="AIDL搭桥传输IBinder对象"><a href="#AIDL搭桥传输IBinder对象" class="headerlink" title="AIDL搭桥传输IBinder对象"></a>AIDL搭桥传输IBinder对象</h2><p>首先通过AIDL创建跨进通信,用于传输动态IPC的Binder对象。</p><p><img src="/../../hexo-img/%E5%8F%8C%E5%BC%80%E7%B3%BB%E5%88%97%EF%BC%9AAIDL%E5%8E%9F%E7%90%86%E7%AE%80%E4%BB%8B%E5%8F%8A%E5%8A%A8%E6%80%81%E6%89%A9%E5%B1%95%E5%AE%9E%E7%8E%B0/20200826041119691.png"></p><ol><li>通过ContentProvider传递AIDL(IServiceFetcher)对象</li><li>通过AIDL(IServiceFetcher)传递动态的Binder对象</li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>IPCBus的关键是把编译时生成的code,改为动态生成,其他机制与自动生成的IRemoteService.Stub里的机制一样。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://juejin.im/post/6844904113046568973">Binder内存拷贝的本质和变迁</a></li><li><a href="https://github.com/asLody/VirtualApp">VirtualApp</a></li></ol>]]></content>
<summary type="html"><h1 id="AIDL的用法"><a href="#AIDL的用法" class="headerlink" title="AIDL的用法"></a>AIDL的用法</h1><p>定义接口,IRemoteService.aidl</p>
<figure class="highli</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>ARCore实现iOS的AR效果</title>
<link href="https://handsomeliuyang.github.io/2020/07/21/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-ARCore%E5%AE%9E%E7%8E%B0iOS%E7%9A%84AR%E6%95%88%E6%9E%9C/"/>
<id>https://handsomeliuyang.github.io/2020/07/21/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-ARCore%E5%AE%9E%E7%8E%B0iOS%E7%9A%84AR%E6%95%88%E6%9E%9C/</id>
<published>2020-07-21T07:35:45.000Z</published>
<updated>2023-10-08T02:53:20.128Z</updated>
<content type="html"><![CDATA[<h1 id="iOS的AR效果"><a href="#iOS的AR效果" class="headerlink" title="iOS的AR效果"></a>iOS的AR效果</h1><img src="https://wos.58cdn.com.cn/IjGfEdCbIlr/ishare/pic_18767146674867936.gif" height="400" style="max-width: 100%; cursor: default;"><p>此AR效果可以分为两个过程:</p><ol><li>旋转显示“找工作卡片”</li><li>点击“找工作卡片”显示“帮帮3D动画效果”</li></ol><h1 id="ARCore实现的效果"><a href="#ARCore实现的效果" class="headerlink" title="ARCore实现的效果"></a>ARCore实现的效果</h1><p>由于ARCore发展比ARKit慢很多,加上终端上的差异,提供的能力比较弱,最新Sceneform-1.16.0版本,才支持glTF的animation。</p><p>iOS的这个效果是三年前实现的,由于之前的Sceneform不支持animation,一直无法完成ARCore的改写,只能借助第三方的SDK,如ViroCore。</p><p>直到最近Sceneform版本支持animation后,才完成改写。dae的3D模型转换失败,改为其他3D模型。</p><p><video src="https://wos.58cdn.com.cn/IjGfEdCbIlr/ishare/video_19920195115761207.mp4" controls="true" width="100%" height="400"></video></p><h1 id="ARCore简单理解"><a href="#ARCore简单理解" class="headerlink" title="ARCore简单理解"></a>ARCore简单理解</h1><blockquote><p>ARCore的主要三个功能</p><ol><li>运动跟踪:让手机可以理解和跟踪它相对于现实世界的位置。</li><li>环境理解:让手机可以检测各类表面(例如地面、咖啡桌或墙壁等水平、垂直和倾斜表面)的大小和位置。</li><li>光估测:让手机可以估测环境当前的光照条件。</li></ol></blockquote><p>AR效果的实现过程:</p><ol><li>通过识别摄像头每帧图像中的<strong>特征点</strong>,计算<strong>特征点</strong>的移动</li><li>将这些<strong>特征点</strong>的移动与手机惯性传感器的读数组合,估算出摄像头的正确位置和方向。</li><li>依据摄像头最新的位置和方向,调整观察坐标系,重新渲染3D模型,使3D模型看起来就像现实世界的一部分。</li></ol><h1 id="ARCore关键术语"><a href="#ARCore关键术语" class="headerlink" title="ARCore关键术语"></a>ARCore关键术语</h1><ol><li>feature points: 特征点</li><li>planes: 平面,一组特征点(clusters of feature points)</li><li>anchor: 锚点,世界坐标系中的一个固定点,随着手机移动,在观察坐标系里,坐标会不断进行变化。</li><li>trackable: 可追踪对象,平面和特征点可被称为trackable,即随着手机移动,可定位其在真实世界的位置</li><li>hit test: 基于屏幕的二维坐标,映射到世界坐标,返回一个可追踪对象(trackable),即平面或特征点</li><li>TrackingState: Camera当前的运动跟踪状态,只有当state为TRACKING时,当前的位置信息才可以使用</li></ol><h1 id="Sceneform"><a href="#Sceneform" class="headerlink" title="Sceneform"></a>Sceneform</h1><blockquote><p>Sceneform是一个3D框架,封装OpenGL,简化3D模型的加载,渲染和交互</p></blockquote><h2 id="导入"><a href="#导入" class="headerlink" title="导入"></a>导入</h2><p>3D资源的格式有:obj、fbx、gltf等等多种格式</p><ol><li>obj:通用格式,大部分的3D工具都支持此格式,主要用于传输</li><li>glTF:最小的3D格式,去掉所有的冗余数据,类似于图片的JPEG格式,很适合移动端及web端。</li></ol><p>为了执行效率,Sceneform会将3D资源进行格式转换为sfa,sfb格式,再进行加载。</p><ol><li>转换插件:Google Sceneform Tools(Android Studio插件)</li><li>sfa文件:json文件,可阅读的描述文件,Task(createAsset-{asset-name})会依据最新的3D资源对此文件进行覆写。</li><li>sfb文件:Sceneform的3D资源的二进制数据,Task(compileAsset-{asset-name})将sfa编译到sfb中。</li></ol><h2 id="加载"><a href="#加载" class="headerlink" title="加载"></a>加载</h2><p>Renderable是Sceneform加载3D资源(*.sfb)后的对象,由顶点,资源,纹理组成。</p><p>支持的加载来源有:xml布局文件;3D资源(*.sfb);运行时创建简单的几何图形;加载动态3D资源</p><h3 id="xml布局文件"><a href="#xml布局文件" class="headerlink" title="xml布局文件"></a>xml布局文件</h3><p>通过ViewRenderable可加载xml布局文件,如下所示:</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></pre></td><td class="code"><pre><span class="line">ViewRenderable.builder()</span><br><span class="line"> .setView(context, R.layout.layout_ad)</span><br><span class="line"> .build()</span><br><span class="line"> .thenAccept(viewRenderable -> {</span><br><span class="line"> adRenderable = viewRenderable;</span><br><span class="line"> })</span><br><span class="line"> .exceptionally(</span><br><span class="line"> throwable -> {</span><br><span class="line"> <span class="type">Toast</span> <span class="variable">toast</span> <span class="operator">=</span> Toast.makeText(context, <span class="string">"Unable to load adRenderable"</span>, Toast.LENGTH_LONG);</span><br><span class="line"> toast.setGravity(Gravity.CENTER, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"> toast.show();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>注意:</p><ol><li>build() 函数在子线程加载</li><li>成功回调(thenAccept)和异常回调(exceptionally),都是在主线程执行</li></ol><p>效果如下所示:<br><img src="/../../hexo-img/ARCore%E5%AE%9E%E7%8E%B0iOS%E7%9A%84AR%E6%95%88%E6%9E%9C/20200722030845713.png"></p><h3 id="3D资源(-sfb)"><a href="#3D资源(-sfb)" class="headerlink" title="3D资源(*.sfb)"></a>3D资源(*.sfb)</h3><p>通过ModelRenderable加载内置的(*.sfb)文件</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></pre></td><td class="code"><pre><span class="line">ModelRenderable.builder()</span><br><span class="line"> .setSource(context, Uri.parse(<span class="string">"andy_dance.sfb"</span>))</span><br><span class="line"> .build()</span><br><span class="line"> .thenAccept(modelRenderable -> mBangbangRenderable = modelRenderable)</span><br><span class="line"> .exceptionally(throwable -> {</span><br><span class="line"> <span class="type">Toast</span> <span class="variable">toast</span> <span class="operator">=</span> Toast.makeText(context, <span class="string">"Unable to load bangbangRenderable"</span>, Toast.LENGTH_LONG);</span><br><span class="line"> toast.setGravity(Gravity.CENTER, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"> toast.show();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>注意:</p><ol><li>build() 函数在子线程加载</li><li>成功回调(thenAccept)和异常回调(exceptionally),都是在主线程执行</li></ol><p>效果如下所示:<br><img src="/../../hexo-img/ARCore%E5%AE%9E%E7%8E%B0iOS%E7%9A%84AR%E6%95%88%E6%9E%9C/20200722030916442.png"></p><h3 id="加载动态3D资源"><a href="#加载动态3D资源" class="headerlink" title="加载动态3D资源"></a>加载动态3D资源</h3><p>运行时加载3D资源,暂不支持直接加载*.sfb文件,只支持glTF格式的3D资源的加载。也是通过ModelRenderable加载。</p><h1 id="实现过程"><a href="#实现过程" class="headerlink" title="实现过程"></a>实现过程</h1><p>有了上面的知识的储备,实现成本其实并不高。</p><h2 id="AR环境搭建"><a href="#AR环境搭建" class="headerlink" title="AR环境搭建"></a>AR环境搭建</h2><p>ARCore和Sceneform的依赖:app/build.gradle</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// Provides ARCore Session and related resources.</span></span><br><span class="line"> implementation <span class="string">'com.google.ar:core:1.15.0'</span></span><br><span class="line"> implementation <span class="string">"com.google.ar.sceneform.ux:sceneform-ux:1.15.0"</span></span><br><span class="line"> <span class="comment">// Alternatively, use ArSceneView without the UX dependency.</span></span><br><span class="line"> implementation <span class="string">'com.google.ar.sceneform:core:1.15.0'</span></span><br><span class="line"> implementation <span class="string">"com.google.ar.sceneform:animation:1.15.0"</span></span><br><span class="line">}</span><br><span class="line">apply plugin: <span class="string">'com.google.ar.sceneform.plugin'</span></span><br></pre></td></tr></table></figure><p>插件依赖:build.gralde</p><figure class="highlight gradle"><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">buildscript</span> {</span><br><span class="line"> <span class="keyword">dependencies</span> {</span><br><span class="line"> <span class="keyword">classpath</span> <span class="string">'com.android.tools.build:gradle:3.4.1'</span></span><br><span class="line"> <span class="keyword">classpath</span> <span class="string">'com.google.ar.sceneform:plugin:1.15.0'</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过com.google.ar.sceneform.ux.ArFragment快速构建AR环境</p><figure class="highlight xml"><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="meta"><?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"utf-8"</span>?></span></span><br><span class="line"><span class="tag"><<span class="name">FrameLayout</span> <span class="attr">xmlns:android</span>=<span class="string">"http://schemas.android.com/apk/res/android"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">fragment</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"match_parent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:name</span>=<span class="string">"com.google.ar.sceneform.ux.ArFragment"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/ux_fragment"</span>/></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">FrameLayout</span>></span></span><br></pre></td></tr></table></figure><p>ARFragment的初始化及配置</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.wuba.sceneform;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MainActivity</span> <span class="keyword">extends</span> <span class="title class_">AppCompatActivity</span> {</span><br><span class="line"> <span class="keyword">private</span> ArFragment arFragment;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">onCreate</span><span class="params">(<span class="meta">@Nullable</span> Bundle savedInstanceState)</span> {</span><br><span class="line"> <span class="built_in">super</span>.onCreate(savedInstanceState);</span><br><span class="line"> <span class="comment">// 做AR支持检测</span></span><br><span class="line"> <span class="keyword">if</span> (!ARCoreUtils.checkIsSupportedDeviceOrFinish(<span class="built_in">this</span>)) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> setContentView(R.layout.layout_main);</span><br><span class="line"> arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);</span><br><span class="line"> <span class="comment">// 移除引导动画</span></span><br><span class="line"> arFragment.getPlaneDiscoveryController().hide();</span><br><span class="line"> arFragment.getPlaneDiscoveryController().setInstructionView(<span class="literal">null</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="加载3D资源"><a href="#加载3D资源" class="headerlink" title="加载3D资源"></a>加载3D资源</h2><p>“找工作”对象:从xml里加载</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></pre></td><td class="code"><pre><span class="line">ViewRenderable.builder()</span><br><span class="line"> .setView(context, R.layout.layout_ad)</span><br><span class="line"> .build()</span><br><span class="line"> .thenAccept(viewRenderable -> {</span><br><span class="line"> adRenderable = viewRenderable;</span><br><span class="line"> })</span><br><span class="line"> .exceptionally(</span><br><span class="line"> throwable -> {</span><br><span class="line"> <span class="type">Toast</span> <span class="variable">toast</span> <span class="operator">=</span> Toast.makeText(context, <span class="string">"Unable to load adRenderable"</span>, Toast.LENGTH_LONG);</span><br><span class="line"> toast.setGravity(Gravity.CENTER, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"> toast.show();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><p>“帮帮3D动画效果”对象:3D模型加载,利用Google Sceneform Tools插件,导入andy_dance.fbx资源,并通过ModelRenderable进行加载:</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></pre></td><td class="code"><pre><span class="line">ModelRenderable.builder()</span><br><span class="line"> .setSource(context, Uri.parse(<span class="string">"andy_dance.sfb"</span>))</span><br><span class="line"> .build()</span><br><span class="line"> .thenAccept(modelRenderable -> mBangbangRenderable = modelRenderable)</span><br><span class="line"> .exceptionally(throwable -> {</span><br><span class="line"> <span class="type">Toast</span> <span class="variable">toast</span> <span class="operator">=</span> Toast.makeText(context, <span class="string">"Unable to load bangbangRenderable"</span>, Toast.LENGTH_LONG);</span><br><span class="line"> toast.setGravity(Gravity.CENTER, <span class="number">0</span>, <span class="number">0</span>);</span><br><span class="line"> toast.show();</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> });</span><br></pre></td></tr></table></figure><h2 id="移动找特征点,并显示“找工作”"><a href="#移动找特征点,并显示“找工作”" class="headerlink" title="移动找特征点,并显示“找工作”"></a>移动找特征点,并显示“找工作”</h2><p>要在真实世界上放置渲染对象,看起来像现实世界的一部分,就需要基于可追踪对象(trackable),放置渲染对象。</p><p>步骤如下:</p><ol><li>监听摄像头的每帧图像处理</li><li>判断Camera.TrackingState是否是TRACKING</li><li>对当前图像帧做frame.hitTest()测试,获取可追踪对象(trackable)</li><li>基于可追踪对象(trackable)的Pose,创建Anchor,通过Anchor添加3D渲染对象。</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 监听</span></span><br><span class="line">arFragment.getArSceneView().getScene().addOnUpdateListener(<span class="keyword">new</span> <span class="title class_">Scene</span>.OnUpdateListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onUpdate</span><span class="params">(FrameTime frameTime)</span> {</span><br><span class="line"> arFragment.onUpdate(frameTime);</span><br><span class="line"> onSceneUpdate();</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">onSceneUpdate</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">View</span> <span class="variable">contentView</span> <span class="operator">=</span> findViewById(android.R.id.content);</span><br><span class="line"></span><br><span class="line"> <span class="type">boolean</span> <span class="variable">trackingChanged</span> <span class="operator">=</span> updateTracking();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(trackingChanged){</span><br><span class="line"> <span class="keyword">if</span>(isTracking){</span><br><span class="line"> contentView.getOverlay().add(pointer);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> contentView.getOverlay().remove(pointer);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> contentView.invalidate();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(isTracking){</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">hitTestChanged</span> <span class="operator">=</span> updateHitTest();</span><br><span class="line"> <span class="keyword">if</span>(hitTestChanged) {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span>(isHitting) {</span><br><span class="line"> adManager.showAd(hitResult, arFragment);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> Log.d(TAG, <span class="string">"hitTestChanged .... "</span> + isHitting);</span><br><span class="line"></span><br><span class="line"> pointer.setEnabled(isHitting);</span><br><span class="line"> contentView.invalidate();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>问题:很多trackable相距很近,“找工作”显示太密集??</p><p><strong>解决方案:</strong>设置两个“找工作”对象之间的最小距离,计算其三维世界的距离。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">isClose</span><span class="params">(Pose adPos, Pose hitPose)</span> {</span><br><span class="line"> <span class="comment">// Compute the difference vector between the two hit locations.</span></span><br><span class="line"> <span class="type">float</span> <span class="variable">dx</span> <span class="operator">=</span> adPos.tx() - hitPose.tx();</span><br><span class="line"> <span class="type">float</span> <span class="variable">dy</span> <span class="operator">=</span> adPos.ty() - hitPose.ty();</span><br><span class="line"> <span class="type">float</span> <span class="variable">dz</span> <span class="operator">=</span> adPos.tz() - hitPose.tz();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Compute the straight-line distance.</span></span><br><span class="line"> <span class="type">float</span> <span class="variable">distanceMeters</span> <span class="operator">=</span> (<span class="type">float</span>) Math.sqrt(dx*dx + dy*dy + dz*dz);</span><br><span class="line"></span><br><span class="line"> Log.d(<span class="string">"MainActivity"</span>, <span class="string">"distanceMeters = "</span> + distanceMeters);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// too close</span></span><br><span class="line"> <span class="keyword">if</span>(distanceMeters < <span class="number">1.7</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><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></pre></td></tr></table></figure><h2 id="“找工作”点击显示机器人对象"><a href="#“找工作”点击显示机器人对象" class="headerlink" title="“找工作”点击显示机器人对象"></a>“找工作”点击显示机器人对象</h2><p>点击“找工作”对象后,修改当前Node的Renderable数据:</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></pre></td><td class="code"><pre><span class="line">transformableNode.setOnTapListener(<span class="keyword">new</span> <span class="title class_">Node</span>.OnTapListener() {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onTap</span><span class="params">(HitTestResult hitTestResult, MotionEvent motionEvent)</span> {</span><br><span class="line"> showBangBang(transformableNode);</span><br><span class="line"> }</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">showBangBang</span><span class="params">(TransformableNode transformableNode)</span>{</span><br><span class="line"> <span class="keyword">if</span> (mBangbangRenderable == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> transformableNode.setRenderable(mBangbangRenderable);</span><br><span class="line"> transformableNode.select();</span><br><span class="line"> startAnimation(transformableNode, mBangbangRenderable);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h1><p>还有很多的问题没有解决,只是撑握了Api的使用,无法基于OpenGL做深度定制:</p><ol><li>四大厂商的手机如何配置ARCore环境?</li><li>dae转换为obj或gltf?</li><li>3D模型的二次编辑修改?</li><li>OpenGL深入研究,实现基于OpenGL版本?</li></ol><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://developers.google.com/ar/discover">ARCore overview</a></li><li><a href="https://developers.google.com/sceneform/develop/emulator">Run Sceneform apps in Android Emulator</a></li><li><a href="https://codelabs.developers.google.com/codelabs/sceneform-intro/index.html#0">Introduction to Sceneform</a></li></ol>]]></content>
<summary type="html"><h1 id="iOS的AR效果"><a href="#iOS的AR效果" class="headerlink" title="iOS的AR效果"></a>iOS的AR效果</h1><img src="https://wos.58cdn.com.cn/IjGfEdCbIlr/is</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>云服务搭建Anki Sync Server</title>
<link href="https://handsomeliuyang.github.io/2020/01/08/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E4%BA%91%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BAAnki-Sync-Server/"/>
<id>https://handsomeliuyang.github.io/2020/01/08/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-%E4%BA%91%E6%9C%8D%E5%8A%A1%E6%90%AD%E5%BB%BAAnki-Sync-Server/</id>
<published>2020-01-08T11:53:36.000Z</published>
<updated>2023-10-08T02:53:20.131Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><blockquote><p>知识的学习过程:懂,熟,巧</p></blockquote><ol><li>懂:通过看视频,看文章,写分享很容易达到懂的状态</li><li>熟:重复,重复,再重复</li><li>巧:实践,实践,再实践</li></ol><p>除了经常用到的知识外,很多知识点,我们都处于懂的阶段,之前的学过的知识,之前写过的分享,只要长时间不使用,就记不清了,导致相同的知识总是处于学习,忘记,再学习的过程。</p><blockquote><p>英国哲学家培根说:“一切知识的获得都是记忆”</p></blockquote><p>要熟练,就要记忆,而要记忆就要重复,如何高效率的重复呢?<a href="http://www.ankichina.net/anki20.html">Anki</a>就是一个使记忆变得容易的学习软件</p><p>其具有如下特点:</p><ol><li>科学安排复习间隔:艾宾浩斯遗忘曲线</li><li>通过主动召回测试,提升学习效率:卡片(问题|答案)</li><li>支持图像、音频、视频和LaTeX</li><li>跨端且开源,支持windows,mac,linux,android,iphone。注意:iphone未开源且收费,其他都免费且开源</li></ol><p>Anki由于是小众软件,没有商业化,所以其同步过程在国内很慢,而且经常还失败,同时有些卡片内容不想同步到server,就有想自己搭建Anki Sync Server的想法。</p><p>由于AnkiWeb没有开源,网上有个牛人把自己实现了一套Anki Sync Server。</p><ol><li>github地址为:<a href="https://github.com/tsudoko/anki-sync-server">tsudoko/anki-sync-server</a></li><li>docker安装:<a href="https://github.com/kuklinistvan/docker-anki-sync-server">kuklinistvan/docker-anki-sync-server</a></li></ol><h1 id="Anki-Sync-Server安装过程"><a href="#Anki-Sync-Server安装过程" class="headerlink" title="Anki Sync Server安装过程"></a>Anki Sync Server安装过程</h1><ol><li>安装docker:curl -sSL <a href="https://get.daocloud.io/docker">https://get.daocloud.io/docker</a> | sh</li><li>启动docker服务:service docker start</li><li>创建两个目录: <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mkdir anki-data // 用于存放Anki的数据</span><br><span class="line">mkdir anki-docker // 用于存放anki docker的shell脚本</span><br></pre></td></tr></table></figure></li><li>参考<a href="https://github.com/kuklinistvan/docker-anki-sync-server">kuklinistvan/docker-anki-sync-server</a>创建run.sh脚本: <figure class="highlight plaintext"><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">cd anki-docker</span><br><span class="line">vim run.sh</span><br><span class="line">---------</span><br><span class="line">// run.sh的内容如下:</span><br><span class="line">export DOCKER_USER=root</span><br><span class="line">export ANKI_SYNC_DATA_DIR=/root/liuyang/anki_sync_server/anki-data</span><br><span class="line">export HOST_PORT=27701</span><br><span class="line"></span><br><span class="line">mkdir -p "$ANKI_SYNC_DATA_DIR"</span><br><span class="line">chown "$DOCKER_USER" "$ANKI_SYNC_DATA_DIR"</span><br><span class="line">chmod 700 "$ANKI_SYNC_DATA_DIR"</span><br><span class="line"></span><br><span class="line">docker run -itd \</span><br><span class="line"> --mount type=bind,source="$ANKI_SYNC_DATA_DIR",target=/app/data \</span><br><span class="line"> -p "$HOST_PORT":27701 \</span><br><span class="line"> --name anki-container \</span><br><span class="line"> --restart always \</span><br><span class="line"> kuklinistvan/anki-sync-server:tsudoku-2.1.9</span><br><span class="line">---------</span><br></pre></td></tr></table></figure></li><li>添加用户: <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">进入docker容器</span></span><br><span class="line">docker exec -it anki-container /bin/sh</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">查看添加用户的命令帮助</span></span><br><span class="line">/app/anki-sync-server # ./ankisyncctl.py --help</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">添加新用户及设置其密码</span></span><br><span class="line">/app/anki-sync-server # ./ankisyncctl.py adduser <username></span><br><span class="line">Enter password for <username>: </span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">列表所有的用户</span></span><br><span class="line">/app/anki-sync-server # ./ankisyncctl.py lsuser</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">修改用户的密码</span></span><br><span class="line">/app/anki-sync-server # ./ankisyncctl.py passwd <username></span><br><span class="line">Enter password for <username>:</span><br></pre></td></tr></table></figure></li></ol><h1 id="Android端配置"><a href="#Android端配置" class="headerlink" title="Android端配置"></a>Android端配置</h1><ol><li>对应的Android Apk的下载地址:链接:<a href="https://pan.baidu.com/s/1CemVYTOOZe0odjkYuKgKMg">https://pan.baidu.com/s/1CemVYTOOZe0odjkYuKgKMg</a> 密码:pesp</li><li>进入【设置】–>【高级设置】–>【自定义同步服务器】如下配置<ol><li>同步地址:http://云服务器的外网ip:27701/</li><li>媒体文件同步地址:http://云服务器的外网ip:27701/msync</li></ol></li><li>进入【设置】–>【AnkiDroid】–>【登录】:输入上面创建的账号与密码</li></ol><h1 id="桌面PC端Anki配置"><a href="#桌面PC端Anki配置" class="headerlink" title="桌面PC端Anki配置"></a>桌面PC端Anki配置</h1><ol><li>对应软件下载地址:<ol><li>mac:链接:<a href="https://pan.baidu.com/s/1xmdO5-IjlPOQnJ-vGLI_tA">https://pan.baidu.com/s/1xmdO5-IjlPOQnJ-vGLI_tA</a> 密码:2689</li><li>window:链接:<a href="https://pan.baidu.com/s/1f4KTJsm-MBNJkxUGAD4dqA">https://pan.baidu.com/s/1f4KTJsm-MBNJkxUGAD4dqA</a> 密码:oz3i</li></ol></li><li>启动软件,进入【工具】–> 【附加组件】–> 点击【获取插件】,输入代码:2124817646,点击【OK】,安装成功SyncRedirector插件</li><li>双击【SyncRedirector】,进行如下配置: <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> "msyncUrl": "http://云服务器的外网ip:27701/msync/",</span><br><span class="line"> "syncUrl": "http://云服务器的外网ip:27701/sync/"</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li>重启Anki</li><li>【文件】–>【切换配置方案】,添加刚才创建的用户名,点击同步,输入刚才创建的用户名和密码</li></ol><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://zhuanlan.zhihu.com/p/70269217">利用群晖Synology进行Anki同步</a></li><li><a href="https://github.com/kuklinistvan/docker-anki-sync-server">kuklinistvan/docker-anki-sync-server</a></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><blockquote>
<p>知识的学习过程:懂,熟,巧</p>
</blockquote>
<ol>
<li>懂:通过看视频,看文章,写分享很</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="docker" scheme="https://handsomeliuyang.github.io/tags/docker/"/>
</entry>
<entry>
<title>58App/Android端的动态化框架实践与思考</title>
<link href="https://handsomeliuyang.github.io/2019/11/22/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/"/>
<id>https://handsomeliuyang.github.io/2019/11/22/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93-58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/</id>
<published>2019-11-22T12:16:45.000Z</published>
<updated>2023-10-08T02:53:20.127Z</updated>
<content type="html"><![CDATA[<h1 id="业务与动态化要求"><a href="#业务与动态化要求" class="headerlink" title="业务与动态化要求"></a>业务与动态化要求</h1><p>58业务需求特点:多端的快速开发,多套跨平台框架,业务跨App迁移。为了满足此业务要求,整体技术框架的实现手段有:</p><ol><li>业务层:转译框架,如WubaRN-M,京东-taro-react标准,MPVue-vue规划,Wepy-类vue规范,滴滴-Mpx-小程序语法等等</li><li>跨平台框架层:Hybrid,ReactNative,小程序,Flutter等等</li><li>基础能力层:统一Plugin</li></ol><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126045232976.png"></p><h1 id="58App动态化实践"><a href="#58App动态化实践" class="headerlink" title="58App动态化实践"></a>58App动态化实践</h1><p>每种技术都有期优缺点,无法做到完全统一,在实际的App里,一般都是多套框架并存,不同的业务采用不同的技术方案,58App里主要有三种技术方案:</p><ol><li>WubaRN:基于ReactNative的二次封装,主要用于追求动态和较高体验要求的需求</li><li>Hybrid:JS-Native框架,主要用于运营活动等需求</li><li>阿里的Tangram框架:用于列表等Native页面的布局动态化</li></ol><p>由于Hybrid框架比较简单,重点分析一下WubaRN与Tangram框架</p><h2 id="WubaRN"><a href="#WubaRN" class="headerlink" title="WubaRN"></a>WubaRN</h2><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126045250000.png"></p><p>基于ReactNative主要的封装点:</p><ol><li>业务组件与模板工程<ol><li>抹平UI组件的平台差异,扩展Native能力</li><li>统一技术栈,提供模板工程</li></ol></li><li>包大小瘦身及解决Bug<ol><li>减少平台支持版本,保留armeabi-v7a;统一Okhttp与Fresco</li><li>通过AOP修改字符码,解决原生Bug</li></ol></li><li>实现热更新<ol><li>Bundle拆分:Common Bundle内置,Business Bundle动态下发</li><li>分步加载:优化加载Common Bundle,具体业务再加载Business Bundle</li></ol></li></ol><p>ReactNative受制于其实现原理,在低端手机里,如果出现白屏,卡顿现象。如下所实现框架图:</p><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126045303449.png"></p><p>主要特点:</p><ol><li>三个线程:<ol><li>UI Thread:Native的UI渲染</li><li>Shadow Thread:yoga引擎,基于flexbox的语法糖转换为各端的扁平化框架</li><li>Javascript Thread:React执行环境,业务逻辑与diff操作执行环境</li></ol></li><li>通信方式:Json格式序列化,通过React Native Bridge</li></ol><p>产出的问题:</p><ol><li>列表滑动白屏:快速滑动,通信量大,过渡依赖Bridge</li><li>转输大数据慢:如图像的base64字符串信息</li><li>无线同步通信:通信都是异步</li><li>Javascritp Thread帧率低:Javascript解释执行,同时需要执行业务逻辑与diff操作,在低端手机里,快速滑动时,掉帧严重</li></ol><p>ReactNative的最新版本也在重新整体底层实现:</p><ol><li>Fabric:<ol><li>将Native API直接暴露给JavaScript,不通过bridge</li><li>允许 UI 线程与JS线程同步</li></ol></li><li>Fiber:利用requestIdleCallback(),实现动画优先</li><li>使用RecyclerView替换FlatList,实现ItemView的复用</li></ol><h2 id="布局动态化—Tangram"><a href="#布局动态化—Tangram" class="headerlink" title="布局动态化—Tangram"></a>布局动态化—Tangram</h2><blockquote><p>Tangram,七巧板,几块简单的积木就能拼出大千世界。我们用Tangram来命名这套界面方案,也是希望他能像七巧板一样可以通过几块积木就搭出丰富多彩的界面</p></blockquote><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050338349.png"></p><p>其在性能与灵活性上取了一个折中解决方案:</p><ol><li>设计原则:牺牲灵活性的情况下,追求极致性能</li><li>切入点:<ol><li>Native列表高性的同时,缺少灵活性</li><li>动态框架的内存与滑动控制的性能瓶颈</li></ol></li><li>目标:通过构建页面结构化描述,实现页面可运营的目的</li></ol><p>其主要的应用场景:</p><ol><li>常规业务:如业务稳定的列表等<ol><li>需求较稳定,对性能与稳定性有很高的要求</li><li>对局部样式有动态化要求,如标签等等</li></ol></li><li>基础业务:如首页<ol><li>需求稳定,对性能与稳定性有很高的要求</li><li>对局部样式有动态化要求,如推荐样式</li></ol></li></ol><p>其高性能的原因:</p><ol><li>基于Native的列表实现的基础上,解决灵活性,如RecyclerView</li><li>页面渲染:大量的计算工作在VM中完成,并缓存在VM组成的树形结构里</li><li>回收和复用:基于组件与控件实现回收复用</li></ol><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050351207.png"></p><p>但淘宝开源Trangram时,没有开源所有的工具,缺少一些模块:</p><ol><li>模板管理后台:负责发布、更新(版本、平台、组件版本、生效优先级)</li><li>页面生成工具(类似索尔平台)</li></ol><h1 id="其他动态化框架分析"><a href="#其他动态化框架分析" class="headerlink" title="其他动态化框架分析"></a>其他动态化框架分析</h1><p>除58App正在使用的技术外,还有其他跨平台的技术:</p><ol><li>小程序:微信小程序,百度小程序等等</li><li>全包型:Flutter,Qt</li><li>转译框架:taro,MPVue,Wepy,Mpx等等</li></ol><p>对于每一种技术选择一个来讲解其实现原理</p><h2 id="小程序-百度小程序"><a href="#小程序-百度小程序" class="headerlink" title="小程序-百度小程序"></a>小程序-百度小程序</h2><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050404831.png"></p><p>通过对百度小程序的已开源的源码分析,其整体框架如上图所示:</p><ol><li>逻辑层<ol><li>小程序Api:App(),Page(),布局标签</li><li>App():创建App对象</li><li>Page():存储在Map中,页面显示时,创建Page对象</li></ol></li><li>渲染层<ol><li>MVVM框架San渲染</li><li>编译期间,小程序标签转化为San的标签</li><li>Page()对应San的Page组件,Template为Swan.xml转译的内容</li></ol></li><li>交互<ol><li>渲染层接收用户的交互事件,由统一的函数处理后,通过消息总线传递到逻辑层的Page对象,再调用对应的函数</li><li>逻辑层依据用户操作,执行业务操作,修改data数据,通过消息总线传递到渲染层的组件里,San.Page组件会自动更新界面</li></ol></li></ol><p>不管是逻辑层与渲染层,其内部实现都还是通过H5来实现,其提升性能的思路:</p><ol><li>把逻辑层与渲染层分离</li><li>异步请求都由native来执行</li><li>编译期转换标签</li></ol><p>受制当前的实现机制,有一些短板:</p><ol><li>无法内嵌Native的View</li><li>通信机制:异步且序列化传递数据<ol><li>传递大数据较慢</li><li>setState()过于频繁时,影响性能</li></ol></li></ol><h2 id="全包型—Flutter"><a href="#全包型—Flutter" class="headerlink" title="全包型—Flutter"></a>全包型—Flutter</h2><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050450876.png"></p><p>Flutter借鉴了ReactNative的设计思路,采用响应式编程,同时实现统一跨平台样式,高性能</p><ol><li>高性能:<ol><li>Debug为字节码,Release为机器码</li><li>不依赖OEM Widgets</li><li>不依赖Bridge</li></ol></li><li>开发效率:<ol><li>声明式布局,一切都是Widget</li><li>热加载(hot reload)</li><li>不依赖OEM,基于Skia,统一UI</li></ol></li></ol><p>真正体验后:</p><ol><li>感受:<ol><li>开发调试非常的快,比Instant Run强</li><li>依赖库管理强,Plugin库</li><li>MVVM框架,声明式布局,便于组件化</li><li>代码精简,相比Java</li></ol></li><li>不足:<ol><li>iOS不支持热更新(思路:Dart转Javascript)</li><li>生态不完善,缺少必须的基础能力<ol><li>渐变Button,图片Button</li><li>崩溃日志收集</li><li>基础Plugin:相册,授权,视频等等</li></ol></li><li>定制开发学习成本高(RenderObject, CostomPaintObject)</li></ol></li></ol><h2 id="转译框架—Taro"><a href="#转译框架—Taro" class="headerlink" title="转译框架—Taro"></a>转译框架—Taro</h2><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050507685.png"></p><p>Taro的设计目标:</p><ol><li>用React写小程序,实现“工业化”<ol><li>PostCSS,Sass, Less</li><li>NPM 支持</li><li>ES6/ES7 语法糖</li><li>TypeScript 的强类型约束</li><li>React 的组件开发</li><li>Redux 的状态管理</li></ol></li><li>多端,统一语法<ol><li>多端转换:运行时和编译时</li><li>抹平多端差异:基础Api与基础组件</li></ol></li></ol><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050524898.png"><br><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050533235.png"></p><p>Taro是类React语法:以小程序为标准,对React语法删减,有如下限制:</p><ol><li>JSX的限制:<ol><li>render()之外不支持jsx</li><li>map 中不支持if 表达式</li><li>只支持Array.map</li><li>props中不支持匿名函数</li><li>props中不支持对象展开符</li><li>props不支持 JSX 元素</li></ol></li><li>不支持无状态组件,都必须使用class定义</li></ol><p>转译框架带来的问题:</p><ol><li>问题定位难:多一层转译</li><li>基础API与基础组件的维护</li></ol><h1 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h1><p>现阶段没有真正能满足所有需求的框架,处于混合时期:依据不同的场景,采用不同的框架</p><ol><li>移动端:<ol><li>首页:布局动态化</li><li>列表,详情:Web技术型,如ReactNative</li><li>活动:Hybrid,小程序</li></ol></li><li>多端:编译框架,如Taro</li></ol><p><img src="/../../hexo-img/58App-Android%E7%AB%AF%E7%9A%84%E5%8A%A8%E6%80%81%E5%8C%96%E6%A1%86%E6%9E%B6%E5%AE%9E%E8%B7%B5%E4%B8%8E%E6%80%9D%E8%80%83/20191126050722611.png"></p><p>从长远看,真正有可能实现统一的框架:Flutter(全包型)</p><p>最关键的还是:持续关注,持续学习</p>]]></content>
<summary type="html"><h1 id="业务与动态化要求"><a href="#业务与动态化要求" class="headerlink" title="业务与动态化要求"></a>业务与动态化要求</h1><p>58业务需求特点:多端的快速开发,多套跨平台框架,业务跨App迁移。为了满足此业务要求,整体</summary>
<category term="经验总结" scheme="https://handsomeliuyang.github.io/categories/%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/"/>
<category term="Android" scheme="https://handsomeliuyang.github.io/tags/Android/"/>
</entry>
<entry>
<title>Flutter混合工程工程化编译改造系列:add to app源码分析</title>
<link href="https://handsomeliuyang.github.io/2019/10/30/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-Flutter%E7%9A%84add-to-app%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/"/>
<id>https://handsomeliuyang.github.io/2019/10/30/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0-Flutter%E7%9A%84add-to-app%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/</id>
<published>2019-10-30T06:19:59.000Z</published>
<updated>2023-10-08T02:53:20.123Z</updated>
<content type="html"><![CDATA[<h1 id="add-to-app"><a href="#add-to-app" class="headerlink" title="add to app"></a>add to app</h1><p>详细教程文档:<a href="https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps">Add Flutter to existing apps</a></p><p>主要步骤:</p><ol><li>创建FlutterModule:flutter create -t module xxx</li><li>在Host App的settings.gradle文件添加如下配置: <figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// MyApp/settings.gradle</span></span><br><span class="line"><span class="keyword">include</span> <span class="string">':app'</span> <span class="comment">// assumed existing content</span></span><br><span class="line">setBinding(<span class="keyword">new</span> Binding([gradle: <span class="keyword">this</span>])) <span class="comment">// new</span></span><br><span class="line">evaluate(<span class="keyword">new</span> <span class="keyword">File</span>( <span class="comment">// new</span></span><br><span class="line"> settingsDir.parentFile, <span class="comment">// new</span></span><br><span class="line"> <span class="string">'my_flutter/.android/include_flutter.groovy'</span> <span class="comment">// new</span></span><br><span class="line">)) </span><br></pre></td></tr></table></figure></li><li>依赖flutter module库: <figure class="highlight gradle"><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">dependencies</span> {</span><br><span class="line"> implementation <span class="keyword">project</span>(<span class="string">':flutter'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ol><h1 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h1><p>通过阅读Flutter官方文档,Flutter的源码分为两部分:</p><ol><li>Flutter Framework:<a href="https://github.com/flutter/flutter">源码地址</a></li><li>Flutter Engine: <a href="https://github.com/flutter/engine">源码地址</a></li></ol><p>Flutter Framework源码的环境配置与编译都比较容易,主要两步:</p><ol><li>clone代码后,把flutter/bin目录添加到环境变量里,执行任务flutter的命令,都会自动编译flutter tool</li><li>修改flutter tool源码后,重新编译flutter tool: <figure class="highlight plaintext"><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">cd flutter_sdk_path</span><br><span class="line">rm ./bin/cache/flutter_tools.stamp</span><br><span class="line">rm ./bin/cache/flutter_tools.snapshot</span><br></pre></td></tr></table></figure></li></ol><h2 id="flutter-create-t-module过程分析"><a href="#flutter-create-t-module过程分析" class="headerlink" title="flutter create -t module过程分析"></a>flutter create -t module过程分析</h2><p>flutter create的源码路径:flutter/packages/flutter_tools/lib/src/commands/create.dart</p><p><img src="/../../hexo-img/Flutter%E7%9A%84add-to-app%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/create%E6%B5%81%E7%A8%8B2.png"></p><p>flutter create 命令都是通过template来创建,但create app与create module的模板不一样,create module处于beta阶段:</p><ol><li>创建的Platform代码在.android文件里</li><li>执行flutter create, pub命令,会强制删除.android再重建</li></ol><h2 id="工程结构分析"><a href="#工程结构分析" class="headerlink" title="工程结构分析"></a>工程结构分析</h2><p>新增一个.android/Flutter Module的主要作用:</p><ol><li>提供一个Flutter类和FlutterFragment类<ol><li>初始化flutter: FlutterMain.startInitialization()</li><li>创建FlutterView:new FlutterView(activity, null, nativeView)</li></ol></li><li>注册Plugin插件</li><li>通过插flutter的gradle插件,编译整个Flutter工程,生成snapshot和libflutter.so文件<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"</span><br></pre></td></tr></table></figure></li></ol><h2 id="FlutterPlugin插件过程分析"><a href="#FlutterPlugin插件过程分析" class="headerlink" title="FlutterPlugin插件过程分析"></a>FlutterPlugin插件过程分析</h2><p>插件源码目录:flutter/packages/flutter_tools/gradle/flutter.gradle</p><p><img src="/../../hexo-img/Flutter%E7%9A%84add-to-app%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/flutterplugin.png"></p><p>从图中可了解到:</p><ol><li>对于flutter engine的依赖,是通过关联其aar实现的</li><li>debug编译时,Flutter的Dart工程,通过JIT编译,生成snapshot</li><li>release编译时,Flutter的Dart工程,通过AOT编译,生成libapp.so</li><li>release编译生成的libapp.so,通过打包成jar依赖</li><li>flutter的其他数据,如字体等等文件,复制到apk的assets目录下</li><li>Debug包的生成文件有:<ol><li>lib/xxx/libflutter.so</li><li>assets/flutter_assets</li></ol></li><li>Release包的生成文件有:<ol><li>lib/xxx/libflutter.so</li><li>lib/xxx/libapp.so</li><li>assets/flutter_assets</li></ol></li></ol><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>现有的add to app的一些问题:</p><ol><li>beta状态,只有master分支才支持,问题很多:<ol><li>.android文件夹执行flutter pub命令时,会被重新生成</li><li>同时master分支不稳定,无法生成可运行的release包</li></ol></li><li>开发期间编译很耗时,同时编译两个工程</li></ol><p>改造思路:</p><ol><li>基于稳定分支,分离Flutter Dart编译与Flutter Native编译过程</li><li>Flutter载体页支持usb或网络加载Flutter Dart的snapshot文件,并支持热更新</li></ol><p>敬请期待后续…</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://github.com/flutter/flutter/wiki/Setting-up-the-Framework-development-environment">Setting up the Framework development environment</a></li><li><a href="https://github.com/flutter/flutter/wiki/The-flutter-tool">The flutter tool</a></li><li><a href="https://github.com/flutter/flutter/wiki/Add-Flutter-to-existing-apps">Add Flutter to existing apps</a></li></ol>]]></content>
<summary type="html"><h1 id="add-to-app"><a href="#add-to-app" class="headerlink" title="add to app"></a>add to app</h1><p>详细教程文档:<a href="https://github.com/flu</summary>
<category term="日常学习" scheme="https://handsomeliuyang.github.io/categories/%E6%97%A5%E5%B8%B8%E5%AD%A6%E4%B9%A0/"/>
<category term="Flutter" scheme="https://handsomeliuyang.github.io/tags/Flutter/"/>
</entry>
</feed>