您的位置:首页 > 移动开发 > Android开发

Android APK DEX分包总结

2017-08-08 16:47 295 查看


因为android热修复需要Dex分包,而Android热修复是现在比较火的技术,所以现在将我这几天学到的相关东西做一个总结,这篇主要从AndroidStudio和Eclipse两个方面总结Dex分包的过程。


为什么要Dex分包

当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象:

1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT

2. 方法数量过多,编译时出错,提示: Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

出现这种问题的原因是:

1. Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M

2. 一个dex文件最多只支持65536个方法。

Dex分包可以解决上面两个问题,由于AndroidStudio采用Gradle构建项目,Eclipse使用Ant构建项目,所以需要分开说明Dex分包的步骤


AndroidStudio中的Dex分包步骤

AndroidStudio使用Gradle构建项目,所以Dex分包主要是在build.gradle文件中做配置,下面通过一个Demo来说明:

使用AndroidStudio新建Android工程MultiDexDemo
配置app模块的build.gradle文件,代码如下:

apply plugin: 'com.android.application'

android {
compileSdkVersion 23
buildToolsVersion "24.0.1"

defaultConfig {
applicationId "com.test.multidexdemo"
minSdkVersion 15
targetSdkVersion 23
versionCode 1
versionName "1.0"

multiDexEnabled true
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}

afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = ['--multi-dex']
} else {
dx.additionalParameters += '--multi-dex'
}
}
}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.4.0'

compile 'com.android.support:multidex:1.0.0'
}
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
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
[/code]

在AndroidManifest.xml文件中配置Application如下:

android:name="android.support.multidex.MultiDexApplication"
1
1
[/code]

这里配置的Application继承自android.support.multidex包中的MultiDexApplication,如果项目中已经有自定义的Application,可以让该Application继承MultiDexApplication,或者在Application的attachBaseContext方法中做如下调用:
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
1
2
3
4
1
2
3
4
[/code]

build工程。经过上面几步后,使用AndroidStudio Build->Build APK菜单构建debug包。

如果我们直接构建apk,解压apk后会发现里面还是只有一个classes.dex文件,并没有将dex文件分包,这里主要原因是,我们的项目中的方法数没有超过65536个,所以gradle只为我们生成了一个dex包,为了测试dex分包,我们在上面建立的项目中加入几个测试类,如下图所示:



其中OtherActivity为MainActivity中的按钮点击后跳转的Activity,Test, Test2, Test3, Test4, Test5这5个类,主要是加入了如下的一些方法:



这里就是添加了很多空方法,让apk中的方法数超过65536个,加入了上面的几个类后,再次使用gradle构建apk,这时候解压生成的apk文件,会发现dex分包终于出来了,如下图所示:



在Android5.1和Android4.4的机器上测试安装apk并运行,发现能正常运行。

以上就是使用AndroidStudio进行dex分包的过程,但是有个问题,就是在上面的步骤中,我们没有指定哪些类放在classes.dex包中,完全是由打包工具来确定哪些类放在classes.dex包中的,反编译生成的apk文件后发现,MainActivity和OtherActivity,还有Test, Test两个类放到classes.dex包中了,如下图所示:



至于如何自定义classes.dex包中的类,这里我还没有找到方法,所以先留个疑问在这里吧。


Eclipse中的Dex分包步骤

Eclipse采用Ant作为项目的构建工具,我们需要自定义Ant脚本,才能实现Dex的多分包,下面还是以一个Demo来说明Dex多分包的步骤:

在Eclipse中新建Android工程Test,这里我们采用如下的配置:



新建工程后会发现Test工程依赖appcompat_v7包,所以我们的Ant脚本里,还要配置appcompat_v7相关的脚本,Ant的build.xml脚本内容比较多,也比较复杂,需要了解Ant打包apk的过程才能懂build.xml文件的意思,我也是参考了这篇博文才实现了ant
dex多分包的,在这里感谢作者,作者的三篇有关Android热修复的文章,给我的帮助非常大。下面贴出我的build.xml文件的内容:

<project
name="multiDex"
default="sign-apk" >

<taskdef
classpath="H:\apache-ant-1.9.7\lib\ant-contrib-1.0b3.jar"
resource="net/sf/antcontrib/antlib.xml" />

