正常apk打包后的文件目录是含有AndroidManifest.xml、R、resource.arcs(资源的索引)、assets、lib、classes.dex这几个模块,而分包后又是怎么样的情况呢?分包之后dex将会多出几个dex,出现classes2.dex,classes3.dex等.
分包解决的问题就是为了解决65536问题,安卓内部使用的short来存放方法数,而short的最大范围就是65536,所以一旦一个dex的方法超过了65536,就会抛出异常.
Java中的常见类加载器有以下:
BootstrapClassLoader:纯c++实现的类加载器,没有对应的Java类,主要加载jre/lib/目录下的核心库
ExtClassLoader:类的全名是sun.misc.launcher ExtClassLoader,主要加载jre/lib/ext/目录下的扩展包AppClassLoader:类的全名是sun.misc.Launcher AppClassLoader,主要加载CLASSPATH路径下的包
为了验证上面的说法,特地写了例子:
public class Main {
public static void main(String[] args) {
//第一步 获取 Main的类加载器
Class mainClass = Main.class;
ClassLoader mainLoader = mainClass.getClassLoader();
//输出AppClassLoader
System.out.println("mainLoader's name:"+mainLoader.toString());
//第二步 打印AppClassLoader的加载路径
URL[] murls = ((URLClassLoader)mainLoader).getURLs();
//输出 classPath路径
print(murls);
//第三步 通过getParent方法,获取mainLoader中的parent字段
ClassLoader parentLoader = mainLoader.getParent();
//输出ExtClassLoader
System.out.println("parentLoader's name:"+parentLoader.toString());
//第四步 打印ExtClassLoader的加载路径
URL[] murlsExt = ((URLClassLoader)parentLoader).getURLs();
//输出 jre/lib/ext/下的扩展包
print(murlsExt);
//第五步 通过getParent方法,获取parentLoader中的parent字段
ClassLoader parentLoaderTwo = parentLoader.getParent();
//输出空指针异常
System.out.println("parentLoaderTwo's name:"+parentLoaderTwo.toString());
//第六步 打印BootstrapClassLoader的加载路径
Class launcherClass = Class.forName("sun.misc.Launcher");
Method method_getClassPath = launcherClass.getDeclareMethod("getBootstrapClassPath",null);
if(method_getClassPath != null){
method_getClassPath.setAccessible(true);
Object mObj = method_getClassPath.invoke(null.null);
if(mObj!=null){
Method methodGetURLs = mObj.getClass().getDeclareMethod("getURLs",null);
if(methodGetURLs!=null){
methodGetURLs.setAccessible(true);
URL[] murlBoot = (URL[])methodGetURLs.invoke(mObj,null);
//输出jre/lib 下的jar包
print(murlBoot);
}
}
}
}
//打印url数组
public static void print(URL[] murls){
for(URL url:murls){
System.out.println(url);
}
}
}
接下来看这段代码:
public class Main {
public static void main(String[] args) {
Class clazz = Main.class;
ClassLoader loader = clazz.getClassLoader();
System.out.println("loader's name:"+loader.toString());
Class listClazz = ArrayList.class;
ClassLoader listLoader = listClazz.getClassLoader();
//空指针异常
System.out.println("listloader's Name:"+listLoader.toString());
}
}
一个输出将会输出appClassLoader,因为加载的是classPath路径下的类,而第二个输出将会报出空指针异常,因为ArrayList是存在于jre/lib/rt.jar中,而这个jar中的代码是BootstrapClassLoader加载的,但是因为它是纯C++的类加载器,没有java类,所以报空指针异常.
源码分析:
一个Class的加载是通过类加载的loadClass方法加载进来的
@Override
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
}
}
return c;
}
从上面代码可以清晰的看出,当当前的ClassLoader是AppClassLoader的时候,会去检测class有没有被当前classloader加载过,如果没有再去调用父类的loadClass方法,在parent.loadClass()方法中,同样先去检测当前classLoader有没有被加载过,没有被加载过,这个时候再走到parent.loadClass(name,false)这行代码,这个时候因为当前的类加载器是ExtClassLoader,它的parent为空,所以会走到findBootstralClassOrNull(name)方法,去看bootstrapClassLoader有没有加载过,如果都没有加载过,则报出空指针异常.这种机制叫做:父委托加载机制.
简单总结:
AppClassLoader–(委托父加载器)–>ExtClassLoader–(委托父加载器)—>由于父加载器是空,就委托BootstrapClassLoader去加载.
由于BootstrapClassLoader是顶级的类加载器,它不会委托任何加载器去加载.
如果BootstrapClassLoader加载成功,会将加载的类返回给ExtClassLoader,ExtClassLoader继续返回给AppClassLoader.
如果BootstrapClassLoader加载失败,这时就需要ExtClassLoader到jre/lib/ext目录下去加载,如果加载成功就直接返回给AppClassLoader,如果没有加载成功,就需要appClassLoader到classPath目录下去加载.
整个过程类似于一个递归过程.
当工程中创建了一个和jdk中一模一样的类的时候,这个类是不会被加载的,比如工程中创建一个类名和包名都是和jdk中一样的ArrayList,这时自己创建的ArrayList是不会被创建的,仍然加载的是jdk中的类,这样做为了保证软件系统的安全性,假如自己在代码写了一个ArrayList类,里面注入了一段恶意代码,如果被加载进来了,就相当于加载了一段恶意代码,别人就很有可能利用这个漏洞来攻击你的代码.
PathClassLoader:加载data/app目录下的apk文件,从这个目录可以看出,PathClassLoader主要用来加载已经安装了的apk
DexClassLoader:加载路径需要在创建DexClassLoader时传入,也就是说可以加载任何路径下的apk/dex/jar
类加载器在Android中的继承关系图:
如何分包以及整个过程可以查看这两篇博客:
http://blog.csdn.net/mynameishuangshuai/article/details/52703029
http://blog.csdn.net/mynameishuangshuai/article/details/52716877
一、谷歌官方提供的分包
因为Android系统在启动应用时只加载了主dex(Classes.dex),其他的 dex 需要我们在应用启动后进行动态加载安装。 Google 官方方案是如何加载的呢,Google官方支持Multidex 的 jar 包是 android-support-multidex.jar,该 jar 包从 build tools 21.1 开始支持.
dex包的加载主要是依赖于Android中的BaseDexClassLoader,BaseDexClassLoader中有一个属性DexPathList,DexPathList中有一个dexElements数组,里面存放的就是dex数组,而使用谷歌官方提供的分包后,是将PathClassLoader加载的dex与DexClassLoader的dex合并到一个新的elements数组,再重新赋值到dexElements.(PathClassLoader与DexClassLoader是BaseDexClassLoader)
二、手动动态加载dex
/**
* 动态加载分dex
*/
private void install() {
//创建存放dex的目录
File dexDir = new File(this.getFilesDir(), "dex");
if (dexDir != null && !dexDir.exists()) {
dexDir.mkdirs();
}
//创建存放odex的目录
File odexDir = new File(this.getFilesDir(), "odex");
if (odexDir != null && !odexDir.exists()) {
odexDir.mkdirs();
}
File dexFile = new File(dexDir, "libs.apk");
//copy assets目录下的libs.apk到dexDir目录下
int len = -1;
byte[] buf = new byte[2048];
InputStream isStream = null;
FileOutputStream fos = null;
try {
isStream = this.getAssets().open("libs.apk");
if (isStream != null) {
fos = new FileOutputStream(dexFile);
while ((len = isStream.read(buf)) != -1) {
fos.write(buf, 0, len);
}
}
if (isStream != null) {
isStream.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
ClassLoader loader = getClassLoader();
String nativeSoPath = null;
ApplicationInfo info = getApplicationInfo();
if (Build.VERSION.SDK_INT > 8) {
//Android2.2
nativeSoPath = info.nativeLibraryDir;
} else {
nativeSoPath = "/data/data/" + info.packageName + "/lib/";
}
DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), odexDir.getAbsolutePath(),
nativeSoPath, loader.getParent());
try {
Field filedParent = ClassLoader.class.getDeclaredField("parent");
if (filedParent != null) {
filedParent.setAccessible(true);
filedParent.set(loader, dexClassLoader);
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
上面采用的是https://github.com/mmin18/Dex65536进行的分包
原理是在asset目录下生成一个lib.apk,里面存放的是分dex,而它加载的原理就是在PahtClassLoader加载一个类的时候原先是委托给BootClassLoader加载,而现在是交由DexClassLoader去加载,DexClassLoader再委托给BootClassLoader去加载,实现了三个之间的关联.
Android拆分与加载Dex的多种方案对比
https://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207151651&idx=1&sn=9eab282711f4eb2b4daf2fbae5a5ca9a&3rd=MzA3MDU4NTYzMw==&scene=6#rd
美团Android DEX自动拆包及动态加载简介
https://tech.meituan.com/mt-android-auto-split-dex.html