【Android热修复与插件化 三】ClassLoader详解
###一. Android中ClassLoader的种类
Android的ClassLoader与Java的ClassLoader基本是一一对应的。如果对Java的ClassLoader不是很了解,可以参考《【Java 虚拟机】类加载器》。
- BootClassLoader(Java的BootStrap ClassLoader)
用于加载Android Framework层class文件。 - PathClassLoader(Java的App ClassLoader)
用于加载已经安装到系统中的apk中的class文件。 - DexClassLoader(Java的Custom ClassLoader)
用于加载指定目录中的class文件。 - BaseDexClassLoader
是PathClassLoader和DexClassLoader的父类。
###二. Android中ClassLoader的特点
遵循双亲委派模型
ClassLoader在加载一个class文件时:会询问当前ClassLoader是否已经加载过子类,如果已经加载过则直接返回,不再重复加载。如果没有加载过,会去查询当前ClassLoader的parent是否已经加载过。
因为遵循双亲委派模型,Android中的classLoader具有两个特点:
- 类加载共享
当一个class文件被任何一个ClassLoader加载过,就不会再被其他ClassLoader加载。 - 类加载隔离
不同ClassLoader加载的class文件肯定不是一个。举个栗子,一些系统层级的class文件在系统初始化的时候被加载,比如java.net.String,这个是在应用启动前就被系统加载好的。如果在一个应用里能简单地用一个自定义的String类把这个String类替换掉的话,将有严重的安全问题。
###三. ClassLoader源码详解
我们从ClassLoader.java的loadClass()方法看起。我们知道Android的ClassLoader是实现了双亲委派模型的,我们来从源码角度来看下是如何实现的。
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自身是否加载过该class文件,如果没有再判断父ClassLoader是否加载过,如果都没有加载过再自己去加载。这和我们上述的双亲委派模型思想完全一致。
好了,我们来看下ClassLoader是如何去加载class文件的呢?也就是去看下findClass()方法的具体实现。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
很遗憾,findClass()方法是一个空实现,也就是说它的具体实现是交给子类的。
如图,可以看到DexClassLoader和PathClassLoader是ClassLoader的间接实现类。
所以,下面我们来着重讲解一下DexClassLoader和PathClassLoader的源码。
###四. DexClassLoader、PathClassLoader、BaseDexClassLoader源码详解
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); }}
可以看到DexClassLoader的源码非常简单,只有一个构造方法。我们来看下其四个参数都是什么含义。
- dexPath。要加载的dex文件路径。
- optimizedDirectory。dex文件要被copy到的目录路径。
- libraryPath。apk文件中类要使用的c/c++代码。
- parent。父装载器,也就是真正loadclass的装载器。
我们接下来看PathClassLoader的源码。
public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); }}
它的源码也只是有两个构造方法,我们来看第二个构造方法,可以看出,它与DexClassLoader的构造方法的区别就是少了一个要把dex文件copy到的目录路径。正是因为缺少这个路径,我们的PathClassLoader只能用来加载安装过的apk中的dex文件。
这两个ClassLoader的真正核心方法都在BaseDexClassLoader中,我们现在来看下源码。
public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List suppressedExceptions = new ArrayList(); 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; } /** * @hide */ public void addDexPath(String dexPath) { pathList.addDexPath(dexPath, null /*optimizedDirectory*/); } @Override protected URL findResource(String name) { return pathList.findResource(name); } @Override protected Enumeration findResources(String name) { return pathList.findResources(name); } @Override public String findLibrary(String name) { return pathList.findLibrary(name); } protected synchronized Package getPackage(String name) { if (name != null && !name.isEmpty()) { Package pack = super.getPackage(name); if (pack == null) { pack = definePackage(name, "Unknown", "0.0", "Unknown", "Unknown", "0.0", "Unknown", null); } return pack; } return null; } /** * @hide */ public String getLdLibraryPath() { StringBuilder result = new StringBuilder(); for (File directory : pathList.getNativeLibraryDirectories()) { if (result.length() > 0) { result.append(':'); } result.append(directory); } return result.toString(); } @Override public String toString() { return getClass().getName() + "[" + pathList + "]"; }}
可以看到,这个类的实现也是比较简单。我们来分析一下。
我们看到BaseDexClassLoader有一个成员变量DexPathList,其次它的核心方法是findClass()。我们来看下具体实现。
@Override protected Class<?> findClass(String name) throws ClassNotFoundException { List suppressedExceptions = new ArrayList(); 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; }
我们看到,findClass其实是通过成员变量pathList的findClass()方法来查找的。
所以,我们接下来还需要去看DexPathList的源码。
###五. DexPathList源码详解(重点)
由于DexPathList源码较长,我们这里分段讲解。
####1. 构造方法
public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { if (definingContext == null) { throw new NullPointerException("definingContext == null"); } if (dexPath == null) { throw new NullPointerException("dexPath == null"); } if (optimizedDirectory != null) { if (!optimizedDirectory.exists()) { throw new IllegalArgumentException( "optimizedDirectory doesn't exist: " + optimizedDirectory); } if (!(optimizedDirectory.canRead() && optimizedDirectory.canWrite())) { throw new IllegalArgumentException( "optimizedDirectory not readable/writable: " + optimizedDirectory); } } this.definingContext = definingContext; ArrayList suppressedExceptions = new ArrayList(); // save dexPath for BaseDexClassLoader this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, suppressedExceptions, definingContext); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } }
构造方法里最重要的一行代码是
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
这行代码的意思是构造一个Element数组。那么Element是什么呢?它是DexPathList的一个内部类。
static class Element { private final File dir; private final boolean isDirectory; private final File zip; private final DexFile dexFile; private ClassPathURLStreamHandler urlHandler; private boolean initialized; public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) { this.dir = dir; this.isDirectory = isDirectory; this.zip = zip; this.dexFile = dexFile; } @Override public String toString() { if (isDirectory) { return "directory \"" + dir + "\""; } else if (zip != null) { return "zip file \"" + zip + "\"" + (dir != null && !dir.getPath().isEmpty() ? ", dir \"" + dir + "\"" : ""); } else { return "dex file \"" + dexFile + "\""; } } public synchronized void maybeInit() { if (initialized) { return; } initialized = true; if (isDirectory || zip == null) { return; } try { urlHandler = new ClassPathURLStreamHandler(zip.getPath()); } catch (IOException ioe) { /* * Note: ZipException (a subclass of IOException) * might get thrown by the ZipFile constructor * (e.g. if the file isn't actually a zip/jar * file). */ System.logE("Unable to open zip file: " + zip, ioe); urlHandler = null; } } public String findNativeLibrary(String name) { maybeInit(); if (isDirectory) { String path = new File(dir, name).getPath(); if (IoUtils.canOpenReadOnly(path)) { return path; } } else if (urlHandler != null) { // Having a urlHandler means the element has a zip file. // In this case Android supports loading the library iff // it is stored in the zip uncompressed. String entryName = new File(dir, name).getPath(); if (urlHandler.isEntryStored(entryName)) { return zip.getPath() + zipSeparator + entryName; } } return null; } public URL findResource(String name) { maybeInit(); // We support directories so we can run tests and/or legacy code // that uses Class.getResource. if (isDirectory) { File resourceFile = new File(dir, name); if (resourceFile.exists()) { try { return resourceFile.toURI().toURL(); } catch (MalformedURLException ex) { throw new RuntimeException(ex); } } } if (urlHandler == null) { /* This element has no zip/jar file. */ return null; } return urlHandler.getEntryUrlOrNull(name); } }
其中,它有一个非常重要的成员变量DexFile。
接着回到主流程,我们通过makeDexElements()方法得到了一个elements数组。那么,makeDexElements()方法具体干了什么呢?
private static Element[] makeElements(List files, File optimizedDirectory, List suppressedExceptions, boolean ignoreDexFiles, ClassLoader loader) { Element[] elements = new Element[files.size()]; int elementsPos = 0; /* * Open all files and load the (direct or contained) dex files * up front. */ for (File file : files) { File zip = null; File dir = new File(""); DexFile dex = null; String path = file.getPath(); String name = file.getName(); if (path.contains(zipSeparator)) { String split[] = path.split(zipSeparator, 2); zip = new File(split[0]); dir = new File(split[1]); } else if (file.isDirectory()) { // We support directories for looking up resources and native libraries. // Looking up resources in directories is useful for running libcore tests. elements[elementsPos++] = new Element(file, true, null, null); } else if (file.isFile()) { if (!ignoreDexFiles && name.endsWith(DEX_SUFFIX)) { // Raw dex file (not inside a zip/jar). try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { zip = file; if (!ignoreDexFiles) { try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { suppressedExceptions.add(suppressed); } } } } else { System.logW("ClassLoader referenced unknown path: " + file); } if ((zip != null) || (dex != null)) { elements[elementsPos++] = new Element(dir, false, zip, dex); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; }
它的作用是通过loadDexFile()方法把dex文件都加载出来,然后返回一个elements数组。
接着,我们来看DexPathList中最重要的方法findClass()。
public Class findClass(String name, List 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; }
它就是通过遍历elements数组,拿到里面的每一个dex文件,通过DexFile的loadClassBinaryName()方法找到class字节码。通过查看DexFile源码,可以得知loadClassBinaryName()方法最终是调用的底层C++方法来load class。
至此,我们就完整地了解了ClassLoader加载class的具体实现。
###六. 总结
本文讲解了Android中几种ClassLoader的作用,并且从源码角度讲解了class的加载过程。在Android中加载class,其实最终是通过DexPathList的findClass来加载的。
另外,这里贴一下几个文件源码的查看地址。
http://androidxref.com/7.1.2_r36/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
http://androidxref.com/7.1.2_r36/xref/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
http://androidxref.com/7.1.2_r36/xref/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
以上就是本文的全部内容,如有疑问,欢迎留言提问。
更多相关文章
- Android中TextView中内容不换行的解决方法
- Android模拟、实现、触发系统按键事件的方法
- android 2.2+ 完全退出程序的方法
- android读写文件
- android MediaPlayer出现RuntimeException: failure code: -38崩
- Android内核的根文件系统