<!-- 代表当前目录 -->

<property
name="project-dir"
value="." />

<!-- v7工程目录 -->

<property
name="appcompat-dir"
value="H:\adt-bundle-windows-x86_64-20140702\workspace\appcompat_v7" />

<target name="init" >

<!-- 提示信息,类似printf(); -->

<echo>
Init

</echo>
<!-- 删除文件,创建文件目录 -->

<delete dir="${project-dir}\bin" />

<delete dir="${project-dir}\gen" />

<mkdir dir="${project-dir}\bin" />

<mkdir dir="${project-dir}\gen" />

<mkdir dir="${project-dir}\bin/classes" />
</target>

<!-- 生成R.java 文件。 -->

<!-- Sdk 的根目录 -->

<property
name="sdk-folder"
value="H:\adt-bundle-windows-x86_64-20140702\sdk" />

<!-- 编译工具的目录 -->

<property
name="platform-tools-folder"
value="${sdk-folder}\build-tools\android-4.4W" />

<!-- aapt 命令详细的目录 -->

<property
name="tools.aapt"
value="${platform-tools-folder}\aapt.exe" />

<property
name="platform-folder"
value="${sdk-folder}\platforms\android-20" />

<!-- android.jar 的路径 -->

<property
name="android-jar"
value="${platform-folder}\android.jar" />

<!-- 当前工程的清单文件 -->

<property
name="manifest"
value="${project-dir}\AndroidManifest.xml" />

<!-- 依赖库的清单文件 -->

<property
name="appcompat.manifest"
value="${appcompat-dir}\AndroidManifest.xml" />

<target
name="GenR"
depends="init" >

<!-- 生成工程的R.java 文件 -->

<echo>
gen project  R.java

</echo>

<exec
executable="${tools.aapt}"
failonerror="true" >

<arg value="package" />

<arg value="-m" />

<arg value="-J" />

<arg value="${project-dir}\gen" />

<arg value="-M" />

<arg value="${manifest}" />

<arg value="-S" />

<arg value="${project-dir}\res" />

<arg value="-S" />

<arg value="${appcompat-dir}\res" />

<arg value="-I" />

<arg value="${android-jar}" />

<arg value="--auto-add-overlay" />
</exec>
<!-- 生成依赖库的R.java 文件 -->

<echo>
gen appcompat  R.java

</echo>

<exec
executable="${tools.aapt}"
failonerror="true" >

<arg value="package" />

<arg value="-m" />

<arg value="-J" />

<arg value="${project-dir}\gen" />

<arg value="-M" />

<arg value="${appcompat.manifest}" />

<arg value="-S" />

<arg value="${project-dir}\res" />

<arg value="-S" />

<arg value="${appcompat-dir}\res" />

<arg value="-I" />

<arg value="${android-jar}" />

<arg value="--auto-add-overlay" />
</exec>
</target>

<!--
编译工程中的src 中的代码
因为我配置了java 环境变量,所以直接使用了javac
-->

<target
name="compile"
depends="GenR" >

<echo>
Compiling java source code...
</echo>
<!-- 编译依赖库的java文件 -->

<javac
bootclasspath="${android-jar}"
destdir="${project-dir}/bin/classes"
encoding="UTF-8" >

<src path="${appcompat-dir}/src" />

<src path="gen" />

<classpath>

<fileset
dir="${appcompat-dir}/libs"
includes="*.jar" />
<!-- 第三方jar包需要引用,用于辅助编译 -->
</classpath>
</javac>

<!-- 编译工程的java文件 -->

<javac
bootclasspath="${android-jar}"
destdir="${project-dir}/bin/classes"
encoding="UTF-8" >

<src path="${project-dir}/src" />

<src path="gen" />

<classpath>

<fileset
dir="${project-dir}/libs"
includes="*.jar" />
<!-- 第三方jar包需要引用,用于辅助编译 -->
</classpath>

<classpath>

<fileset
dir="${appcompat-dir}/libs"
includes="*.jar" />
<!-- 第三方jar包需要引用,用于辅助编译 -->
</classpath>
</javac>
</target>

<!-- 将工程中的源码打成dex -->

<property
name="tools.dx"
value="${platform-tools-folder}\dx.bat" />

