-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch.xml
More file actions
390 lines (390 loc) · 103 KB
/
search.xml
File metadata and controls
390 lines (390 loc) · 103 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>Android NDK(一) 全面理解JNI技术</title>
<url>/posts/1e51d4cb/</url>
<content><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200322191525.jpg" alt><br><a id="more"></a></p>
<p>开始之前,可以先提几个问题</p>
<ol>
<li>JNI的本质是什么,java和native如何实现互操作?</li>
<li>JNI静态注册与动态注册可以混用吗?</li>
</ol>
<p>首先是理论部分,先弄清楚NDK与JNI的概念。</p>
<h1 id="NDK与JNI"><a href="#NDK与JNI" class="headerlink" title="NDK与JNI"></a>NDK与JNI</h1><p>NDK全称为Native Development Kit,是一套允许使用 C 和 C++ 等语言,以原生代码实现应用的工具集,包括但不限于编译工具、公共库等。</p>
<p>在Android NDK开发中,应用层就是借助JNI以实现对本地代码的调用。<br>不只是应用层,JNI技术也广泛应用在Framework层中,主要源码在 <code>framework/base/</code>目录下</p>
<p>下图简单地从另一个角度看Android的系统架构,其中,JNI接口的实现在Android虚拟机,宿主环境包括Linux kernel。<br><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200315121813.png" alt></p>
<h1 id="JNI技术"><a href="#JNI技术" class="headerlink" title="JNI技术"></a>JNI技术</h1><h3 id="JNI是什么"><a href="#JNI是什么" class="headerlink" title="JNI是什么"></a>JNI是什么</h3><p>JNI,全称Java Native Interface,是一种编程框架,使得Java虚拟机中的Java程序与本地应用/或库可以相互调用。本地程序一般是用其它语言(C、C++或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序。为了通俗地介绍JNI的作用,摘抄网上的一个比喻,就是中国讲普通话,日本讲日语,我们无法交流,如果此时我们都学会了英语,就可以用英语交流了。JNI就是java和本地语言之间交互的媒介。</p>
<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200315111805.png" alt></p>
<p>上图,我们可以看到JNI接口的具体实现是在虚拟机。历史上存在过不同本地方法接口的实现,使得开发者不得不编写和维护多种不同的版本。JNI的目标就是提供一套统一的,考虑充分的标准接口,使得开发者可以只写一个版本的本地代码,在不同的Java虚拟机上运行,也就是实现虚拟机无关性。</p>
<h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><ol>
<li>实现Java库无法提供的基于平台系统相关特性的功能</li>
<li>直接复用现有的本地开源库</li>
<li>使用C/C++开发对时间和性能要求很高的逻辑,如音视频,图片处理,游戏逻辑等</li>
<li>保护关键代码,增大反编译难度</li>
<li>…</li>
</ol>
<h3 id="优势与劣势"><a href="#优势与劣势" class="headerlink" title="优势与劣势"></a>优势与劣势</h3><p>如上的应用场景就是使用JNI的优势,当然,任何技术都会在解决一个问题的同时带来其他新的问题,使用JNI也会有劣势:</p>
<ol>
<li>使用JNI编程时,容易操作不当会引起崩溃。相比Java崩溃,Native崩溃难以捕获与定位</li>
<li>可能引起内存泄漏等</li>
<li>JNI上下文环境切换开销</li>
</ol>
<h3 id="其他方案对比"><a href="#其他方案对比" class="headerlink" title="其他方案对比"></a>其他方案对比</h3><p>有些替代方案可以允许Java与其他语言编写的代码进行互操作,例如:</p>
<ol>
<li>Java应用程序可以通过TCP/IP连接或者其他IPC通信机制与本地应用进行通信</li>
<li>可以利用分布式对象技术,如Java IDL API<br>…</li>
</ol>
<p>但是,以上方案的共性是将Java应用程序和原生代码隔离在不同的进程空间中。而JNI技术是在相同的进程空间。使用其他替代方案的问题在于,进程间操作麻烦且低效,复制和传输数据会增加额外开销。</p>
<p>通过上文对JNI技术有了比较清晰的认识之后,我们再看看实战部分。</p>
<h1 id="NDK-JNI开发流程"><a href="#NDK-JNI开发流程" class="headerlink" title="NDK/JNI开发流程"></a>NDK/JNI开发流程</h1><p>在Android Studio中,我们可以直接新建一个Native C++工程,用kotlin实现一个最简单的JNI调用。</p>
<ol>
<li>Java层声明Native方法<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> com.devnan.ndk</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MainActivity</span> : <span class="type">AppCompatActivity</span></span>() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">external</span> <span class="function"><span class="keyword">fun</span> <span class="title">stringFromJNI</span><span class="params">()</span></span>: String</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
</li>
</ol>
<ol start="2">
<li><p>JNI层实现Java层声明的方法, 可以调用底层库或者回调Java层方法</p>
<figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="comment">//native-lib.cpp</span></span><br><span class="line"><span class="keyword">extern</span> <span class="string">"C"</span> JNIEXPORT jstring JNICALL</span><br><span class="line">Java_com_devnan_ndk_MainActivity_stringFromJNI(</span><br><span class="line"> JNIEnv* env,</span><br><span class="line"> jobject <span class="comment">/* this */</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> hello = <span class="string">"Hello from C++"</span>;</span><br><span class="line"> <span class="keyword">return</span> env->NewStringUTF(hello.c_str());</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
</li>
<li><p>Java层加载编译后生成的动态库</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="comment">// MainActivity</span></span><br><span class="line"><span class="keyword">companion</span> <span class="keyword">object</span> {</span><br><span class="line"> <span class="keyword">init</span> {</span><br><span class="line"> System.loadLibrary(<span class="string">"native-lib"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
</li>
<li><p>定义cmake脚本,编译C/C++源码生成动态库so文件</p>
</li>
</ol>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cmake_minimum_required(VERSION 3.4.1)</span><br><span class="line">add_library(native-lib SHARED native-lib.cpp)</span><br></pre></td></tr></table></figure>
<p>虽然IDE帮我们做了大部分事情,但是还是要明白整个过程的本质。<br>Android NDK开发中,在编译阶段,cmake会根据CMakeLists定义的规则,借助NDK工具包的交叉编译链将C/C++代码编译生成动态库so文件(当然也可以是静态库),最后gradle会通过package task将so一起打包到APK中,这是IDE以及gradle帮我们做的。另外,在安装阶段,Android系统安装apk时会通过PackageManagerService 将apk lib 目录下的 so 文件会被解压到对应apk目录,一般在 /data/app/package-name-xxx/lib 目录;<br>在运行阶段,调用 System.loadLibrary 会首先在对应apk目录下查找so,找不到才在系统目录下查找。这里涉及到so的拷贝和加载策略,详细分析会放到后续文章。</p>
<h1 id="JNI数据类型"><a href="#JNI数据类型" class="headerlink" title="JNI数据类型"></a>JNI数据类型</h1><p>由于Java语言与C/C++语言数据类型的不匹配,需要单独定义一系列的数据类型转换关系来完成两者之间的映射。</p>
<p>基本数据类型:<br><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200315143537.png" alt><br>在NDK包的jni.h文件可以看到定义</p>
<figure class="highlight h"><table><tr><td class="code"><pre><span class="line"><span class="comment">//$NDK/sysroot/usr/include/jni.h</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Primitive types that match up with Java equivalents. */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">uint8_t</span> jboolean; <span class="comment">/* unsigned 8 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int8_t</span> jbyte; <span class="comment">/* signed 8 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">uint16_t</span> jchar; <span class="comment">/* unsigned 16 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int16_t</span> jshort; <span class="comment">/* signed 16 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int32_t</span> jint; <span class="comment">/* signed 32 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">int64_t</span> jlong; <span class="comment">/* signed 64 bits */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">float</span> jfloat; <span class="comment">/* 32-bit IEEE 754 */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">double</span> jdouble; <span class="comment">/* 64-bit IEEE 754 */</span></span><br></pre></td></tr></table></figure>
<p>引用数据类型,详细可以查看jni.h<br><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200315143655.png" alt></p>
<h1 id="JNI签名规则"><a href="#JNI签名规则" class="headerlink" title="JNI签名规则"></a>JNI签名规则</h1><p>由于Java支持方法重载,在JNI访问Java层方法时仅靠函数名无法唯一确定一个方法,因此JNI提供了一套签名规则,以使用一个字符串来唯一确定一个方法。其规则:(参数1类型签名参数2类型签名…)返回值类型签名,比如:<br>native方法: <code>external fun stringFromJNI(bool:Boolean, str: String): String</code><br>类型签名: <code>(ZLjava/lang/String;)Ljava/lang/String;</code><br>建议对class文件使用<code>javap -s -p</code>拿到签名信息,毕竟手写容易出错。<br><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200322190933.png" alt></p>
<h1 id="JavaVM与JNIEnv"><a href="#JavaVM与JNIEnv" class="headerlink" title="JavaVM与JNIEnv"></a>JavaVM与JNIEnv</h1><p>JNI技术中,native层怎么调用到java层呢?就是利用JavaVM 和 JNIEnv 这两个结构体。可以说,他们就是native通往java世界大门的钥匙。<br>JavaVM是进程虚拟机环境,每个进程有且只有一个JavaVM实例。Android应用进程启动时调用AndroidRuntime.cpp的start()方法完成了JavaVM的启动。<br>JNIEnv是线程上下文环境,每个线程有且只有一个JNIEnv实例,可以看到每个JNI方法的第一个参数就是JNIEnv。<br>同样,来看一下JavaVM在jni.h的定义</p>
<figure class="highlight h"><table><tr><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">if</span> defined(__cplusplus)</span></span><br><span class="line"><span class="keyword">typedef</span> _JNIEnv JNIEnv;</span><br><span class="line"><span class="keyword">typedef</span> _JavaVM JavaVM;</span><br><span class="line"><span class="meta">#<span class="meta-keyword">else</span></span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">JNINativeInterface</span>* <span class="title">JNIEnv</span>;</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="keyword">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">JNIInvokeInterface</span>* <span class="title">JavaVM</span>;</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br></pre></td></tr></table></figure>
<figure class="highlight h"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> _<span class="title">JavaVM</span> {</span></span><br><span class="line"> <span class="keyword">const</span> <span class="class"><span class="keyword">struct</span> <span class="title">JNIInvokeInterface</span>* <span class="title">functions</span>;</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">if</span> defined(__cplusplus)</span></span><br><span class="line"> <span class="function">jint <span class="title">DestroyJavaVM</span><span class="params">()</span></span></span><br><span class="line"><span class="function"> </span>{ <span class="keyword">return</span> functions->DestroyJavaVM(<span class="keyword">this</span>); }</span><br><span class="line"> <span class="function">jint <span class="title">AttachCurrentThread</span><span class="params">(JNIEnv** p_env, <span class="keyword">void</span>* thr_args)</span></span></span><br><span class="line"><span class="function"> </span>{ <span class="keyword">return</span> functions->AttachCurrentThread(<span class="keyword">this</span>, p_env, thr_args); }</span><br><span class="line"> <span class="function">jint <span class="title">DetachCurrentThread</span><span class="params">()</span></span></span><br><span class="line"><span class="function"> </span>{ <span class="keyword">return</span> functions->DetachCurrentThread(<span class="keyword">this</span>); }</span><br><span class="line"> <span class="function">jint <span class="title">GetEnv</span><span class="params">(<span class="keyword">void</span>** env, jint version)</span></span></span><br><span class="line"><span class="function"> </span>{ <span class="keyword">return</span> functions->GetEnv(<span class="keyword">this</span>, env, version); }</span><br><span class="line"> <span class="function">jint <span class="title">AttachCurrentThreadAsDaemon</span><span class="params">(JNIEnv** p_env, <span class="keyword">void</span>* thr_args)</span></span></span><br><span class="line"><span class="function"> </span>{ <span class="keyword">return</span> functions->AttachCurrentThreadAsDaemon(<span class="keyword">this</span>, p_env, thr_args); }</span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span> <span class="comment">/*__cplusplus*/</span></span></span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>以上可以看到,JavaVM的所有操作都是由结构体 JNIInvokeInterface 实现的。<br><figure class="highlight h"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">JNIInvokeInterface</span> {</span></span><br><span class="line"> <span class="keyword">void</span>* reserved0;</span><br><span class="line"> <span class="keyword">void</span>* reserved1;</span><br><span class="line"> <span class="keyword">void</span>* reserved2;</span><br><span class="line"></span><br><span class="line"> jint (*DestroyJavaVM)(JavaVM*);</span><br><span class="line"> jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, <span class="keyword">void</span>*);</span><br><span class="line"> jint (*DetachCurrentThread)(JavaVM*);</span><br><span class="line"> jint (*GetEnv)(JavaVM*, <span class="keyword">void</span>**, jint);</span><br><span class="line"> jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, <span class="keyword">void</span>*);</span><br><span class="line">};</span><br></pre></td></tr></table></figure></p>
<p>前三个指针作为保留使用,后面为函数指针。根据函数名可以推测大概的用法,其中,AttachCurrentThread 函数可以把当前线程附着到虚拟机,getEnv 函数用来获取 JNIEnv 指针。JavaVM的开发使用场景是,若无法获取到线程的JNIEnv,比如C/C++函数回调到Java层,可以全局保存JavaVM,通过AttachCurrentThread将当前线程附着到 JavaVM 以拿到 JNIEnv,最后记得调用 DetachCurrentThread 函数,否则会发生内存泄露。</p>
<p>同理,也可以定位到JNIEnv最后由结构体JNINativeInterface实现。<br><figure class="highlight h"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">JNINativeInterface</span> {</span></span><br><span class="line"> ...</span><br><span class="line"> jint (*GetVersion)(JNIEnv *);</span><br><span class="line"> jclass (*DefineClass)(JNIEnv*, <span class="keyword">const</span> <span class="keyword">char</span>*, jobject, <span class="keyword">const</span> jbyte*,</span><br><span class="line"> jsize);</span><br><span class="line"> jclass (*FindClass)(JNIEnv*, <span class="keyword">const</span> <span class="keyword">char</span>*);</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line">};</span><br></pre></td></tr></table></figure></p>
<p>可以看到,JNINativeInterface包含了JNI编程中经常用到的JNI函数,可以说JNIEnv就是我们操作 Java 层的入口。篇幅有限,由于JNIEnv相关函数过多,不在此处详细描述。如果对Android ART虚拟机中JNI接口的实现感兴趣,可以看看 <code>platform/art/runtime/jni</code> 目录,JNIEnv函数实现应该在<code>jni_internal.cc</code> 文件。</p>
<h1 id="JNI静态注册与动态注册"><a href="#JNI静态注册与动态注册" class="headerlink" title="JNI静态注册与动态注册"></a>JNI静态注册与动态注册</h1><p>JNI技术中,java层怎么找到native方法呢?就是利用了JNI静态注册或动态注册。<br>顾名思义,静态注册,是根据函数名来建立Java方法和JNI方法间的对应关系。<br>上文例子中,Java层 <code>com.devnan.ndk.MainActivity</code>的native方法 stringFromJNI ,对应的JNI方法是<code>JNIEXPORT jstring JNICALL Java_com_devnan_ndk_MainActivity_stringFromJNI</code>,即JNI规则是Java_包名_类名_方法名。Android应用层一般采用这种方式进行静态注册。<br>其中,JNIEXPORT和JNICALL是两个宏定义。default表示外部可见,类似public,JNICALL在Linux平台只是一个空定义。<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#define JNIEXPORT __attribute__ ((visibility ("default")))</span><br><span class="line">#define JNICALL</span><br></pre></td></tr></table></figure></p>
<p>而动态注册原理是,在 Java 层调用 <code>System.loadLibrary</code> 时,底层最后会调用 <code>JNI_OnLoad ()</code> 函数。在这个函数中通过JNIEnv 提供的 <code>RegisterNatives()</code> 方法,我们可以传递 JNINativeMethod 数组主动建立native方法和JNI方法的映射关系。<br>JNINativeMethod在jni.h的定义如下:<br><figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> {</span></span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* name; <span class="comment">//native方法的方法名</span></span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* signature; <span class="comment">//native方法的签名</span></span><br><span class="line"> <span class="keyword">void</span>* fnPtr; <span class="comment">//JNI层对应实现的方法指针</span></span><br><span class="line">} JNINativeMethod;</span><br></pre></td></tr></table></figure></p>
<p>对应上文例子就是<br><figure class="highlight h"><table><tr><td class="code"><pre><span class="line"><span class="comment">//native-lib.h</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">char</span> *className = <span class="string">"com/devnan/ndk/MainActivity"</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> JNINativeMethod gMethods[] = {</span><br><span class="line"> <span class="comment">/* name, signature, funcPtr */</span></span><br><span class="line"> {<span class="string">"stringFromJNI"</span>, <span class="string">"()Ljava/lang/String;"</span>, (<span class="keyword">void</span> *) stringFromJNI},</span><br><span class="line">};</span><br></pre></td></tr></table></figure></p>
<figure class="highlight c++"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">//native-lib.cpp</span></span><br><span class="line"></span><br><span class="line">```C++</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * JNI方法随意起名为"stringFromJNI"</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">JNIEXPORT jstring JNICALL <span class="title">stringFromJNI</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> JNIEnv *env,</span></span></span><br><span class="line"><span class="function"><span class="params"> jobject <span class="comment">/* this */</span>)</span> </span>{</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> hello = <span class="string">"Hello JNI!"</span>;</span><br><span class="line"> <span class="keyword">return</span> env->NewStringUTF(hello.c_str());</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * System.loadLibrary()最后会调用到JNI_OnLoad</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function">JNIEXPORT jint JNICALL <span class="title">JNI_OnLoad</span><span class="params">(JavaVM *vm, <span class="keyword">void</span> *reserved)</span> </span>{</span><br><span class="line"> JNIEnv *env = <span class="literal">NULL</span>;</span><br><span class="line"> <span class="keyword">if</span> (vm->GetEnv((<span class="keyword">void</span> **) &env, JNI_VERSION_1_6) != JNI_OK) {</span><br><span class="line"> <span class="keyword">return</span> JNI_ERR;</span><br><span class="line"> }</span><br><span class="line"> assert(env != <span class="literal">NULL</span>);</span><br><span class="line"> <span class="keyword">if</span> (!registerNatives(env)) {</span><br><span class="line"> <span class="keyword">return</span> JNI_ERR;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> JNI_VERSION_1_6;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 调用JNIEnv函数RegisterNatives进行注册</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)</span></span><br><span class="line"><span class="comment"> * clazz: java类名,通过FindClass得到</span></span><br><span class="line"><span class="comment"> * methods: JNINativeMethod结构体指针</span></span><br><span class="line"><span class="comment"> * nMethods: 方法个数</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">registerNatives</span><span class="params">(JNIEnv *env)</span> </span>{</span><br><span class="line"> jclass clazz;</span><br><span class="line"> clazz = env->FindClass(className);</span><br><span class="line"> <span class="keyword">if</span> (clazz == <span class="literal">NULL</span>) {</span><br><span class="line"> <span class="keyword">return</span> JNI_FALSE;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> JNI_FALSE;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> JNI_TRUE;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>可以看到,JNI动态注册比静态注册要麻烦,但是,动态注册效率更快。Android Framework层几乎都是动态注册,Android系统在启动时会执行一些系统函数的JNI注册,从而把映射关系注册到了虚拟机中。init进程启动zygote进程后, 在AndroidRuntime中会先启动虚拟机,然后开始注册JNI方法。简要流程如下:<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">//frameworks/base/cmds/app_process/app_main.cpp</span><br><span class="line"></span><br><span class="line">- zygote进程启动(App_main.cpp执行main函数)</span><br><span class="line"> - AndroidRuntime.start</span><br><span class="line"> - startVm //启动虚拟机</span><br><span class="line"> - startReg //注册JNI函数</span><br><span class="line"> - register_jni_procs //遍历RegJNI数组进行注册</span><br></pre></td></tr></table></figure></p>
<p>小结:<br>对比动态注册,静态注册的优点是使用简单明了,但是缺点是首次调用时需要Java虚拟机搜索JNI方法以建立映射关系,第一次的运行效率低。另外,静态注册和动态注册可以混用,也就是一部分native方法用静态注册,另一部分用动态注册。思考一下,如果一个native方法同时做了静态注册和动态注册会发生什么情况?那么最后调用的是动态注册的JNI方法的实现,因为 <code>System.loadLibrary("native-lib")</code>是一般放在static代码块,也就是在类初始化阶段中,通过动态注册已经在虚拟机保存了JNI的映射关系(kotlin的写法本质也一样),而静态注册是第一次调用native方法时才建立这种映射。</p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>本文主要介绍了JNI技术的背景、应用场景和优缺点等,并结合 Android 介绍了JNI的开发流程,JNI数据类型和签名规则,JavaVM与JNIEnv的概念,以及JNI静态注册和动态注册的用法和区别。代码已放到 <a href="https://github.com/devnan/ndk-practices">https://github.com/devnan/ndk-practices</a></p>
]]></content>
<tags>
<tag>ndk</tag>
</tags>
</entry>
<entry>
<title>关于Gradle增量编译的坑</title>
<url>/posts/82b9ce3/</url>
<content><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200301223321.png" alt><br><a id="more"></a></p>
<p>又到周末时间了,目前的计划是至少每周一更,欢迎关注~</p>
<p>大家都知道gradle编译很慢,在充分利用各种优化编译的配置之后还是不够理想,既然如此,能否再加快增量编译速度?</p>
<p>今天经过本地的反复验证,基本了解了gradle增量编译失效的其中一个原因(坑),本文也会阐述整个验证过程以及如何避开这个坑。在开始之前,先直接抛出结论,就是修改包含常量的类会触发gradle全量编译,使增量编译失效,从而增加了编译时间。这里先简单介绍下全量编译和增量编译的概念。</p>
<h2 id="全量编译与增量编译"><a href="#全量编译与增量编译" class="headerlink" title="全量编译与增量编译"></a>全量编译与增量编译</h2><p>顾名思义,全量编译通俗一点来说就是,工程里的所有代码文件在构建时都会被重新编译一次。这样的弊端是项目规模越大,编译时间就越长。于是,为了加快编译速度,后面发展出了增量编译。<br>增量编译的目标是尽可能地只编译变化的部分,如果项目中修改了一个代码文件,只需编译这个文件以及一系列依赖了它的文件。所以,最理想的情况当然就是除了第一次会触发全量编译以外,后续都是增量编译。但是,现在有一些特定情况还是会全量编译,比如编译失败之后的下一次编译;gradle 4.6版本前使用注解处理器,还有本文所说的情况等等。我们的目标也就是尽可能的去避开这些触发全量编译的条件,最大程度地利用增量编译带来的速度提升。</p>
<p>知道以上的概念后,我们回到增量编译失效的问题上。</p>
<h2 id="修改包含常量的类将触发全量编译?"><a href="#修改包含常量的类将触发全量编译?" class="headerlink" title="修改包含常量的类将触发全量编译?"></a>修改包含常量的类将触发全量编译?</h2><p>如果修改的类中包含了常量(注意:本文所说的常量都是指<code>public static final</code>常量),那么本次修改及依赖的模块需要全量编译。关于此问题的讨论可以查看gradle下的这个issue:full rebuild if a class contains a constant #2767,<a href="https://github.com/gradle/gradle/issues/2767">https://github.com/gradle/gradle/issues/2767</a>,</p>
<p>首先要解释下为什么gradle要这么处理?<br>因为static final常量的字面量值在编译阶段就会加入到文件的常量池中,也就是常量池会直接把值编译到其他类中,而gradle并不知道有哪些类可能使用了这个常量。举个简单的例子理解下~<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//TestA.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestA</span></span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String NAME = <span class="string">"devnan"</span>;</span><br><span class="line">}</span><br><span class="line"><span class="comment">//TestB.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestB</span></span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span></span>{</span><br><span class="line"> System.out.println(<span class="string">"name = "</span> + TestA.NAME);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>现在编译TestA.java和TestB.java,运行<code>java TestB</code>,显然结果是:<br><code>name = devnan</code></p>
<p>现在我们修改<code>TestA.java</code>如下:<br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="comment">//TestA.java</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestA</span></span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> String NAME = <span class="string">"linqinan"</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>编译TestA.java后,重新运行<code>java TestB</code>,此时结果依然是:<br><code>name = devnan</code></p>
<p>没错,编译器已经将name的值在编译时写进了TestB字节码中。因此,我们不能只增量编译TestA,应该也要编译TestB才能保证输出结果的正确性。</p>
<p>同理,gradle目前的做法是,只要检测到包含常量的类发生了修改,为了保证编译的正确性,就会触发全量编译。这里可以看到gradle并不知道修改的部分是否是常量,也不知道是哪些类引用了常量。</p>
<p>这个issue最后被关闭了,在最后开发者建议使用静态方法获取私有常量来代替公有常量,例如:<br><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Replace this: public static final int MAGIC = 23</span><br><span class="line">With this: public static int magic() { return 23; }</span><br></pre></td></tr></table></figure></p>
<p>这么做其实就是把常量从编译时确定变成了运行时确定。这种做法是否合理呢?<br>个人认为,这相当于gradle作为一个构建工具,在推荐Java的代码规范,以满足其最佳的编译性能,没有解决根本问题。</p>
<p>现在,还有另一个让我困惑的点是,gradle官方有一篇blog明确说明了gradle 3.4解决了常量修改会触发全量编译的问题,详见链接<br><a href="https://blog.gradle.org/incremental-compiler-avoidance" target="_blank" rel="noopener">https://blog.gradle.org/incremental-compiler-avoidance</a>,这不是前后矛盾了?所以,我们有必要先验证下这个问题是否真的出现。本文采用的是断点调试gradle的方法。</p>
<h2 id="断点调试Gradle确定问题"><a href="#断点调试Gradle确定问题" class="headerlink" title="断点调试Gradle确定问题"></a>断点调试Gradle确定问题</h2><p>在工程中引入gradleApi依赖以方便我们在源码上断点。<br><figure class="highlight gradle"><table><tr><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> {</span><br><span class="line"> compileOnly gradleApi()</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>我们想验证的是最新的gradle版本6.2.1,所以要修改下gradle-wrapper.properties:<br><code>distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip</code></p>
<p>关于增量编译的逻辑在incremental包下,主要的断点是<code>SourceFileChangeProcessor</code><br><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200301113156.png" alt></p>
<p><code>SelectiveCompiler</code><br><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200301113215.png" alt></p>
<p>验证流程大概就是以下这些操作是否引起全量编译:</p>
<ol>
<li>新建/修改一个包含/不包含常量的java文件</li>
<li>新建/修改一个包含/不包含常量的kotlin文件</li>
</ol>
<p>另外,最简单明了的做法是直接查看编译产物.class文件的修改时间,也能知道编译了哪些文件,确定增量编译的范围。<br>java编译后class文件在<code>build/intermediates/javac/debug/classes</code>,<br>kotlin编译后class文件在<code>build/tmp/kotlin-classes/debug</code>。</p>
<h2 id="目前结论"><a href="#目前结论" class="headerlink" title="目前结论"></a>目前结论</h2><p>经过以上验证之后,可以发现此问题确实存在,具体表现是:</p>
<ol>
<li>修改包含常量的java文件会引起全量编译(任何修改,包括添加注释或换行)</li>
<li>修改kotlin文件的常量会引起全量编译,但修改非常量区域不会</li>
<li>新建包含常量的kotlin/java文件不会引起全量编译</li>
</ol>
<p>需要明确的是,这里说的全量编译具体指的是全量javac,javac的工作是把.java编译成.class,如果表现在gradle task上的话,就是执行<code>compileJavaWithJavac</code>,我们关注<code>compileJavaWithJavac</code>的耗时也可以基本判断是全量javac还是增量javac。</p>
<h2 id="如何避开增量编译的坑"><a href="#如何避开增量编译的坑" class="headerlink" title="如何避开增量编译的坑"></a>如何避开增量编译的坑</h2><p>在目前gradle最新版本还没有解决这个问题的情况下,根据上面的测试结论,我们可以有以下做法来尽可能利用好增量编译:</p>
<ol>
<li>对于不必要声明public的常量修改为private</li>
<li>对于public常量可以统一放到一个不经常修改的文件中</li>
<li>官方建议的使用静态方法来获取private常量</li>
<li>从java迁移到kotlin,kotlin的增量支持更好。虽然目前kotlin的全量编译要比java慢,但是在开发过程中我们大多数情况都是增量编译。</li>
</ol>
<h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>在做完实验并得出结论后,偶然又看到gardle的一个opened issues:<br>Make incremental compile faster on constant changes #6482,<a href="https://github.com/gradle/gradle/issues/6482">https://github.com/gradle/gradle/issues/6482</a>,这个讨论也解答了我上文的困惑,就是此问题确实存在,gradle 3.4有尝试解决它,但是忽视了其他问题,所以应该是gradle 3.5又回滚了,修改常量还是会全量编译。具体原因可以参考issues的一句话:<br><code>Inlined constants can be derived from other constants. 3.4 was broken in that case and 3.5 fixed that.</code><br>内联常量可以从其他常量派生?对于这句话就不太理解意思了,估计要看看gradle3.4代码。</p>
<h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>还有一点想说的就是,gradle一直在提升增量编译性能,gradle 6.0以上也对java和groovy增量编译做了优化,解决了更长的依赖链需要编译更多文件的问题。这也是为什么建议使用最新gradle版本的原因。Android Studio前几天发布了3.6稳定版,AGP(Android gradle plugin)也发布了3.6.0版本,搭配的gradle版本是5.6.4+。所以,现在可以直接升级到AGP 3.6.0 + gradle 6.0以上尝尝鲜,当然了,适配的坑也会有的。</p>
<p>好了,终于写完了。关于gradle这块后面也想继续写一个系列好好理一理。最后,本文如有错误与疑问,欢迎指正与交流~</p>
]]></content>
<tags>
<tag>gradle</tag>
</tags>
</entry>
<entry>
<title>说说Android分区存储</title>
<url>/posts/b1cb81e6/</url>
<content><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200223155102.jpg" alt><br><a id="more"></a></p>
<p>前几天,Android11预览版出来了。和Android10一样,继续加强权限限制和隐私保护,我们也都看到了scoped storage(本文称为分区存储)这块的变化,即Android11将强制执行分区存储。详见<a href="https://developer.android.com/preview/privacy" target="_blank" rel="noopener">https://developer.android.com/preview/privacy</a></p>
<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200222213902.png" alt></p>
<p>分区存储是什么?可能有些开发者还没适配Android10,所以这里简单说一下枯燥的概念,先直接上图。</p>
<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200223120131.png" alt></p>
<p>所谓分区,就是每个App都有一个属于自己的目录,我们称为App-specific目录,App在这个目录可以直接访问文件,无需申明权限。App-specific目录其实包括了两个部分,一个是我们以前所说的App内存目录<code>/data/data/packageName</code>,另一个就是App外存目录<code>/sdcard/Android/data/packageName</code>。当应用卸载时,App-specific目录的内容也自然会被删除。</p>
<p>除了定义了App-specific目录,分区存储的主要目的其实是限制应用对App-specific目录以外的文件操作。想想Android 10之前,应用的写入权限范围真的太大了,可以随便在sdcard的任何一个目录创建文件,导致文件管理混乱,而且就算应用卸载了,很多文件也遗留在sdcard里占用着存储空间,用户也完全不知道,Android设备经常出现存储空间不够。所以,Android10才大搞分区存储来限制权限,不让开发者这么乱来了。那么应用要在App-specific目录以外创建文件该怎么做?</p>
<p>首先,我们可以把App-specific目录以外的区域称为公共目录。公共目录又分为了多媒体目录和下载目录。在多媒体目录下,应用可以通过MediaStore API来创建多媒体文件(图片、音频、视频);而在下载目录下,也就是对于其他类型的文件(文档等),应用只能通过Android提供的SAF存储访问框架来创建,用户可以用系统的文件选择器明确地选择想要的存储位置。至于公共目录为什么还要细分,可能是因为不同的文件类型使用场景和访问频率不一样,像多媒体文件比较常用,总不能创建一个图片也要让用户选择位置吧。</p>
<p>理解分区存储的概念以后,我们来看看应用适配分区存储要怎么做。首先要知道,对于很多不遵循存储规范的较大体量应用,适配分区存储会比较难受。所以Android10 beta版本刚出来的时候,开发者都在Android社区反馈适配的复杂性。于是,google爸爸在开发者体验和用户体验中间选择了站在开发者这边,Android10 beta3版本在分区存储上选择了妥协,就是应用可以在Manifest设置<code>requestLegacyExternalStorage</code>,以继续使用传统的存储方式,这样就给了开发者们更多的适配时间。目前可以确定的是,Android11会强制执行分区存储,如果使用google的亲儿子系列机型,升级至最新的Android11预览版本,一大波没有适配分区存储的应用都会崩溃。不过离Android11稳定版出来还有一段时间,还没适配分区存储的应用可以开始了。</p>
<p>回到应用适配分区存储怎么做的问题上,我们可以直接在项目代码中全局搜索<code>Environment.getExternalStorageDirectory()</code>,看看在sdcard根目录而非App-specific目录下,直接通过File API来操作文件的代码有多少处,基本就可以判断适配分区存储的工作量了。具体的适配细节可以阅读官方文档,参考以下链接:<a href="https://developer.android.com/training/data-storage/files/external-scoped?hl=zh-cn" target="_blank" rel="noopener">https://developer.android.com/training/data-storage/files/external-scoped?hl=zh-cn</a></p>
<p>另外,开发者需要注意的是,应用适配了Android10,也就是target version升级到29之后,用户升级此应用后,应用还是沿用传统的存储方式。也就是说,分区存储特性只针对Android10上新安装的应用生效。所以,开发的时候记得卸载重装,不然发现不了Android10设备上应用存在的存储问题。</p>
<p>总的来看,分区存储的结果对于用户体验来说是相当直观的,最起码Android根目录会变得非常整洁。但是,个人觉得这件事还是太晚做了,看看iOS从一开始就不允许应用在公共存储里随便放东西,Android直到版本10才把这件事情做了一半。</p>
]]></content>
<tags>
<tag>Android</tag>
</tags>
</entry>
<entry>
<title>2019年总结</title>
<url>/posts/b15a3c33/</url>
<content><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200126211450.jpg" alt></p>
<a id="more"></a>
<h3 id="新环境与思考"><a href="#新环境与思考" class="headerlink" title="新环境与思考"></a>新环境与思考</h3><p>过来新环境一个多月了,工作上还算充实。有输入有输出,整体感觉很棒,也还需要继续加油。年底的时候换环境,看起来是比较吃亏的,所以之前是下了一个比较大的决心,就为了快速渡过自己的瓶颈期。我自己的理解是,技术人所谓的瓶颈期并不是技术本身的瓶颈,而是外部环境带给人的认知上的瓶颈。一个技术人在某一个狭小的环境待久了,很可能以为这是技术的全部;在一个地方拧螺丝很熟练,很容易散失对技术的好奇心,没有了之前那股冲劲。所以工作前几年,我还是想尽量扩大自己的技术视野,当业务的发展已经满足不了自身的成长,我知道是时候要换环境了。</p>
<h3 id="2019回顾"><a href="#2019回顾" class="headerlink" title="2019回顾"></a>2019回顾</h3><p>2019年,总体过得还好,比上一年有进步。但是这一年定下的目标也只完成了部分,还是比较懒的。年初定的目标是看完n本技术书,后面一想也没什么意义,因为平时看技术书都是按照章节或主题来看的,而不是一本本来,很难衡量完成情况。所以干脆就不按目标来,想看、需要看就翻一翻书。<br>2019最迷茫的时候应该是年中,从一个新业务组出来,一时不知道要去哪个组好。在新业务组应该是最有干劲的时候,很多项目从0到1,学到不少东西。换组之后,感觉成长速度放慢了,脑子也开始想东想西。<br>2019最忙的时候是11月。一边需求压身,一边准备简历。从开始复习到面试只有两周,这么赶是为了早点过去新环境以减少损失,毕竟年终奖没了。全程一共面了4家,印象比较深的是bigo和tx的面试,因为bigo是第一家面试,有点紧张,感觉准备不够充分。终面面试官刚好不在现场,只能电话面,更紧张了,也问的很深,面完以为自己跪了,全程差不多花了一个下午。而tx的全程面试时间比较长,从上午开始,终面从中午等到天黑,听说是大佬太忙了,我只能整个下午在大厦办公室里边等边玩自己电脑,有一种是这里员工的错觉。那天晚上还被顺风车司机放了鸽子,在深圳夜里的寒风中走了很久很久…总之,这几次的面试经历既坎坷又宝贵,也是一个全面认清自己的机会,让我知道哪方面不足还需要学习的,感谢所有专业又耐心的面试官们~</p>
<h3 id="2020年"><a href="#2020年" class="headerlink" title="2020年"></a>2020年</h3><p>2020年,充满希望的一年。主要是希望自己可以不那么懒,具体的目标和计划就不说了,我怕说出来就不灵了= =</p>
]]></content>
<tags>
<tag>总结</tag>
</tags>
</entry>
<entry>
<title>内存泄漏之库工程集成LeakCanary</title>
<url>/posts/b15a3c38/</url>
<content><![CDATA[<p>业务项目中采用模块化的架构,需要在每个模块集成LeakCanary,这样可以在模块的开发调试阶段发现内存泄漏问题,比较好的做法是把LeakCanary下沉到基础的库工程中。</p>
<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p><del>1. 库工程默认只会发布release包,官网的集成依赖方式是release采用no-op空实现,所以需要对库工程做处理</del><br>注:gradle插件3.0之后可以发布所有变种包,不存在问题1。网上很多资料会误导,可以参考 <a href="https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html" target="_blank" rel="noopener">https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html</a></p>
<ol start="2">
<li>LeakCanary的泄漏分析是在子进程中,app依赖库工程后,需要避免application重复初始化<h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h4 id="问题1:-gradle插件3-0之前"><a href="#问题1:-gradle插件3-0之前" class="headerlink" title="问题1:(gradle插件3.0之前)"></a>问题1:(gradle插件3.0之前)</h4>有两种解决办法<br>1、第一种可以采用官网的依赖方式。由于releaseImplementation是空实现,而库工程默认只会发布release包,导致leakcanary失效。</li>
</ol>
<figure class="highlight gradle"><table><tr><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> {</span><br><span class="line"> debugImplementation <span class="string">'com.squareup.leakcanary:leakcanary-android:1.6.3'</span></span><br><span class="line"> releaseImplementation <span class="string">'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>我们需要对库工程打包多个变种,设置publishNonDefault可以生成所有变种包。这样app就可以区分不同的构建类型进行依赖。</p>
<figure class="highlight gradle"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line">apply plugin: <span class="string">'com.android.library'</span></span><br><span class="line">android {</span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> publishNonDefault <span class="keyword">true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>app依赖方式如下,让app工程在debug版本下依赖库工程debug版本,在release版本下依赖库工程release版本<br><figure class="highlight gradle"><table><tr><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> {</span><br><span class="line"> debugImplementation <span class="keyword">project</span>(path: <span class="string">':myLibrary'</span>, configuration: <span class="string">'debug'</span>)</span><br><span class="line"> releaseImplementation <span class="keyword">project</span>(path: <span class="string">':myLibrary'</span>, configuration: <span class="string">'release'</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>2、第二种方法,使用依赖如下:<br><figure class="highlight gradle"><table><tr><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> {</span><br><span class="line"> implementation <span class="string">'com.squareup.leakcanary:leakcanary-android:1.6.3'</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>基础库工程抽离一个BaseApplication做初始化的公共操作。如果release版本则不初始化LeakCanary,代替了第一种方法中releaseImplementation的空实现方式。需要注意的是,库工程BuildConfig.debug一直为false,需要在app工程中判断是否调试版本。项目工程采用了这种方法。</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">BaseApplication</span> : <span class="type">Application</span></span>() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">companion</span> <span class="keyword">object</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> TAG = <span class="keyword">this</span>::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>.<span class="title">simpleName</span></span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (isDebug()) {</span><br><span class="line"> <span class="keyword">if</span> (LeakCanary.isInAnalyzerProcess(<span class="keyword">this</span>)) {</span><br><span class="line"> <span class="comment">// This process is dedicated to LeakCanary for heap analysis.</span></span><br><span class="line"> <span class="comment">// You should not init your app in this process.</span></span><br><span class="line"> logd(TAG, <span class="string">"LeakCanary process is starting for heap analysis"</span>)</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> LeakCanary.refWatcher(<span class="keyword">this</span>)</span><br><span class="line"> .listenerServiceClass(LeakService::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>)</span></span><br><span class="line"> .buildAndInstall()</span><br><span class="line"> logd(TAG, <span class="string">"LeakCanary installed"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Normal app init code... </span></span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 是否为调试版本,需要在app工程判断</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="function"><span class="keyword">fun</span> <span class="title">isDebug</span><span class="params">()</span></span>: <span class="built_in">Boolean</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<h4 id="问题2:"><a href="#问题2:" class="headerlink" title="问题2:"></a>问题2:</h4><p>由于LeakCanary堆分析是另起子进程进行的,会重新触发application的onCreate。需要记得在子进程中return掉以避免重复初始化。<br><figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">if</span> (LeakCanary.isInAnalyzerProcess(<span class="keyword">this</span>)) {</span><br><span class="line"> <span class="comment">// This process is dedicated to LeakCanary for heap analysis.</span></span><br><span class="line"> <span class="comment">// You should not init your app in this process.</span></span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>我们想把LeakCanary的逻辑放到基础库工程,app工程不需要关心这些操作。所以设计如下,</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">BaseApplication</span> : <span class="type">Application</span></span>() {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">companion</span> <span class="keyword">object</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">val</span> TAG = <span class="keyword">this</span>::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>.<span class="title">simpleName</span></span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (isDebug()) {</span><br><span class="line"> <span class="keyword">if</span> (LeakCanary.isInAnalyzerProcess(<span class="keyword">this</span>)) {</span><br><span class="line"> <span class="comment">// This process is dedicated to LeakCanary for heap analysis.</span></span><br><span class="line"> <span class="comment">// You should not init your app in this process.</span></span><br><span class="line"> logd(TAG, <span class="string">"LeakCanary process is starting for heap analysis"</span>)</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> }</span><br><span class="line"> LeakCanary.refWatcher(<span class="keyword">this</span>)</span><br><span class="line"> .listenerServiceClass(LeakService::<span class="class"><span class="keyword">class</span>.<span class="title">java</span>)</span></span><br><span class="line"> .buildAndInstall()</span><br><span class="line"> logd(TAG, <span class="string">"LeakCanary installed"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//公共初始化...</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">//子类application初始化,为了避免在LeakCanary子进程中重复初始化</span></span><br><span class="line"> onAppCreate()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 子类application初始化</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="function"><span class="keyword">fun</span> <span class="title">onAppCreate</span><span class="params">()</span></span></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 是否为调试版本,需要在app工程判断</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">abstract</span> <span class="function"><span class="keyword">fun</span> <span class="title">isDebug</span><span class="params">()</span></span>: <span class="built_in">Boolean</span></span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>在app工程中继承BaseApplication</p>
<figure class="highlight kotlin"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Application</span> : <span class="type">BaseApplication {</span></span></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">onAppCreate</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="comment">//no-op</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">isDebug</span><span class="params">()</span></span>: <span class="built_in">Boolean</span> {</span><br><span class="line"> <span class="keyword">return</span> BuildConfig.DEBUG;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>以上,是库工程集成LeakCanary的一种实现方案,有更好的设计可以留言交流。</p>
]]></content>
<tags>
<tag>内存泄漏</tag>
</tags>
</entry>
<entry>
<title>Android persistent机制</title>
<url>/posts/8417e24a/</url>
<content><![CDATA[<p><img src="https://cdn.jsdelivr.net/gh/devnan/pic/blog/20200126213606.jpg" alt><br><a id="more"></a></p>
<p>本文简单分析persistent属性的相关源码流程,总结persistent的作用及注意事项。</p>
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在一次调试系统应用过程中,修改部分代码逻辑后,执行<code>adb install -r</code> 并启动,发现应用界面更新了,但是修改到的逻辑并没有变,还是之前的版本逻辑。<br>分别执行了pm clear和am force-stop再起来应用,发现这两种做法进程id都没有变。<br>于是直接kill掉对应进程id,发现进程id变了,进程重启了,但是发现修改到的逻辑还是没变…<br>最后重启了一下机器?应用的界面和逻辑都正常更新了。</p>
<p>为什么呢?可以看一下此系统应用的manifest</p>
<figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="tag"><<span class="name">manifest</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:tools</span>=<span class="string">"http://schemas.android.com/tools"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">package</span>=<span class="string">"com.seewo.trunning"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:sharedUserId</span>=<span class="string">"android.uid.system"</span>></span></span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> <span class="tag"><<span class="name">application</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:name</span>=<span class="string">".TRunningApplication"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:label</span>=<span class="string">"@string/app_name"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:persistent</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:supportsRtl</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:theme</span>=<span class="string">"@style/AppTheme"</span>></span></span><br><span class="line"> ...</span><br><span class="line"> <span class="tag"></<span class="name">application</span>></span></span><br><span class="line"><span class="tag"></<span class="name">manifest</span>></span></span><br></pre></td></tr></table></figure>
<p>原来是persistent属性在作怪。persistent有什么作用?看看官网解释:<br><img src="https://raw.githubusercontent.com/devnan/pic/master/20190507223758.png" alt></p>
<p>可以知道,persistent表示此应用是可持久的,也就是常驻应用。官网的解释就这么多,下面只能从源码入手来了解persistent的原理,并解释上面例子的原因。</p>
<h1 id="开机启动过程"><a href="#开机启动过程" class="headerlink" title="开机启动过程"></a>开机启动过程</h1><p>众所周知,带persistent属性的系统应用开机会自启动。并且persistent应用启动时机很早,早于Launcher启动及开机广播。可以看一下framework层是怎么做的?<br>android系统开机启动时,AMS会调用到systemReady(),然后扫描所有安装的应用,并启动属性persistent为true的应用。</p>
<p><code>/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java</code><br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">systemReady</span><span class="params">(<span class="keyword">final</span> Runnable goingCallback, TimingsTraceLog traceLog)</span> </span>{</span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">synchronized</span> (<span class="keyword">this</span>) {</span><br><span class="line"> startPersistentApps(PackageManager.MATCH_DIRECT_BOOT_AWARE);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// Start up initial activity.</span></span><br><span class="line"> mBooting = <span class="keyword">true</span>;</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> startHomeActivityLocked(currentUserId, <span class="string">"systemReady"</span>); <span class="comment">//0</span></span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">startPersistentApps</span><span class="params">(<span class="keyword">int</span> matchFlags)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL) <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">synchronized</span> (<span class="keyword">this</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">final</span> List<ApplicationInfo> apps = AppGlobals.getPackageManager()</span><br><span class="line"> .getPersistentApplications(STOCK_PM_FLAGS | matchFlags).getList(); <span class="comment">//1</span></span><br><span class="line"> <span class="keyword">for</span> (ApplicationInfo app : apps) {</span><br><span class="line"> <span class="keyword">if</span> (!<span class="string">"android"</span>.equals(app.packageName)) {</span><br><span class="line"> addAppLocked(app, <span class="keyword">null</span>, <span class="keyword">false</span>, <span class="keyword">null</span> <span class="comment">/* ABI override */</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">catch</span> (RemoteException ex) {</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>上面注释0处,startHomeActivityLocked()会启动Launcher,晚于persistent应用的启动;<br>注释1处,通过PMS的getPersistentApplications()方法获取persistent应用, 最后再通过addAppLocked()启动应用进程</p>
<p><code>/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java</code></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> ProcessRecord <span class="title">addAppLocked</span><span class="params">(ApplicationInfo info, String customProcess, <span class="keyword">boolean</span> isolated,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">boolean</span> disableHiddenApiChecks, String abiOverride)</span> </span>{</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> ((info.flags & PERSISTENT_MASK) == PERSISTENT_MASK) {</span><br><span class="line"> app.persistent = <span class="keyword">true</span>;</span><br><span class="line"> app.maxAdj = ProcessList.PERSISTENT_PROC_ADJ; <span class="comment">//2</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (app.thread == <span class="keyword">null</span> && mPersistentStartingProcesses.indexOf(app) < <span class="number">0</span>) {</span><br><span class="line"> mPersistentStartingProcesses.add(app);</span><br><span class="line"> startProcessLocked(app, <span class="string">"added application"</span>, <span class="comment">//3</span></span><br><span class="line"> customProcess != <span class="keyword">null</span> ? customProcess : app.processName, disableHiddenApiChecks,</span><br><span class="line"> abiOverride); </span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">ProcessList</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// This is a system persistent process, such as telephony. Definitely</span></span><br><span class="line"> <span class="comment">// don't want to kill it, but doing so is not completely fatal.</span></span><br><span class="line"> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> PERSISTENT_PROC_ADJ = -<span class="number">800</span>;</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面主要做了两件事,注释2处设定进程adj为PERSISTENT_PROC_ADJ,值为-800,定义在ProcessList类中,表示进程属于高优先级,当系统资源紧张时,lowmemorykiller机制回收应用时会跳过persistent进程。<br>注释3处就是启动进程了,startProcessLocked方法最后会发消息到zygote进程,然后fork出应用进程,这里不再深入。<br>通过上面的步骤,android系统就在启动时预加载了persistent应用,并赋予了应用进程高优先级以保证持久性,但是,如果persistent应用被杀死或运行时异常崩溃,android如何对应用进行拉活?继续往下看。</p>
<h1 id="重启机制"><a href="#重启机制" class="headerlink" title="重启机制"></a>重启机制</h1><p>android在进程启动后调用到<code>attachApplicationLocked</code>时会创建一个进程死亡监听器<code>AppDeathRecipient</code></p>
<p><code>/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java</code></p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">boolean</span> <span class="title">attachApplicationLocked</span><span class="params">(IApplicationThread thread,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">int</span> pid, <span class="keyword">int</span> callingUid, <span class="keyword">long</span> startSeq)</span> </span>{</span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> AppDeathRecipient adr = <span class="keyword">new</span> AppDeathRecipient(</span><br><span class="line"> app, pid, thread);</span><br><span class="line"> thread.asBinder().linkToDeath(adr, <span class="number">0</span>);</span><br><span class="line"> app.deathRecipient = adr;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">AppDeathRecipient</span> <span class="keyword">implements</span> <span class="title">IBinder</span>.<span class="title">DeathRecipient</span> </span>{</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">binderDied</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (DEBUG_ALL) Slog.v(</span><br><span class="line"> TAG, <span class="string">"Death received in "</span> + <span class="keyword">this</span></span><br><span class="line"> + <span class="string">" for thread "</span> + mAppThread.asBinder());</span><br><span class="line"> <span class="keyword">synchronized</span>(ActivityManagerService.<span class="keyword">this</span>) {</span><br><span class="line"> appDiedLocked(mApp, mPid, mAppThread, <span class="keyword">true</span>); <span class="comment">//4</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>当应用进程死亡时,系统会回调到死亡监听器的binderDied()方法,执行注释4处的appDiedLocked()做一些进程死亡后的相关善后工作。<br>继续走下去,appDiedLocked()最终会执行到cleanUpApplicationRecordLocked(),我们可以看一下这个方法做了什么。</p>
<figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">boolean</span> <span class="title">cleanUpApplicationRecordLocked</span><span class="params">(ProcessRecord app,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">boolean</span> restarting, <span class="keyword">boolean</span> allowRestart, <span class="keyword">int</span> index, <span class="keyword">boolean</span> replacingPid)</span> </span>{</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (!app.persistent || app.isolated) {</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line"> </span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (!app.removed) {</span><br><span class="line"> <span class="comment">// This app is persistent, so we need to keep its record around.</span></span><br><span class="line"> <span class="comment">// If it is not already on the pending app list, add it there</span></span><br><span class="line"> <span class="comment">// and start a new process for it.</span></span><br><span class="line"> <span class="keyword">if</span> (mPersistentStartingProcesses.indexOf(app) < <span class="number">0</span>) {</span><br><span class="line"> mPersistentStartingProcesses.add(app);</span><br><span class="line"> restart = <span class="keyword">true</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>上面的注释很清楚,如果是persistent应用,会继续保留其进程记录,然后重新启动进程。<br>当然,如果是persistent应用进程死亡,系统则直接回收所有的进程资源。<br>到这里,我们可以了解到,当persistent应用被异常杀死时,系统通过进程死亡监听器来重启进程,并且过程中不会清理进程记录ProcessRecord。而ProcessRecord对象其实记录着进程的四大组件和进程状态等信息。</p>
<h2 id="persistent带来的问题"><a href="#persistent带来的问题" class="headerlink" title="persistent带来的问题"></a>persistent带来的问题</h2><p>我们可以看到,系统应用声明persistent,可以开机立即加载进程,更快响应;可以保证一些关键应用常驻在系统中,不会被系统回收;比如长连接应用等。但是persistent的使用同时也会带来一些问题,如长期占用系统资源不释放;如果app出现不可恢复的crash,将陷入一直崩溃启动的死循环;应用自升级不会kill并清理进程,需要特殊处理,并且升级后会失去persistent特性。</p>
<h4 id="系统应用自升级"><a href="#系统应用自升级" class="headerlink" title="系统应用自升级"></a>系统应用自升级</h4><p>正常的应用自升级流程,覆盖安装时系统会kill应用进程并清理在AMS中的进程记录,在新版应用重新启动时,系统会新建一个进程并重新加载各种组件运行。<br>但是,persistent应用自升级时,系统不会kill此应用进程,AMS也不会清理进程记录,系统只会把新版应用中的各种组件信息记录到AMS中,这样可能出现两个版本逻辑融合到一起,导致应用功能出现错乱。<br>解决方法有,一种是应用自升级后重启系统,这样会重新加载进程,但是这个明显不符合需求,影响用户体验,不可取。<br>第二种是接收新版应用的安装广播,调用context的startInstrumentation方法,会强制系统kill应用进程,清理AMS对各种组件的状态记录,并重新启动应用。</p>
<p>解决上面这个问题以后,还有一个问题,升级后的persistent系统应用会变成普通应用,导致后续失去persistent特性。</p>
<h4 id="低端设备禁用硬件加速"><a href="#低端设备禁用硬件加速" class="headerlink" title="低端设备禁用硬件加速"></a>低端设备禁用硬件加速</h4><p>在低端设备上对于persistent进程会禁用硬件加速,下面代码注释已经说明了。android系统这么做,可能就是因为persistent应用是常驻的,这样可以避免占用太多的资源。</p>
<p><code>/frameworks/base/core/java/android/view/ViewRootImpl.java</code><br><figure class="highlight java"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">enableHardwareAcceleration</span><span class="params">(WindowManager.LayoutParams attrs)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (hardwareAccelerated) {</span><br><span class="line"> <span class="keyword">if</span> (!ThreadedRenderer.isAvailable()) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Persistent processes (including the system) should not do</span></span><br><span class="line"> <span class="comment">// accelerated rendering on low-end devices. In that case,</span></span><br><span class="line"> <span class="comment">// sRendererDisabled will be set. In addition, the system process</span></span><br><span class="line"> <span class="comment">// itself should never do accelerated rendering. In that case, both</span></span><br><span class="line"> <span class="comment">// sRendererDisabled and sSystemRendererDisabled are set. When</span></span><br><span class="line"> <span class="comment">// sSystemRendererDisabled is set, PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED</span></span><br><span class="line"> <span class="comment">// can be used by code on the system process to escape that and enable</span></span><br><span class="line"> <span class="comment">// HW accelerated drawing. (This is basically for the lock screen.)</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">boolean</span> fakeHwAccelerated = (attrs.privateFlags &</span><br><span class="line"> WindowManager.LayoutParams.PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED) != <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">boolean</span> forceHwAccelerated = (attrs.privateFlags &</span><br><span class="line"> WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED) != <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure></p>
<h2 id="其他问题"><a href="#其他问题" class="headerlink" title="其他问题"></a>其他问题</h2><p>回头看最开始的调试例子应该清楚了。说到应用界面更新了,但是修改到的逻辑并没有变,其实是因为刚好应用界面跑在子进程里,这也说明了persistent属性只能对应用主进程生效,子进程不生效。<br>persistent进程的adj值是负数,普通进程一般大于等于0,我们可以直接查看进程adj来验证一下<br><img src="https://raw.githubusercontent.com/devnan/pic/master/20190507224132.png" alt></p>
<p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190507224146.png" alt></p>
<p>可以发现,其中home进程是子进程,adj为0,是前台进程。应用主进程pid为1091,adj为-11,其优先级很高,是persistent进程。</p>
<h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ol>
<li>普通应用manifest中声明persistent属性是无效的,只有系统应用才能生效;</li>
<li>persistent应用启动早于Launcher启动早于开机广播,开发中需要注意时序问题;</li>
<li>persistent应用会一直常驻在系统中,进程资源不会被系统回收,应用异常退出后系统会立即重启应用,并保留进程记录;</li>
<li>persistent应用自升级需要做特殊处理。所以,开发中覆盖安装persistent应用后,必须重启android设备才能正常运行最新代码。当然,第一次覆盖安装后,该应用会失去persistent特性。</li>
<li>persistent应用的子进程不具备persistent特性;</li>
</ol>
]]></content>
</entry>
<entry>
<title>Cocos2d-x入门及Android原生平台发布</title>
<url>/posts/9e519237/</url>
<content><Cocos2d-x 的图形渲染基于OpenGL ES<br>2)Cocos2d-x 还包含其他如资源管理,音频播放、物理引擎等模块,比直接使用 OpenGL ES 开发游戏,更加迅速简单。让开发者将精力集中在游戏本身,而不是底层的图像绘制上。<br>3)DirectX是Windows平台图形编程接口,Cocos2d-x通过AngleProject间接支持windows平台,AngleProject的作用是将OpenGL ES转化为DirectX </p>
<p>所以,Cocos2d-x的跨平台特性其实也就是借助了OpenGL ES的跨平台性,从而横跨iOS, Android, WP8三个主要平台。</p>
<hr>
<h1 id="2、开发环境"><a href="#2、开发环境" class="headerlink" title="2、开发环境"></a>2、开发环境</h1><h4 id="Cocos-Creator:"><a href="#Cocos-Creator:" class="headerlink" title="Cocos Creator:"></a>Cocos Creator:</h4><p>类似于 Unity3D 的游戏编辑器,并且内部已经包含完整的 JavaScript 引擎和 cocos2d-x 原生引擎</p>
<h4 id="VS-Code-WebStorm、SublimeText"><a href="#VS-Code-WebStorm、SublimeText" class="headerlink" title="VS Code (WebStorm、SublimeText):"></a>VS Code (WebStorm、SublimeText):</h4><p>JavaScript脚本代码编辑器</p>
<hr>
<p>下图是mac平台显示Cocos Creator包内容的目录:</p>
<h2 id><a href="#" class="headerlink" title></a><img src="https://raw.githubusercontent.com/devnan/pic/master/20190331225933.png" alt></h2><h4 id="Android原生平台配置:"><a href="#Android原生平台配置:" class="headerlink" title="Android原生平台配置:"></a>Android原生平台配置:</h4><p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190331230052.png" alt></p>
<p>注:js引擎和cocos2d-x引擎在Cocos Creator已经自带了,不用我们配置。另外,使用android studio也要配置ANT路径。</p>
<hr>
<h1 id="3、关于Cocos-Creator"><a href="#3、关于Cocos-Creator" class="headerlink" title="3、关于Cocos Creator"></a>3、关于Cocos Creator</h1><ul>
<li>组件化</li>
<li>脚本化</li>
<li>数据驱动</li>
<li>跨平台发布</li>
</ul>
<hr>
<p>Cocos Creator 的技术架构图</p>
<p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190331232557.png" alt></p>
<hr>
<h2 id="3-1、组件化概念"><a href="#3-1、组件化概念" class="headerlink" title="3-1、组件化概念"></a>3-1、组件化概念</h2><p>Cocos Creator 工作流以组件化开发为核心,也就是基于组件的实体系统开发模式 (Entity-Component System)。简单的说,就是以组合而非继承的方式进行实体的构建。</p>
<p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190401000534.jpeg" alt></p>
<p>一个实体指的是存在于游戏世界中的物体,多个组件可以挂载到实体上,比如动画组件,碰撞组件,渲染组件等。</p>
<hr>
<h2 id="3-2、数据驱动代替代码驱动"><a href="#3-2、数据驱动代替代码驱动" class="headerlink" title="3-2、数据驱动代替代码驱动"></a>3-2、数据驱动代替代码驱动</h2><ul>
<li>所有场景都会序列化为数据,在运行时使用这些数据来重新构建场景,界面,动画等元素。</li>
<li>数据驱动实现了场景的可视化,使得场景可以被自由的编辑。从而使入口点变成了编辑器,而不是代码。</li>
</ul>
<hr>
<h1 id="4、脚本组件开发"><a href="#4、脚本组件开发" class="headerlink" title="4、脚本组件开发"></a>4、脚本组件开发</h1><h2 id="4-1、基本概念"><a href="#4-1、基本概念" class="headerlink" title="4-1、基本概念"></a>4-1、基本概念</h2><ul>
<li><p>导演和场景<br>场景相当于一部电影的某个场景画面,导演就负责画面的切换。</p>
</li>
<li><p>节点和组件<br>把功能点设计封装成组件(Component)的形式,然后将这些组件,按需挂载到一个个类似于容器的节点上,从而形成一个个功能各异的实体(Entity)</p>
</li>
</ul>
<hr>
<h2 id="4-2、简单的脚本示例"><a href="#4-2、简单的脚本示例" class="headerlink" title="4-2、简单的脚本示例"></a>4-2、简单的脚本示例</h2><figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">cc.Class({</span><br><span class="line"> extends: cc.Component,</span><br><span class="line"> </span><br><span class="line"> //属性声明,会展示在属性检查器中</span><br><span class="line"> properties: {</span><br><span class="line"> username: "Devnan", //基本JavaScript类型, string</span><br><span class="line"> age: 18, //基本JavaScript类型, number</span><br><span class="line"> girlFriend: cc.Node //cc类型,cc.Node用于获取其他节点(实体)</span><br><span class="line"> },</span><br><span class="line"> </span><br><span class="line"> //构造函数</span><br><span class="line"> ctor: function () {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> // 首次加载,做初始化操作</span><br><span class="line"> onLoad: function () {</span><br><span class="line"> },</span><br><span class="line"></span><br><span class="line"> // 每一帧渲染都会调用此函数进行更新</span><br><span class="line"> update: function (dt) {</span><br><span class="line"> },</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<hr>
<h2 id="4-3、生命周期"><a href="#4-3、生命周期" class="headerlink" title="4-3、生命周期"></a>4-3、生命周期</h2><ul>
<li>onLoad:组件首次激活时触发,在 start 调用前执行,一般做初始化操作</li>
<li>start:第一次执行 update 前触发,初始化一些可能在update时发生改变的中间状态</li>
<li>update:每一帧渲染前触发,做更新操作</li>
<li>lateUpdate:update 执行完之后触发,做动画更新后的一些额外操作</li>
<li>onDestroy:当组件或者所在节点调用了destroy(),则会调用此回调,并做回收操作<br>……</li>
</ul>
<hr>
<h2 id="4-4、事件分发机制"><a href="#4-4、事件分发机制" class="headerlink" title="4-4、事件分发机制"></a>4-4、事件分发机制</h2><ul>
<li>node.on(type, callback, target):持续监听 node 的 type 事件。</li>
<li>node.once(type, callback, target):监听一次 node 的 type 事件。</li>
<li>node.off(type, callback, target):取消监听所有 type 事件或取消 type 的某个监听器(用 callback 和 target 指定)。</li>
<li>node.emit(type, detail):通知所有监听 type 事件的监听器,可以发送一个附加参数。</li>
<li>node.dispatchEvent(event):发送一个事件给它的监听器,支持冒泡。</li>
</ul>
<hr>
<p><strong>node.dispatchEvent</strong> 采用冒泡派送的方式分发事件。冒泡派送会将事件从事件发起节点,不断地向上传递给他的父级节点,直到到达根节点或者在某个节点通过event.stopPropagation() 拦截了此事件。</p>
<p><strong>触摸事件</strong>也支持节点树的事件冒泡。</p>
<p>注:和android的事件分发机制有差别,cocos2d没有事件捕获的过程,而且消费事件后需要主动去拦截事件传递</p>
<hr>
<h1 id="5、其他组件"><a href="#5、其他组件" class="headerlink" title="5、其他组件"></a>5、其他组件</h1><ul>
<li>物理组件</li>
<li>动画组件<br>……</li>
</ul>
<hr>
<h1 id="6、构建发布流程"><a href="#6、构建发布流程" class="headerlink" title="6、构建发布流程"></a>6、构建发布流程</h1><ul>
<li><p>link、default:使用源码引擎构建,速度慢</p>
</li>
<li><p>binary:使用预编译库构建,无需编译C++,速度快,但是无法在原生工程里调试引擎源码</p>
</li>
</ul>
<hr>
<p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190401000622.png" alt></p>
<hr>
<p>下面目录可以拿到构建编译后android平台下的apk</p>
<p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190401000659.png" alt></p>
<hr>
<p>这样是不是都搞定了?可以开心地玩游戏了</p>
<p><img src="https://raw.githubusercontent.com/devnan/pic/master/20190401000725.jpg" alt></p>
<hr>
<p>最后还要导入到原生平台Android Studio进行开发,以修改一些默认配置,比如应用图标,混淆开关等<br><img src="https://raw.githubusercontent.com/devnan/pic/master/20190401000758.png" alt></p>
<p>这里可以用脚本处理整个流程,从构建编译,导出原生平台,到部分配置项的修改,以加快打包速度。</p>
<p>可以在工程主目录新建android-build文件夹,builder.json是构建编译的配置文件,build.sh是整个流程的脚本,如下,</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">#!/usr/bin/env bash</span><br><span class="line">PROJECT_PATH=$(dirname `pwd`)</span><br><span class="line"></span><br><span class="line"># 构建编译</span><br><span class="line">buildCompile(){</span><br><span class="line"> echo "PROJECT_PATH:" $PROJECT_PATH</span><br><span class="line"> /Applications/CocosCreator.app/Contents/MacOS/CocosCreator --path $PROJECT_PATH --build "configPath=$PROJECT_PATH/android-build/builder.json;autoCompile=true"</span><br><span class="line">}</span><br><span class="line"> </span><br><span class="line">handleAndroidProject() {</span><br><span class="line"> # 适配环境</span><br><span class="line"> echo "android.enableAapt2=false" >> $ANDROID_PATH"/gradle.properties"</span><br><span class="line"> </span><br><span class="line"> # 替换资源</span><br><span class="line"> ANDROID_PATH=$PROJECT_PATH"/build/jsb-binary/frameworks/runtime-src/proj.android-studio"</span><br><span class="line"> RES_PATH=${ANDROID_PATH}"/app/res"</span><br><span class="line"> echo "ANDROID_PATH:" $ANDROID_PATH</span><br><span class="line"></span><br><span class="line"> echo "replace $RES_PATH/values/strings.xml"</span><br><span class="line"> rm $RES_PATH"/values/strings.xml"</span><br><span class="line"> cp ./res/strings.xml $RES_PATH"/values/"</span><br><span class="line"></span><br><span class="line"> echo "replace $RES_PATH/mipmap-xhdpi/ic_launcher.png"</span><br><span class="line"> rm $RES_PATH"/mipmap-xhdpi/ic_launcher.png"</span><br><span class="line"> cp ./res/ic_launcher.png $RES_PATH"/mipmap-xhdpi/"</span><br><span class="line"> rm $RES_PATH"/mipmap-xxhdpi/ic_launcher.png"</span><br><span class="line"> cp ./res/ic_launcher.png $RES_PATH"/mipmap-xxhdpi/"</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">generateAPK(){</span><br><span class="line"> cd $ANDROID_PATH/app/</span><br><span class="line"> gradle clean assembleRelease</span><br><span class="line"></span><br><span class="line"> mkdir $PROJECT_PATH/android-build/apk</span><br><span class="line"> cp $ANDROID_PATH/app/build/outputs/apk/release/*.apk $PROJECT_PATH/android-build/apk</span><br><span class="line"> echo "regenerate apk in $PROJECT_PATH/android-build/apk"</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"># 执行流程</span><br><span class="line">buildCompile</span><br><span class="line">handleAndroidProject</span><br><span class="line">generateAPK</span><br></pre></td></tr></table></figure>
<hr>
<h1 id="7、推荐资料"><a href="#7、推荐资料" class="headerlink" title="7、推荐资料"></a>7、推荐资料</h1><ul>
<li>Cocos Creator v1.8.x 用户手册<br><a href="http://docs.cocos.com/creator/manual/zh/" target="_blank" rel="noopener">http://docs.cocos.com/creator/manual/zh/</a></li>
<li>Cocos Creator 之旅:<br><a href="http://www.cocos.com/607" target="_blank" rel="noopener">http://www.cocos.com/607</a></li>
</ul>
]]></content>
</entry>
<entry>
<title>Android分屏多窗口实现</title>
<url>/posts/11a8ecdc/</url>
<content><![CDATA[<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>基于公司产品的要求,需要研究app如何在Android N中支持分屏模式。以下是自己思路的总结,也顺便帮助到各位。</p>
<h1 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h1><p><strong>Android N</strong> 的新特性就是对应用分屏的支持。在分屏模式中,系统以左右并排或上下并排的方式分屏显示两个应用。用户可以通过拖动两个应用之间的分界线来改变两个应用的尺寸。</p>
<p>用户可以通过以下方式切换到分屏模式:</p>
<ul>
<li>打开 Overview 屏幕并长按 Activity 标题,则可以拖动该 Activity 至屏幕突出显示的区域,使Activity 进入多窗口模式。</li>
<li>长按 Overview 按钮,设备上的当前 Activity 将进入分屏模式,同时将打开 Overview 屏幕,可在该屏幕中选择要共享屏幕的另一个 Activity。</li>
</ul>
<p>另外,用户可以在两个 Activity 共享屏幕的同时在这两个 Activity 之间拖放数据 </p>
<h1 id="分屏模式下的生命周期"><a href="#分屏模式下的生命周期" class="headerlink" title="分屏模式下的生命周期"></a>分屏模式下的生命周期</h1><p>首先,分屏模式并没有改变Activity的生命周期。<br>在分屏模式中,与用户交互的 Activity 为活动状态。另一个 Activity 虽然可见,但处于暂停状态。 如果用户与此暂停的 Activity 交互,该 Activity 将恢复,而之前的Activity 将暂停。所以,在分屏模式下,应用在对用户可见的状态下进入paused状态,可能仍需要继续其操作。就如一个视频播放器,如果进入了分屏模式,不应该在onPaused()中暂停视频播放,而应该在onStop()中才暂停视频,然后对应的在onStart中恢复视频播放。</p>
<p>另外,在Android N的Activity类中,增加了一个void onMultiWindowChanged(boolean</p>
<blockquote>
<p>inMultiWindow)回调,Activity 进入或退出分屏模式时系统将调用此方法。 在 Activity<br>进入分屏模式时,系统向该方法传递 true 值,在退出分屏模式时,则传递 false 值。</p>
</blockquote>
<p>#处理运行时变更<br>在分屏模式显示应用,调整应用大小,或将应用恢复到全屏模式时,系统将通知 Activity 发生配置变更。这与<strong>横竖屏切换</strong>时的 Activity 生命周期影响相同,即Activity会重启(调用onPause–>onStop–>onDestory–>onCreate–>onStart–>onResume),<br>为了分屏后使Activity恢复原来的状态,一般有两种方法。一是在Activity重启前保存数据;二是阻止Activity的重启。</p>
<ul>
<li>在Activity重启前保存数据 </li>
</ul>
<p>1、通过onSaveInstanceState()保存bundle数据,以保证该状态可以在onCreate(Bundle)或者onRestoreInstanceState(Bundle)中恢复<br>2、重启 Activity需要恢复大量数据、重新建立网络连接等,那么因配置变更而引起的Activity重启会很缓慢,这时可以通过保存 Fragment来减轻重新初始化 Activity的负担,具体做法是,在Activity中包含此Fragment的引用,而Fragment包含要保存的对象的引用;接着,通过在Fragment的onCreate方法中设置setRetainInstance(true)对Fragment进行非中断式的保存;这样,当Activity被销毁重启时,fragment实例还在,可以Activity中恢复了对fragment的引用从而恢复数据。具体可参考<a href="https://developer.android.com/guide/topics/resources/runtime-changes.html?hl=zh-cn#RetainingAnObject" target="_blank" rel="noopener">处理运行时变更</a>。</p>
<ul>
<li>阻止Activity的重启</li>
</ul>
<p>在清单文件中声明:</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><activity </span><br><span class="line"> android:name=".MyActivity"</span><br><span class="line"> android:configChanges="orientation|keyboardHidden|screenSize"</span><br><span class="line"> android:label="@string/app_name"></span><br></pre></td></tr></table></figure>
<p>android:configChanges属性最常用的值包括 “orientation” , “keyboardHidden”和”screenSize”,分别用于避免因屏幕方向,可用键盘改变和屏幕尺寸而导致重启。<br>这种方法不同于第一种,系统不会自动根据当前的配置去应用资源。所以如果需要的话,要在onConfigurationChanged()中自行设置备用资源,即通过setContentView()去调用相应的layout文件。不需要基于这些配置变更去更新应用,则可不用实现onConfigurationChanged()。</p>
<p>注意,第二种方法官方不推荐使用,主要是因为Activity销毁重启不仅仅在分屏模式下会出现,比如手机内存不够时Activity同样可能被销毁,而这时候我们还是需要在Activity销毁前做好数据状态的保存。当然,如果没有备用资源的话,也就是Activity横向和纵向都共用同一个layout文件,则第二种方法无疑是更好的选择。</p>
<h1 id="配置分屏模式"><a href="#配置分屏模式" class="headerlink" title="配置分屏模式"></a>配置分屏模式</h1><p>在清单的activity或application节点中设置该属性,启用或禁用分屏显示:</p>
<pre><code>android:resizeableActivity=["true" | "false"]
</code></pre><p>如果该属性设置为 true,Activity 将能以分屏和自由形状模式启动。 如果此属性设置为 false,Activity 将不支持多窗口模式。如果该值为 false,且用户尝试在分屏模式下启动 Activity,该 Activity 将全屏显示。 </p>
<blockquote>
<p>如果应用没有适配到Android N(targetSDKVersion < Android<br>N,compileSDKVersion < Android N),也是可以支持分屏的。系统将强制调整应用大小。不过会显示对话框提醒用户应用可能会发生异常。 </p>
</blockquote>
<p>另外,在Android N中,可以在清单文件中添加layout节点,并设置一些新增加的属性,通过这些属性来设置分屏模式的行为,如最小尺寸等<br>例如,以下节点显示了如何指定 Activity 在自由形状模式中显示时 Activity 的默认大小、位置和最小尺寸:</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"><activity android:name=".MyActivity"></span><br><span class="line"> <layout android:defaultHeight="500dp"</span><br><span class="line"> android:defaultWidth="600dp"</span><br><span class="line"> android:gravity="top|end"</span><br><span class="line"> android:minimalHeight="450dp"</span><br><span class="line"> android:minimalWidth="300dp" /></span><br><span class="line"></activity></span><br></pre></td></tr></table></figure>
<h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://developer.android.com/preview/features/multi-window.html#configuring" target="_blank" rel="noopener">https://developer.android.com/preview/features/multi-window.html#configuring</a><br><a href="http://blog.csdn.net/airk000/article/details/38557605" target="_blank" rel="noopener">http://blog.csdn.net/airk000/article/details/38557605</a></p>
]]></content>
<tags>
<tag>android</tag>
</tags>
</entry>
</search>