##Classloader基础
Classloader的简单定义:
通过类的全限定名来获取描述此类的二进制字节流,负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个类加载器都有一个父类加载器(包含的关系),顶级类加载器(native)除外。
- 独立的类名称空间
能够结合java类本身来确定该类在Java虚拟机中的唯一性。用通俗的话来说就是:比较两个类是否相等,只有这两个类是由同一个类加载器加载才有意义 - 双亲委托模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
这部分文件在Android源码路径分别为:
libcore/dalvik/src/main/java/dalvik/system/ 、art/runtime/native/
public abstract class ClassLoader {
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
// Don't want to see this.
}
if (clazz == null) {
clazz = findClass(className);
}
}
return clazz;
}
protected final Class<?> findLoadedClass(String className) {
ClassLoader loader;
if (this == BootClassLoader.getInstance())
loader = null;
else
loader = this;
return VMClassLoader.findLoadedClass(loader, className);
}
在Android中,提供了PathClassLoader和DexClassLoader两个加载器,它们都继承于BaseDexClassLoader,而BaseDexClassLoader则继承于ClassLoader。对于PathClassLoader,Android是使用这个类作为其系统类和应用类的加载器,只能去加载已经安装到Android系统中的apk文件。而DexClassLoader可以用来从.jar和.apk类型的文件内部加载classes.dex文件,可以用来执行非安装的程序代码。从上面ClassLoader的源码可以看出当需要PathClassLoader和DexClassLoader自己加载类的时候,则会掉用findClass(String name)
方法,而PathClassLoader和DexClassLoader都只是简单调用了BaseDexClassLoader的构造方法,所以我们只用看BaseDexClassLoader的findClass(String name)
方法。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
}
可以看出BaseDexClassLoader会掉用pathList
的findClass()
方法,而pathList
是在BaseDexClassLoader构造时创建的,是DexPathList的实例。
final class DexPathList {
private final Element[] dexElements;
……
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
File zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
} else if (file.isDirectory()) {
// We support directories for looking up resources.
// This is only useful for running libcore tests.
elements.add(new Element(file, true, null, null));
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
可以看出DexPathList会轮询自己的Element数组,通过Element的DexFile属性对象通过loadClassBinaryName()
方法来加载类,而且一旦加载成功,则返回,不会继续寻找。而Element数组则是在DexPathList的构造方法中通过makeDexElements()
来生成的,每个Element对象则对应一个dex单元,它的DexFile属性对象对应这个单元里面的dex文件。这个方法是后面一个热修复方案原理的重要来源。下面我们看DexFile的loadClassBinaryName()
方法是如何加载类的。
public final class DexFile {
private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
}
mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie);
}
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, int cookie,
List<Throwable> suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, int cookie)
throws ClassNotFoundException, NoClassDefFoundError;
}
可以看出DexFile的loadClassBinaryName()
方法最终会掉用native方法defineClassNative()
来加载类。而从DexFile的私有构造方法DexFile(String sourceName, String outputName, int flags)
来看加载输出文件路径必须所属应用本身,否则会报错,其中DexClassLoader最终会调用到该方法,所以需要注意。
/###########################################################################################################更新分割线############################################################################################################/
从参考文章中,我们对PathClassLoader有下面的认识:
PathClassLoader只能加载已经安装到 Android 系统中的 apk 文件,也就是 /data/app 目录下的 apk 文件。因为PathClassLoader 会去读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候 由 Dalvik 生成的。例如,如果包的名字是 com.qihoo360.test,Android 应用安装之后都保存在 /data/app 目录下,即 /data/app/com.qihoo360.test-1.apk,那么 /data/dalvik-cache 目录下就会生成 data@app@com.qihoo360.test-1.apk@classes.dex 文件。 在调用 PathClassLoader 时,它就会按照这个规则去找 dex 文件,如果你指定的 apk 文件是 /sdcard/test.apk,它按照这个 规则就会去读 /data/dalvik-cache/sdcard@test.apk@classes.dex 文件,显然这个文件不会存在,所以 PathClassLoader 会报错。
下面我们写一个demo来验证上面的结论,我们分别通过三种方式来加载一个写好的类:DexClassLoader、PathClassLoader和DexFile。首先把写好的类打成dex放在zip包中,这个我们直接用AndFix的demo里面的out.apatch即可。
private Class dexLoadClass() {
try {
DexClassLoader dexClassLoader = new DexClassLoader(extraDexPath, outputPath, getApplicationInfo().nativeLibraryDir, getClassLoader());
printElements(dexClassLoader);
Class clazz = dexClassLoader.loadClass(loadClassName);
Log.i(TAG, "dexClassLoader load result:" + (clazz != null ? clazz.getSimpleName() : "null"));
return clazz;
} catch (ClassNotFoundException e) {
e.printStackTrace();
Log.i(TAG, "dexClassLoader load failed");
}
return null;
}
private Class dexFileLoadClass() {
try {
DexFile dexFile = DexFile.loadDex(extraDexPath, optimizedPathFor(new File(extraDexPath), new File(outputPath)), 0);
Class clazz = dexFile.loadClass(loadClassName, getClassLoader());
Log.i(TAG, "dexFile load result:" + (clazz != null ? clazz.getSimpleName() : "null"));
return clazz;
} catch (IOException e) {
e.printStackTrace();
Log.i(TAG, "dexFile load failed");
}
return null;
}
private Class pathLoadClass() {
try {
PathClassLoader pathClassLoader = new PathClassLoader(extraDexPath, getClassLoader());
printElements(pathClassLoader);
Class clazz = pathClassLoader.loadClass(loadClassName);
Log.i(TAG, "pathClassLoader load result:" + (clazz != null ? clazz.getSimpleName() : "null"));
return clazz;
} catch (ClassNotFoundException e) {
e.printStackTrace();
Log.i(TAG, "pathClassLoader load failed");
}
return null;
}
可以看出我们是直接构造了这三个对象来进行类的加载的,然后打印出了Element数组的个数。我们再写个测试方法来调用它们,把这个测试方法放在Application的attachBaseContext()
方法里运行。
private final String SECONDARY_FOLDER_NAME = "code_cache";
private final String DexDir = "dex";
private final String DEX_SUFFIX = ".dex";
private final String loadClassName = "com.euler.test.A_CF";
private final String dexFileName = "out.zip";
private final String TAG = "popo";
private String extraDexPath;
private String outputPath;
private void init() {
extraDexPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + dexFileName;
// copyDex(extraDexPath);
Log.i(TAG, "apk exist:" + new File(extraDexPath).exists());
outputPath = getApplicationInfo().dataDir + File.separator + SECONDARY_FOLDER_NAME;
File file = new File(outputPath);
if (!file.exists() && !file.mkdirs()) {
return;
}
Class dexClazz = dexLoadClass();
Class pathClazz = pathLoadClass();
Class dexFileClazz = dexFileLoadClass();
Log.i(TAG, "dexClazz == pathClazz:" + (dexClazz == pathClazz ? "true" : "false"));
Log.i(TAG, "dexClazz == dexFileClass:" + (dexClazz == dexFileClazz ? "true" : "false"));
Log.i(TAG, "dexFileClass == pathClass:" + (dexFileClazz == pathClazz ? "true" : "false"));
if (dexClazz != null) {
Log.i(TAG, "dexClazz classloader:" + dexClazz.getClassLoader());
}
if (pathClazz != null) {
Log.i(TAG, "pathClazz classloader:" + pathClazz.getClassLoader());
}
if (dexFileClazz != null) {
Log.i(TAG, "dexFileClazz classloader:" + dexFileClazz.getClassLoader());
}
Log.i(TAG, "app classloader:" + getClassLoader());
try {
Class appClazz = getClassLoader().loadClass(loadClassName);
if (appClazz != null) {
Log.i(TAG, "dexFileClazz == appClazz:" + (dexFileClazz == appClazz ? "true" : "false"));
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
首先我们在乐视手机Le X620 - 6.0上运行,日志为下面的结果:
I/popo﹕ apk exist:true
I/popo﹕ DexClassLoader elements size:1
I/popo﹕ dexClassLoader load result:A_CF
I/popo﹕ PathClassLoader elements size:1
I/popo﹕ pathClassLoader load result:A_CF
I/popo﹕ dexFile load result:A_CF
I/popo﹕ dexClazz == pathClazz:false
I/popo﹕ dexClazz == dexFileClass:false
I/popo﹕ dexFileClass == pathClass:false
I/popo﹕ dexClazz classloader:dalvik.system.DexClassLoader[DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/data/app/com.example.pop.testandroidclassloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
I/popo﹕ pathClazz classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]]
I/popo﹕ dexFileClazz classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.pop.testandroidclassloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
I/popo﹕ app classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1/base.apk"],nativeLibraryDirectories=[/data/app/com.example.pop.testandroidclassloader-1/lib/arm64, /vendor/lib64, /system/lib64]]]
I/popo﹕ dexFileClazz == appClazz:true
从日志信息我们可以看出三种方式都加载成功了,并且它们三个各不相等,说明不是一个加载器加载的,从分别打印出的classloader信息我们也可以看出来。最后我们又用了app的classloader(也就是PathClassLoader的一个实例)来加载这个类,我们看到也加载成功了,并且加载出来的Class对象和DexFile加载出来的Class对象是相等的,而且两者打印出来的classloader信息也是一致的,所以最后的app的classloader加载应该只是直接返回了之前DexFile加载出来的Class对象,这是因为我们在构造DexFile(即调用DexFile的loadDex()
方法)时传入的参数为app的classloader。
接着我们再在华为手机H60-L02 - 4.4.2上运行,看看会有什么样的结果:
I/popo﹕ apk exist:true
I/popo﹕ DexClassLoader elements size:1
I/popo﹕ dexClassLoader load result:A_CF
I/popo﹕ PathClassLoader elements size:1
I/popo﹕ pathClassLoader load failed
I/popo﹕ dexFile load result:A_CF
I/popo﹕ dexClazz == pathClazz:false
I/popo﹕ dexClazz == dexFileClass:false
I/popo﹕ dexFileClass == pathClass:false
I/popo﹕ dexClazz classloader:dalvik.system.DexClassLoader[DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/data/app-lib/com.example.pop.testandroidclassloader-1, /vendor/lib, /system/lib, /data/datalib]]]
I/popo﹕ dexFileClazz classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.example.pop.testandroidclassloader-1, /vendor/lib, /system/lib, /data/datalib]]]
I/popo﹕ app classloader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.pop.testandroidclassloader-1.apk"],nativeLibraryDirectories=[/data/app-lib/com.example.pop.testandroidclassloader-1, /vendor/lib, /system/lib, /data/datalib]]]
I/popo﹕ dexFileClazz == appClazz:true
从结果可以看出除了我们自己new的pathClassLoader没有加载成功,其它的基本跟在6.0上的结果是一样的,在运用app本身的pathClassLoader构造的DexFile成功加载了该类后,app本身的pathClassLoader自然也成功加载了该类。只是我们自己new的pathClassLoader为什么没有加载成功,是否又和参考文章中说的原因一样呢,下面我们把其它的都注释掉,只调用自己构造的pathClassLoader来加载,看情况如何:
//Le X620 - 6.0
I/popo﹕ apk exist:true
W/dex2oat﹕ type=1400 audit(0.0:16537): avc: denied { module_request } for kmod="personality-8" scontext=u:r:untrusted_app:s0:c512,c768 tcontext=u:r:kernel:s0 tclass=system permissive=0
E/dex2oat﹕ Failed to create oat file: /data/dalvik-cache/arm64/storage@emulated@0@out.zip@classes.dex: Permission denied
I/dex2oat﹕ dex2oat took 379.308us (threads: 10)
W/art﹕ Failed execv(/system/bin/dex2oat --runtime-arg -classpath --runtime-arg --instruction-set=arm64 --instruction-set-features=smp,a53 --runtime-arg -Xrelocate --boot-image=/system/framework/boot.art --runtime-arg -Xms64m --runtime-arg -Xmx512m --instruction-set-variant=cortex-a53 --instruction-set-features=default --dex-file=/storage/emulated/0/out.zip --oat-file=/data/dalvik-cache/arm64/storage@emulated@0@out.zip@classes.dex) because non-0 exit status
I/popo﹕ PathClassLoader elements size:1
I/popo﹕ pathClassLoader load result:A_CF
//H60-L02 - 4.4.2
I/popo﹕ apk exist:true
E/dalvikvm﹕ Dex cache directory isn't writable: /data/dalvik-cache
I/dalvikvm﹕ Unable to open or create cache for /storage/emulated/0/out.zip (/data/dalvik-cache/storage@emulated@0@out.zip@classes.dex)
I/popo﹕ PathClassLoader elements size:1
W/System.err﹕ java.lang.ClassNotFoundException: Didn't find class "com.euler.test.A_CF" on path: DexPathList[[zip file "/storage/emulated/0/out.zip"],nativeLibraryDirectories=[/vendor/lib, /system/lib, /data/datalib]]
W/System.err﹕ at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
W/System.err﹕ at com.example.pop.testandroidclassloader.MyApplication.pathLoadClass(MyApplication.java:110)
W/System.err﹕ Suppressed: java.io.IOException: unable to open DEX file
W/System.err﹕ at dalvik.system.DexFile.openDexFileNative(Native Method)
W/System.err﹕ at com.example.pop.testandroidclassloader.MyApplication.pathLoadClass(MyApplication.java:108)
W/System.err﹕ ... 17 more
I/popo﹕ pathClassLoader load failed
可以看到单独运行,在6.0上pathClassLoader依旧可以成功加载,而在4.4.2上依旧失败,并且从日志上可以看出它们都尝试在/data/dalvik-cache下创建加载后文件,但都因为权限问题失败了。从老罗的ART运行时无缝替换Dalvik虚拟机的分析中我们知道加载时6.0中art执行的是dex2oat生成的是本地运行文件oat,而4.4.2上dalvik执行的是dex2opt生成的是odex优化文件。而且第三篇参考文章写于2010年,当时都还未有art,所以猜测应该是5.0版本更改虚拟机为art时做了调整pathClassLoader可以加载外部包含dex的zip包了,不过这个需要源码的验证!
小结:
1.Android中继续使用类命名空间隔离和双亲委托模型机制。
2.Android中应用大部份类都是通过PathClassLoader来加载,而且Android系统在启动应用的时候只会加载apk中的classes.dex文件。如果有多个dex文件,则在5.0以下版本需要自己动态加载。
3.Android在第一次加载dex文件的时候,如果虚拟机是dalvik会掉用dexopt进行优化,生成生成一个odex文件,即 Optimised Dex,如果是art则会调用dex2oat生成本地运行文件oat。
4.在虚拟机是dalvik时,PathClassLoader只能加载已经安装的apk文件,而art时则可能加载外部包含dex的zip文件也能成功。不过为了兼容建议还是通过DexClassLoader来加载。
以上有些结论都是我自己通过程序结果的推论和部份猜测,有什么不对的地方,希望有兴趣阅读后发现的朋友及时告知。下面为测试的demo下载地址,demo中本来用的AndFix的out.apatch但是后来在华为4.4.2的手机上跑无法识别,所以后来改为zip后缀才好,还有如果要放在sd卡上的话,要特别注意权限问题,最开始我忘记添加权限,一直加载不了,弄了很长时间特别苦恼,而且因为后面运行在native层并不能从日志看出是权限的问题,后来加上了有些6.0的手机上默认权限又没有打开,还是加载失败。
demo下载地址
参考链接:
http://blog.csdn.net/xyang81/article/details/7292380
https://segmentfault.com/a/1190000004062880
http://blog.csdn.net/quaful/article/details/6096951
http://blog.kifile.com/android/2015/11/10/dex_class_loader.html