<target
name="dex"
depends="compile" >

<echo>
Generate multi-dex...

</echo>
<!-- 分为三个部分  jar  一部分,本地 -->

<exec
executable="${tools.dx}"
failonerror="true" >

<arg value="--dex" />

<arg value="--multi-dex" />

<arg value="--set-max-idx-number=10000" />

<arg value="--minimal-main-dex" />

<arg value="--main-dex-list" />

<arg value="main-dex-rule.txt" />

<arg value="--output=bin" />

<arg value="bin/classes" />
</exec>

<echo>
Generate dex...
</echo>

<exec
executable="${tools.dx}"
failonerror="true" >

<arg value="--dex" />

<arg value="--output=bin/classes3.dex" />

<arg value="${project-dir}/libs" />
<!-- classes文件位置 -->

<arg value="${appcompat-dir}\libs" />
</exec>

</target>

<!-- 打包资源文件 -->

<target
name="build-res-and-assets"
depends="dex" >

<echo>
build-res-and-assets

</echo>
<!-- 打包资源文件 -->

<exec
executable="${tools.aapt}"
failonerror="true" >

<arg value="package" />

<arg value="-f" />
<!-- 资源覆盖重写 -->

<arg value="-M" />
<!-- 指定清单文件 -->

<arg value="${manifest}" />

<arg value="-S" />
<!-- 指定资源路径 -->

<arg value="res" />

<arg value="-S" />

<arg value="${appcompat-dir}/res" />

<arg value="-A" />

<arg value="assets" />

<arg value="-I" />

<arg value="${android-jar}" />

<arg value="-F" />
<!-- 输出资源压缩包 -->

<arg value="${project-dir}/bin/resources.arsc" />

<arg value="--auto-add-overlay" />
</exec>
</target>

<!-- 打包 -->

<target
name="package"
depends="build-res-and-assets" >

<echo>
package unsign

</echo>

<java
classname="com.android.sdklib.build.ApkBuilderMain"
classpath="${sdk-folder}/tools/lib/sdklib.jar" >

<arg value="${project-dir}/bin/unsign.apk" />
<!-- 未签名apk的生成路径 -->

<arg value="-u" />
<!-- 创建一个未签名的包 -->

<arg value="-z" />
<!-- 需要添加的压缩包 -->

<arg value="${project-dir}/bin/resources.arsc" />

<arg value="-f" />
<!-- 需要添加的文件 -->

<arg value="bin/classes.dex" />
</java>
</target>

<!-- jdk 路径 -->

<property
name="jdk-folder"
value="C:\Program Files\Java\jdk1.6.0_43" />
<!-- 签名文件路径 -->

<property
name="tools.jarsigner"
value="${jdk-folder}\bin\jarsigner.exe" />

<!-- 拷贝文件到项目的根目录下面,因为我们的脚本是在根目录下面,这样在运行aapt的时候,可以直接操作dex文件了 -->

<target
name="copy_dex"
depends="package" >

<echo message="copy dex..." />

<copy todir="${project-dir}" >

<fileset dir="bin" >

<include name="classes*.dex" />
</fileset>
</copy>
</target>

<target
name="add-subdex-toapk"
depends="copy_dex" >

<echo message="Add Subdex to Apk ..." />

<foreach
param="dir.name"
target="aapt-add-dex" >

<path>
<fileset
dir="bin"
includes="classes*.dex" />
</path>
</foreach>
</target>

<!-- 使用aapt命令添加dex文件 -->

<target name="aapt-add-dex" >

<echo message="${dir.name}" />
<!-- 使用正则表达式获取classes的文件名 -->

<propertyregex
casesensitive="false"
input="${dir.name}"
property="dexfile"
regexp="classes(.*).dex"
select="\0" />
<!-- 这里不需要添加classes.dex文件 -->

<if>

<equals
arg1="${dexfile}"
arg2="classes.dex" />

<then>

<echo>
${dexfile} is not handle

</echo>
</then>

<else>

<echo>
${dexfile} is handle

</echo>

<exec
executable="${tools.aapt}"
failonerror="true" >

<arg value="add" />

<arg value="bin/unsign.apk" />

<arg value="${dexfile}" />
</exec>
</else>
</if>

