-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
520 lines (305 loc) · 304 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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>lefo's blog</title>
<icon>https://www.gravatar.com/avatar/4d1e20579d737b72764e8d457f1900cd</icon>
<subtitle>又一个android开发人员</subtitle>
<link href="https://www.lefo.me/atom.xml" rel="self"/>
<link href="https://www.lefo.me/"/>
<updated>2024-08-24T21:40:15.783Z</updated>
<id>https://www.lefo.me/</id>
<author>
<name>lefo</name>
<email>lephones#lephones.net(#换成@你懂得)</email>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>如何构建一个pdfium.so</title>
<link href="https://www.lefo.me/2024/05/12/build-pdfium/"/>
<id>https://www.lefo.me/2024/05/12/build-pdfium/</id>
<published>2024-05-12T06:50:01.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>项目中接入了打开pdf文件的功能,使用的是<a href="https://github.com/barteksc/PdfiumAndroid/">https://github.com/barteksc/PdfiumAndroid/</a>。现在想升级一下pdfium库,旧库具体原因就不解释了,毕竟这个项目最后一个commit在6年前,当然,平常用来接入打开pdf也是没问题的。</p><h1 id="开源库"><a href="#开源库" class="headerlink" title="开源库"></a>开源库</h1><p>在google一通搜索,发现pdfium库包含两套,一套在aosp中,另一套在chromium中。</p><ul><li><a href="https://android.googlesource.com/platform/external/pdfium/">https://android.googlesource.com/platform/external/pdfium/</a></li><li><a href="https://pdfium.googlesource.com/pdfium/">https://pdfium.googlesource.com/pdfium/</a><br>两款代码应该差不多,aosp的构建使用Android.bp,版本要比pdfium版本的落后一些。再看PdfiumAndroid项目README,第一句就说Uses pdfium library from AOSP,使用的库应该也是aosp的库。</li></ul><h1 id="构建aosp版"><a href="#构建aosp版" class="headerlink" title="构建aosp版"></a>构建aosp版</h1><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><ol><li>ubuntu 可以使用virtual box去装个虚拟机</li><li>200GB以上硬盘空间,清华会推荐你下载aosp-latest.tar再解压,光压缩包80G了,解压完我就把tar包删了</li></ol><h2 id="获取aosp源码"><a href="#获取aosp源码" class="headerlink" title="获取aosp源码"></a>获取aosp源码</h2><p>推荐使用清华镜像<a href="https://mirrors-i.tuna.tsinghua.edu.cn/help/AOSP/">https://mirrors-i.tuna.tsinghua.edu.cn/help/AOSP/</a>,可结合google的官方说明<a href="https://source.android.com/docs/setup?hl=zh-cn">https://source.android.com/docs/setup?hl=zh-cn</a>。具体指令就不介绍了,主要是repo sync费时,注意选好你想构建的build tag。</p><span id="more"></span><h2 id="Build"><a href="#Build" class="headerlink" title="Build"></a>Build</h2><p>使用aosp的编译比较简单,直接走aosp那一套就行,主要就是使用下列命令,不过也可以结合官方文档介绍,自行修改。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> aosp <span class="comment"># 进到aosp根目录</span></span><br><span class="line">. build/envsetup.sh <span class="comment"># 配置构建环境,这条命令会把一系列命令加到可执行环境中</span></span><br><span class="line">lunch armv8_eng <span class="comment"># 配置要<product_name>-<build_variant>,我这里要的是arm64的</span></span><br><span class="line"><span class="built_in">cd</span> external/pdfium <span class="comment"># go to pdfium directory</span></span><br><span class="line">mma <span class="comment"># build module and deps</span></span><br><span class="line"><span class="comment"># out/target/product/armv8/obj/SHARED_LIBRARIES/libpdfium_intermediates/libpdfium.so</span></span><br></pre></td></tr></table></figure><h1 id="构建chromium版本"><a href="#构建chromium版本" class="headerlink" title="构建chromium版本"></a>构建chromium版本</h1><h2 id="编好的库,直接拿来用"><a href="#编好的库,直接拿来用" class="headerlink" title="编好的库,直接拿来用"></a>编好的库,直接拿来用</h2><p><a href="https://github.com/bblanchon/pdfium-binaries">pdfium-binaries</a><br>这个项目是利用github action自动构建pdfium,其提供了所有版本的so,相当方便。另外想手动构建,有些要修改的内容,也可以参考这个项目中的patch。这个项目是我构建完以后才找到的,算是白忙活,不过也算是一点知识。</p><h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p>一开始我在网上搜了很多资料,都是讲如何构建这个版本的,所以我在aosp代码同步的时候,好顺带搞了搞这个,和aosp的mm就可以构建,这个是相当头疼啊。</p><ol><li>官方文档简单。官方只介绍了如何Build,但是没告诉你如何build安卓用的库。</li><li>网上内容复杂、老旧,对伸手党不友好。比如 <code>install-build-deps-android.sh</code>,我搜的所有资料,里面都有这条命令,但是,这个文件其实已经删除了。</li><li>没有镜像。可能是我没找到,反正代码是我全程开科技同步的。</li></ol><h2 id="准备-1"><a href="#准备-1" class="headerlink" title="准备"></a>准备</h2><ol><li>ubuntu。不确定mac行不行,我mac上试了一下,install-build-deps.sh提示我没有lsb-release,我就直接换ubuntu了</li><li>80GB硬盘。其实用不了,我虚拟机开了这么多。</li><li>上网要有科技,所有代码都在googlesource,建议有较好的网络。</li></ol><h2 id="构建"><a href="#构建" class="headerlink" title="构建"></a>构建</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><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">git <span class="built_in">clone</span> https://chromium.googlesource.com/chromium/tools/depot_tools.git</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="string">"<span class="variable">$PATH</span>:/path/to/depot_tools"</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># get code</span></span><br><span class="line"><span class="built_in">mkdir</span> repo</span><br><span class="line"><span class="built_in">cd</span> repo</span><br><span class="line">gclient config --unmanaged https://pdfium.googlesource.com/pdfium.git</span><br><span class="line"><span class="built_in">echo</span> <span class="string">"target_os = [ 'android' ]"</span> >> .gclient</span><br><span class="line">gclient <span class="built_in">sync</span></span><br><span class="line">gclient runhooks</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment"># setup build</span></span><br><span class="line"><span class="built_in">cd</span> pdfium</span><br><span class="line">./build/install-build-deps.sh --android</span><br><span class="line"></span><br><span class="line">gn args out</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="args"><a href="#args" class="headerlink" title="args"></a>args</h2><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></pre></td><td class="code"><pre><span class="line">target_os="android"</span><br><span class="line">target_cpu="arm64"</span><br><span class="line"></span><br><span class="line">use_goma = false</span><br><span class="line">pdf_bundle_freetype=true</span><br><span class="line">pdf_is_standalone=true</span><br><span class="line">is_component_build=false</span><br><span class="line">pdf_enable_xfa=false</span><br><span class="line">pdf_enable_v8=false</span><br><span class="line">pdf_use_skia = false</span><br><span class="line">use_cxx11_on_android=false</span><br><span class="line"></span><br><span class="line">is_debug=false</span><br><span class="line">is_official_build=true</span><br><span class="line">chrome_pgo_phase = 0</span><br></pre></td></tr></table></figure><h2 id="proxy"><a href="#proxy" class="headerlink" title="proxy"></a>proxy</h2><p>[Boto]<br>proxy = example-host<br>proxy_port = port number</p><p>save as your_file.boto<br>export NO_AUTH_BOTO_CONFIG=/path/your_file.boto</p><h2 id="config"><a href="#config" class="headerlink" title="config"></a>config</h2><p>此节内容收集自互联网,有些内容并不适用,但第一条的FPDFSDK_EXPORTS是必须的。</p><ol><li>打开pdfium/BUILD.gn 文件,找到 config(“pdfium_common_config”),将以下配置: <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></pre></td><td class="code"><pre><span class="line">config("pdfium_common_config") {</span><br><span class="line"> cflags = [ ]</span><br><span class="line"> ldflags = []</span><br><span class="line"> include_dirs = [ "." ]</span><br><span class="line"> defines = [</span><br><span class="line"> "PNG_PREFIX",</span><br><span class="line"> "PNG_USE_READ_MACROS",</span><br><span class="line"> **"FPDFSDK_EXPORTS"**</span><br><span class="line"> ]</span><br></pre></td></tr></table></figure></li></ol><ul><li>增加”-fvisibility=default”到 cflags ,将符号的可见性设置为默认值。这意味着其他组件可以引用该符号,并且符号定义可以被另一个组件中的同名定义覆盖。</li><li>增加”-DNOCJK”到 cflags ,这将停止将字体嵌入到.so文件中并减小其大小。</li><li>增加“FPDFSDK_EXPORTS” 到 defines — 把符号表嵌入到共享库中。</li></ul><ol start="2"><li>在component(“pdfium”)前追加以下配置 -L/lib/x86_64-linux-gnu/libatomic貌似可以不加,因为之前是 -latomic,结果系统死活找不到 <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></pre></td><td class="code"><pre><span class="line">shared_library("pdfsdk") {</span><br><span class="line"> deps = [":pdfium"]</span><br><span class="line"> ldflags = [ "-L/lib/x86_64-linux-gnu/libatomic" ]</span><br><span class="line"> if (target_os == "android") {</span><br><span class="line"> configs -= [ "//build/config/android:hide_all_but_jni_onload" ]</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">component("pdfium") {</span><br><span class="line">//…</span><br></pre></td></tr></table></figure></li></ol><h2 id="build"><a href="#build" class="headerlink" title="build"></a>build</h2><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">ninja -C out pdfsdk</span><br></pre></td></tr></table></figure><h2 id="备注"><a href="#备注" class="headerlink" title="备注"></a>备注</h2><ol><li>指定commit同步代码:gclient sync –revision 837fe726e639f823a62ed4aa4abda45d40f3c803 </li><li>同步代码时要加的参数,因为我们只想要代码 并不想要history<br> repo init –depth=1 -u <a href="https://github.com/zawzaww/aosp-android.git-bandroid-8.1.0">https://github.com/zawzaww/aosp-android.git-bandroid-8.1.0</a><br> repo sync -f –force-sync –no-clone-bundle –no-tags -j$(nproc –all)</li><li>gclient也可以加上-no-history节省同步时间</li></ol><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://github.com/bblanchon/pdfium-binaries">pdfium-binaries</a><br><a href="https://i.lckiss.com/?p=5897">https://i.lckiss.com/?p=5897</a><br><a href="https://github.com/bblanchon/pdfium-binaries/blob/master/patches/shared_library.patch">https://github.com/bblanchon/pdfium-binaries/blob/master/patches/shared_library.patch</a><br><a href="https://groups.google.com/g/pdfium/c/-WdW5n0KZ-8">https://groups.google.com/g/pdfium/c/-WdW5n0KZ-8</a></p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>项目中接入了打开pdf文件的功能,使用的是<a href="https://github.com/barteksc/PdfiumAndroid/">https://github.com/barteksc/PdfiumAndroid/</a>。现在想升级一下pdfium库,旧库具体原因就不解释了,毕竟这个项目最后一个commit在6年前,当然,平常用来接入打开pdf也是没问题的。</p>
<h1 id="开源库"><a href="#开源库" class="headerlink" title="开源库"></a>开源库</h1><p>在google一通搜索,发现pdfium库包含两套,一套在aosp中,另一套在chromium中。</p>
<ul>
<li><a href="https://android.googlesource.com/platform/external/pdfium/">https://android.googlesource.com/platform/external/pdfium/</a></li>
<li><a href="https://pdfium.googlesource.com/pdfium/">https://pdfium.googlesource.com/pdfium/</a><br>两款代码应该差不多,aosp的构建使用Android.bp,版本要比pdfium版本的落后一些。再看PdfiumAndroid项目README,第一句就说Uses pdfium library from AOSP,使用的库应该也是aosp的库。</li>
</ul>
<h1 id="构建aosp版"><a href="#构建aosp版" class="headerlink" title="构建aosp版"></a>构建aosp版</h1><h2 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h2><ol>
<li>ubuntu 可以使用virtual box去装个虚拟机</li>
<li>200GB以上硬盘空间,清华会推荐你下载aosp-latest.tar再解压,光压缩包80G了,解压完我就把tar包删了</li>
</ol>
<h2 id="获取aosp源码"><a href="#获取aosp源码" class="headerlink" title="获取aosp源码"></a>获取aosp源码</h2><p>推荐使用清华镜像<a href="https://mirrors-i.tuna.tsinghua.edu.cn/help/AOSP/">https://mirrors-i.tuna.tsinghua.edu.cn/help/AOSP/</a>,可结合google的官方说明<a href="https://source.android.com/docs/setup?hl=zh-cn">https://source.android.com/docs/setup?hl=zh-cn</a>。具体指令就不介绍了,主要是repo sync费时,注意选好你想构建的build tag。</p></summary>
<category term="android开发" scheme="https://www.lefo.me/categories/android%E5%BC%80%E5%8F%91/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="pdfium" scheme="https://www.lefo.me/tags/pdfium/"/>
</entry>
<entry>
<title>H3C NX54路由器如何永久开telnet,永久关闭ipv6防火墙</title>
<link href="https://www.lefo.me/2024/02/20/nx54-telnet/"/>
<id>https://www.lefo.me/2024/02/20/nx54-telnet/</id>
<published>2024-02-20T03:17:40.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>之前买了一台NX54路由器,结果这路由固件完全不给用户定制化的机会,研究如何保存ip6table的配置没有成功,但是成功保存了telnet开启配置,分享一下。这套固件的情况如下:</p><ol><li>默认开启ipv6防火墙,且关闭后,重启会失效,再次打开。</li><li>可以通过telnet连接路由,telnet配置也不保存,停电或者重启后失效。</li><li>根目录使用overlayfs做到了tmp下,是整个系统会恢复的主要原因。</li><li>提供了/mnt目录保存数据,/mnt/bak/startup.bak就是所有配置项。<span id="more"></span></li></ol><h1 id="关闭ipv6防火墙"><a href="#关闭ipv6防火墙" class="headerlink" title="关闭ipv6防火墙"></a>关闭ipv6防火墙</h1><ol><li>访问http://[ip]/debug.asp</li><li>向下拉,找到telnet管理,勾选启动telnet,点旁边应用</li><li>在命令行执行如下命令,关闭ipv6防火墙<figure class="highlight sh"><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">telnet <路由ip> 15000</span><br><span class="line"><span class="comment"># 输入密码</span></span><br><span class="line"><your password></span><br><span class="line"><span class="comment"># <H3C_NX54>后输入 debugshell</span></span><br><span class="line">debugshell</span><br><span class="line">ip6tables -D FORWARD -j ACCEPT</span><br><span class="line">ip6tables -I FORWARD -j ACCEPT</span><br></pre></td></tr></table></figure></li></ol><h1 id="永久开telnet"><a href="#永久开telnet" class="headerlink" title="永久开telnet"></a>永久开telnet</h1><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><ol><li>系统保存有一份配置文件,重启后会根据这份配置文件恢复配置。</li><li>配置文件格式如下 <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"><md5> /var/run/.tmpcfg</span><br><span class="line"><版本></span><br><span class="line"><具体配置...></span><br></pre></td></tr></table></figure></li><li>其中md5用于校验整个文件,就是从版本号开始,后续所有配置计算得到的。</li><li>后续有个telnetenable=的配置,但系统做了特殊设定,后台修改telnet并不会保存到配置中,导致telnet不能自动开,这里我们就需要手动修改配置文件。</li><li>修改后,替换md5摘要,整个配置就是一个有效配置。</li></ol><p>流程简单如下,移除第一行 -> 计算md5 -> md5重新加到第一行。</p><h1 id="具体操作"><a href="#具体操作" class="headerlink" title="具体操作"></a>具体操作</h1><h2 id="快速方法"><a href="#快速方法" class="headerlink" title="快速方法"></a><del>快速方法</del></h2><p>本方法只适合配置纯英文,不支持配置文件中有中文,如果失败,可以尝试下一节的手动处理方法。</p><ul><li><a href="/html/nx54config.html"><strong>NX54开启telnet配置生成器</strong></a>(输入所有配置文件内容,网页会帮你修改)</li><li><a href="/html/md5.html">md5在线计算</a>(注意本站会计算所有内容的md5,这里用不上,只做参考)<br>我搭建了一个网页,只要把路由的配置复制过来,就会生成开telnet的新配置。网页纯静态,不会向外发送你的任何配置信息,纯javascript本地运行的,这里教如何用网页转化配置。配置信息里有宽带密码账号,如果实在不放心我,下一个大标题的环节是纯手动改。</li></ul><ol><li>登录路由后台,设备管理 -> 基本管理,备份,下载完成后,复制一份(防止万一需要初始化后恢复得再配置)。</li><li>用文本编辑器打开下载的文件</li><li>复制所有内容到 <a href="/html/nx54config.html">NX54开启telnet配置生成器</a> 左侧,在右侧会生成一份新配置。</li><li>点<code>COPY</code>,替换掉原配置文件内容,保存。</li><li>路由后台,选择文件,恢复。</li><li>等重启后,telnet就永久开了。接下来我们写一个shell,自动执行telnet并开ipv6,这些内容就自己找文心一言或者chatgpt生成吧。</li></ol><h2 id="配置文件带中文或者格式错误的手动处理方法"><a href="#配置文件带中文或者格式错误的手动处理方法" class="headerlink" title="配置文件带中文或者格式错误的手动处理方法"></a>配置文件带中文或者格式错误的手动处理方法</h2><p><strong>推荐,用上面的网页更快,但是配置中不得含有中文,否则生成的文件验证不通过。这里只是把流程纯手动,原理一样,但支持特殊符号、中文。</strong><br><strong><a href="/html/filemd5.html">文件md5计算</a></strong></p><ol><li>登录路由后台,设备管理 -> 基本管理,备份,下载完成后,复制一份备份(防止万一需要初始化后恢复得再配置)。</li><li>打开如下 <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">5f0521762cfa7553351d4173706b4c99 /var/run/.tmpcfg</span><br><span class="line">NX54/NX54V100D011</span><br><span class="line">....</span><br></pre></td></tr></table></figure></li><li>找到<code>telnetenable=</code>,将=后面的内容改为enable,保存。 <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">@telnet</span><br><span class="line">#</span><br><span class="line"> telnetenable=enable</span><br></pre></td></tr></table></figure></li><li>删除第一行,只保留第二行<code>NX54/NX54V100D011</code>开始及后面内容,保存文件。</li><li>打开<a href="/html/filemd5.html">文件md5计算</a>计算,你也可以验证一下修改前的md5。</li><li>将生成的md5再次粘贴到第一行替换原md5(注意一定是小写) <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></pre></td><td class="code"><pre><span class="line">新md5 /var/run/.tmpcfg</span><br><span class="line">NX54/NX54V100D011</span><br><span class="line">...</span><br><span class="line">@telnet</span><br><span class="line">#</span><br><span class="line"> telnetenable=enable</span><br><span class="line">...</span><br></pre></td></tr></table></figure></li><li>去路由后台,备份那里找从文件中恢复设置信息,选择文件,点恢复。</li></ol><h1 id="注意"><a href="#注意" class="headerlink" title="注意"></a>注意</h1><ol><li>最好不要直接全关ipv6防火墙,可以指定ip允许转发。</li><li>nx54分配的ip6地址不固定,所以如果指定ip后,每次需要移除旧的规则</li><li>通常我们都是让文件服务器或者NAS在外网访问,所以可以直接把命令写到nas上,一旦ip变动,则执行命令</li><li>具体iptable指令怎么调整,还是边学指令边问chatgpt吧,我这里就不弄了,我也没NAS,有个旧的arm板子,没怎么搭,去年看了一下快买不起硬盘了,硬盘现在价格逆天了。</li><li>全网首发,没找到怎么配置的方法,要转载请附上我的网址,给点流量。</li></ol><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>不能用中文是后来发现的,感谢新疆的大哥,提供了他的配置文件让我发现了问题。</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>之前买了一台NX54路由器,结果这路由固件完全不给用户定制化的机会,研究如何保存ip6table的配置没有成功,但是成功保存了telnet开启配置,分享一下。这套固件的情况如下:</p>
<ol>
<li>默认开启ipv6防火墙,且关闭后,重启会失效,再次打开。</li>
<li>可以通过telnet连接路由,telnet配置也不保存,停电或者重启后失效。</li>
<li>根目录使用overlayfs做到了tmp下,是整个系统会恢复的主要原因。</li>
<li>提供了&#x2F;mnt目录保存数据,&#x2F;mnt&#x2F;bak&#x2F;startup.bak就是所有配置项。</summary>
<category term="nx54" scheme="https://www.lefo.me/categories/nx54/"/>
<category term="h3c" scheme="https://www.lefo.me/tags/h3c/"/>
<category term="nx54" scheme="https://www.lefo.me/tags/nx54/"/>
</entry>
<entry>
<title>重学安卓之Jetpack</title>
<link href="https://www.lefo.me/2024/01/30/jetpack/"/>
<id>https://www.lefo.me/2024/01/30/jetpack/</id>
<published>2024-01-30T14:07:12.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="导读"><a href="#导读" class="headerlink" title="导读"></a>导读</h1><p>jetpack当时还没来得及用,整个项目就被砍掉了,这两年一直没有接触,最近好多岗位都要求会jetpack,这两年逐渐用的人越来越多,重学Android赶紧简单了解一下,做了一点整理,都是皮毛,供自己查笔记用吧,如果有一些知识点需要增加备注,随时修改。</p><h1 id="Navigation-用于fragment切换"><a href="#Navigation-用于fragment切换" class="headerlink" title="Navigation 用于fragment切换"></a>Navigation 用于fragment切换</h1><h2 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h2><ul><li>可视化,可以as中可视化编辑</li><li>通过destination和action完成页面的导航</li><li>参数传递安全, safe args</li><li>支持deeplink,支持创建PendingIntent<span id="more"></span><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"><span class="comment">/**</span></span><br><span class="line"><span class="comment">* 返回一个 PendingIntent</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">private</span> PendingIntent <span class="title function_">getPendingIntent</span><span class="params">()</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Navigation.findNavController(requireActivity(), R.id.button)</span><br><span class="line"> .createDeepLink()</span><br><span class="line"> .setGraph(R.navigation.my_nav_graph)</span><br><span class="line"> .setDestination(R.id.detailFragment)</span><br><span class="line"> .createTaskStackBuilder()</span><br><span class="line"> .getPendingIntent(<span class="number">0</span>, PendingIntent.FLAG_IMMUTABLE);</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><h2 id="主要组件"><a href="#主要组件" class="headerlink" title="主要组件"></a>主要组件</h2><ol><li>Graph xml资源文件,包含所有页面和页面间的关系</li><li>Controller 用于完成Graph中的页面切换</li><li>NavHostFragment 特殊fragment容器</li><li>提供了NavigationUi控制各种bar的改变</li></ol><h2 id="操作步骤"><a href="#操作步骤" class="headerlink" title="操作步骤:"></a>操作步骤:</h2><ol><li>新建Android Resource File中的Graph文件,Resource type为Navigation,会在navigation目录下生成一个xml</li><li>将NavHostFragment添加到activity。``,作为其它fragment的容器,defaultNaveHost=true会自动处理返回键<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">fragment</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">android:name</span>=<span class="string">"android.navigation. fragment .NavHostFragment"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:defaultNaveHost</span>=<span class="string">"true"</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">app:navGraph</span>=<span class="string">"@navigation/my_graph"</span> /></span></span><br></pre></td></tr></table></figure></li><li>创建新的destination,其实就是将fragment添加到navigation,navigation会有一个startDesination</li><li>建立action,在可视编辑里是将一个和另一个连起来,xml中为fragment含有一个action标签,指向另一个fragment。</li><li>Navigation.findNavController(view).navigate(id)来完成导航</li></ol><h2 id="safe-args传递参数"><a href="#safe-args传递参数" class="headerlink" title="safe args传递参数"></a>safe args传递参数</h2><p>在fragment中添加参数</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></pre></td><td class="code"><pre><span class="line">// 添加插件</span><br><span class="line">apply plugin: 'androidx.navigation.safeargs'</span><br><span class="line"></span><br><span class="line"><fragment></span><br><span class="line"> <action/></span><br><span class="line"> <argument </span><br><span class="line"> android:name="name"</span><br><span class="line"> app:argType="string"</span><br><span class="line"> android:defaultValue='"lefo"'/></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>添加后,会生成对应的代码文件,就可以用Build创建对应的bundle了</p><h2 id="NavigationUi"><a href="#NavigationUi" class="headerlink" title="NavigationUi"></a>NavigationUi</h2><p>NavigationUI 提供了一些静态方法来处理 顶部应用栏 / 抽屉式导航栏 / 底部导航栏中 的界面导航, 统一管理 Fragment 页面切换相关的UI改变<br>CollapsingToolbarlayout、Toolbar、ActionBar</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="comment">// 1. 获取 NavController</span></span><br><span class="line">navController = Navigation.findNavController(<span class="built_in">this</span>, R.id.fragment)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 创建 AppBarConfiguration</span></span><br><span class="line">appBarConfiguration = AppBarConfiguration.Builder(navController.graph).build();</span><br><span class="line">NavigationUI.setupActionBarWithNavController(<span class="built_in">this</span>,navController,appBarConfiguration);</span><br></pre></td></tr></table></figure><h1 id="LifeCycle"><a href="#LifeCycle" class="headerlink" title="LifeCycle"></a>LifeCycle</h1><h2 id="LifecycleOwner和LifecycleObserver"><a href="#LifecycleOwner和LifecycleObserver" class="headerlink" title="LifecycleOwner和LifecycleObserver"></a>LifecycleOwner和LifecycleObserver</h2><p>实现LifecyclerObserver接口,将要在具体生命周期执行的方法使用@OnLifecycleEvent注解</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@OnLifecycleEvent</span> (Lifecycle.Event.ONRESUME) </span><br><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">onResumeEvent</span> <span class="params">()</span> {</span><br><span class="line"> Log.d (TAG, <span class="string">"onResumeEvent() "</span>);</span><br><span class="line">}</span><br><span class="line"><span class="comment">// ...</span></span><br><span class="line"><span class="comment">//在Activity中注册观察者</span></span><br><span class="line">getLifecycle().addObserver(myObserver);</span><br></pre></td></tr></table></figure><ul><li>LifecycleService,使用方式等于Service</li><li>ProcessLifecycleOwner 整个应用的生命周期 使用方法ProcessLifecycleOwner.get().getLifecycle()</li><li>LifecycleRegistry类,管理mObserverMap维护state和event</li></ul><h2 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h2><p>通过在Activity中绑定一个空的fragment来实现监听Activity生命周期。</p><h1 id="ViewModel"><a href="#ViewModel" class="headerlink" title="ViewModel"></a>ViewModel</h1><p>使用ViewModelProvider创建一个ViewModel</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">constructor</span>(</span><br><span class="line"> owner: ViewModelStoreOwner</span><br><span class="line">) : <span class="keyword">this</span>(owner.viewModelStore, defaultFactory(owner), defaultCreationExtras(owner))</span><br></pre></td></tr></table></figure><p>基本原理如下</p><ol><li>ViewModelProvider接收参数ViewModelStoreOwner,参数的对象实现方法getViewModelStore</li><li>ViewModelStore提供一个HashMap<String,ViewModel></li></ol><h1 id="LiveData"><a href="#LiveData" class="headerlink" title="LiveData"></a>LiveData</h1><p>LiveData的作用是ViewModel在数据发生变化时通知页面</p><ul><li>实际上就是一个观察者模式的数据容器,当数据改变时,通知UI刷新;</li><li>能感知activity fragment组件生命周期,</li><li>通常和viewModel一起使用,LiveData<T>作为ViewModel的一个成员变量。</li><li>liveData.observe()注册一个对数据的观察</li><li>observeForever使用后一定要用removeObserver方法停止观察</li></ul><h1 id="DataBinding"><a href="#DataBinding" class="headerlink" title="DataBinding"></a>DataBinding</h1><h2 id="基础用法"><a href="#基础用法" class="headerlink" title="基础用法"></a>基础用法</h2><ul><li>DataBinding结合ViewModel是常用的做法</li><li>向include的二级页面传递数据</li><li>BaseObservable和ObservableField支持多个控件互相绑定数据<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></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- activity_main.xml --></span></span><br><span class="line"><span class="tag"><<span class="name">layout</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">xmlns:app</span>=<span class="string">"http://schemas.android.com/apk/res-auto"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">xmlns:tools</span>=<span class="string">"http://schemas.android.com/tools"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span></span></span><br><span class="line"><span class="tag"> <span class="attr">name</span>=<span class="string">"user"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">type</span>=<span class="string">"com.example.User"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">data</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">RelativeLayout</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">tools:context</span>=<span class="string">".MainActivity"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">TextView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"@{user.name}"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">Button</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:text</span>=<span class="string">"Click"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:onClick</span>=<span class="string">"@{() -> user.onButtonClick()}"</span> /></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"></<span class="name">RelativeLayout</span>></span></span><br><span class="line"><span class="tag"></<span class="name">layout</span>></span></span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : AppCompatActivity() {</span><br><span class="line"></span><br><span class="line"> override fun <span class="title function_">onCreate</span><span class="params">(savedInstanceState: Bundle?)</span> {</span><br><span class="line"> <span class="built_in">super</span>.onCreate(savedInstanceState)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 使用 DataBindingUtil 设置布局文件和数据绑定</span></span><br><span class="line"> val binding: ActivityMainBinding = DataBindingUtil.setContentView(<span class="built_in">this</span>, R.layout.activity_main)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 创建 User 对象</span></span><br><span class="line"> <span class="type">val</span> <span class="variable">user</span> <span class="operator">=</span> User(<span class="string">"John Doe"</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将 User 对象绑定到布局中的 user 变量</span></span><br><span class="line"> binding.user = user</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 设置点击事件</span></span><br><span class="line"> binding.button.setOnClickListener {</span><br><span class="line"> user.onButtonClick()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">include</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">layout</span>=<span class="string">"@layout/layout2"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:user</span>=<span class="string">"@{user}"</span>/></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!--在2级页面这么写--></span></span><br><span class="line"><span class="tag"><<span class="name">data</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">variable</span></span></span><br><span class="line"><span class="tag"> <span class="attr">name</span>=<span class="string">"user"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">type</span>=<span class="string">"com.example.User"</span> /></span></span><br></pre></td></tr></table></figure></li></ul><h2 id="自定义BindAdapter"><a href="#自定义BindAdapter" class="headerlink" title="自定义BindAdapter"></a>自定义BindAdapter</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> CustomBindingAdapters {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@BindingAdapter(<span class="string">"imageUrl"</span>)</span></span><br><span class="line"> <span class="meta">@JvmStatic</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">loadImage</span><span class="params">(imageView: <span class="type">ImageView</span>, url: <span class="type">String</span>?)</span></span> {</span><br><span class="line"> <span class="comment">// 使用 Glide 加载图片</span></span><br><span class="line"> Glide.with(imageView.context)</span><br><span class="line"> .load(url)</span><br><span class="line"> .into(imageView)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@BindingAdapter(<span class="string">"visibility"</span>)</span></span><br><span class="line"> <span class="meta">@JvmStatic</span></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">setVisibility</span><span class="params">(view: <span class="type">View</span>, isVisible: <span class="type">Boolean</span>)</span></span> {</span><br><span class="line"> <span class="comment">// 设置 View 的可见性</span></span><br><span class="line"> view.visibility = <span class="keyword">if</span> (isVisible) View.VISIBLE <span class="keyword">else</span> View.GONE</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><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="tag"><<span class="name">ImageView</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/imageView"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_width</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:layout_height</span>=<span class="string">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:imageUrl</span>=<span class="string">"@{viewModel.imageUrl}"</span> /></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">View</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:id</span>=<span class="string">"@+id/customView"</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">"wrap_content"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">app:visibility</span>=<span class="string">"@{viewModel.isVisible}"</span> /></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="ViewBinding"><a href="#ViewBinding" class="headerlink" title="ViewBinding"></a>ViewBinding</h1><p>减少findviewbyid,简单举例一下</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></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用 View Binding</span></span><br><span class="line"><span class="keyword">val</span> binding = ActivityMainBinding.inflate(layoutInflater)</span><br><span class="line">setContentView(binding.root)</span><br></pre></td></tr></table></figure><h1 id="Room"><a href="#Room" class="headerlink" title="Room"></a>Room</h1><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">@Entity(tableName = <span class="string">"user_table"</span>)</span></span><br><span class="line"><span class="keyword">data</span> <span class="keyword">class</span> <span class="title class_">User</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">Long</span> = <span class="number">0</span>,</span><br><span class="line"> <span class="keyword">val</span> name: String,</span><br><span class="line"> <span class="keyword">val</span> email: String</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Dao</span></span><br><span class="line"><span class="meta">@Dao</span></span><br><span class="line"><span class="keyword">interface</span> <span class="title class_">UserDao</span> {</span><br><span class="line"> <span class="meta">@Query(<span class="string">"SELECT * FROM user_table"</span>)</span></span><br><span class="line"> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">getAllUsers</span><span class="params">()</span></span>: List<User></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Insert(onConflict = OnConflictStrategy.REPLACE)</span></span><br><span class="line"> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">insertUser</span><span class="params">(user: <span class="type">User</span>)</span></span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// @Database(entities = [User::class], version = 1, exportSchema = false)</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AppDatabase</span> : <span class="type">RoomDatabase</span>() {</span><br><span class="line"> <span class="keyword">abstract</span> <span class="function"><span class="keyword">fun</span> <span class="title">userDao</span><span class="params">()</span></span>: UserDao</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>数据库,使用起来和之前的注解框架差不多,可以配合LiveData和ViewModel实现数据的刷新。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">UserRepository</span>(<span class="keyword">private</span> <span class="keyword">val</span> userDao: UserDao) {</span><br><span class="line"> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">getUsersFromNetwork</span><span class="params">()</span></span>: List<User> {</span><br><span class="line"> <span class="comment">// 从网络获取数据的逻辑</span></span><br><span class="line"> <span class="keyword">val</span> networkData = <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// 将数据存储到数据库</span></span><br><span class="line"> userDao.insertUser(networkData)</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> networkData</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">getAllUsers</span><span class="params">()</span></span>: List<User> {</span><br><span class="line"> <span class="comment">// 从数据库获取数据的逻辑</span></span><br><span class="line"> <span class="keyword">return</span> userDao.getAllUsers()</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// ViewModel中使用Repository</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MyViewModel</span>(<span class="keyword">private</span> <span class="keyword">val</span> userRepository: UserRepository) : ViewModel() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> _users = MutableLiveData<List<User>>()</span><br><span class="line"> <span class="keyword">val</span> users: LiveData<List<User>> <span class="keyword">get</span>() = _users</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">fetchData</span><span class="params">()</span></span> {</span><br><span class="line"> viewModelScope.launch {</span><br><span class="line"> <span class="comment">// 从网络获取数据并更新数据库</span></span><br><span class="line"> userRepository.getUsersFromNetwork()</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 从数据库获取数据并更新 LiveData</span></span><br><span class="line"> _users.value = userRepository.getAllUsers()</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><h1 id="Paging"><a href="#Paging" class="headerlink" title="Paging"></a>Paging</h1><p>Paging还是看官方文档更合适,网上搜的信息Paging2和Paging3内容混乱,这里我也不整理了,特别是使用RemoteMediator去同时接入 db + network 的方案。<br><strong>paging库3.0变化较大,放个<a href="%5Bhttps://developer.android.google.cn/topic/libraries/architecture/paging/v3-overview?hl=zh-cn%5D">官方链接</a></strong><br><strong><a href="https://github.com/android/architecture-components-samples/blob/main/PagingWithNetworkSample/">官方DEMO</a></strong></p><ol><li>PagingDataAdapter,首先RecyclerView的Adapter需要继承这个类,此外,您也可以使用随附的 AsyncPagingDataDiffer 组件构建自己的自定义适配器</li><li>Pager 基于 PagingSource 对象和 PagingConfig 配置对象来构造在响应式流中公开的 PagingData 实例。</li><li>PagingSource,数据载入,有一个load方法需要实现</li><li>PagingData 用于存放分页数据快照的容器。它会查询 PagingSource 对象并存储结果。</li><li>RemoteMediator 处理分层数据源。</li></ol><blockquote><p>Paging 2 中的 PageKeyedDataSource、PositionalDataSource 和 ItemKeyedDataSource 都合并到了 Paging 3 中的 PagingSource API 中。所有旧版 API 类中的加载方法都合并到了 PagingSource 中的单个 load() 方法中。这样可以减少代码重复,因为在旧版 API 类的实现中,各种加载方法之间的很多逻辑通常是相同的。<br>在 Paging 3 中,所有加载方法参数被一个 LoadParams 密封类替代,该类中包含了每个加载类型所对应的子类。如果需要区分 load() 方法中的加载类型,请检查传入了 LoadParams 的哪个子类:LoadParams.Refresh、LoadParams.Prepend 还是 LoadParams.Append。</p></blockquote><ol><li><del>BoundaryCallback</del>弃用了,使用RemoteMediator。在分页数据耗尽时,Paging 库会触发 RemoteMediator 以从网络源加载新数据。RemoteMediator 会将新数据存储在本地数据库中,因此无需在 ViewModel 中使用内存缓存。最后,PagingSource 会使自身失效,而 Pager 会创建一个新实例以从数据库中加载新数据。</li></ol><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@OptIn(ExperimentalPagingApi::class)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">ExampleRemoteMediator</span>(</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> query: String,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> database: RoomDb,</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> networkService: ExampleBackendService</span><br><span class="line">) : RemoteMediator<<span class="built_in">Int</span>, User>() {</span><br><span class="line"> <span class="keyword">val</span> userDao = database.userDao()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">load</span><span class="params">(</span></span></span><br><span class="line"><span class="params"><span class="function"> loadType: <span class="type">LoadType</span>,</span></span></span><br><span class="line"><span class="params"><span class="function"> state: <span class="type">PagingState</span><<span class="type">Int</span>, User></span></span></span><br><span class="line"><span class="params"><span class="function"> )</span></span>: MediatorResult {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="WorkManager"><a href="#WorkManager" class="headerlink" title="WorkManager"></a>WorkManager</h1><p>最低兼容api 14,在api 23以上使用JobScheduler,在api 23以下使用AlarmManager</p><ol><li>创建Worker<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></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MyWorker</span>(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {</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">doWork</span><span class="params">()</span></span>: Result {</span><br><span class="line"> <span class="comment">// 后台任务逻辑</span></span><br><span class="line"> Timber.d(<span class="string">"Performing long running task in MyWorker"</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 返回任务执行结果</span></span><br><span class="line"> <span class="keyword">return</span> Result.success()</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li>设置约束条件<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></pre></td><td class="code"><pre><span class="line">import androidx.work.Constraints</span><br><span class="line">import androidx.work.NetworkType</span><br><span class="line"></span><br><span class="line">val constraints = Constraints.Builder()</span><br><span class="line"> .setRequiredNetworkType(NetworkType.CONNECTED)</span><br><span class="line"> .setRequiresCharging(true)</span><br><span class="line"> .build()</span><br></pre></td></tr></table></figure></li><li>创建请求<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 一次性工作请求</span></span><br><span class="line"><span class="keyword">val</span> oneTimeRequest = OneTimeWorkRequest.Builder(MyWorker::<span class="keyword">class</span>.java)</span><br><span class="line"> .setConstraints(constraints)</span><br><span class="line"> .build()</span><br><span class="line"></span><br><span class="line"><span class="comment">// 周期性工作请求</span></span><br><span class="line"><span class="keyword">val</span> periodicRequest = PeriodicWorkRequest.Builder(MyWorker::<span class="keyword">class</span>.java, <span class="number">24</span>, TimeUnit.HOURS)</span><br><span class="line"> .setConstraints(constraints)</span><br><span class="line"> .build()</span><br></pre></td></tr></table></figure></li><li>加入队列<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">// 将一次性工作请求加入队列</span><br><span class="line">WorkManager.getInstance(context).enqueue(oneTimeRequest)</span><br><span class="line"></span><br><span class="line">// 将周期性工作请求加入队列</span><br><span class="line">WorkManager.getInstance(context).enqueue(periodicRequest)</span><br></pre></td></tr></table></figure>WorkManager还支持链式任务,观察任务状态,传递参数。</li></ol><ul><li>链式: 在加入队列时使用beginWith(oneTimeRequest).then(periodicRequest),还可以combine…then…</li><li>观察任务状态: WorkManager.getWorkInfosXXX,可以通过tag,id,worker对象查看任务状态,还能获取对应的LiveData,通过LiveData就能在任务状态变化时收到通知</li><li>传参: Data类</li></ul><h1 id="Hilt"><a href="#Hilt" class="headerlink" title="Hilt"></a>Hilt</h1><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></pre></td><td class="code"><pre><span class="line">// 在应用模块的 build.gradle 文件中</span><br><span class="line">implementation "com.google.dagger:hilt-android:2.38.1"</span><br><span class="line">kapt "com.google.dagger:hilt-android-compiler:2.38.1"</span><br><span class="line"></span><br><span class="line">// 如果使用了ViewModel,还需要添加以下依赖</span><br><span class="line">implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"</span><br><span class="line">kapt "androidx.hilt:hilt-compiler:1.0.0-alpha03"</span><br></pre></td></tr></table></figure><ol><li>在 Application 类中启用 Hilt: 在你的 Application 类中使用 @HiltAndroidApp 注解启用 Hilt。<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">@HiltAndroidApp</span><br><span class="line">class MyApplication : Application()</span><br></pre></td></tr></table></figure></li><li>配置依赖注入: 使用 @Inject 注解标记需要注入的依赖,同时使用 @HiltViewModel 注解标记需要注入的 ViewModel。<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">class MyRepository @Inject constructor() {</span><br><span class="line"> // ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">@HiltViewModel</span><br><span class="line">class MyViewModel @Inject constructor(private val repository: MyRepository) : ViewModel() {</span><br><span class="line"> // ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li>在 Activity 或 Fragment 中使用 Hilt 注入: 在需要注入依赖的 Activity 或 Fragment 中,使用 @AndroidEntryPoint 注解启用 Hilt,并使用 @Inject 注解标记需要注入的字段。<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">@AndroidEntryPoint</span><br><span class="line">class MyActivity : AppCompatActivity() {</span><br><span class="line"></span><br><span class="line"> @Inject</span><br><span class="line"> lateinit var viewModel: MyViewModel</span><br><span class="line"></span><br><span class="line"> // ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li>在 Module 中配置依赖: 如果需要自定义依赖注入的配置,可以创建一个 Dagger Module,并在其上使用 @InstallIn 注解指定作用范围。</li></ol><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></pre></td><td class="code"><pre><span class="line">@Module</span><br><span class="line">@InstallIn(ApplicationComponent::class)</span><br><span class="line">object MyModule {</span><br><span class="line"></span><br><span class="line"> @Provides</span><br><span class="line"> fun provideMyRepository(): MyRepository {</span><br><span class="line"> return MyRepository()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="DataStore"><a href="#DataStore" class="headerlink" title="DataStore"></a>DataStore</h1><ul><li>用于存储较小的数据集,分为Preferences DataStore和Proto DataStore,可以代替SP,可以轻松迁移数据。</li><li><a href="https://developer.android.com/topic/libraries/architecture/datastore?hl=zh-cn">官方介绍</a></li></ul><h2 id="Preferences-DataStore"><a href="#Preferences-DataStore" class="headerlink" title="Preferences DataStore"></a>Preferences DataStore</h2><ul><li>读取用flow</li><li>写入用edit<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建dataStore context.createDataStore</span></span><br><span class="line"><span class="comment">// 使用委托创建dataStore</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> USER_PREFERENCES_NAME = <span class="string">"user_preferences"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> Context.dataStore <span class="keyword">by</span> preferencesDataStore(</span><br><span class="line"> name = USER_PREFERENCES_NAME,</span><br><span class="line"> produceMigrations = { context -></span><br><span class="line"> <span class="comment">// Since we're migrating from SharedPreferences, add a migration based on the</span></span><br><span class="line"> <span class="comment">// SharedPreferences name</span></span><br><span class="line"> listOf(SharedPreferencesMigration(context, USER_PREFERENCES_NAME))</span><br><span class="line"> }</span><br><span class="line">)</span><br></pre></td></tr></table></figure></li></ul><p>当 DataStore 从文件读取数据时,如果读取数据期间出现错误,系统会抛出 IOExceptions。我们可以通过以下方式处理这些事务:在 map() 之前使用 catch() Flow 运算符,并且在抛出的异常是 IOException 时发出 emptyPreferences()。如果出现其他类型的异常,最好重新抛出该异常。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> userPreferencesFlow: Flow<UserPreferences> = dataStore.<span class="keyword">data</span></span><br><span class="line"> .<span class="keyword">catch</span> { exception -></span><br><span class="line"> <span class="comment">// dataStore.data throws an IOException when an error is encountered when reading data</span></span><br><span class="line"> <span class="keyword">if</span> (exception <span class="keyword">is</span> IOException) {</span><br><span class="line"> emit(emptyPreferences())</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> exception</span><br><span class="line"> }</span><br><span class="line"> }.map { preferences -></span><br><span class="line"> <span class="comment">// Get our show completed value, defaulting to false if not set:</span></span><br><span class="line"> <span class="keyword">val</span> showCompleted = preferences[PreferencesKeys.SHOW_COMPLETED]?: <span class="literal">false</span></span><br><span class="line"> UserPreferences(showCompleted)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">updateShowCompleted</span><span class="params">(showCompleted: <span class="type">Boolean</span>)</span></span> {</span><br><span class="line"> dataStore.edit { preferences -></span><br><span class="line"> preferences[PreferencesKeys.SHOW_COMPLETED] = showCompleted</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 可以使用扩展函数asLiveData转换flow为LiveData</span></span><br><span class="line">userPreferencesFlow.asLiveData().observe(viewLifecycleOwner,{ })</span><br></pre></td></tr></table></figure><h2 id="Proto-DataStore"><a href="#Proto-DataStore" class="headerlink" title="Proto DataStore"></a>Proto DataStore</h2><ol><li>区别于之前的Preferences DataStore的读取方式.map{ preferences-> { } }</li><li>写入方式使用updateData(){ preferences-> {…}}<br>生成pb对象和类<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">syntax = <span class="string">"proto3"</span>;</span><br><span class="line"></span><br><span class="line">option java_package = <span class="string">"com.codelab.android.datastore"</span>;</span><br><span class="line">option java_multiple_files = <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">message UserPreferences {</span><br><span class="line"> <span class="comment">// filter for showing / hiding completed tasks</span></span><br><span class="line"> bool show_completed = <span class="number">1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>创建UserPreferencesSerializer<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">object</span> UserPreferencesSerializer : Serializer<UserPreferences> {</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">val</span> defaultValue: UserPreferences = UserPreferences.getDefaultInstance()</span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">readFrom</span><span class="params">(input: <span class="type">InputStream</span>)</span></span>: UserPreferences {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> UserPreferences.parseFrom(input)</span><br><span class="line"> } <span class="keyword">catch</span> (exception: InvalidProtocolBufferException) {</span><br><span class="line"> <span class="keyword">throw</span> CorruptionException(<span class="string">"Cannot read proto."</span>, exception)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">writeTo</span><span class="params">(t: <span class="type">UserPreferences</span>, output: <span class="type">OutputStream</span>)</span></span> = t.writeTo(output)</span><br><span class="line">}</span><br></pre></td></tr></table></figure>创建DataStore<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> USER_PREFERENCES_NAME = <span class="string">"user_preferences"</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> DATA_STORE_FILE_NAME = <span class="string">"user_prefs.pb"</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> SORT_ORDER_KEY = <span class="string">"sort_order"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> Context.userPreferencesStore: DataStore<UserPreferences> <span class="keyword">by</span> dataStore(</span><br><span class="line"> fileName = DATA_STORE_FILE_NAME,</span><br><span class="line"> serializer = UserPreferencesSerializer</span><br><span class="line">)</span><br></pre></td></tr></table></figure>读取数据<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">val</span> TAG: String = <span class="string">"UserPreferencesRepo"</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">val</span> userPreferencesFlow: Flow<UserPreferences> = dataStore.<span class="keyword">data</span></span><br><span class="line"> .<span class="keyword">catch</span> { exception -></span><br><span class="line"> <span class="comment">// dataStore.data throws an IOException when an error is encountered when reading data</span></span><br><span class="line"> <span class="keyword">if</span> (exception <span class="keyword">is</span> IOException) {</span><br><span class="line"> Log.e(TAG, <span class="string">"Error reading sort order preferences."</span>, exception)</span><br><span class="line"> emit(UserPreferences.getDefaultInstance())</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> exception</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure>写入数据<figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">suspend</span> <span class="function"><span class="keyword">fun</span> <span class="title">updateShowCompleted</span><span class="params">(completed: <span class="type">Boolean</span>)</span></span> {</span><br><span class="line"> dataStore.updateData { preferences -></span><br><span class="line"> preferences.toBuilder().setShowCompleted(completed).build()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ol>]]></content>
<summary type="html"><h1 id="导读"><a href="#导读" class="headerlink" title="导读"></a>导读</h1><p>jetpack当时还没来得及用,整个项目就被砍掉了,这两年一直没有接触,最近好多岗位都要求会jetpack,这两年逐渐用的人越来越多,重学Android赶紧简单了解一下,做了一点整理,都是皮毛,供自己查笔记用吧,如果有一些知识点需要增加备注,随时修改。</p>
<h1 id="Navigation-用于fragment切换"><a href="#Navigation-用于fragment切换" class="headerlink" title="Navigation 用于fragment切换"></a>Navigation 用于fragment切换</h1><h2 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h2><ul>
<li>可视化,可以as中可视化编辑</li>
<li>通过destination和action完成页面的导航</li>
<li>参数传递安全, safe args</li>
<li>支持deeplink,支持创建PendingIntent</summary>
<category term="android" scheme="https://www.lefo.me/categories/android/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="重学android" scheme="https://www.lefo.me/tags/%E9%87%8D%E5%AD%A6android/"/>
<category term="Jetpack" scheme="https://www.lefo.me/tags/Jetpack/"/>
</entry>
<entry>
<title>第一次尝试frida</title>
<link href="https://www.lefo.me/2023/10/06/frida/"/>
<id>https://www.lefo.me/2023/10/06/frida/</id>
<published>2023-10-06T15:31:26.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>大概半个月前,群里的一个小伙伴问,谁能把某电商APP的订单列表导出到电脑,有个私活,报个价。其实我对这种稍微有点了解,无非是抓包,破解。但通常App的网络请求会有一套签名验证机制,所以要想模拟出整套请求流程,也算比较复杂。和群里的讨论后,觉得用hook做合适,但群里和我一样,都是安卓开发,没有逆向工程师,首先想到的是xposed。</p><h1 id="xposed太重"><a href="#xposed太重" class="headerlink" title="xposed太重"></a>xposed太重</h1><p>我之前倒是写过xposed,xposed缺点有两个,第一就是环境复杂,你得安装xposed环境才可以,目前都是通过virtual xposed来使用。再就是插件一但有修改,就得重启手机。不过这也带来了一个有点就是持久化,只要装进去,就一直存在。那有没有更轻量级的工具可以达到这种效果,后来在网上找到了frida。(这也好几年了,我居然不知道)</p><span id="more"></span><h1 id="Frida"><a href="#Frida" class="headerlink" title="Frida"></a>Frida</h1><p>官网<br><a href="https://github.com/frida/frida/releases">https://github.com/frida/frida/releases</a><br><a href="https://frida.re/">https://frida.re</a></p><h2 id="环境搭建"><a href="#环境搭建" class="headerlink" title="环境搭建"></a>环境搭建</h2><p>Frida分为客户端(电脑) + 服务端(手机端)。</p><figure class="highlight sh"><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">pip3 install frida</span><br><span class="line">pip3 install frida-tools</span><br><span class="line"><span class="comment"># 安装完成后运行,查看是否安装成功,有进程输出说明成功</span></span><br><span class="line">frida-ps -U</span><br></pre></td></tr></table></figure><p>下载服务端程序到手机(模拟器),必须有root权限。下载时注意选择你对应平台的frida-server</p><figure class="highlight sh"><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">adb push frida-server /data/local/tmp/</span><br><span class="line">adb shell</span><br><span class="line">su</span><br><span class="line"><span class="built_in">chmod</span> 777 /data/local/tmp/frida-server</span><br><span class="line">/data/local/tmp/frida-server</span><br></pre></td></tr></table></figure><h2 id="注入js,hook"><a href="#注入js,hook" class="headerlink" title="注入js,hook"></a>注入js,hook</h2><p>举一个hook okhttp的例子,okhttp的响应信息都打印出来。在电脑上新建一个test.js</p><figure class="highlight js"><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="comment">// test.js hook okhttp 因为okhttp的response只能消费一次,为了不影响默认的程序,将buffer复制一份</span></span><br><span class="line"><span class="keyword">function</span> <span class="title function_">get_http</span>(<span class="params"></span>){</span><br><span class="line"> <span class="title class_">Java</span>.<span class="title function_">perform</span>(<span class="keyword">function</span> (<span class="params"></span>){</span><br><span class="line"> <span class="keyword">var</span> <span class="title class_">RealCall</span> = <span class="title class_">Java</span>.<span class="title function_">use</span>(<span class="string">"okhttp3.RealCall"</span>);</span><br><span class="line"> <span class="keyword">var</span> <span class="title class_">Buffer</span> = <span class="title class_">Java</span>.<span class="title function_">use</span>(<span class="string">"okio.Buffer"</span>);</span><br><span class="line"> <span class="keyword">var</span> <span class="title class_">ResponseBody</span> = <span class="title class_">Java</span>.<span class="title function_">use</span>(<span class="string">"okhttp3.ResponseBody"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="title class_">RealCall</span>.<span class="property">getResponseWithInterceptorChain</span>.<span class="title function_">overload</span>().<span class="property">implementation</span> = <span class="keyword">function</span> (<span class="params"></span>) {</span><br><span class="line"> <span class="keyword">var</span> response = <span class="variable language_">this</span>.<span class="title function_">getResponseWithInterceptorChain</span>();</span><br><span class="line"> <span class="keyword">var</span> responseBody = response.<span class="title function_">body</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> buffer = <span class="title class_">Buffer</span>.$new();</span><br><span class="line"> responseBody.<span class="title function_">source</span>().<span class="title function_">readAll</span>(buffer);</span><br><span class="line"> <span class="keyword">var</span> responseBodyString = buffer.<span class="title function_">readUtf8</span>();</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"HTTP Request -> "</span> + <span class="variable language_">this</span>.<span class="title function_">request</span>().<span class="title function_">url</span>().<span class="title function_">toString</span>());</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">"HTTP Response -> "</span> + responseBodyString);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> builder = response.<span class="title function_">newBuilder</span>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">var</span> newResponse = builder.<span class="title function_">body</span>(<span class="title class_">ResponseBody</span>.<span class="title function_">create</span>(responseBody.<span class="title function_">contentType</span>(), responseBodyString)).<span class="title function_">build</span>();</span><br><span class="line"> <span class="keyword">return</span> newResponse;</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>启动服务端后,在客户端(电脑)上运行</p><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"># U是USB F是当前app,这里用-n也不会重启(frida-ps查到的名字),用-f 包名 会重启app</span><br><span class="line">frida -UF test.js</span><br><span class="line"># 运行后,会进入frida,我们还需要输入具体js函数,比如get_http()</span><br><span class="line">get_http()</span><br></pre></td></tr></table></figure><h2 id="关于frida的语法,简单介绍一个常用的,剩下的上官网查。"><a href="#关于frida的语法,简单介绍一个常用的,剩下的上官网查。" class="headerlink" title="关于frida的语法,简单介绍一个常用的,剩下的上官网查。"></a>关于frida的语法,简单介绍一个常用的,剩下的上官网查。</h2><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">var Class = Java.use("class name");</span><br><span class="line">Class.method.overload("param class name").implementation = function( param) {</span><br><span class="line"> # param.xxxx</span><br><span class="line"> # console.log("");</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>大概半个月前,群里的一个小伙伴问,谁能把某电商APP的订单列表导出到电脑,有个私活,报个价。其实我对这种稍微有点了解,无非是抓包,破解。但通常App的网络请求会有一套签名验证机制,所以要想模拟出整套请求流程,也算比较复杂。和群里的讨论后,觉得用hook做合适,但群里和我一样,都是安卓开发,没有逆向工程师,首先想到的是xposed。</p>
<h1 id="xposed太重"><a href="#xposed太重" class="headerlink" title="xposed太重"></a>xposed太重</h1><p>我之前倒是写过xposed,xposed缺点有两个,第一就是环境复杂,你得安装xposed环境才可以,目前都是通过virtual xposed来使用。再就是插件一但有修改,就得重启手机。不过这也带来了一个有点就是持久化,只要装进去,就一直存在。那有没有更轻量级的工具可以达到这种效果,后来在网上找到了frida。(这也好几年了,我居然不知道)</p></summary>
<category term="android" scheme="https://www.lefo.me/categories/android/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="frida" scheme="https://www.lefo.me/tags/frida/"/>
</entry>
<entry>
<title>重学安卓之kotlin协程</title>
<link href="https://www.lefo.me/2023/08/08/kotlin-coroutine/"/>
<id>https://www.lefo.me/2023/08/08/kotlin-coroutine/</id>
<published>2023-08-07T18:03:24.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="导读"><a href="#导读" class="headerlink" title="导读"></a>导读</h1><p>因为2021年开始做C++,之前的项目还没来得及怎么用kotlin,就直接被爸爸宣布死刑了。所以目前找工作等于需要再学一次kotlin,特别是协程,之前几乎没有了解。买了本书,再加上网上的各种文章,整理一点笔记出来。如果有和我差不多的,可以看一看</p><p>协程,协程可以理解为某种自由调度的任务,主要是对函数挂起,恢复。听起来像线程的唤醒,本质上和线程没什么关系。使用协程,我们可以以单线程的方式写多线程的代码,同时还能节省线程资源,并且能方便处理异常信息,返回结果等。我们给函数前面加一个<code>suspend</code>关键字,就变成一个挂起函数,可以在函数内操作协程的挂起和恢复。其中部分内容有重复,不必理会。主要参考资料<深入理解Kotlin协程>,算是一个简单的笔记整理。</p><h1 id="协程的API"><a href="#协程的API" class="headerlink" title="协程的API"></a>协程的API</h1><h2 id="什么是挂起"><a href="#什么是挂起" class="headerlink" title="什么是挂起"></a>什么是挂起</h2><span id="more"></span><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><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">fun download(url: String) {</span><br><span class="line"> // ...</span><br><span class="line">}</span><br><span class="line">suspend fun bitmapSuspendable(url: String): Bitmap = </span><br><span class="line"> // 挂起协程 下载完成后,调用resume</span><br><span class="line"> suspendCoroutine<Bitmap> { continuation -> </span><br><span class="line"> thread {</span><br><span class="line"> try {</span><br><span class="line"> continuation.resume(download(url))</span><br><span class="line"> } catch (e: Exception) {</span><br><span class="line"> continuation.resumeWithException(e)</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">suspend fun main() {</span><br><span class="line"> try {</span><br><span class="line"> var bitmap = bitmapSuspendable("url1")</span><br><span class="line"> } catch ( e: Exception) {</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>上面的代码使用suspendCoroutine函数包装download,协程结束后,使用resume返回bitmap结果。当然,Kotlin 协程提供了更方便的库函数来处理挂起操作,不需要我们来实现。</p><ul><li><strong>suspend</strong>关键字,被suspend关键字修饰的函数叫挂起函数。suspend函数只能被协程或者suspend函数调用。</li><li><strong>suspendCoroutine<T></strong> 用于挂起协程的函数,返回值类型作为挂起函数的返回值,也就是泛型参数T的实参Bitmap,这个函数除了确实返回值类型外,还能帮我们拿到一个Continuation实例,负责保存和恢复挂起状态。</li><li><strong>resume</strong>: 恢复协程 resumeWithException:将exception携带恢复协程</li><li><strong>resumeWith</strong>: 用于恢复协程执行,并提供一个 Result 对象作为协程的结果,该对象可以包含成功的返回值或异常。</li><li><strong>resumeWithException</strong>: 恢复一个协程,并传递一个异常。一般无需显式调用,因为协程库会自动处理协程的取消和异常传播。</li></ul><h2 id="系统提供的函数"><a href="#系统提供的函数" class="headerlink" title="系统提供的函数"></a>系统提供的函数</h2><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></pre></td><td class="code"><pre><span class="line">suspend fun load(time: Int): String {</span><br><span class="line"> delay(1000)</span><br><span class="line"> return "result $time"</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">fun main = runBlocking {</span><br><span class="line"> val load1 = async { load(1) }</span><br><span class="line"> val load2 = async { load(2) }</span><br><span class="line"> // 等待获取结果</span><br><span class="line"> load1.await()</span><br><span class="line"> load2.await()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述代码中,runBlocking用来创建一个阻塞的主协程,runBlocking中的代码会阻塞当前线程,直到所有协程都执行完成,这样可以不改变代码结构的情况下使用挂起函数。还有下面这些常用的函数。通常我们使用<code>launch</code>和<code>withContext</code>多一些,具体使用在后面有,这里就不放代码了。</p><ul><li>async 启动异步协程,可以通过调用await来等待获取异步结果</li><li>launch: 启动一个新协程,新协程和当前协程并行运行。但是它不会等待新协程的完成,继续执行后续代码。该函数会返回一个Job对象,可以使用该对象来控制、取消、等待该协协程的执行。</li><li>withContext: 用于在指定的上下文中执行协程代码块,并等待代码块执行完成。与 launch 不同,withContext 挂起当前协程。常用于在协程中切换到指定的调度器上执行代码。</li><li>produce: 创建一个生产者协程,用于生成序列化的值。生产者协程可以使用 send 函数将值发送到管道中,并使用 receive 函数从管道中接收值。</li><li>actor: 创建一个带有状态的协程,用于执行并发操作并保持状态。actor 协程可以接收并处理发送到它的消息,允许在协程之间共享和交换数据。</li><li>supervisorScope: 创建一个作用域,其中的子协程的失败不会影响其他子协程。当一个子协程失败时,supervisorScope 中的其他子协程仍然可以继续执行。</li><li>repeat: 创建一个协程,重复执行指定的代码块,类似于循环。可以使用 delay 函数来控制重复执行的间隔。</li><li>select: 用于在多个挂起操作之间进行选择,类似于 select 语句。可以在一个 select 代码块中等待多个挂起函数的结果,当其中一个操作准备就绪时,select 将执行相应的代码块。</li></ul><h2 id="协程怎么创建的"><a href="#协程怎么创建的" class="headerlink" title="协程怎么创建的"></a>协程怎么创建的</h2><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">val continuation = suspend {</span><br><span class="line"> println("In Coroutine")</span><br><span class="line"> 5</span><br><span class="line">}.createCoroutine(object: Continuation<Int>) {</span><br><span class="line"> override fun resumeWith(result: Result<Int>) {</span><br><span class="line"> println("Coroutine End: $result")</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>查看createCoroutine声明</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">fun <T> (suspend () -> T).createCoroutine(</span><br><span class="line"> completion:Continuation<T></span><br><span class="line">): Continuation<Unit></span><br></pre></td></tr></table></figure><ul><li>suspend () -> T是被suspend修饰的挂起函数,可以称作是协程体。</li><li>completion是协程完成后的回调</li><li>返回值是一个Continuation,用于触发协程的启动,<strong>其实就是包了几层马甲的协程体</strong></li></ul><p>作用域 scope,可以在协程体中直接调用作用域对象的函数。挂起函数加了RestrictsSuspension注解就不能调用</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><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">fun <R, T> (suspend R.() -> T).createCoroutine(</span><br><span class="line"> receiver R,</span><br><span class="line"> completion: Continuation<T></span><br><span class="line">): Continuation<Unit></span><br><span class="line"></span><br><span class="line">// 封装一个启动协程的函数</span><br><span class="line">fun <R, T> launchCoroutine(receiver: R, block: suspend R.() -> T) {</span><br><span class="line"> block.startCoroutine(receiver, object : Continuation<T>) {</span><br><span class="line"> override fun resumeWith(result: Result<Int>) {</span><br><span class="line"> println("Coroutine End: $result")</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">// 启动带有Receiver的协程</span><br><span class="line">class ProducerScope<T> {</span><br><span class="line"> suspend fun produce(value: T) { ... }</span><br><span class="line">}</span><br><span class="line">fun callLaunchCoroutine() {</span><br><span class="line"> launchCoroutine(ProducerScope<Init>()) {</span><br><span class="line"> produce(1024)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><strong>挂起函数就是普通函数的参数中多了一个Continuation实例。任何一个挂起函数或者协程体都有一个Continuation实例,所以挂起函数一定要运行在挂起函数或者其它协程体中。</strong> </p><h2 id="协程scope的概念"><a href="#协程scope的概念" class="headerlink" title="协程scope的概念"></a>协程scope的概念</h2><p>CoroutineScope,CoroutineScope可以理解为协程的作用域,会跟踪它使用 launch 或 async 创建的所有协程。您可以随时调用 scope.cancel() 以取消正在进行的工作(即正在运行的协程)。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope。例如,ViewModel 有 viewModelScope,Lifecycle 有 lifecycleScope。不过,与调度程序不同,CoroutineScope 不运行协程。</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><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">class ExampleClass {</span><br><span class="line"></span><br><span class="line"> // Job and Dispatcher are combined into a CoroutineContext which</span><br><span class="line"> // will be discussed shortly</span><br><span class="line"> val scope = CoroutineScope(Job() + Dispatchers.Main)</span><br><span class="line"></span><br><span class="line"> fun exampleMethod() {</span><br><span class="line"> // Starts a new coroutine within the scope</span><br><span class="line"> scope.launch {</span><br><span class="line"> // New coroutine that can call suspend functions</span><br><span class="line"> fetchDocs()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> fun cleanUp() {</span><br><span class="line"> // Cancel the scope to cancel ongoing coroutines work</span><br><span class="line"> scope.cancel()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>使用launch 或 async 创建的协程会返回一个Job。</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><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">class ExampleClass {</span><br><span class="line"> ...</span><br><span class="line"> fun exampleMethod() {</span><br><span class="line"> // Handle to the coroutine, you can control its lifecycle</span><br><span class="line"> val job = scope.launch {</span><br><span class="line"> // New coroutine</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> if (...) {</span><br><span class="line"> // Cancel the coroutine started above, this doesn't affect the scope</span><br><span class="line"> // this coroutine was launched in</span><br><span class="line"> job.cancel()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Job有一个函数叫<code>join()</code>,它会阻塞当前协程,直到目标协程执行完成为止。</p><h1 id="Android中的协程"><a href="#Android中的协程" class="headerlink" title="Android中的协程"></a>Android中的协程</h1><h2 id="常用启动协程的方式"><a href="#常用启动协程的方式" class="headerlink" title="常用启动协程的方式"></a>常用启动协程的方式</h2><ul><li>launch 启动新协和而不将结果返回调用方。</li><li>async 启动新协程,并允许使用一个名为await的挂起函数返回结果。</li><li>withContext 挂起当前协程,本身就是一个挂起函数,作用等价于async{}.await()</li></ul><p>通常应该用launch,只有另一个协程内时,或在挂起函数内且正在执行并行分解时,才使用 async。在suspend函数启动的所有协程,都必须在函数返回结果前停止。可以使用await()/awaitAll()保证协程在函数返回结果前完成。</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><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">suspend fun fetchTwoDocs() =</span><br><span class="line"> coroutineScope {</span><br><span class="line"> val deferredOne = async { fetchDoc(1) }</span><br><span class="line"> val deferredTwo = async { fetchDoc(2) }</span><br><span class="line"> deferredOne.await()</span><br><span class="line"> deferredTwo.await()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)</span><br><span class="line"> coroutineScope {</span><br><span class="line"> val deferreds = listOf( // fetch two docs at the same time</span><br><span class="line"> async { fetchDoc(1) }, // async returns a result for the first doc</span><br><span class="line"> async { fetchDoc(2) } // async returns a result for the second doc</span><br><span class="line"> )</span><br><span class="line"> deferreds.awaitAll() // use awaitAll to wait for both network requests</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h2 id="调度器"><a href="#调度器" class="headerlink" title="调度器"></a>调度器</h2><ol><li>Dispatchers.Default: 用于执行 CPU 密集型的计算任务,比如对数据进行处理、转换等。它使用共享的线程池,并且适用于不会阻塞线程的操作。</li><li>Dispatchers.Main: 用于在主线程中执行操作,比如更新 UI、处理用户交互等。在 Android 中,它与主线程关联,因此适用于 UI 操作。</li><li>Dispatchers.Unconfined: 不受限制的调度器,它会在协程恢复执行时,继续使用调用者线程。但是,一旦恢复执行的代码开始执行挂起操作,调度器会切换到其他线程。<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">button.setOnclickListener {</span><br><span class="line"> GlobalScope.launch(Dispatchers.Main) {</span><br><span class="line"> // ..</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>另外还有一个<code>MainScop()</code>函数,注意要在关闭的时候调用cancel<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></pre></td><td class="code"><pre><span class="line">//activity</span><br><span class="line">private val mainScope by lazy { MainScop() }</span><br><span class="line"></span><br><span class="line">button.setOnClickListener {</span><br><span class="line"> mainScope.launch {</span><br><span class="line"> // ui线程</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">// onDestory 注意要调用cancel</span><br><span class="line">mainScope.cancel()</span><br><span class="line"></span><br></pre></td></tr></table></figure></li></ol><ul><li>lifecycleScope.launch {}</li><li>viewModelScope.launch {}</li></ul><h2 id="常见函数"><a href="#常见函数" class="headerlink" title="常见函数"></a>常见函数</h2><ul><li>withTimeout(1000) : 超时取消</li></ul><h2 id="实用技巧"><a href="#实用技巧" class="headerlink" title="实用技巧"></a>实用技巧</h2><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">// </span><br><span class="line">suspend fun a() {</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line">scope.launch(Dispatchers.IO) { </span><br><span class="line"> //...</span><br><span class="line"> a()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上方的代码存在的问题是,因为要想调用a(),每次必须显式在子线程中调用。可以将a改进,使用withContext封装</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></pre></td><td class="code"><pre><span class="line">// 这种方式可以确保a的调用者不用关注在哪个线程中执行</span><br><span class="line">supsend fun a() : Result {</span><br><span class="line"> return withContext(Dispatchers.IO) {</span><br><span class="line"> //...</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>改进后,要想调用a,必须使用协程。这种方式可以确保a的调用者不用关注在哪个线程中执行。</p><h1 id="Channel-Flow"><a href="#Channel-Flow" class="headerlink" title="Channel & Flow"></a>Channel & Flow</h1><h2 id="Channel-(热流)"><a href="#Channel-(热流)" class="headerlink" title="Channel (热流)"></a>Channel (热流)</h2><p>Channel相当于生产者消费者,<strong>先生产再消费</strong>,在协程中间建立一个缓存区。produce函数可以构建一个生产者协程,也可以在协程中只创建Channel。推荐用produce channel,不要用actor(obsolete)</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></pre></td><td class="code"><pre><span class="line">// 相关代码</span><br><span class="line">val channel = produce {</span><br><span class="line"> //...</span><br><span class="line"> send(it)</span><br><span class="line">}</span><br><span class="line">val channel = actor<Int> {</span><br><span class="line"> //...</span><br><span class="line"> val element = receiver()</span><br><span class="line">}</span><br><span class="line">channel.send()</span><br><span class="line">channel.receive()</span><br></pre></td></tr></table></figure><p>Channel的内部实现</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></pre></td><td class="code"><pre><span class="line">public fun <E> Channel(capacity: Int = RENDEZVOUS): Channel<E> =</span><br><span class="line"> when (capcity) {</span><br><span class="line"> // 容量为1,send后,不调用receiver就一直挂起</span><br><span class="line"> RENDEZVOUS -> RendezvousChannel()</span><br><span class="line"> // 容量无限</span><br><span class="line"> UNLIMITED -> LinkedListChannel()</span><br><span class="line"> // 只保留最后一个</span><br><span class="line"> CONFLATED -> ConflatedChannel()</span><br><span class="line"> // 指定大小</span><br><span class="line"> else -> ArrayChannel(capacity)</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>Channel是需要关闭的,produce方法会在协程结束后自去关闭对应的_channel,所以不用担心。需要注意的是,<strong>一旦调用close,它的isClosoedForSend方法会立即返回true,但是因为里面还有元素,isClosedForReceiver要等全处理完才返回true</strong></p><p><strong>BroadcastChannel</strong> 顾名思义,广播所以receiver都会收到。需要注意,必须有一个订阅者,不然数据会被丢弃。</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">val broadcastChannel = BroadcastChannel<Int>(5)</span><br><span class="line">val receiverChannel = broadcastChannel.openSubscription()</span><br><span class="line">receiverChannle.receiver()</span><br><span class="line">receiverChannle.cancel()</span><br></pre></td></tr></table></figure><p>普通Channel也可以通过调用<code>broadcast()</code>函数来转换成broadcast</p><h2 id="Flow-(冷流)"><a href="#Flow-(冷流)" class="headerlink" title="Flow (冷流)"></a>Flow (冷流)</h2><p>flow <strong>不消费不生产</strong>,不会立即执行flow的内容,会在collect的时候,才去执行。消费者协程cancel()后,flow会跟着结束(没有缓冲队列,区别channel)</p><p>flow的末端函数都是suspend函数</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></pre></td><td class="code"><pre><span class="line">val flow = flow {</span><br><span class="line"> val data = "DATA"</span><br><span class="line"> emit(data)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">launch {</span><br><span class="line"> flow.collect { data -></span><br><span class="line"> //...</span><br><span class="line"> cancel()</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>不同的消费者协程不共享flow,如下列代码,flow中的代码会被多次执行。(每个消费者拥有独立的生产者)</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></pre></td><td class="code"><pre><span class="line">repeat(2) {ci -></span><br><span class="line"> launch {</span><br><span class="line"> flow.collect { data -></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><ul><li>如果在不同线程操作,可以使用flow.flowOn(Dispatchers.Main)进行上下文切换。</li><li>如果要在Flow完成时执行逻辑,可以使用onCompletion,相当于try catch的finally</li></ul><p>** 背压问题 ** 生产者生产太快,消费者跟不上。解决:</p><ol><li>调用flow.buffer() 加入缓存</li><li>调用flow.conflate() 新数据替换旧数据</li><li>调用使用flow.conllectLatest() 和 conllect的区别是,它不会直接使用新数据覆盖老数据,会依序处理,但如果前一个没处理完的话会被取消。</li></ol><h2 id="channelFlow"><a href="#channelFlow" class="headerlink" title="channelFlow"></a>channelFlow</h2><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></pre></td><td class="code"><pre><span class="line">channelFlow,使flow具体channel的特性</span><br><span class="line">val flow = channelFlow {</span><br><span class="line"> withContext(Dispatcher.IO) {</span><br><span class="line"> send("DATA")</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="callbackFlow"><a href="#callbackFlow" class="headerlink" title="callbackFlow"></a>callbackFlow</h2><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">val result = trySend("DATA")</span><br><span class="line">result.onSuccess {</span><br><span class="line"></span><br><span class="line">}.onFailure {</span><br><span class="line"></span><br><span class="line">}.onClosed {</span><br><span class="line"></span><br><span class="line">}</span><br><span class="line">协程cancel后,会调用awaitClose</span><br><span class="line">awaitClose {</span><br><span class="line"> //...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">launch {</span><br><span class="line"> flow.collect {</span><br><span class="line"> //...</span><br><span class="line"> cancel()</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><table><thead><tr><th>项目</th><th>冷流</th><th>热流</th></tr></thead><tbody><tr><td>生产时间机</td><td>一旦创建立即生产</td><td>需要时生产</td></tr><tr><td>多个消费者</td><td>消费者按顺序获取</td><td>每个消费者有独立的生产线路</td></tr><tr><td>生命周期</td><td>生产者消费者无关系</td><td>生产者消费者生命周期一致</td></tr></tbody></table><h2 id="SharedFlow-StateFlow-热流"><a href="#SharedFlow-StateFlow-热流" class="headerlink" title="SharedFlow & StateFlow 热流"></a>SharedFlow & StateFlow 热流</h2><p>flow.ShareIn()方法,传递3个参数:scope协程域<br>flow.StateIn()</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></pre></td><td class="code"><pre><span class="line">flow {...}.stateIn(scope)</span><br><span class="line">//类似</span><br><span class="line">flow {...}.shareIn(scope,</span><br><span class="line"> stared = SharingStarted.Eagerly,</span><br><span class="line"> replay = 1</span><br><span class="line">)</span><br></pre></td></tr></table></figure><p>stateFlow要和repeatOnLifecycle一起使用防止热流在view的无效生命周期更新</p><h1 id="协程并发问题"><a href="#协程并发问题" class="headerlink" title="协程并发问题"></a>协程并发问题</h1><h2 id="Mutex"><a href="#Mutex" class="headerlink" title="Mutex"></a>Mutex</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><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">var count = 0</span><br><span class="line">val mutex = mutex()</span><br><span class="line">List(1000) {</span><br><span class="line"> GlobalScope.launch {</span><br><span class="line"> mutext.withLock {</span><br><span class="line"> count ++</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}.joinAll()</span><br></pre></td></tr></table></figure><h2 id="Semaphore"><a href="#Semaphore" class="headerlink" title="Semaphore"></a>Semaphore</h2><p>信号量,信号量可以有多个,当参数为1时,效果等价于Mutex</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">val semaphore = Semaphore(1)</span><br><span class="line">List(1000) {</span><br><span class="line"> GlobalScopre.launch {</span><br><span class="line"> semaphore.withPermit {</span><br><span class="line"> count++</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}.joinAll()</span><br></pre></td></tr></table></figure><h2 id="注意"><a href="#注意" class="headerlink" title="注意"></a>注意</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><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">val count = 0</span><br><span class="line">val result = count + List(1000) {</span><br><span class="line"> GlobalScope.async {1}</span><br><span class="line">}.map {</span><br><span class="line"> it.await()</span><br><span class="line">}.sum()</span><br></pre></td></tr></table></figure><p> GlobalScope.async 创建了一个由 1000 个协程组成的集合,每个协程都会返回整数值 1。然后通过 map 函数等待所有协程的执行结果,并将结果求和。最终的结果会赋值给变量 result。</p>]]></content>
<summary type="html"><h1 id="导读"><a href="#导读" class="headerlink" title="导读"></a>导读</h1><p>因为2021年开始做C++,之前的项目还没来得及怎么用kotlin,就直接被爸爸宣布死刑了。所以目前找工作等于需要再学一次kotlin,特别是协程,之前几乎没有了解。买了本书,再加上网上的各种文章,整理一点笔记出来。如果有和我差不多的,可以看一看</p>
<p>协程,协程可以理解为某种自由调度的任务,主要是对函数挂起,恢复。听起来像线程的唤醒,本质上和线程没什么关系。使用协程,我们可以以单线程的方式写多线程的代码,同时还能节省线程资源,并且能方便处理异常信息,返回结果等。我们给函数前面加一个<code>suspend</code>关键字,就变成一个挂起函数,可以在函数内操作协程的挂起和恢复。其中部分内容有重复,不必理会。主要参考资料&lt;深入理解Kotlin协程&gt;,算是一个简单的笔记整理。</p>
<h1 id="协程的API"><a href="#协程的API" class="headerlink" title="协程的API"></a>协程的API</h1><h2 id="什么是挂起"><a href="#什么是挂起" class="headerlink" title="什么是挂起"></a>什么是挂起</h2></summary>
<category term="kotlin" scheme="https://www.lefo.me/categories/kotlin/"/>
<category term="重学android" scheme="https://www.lefo.me/tags/%E9%87%8D%E5%AD%A6android/"/>
<category term="kotlin" scheme="https://www.lefo.me/tags/kotlin/"/>
</entry>
<entry>
<title>C++学习笔记</title>
<link href="https://www.lefo.me/2021/08/13/cpp-note/"/>
<id>https://www.lefo.me/2021/08/13/cpp-note/</id>
<published>2021-08-13T05:28:05.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="第2节"><a href="#第2节" class="headerlink" title="第2节"></a>第2节</h1><p>防卫式声明,防止重复include引起的问题(guard)</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">//complex.h</span><br><span class="line">#ifndef _COMPLEX_</span><br><span class="line">#def _COMPLEX_</span><br><span class="line">...</span><br><span class="line">#endif</span><br></pre></td></tr></table></figure><span id="more"></span><h1 id="第3节"><a href="#第3节" class="headerlink" title="第3节"></a>第3节</h1><p>inline 内联函数</p><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><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">public:</span><br><span class="line"> complex(double r = 0,double i = 0)</span><br><span class="line"> :re(r),im(i)</span><br><span class="line"> //上面的默认值是推荐用法</span><br><span class="line"> {}</span><br><span class="line">...</span><br><span class="line">complex c1(2,1)</span><br><span class="line">complex c2</span><br><span class="line">complex* c3 = new complex(4)</span><br></pre></td></tr></table></figure><h1 id="第4节"><a href="#第4节" class="headerlink" title="第4节"></a>第4节</h1><p>数据是private</p><p>参数、返回值尽可能拿引用传递:除了返回值需要新创建对象的情况</p><p>能加const的函数尽量加const</p><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">type function () const { return result}</span><br></pre></td></tr></table></figure><h1 id="第6节"><a href="#第6节" class="headerlink" title="第6节"></a>第6节</h1><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></pre></td><td class="code"><pre><span class="line">complex::operator += (const complex& r){</span><br><span class="line">return </span><br><span class="line">}</span><br></pre></td></tr></table></figure><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">//ostream不加const </span><br><span class="line">ostream& operator << (ostream& os, const complex& x)</span><br><span class="line">{</span><br><span class="line">return os << '(' << real(x) << ',' << imag(x) << ')';</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="第7节"><a href="#第7节" class="headerlink" title="第7节"></a>第7节</h1><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></pre></td><td class="code"><pre><span class="line">class String</span><br><span class="line">{</span><br><span class="line">public:</span><br><span class="line">String(const char* cstr = 0);</span><br><span class="line">String(const String& str);</span><br><span class="line">String& operator =(const String& str);</span><br><span class="line">~String()</span><br><span class="line">char* get_c_str() const {return mdata};</span><br><span class="line"></span><br><span class="line">private:</span><br><span class="line">char* m_data;</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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">inline String::String(const String& str)</span><br><span class="line">{</span><br><span class="line">m_data = new char[ strlen(str.m_data) + 1];</span><br><span class="line">strcpy(m_data,str.m_data);</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><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">inline String::operator = (const string& str)</span><br><span class="line">{</span><br><span class="line">//不使用这个自我赋值判断,会刹掉自己</span><br><span class="line">if(this == &str)</span><br><span class="line">retrun *this</span><br><span class="line"> delete[] m_data;</span><br><span class="line">m_data = new char[ strlen(str.m_data) + 1];</span><br><span class="line">strcpy(m_data,str.m_data)</span><br><span class="line">return *this;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="第8节"><a href="#第8节" class="headerlink" title="第8节"></a>第8节</h1><p>new :先分配内存,再调用ctor</p><p>delete:先调用析构函数,再释放内存</p><p>array new 要搭配 array delete使用</p><h1 id="第15-16-17-18节"><a href="#第15-16-17-18节" class="headerlink" title="第15 16 17 18节"></a>第15 16 17 18节</h1><p>转换函数 ,构造方法也可以当转换函数转,比如4转为4/1</p><p>explicit :加在构造方法前面,表示必须得显示调用</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></pre></td><td class="code"><pre><span class="line">class Fraction</span><br><span class="line">{</span><br><span class="line">public:</span><br><span class="line"> Fraction(int num,int den=1)</span><br><span class="line"> : m_numberator(m), m_denominato {}</span><br><span class="line"> operator double() const{</span><br><span class="line"> return (double)(a/b)</span><br><span class="line"> }</span><br><span class="line">...</span><br><span class="line">Fraction f(3,5)</span><br><span class="line">double d=4+5</span><br></pre></td></tr></table></figure><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">Fraction operator+(const Fraction& f){</span><br><span class="line">return Fraction(...);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>pointer like classes 智能指针</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><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">template<class T></span><br><span class="line">class shared_ptr</span><br><span class="line">{</span><br><span class="line">public:</span><br><span class="line">T& operator*() const</span><br><span class="line">{ return *px; }</span><br><span class="line">T* operator->() const</span><br><span class="line">{ return px; }</span><br><span class="line"></span><br><span class="line">shared_ptr(T* P) :px(p){}</span><br><span class="line">private:</span><br><span class="line">T* px;</span><br><span class="line">long* pn;</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Function-like classes</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">template <class T></span><br><span class="line">struct identity{</span><br><span class="line">const T&</span><br><span class="line">operator()(const T& x) const {return x;}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="第19-25节"><a href="#第19-25节" class="headerlink" title="第19 - 25节"></a>第19 - 25节</h1><p>namespace 可以代码放一个文件进行,然后用namespace分割</p><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><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">template <class T1,class T2></span><br><span class="line">struct pair{</span><br><span class="line">T1 first;</span><br><span class="line">T2 second;</span><br><span class="line">pair():first(T1()),second(T2);</span><br><span class="line">pair(const T1& a,const T2& b):</span><br><span class="line"> first(a),second(b){}</span><br><span class="line"></span><br><span class="line">template <class U1,class U2></span><br><span class="line">pair(const pair<U1,U2>& p):</span><br><span class="line"> first(p.first),second(p.second){}</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><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">template <></span><br><span class="line">struct hash<char>{</span><br><span class="line">size_t operator()(char x) const{</span><br><span class="line">return x;</span><br><span class="line">}</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><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">template<typename T,</span><br><span class="line"> template <typename T>></span><br><span class="line"> class Container</span><br><span class="line"> ></span><br><span class="line">class XCLs{</span><br><span class="line">private:</span><br><span class="line"> Container<T>c;</span><br><span class="line">public:</span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">template<typename T></span><br><span class="line">using Lst = list<T, allocator<T>>;</span><br><span class="line"></span><br><span class="line">//XCLs<string, list> mylst1; 错的</span><br><span class="line">XCLs<string,Lst> mylst2;</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><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">template<class T,class Alloc=...></span><br><span class="line">class vector<bool,Alloc>{</span><br><span class="line">...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">template <typename T></span><br><span class="line">//指针</span><br><span class="line">class C<T*>{</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="第27节"><a href="#第27节" class="headerlink" title="第27节"></a>第27节</h1><p>auto 关键字 必须得让编译器能推出你的变量是什么</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">list<string> c;</span><br><span class="line">...</span><br><span class="line">list<string>::iterator ite;</span><br><span class="line">ite = find(c.begin(),c.end,target);</span><br><span class="line">auto ite = find(c.begin(),c.end,target);</span><br></pre></td></tr></table></figure><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">for (auto eleme : vec){</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">for (auto& eleme : vec){</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="第28节"><a href="#第28节" class="headerlink" title="第28节"></a>第28节</h1><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></pre></td><td class="code"><pre><span class="line">int x = 0;</span><br><span class="line">int* p = &x;</span><br><span class="line">int& r = x;//r代表x 现在r, x 都是0</span><br><span class="line">int x2 = 5;</span><br><span class="line">//等于把5赋值给x</span><br><span class="line">r = x2;//r 不能重新代表其它物体,现在r x都是5</span><br><span class="line">int& r2 = r;//r2 x 都是5</span><br></pre></td></tr></table></figure><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">double image(const double& im){...}</span><br><span class="line">double image(const double im){...}</span><br><span class="line">//不能共存 Ambiguity</span><br></pre></td></tr></table></figure><p>const属于签名的一部分,有const和没有const可以并存</p><h1 id="第30-31节"><a href="#第30-31节" class="headerlink" title="第30-31节"></a>第30-31节</h1><p>vptr和vtbl</p><p>先通过指针 找到函数表再找到第n个 再调用</p><p>(*(p->vptr)[n])(p);</p><p>(* p->vptr[n] )(p);</p><p>编译器三个条件就会动态绑定</p><p>1.指针调用 2.向上转型 3.虚函数</p><p>(*(this -> vptr)[n])(this);</p><h1 id="第33-38节"><a href="#第33-38节" class="headerlink" title="第33-38节"></a>第33-38节</h1><p>当成员函数的const和 non-const版本同时存在,const object只会调用const版本,non-const object只会调用non-const版本</p><p>new 和 delete可以重载</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">inline void* operator new(size_t size)</span><br><span class="line">{ return myAlloc(size);}</span><br><span class="line">inline void operator delete(void* ptr)</span><br><span class="line">{ myfree(ptr);}</span><br></pre></td></tr></table></figure><p>可以对成员的new /delete 做重载,写到类里</p><p>如果想使用globals</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">Foo* pf = ::new Foo;</span><br><span class="line">::delete pf</span><br><span class="line">//将使用全局的new 和delete</span><br></pre></td></tr></table></figure><p>数组在内存中的size是countsize + unit * count </p><p>我们可以重载class member operator new(),写多个版本,每个声明必须有独特的参数列,且第一个必须是size_t,其余参数是new指定的placement arguments为初值。小括号内的便是placement arguments</p><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">Foo* pf = new(300,'c') Foo;</span><br></pre></td></tr></table></figure><p>可以重载class member operator delete(),写出多个版本,调不到,大师说老版本的能调到,在new异常的时候</p><p>operator new()在标准库的用处,多分配一些内存</p>]]></content>
<summary type="html"><h1 id="第2节"><a href="#第2节" class="headerlink" title="第2节"></a>第2节</h1><p>防卫式声明,防止重复include引起的问题(guard)</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">//complex.h</span><br><span class="line">#ifndef _COMPLEX_</span><br><span class="line">#def _COMPLEX_</span><br><span class="line">...</span><br><span class="line">#endif</span><br></pre></td></tr></table></figure></summary>
<category term="c++" scheme="https://www.lefo.me/categories/c/"/>
<category term="c++" scheme="https://www.lefo.me/tags/c/"/>
</entry>
<entry>
<title>记一次编译安卓模拟器镜像</title>
<link href="https://www.lefo.me/2021/08/11/make-image/"/>
<id>https://www.lefo.me/2021/08/11/make-image/</id>
<published>2021-08-11T13:49:03.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>想要编译一个自己的framework,用来在系统api中插入自己的代码,监听一下应用的行为,比如获取imei,获取网络。最直接的想法就是,修改安卓源码,直接编译出一个系统镜像,给模拟器使用。</p><p>准备:</p><ol><li><p>因为之前编译过android.jar,所以源码环境还有,直接开工,或者看以前编译的文章有讲。</p></li><li><p>修改代码,比如找到TelephonyManager.java,在getDeviceId()中插入自己的代码。</p></li></ol><span id="more"></span><h1 id="编译"><a href="#编译" class="headerlink" title="编译"></a>编译</h1><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">source ./build/envsetup.sh</span><br><span class="line">lunch sdk_phone_x86-userdebug</span><br><span class="line">make update-api</span><br><span class="line">make -j16 sdk sdk_repo</span><br></pre></td></tr></table></figure><p><em>注意:下面的部分是官方写的,如何编译一个sdk给别人也能用。但我没有成功,make sdk_repo的时候,失败了</em></p><p>在<code>host/linux-x86/sdk/sdk_phone_x86</code>下找到两个文件</p><ul><li><code>sdk-repo-linux-system-images-eng.[username].zip</code></li><li><code>repo-sys-img.xml</code></li></ul><p>将这两个文件放到你服务器可访问的位置,记下url。再将xml中的<code><sdk:url></code>替换成zip的url。</p><p>在SDK Manager中,点击 <strong>SDK Update Sites</strong> 添加你的xml的url。</p><p><em>我的操作:直接将zip下载下来解压,替换原来sdk对应的system-image,如<code>/sdk/system-images/android-23/default/x86</code></em></p><h2 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h2><p>如果没有修改代码,是不需要make update-api的,但是有代码修改后,make过程中,会提示你使用make update-api。我在make sdk的过程中,还提示要make update-support-api(不大记得是update-support-api还是support-update-api),有需要系统会提示的,根据系统提示操作。</p><h1 id="运行"><a href="#运行" class="headerlink" title="运行"></a>运行</h1><ol><li>缺少kernel-ranchu,复制一个解决</li><li>开机黑屏,AVD Manager下,点击编辑,Emulated Performance - Graphics:改为SoftWare(尚不清楚其它解决方案)。关于这个黑屏,尝试好多次,启动后也有日志提示<code>EGL_emulation: tid 1232: eglCreateImageKHR(1206): error 0x300c E/GLConsumer: error creating EGLImage: 0x300c</code>,我一直以为是镜像的问题,试了几个版本,一直不行,最后几经搜索才找到改Graphics的方案,很是郁闷。</li></ol><h1 id="记录"><a href="#记录" class="headerlink" title="记录"></a>记录</h1><ol><li><p>error: ro.build.fingerprint cannot exceed 91 bytes:</p><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">error: ro.build.fingerprint cannot exceed 91 bytes:</span><br></pre></td></tr></table></figure><p>编辑<code>build/tools/post_process_props.py</code>。更改行,如下所示:</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">PROP_NAME_MAX = 31</span><br><span class="line"># PROP_VALUE_MAX = 91</span><br><span class="line">PROP_VALUE_MAX = 128</span><br></pre></td></tr></table></figure><p>编辑<code>bionic/libc/include/sys/system_properties.h</code>。更改行,如下所示:</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">#define PROP_NAME_MAX 32</span><br><span class="line">// #define PROP_VALUE_MAX 92</span><br><span class="line">#define PROP_VALUE_MAX 128</span><br></pre></td></tr></table></figure><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">make clobber</span><br><span class="line">make sdk sdk_repo</span><br></pre></td></tr></table></figure></li><li><p>jack服务错误,因为我是在6.0的源码上编译,还是使用的jack,报了两次服务没启动后,我就直接关了,修改为下面的SERVER=false</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">vi ~/.jack</span><br><span class="line"></span><br><span class="line"># Server settings</span><br><span class="line">SERVER=false</span><br><span class="line">SERVER_PORT_SERVICE=8072</span><br><span class="line">SERVER_PORT_ADMIN=8073</span><br><span class="line">SERVER_COUNT=1</span><br><span class="line">SERVER_NB_COMPILE=4</span><br><span class="line">SERVER_TIMEOUT=60</span><br><span class="line">SERVER_LOG=${SERVER_LOG:=$SERVER_DIR/jack-$SERVER_PORT_SERVICE.log}</span><br><span class="line">JACK_VM_COMMAND=${JACK_VM_COMMAND:=java}</span><br><span class="line"># Internal, do not touch</span><br><span class="line">SETTING_VERSION=2</span><br></pre></td></tr></table></figure></li><li><p>make sdk_repo错误,生成的xml中值校验不通过。</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><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">Building SDK repository package system-images from android-sdk_eng.root_linux-x86.zip</span><br><span class="line">## Using xmlns:sdk=http://schemas.android.com/sdk/android/addon/2</span><br><span class="line">## Using root element sdk-addon</span><br><span class="line">## Add support/linux archive out/host/linux-x86/sdk/sdk_phone_x86//sdk-repo-linux-support-eng.root.zip</span><br><span class="line">## Validate XML against schema</span><br><span class="line"><?xml version="1.0"?></span><br><span class="line"><sdk:sdk-addon xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sdk="http://schemas.android.com/sdk/android/addon/2"></span><br><span class="line"> <sdk:extra></span><br><span class="line"> <sdk:revision>23.0.1</sdk:revision></span><br><span class="line"> <sdk:vendor>android</sdk:vendor></span><br><span class="line"> <sdk:path>support</sdk:path></span><br><span class="line"> <sdk:archives></span><br><span class="line"> <sdk:archive os="linux"></span><br><span class="line"> <sdk:size>5504615</sdk:size></span><br><span class="line"> <sdk:checksum type="sha1">d7d984cfe9288f2cd32bb1601a138de32724faa2</sdk:checksum></span><br><span class="line"> <sdk:url>sdk-repo-linux-support-eng.root.zip</sdk:url></span><br><span class="line"> </sdk:archive></span><br><span class="line"> </sdk:archives></span><br><span class="line"> </sdk:extra></span><br><span class="line"></sdk:sdk-addon></span><br><span class="line">out/host/linux-x86/sdk/sdk_phone_x86/repo-extras.xml:6: element revision: Schemas validity error : Element '{http://schemas.android.com/sdk/android/addon/2}revision': '23.0.1' is not a valid value of the atomic type 'xs:positiveInteger'.</span><br><span class="line">out/host/linux-x86/sdk/sdk_phone_x86/repo-extras.xml fails to validate</span><br><span class="line">make: *** [out/host/linux-x86/sdk/sdk_phone_x86/repo-extras.xml] 错误 3</span><br><span class="line">make: *** 正在删除文件“out/host/linux-x86/sdk/sdk_phone_x86/repo-extras.xml”</span><br></pre></td></tr></table></figure><p>应该是xml中revision不是整数的问题。这个我就很郁闷,明明xml是自己生成的,偏偏校验不通过,make文件在<code>development/build/tools/mk_sdk_repo_xml.sh</code>里,xsd的文件在<code>prebuilts/devtools/repository</code>找到了,后续我也没改,直接把zip拿来用了。有兴趣的可以自己试试。</p></li></ol>]]></content>
<summary type="html"><h1 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h1><p>想要编译一个自己的framework,用来在系统api中插入自己的代码,监听一下应用的行为,比如获取imei,获取网络。最直接的想法就是,修改安卓源码,直接编译出一个系统镜像,给模拟器使用。</p>
<p>准备:</p>
<ol>
<li><p>因为之前编译过android.jar,所以源码环境还有,直接开工,或者看以前编译的文章有讲。</p>
</li>
<li><p>修改代码,比如找到TelephonyManager.java,在getDeviceId()中插入自己的代码。</p>
</li>
</ol></summary>
<category term="android" scheme="https://www.lefo.me/categories/android/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
</entry>
<entry>
<title>使用jenkins为android工程打包,支持多包名,改资源(优化方案)</title>
<link href="https://www.lefo.me/2020/09/04/jenkins-android-3/"/>
<id>https://www.lefo.me/2020/09/04/jenkins-android-3/</id>
<published>2020-09-04T07:32:15.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>没有需求,自己看了一眼自己之前写的打包脚本,简直无法看下去。而且,产品经理的定制化需求越来越多,用shell脚本的可读性也越来越差,再加上里面一堆的sed命令,惨不忍睹。</p><p>改!!!</p><h1 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h1><p>gradle其实支持自定义参数,关于自定义参数的介绍,参考官方文档:<a href="https://docs.gradle.org/current/userguide/build_environment.html%EF%BC%8C%E7%AE%80%E5%8D%95%E8%AF%B4%E4%B8%80%E4%B8%8B%E7%94%A8%E5%88%B0%E7%9A%84%EF%BC%9A[Gradle">https://docs.gradle.org/current/userguide/build_environment.html,简单说一下用到的:[Gradle</a> properties](<a href="https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties)%EF%BC%9A">https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties):</a></p><span id="more"></span><p>有两种方式传给gradle,一种是-P。</p><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">gradlew clean -Pname=zhangsan</span><br></pre></td></tr></table></figure><p>另一种是gradle.properties,我们可以在build.gradle文件所在的目录下创建一个gradle.properties文件,写上</p><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">name=zhangsan</span><br></pre></td></tr></table></figure><p>通过这两种方式,在gradle脚本中就可以直接使用这个变量名。</p><p>比如如下的gradle配置,其中包含了int型,String型,boolean型,基本能满足需求了。</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">buildConfigField "boolean", "SHOW_INFO", "$SHOW_INFO"</span><br><span class="line">applicationId "$PACKAGE_NAME"</span><br><span class="line"></span><br><span class="line">versionCode Integer.valueOf(VERSION_CODE)</span><br><span class="line">versionName VERSION_NAME</span><br><span class="line"></span><br><span class="line">buildConfigField "String", "QQ_ID", "\"$qq_id\""</span><br><span class="line">buildConfigField "String", "WX_RELEASE_ID", "\"$wx_id\""</span><br></pre></td></tr></table></figure><p>对应的gradle.properties文件</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></pre></td><td class="code"><pre><span class="line">VERSION_NAME=1.2</span><br><span class="line">VERSION_CODE=1110</span><br><span class="line">PACKAGE_NAME=me.lefo.jenkins</span><br><span class="line">SHOW_INFO=true</span><br><span class="line">qq_id=1001</span><br><span class="line">wx_id=1002</span><br></pre></td></tr></table></figure><h1 id="结合jenkins的参数化构建"><a href="#结合jenkins的参数化构建" class="headerlink" title="结合jenkins的参数化构建"></a>结合jenkins的参数化构建</h1><p>通常用jenkins在构建的时候,都会自定义一些参数,比如上文的VERSION_NAME,VERSION_CODE。当然,我们还有个奇葩的需求,改包名。</p><p>jenkins在搭建的时候,构建那一步一定要选<code>invoke gradle script</code>,点开下面的高级选项,勾选<code>Pass all job parameters as Project properties</code>,勾选这一项会将jenkins参数化构建时的参数写到gradle中,还会替换掉gradle.properties中的默认值。其实就是通过-P把参数传到了gradle,-P传入的优先级高于properties文件。</p><p><strong>一定要保证参数名和gradle.properties文件中的名字一样!!!</strong></p><p><strong>一定要保证参数名和gradle.properties文件中的名字一样!!!</strong></p><p><strong>一定要保证参数名和gradle.properties文件中的名字一样!!!</strong></p><h1 id="和包名绑定的配置"><a href="#和包名绑定的配置" class="headerlink" title="和包名绑定的配置"></a>和包名绑定的配置</h1><p>我们有个改包名的需求,有些配置化的值,是要跟着包名变动的,比如第三方平台的id。怎么办呢?这里的做法是,不同包名,建立不同的properties文件,执行时,使用cp -f命令,替换掉gradle.properties文件。</p><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">cp -f $PACKAGE_NAME.properties gradle.properties</span><br></pre></td></tr></table></figure><p>像一些构建时jenkins传入的参数,可以不在<code>包名.properties</code>里出现,但是,默认的gradle.properties文件必须有这些值,在as run的时候,还是得在gradle.properties中有个默认值的。但jenkins选了<code>invoke gradle script</code>,这些参数会通过-P传进去。</p><h1 id="manifest文件"><a href="#manifest文件" class="headerlink" title="manifest文件"></a>manifest文件</h1><p>manifest文件中,也会配置一些值,可以通过placeHolder传进去。比如channel</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">defaultConfig {</span><br><span class="line">manifestPlaceholders = [label:"@string/app_name",</span><br><span class="line"> channel:"$CHANNEL"]</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>在manifest中可以通过${channel}来使用,manifest中的authorities也可以通过这种方式来处理。</p><h1 id="改应用名"><a href="#改应用名" class="headerlink" title="改应用名"></a>改应用名</h1><p>我们有两个需求,一个是改用户界面显示的应用名,另一个是改APP内部显示的应用名,这两个有可能不一样。这里当然是通过sed改的,貌似也没有别的办法,上一段中的label也是用于这个功能的。我们的做法是把包含应用名关键字的strings.xml单独提取到一个value资源文件,然后通过sed统一修改。</p><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">sed -i 's/默认应用名/'"$APP_NAME"'/g' myflavor/res/values/stringfile.xml</span><br></pre></td></tr></table></figure><h1 id="完成"><a href="#完成" class="headerlink" title="完成"></a>完成</h1><p>通过这波修改,之前的一坨sh文件中,就只剩下修改应用名的脚本了,其它的都通过gradle的环境变量来支持了。后续如果要加配置项,只需要在gradle.properties中加个默认项,如果是跟包名的,就放在包名.properties中,如果是jenkins配置的,就按对应的名称配在jenkins就OK。</p>]]></content>
<summary type="html"><h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>没有需求,自己看了一眼自己之前写的打包脚本,简直无法看下去。而且,产品经理的定制化需求越来越多,用shell脚本的可读性也越来越差,再加上里面一堆的sed命令,惨不忍睹。</p>
<p>改!!!</p>
<h1 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h1><p>gradle其实支持自定义参数,关于自定义参数的介绍,参考官方文档:<a href="https://docs.gradle.org/current/userguide/build_environment.html%EF%BC%8C%E7%AE%80%E5%8D%95%E8%AF%B4%E4%B8%80%E4%B8%8B%E7%94%A8%E5%88%B0%E7%9A%84%EF%BC%9A[Gradle">https://docs.gradle.org/current/userguide/build_environment.html,简单说一下用到的:[Gradle</a> properties](<a href="https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties)%EF%BC%9A">https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties):</a></p></summary>
<category term="android打包" scheme="https://www.lefo.me/categories/android%E6%89%93%E5%8C%85/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="jenkins" scheme="https://www.lefo.me/tags/jenkins/"/>
</entry>
<entry>
<title>android进程保活(掌握黑心科技)</title>
<link href="https://www.lefo.me/2020/08/24/keep-alive/"/>
<id>https://www.lefo.me/2020/08/24/keep-alive/</id>
<published>2020-08-24T13:31:23.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="进程保活"><a href="#进程保活" class="headerlink" title="进程保活"></a>进程保活</h1><p>android这个生态真的是不好,从一开始,大家就想方设法在后台常驻,为了让自己app不被杀,绞尽脑汁想各种存活办法。目前,之前用的大部分保活方案,在手机上几乎都是失效的,尤其是一些国内厂家机型,提供一键清理功能,把你正的运行的APP,杀的渣也不剩。所以,目前还能正常使用的方法就只有一种,那就是两个进程互相监听。</p><span id="more"></span><h1 id="技术原理"><a href="#技术原理" class="headerlink" title="技术原理"></a>技术原理</h1><ul><li>B发现A死了后,赶紧把A拉活,然后再把自己杀死,这样达到瞒天过海的效果。</li><li>进程A使用flock锁定一个文件,此时B进程再flock就会就会挂起,除非A进程主动释放或者结束,B进程才会继续,也就是所谓的监听</li><li>反射使用ActivityManagerService直接拉活组件</li></ul><h2 id="启动子进程"><a href="#启动子进程" class="headerlink" title="启动子进程"></a>启动子进程</h2><p>方案一:就是fork。fork会分离出一个子进程,在fork的地方开始执行接下来的工作,可以根据fork函数返回值来判断是主进程还是子进程。这里提到的另一个概念就是二次fork,关于二次fork和进程托孤,可以自行上网了解一下。</p><p>方案二:使用app_process命令启动一个java进程。<a href="https://blog.csdn.net/u010651541/article/details/53163542">https://blog.csdn.net/u010651541/article/details/53163542</a></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">app_process <dir> <class name> <parameters> --application --nice-name=nice_name</span><br><span class="line">app_process -Djava.class.path=Helloworld.dex dir classname</span><br></pre></td></tr></table></figure><p>当然了,要想启动java进程,classpath得设置,如执行export CLASSPATH,也可以使用-D来指定路径。如果有so要调用,还要将共享库加到环境变量中。export LD_LIBRARY_PATH 或者export _LD_LIBRARY_PATH</p><h2 id="两个进程如何互相监听"><a href="#两个进程如何互相监听" class="headerlink" title="两个进程如何互相监听"></a>两个进程如何互相监听</h2><p>这里有一个相当巧妙的方式,使用linux提供的函数。</p><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">int flock(int fd,LOCK_EX);</span><br></pre></td></tr></table></figure><p>具体用法请自行搜索,简单讲,A进程调用该函数后,此时B进程再调用,会造成B进程阻塞。而当A进程挂了后,B进程就会立马继续执行。</p><h2 id="如何拉活APP"><a href="#如何拉活APP" class="headerlink" title="如何拉活APP"></a>如何拉活APP</h2><p>要获取ams的IBinder,然后调用transact来启动组件。这里给大家提供源代码的路径,这里需要用反射。</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">mRemote = android.os.ServiceManager.getService("activity")</span><br><span class="line">mRemote.transact(code,data,null,FLAG_ONEWAY);</span><br></pre></td></tr></table></figure><p>这里有两部分需要处理,一个是code值,另一个就是data。关于code值,不同的安卓版本不一样,另外有的rom也会修改,翻看源码android.app.IActivityManager。关于data,也会根据不同的系统版本有不同的拼装方法。我这里以service为例。具体参数,也可以参考源码,下面的写法也是根据IActivityManager不同版本中的startService写的。</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><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">mServiceData = Parcel.obtain();</span><br><span class="line">mServiceData.writeInterfaceToken("android.app.IActivityManager");</span><br><span class="line">mServiceData.writeStrongBinder(null);</span><br><span class="line">if (Build.VERSION.SDK_INT >= 26) {</span><br><span class="line"> mServiceData.writeInt(1);</span><br><span class="line">}</span><br><span class="line">//这个是service的intent</span><br><span class="line">serviceIntent.writeToParcel(mServiceData, 0);</span><br><span class="line">mServiceData.writeString(null);</span><br><span class="line">if (Build.VERSION.SDK_INT >= 26) {</span><br><span class="line"> //fg service</span><br><span class="line"> mServiceData.writeInt(1);</span><br><span class="line">}</span><br><span class="line">if (Build.VERSION.SDK_INT >= 23) {</span><br><span class="line"> String packageName = serviceIntent.getComponent().getPackageName();</span><br><span class="line"> mServiceData.writeString(packageName);</span><br><span class="line">}</span><br><span class="line">mServiceData.writeInt(0);</span><br></pre></td></tr></table></figure><p>我声明一下,service保活在部分手机上可用。</p><h1 id="上代码"><a href="#上代码" class="headerlink" title="上代码"></a>上代码</h1><p><a href="https://github.com/xs93/free/tree/master/common/library-keepalivecore/src/main/java/com/keepalive/daemon/core">->具体保活代码看这里</a></p><p>20年最早做的时候是没找到这坨代码的,当时纯逆向+找资料总结的。前两天一搜,现在不知道谁上传了,就给看到的有缘人发出来,这套保活目前应该还能稍微撑一撑吧,就挺恶心的。</p>]]></content>
<summary type="html"><h1 id="进程保活"><a href="#进程保活" class="headerlink" title="进程保活"></a>进程保活</h1><p>android这个生态真的是不好,从一开始,大家就想方设法在后台常驻,为了让自己app不被杀,绞尽脑汁想各种存活办法。目前,之前用的大部分保活方案,在手机上几乎都是失效的,尤其是一些国内厂家机型,提供一键清理功能,把你正的运行的APP,杀的渣也不剩。所以,目前还能正常使用的方法就只有一种,那就是两个进程互相监听。</p></summary>
<category term="android" scheme="https://www.lefo.me/categories/android/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
</entry>
<entry>
<title>python+selenium为你自动挂海淀驾校法陪课</title>
<link href="https://www.lefo.me/2020/05/18/python-selenium/"/>
<id>https://www.lefo.me/2020/05/18/python-selenium/</id>
<published>2020-05-18T12:25:12.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在北京摇号摇了18次了,下次开始就是四倍概率。然后,中签还是遥遥无期,隔壁坐的同事摇了两年就摇到了,万分羡慕。有时候晚上想去溜达一下,要去找gofun共享汽车还要走1公里多,还车还要走1公里。于是就有了想买一辆摩托车的想法。</p><p>要买车,肯定得有驾照,挑选完以后,报了海淀驾校,小区门口就有驾校的班车,关键是便宜,只要1000块。在我家那18线城市的小地方也得800多。于是报名,开始上法陪课。但法陪课每一章节必须自已手动点开始,很是麻烦,于是就想写个程序代替自己手点。</p><span id="more"></span><h2 id="手机端"><a href="#手机端" class="headerlink" title="手机端"></a>手机端</h2><p>法陪课可以在网页上上,也可以在APP上上,没自动播放,估计也是想让你好好学习,怕你偷懒吧。</p><p>因为我是做安卓的,起先打算拿在手机上搞,发现手机上是个自定义控件,基本不能用辅助功能(类似微信抢红包插件的技术)下手。唯一可用的就是</p><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">adb -s device_name shell input tap x y</span><br></pre></td></tr></table></figure><p>然后多久发一次命令又是个问题,于是就打算从网页上下手。</p><h2 id="网页端"><a href="#网页端" class="headerlink" title="网页端"></a>网页端</h2><p>网页上看,播放视频是一个flash,想用javascript也就没办法搞了,只得用selenium。</p><p>准备工作:</p><ul><li>Python:不解释</li><li>selenium: pip3 install selenium安装</li><li>Firefox:浏览器,本来我使用的是chrome,发现chrome对flash做了个优化,切后台后,flash不自动加载,换火狐就没问题了。</li><li>geckodriver:火狐浏览器的驱动,供selenium调用</li></ul><h1 id="放代码"><a href="#放代码" class="headerlink" title="放代码"></a>放代码</h1><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><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></pre></td><td class="code"><pre><span class="line">from selenium import webdriver</span><br><span class="line">from selenium.webdriver.common.action_chains import ActionChains</span><br><span class="line">from selenium.webdriver import FirefoxOptions</span><br><span class="line">from selenium.webdriver.support.wait import WebDriverWait</span><br><span class="line">import re</span><br><span class="line">import time</span><br><span class="line"></span><br><span class="line">def findTime(dr):</span><br><span class="line"> timeText = driver.find_element_by_class_name('playing').text</span><br><span class="line"> p1 = re.compile(r'[(](.*?)[)]', re.S)</span><br><span class="line"> timeList = re.findall(p1, timeText)</span><br><span class="line"> if len(timeList) == 0:</span><br><span class="line"> return 0</span><br><span class="line"> else:</span><br><span class="line"> timearr = timeList[0].split(':')</span><br><span class="line"> return int(timearr[1]) * 60 + int(timearr[2])</span><br><span class="line"></span><br><span class="line">#flash的事件不能通过element触发 </span><br><span class="line">def click_locxy(dr, x, y, left_click=True):</span><br><span class="line"> '''</span><br><span class="line"> dr:浏览器</span><br><span class="line"> x:页面x坐标</span><br><span class="line"> y:页面y坐标</span><br><span class="line"> left_click:True为鼠标左键点击,否则为右键点击</span><br><span class="line"> '''</span><br><span class="line"> if left_click:</span><br><span class="line"> ActionChains(dr).move_by_offset(x, y).click().perform()</span><br><span class="line"> else:</span><br><span class="line"> ActionChains(dr).move_by_offset(x, y).context_click().perform()</span><br><span class="line"> ActionChains(dr).move_by_offset(-x, -y).perform() # 将鼠标位置恢复到移动前</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">opts = FirefoxOptions()</span><br><span class="line">#opts.add_argument("--headless")</span><br><span class="line">option_profile = webdriver.FirefoxProfile()</span><br><span class="line">option_profile.set_preference("plugin.state.flash",2)</span><br><span class="line"></span><br><span class="line">path = "/Users/lefo/Documents/dev/chrome/geckodriver"# 注意这个路径需要时可执行路径(chmod 777 dir or 755 dir)</span><br><span class="line">driver = webdriver.Firefox(executable_path=path,options=opts)</span><br><span class="line">driver.get('http://www.xuechebu.com/sign.html')</span><br><span class="line">def playing():</span><br><span class="line"> playing = WebDriverWait(driver,60,1).until(lambda x:x.find_element_by_class_name('playing')) #等一分钟,直到获取到正在播放的控件</span><br><span class="line"> text = playing.text</span><br><span class="line"> print(text)</span><br><span class="line"> time.sleep(3) #等三秒,有时候文字可能加载慢</span><br><span class="line"> nextTime = findTime(dr=driver) #文字中提取括号内的时间</span><br><span class="line"> playing.click() #这里貌似无所谓,不点击也可以</span><br><span class="line"> print(str(nextTime)) #下一次执行的时间</span><br><span class="line"> time.sleep(10) #这个10s主要是为了flash允许有时间点</span><br><span class="line"> click_locxy(driver,750,540,left_click=True) #根据坐标点上去</span><br><span class="line"> time.sleep(nextTime + 5) #这个5s和上面10s同样的道理</span><br><span class="line">count = 0</span><br><span class="line">while(count <= 99): #99这个数值具体自己设</span><br><span class="line"> playing()</span><br><span class="line"> count = count +1</span><br><span class="line"></span><br><span class="line">driver.quit()</span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="说明"><a href="#说明" class="headerlink" title="说明"></a>说明</h1><p>首先,获取正在播放的超链接,上面文字的格式:第x章节(00:02:30)表示2分30秒长,计算成秒,然后间隔这个时间值再去下一个循环。因为播放完成后会自动跳到下一个章节,只是不会开始播放而已,所以,我们要做的就是,间隔一段时间,点一下播放。</p><p>试过传入启动flash插件的参数,最后也失败了,所以启动后需要在一分钟内登录,然后去 附加组件 - 插件 将flash插件启用。再将页面上的flash点个允许。点完允许后,10s内会触发一次播放点击。如果你觉得1分钟的登录时间不够,那就改一下上面的时间,或者,加上登录的逻辑(其实我是有登录的,发文章的时候,删掉了)</p><p>如果你是其它挂课的网页,道理也是相同的,如果用JS可以实现还是用js吧。因为这个网页有flash的特殊性,才用的selenium,我对python和selenium都不熟,里面的函数几乎一个都不知道,只能边学边搜边写。</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在北京摇号摇了18次了,下次开始就是四倍概率。然后,中签还是遥遥无期,隔壁坐的同事摇了两年就摇到了,万分羡慕。有时候晚上想去溜达一下,要去找gofun共享汽车还要走1公里多,还车还要走1公里。于是就有了想买一辆摩托车的想法。</p>
<p>要买车,肯定得有驾照,挑选完以后,报了海淀驾校,小区门口就有驾校的班车,关键是便宜,只要1000块。在我家那18线城市的小地方也得800多。于是报名,开始上法陪课。但法陪课每一章节必须自已手动点开始,很是麻烦,于是就想写个程序代替自己手点。</p></summary>
<category term="python" scheme="https://www.lefo.me/categories/python/"/>
<category term="python" scheme="https://www.lefo.me/tags/python/"/>
<category term="selenium" scheme="https://www.lefo.me/tags/selenium/"/>
</entry>
<entry>
<title>OpenCV在Android NDK按需要的模块编译</title>
<link href="https://www.lefo.me/2020/05/02/opencv-android/"/>
<id>https://www.lefo.me/2020/05/02/opencv-android/</id>
<published>2020-05-02T09:04:20.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>公司要做一个和图片有关的功能,一说图片处理,大家首先想到的就是强大的OpenCV。OpenCV很强大,官方也提供了android专用的sdk,直接将so和jar放入项目中就能使用。尽管官方推荐的也是这种方式,但有一个问题是,OpenCV的库很大,有10MB,很多公司整个APK都没有10MB,如果要把真个库都放到项目中,那还是挺大的。所以,这里就需要我们自己编译。中间折腾了好久,写个文章记录一下。</p><h1 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h1><h2 id="下载OpenCV"><a href="#下载OpenCV" class="headerlink" title="下载OpenCV"></a>下载OpenCV</h2><p>地址 <a href="https://opencv.org/releases/">https://opencv.org/releases/</a><br>选择Android平台的包</p><p>这里注意了,4.0的版本,要求api level是21以上,所以,如果你的APP是要在21以下使用,不要下载这个。</p><p>3.x的版本不清楚,但我试了OpenCV 2.x的版本是api level 8以上。2.x的版本,需要自行google,官方应该已经不提供了。</p><span id="more"></span><h2 id="配置gradle"><a href="#配置gradle" class="headerlink" title="配置gradle"></a>配置gradle</h2><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">defaultConfig</span><br><span class="line"></span><br><span class="line">android {</span><br><span class="line">ndkVersion "16.1.4479499"</span><br><span class="line"> defaultConfig {</span><br><span class="line"> ndk {</span><br><span class="line"> moduleName "opencv-test"</span><br><span class="line"> abiFilters 'armeabi-v7a','x86'</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> externalNativeBuild {</span><br><span class="line"> cmake {</span><br><span class="line"> path "src/main/jni/CMakeLists.txt"</span><br><span class="line"> version "3.10.2"</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意,上面的ndkVersion可以不填,我这里是为了使用OpenCV 2.X版本,应该是需要r16的版本编译,因为使用当前最新的版本编译时,有个库找不到。</p><p>abi需要哪些请根据你自己的需要来填,有很多新入门的朋友抄的时候都不看内容,否则如果你其它so库在某平台缺失,可能会闪退。</p><p>CMakeLists.txt注意对应的目录是你JNI代码的目录</p><h2 id="CMakeLists-txt"><a href="#CMakeLists-txt" class="headerlink" title="CMakeLists.txt"></a>CMakeLists.txt</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><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">#你的opencv解压目录</span><br><span class="line">set(OpenCV_DIR /OpenCV-android-sdk/sdk/native/jni)</span><br><span class="line">#这里也可以在后面跟具体模块OpenCV REQUIRED core imgproc 注意,写上并不代表会编译到包内</span><br><span class="line">FIND_PACKAGE(OpenCV REQUIRED)</span><br><span class="line">if(OpenCV_FOUND)</span><br><span class="line"> include_directories(${OpenCV_INCLUDE_DIRS})</span><br><span class="line"> message(STATUS "OpenCV library status:")</span><br><span class="line"> message(STATUS " version: ${OpenCV_VERSION}")</span><br><span class="line"> message(STATUS " libraries: ${OpenCV_LIBS}")</span><br><span class="line"> message(STATUS " include path: ${OpenCV_INCLUDE_DIRS}")</span><br><span class="line">else(OpenCV_FOUND)</span><br><span class="line"> message(FATAL_ERROR "OpenCV library not found")</span><br><span class="line">endif(OpenCV_FOUND)</span><br><span class="line">#你的库</span><br><span class="line">add_library( native_test</span><br><span class="line"> SHARED</span><br><span class="line"> native.cpp)</span><br><span class="line"></span><br><span class="line">#include为头文件所在的目录hpp</span><br><span class="line">include_directories(/OpenCV-android-sdk/sdk/native/jni/include)</span><br><span class="line"></span><br><span class="line">#要链接的库</span><br><span class="line">target_link_libraries( native_test</span><br><span class="line"> ${OpenCV_LIBS}</span><br><span class="line"> log</span><br><span class="line"> jnigraphics)</span><br></pre></td></tr></table></figure><p>简单说一下上面的代码</p><ol><li>首先设置OpenCV_DIR环境变量,指定所在目录</li><li>找出配置的所包含的库,也就是OpenCV的模块</li><li>环境的打印,不用在意</li><li>add_library是你的库的配置,可以参考ndk的官方文档</li><li>include_directories是OpenCV的头文件目录</li><li>target_link_libraries这里要用到jnigraphics,还有日志打印的log(常用),注意看OpenCV_LIBS就是FIND_PACKAGE的内容,在3中会打印出来。</li></ol><p>搭建完后你就可以在代码中编写你的jni代码了,这样写出来的库,只会包含你用到的模块,比如你只用到了core和imgproc,那你的库打出来就只有1.8mb,这比OpenCV官方的要小了好多。</p><h1 id="附"><a href="#附" class="headerlink" title="附"></a>附</h1><p>写代码的时候,不要使用OpenCV的imread,这会导致有需要多导入模块,增加了包的体积。ndk中,有android/bitmap.h,可以直接使用Bitmap对象来转Mat.</p>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>公司要做一个和图片有关的功能,一说图片处理,大家首先想到的就是强大的OpenCV。OpenCV很强大,官方也提供了android专用的sdk,直接将so和jar放入项目中就能使用。尽管官方推荐的也是这种方式,但有一个问题是,OpenCV的库很大,有10MB,很多公司整个APK都没有10MB,如果要把真个库都放到项目中,那还是挺大的。所以,这里就需要我们自己编译。中间折腾了好久,写个文章记录一下。</p>
<h1 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h1><h2 id="下载OpenCV"><a href="#下载OpenCV" class="headerlink" title="下载OpenCV"></a>下载OpenCV</h2><p>地址 <a href="https://opencv.org/releases/">https://opencv.org/releases/</a><br>选择Android平台的包</p>
<p>这里注意了,4.0的版本,要求api level是21以上,所以,如果你的APP是要在21以下使用,不要下载这个。</p>
<p>3.x的版本不清楚,但我试了OpenCV 2.x的版本是api level 8以上。2.x的版本,需要自行google,官方应该已经不提供了。</p></summary>
<category term="android" scheme="https://www.lefo.me/categories/android/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="opencv" scheme="https://www.lefo.me/tags/opencv/"/>
</entry>
<entry>
<title>jenkins中实用的插件</title>
<link href="https://www.lefo.me/2020/02/15/jenkins-plugins/"/>
<id>https://www.lefo.me/2020/02/15/jenkins-plugins/</id>
<published>2020-02-14T18:27:04.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="记录"><a href="#记录" class="headerlink" title="记录"></a>记录</h1><p>最近疫情闹的有点凶,大家都是在家办公,刚好群里的一个妹子一边卖萌,一边问jenkins的搭建及使用,就用腾讯会议给辅导了一下。给她讲完后,翻了翻之前给公司搭建的环境,想记录一下之前使用过的Jenkins插件,供以后使用。</p><p>暂时就这些,有好用的插件会不定时更新。</p><h2 id="自带插件"><a href="#自带插件" class="headerlink" title="自带插件"></a>自带插件</h2><p>安装的时候,会提示你要选哪些插件,建议默认,像git svn以及gradle,这些插件会在默认插件列表里就存在。</p><h2 id="Git-Parameter"><a href="#Git-Parameter" class="headerlink" title="Git Parameter"></a>Git Parameter</h2><p>可用于把git的tag branch当作构建参数传进来,方便使用branch构建。</p><h2 id="SVN-Parameter"><a href="#SVN-Parameter" class="headerlink" title="SVN Parameter"></a>SVN Parameter</h2><p>同Git Parameter 一样是可以将tag branch当作构建参数传进来。</p><span id="more"></span><h2 id="Multiple-SCMs-plugin"><a href="#Multiple-SCMs-plugin" class="headerlink" title="Multiple SCMs plugin"></a>Multiple SCMs plugin</h2><p>可以加多个源码的插件,比如我们项目就是资源单独有一个SVN,所以要把代码checkout以后,再checkout资源。</p><h2 id="Environment-Injector-Plugin"><a href="#Environment-Injector-Plugin" class="headerlink" title="Environment Injector Plugin"></a>Environment Injector Plugin</h2><p>可以在构建时注入一些环境变量,这款插件有一个好处就是,它支持<code>Prepare an environment for the run</code>,可以在SCM以前注入变量,比如我们有个资源的svn地址,需要在构建时传进来然后checkout。为了更直观,参数默认值就留了空。有了这个插件,写了groovy脚本,手动将空值改成了默认资源地址,SCM那里直接使用参数名就OK了。</p><h2 id="Version-Number-Plug-In"><a href="#Version-Number-Plug-In" class="headerlink" title="Version Number Plug-In"></a>Version Number Plug-In</h2><p>用于自定义构建记录的名字的插件,使构建记录更直观,不再是#1 #2这种格式。</p><h2 id="Build-Keeper-Plugin"><a href="#Build-Keeper-Plugin" class="headerlink" title="Build Keeper Plugin"></a>Build Keeper Plugin</h2><p>可以按天数保留几天内的构建,没什么大用,如果硬盘紧张,推荐使用自带的Discard old builds</p><h2 id="Role-based-Authorization-Strategy"><a href="#Role-based-Authorization-Strategy" class="headerlink" title="Role-based Authorization Strategy"></a>Role-based Authorization Strategy</h2><p>按项目分权限,比如某个project不想让某人访问到,可以用这个插件。</p><h2 id="Copy-Artifact"><a href="#Copy-Artifact" class="headerlink" title="Copy Artifact"></a>Copy Artifact</h2><p>可以将上游JOB构建后生成的Artifact,复制到下游的JOB来使用,比如上游的JOB生成的apk。</p><h2 id="Validating-String-Parameter-Plugin"><a href="#Validating-String-Parameter-Plugin" class="headerlink" title="Validating String Parameter Plugin"></a>Validating String Parameter Plugin</h2><p>用于参数校验的插件,像产品打包的时候versionCode总是喜欢空着,说多少次都不长心,还让开发看一下为什么打包失败,最后就使用了这个插件,输入时校验。</p>]]></content>
<summary type="html"><h1 id="记录"><a href="#记录" class="headerlink" title="记录"></a>记录</h1><p>最近疫情闹的有点凶,大家都是在家办公,刚好群里的一个妹子一边卖萌,一边问jenkins的搭建及使用,就用腾讯会议给辅导了一下。给她讲完后,翻了翻之前给公司搭建的环境,想记录一下之前使用过的Jenkins插件,供以后使用。</p>
<p>暂时就这些,有好用的插件会不定时更新。</p>
<h2 id="自带插件"><a href="#自带插件" class="headerlink" title="自带插件"></a>自带插件</h2><p>安装的时候,会提示你要选哪些插件,建议默认,像git svn以及gradle,这些插件会在默认插件列表里就存在。</p>
<h2 id="Git-Parameter"><a href="#Git-Parameter" class="headerlink" title="Git Parameter"></a>Git Parameter</h2><p>可用于把git的tag branch当作构建参数传进来,方便使用branch构建。</p>
<h2 id="SVN-Parameter"><a href="#SVN-Parameter" class="headerlink" title="SVN Parameter"></a>SVN Parameter</h2><p>同Git Parameter 一样是可以将tag branch当作构建参数传进来。</p></summary>
<category term="jenkins" scheme="https://www.lefo.me/categories/jenkins/"/>
<category term="jenkins" scheme="https://www.lefo.me/tags/jenkins/"/>
</entry>
<entry>
<title>Git中rebase的常规使用</title>
<link href="https://www.lefo.me/2020/01/09/git-rebase/"/>
<id>https://www.lefo.me/2020/01/09/git-rebase/</id>
<published>2020-01-09T12:21:39.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="git"><a href="#git" class="headerlink" title="git"></a>git</h1><p>公司从svn版本管理切换成了git,Android Studio(Intellij IDEA)也可以较好的支持git,相当方便。git有一个特别强大的命令,那就是rebase,这篇blog就记一下git rebase的一些使用。</p><h2 id="官方文档"><a href="#官方文档" class="headerlink" title="官方文档"></a>官方文档</h2><p><a href="https://git-scm.com/docs/git-rebase">https://git-scm.com/docs/git-rebase</a></p><h2 id="一个不错的操作模拟"><a href="#一个不错的操作模拟" class="headerlink" title="一个不错的操作模拟"></a>一个不错的操作模拟</h2><p><a href="https://learngitbranching.js.org/">https://learngitbranching.js.org/</a></p><span id="more"></span><h1 id="git-rebase的常用操作"><a href="#git-rebase的常用操作" class="headerlink" title="git rebase的常用操作"></a>git rebase的常用操作</h1><p>本文不另辟捷径,官方文档讲的很详细,就挑几个官方的例子翻译一下。</p><h2 id="案例1-:"><a href="#案例1-:" class="headerlink" title="案例1 :"></a>案例1 :</h2><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"> A---B---C topic</span><br><span class="line"> /</span><br><span class="line">D---E---F---G master</span><br></pre></td></tr></table></figure><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">git rebase master</span><br><span class="line">git rebase master topic</span><br></pre></td></tr></table></figure><p>上面两条命令,执行其中的一条就可以,执行以后。其实就是git rebase upstream branch</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"> A'--B'--C' topic</span><br><span class="line"> /</span><br><span class="line">D---E---F---G master</span><br></pre></td></tr></table></figure><h2 id="案例2:–onto的使用"><a href="#案例2:–onto的使用" class="headerlink" title="案例2:–onto的使用"></a>案例2:–onto的使用</h2><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">git rebase [-i | --interactive] [<options>] [--exec <cmd>]</span><br><span class="line">[--onto <newbase> | --keep-base] [<upstream> [<branch>]]</span><br></pre></td></tr></table></figure><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">o---o---o---o---o master</span><br><span class="line"> \</span><br><span class="line"> o---o---o---o---o next</span><br><span class="line"> \</span><br><span class="line"> o---o---o topic</span><br></pre></td></tr></table></figure><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">git rebase --onto master next topic</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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">o---o---o---o---o master</span><br><span class="line"> | \</span><br><span class="line"> | o'--o'--o' topic</span><br><span class="line"> \</span><br><span class="line"> o---o---o---o---o next</span><br></pre></td></tr></table></figure><p>这里–onto的后面有三个参数(其实属于–onto的就一个)</p><ol><li>参数1:要跟随在哪个commit,可以是分支名,也可以是commit的 Revision Number</li><li>参数2:after-this-commit 意思是,从这个commit之后的开始算,这个commit是不算的</li><li>参数3:要操作的最后一个commit,这个本身是要算进去的。</li></ol><h2 id="复制一段commits到另一个分支(as应该不支持)"><a href="#复制一段commits到另一个分支(as应该不支持)" class="headerlink" title="复制一段commits到另一个分支(as应该不支持)"></a>复制一段commits到另一个分支(as应该不支持)</h2><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">A---B---C---D---E master</span><br><span class="line"> \</span><br><span class="line"> F---G---H---I---J---K---L---M topic</span><br></pre></td></tr></table></figure><p>比如想把K L M的提交copy到master上,那么可以使用命令</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">git checkout master</span><br><span class="line">git rebase --onto E J M</span><br><span class="line">git rebase HEAD master</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></pre></td><td class="code"><pre><span class="line">A---B---C---D---E---K‘---L’---M‘ master</span><br><span class="line"> \</span><br><span class="line"> F---G---H---I---J---K---L---M topic</span><br></pre></td></tr></table></figure><p>注意这里一定要<code>git rebase HEAD master</code>,因为执行了第二条rebase后,只是复制了一份commit过去,需要处理一下master和HEAD。</p><h2 id="案例3:合并多条commit为一条"><a href="#案例3:合并多条commit为一条" class="headerlink" title="案例3:合并多条commit为一条"></a>案例3:合并多条commit为一条</h2><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">git rebase -i <after-this-commit></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><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">pick :不做任何修改;</span><br><span class="line">reword:只修改提交注释信息;</span><br><span class="line">edit:修改提交的文件,做增补提交;</span><br><span class="line">squash:将该条提交合并到上一条提交,提交注释也一并合并;</span><br><span class="line">fixup:将该条提交合并到上一条提交,废弃该条提交的注释;</span><br></pre></td></tr></table></figure><h1 id="关于Android-Studio"><a href="#关于Android-Studio" class="headerlink" title="关于Android Studio"></a>关于Android Studio</h1><p>AS内置的插件可以满足大部分功能,按command + 9可以唤出来log tab<br>附上操作指南:<br><a href="https://www.jetbrains.com/help/idea/edit-project-history.html">https://www.jetbrains.com/help/idea/edit-project-history.html</a></p><p>我试了试,最终在as里没有找到 –onto 复制一段commits的方式,因为第三个参数必须得选branch,在stackoverflow里,找到一处资料。对比可以看到,如果和上述一段commits,是缺少参数的,所以这里得用命令行了。实际中,也劝大家尽量不要用这种方式。</p><blockquote><p>The rebase dialog in IntelliJ 12.1 uses the most general version of the rebase command:</p><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">git rebase [-i] [--onto newbase] [upstream] [branch]</span><br></pre></td></tr></table></figure><p>where IntelliJ’s “Onto” field corresponds to <code>--onto newbase</code>, IntelliJ’s “From” field corresponds to “upstream” and IntelliJ’s “Branch” field corresponds to “branch”.</p><p>In above git rebase command all parameters are optional, while in IntelliJ they are not. This means that you have to take your git rebase command and express it using the general form shown above.</p><p>Note that what you actually do with the arguments of the rebase command is define a range of commits that will be replayed in a new target location. Generally, the range is <code>upstream..branch</code>. If you are not familiar with commit ranges you should read up on them.</p><p>Let’s look at your example and assume that you are on branch “branch”:</p><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">git rebase -i HEAD~4</span><br></pre></td></tr></table></figure><p>Let’s first figure out which is the range. Since you have only one argument, <code>HEAD~4</code>, this corresponds to upstream, i.e. the range is <code>HEAD~4..branch</code> or in other words <code>HEAD~4..HEAD</code> on branch “branch”. The question is now which is your –onto target. If you avoid <code>--onto</code>, then git assumes that your upstream is also your <code>--onto</code>.</p><p>This yields:</p><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">git rebase -i --onto HEAD~4 HEAD~4 branch</span><br></pre></td></tr></table></figure><p>and now you can fill in IntelliJ’s rebase dialog using</p><ul><li>Onto: <code>HEAD~4</code></li><li>From: <code>HEAD~4</code></li><li>Branch: <code>branch</code></li></ul><p>IntelliJ actually forces you to think first and identify your range and target, which looks more complicated, but which prevents you from doing a rebase without understanding what the result will be.</p><p><a href="https://stackoverflow.com/questions/14608812/how-to-do-interactive-rebase-with-intellij-idea">https://stackoverflow.com/questions/14608812/how-to-do-interactive-rebase-with-intellij-idea</a></p></blockquote><p>注意AS中,也可以勾选-i参数</p><p>如果是只有一条提交记录,通常用cherry-pick很方便,rebase可以用来处理多条commits的时候。当然,你也可以把commit先执行squash,再执行cherry-pick。</p>]]></content>
<summary type="html"><h1 id="git"><a href="#git" class="headerlink" title="git"></a>git</h1><p>公司从svn版本管理切换成了git,Android Studio(Intellij IDEA)也可以较好的支持git,相当方便。git有一个特别强大的命令,那就是rebase,这篇blog就记一下git rebase的一些使用。</p>
<h2 id="官方文档"><a href="#官方文档" class="headerlink" title="官方文档"></a>官方文档</h2><p><a href="https://git-scm.com/docs/git-rebase">https://git-scm.com/docs/git-rebase</a></p>
<h2 id="一个不错的操作模拟"><a href="#一个不错的操作模拟" class="headerlink" title="一个不错的操作模拟"></a>一个不错的操作模拟</h2><p><a href="https://learngitbranching.js.org/">https://learngitbranching.js.org/</a></p></summary>
<category term="git" scheme="https://www.lefo.me/categories/git/"/>
<category term="git" scheme="https://www.lefo.me/tags/git/"/>
</entry>
<entry>
<title>android 8.0 foregroud-service的坑</title>
<link href="https://www.lefo.me/2019/12/13/foregroud-service-oreo/"/>
<id>https://www.lefo.me/2019/12/13/foregroud-service-oreo/</id>
<published>2019-12-13T10:15:23.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="ForegroudService"><a href="#ForegroudService" class="headerlink" title="ForegroudService"></a>ForegroudService</h1><p>都知道8.0以后,不可以在后台调用startService()来启动一个服务,要想通过startService启动,必须activity在前台时才能使用。当然onResume和onPause状态下的activity都可以。但是,也有一种情况是例外。</p><p><a href="https://developer.android.com/about/versions/oreo/background#services">https://developer.android.com/about/versions/oreo/background#services</a></p><p>这里在官方文档也有讲,就是: 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用 Service。 也就是,当你的activity刚进入后台时,是可以调用startService的。</p><p>如果不使用startService,就得使用startForegroundService,但是需要绑定一个通知,可以在调用时传入通知id,也可以在调用后,通过startForeground来绑定。</p><p>然而,除了以上,还是有一些疏忽了的,需要注意的地方。</p><span id="more"></span><h2 id="使用了startForegroudService还是有错"><a href="#使用了startForegroudService还是有错" class="headerlink" title="使用了startForegroudService还是有错"></a>使用了startForegroudService还是有错</h2><p>做了8.0兼容后,已经把所有的startService都改成了startForegroundService,但是后台还是得到了很多的错误,特别是在android 8.0 和8.1的机子上。</p><p>android.app.RemoteServiceException: Context.startForegroundService() did not then call Service.startForeground()</p><p>经过排查,测试,发现还有一些需要注意的点,这里大概列一下。</p><h2 id="可以不调用notify方法"><a href="#可以不调用notify方法" class="headerlink" title="可以不调用notify方法"></a>可以不调用notify方法</h2><p>首先,要想启动一个前台服务,必须使用startForegroundService,只要调用了startForegroundService,必须调用startForeground为其设置一个notification。</p><p>注意:这里的notification可以不调用notify方法,但是,在调用startForeground后,会自动调用这个notify方法将notification展示出来。</p><h2 id="stopSelf前要startForeground"><a href="#stopSelf前要startForeground" class="headerlink" title="stopSelf前要startForeground"></a>stopSelf前要startForeground</h2><p>google文档显示,如果在5s内未调用startForeground,则系统将停止此Service并声明此应用为ANR。那么,5s内如果stopSelf()呢??亲测,这样也不行,按常理分析,如果直接调用stopSelf可行,是有违ForegroudService的设计初衷的。所以,在stopSelf前,如果想一个startForegroundService调用后直接关闭,也是需要调用startForeground()的。</p><h2 id="stopForeground不要乱用"><a href="#stopForeground不要乱用" class="headerlink" title="stopForeground不要乱用"></a>stopForeground不要乱用</h2><p>stopForeground是将一个service从前台改为后台的,如果你中途调用了stopForeground,再次调用startForegroundService时,一但没有走到startForeground,(比如是在onCreate方法中,就不会走到)还是会报出Context.startForegroundService() did not then call Service.startForeground()的异常,所以,看清楚再使用它。</p>]]></content>
<summary type="html"><h1 id="ForegroudService"><a href="#ForegroudService" class="headerlink" title="ForegroudService"></a>ForegroudService</h1><p>都知道8.0以后,不可以在后台调用startService()来启动一个服务,要想通过startService启动,必须activity在前台时才能使用。当然onResume和onPause状态下的activity都可以。但是,也有一种情况是例外。</p>
<p><a href="https://developer.android.com/about/versions/oreo/background#services">https://developer.android.com/about/versions/oreo/background#services</a></p>
<p>这里在官方文档也有讲,就是: 进入后台时,在一个持续数分钟的时间窗内,应用仍可以创建和使用 Service。 也就是,当你的activity刚进入后台时,是可以调用startService的。</p>
<p>如果不使用startService,就得使用startForegroundService,但是需要绑定一个通知,可以在调用时传入通知id,也可以在调用后,通过startForeground来绑定。</p>
<p>然而,除了以上,还是有一些疏忽了的,需要注意的地方。</p></summary>
<category term="android" scheme="https://www.lefo.me/categories/android/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
</entry>
<entry>
<title>python入门:使用python刷自如的房子</title>
<link href="https://www.lefo.me/2019/07/31/python-ziroom/"/>
<id>https://www.lefo.me/2019/07/31/python-ziroom/</id>
<published>2019-07-31T08:43:48.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="错过房子"><a href="#错过房子" class="headerlink" title="错过房子"></a>错过房子</h1><p>自如在北京租房行业上占据着龙头地位,它们的房租也是一年比一年高,特别是新签约的房子,价格很是离谱。但偶尔也会有一两个换租的房子,性价比超级高。如果有自己比较中意的小区,想监控里面的房子,我就有一次看中一个房子,看到的时候,就已经晚了,等想签的时候,被别人抢先了。这个脚本,可以在某小区有新房源的时候,第一时间通知自己。</p><span id="more"></span><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><ol><li>python 2.x 语言及运行环境</li><li>tesseract ocr 这是google的一款图片识别文字的软件,需要安装库,自行搜索python+tesseract</li><li>支持smtp协议的邮箱,建议新注册一个。</li><li>绑定微信的QQ的邮箱。</li></ol><p>安装python和tesseract就不讲了,主要说一下tesseract。其实没有这个也不重要,主要是自如的房子,价格是一张图片。然后用坐标截取相应的数字生成价格图,比如坐标是[8,9,2,1],那价格就是1345元。其实只要发送房子id就行,去官网看一下,价格就有了。</p><p><img src="/image/20190731/price.png"></p><p>使用邮箱,主要是为了方便微信接收,微信可以绑定QQ邮箱。只要给QQ发送邮件,微信就可以收到信息。</p><h1 id="流程"><a href="#流程" class="headerlink" title="流程"></a>流程</h1><ol><li>发送请求,搜索关键字 <a href="http://m.ziroom.com/v7/room/list.json?city_code=110000&type=11&keywords=">http://m.ziroom.com/v7/room/list.json?city_code=110000&type=11&keywords=</a> 后面跟上要搜索的关键字。type=11是一居室,具体参数,可以通过<a href="http://m.ziroom.com/">http://m.ziroom.com</a>,上面选择搜索信息后,可以查看到具体的type。</li><li>拿到列表,解析房源信息。</li><li>拿到房子价格图片,识别出内容,根据坐标拼出房价及单元(日租和月租)</li><li>检查数据库中是否保存过该房源(已经发送过)</li><li>分页会再请求下一页。</li></ol><h1 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h1><p>代码用定时任务跑在了我的服务器上,每隔一段时间跑一下。</p><p>写python比较少,再加上后来改代码时,直接用ssh连接的服务器用vi改的,导致代码写的比较乱。</p><p>我把信息保存到了ziroom.db里,是sqlite格式。价格为了不每次都请求,也缓存到了image文件夹下。</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><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></pre></td><td class="code"><pre><span class="line"># -*- coding: utf-8 -*-</span><br><span class="line">import sqlite3</span><br><span class="line">import urllib</span><br><span class="line">import urllib2</span><br><span class="line">import os</span><br><span class="line">import re</span><br><span class="line">import sys</span><br><span class="line">import json</span><br><span class="line">import smtplib</span><br><span class="line">import pytesseract</span><br><span class="line">import time</span><br><span class="line">from email.mime.text import MIMEText</span><br><span class="line">from email.header import Header</span><br><span class="line">from PIL import Image</span><br><span class="line"></span><br><span class="line"># 第三方 SMTP 服务</span><br><span class="line">mail_host="" #设置服务器</span><br><span class="line">mail_user="" #用户名</span><br><span class="line">mail_pass="" #口令 </span><br><span class="line">sender = ""#发送邮箱 一般等同于用户名</span><br><span class="line">receivers = ['2323354557@qq.com'] # 接收邮件,可设置为你的QQ邮箱或者其他邮箱</span><br><span class="line"></span><br><span class="line">BaseURL = 'http://m.ziroom.com/v7/room/list.json?city_code=110000&type=11&keywords=' </span><br><span class="line">#keywords = '%E9%BE%99%E5%8D%8E%E5%9B%AD'</span><br><span class="line">keywords = ['华清嘉园' , '展春园']</span><br><span class="line">cur_page = 1</span><br><span class="line">room_string = ''</span><br><span class="line"></span><br><span class="line">def sendEmail(content,keyword):</span><br><span class="line">message = MIMEText(content, 'plain', 'utf-8')</span><br><span class="line">message['From'] = '自如<'+sender+'>'</span><br><span class="line">message['To'] = 'lefo<2323354557@qq.com>'</span><br><span class="line"></span><br><span class="line">subject = '有'+ keyword +'的新房子了'</span><br><span class="line">message['Subject'] = Header(subject, 'utf-8')</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">try:</span><br><span class="line">smtpObj = smtplib.SMTP() </span><br><span class="line">smtpObj.connect(mail_host, 25) # 25 为 SMTP 端口号</span><br><span class="line">smtpObj.login(mail_user,mail_pass) </span><br><span class="line">smtpObj.sendmail(sender, receivers, message.as_string())</span><br><span class="line">print("邮件发送成功")</span><br><span class="line">except smtplib.SMTPException as e:</span><br><span class="line">print(e);</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">def getJson(URL,page,keyword):</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">global cur_page</span><br><span class="line">global room_string</span><br><span class="line"></span><br><span class="line">send_headers = {</span><br><span class="line"> 'Host':'m.ziroom.com',</span><br><span class="line"> 'User-Agent':'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1',</span><br><span class="line"> 'Accept':'application/json;version=6',</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line"></span><br><span class="line">url = URL +'&page=' + page;</span><br><span class="line">req = urllib2.Request(url,headers=send_headers)</span><br><span class="line"></span><br><span class="line">content = urllib2.urlopen(req).read().decode('utf-8')</span><br><span class="line">data = json.loads(content)</span><br><span class="line">rooms = data['data']['rooms'];</span><br><span class="line">print(URL)</span><br><span class="line">print(content)</span><br><span class="line"></span><br><span class="line">for room in rooms:</span><br><span class="line">tags =''</span><br><span class="line">for tag in room['tags']:</span><br><span class="line">tags +=(' ' + (tag['title']))</span><br><span class="line">#print (room['id'] + room['face'] + ' ' + room['name'] + tags + room['price'])</span><br><span class="line">cursor = c.execute("SELECT * from ROOM WHERE ZID=" + room['id'])</span><br><span class="line">result = cursor.fetchall()</span><br><span class="line"></span><br><span class="line">if len(result) <= 0:</span><br><span class="line">pricedata = room['price']</span><br><span class="line">priceurl = 'http:' + pricedata[0]</span><br><span class="line">print(priceurl)</span><br><span class="line">path = "image/" + os.path.basename(pricedata[0])</span><br><span class="line">if not os.path.exists(path):</span><br><span class="line">res = urllib.urlopen(priceurl).read()</span><br><span class="line">f = open(path,"wb")</span><br><span class="line">f.write(res)</span><br><span class="line">f.close()</span><br><span class="line">img = Image.open(path)</span><br><span class="line">print(path)</span><br><span class="line">imgprice = pytesseract.image_to_string(img,lang='eng',config='-psm 7')</span><br><span class="line">print(imgprice)</span><br><span class="line">price = ''</span><br><span class="line">unit = room['price_unit']</span><br><span class="line">if len(imgprice)>0:</span><br><span class="line">for index in pricedata[1]:</span><br><span class="line">price =price + imgprice[index]</span><br><span class="line">roomInfo = room['id'] + ' ' + room['name'] + ' ' + price + unit</span><br><span class="line"> </span><br><span class="line"></span><br><span class="line">room_string = room_string + roomInfo + '\n'</span><br><span class="line">sql = 'INSERT INTO ROOM (ZID,HID,TITLE) VALUES ("' + room['id'] +'","' + room['house_id']+'","' + room['name'] +'")'</span><br><span class="line">c.execute(sql )</span><br><span class="line">conn.commit()</span><br><span class="line"></span><br><span class="line">if len(rooms) > 0:</span><br><span class="line">cur_page += 1</span><br><span class="line">getJson(BaseURL + urllib.quote(keyword),str(cur_page),keyword)</span><br><span class="line">else:</span><br><span class="line">if len(room_string) > 0:</span><br><span class="line">print('send email \n' + room_string)</span><br><span class="line">sendEmail(room_string,keyword)</span><br><span class="line">room_string = ''</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">conn = sqlite3.connect('ziroom.db')</span><br><span class="line">c = conn.cursor()</span><br><span class="line">c.execute('''CREATE TABLE IF NOT EXISTS ROOM</span><br><span class="line"> (ID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,</span><br><span class="line"> ZID TEXT NOT NULL,</span><br><span class="line"> HID TEXT NOT NULL,</span><br><span class="line"> TITLE CHAR(50));''')</span><br><span class="line"></span><br><span class="line">for keyword in keywords:</span><br><span class="line">cur_page = 1</span><br><span class="line">keywordsquote = urllib.quote(keyword)</span><br><span class="line">getJson(BaseURL + keywordsquote,str(cur_page),keyword)</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><p>说一下优化的事,自如的房子有各种状态,参数名叫status,后续,可以把代码加一个状态的判断。比如status == ‘dzz’是待租中,还有退租配置中,最关键的是,有一个释放倒计时的状态。这样可以实现某个房子的要释放时,提前通知自己。</p>]]></content>
<summary type="html"><h1 id="错过房子"><a href="#错过房子" class="headerlink" title="错过房子"></a>错过房子</h1><p>自如在北京租房行业上占据着龙头地位,它们的房租也是一年比一年高,特别是新签约的房子,价格很是离谱。但偶尔也会有一两个换租的房子,性价比超级高。如果有自己比较中意的小区,想监控里面的房子,我就有一次看中一个房子,看到的时候,就已经晚了,等想签的时候,被别人抢先了。这个脚本,可以在某小区有新房源的时候,第一时间通知自己。</p></summary>
<category term="python" scheme="https://www.lefo.me/categories/python/"/>
<category term="python" scheme="https://www.lefo.me/tags/python/"/>
</entry>
<entry>
<title>从zip结构看APK采集时优化</title>
<link href="https://www.lefo.me/2019/07/08/zip-file-struct/"/>
<id>https://www.lefo.me/2019/07/08/zip-file-struct/</id>
<published>2019-07-08T09:44:20.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="现有采集"><a href="#现有采集" class="headerlink" title="现有采集"></a>现有采集</h1><p>都知道APK就是一个zip包,目前,收集别人家的APK信息,原理都一样,一般都是先将APK文件下载,再提取AndroidManifest.xml,通过<code>AXmlPrint2.jar</code>打开,得到反编译后的xml,解析xml得到包信息。</p><p>那么,一个游戏好几个GB,真正用到的却只有几KB信息。如果能跳过内容,结合断点下载,直接下载到AndroidManifest.xml,那就能省很多流量了。</p><span id="more"></span><h1 id="分析ZIP"><a href="#分析ZIP" class="headerlink" title="分析ZIP"></a>分析ZIP</h1><p>图文并茂的文章:<a href="https://blog.csdn.net/hp910315/article/details/77717746">https://blog.csdn.net/hp910315/article/details/77717746</a></p><p>表格介绍的文章:<a href="https://blog.csdn.net/a200710716/article/details/51644421">https://blog.csdn.net/a200710716/article/details/51644421</a></p><p>上面这两篇文章很详细的介绍了zip文件的结构,我再简单提一下,zip是先保存的文件,最后又将文件信息做了个目录放在最后。如下:</p><p><strong>[文件实体头+文件数据+数据描述符][..重复..]+核心目录+目录结束标识</strong></p><p>核心目录是关键内容,结构为重复n个[文件头],也就是,所有在zip中重复的文件,都有会在核心目录区保存一些关键信息(文件信息,不含文件内容)。其中,包含了每个文件在zip中起始偏移、压缩后的大小,所以,只要我们拿到核心目录的内容,就可以定位到AndroidManifest.xml的位置。</p><p>附上两张图:</p><p>manifest文件实体头</p><p><img src="/image/20190708/zip-header.jpg" alt="manifest文件实体头"></p><p>manifest核心目录</p><p><img src="/image/20190708/zip-dir.jpg" alt="manifest核心目录"></p><h1 id="关键头"><a href="#关键头" class="headerlink" title="关键头"></a>关键头</h1><p>核心目录中,每个文件头的标记位开始都是4个字节<code>0x02014b50</code>,在文件中,高位是在后面存放的,所以,要找的4个字节是0x50,0x4b,0x01,0x02。找到以后,根据对应的偏移,来找到文件名、文件名长度、压缩后大小以及在文件中的位置。</p><p>我只列举一下用到的位置</p><table><thead><tr><th>offset</th><th>大小(字节)</th><th>代表意义</th></tr></thead><tbody><tr><td>0</td><td>4</td><td>0x02014b50</td></tr><tr><td>20</td><td>4</td><td>压缩后文件大小</td></tr><tr><td>28</td><td>2</td><td>文件名长度(n)</td></tr><tr><td>42</td><td>4</td><td>文件保存的位置</td></tr><tr><td>46</td><td>文件名长度(n)</td><td>文件名</td></tr></tbody></table><p>找到文件保存的位置和大小后,直接读取成字节数组,然后使用<code>ZipInputStream</code>解压。</p><h1 id="文件"><a href="#文件" class="headerlink" title="文件"></a>文件</h1><p>文件分为:文件头、文件数据、数据描述符</p><p>文件头后面,就是文件内容了,但是,这些我们都不用关注,我们只需要拿到最开始0x04034b50所在的位置就行。因为ZipInputStream调用getNextEntry可以直接读取。</p><p>实际中发现文件头中很多信息都没有,比如文件大小。另外,扩展内容一般情况也是空的,所以基本上文件名后面就是文件内容。</p><table><thead><tr><th>offset</th><th>大小</th><th>意义</th></tr></thead><tbody><tr><td>0</td><td>4</td><td>0x04034b50</td></tr><tr><td>26</td><td>2</td><td>文件名长度n</td></tr><tr><td>28</td><td>2</td><td>扩展内容长度m</td></tr><tr><td>30</td><td>n</td><td>文件名</td></tr><tr><td>30+n</td><td>m</td><td>扩展内容</td></tr><tr><td>30+n+m</td><td>不固定</td><td>文件内容</td></tr></tbody></table><h1 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h1><ol><li>找到核心目录开头</li><li>找出AndroidManifest.xml文件信息</li><li>找到在文件中对应的偏移</li><li>解压出AndroidManifest.xml</li><li>使用AXmlPrint2.jar转换xml</li></ol><p>比较难的是确认核心目录开头,我们可以先获取后1MB的内容,然后读取,如果没有匹配上manifest,则再向前取1MB的内容,再进行一次匹配。使用断点下载时同理。</p><h1 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h1><p>附上代码:这个代码是一边学习一边随手尝试写的,查找目录偏移,读取文件等有很多偷懒的写法,请自行优化。代码只是提供思路以及验证可行性,请不要在正式环境中使用。</p><p>另外注意,read()过后,计算下一个offset的时候,要去掉本身的大小的。比如:得到压缩后文件大小,再去获取文件名长度时,是20偏移 + 4字节再到28偏移,所以是skipBytes(4)。</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><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></pre></td><td class="code"><pre><span class="line">import java.io.ByteArrayInputStream;</span><br><span class="line">import java.io.File;</span><br><span class="line">import java.io.FileOutputStream;</span><br><span class="line">import java.io.RandomAccessFile;</span><br><span class="line">import java.util.zip.ZipEntry;</span><br><span class="line">import java.util.zip.ZipInputStream;</span><br><span class="line"></span><br><span class="line">public class ManifestGetter{</span><br><span class="line"></span><br><span class="line"> public static final int DEFAULT_SIZE = 1024 * 1024 *2;</span><br><span class="line"> public static void main(String... args) {</span><br><span class="line"></span><br><span class="line"> byte[] signature = new byte[]{0x50,0x4b,0x01,0x02};</span><br><span class="line"> </span><br><span class="line"> String filename = "./test.apk";</span><br><span class="line"> File file = new File( filename);</span><br><span class="line"></span><br><span class="line"> try {</span><br><span class="line"> RandomAccessFile accessFile = new RandomAccessFile(file,"r");</span><br><span class="line"> RandomAccessFile accessFile2 = new RandomAccessFile(file,"r");</span><br><span class="line"> long offset = file.length() - DEFAULT_SIZE;</span><br><span class="line"> </span><br><span class="line"> accessFile.seek(offset);</span><br><span class="line"> </span><br><span class="line"> byte[] buffer = new byte[1024];</span><br><span class="line"> int len = 0;</span><br><span class="line"> while((len = accessFile.read(buffer) )> 0){</span><br><span class="line"> </span><br><span class="line"> int index = 0;</span><br><span class="line"> for (; index < len; index++) {</span><br><span class="line"> </span><br><span class="line"> boolean flag = true;</span><br><span class="line"></span><br><span class="line"> for (byte signatureB : signature) {</span><br><span class="line"></span><br><span class="line"> byte b = 0;</span><br><span class="line"> if (index < buffer.length - 4){</span><br><span class="line"> b = buffer[index++];</span><br><span class="line"> }else{</span><br><span class="line"> b = (byte)accessFile.read();</span><br><span class="line"> }</span><br><span class="line"> if (signatureB != b){</span><br><span class="line"> flag = false;</span><br><span class="line"> break;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">//匹配到了0x02014b50标记</span><br><span class="line"> if (flag){</span><br><span class="line"></span><br><span class="line"> long subOffset = 0;</span><br><span class="line"> if (index < buffer.length - 4){</span><br><span class="line"> subOffset = accessFile.getFilePointer() - buffer.length + index -4;</span><br><span class="line"> }else{</span><br><span class="line"> subOffset = accessFile.getFilePointer();</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> //0x04034B50</span><br><span class="line"> System.out.println("中心目录文件开始-----------" + subOffset);</span><br><span class="line"> System.out.println("offset =" + subOffset);</span><br><span class="line">//压缩后文件大小 4字节</span><br><span class="line"> accessFile2.seek(subOffset + 20);</span><br><span class="line"> int fileSize = 0;</span><br><span class="line"> for (int i =0; i < 4; i++) {</span><br><span class="line"> fileSize += (accessFile2.read()<< (8 *i));</span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> accessFile2.skipBytes(4);</span><br><span class="line">//文件名大小 2字节</span><br><span class="line"> int size_byte1 = accessFile2.read();</span><br><span class="line"> int size_byte2 = accessFile2.read();</span><br><span class="line"></span><br><span class="line"> int name_size = (size_byte2 << 8 ) + size_byte1;</span><br><span class="line"> System.out.println("name size = " + name_size);</span><br><span class="line"> accessFile2.skipBytes(12);</span><br><span class="line"></span><br><span class="line"> long fileOffset = 0;</span><br><span class="line"></span><br><span class="line"> for (int i =0; i < 4; i++) {</span><br><span class="line"> fileOffset += (accessFile2.read()<< (8 *i));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> byte[] nameBuf = new byte[name_size];</span><br><span class="line"> accessFile2.read(nameBuf);</span><br><span class="line"> String nameString = new String(nameBuf);</span><br><span class="line"></span><br><span class="line"> System.out.println(nameString + " offset = "+ Long.toHexString(fileOffset));</span><br><span class="line"></span><br><span class="line"> if (nameString.contains("AndroidManifest.xml")){</span><br><span class="line"> </span><br><span class="line"> // file header</span><br><span class="line"> accessFile2.seek(fileOffset);</span><br><span class="line"></span><br><span class="line"> FileOutputStream out = new FileOutputStream("./dest.xml");</span><br><span class="line"> // 文件里还有一个小文件头,这里加了1024字节,实际情况很小</span><br><span class="line"> byte[] buf = new byte[fileSize + 1024];</span><br><span class="line"> accessFile2.read(buf);</span><br><span class="line"> ByteArrayInputStream bInputStream = new ByteArrayInputStream(buf);</span><br><span class="line"> ZipInputStream zin = new ZipInputStream(bInputStream);</span><br><span class="line"></span><br><span class="line"> ZipEntry zipEntry = zin.getNextEntry();</span><br><span class="line"></span><br><span class="line"> System.out.println(zipEntry.getName() + "文件信息在目录里,这里的size = " + zipEntry.getSize());</span><br><span class="line"> </span><br><span class="line"> byte[] readBuf = new byte[fileSize + 1024];</span><br><span class="line"></span><br><span class="line"> int readLength = 0;</span><br><span class="line"> while ((readLength = zin.read(readBuf)) > 0) {</span><br><span class="line"> byte[] bytes = new byte[readLength];</span><br><span class="line"> System.arraycopy(readBuf, 0, bytes, 0, readLength);</span><br><span class="line"> out.write(bytes);</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> accessFile.close();</span><br><span class="line"> accessFile2.close();</span><br><span class="line"> out.close();</span><br><span class="line"> return;</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> System.out.println(nameString);</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"> offset += len;</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"> accessFile.close();</span><br><span class="line"> accessFile2.close();</span><br><span class="line"> } catch (Exception e) {</span><br><span class="line"> e.printStackTrace();</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><h1 id="重点优化"><a href="#重点优化" class="headerlink" title="重点优化"></a>重点优化</h1><p>因为上面代码偷懒,并不是一个完整的zip处理方案,下面附一个标准的方法。</p><h2 id="流程优化"><a href="#流程优化" class="headerlink" title="流程优化"></a>流程优化</h2><p>加载一个zip的流程是先加载EOCD ending of central directory,再找到CD起始位置,再找里面文件的偏移。我上边的做法是错误的。<br>EOCD保存有CD的起始偏移,CD保存着每个一个file header<br>EOCD偏移值为0x06054b50</p><h2 id="为什么-EOCD-记录在-ZIP-文件的最后-65535-字节内?"><a href="#为什么-EOCD-记录在-ZIP-文件的最后-65535-字节内?" class="headerlink" title="为什么 EOCD 记录在 ZIP 文件的最后 65535 字节内?"></a>为什么 EOCD 记录在 ZIP 文件的最后 65535 字节内?</h2><p>EOCD 记录的最大偏移量字段限制为 16 位,最大值为 65535。这意味着 EOCD 记录的偏移量不能超过 65535 字节。如果 ZIP 文件的大小超过 65535 字节,EOCD 记录仍然必须在最后 65535 字节内找到。这个设计是为了确保即使是大文件,EOCD 记录也可以通过读取文件的最后 65535 字节来定位。</p><h2 id="EOCD结构"><a href="#EOCD结构" class="headerlink" title="EOCD结构"></a>EOCD结构</h2><table><thead><tr><th>offset</th><th>大小</th><th>意义</th></tr></thead><tbody><tr><td>0</td><td>4</td><td>0x06054b50</td></tr><tr><td>4</td><td>2</td><td>当前分卷号</td></tr><tr><td>6</td><td>2</td><td>central directory开始分卷号</td></tr><tr><td>8</td><td>2</td><td>当前分卷记录的contral directory数量</td></tr><tr><td>10</td><td>2</td><td>central directory记录的总数量</td></tr><tr><td>12</td><td>4</td><td>contral directory的大小</td></tr><tr><td>16</td><td>4</td><td>contral directory的偏移</td></tr><tr><td>20</td><td>2</td><td>zip文件注释的长度</td></tr><tr><td>22</td><td>n</td><td>zip文件注释</td></tr></tbody></table><h2 id="四字节比较"><a href="#四字节比较" class="headerlink" title="四字节比较"></a>四字节比较</h2><p>刚开始写的代码偷懒用string比较了,实际上应该拿int值比较,一次读取4个字节,使用位运算合成一个整数,以下是合成方法:</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></pre></td><td class="code"><pre><span class="line">private static int getInt(byte[] buffer, int offset) {</span><br><span class="line">return (buffer[offset] & 0xFF) |</span><br><span class="line"> ((buffer[offset + 1] & 0xFF) << 8) |</span><br><span class="line"> ((buffer[offset + 2] & 0xFF) << 16) |</span><br><span class="line"> ((buffer[offset + 3] & 0xFF) << 24);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>也可以使用nio中的ByteBuffer</p>]]></content>
<summary type="html"><h1 id="现有采集"><a href="#现有采集" class="headerlink" title="现有采集"></a>现有采集</h1><p>都知道APK就是一个zip包,目前,收集别人家的APK信息,原理都一样,一般都是先将APK文件下载,再提取AndroidManifest.xml,通过<code>AXmlPrint2.jar</code>打开,得到反编译后的xml,解析xml得到包信息。</p>
<p>那么,一个游戏好几个GB,真正用到的却只有几KB信息。如果能跳过内容,结合断点下载,直接下载到AndroidManifest.xml,那就能省很多流量了。</p></summary>
<category term="android开发" scheme="https://www.lefo.me/categories/android%E5%BC%80%E5%8F%91/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
</entry>
<entry>
<title>升级targetSdkVersion为26以后</title>
<link href="https://www.lefo.me/2019/07/02/target-sdk-26/"/>
<id>https://www.lefo.me/2019/07/02/target-sdk-26/</id>
<published>2019-07-02T13:02:00.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>google从下个月开始,所有在play上架的app都会要求升级的。为了与时俱进,我们的app也要升级到targetSdkVersion=26了,虽然我们的APP是助手类,不可能上架google play。</p><p>关于升级后8.0的各种介绍,参考这里<a href="https://developer.android.com/about/versions/oreo/">https://developer.android.com/about/versions/oreo/</a></p><p>8.0行为变更看这里<a href="https://developer.android.com/about/versions/oreo/android-8.0-changes">https://developer.android.com/about/versions/oreo/android-8.0-changes</a></p><p>基本上,看完上面链接里的内容以后,你就能针对你的代码做修改了。下面主要记录一下实际修改中的几处重点。</p><span id="more"></span><h1 id="后台限制"><a href="#后台限制" class="headerlink" title="后台限制"></a>后台限制</h1><p>官方介绍:<a href="https://developer.android.com/about/versions/oreo/background">https://developer.android.com/about/versions/oreo/background</a></p><h2 id="Service"><a href="#Service" class="headerlink" title="Service"></a>Service</h2><p>如果改成了targetSdkVersion=26+,是不允许在后台启动service的。但是,要注意这里的后台两个字。google文档里已经讲的很清楚,当你的应用处于前台应用时,仍然可以使用startService。</p><p>所以,首先要做的是,把之前在Application中startService的代码删除掉,挪到activity的onCreate方法中。或者,在Application中,调用<a href="https://developer.android.com/reference/kotlin/android/app/Application?hl=en#registeractivitylifecyclecallbacks">registerActivityLifecycleCallbacks</a>,在回调里startService。</p><p>当然,如果你的程序在运行时,有一个常驻通知,那么你可以将你的service变成一个前台service。此时,不能使用startService来启动,要使用startForegroundService,调用该方法就没有必须前台应用的限制了,任何地方都可以。但是,在startForegroundService开启一个service后,必须在5s内,调用它的startForeground()方法,将某个通知和其绑定起来,否则也会出错。</p><p>google也给了另外一种官方的后台任务实现方式,那就是使用<code>JobService</code>,关于用法网上一大堆,不再介绍,需要注意的是,这里要规范它的id。如果相同的id重复schedule,那么会先调用onStopJob再调用onStartJob。</p><p>如果之前是IntentService,那可以直接用JobIntentService,使用起来更简单。</p><p><a href="https://developer.android.com/reference/android/support/v4/app/JobIntentService">JobIntentService</a></p><h3 id="附上官方demo"><a href="#附上官方demo" class="headerlink" title="附上官方demo"></a>附上官方demo</h3><p>官方DEMO:<a href="https://github.com/googlesamples/android-JobScheduler">https://github.com/googlesamples/android-JobScheduler</a></p><h2 id="Receiver"><a href="#Receiver" class="headerlink" title="Receiver"></a>Receiver</h2><p>静态注册广播监听系统事件,失效了。关于广播的限制,官方文档里讲的也很清楚。我们要做的就是,把注册了一系列系统action的Receiver,再动态注册一次。比如常用的网络状态切换监听,之前只要写到manifest中就OK,现在必须手动调用一次register方法。还有我们项目中监听其它应用的安装卸载,一样需要动态注册。</p><p>registerReceiver()的文档中明确写到,你的Receiver是注册到Application中,我就直接将receiver注册到了application上。</p><blockquote><p>Context-registered receivers receive broadcasts as long as their registering context is valid. For an example, if you register within an <code>Activity</code> context, you receive broadcasts as long as the activity is not destroyed. If you register with the Application context, you receive broadcasts as long as the app is running.</p></blockquote><h1 id="Notification"><a href="#Notification" class="headerlink" title="Notification"></a>Notification</h1><p>Notification要求必须设置一个channel,否则是不予展示的。</p><p><a href="https://developer.android.com/training/notify-user/channels.html?hl=zh-cn">https://developer.android.com/training/notify-user/channels.html?hl=zh-cn</a></p><p>官方DEMO:<a href="https://github.com/googlesamples/android-Notifications">https://github.com/googlesamples/android-Notifications</a></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><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">if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.O) {</span><br><span class="line"> NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);</span><br><span class="line"> NotificationChannelGroup group = new NotificationChannelGroup(GROUP_ID, GROUP_NAME);</span><br><span class="line"> manager.createNotificationChannelGroup(group);</span><br><span class="line"> NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);</span><br><span class="line"> channel.setGroup(GROUP_ID);</span><br><span class="line"> // ...</span><br><span class="line"> manager.createNotificationChannel(channel);</span><br><span class="line"> notification = new Notification.Builder(getApplicationContext(), CHANNEL_ID)</span><br><span class="line"> // ...</span><br><span class="line"> .build();</span><br><span class="line">} else {</span><br><span class="line"> notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)</span><br><span class="line"> // ...</span><br><span class="line"> .build();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="其它影响的地方"><a href="#其它影响的地方" class="headerlink" title="其它影响的地方"></a>其它影响的地方</h1><p>升级以后还是有不少影响需要注意:</p><ol><li>第三方SDK,像push sdk,大多使用了service,要在26以上支持,必须更新了。</li><li>权限问题,比如设置屏幕亮度,悬浮窗等。</li><li>API变更,比如getRunningService方法在api26以上就失效了。</li></ol>]]></content>
<summary type="html"><h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>google从下个月开始,所有在play上架的app都会要求升级的。为了与时俱进,我们的app也要升级到targetSdkVersion&#x3D;26了,虽然我们的APP是助手类,不可能上架google play。</p>
<p>关于升级后8.0的各种介绍,参考这里<a href="https://developer.android.com/about/versions/oreo/">https://developer.android.com/about/versions/oreo/</a></p>
<p>8.0行为变更看这里<a href="https://developer.android.com/about/versions/oreo/android-8.0-changes">https://developer.android.com/about/versions/oreo/android-8.0-changes</a></p>
<p>基本上,看完上面链接里的内容以后,你就能针对你的代码做修改了。下面主要记录一下实际修改中的几处重点。</p></summary>
<category term="android开发" scheme="https://www.lefo.me/categories/android%E5%BC%80%E5%8F%91/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
</entry>
<entry>
<title>垂直滚动的ViewPager存在滑动不灵敏的问题</title>
<link href="https://www.lefo.me/2019/06/02/vertical-viewpager/"/>
<id>https://www.lefo.me/2019/06/02/vertical-viewpager/</id>
<published>2019-06-02T09:49:00.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="垂直滚动的ViewPager"><a href="#垂直滚动的ViewPager" class="headerlink" title="垂直滚动的ViewPager"></a>垂直滚动的ViewPager</h1><p>凡是找到我这篇文章的,肯定都在网上看过这样一篇文章<code>[几行代码实现ViewPager垂直滚动]</code>,地址我就不上了,随便一搜,到处都是,原理也很简单,交换一下横竖坐标,再设置一个上下的<code>Transformer</code>。然而,事情并没有想像的那么简单。</p><span id="more"></span><h1 id="ViewPager滚动源码解析"><a href="#ViewPager滚动源码解析" class="headerlink" title="ViewPager滚动源码解析"></a>ViewPager滚动源码解析</h1><p>在我提交上面的交换xy坐标代码后,测试拿着手机过来找我了,出现的问题就是,必须慢慢的将当前页滑动过页面的一半,松手时,才能滚到上一页或者下一页。这就导致如果是大屏手机,用户会很难去到下一页。我本来是以为x y坐标的问题,跟踪源码后,发现并不是。</p><p>在ViewPager中,滚动到上一页或者下一页的关键方法叫<code>determineTargetPage</code>,其中,如果是滚动到下一页,currentPage是当前页的position,如果是滚动到上一页,currentPage是上一页的position。</p><p><img src="/image/20190602/viewpager-src.jpg" alt="image-20190602181111242"></p><p>第一个if的逻辑是,先计算滑动距离和滑动的速度,如果超过了<code>mFlingDistance</code>和<code>mMininumVelocity</code>,再根据速度的正负值来判断方向,如果velocity > 0,则说明是[向右滑]的手势(先不考虑更换xy坐标),也就是滑动到上一页,返回currentPage。(currentPage是上一页的position)。如果velocity < 0,那就是[向左滑]的手势(先不考虑更换xy坐标),则是去到下一页。(currentPage是当前页的position)</p><p>else中的逻辑是,当用户慢慢拖动后松开手,也就是滚动速度和滚动距离的条件没有同时满足。此时通过计算出的<code>truncator</code>来计算目标页,可以理解为<code>pageOffset</code>是<code>currentPage</code>进了屏幕多少多少,而truncator就是剩下多少时滑动的一个比例。举个例子:</p><p>假如我们向下滑,上一页只露出了40%,那么pageOffset就是0.6,此时,0.6+0.6取整就是1,targetPage = currentPage + 1,而因为此时currentPage是上一页,所以又回滚回去。同理,假如我们向上滑,currentPage是当前页,也需要把pageOffSet滑动到0.6的位置,才能到下一页。</p><h1 id="修改速率不成功"><a href="#修改速率不成功" class="headerlink" title="修改速率不成功"></a>修改速率不成功</h1><p>找到了原理,就要修改了,起初我想着是,修改<code>mFlingDistance</code>和<code>mMininumVelocity</code>这两个值就好,因为这俩变量本身不支持修改,就把ViewPager的源码拷贝了一份。满心欢喜的做了个测试,失败,失败的原因,也比较蹊跷。</p><p>MotionEvent有一个方法,叫setLocation,交换xy坐标的方法,正是采用调用该方法实现的。但是在viewPager中,通过调用VelocityTracker的getXVelocity方法时,不论你如何执行setLocation,最终计算出的velocity永远都是指着真正在屏幕X方向上滑动的速度,也就是此时并没有把y轴的速度转到x上。这部分应该是跟native内部实现有关。</p><h1 id="去掉速度,只判断距离"><a href="#去掉速度,只判断距离" class="headerlink" title="去掉速度,只判断距离"></a>去掉速度,只判断距离</h1><p>既然速度废了,那就不判断速率了,只判断距离。于是代码被我改成了这样子。</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">int</span> <span class="title function_">determineTargetPage</span><span class="params">(<span class="type">int</span> currentPage, <span class="type">float</span> pageOffset, <span class="type">int</span> velocity, <span class="type">int</span> deltaX)</span> {</span><br><span class="line"> <span class="type">int</span> targetPage;</span><br><span class="line"> <span class="keyword">if</span> (Math.abs(deltaX) > mFlingDistance) {</span><br><span class="line"> targetPage = velocity > <span class="number">0</span> ? currentPage : currentPage + <span class="number">1</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">final</span> <span class="type">float</span> <span class="variable">truncator</span> <span class="operator">=</span> currentPage >= mCurItem ? <span class="number">0.9f</span> : <span class="number">0.1f</span>;</span><br><span class="line"> targetPage = currentPage + (<span class="type">int</span>) (pageOffset + truncator);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (mItems.size() > <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">final</span> <span class="type">ItemInfo</span> <span class="variable">firstItem</span> <span class="operator">=</span> mItems.get(<span class="number">0</span>);</span><br><span class="line"> <span class="keyword">final</span> <span class="type">ItemInfo</span> <span class="variable">lastItem</span> <span class="operator">=</span> mItems.get(mItems.size() - <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Only let the user target pages we have items for</span></span><br><span class="line"> targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> targetPage;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>此时,只判断了<code>mFlingDistance</code>,为了保险起见,我又将else中的代码,计算truncator时,改成了0.8和0.2。也就是当上一页或者下一页露出10%以上时,则滚动到相应的页面。代码是周末前提交的,写这篇博客是在周末,暂没有产品和测试的体验结果。</p><h1 id="其它"><a href="#其它" class="headerlink" title="其它"></a>其它</h1><p>复制ViewPager.java的时候,PagerAdapter有一个方法是setViewPagerObserver,因为这个方法是不允许访问的,这里可以改成registerDataSetObserver和unregisterDataSetObserver</p><p>google已经出了viewpager2了</p>]]></content>
<summary type="html"><h1 id="垂直滚动的ViewPager"><a href="#垂直滚动的ViewPager" class="headerlink" title="垂直滚动的ViewPager"></a>垂直滚动的ViewPager</h1><p>凡是找到我这篇文章的,肯定都在网上看过这样一篇文章<code>[几行代码实现ViewPager垂直滚动]</code>,地址我就不上了,随便一搜,到处都是,原理也很简单,交换一下横竖坐标,再设置一个上下的<code>Transformer</code>。然而,事情并没有想像的那么简单。</p></summary>
<category term="android开发" scheme="https://www.lefo.me/categories/android%E5%BC%80%E5%8F%91/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
</entry>
<entry>
<title>使用jenkins为android工程打包,支持多包名,改资源(踩坑指南)</title>
<link href="https://www.lefo.me/2019/03/29/jenkins-android-2/"/>
<id>https://www.lefo.me/2019/03/29/jenkins-android-2/</id>
<published>2019-03-29T05:30:00.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>上一篇文章主要写了打包刚开始的配置和参数化构建。这篇文章主要讲一讲在改包名、改资源的打包实践中,常会碰到的问题以及解决办法。如果看博客的人有更好的解决办法,也可以找我交流,关于页面有我联系方式。</p><p>打包的主要需求如下:</p><ol><li>改包名</li><li>可以替换icon,可以修改应用名,包括应用内部显示的名称(如版权信息)</li><li>可以控制部分功能是否开启</li></ol><h1 id="改包名分析"><a href="#改包名分析" class="headerlink" title="改包名分析"></a>改包名分析</h1><p>我们知道,改包名只是修改applicationId,和代码中类的package无关,所以基本上代码和AndroidManifest.xml中组件的name,都是不需要改动的。但是还是会涉及到下面的这些问题:</p><span id="more"></span><ol><li>代码中使用”包名”写死的路径,比如<code>/data/data/com.xx.xx/</code>。</li><li>AndroidManifest.xml中,ContentProvider会填一个<code>authorities</code>属性,换包名等于换应用<code>authorities</code>就得跟着变。部分<code>action</code>,部分activity的<code>taskAffinity</code>。</li><li>第三方的client_key(支持多包名就没关系了,比如umeng push)。</li><li>提供给外部调用的activity和service,偶尔会要求必须在包名路径下(刚好我碰上了这奇葩事)。</li></ol><p>具体解决办法:</p><ol><li>使用全局查找,找出包名的字符串,统一改为context.getPackageName(),没有context对象的地方,使用<code>BuildConfig.APPLICATION_ID</code>。</li><li>当我们配了applicationId以后,就可以在AndroidMenifest.xml中使用<code>{$applicationId}</code>代替写死的包名。</li><li>像<code>applicationId</code>一样,在gradle文件的<code>defaultConfig</code>中,把每个key都声明成常量调用。在做打包之前,我想的是给产品运营人员提供一个构建参数让他们填写,后来意识到,其实这些clientkey和包名是绑定的,于是在工程里新建了一个名包文件夹,里面保存了key=value的格式,打包时,通过shell读取到环境变量,然后用shell修改gradle的配置。</li><li>actiivity可以使用<code>activity-alias</code>标签<code><activity-alias android:name="{$applicationId}.XActivity" android:targetActivity=""</code>,至于service,就比较难搞,我这里的解决办法是,给当前包名写了个继承原包名Service的空子类,修改AndroidManifest.xml中的name使用<code>${applicationId}</code>。然后代码中,给如果有startService的地方,就使用<code>Class.forName(BuildConfig.APPLICATION_ID + ".XService")</code>反射。</li></ol><h1 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h1><p><strong>因为这里的脚本过于繁琐,从这里开始,请大家放弃看这一篇,直接去这里[<a href="/2020/09/04/jenkins-android-3/">使用jenkins为android工程打包,支持多包名,改资源(优化方案)</a></strong></p><p>部分shell脚本的源码,这里只是修改build.gradle的脚本,要打包还要再加入<code>./gradlew assembleRelease</code>等其它个性化脚本<br>将完整的shell脚本保存成sh文件,放到工程目录下,在jenkins中执行该sh文件<code>sh ./jenkinsbuild.sh</code></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><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">field_name=('BOOL_FIELD1' )</span><br><span class="line">value=($BOOL_FIELD1 )</span><br><span class="line">#注意 这里是用测试的值 注意value要和filed的name对应 在shell中全是字符串格式</span><br><span class="line">#value=('false' 'false' 'false' 'false' 'false' 'false' 'false' 'false' 'false' 'false')</span><br><span class="line"></span><br><span class="line">for i in "${!field_name[@]}";</span><br><span class="line">do</span><br><span class="line"> echo ${field_name[$i]} ${value[$i]}</span><br><span class="line"> #只有当变量不为null的情况下,才去修改Build文件</span><br><span class="line"> if [ -n "${value[$i]}" ];then</span><br><span class="line"> toReplace='buildConfigField "boolean", \"'${field_name[$i]}'\", "'${value[$i]}'"'</span><br><span class="line"> echo $toReplace</span><br><span class="line"> sed -i 's/buildConfigField \"boolean\", \"'"${field_name[$i]}"'\", \"\([a-zA-Z]*\)\"/'"$toReplace"'/g' build.gradle</span><br><span class="line"> fi</span><br><span class="line">done</span><br><span class="line"></span><br><span class="line">toReplace='applicationId \"'$PACKAGE_NAME'\"'</span><br><span class="line">echo $toReplace</span><br><span class="line">sed -i 's/applicationId \"com.lefo.oldpkg\"/'"$toReplace"'/g' build.gradle</span><br><span class="line"></span><br><span class="line">#字符串变量 操作方式是删除掉gradle文件中旧的 添加新的</span><br><span class="line">field_name=('PT_CLIENT_ID' 'PT_CLIENT_SECRET' 'PT_QQ_ID' 'PT_WX_RELEASE_ID' 'BUGLY_ID' 'APP_TYPE' 'PT_SERVER_SECRET' 'LICENSE_URL' 'POLICY_URL')</span><br><span class="line">value_name=('client_id' 'client_secret' 'qq_id' 'wx_id' 'bugly_id' 'app_type' 'server_secret' 'license' 'policy')</span><br><span class="line"></span><br><span class="line">for i in "${!field_name[@]}";</span><br><span class="line">do</span><br><span class="line"> echo ${field_name[$i]}</span><br><span class="line"> value=${value_name[$i]}</span><br><span class="line">toReplace='buildConfigField \"String\", \"'${field_name[$i]}'\", \"\\\"'${!value}'\\\"\"'</span><br><span class="line">echo $toReplace</span><br><span class="line">sed -i '/buildConfigField \"String\", \"'${field_name[$i]}'\", /d' build.gradle</span><br><span class="line">sed -i '/defaultConfig/a buildConfigField \"String\", \"'${field_name[$i]}'\", \"\\\"'${!value}'\\\"\"' build.gradle</span><br><span class="line">done</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>那么,脚本中,<code>client_id</code>这些,是如何一次性加入到环境变量中的?这里要用到另一款插件,<strong>Environment Injector Plugin</strong>。这款插件可以在执行时往执行环境中注入变量。关于变量的配置信息,我在工程目录下新建了一个文件夹保存的配置文件。就是key=value的格式,就是你熟悉的java properties file。这里的<code>PACKAGE_NAME</code>,在jenkins参数化构建时,给产品提供的是一个选项列表,固定好包名列表给产品选择选,毕竟涉及到client_key的修改,不能随便填。<br><img src="/image/20190329/key-config.jpg"></p><h1 id="改资源该怎么改"><a href="#改资源该怎么改" class="headerlink" title="改资源该怎么改"></a>改资源该怎么改</h1><p>产品居然想要修改APP名称,图标,部分页面的图片,这里要用到gradle配置中的productFlavors。工程中新建一个文件夹<code>lefo</code>,注意要名字对应,然后我们将要替换的资源,按原资源存放位置,放到package1目录下,打包时,不能再执行assembleRelease,这个时候就应该是<code>./gradlew assembleLefoRelease</code></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></pre></td><td class="code"><pre><span class="line">productFlavors{</span><br><span class="line">lefo{</span><br><span class="line"> //产品说,可不可以做到应用显示的名字和内部的名字不一样 可以 给应用单独配一个名字 本来有个资源叫app_name</span><br><span class="line"> manifestPlaceholders = [label:"@string/app_label"]</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>既然想这么定制,那就彻底交给产品,他们想放什么放什么。</p><p>修改应用名称:直接使用jenkins中参数配置,通过sed命令修改strings.xml<br>图标资源替换:最初想的是从某处下载一份资源,然后替换掉包中资源,产品那边和我们一样用的是svn,jenkins在构建的时候,SCM是在构建之前的,也就是说,在构建以前就要checkout代码。问题就来了,资源目录的URL可以做为一个参数传进来,但并不是每一次打包,都需要替换资源。如果URL没有传,那SCM就不能过,构建就会失败,根本走不到编译过程。这里同样要使用插件<strong>Environment Injector Plugin</strong>,它可以在SCM以前,使用脚本修改某个变量的值。</p><p>注意下图中我使用的是<code>groovy script</code>,如果我没有记错的话,只有<code>groovy script</code>是可以在SCM之前执行的。如果没有填,脚本就给它一个内容为空的文件夹路径,SCM配置中,支持<code>${RES_URL}</code>使用。为什么不给<code>RES_URL</code>配一个默认值呢?因为不填写更直观,产品修改的时候,也不用每次都删除旧的再复制上新的还得检查一遍。</p><p><img src="/image/20190329/res-url.jpg"></p><h1 id="续"><a href="#续" class="headerlink" title="续"></a>续</h1><p>产品经理用的很嗨,暂时没有续,为了不涉及隐私,整篇加上前一篇都是一些片段,想直接copy进去使用的可能要失望了。不过,拼凑一起还是可以的,实在不懂的来找我。</p><p><strong>写了一篇最新的介绍,打包脚本更简洁,大家可以结合着看[<a href="2020/09/04/jenkins-android-3/">使用jenkins为android工程打包,支持多包名,改资源(优化方案)</a></strong></p>]]></content>
<summary type="html"><h1 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h1><p>上一篇文章主要写了打包刚开始的配置和参数化构建。这篇文章主要讲一讲在改包名、改资源的打包实践中,常会碰到的问题以及解决办法。如果看博客的人有更好的解决办法,也可以找我交流,关于页面有我联系方式。</p>
<p>打包的主要需求如下:</p>
<ol>
<li>改包名</li>
<li>可以替换icon,可以修改应用名,包括应用内部显示的名称(如版权信息)</li>
<li>可以控制部分功能是否开启</li>
</ol>
<h1 id="改包名分析"><a href="#改包名分析" class="headerlink" title="改包名分析"></a>改包名分析</h1><p>我们知道,改包名只是修改applicationId,和代码中类的package无关,所以基本上代码和AndroidManifest.xml中组件的name,都是不需要改动的。但是还是会涉及到下面的这些问题:</p></summary>
<category term="android打包" scheme="https://www.lefo.me/categories/android%E6%89%93%E5%8C%85/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="jenkins" scheme="https://www.lefo.me/tags/jenkins/"/>
</entry>
<entry>
<title>使用jenkins为android工程打包,支持多包名,改资源(简单上手)</title>
<link href="https://www.lefo.me/2019/03/26/jenkins-android-1/"/>
<id>https://www.lefo.me/2019/03/26/jenkins-android-1/</id>
<published>2019-03-26T06:44:00.000Z</published>
<updated>2024-08-24T21:40:15.783Z</updated>
<content type="html"><![CDATA[<h1 id="下载安装jenkins"><a href="#下载安装jenkins" class="headerlink" title="下载安装jenkins"></a>下载安装jenkins</h1><p>官网地址:<a href="https://jenkins.io/">https://jenkins.io/</a><br>没什么好说的,网上教程一大堆,唯一要做的是要修改jenkins_home目录所在分区,因为将来所有的内容都要放在这里,如果分区太小,指不定哪天就满了,到时就打不了包了。</p><p>实践中,发现/home/jenkins目录也要处理一下,我就碰上了/home/jenkins目录占满了根分区,打开发现都是gradle打包时生成的一些缓存,就将/home/jenkins使用<code>ln</code>命令做了个软连接到另一个分区目录下。</p><span id="more"></span><h1 id="新建jenkins项目"><a href="#新建jenkins项目" class="headerlink" title="新建jenkins项目"></a>新建jenkins项目</h1><p><img src="/image/20190326/create-jenkins.jpg" alt="创建项目"><br><img src="/image/20190326/source.jpg" alt="代码库"></p><p>选择<code>新建任务</code>,填一个名字,选择自由风格的项目,点下方OK<br>进入配置页,源码管理里选择你们所用的源码管理工具,填入地址,用户名认证信息等。<br>注意,这里可能没有git和svn,那么需要你去系统管理,管理插件模块,搜索git或者svn插件并安装。后续还有其它功能需要安装插件。</p><p>构建步骤 选择 Execute shell,Command里填入<code>./gradlew assembleRelease</code>就好了。</p><h1 id="改渠道号、版本号打包"><a href="#改渠道号、版本号打包" class="headerlink" title="改渠道号、版本号打包"></a>改渠道号、版本号打包</h1><p>我们搭建jenkins,肯定不只是简单的打个包。一般常见的需求有,打渠道包,改包名,改资源文件。</p><h2 id="参数化打包,打包时修改渠道号,版本号"><a href="#参数化打包,打包时修改渠道号,版本号" class="headerlink" title="参数化打包,打包时修改渠道号,版本号"></a>参数化打包,打包时修改渠道号,版本号</h2><p>jenkins提供了一个功能叫<code>参数化构建</code>,打包时,可以手动配一些参数。在<code>配置</code>中勾选<code>参数化构建过程</code>,下面添加你需要的参数,支持布尔型,字符串型。这些添加的参数,在shell中可以直接使用${参数名}使用,比如我们可以填入渠道号,versionname,versioncode等,如果你想填APP的名称也可以。</p><p>参数填了,如何在打包时配到gradle文件中呢?</p><p><strong>因为这里的脚本过于繁琐,请大家不要使用sed修改这种方案,直接去这里[<a href="2020/09/04/jenkins-android-3/">使用jenkins为android工程打包,支持多包名,改资源(优化方案)</a></strong></p><p>使用<code>sed</code>命令开修改</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></pre></td><td class="code"><pre><span class="line">#echo get versioncode</span><br><span class="line">vc=$VERSION_CODE;</span><br><span class="line">sed -i 's/android:versionCode=\"\([a-zA-Z0-9.]*\)\"/android:versionCode=\"'$vc'\"/g' AndroidManifest.xml</span><br><span class="line">echo "version code : $vc"</span><br><span class="line"></span><br><span class="line">#echo get versionname</span><br><span class="line">vn=$VERSION_NAME</span><br><span class="line">sed -i 's/android:versionName=\"\([a-zA-Z0-9.]*\)\"/android:versionName=\"'$vn'\"/g' AndroidManifest.xml</span><br><span class="line">echo "version name : $vn"</span><br><span class="line"></span><br><span class="line">./gradlew assembleRelease</span><br></pre></td></tr></table></figure><p>其中,VERSION_CODE和VERSION_NAME就是在jenkins中配置的参数,我这里是改的manifest文件,我们也可以用sed命令修改gradle文件。</p><h2 id="改包名"><a href="#改包名" class="headerlink" title="改包名"></a>改包名</h2><p>通过sed命令修改gradle文件中的<code>build applicationId packagename</code>,首先在参数化构建中,添加一个名称为PACKAGE_NAME的变量,供打包的人填写包名。然后在构建shell前面加入下列shell脚本。</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">toReplace='applicationId \"'$PACKAGE_NAME'\"'</span><br><span class="line">echo $toReplace</span><br><span class="line">sed -i 's/applicationId \"com.old.pkgname\"/'"$toReplace"'/g' build.gradle</span><br></pre></td></tr></table></figure><h2 id="修改构建后生成的名称"><a href="#修改构建后生成的名称" class="headerlink" title="修改构建后生成的名称"></a>修改构建后生成的名称</h2><p>通常jenkins打包完成后,左侧的构建历史,是一个数字序号,时间一长可能就忘了对应关系,所以一般情况需要将名称修改成可读的格式,那么如何在构建时,自动就修改好名称?</p><p>使用插件<code>Version Number</code>,安装该插件后,去到项目配置页,在构建环境下,勾选<code>Create a formatted version number</code>,再在下面的<code>Build Display Name</code>选项勾选<code>Build Display Name</code></p><p>在<code>Environment Variable Name</code>中可以将生成的名称赋值给一个变量名,在后续构建时的shell中可以使用,根据自己喜好起一个就行。</p><p>在<code>Version Number Format String</code>中可以填写想要生成的名称格式,可以使用<code>${变量名}</code>引用环境变量和参数名称,同时点右侧的问号可以看到系统提供的一部分时间相关的变量名。</p><p>如:<code>${VERSION_CODE}_${VERSION_NAME}_${BUILD_ID}</code></p><p><img src="/image/20190326/formatted-version-number.jpg" alt="构建名称"></p><h1 id="后续"><a href="#后续" class="headerlink" title="后续"></a>后续</h1><p>以上讲的就是基本打包流程,但是实际应用中,还存在一些问题,比如,改了包名后,一些第三方的client key也要修改。再加上产品的对资源、功能定制需求也越来越复杂,上面的方式是绝对难满足他们定制化的要求的。下篇见。。。</p><p>下篇博客写在打包实际应用中,是如何通过jenkins改包名和改资源,以及解决一些随包名存在的问题和注意事项。</p><p><strong>写了一篇最新的介绍,打包脚本更简洁,大家可以结合着看[<a href="2020/09/04/jenkins-android-3/">使用jenkins为android工程打包,支持多包名,改资源(优化方案)</a></strong></p>]]></content>
<summary type="html"><h1 id="下载安装jenkins"><a href="#下载安装jenkins" class="headerlink" title="下载安装jenkins"></a>下载安装jenkins</h1><p>官网地址:<a href="https://jenkins.io/">https://jenkins.io/</a><br>没什么好说的,网上教程一大堆,唯一要做的是要修改jenkins_home目录所在分区,因为将来所有的内容都要放在这里,如果分区太小,指不定哪天就满了,到时就打不了包了。</p>
<p>实践中,发现&#x2F;home&#x2F;jenkins目录也要处理一下,我就碰上了&#x2F;home&#x2F;jenkins目录占满了根分区,打开发现都是gradle打包时生成的一些缓存,就将&#x2F;home&#x2F;jenkins使用<code>ln</code>命令做了个软连接到另一个分区目录下。</p></summary>
<category term="android打包" scheme="https://www.lefo.me/categories/android%E6%89%93%E5%8C%85/"/>
<category term="android" scheme="https://www.lefo.me/tags/android/"/>
<category term="jenkins" scheme="https://www.lefo.me/tags/jenkins/"/>
</entry>
</feed>