Android(安卓)APK DEX分包总结
最近几天一直在学习Android APK Dex分包的相关知识,因为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'}
- 在AndroidManifest.xml文件中配置Application如下:
android:name="android.support.multidex.MultiDexApplication"
这里配置的Application继承自android.support.multidex包中的MultiDexApplication,如果项目中已经有自定义的Application,可以让该Application继承MultiDexApplication,或者在Application的attachBaseContext方法中做如下调用:
protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this);}
- 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="." /> <property name="appcompat-dir" value="H:\adt-bundle-windows-x86_64-20140702\workspace\appcompat_v7" /> <target name="init" > <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> <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" /> <property name="tools.aapt" value="${platform-tools-folder}\aapt.exe" /> <property name="platform-folder" value="${sdk-folder}\platforms\android-20" /> <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" > <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> <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> <target name="compile" depends="GenR" > <echo> Compiling java source code... echo> <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" /> classpath> javac> <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" /> classpath> <classpath> <fileset dir="${appcompat-dir}/libs" includes="*.jar" /> classpath> javac> target> <property name="tools.dx" value="${platform-tools-folder}\dx.bat" /> <target name="dex" depends="compile" > <echo> Generate multi-dex... echo> <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" /> <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" /> <arg value="-u" /> <arg value="-z" /> <arg value="${project-dir}/bin/resources.arsc" /> <arg value="-f" /> <arg value="bin/classes.dex" /> java> target> <property name="jdk-folder" value="C:\Program Files\Java\jdk1.6.0_43" /> <property name="tools.jarsigner" value="${jdk-folder}\bin\jarsigner.exe" /> <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> <target name="aapt-add-dex" > <echo message="${dir.name}" /> <propertyregex casesensitive="false" input="${dir.name}" property="dexfile" regexp="classes(.*).dex" select="\0" /> <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>
这里需要注意的一点是:以上配置文件中有关路径的,如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.classcom/example/test/MainActivity.class
即将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)
这是由于电脑上的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; }}
在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; }}
拷贝dex文件的代码主要是loadDex()方法和injectDex()方法,这里需要注意的是,getApplicationInfo().sourceDir代表应用的apk目录,当一个应用被安装到手机上后,会在getApplicationInfo().sourceDir生成一个和被安装的apk一样的apk,我们要拷贝的也是这个apk,拷贝好apk到应用私有目录后,再解压出其中的dex文件,由于我们使用了openFileOutput(),所以dex文件的存放路径在data/data/
经过以上加载dex的过程,再使用ant打包后安装运行apk,发现从MainActivity跳转到OtherActivity不会报错了~
参考文章:
- Android 热修复三部曲之基本的Ant打包脚本
- Android热修复三部曲之MultiDex 分包架构
- Android热修复三部曲之动态加载补丁.dex文件
- dex分包变形记
- Android dex分包方案
本文的demo代码已上传到我的github:
Eclipse项目
AndroidStudio项目
更多相关文章
- 没有一行代码,「2020 新冠肺炎记忆」这个项目却登上了 GitHub 中
- 一款常用的 Squid 日志分析工具
- GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
- RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
- Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
- android实现权限管理和签名静默卸载
- Android目录结构(详解)
- 什么特性造就了Android快速启动
- Android中使用kotlin实现多行文本的上下滚动播放