<delete file="${project-dir}\${dexfile}" />
</target>

<target
name="sign-apk"
depends="add-subdex-toapk" >

<echo>
Sign apk
</echo>

<exec
executable="${tools.jarsigner}"
failonerror="true" >

<arg value="-keystore" />

<arg value="${project-dir}/my.keystore" />

<arg value="-storepass" />

<arg value="123456" />
<!-- 仓库密码 -->

<arg value="-keypass" />

<arg value="123456" />
<!-- 秘钥的密码 -->

<arg value="-signedjar" />

<arg value="${project-dir}/bin/sign.apk" />

<arg value="${project-dir}/bin/unsign.apk" />

<arg value="test" />
<!-- 秘钥的别名 -->
</exec>
</target>

</project>
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
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
[/code]

这里需要注意的一点是:以上配置文件中有关路径的,如JDK路径,AndroidSDK路径,appcompat_v7包的路径等,需要配置你自己的电脑上的路径。以上的build.xml脚本的过程大致如下:

初始化,主要是删除bin目录和其中的文件,然后配置一些参数。
使用aapt工具将src目录下的资源文件编译成R.java文件。(由于这里的项目中没有使用aidl,所以这里的ant脚本中没有配置编译aidl文件的过程)
使用aapt工具将依赖库编译。
编译java源文件为class文件。
编译class文件为Dex文件,包括Dex分包。
将Dex包打成未签名的apk包,然后再签名apk。

以上就是build.xml文件中配置的过程的一个粗略说明。然后需要注意main-dex-rule.txt文件,该文件指定了哪些类放在classes.dex文件中,这里我的main-dex-rule.txt文件内容如下:
com/example/test/BaseApplication.class
com/example/test/MainActivity.class
1
2
1
2
[/code]

即将BaseApplication和MainActivity放到主Dex文件中。

编写完build.xml文件后,再编写我们工程中的java代码,这里的代码非常简单,就是一个普通的BaseApplication类,继承自Application,然后两个Activity,从MainActivity跳转到OtherActivity。

在项目的根目录下运行ant命令,有可能会遇到如下错误:

com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000)
1
1
[/code]

这是由于电脑上的jdk版本和Eclipse中配置的jdk版本不一致造成的,建议不要用太新的Java版本,这里我卸载了电脑上的java8后重新安装了java6将该问题解决。

ant打包成功后,我们可以看到项目的bin目录下,出现了如下几个文件:



可以看到dex分包已经成功生成了,而且在sign.apk文件中,也存在两个dex包。下面将sign.apk安装到手机上,发现在Android5.1系统上可以正常运行,从MainActivity跳转到OtherActivity也没问题,但是在Android4.4的手机上,从MainActivity跳转到OtherActivity时,应用直接崩溃,查看log信息发现错误主要是找不到OtherActivity类,这个问题困扰我好几天,最后发现原来在Android4.4系统上,classes2.dex文件是不会主动加载的,需要我们在代码里手动加载。下面问题来了,怎么在代码中去加载apk中的dex分包呢?


Dex分包的加载

关于dex分包的加载,网上的很多文章里虽然都有介绍,但是要么写得不详细,要么就是代码不全,这里我先是通过将上面生成的classes2.dex和classes3.dex两个dex分包放到手机的sd卡根目录下,然后在BaseApplication中通过如下的代码去加载:
package com.example.test;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

import android.app.Application;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class BaseApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
Toast.makeText(this, "Application init...", Toast.LENGTH_SHORT).show();
String dex2 = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex";
String dex3 = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes3.dex";
Log.i("yubo", "dex2 path: " + dex2);
Log.i("yubo", "dex3 path: " + dex3);
inject(dex2);
inject(dex3);
}

public String inject(String libPath) {
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath, getClassLoader());
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}

public Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

public Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}

public static void setField(Object obj, Class<?> cl, String field, Object value)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}

public Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}

public static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}

}
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
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
[/code]

在BaseApplication中加入以上代码后,重新用ant打包apk,安装到Android4.4的手机上,发现终于成功从MainActivity跳转到OtherActivity了,这说明我们放到sd卡的dex文件得到了加载,但是这里仅仅是从sd卡加载dex文件,我们的Android应用打包成apk后,dex文件都放在apk包中,这里就出现新问题了,怎么在代码中加载apk包里的dex分包呢?

在AndroidStudio项目中,我们使用了android.support.multidex包来加载dex分包,通过查看android.support.multidex包的源码,我发现在源码中就有从apk文件里读取dex文件的代码,主要是下面的部分:



下面贴上我的代码,这段代码主要是从已安装的apk中获取dex文件,并将这些dex文件拷贝到应用的私有目录中,当app启动时,从私有目录中读取并加载这些dex文件,下面是BaseApplication的完整代码:
package com.example.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import android.app.Application;
import android.util.Log;
import android.widget.Toast;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class BaseApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
Toast.makeText(this, "Application init...", Toast.LENGTH_SHORT).show();
try {
loadDex();
injectDex();
} catch (Exception e) {
e.printStackTrace();
}
}

//加载dex分包
private void loadDex() throws Exception {
File originFile = new File(getApplicationInfo().sourceDir); //需要拷贝的apk文件
File newAPK = new File(getFilesDir().getAbsolutePath() + File.separator + "app.apk"); // 获取拷贝后的apk存放路径
if(!newAPK.exists()) {
newAPK.createNewFile();
}
//拷贝apk到私有目录
copyFile(new FileInputStream(originFile), new FileOutputStream(newAPK));
//解压apk中的dex文件
ZipFile apk = new ZipFile(newAPK);
Enumeration<? extends ZipEntry> en = apk.entries();
ZipEntry zipEntry = null;
while(en.hasMoreElements()) {
zipEntry = (ZipEntry) en.nextElement();
if(!zipEntry.isDirectory() && zipEntry.getName().endsWith("dex") && !"classes.dex".equals(zipEntry.getName())) {
//拷贝dex到destDir
Log.e("yubo", "zip entry name: " + zipEntry.getName() + ", file size: " + zipEntry.getSize());
InputStream inputStream = apk.getInputStream(zipEntry);
FileOutputStream fout = openFileOutput(zipEntry.getName(), MODE_PRIVATE);
copyFile(inputStream, fout);
}
}
apk.close();
}

//从app_dex目录下取出dex文件并注入到系统的PathClassLoader
private void injectDex() {
File[] files = getFilesDir().listFiles();
if(files != null) {
for(File f : files) {
String fileName = f.getName();
if(fileName.endsWith("dex") && !"classes.dex".equals(fileName)) {
inject(f.getAbsolutePath());
Log.e("yubo", "inject file: " + f.getAbsolutePath());
}
}
}
}

//拷贝文件
private void copyFile(InputStream is, FileOutputStream fos) {
try {
int hasRead = 0;
byte[] buf = new byte[1024];
while((hasRead = is.read(buf)) > 0) {
fos.write(buf, 0, hasRead);
}
fos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fos != null) {
fos.close();
}
if(is != null) {
is.close();
}
} catch (Exception e2) {
}
}
}

//注入dex包,libPath为dex包的路径
public String inject(String libPath) {
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath, getClassLoader());
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}

public Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

public Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}

public static void setField(Object obj, Class<?> cl, String field, Object value)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {

Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}

public Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}

public static Object combineArray(Object arrayLhs, Object arrayRhs) {
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}

}
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
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
[/code]

拷贝dex文件的代码主要是loadDex()方法和injectDex()方法,这里需要注意的是,getApplicationInfo().sourceDir代表应用的apk目录,当一个应用被安装到手机上后,会在getApplicationInfo().sourceDir生成一个和被安装的apk一样的apk,我们要拷贝的也是这个apk,拷贝好apk到应用私有目录后,再解压出其中的dex文件,由于我们使用了openFileOutput(),所以dex文件的存放路径在data/data/<PackageName>/files目录下,通过getFileDir()也能得到这个目录的路径。

经过以上加载dex的过程,再使用ant打包后安装运行apk,发现从MainActivity跳转到OtherActivity不会报错了~

参考文章:

Android 热修复三部曲之基本的Ant打包脚本
Android热修复三部曲之MultiDex
分包架构
Android热修复三部曲之动态加载补丁.dex文件
dex分包变形记
Android dex分包方案

本文的demo代码已上传到我的github:

Eclipse项目

AndroidStudio项目
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  dex分包 ant Android