企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
#### **ClassLoade源码分析** > **注意**: > 1. 下面的源码是以Android6.0源码为准 > 2. 由于Android API版本的升级,低版本的的源码中DexClassLoader 和PathClassLoader可能和高版本的源码不一样,具体以高版本的源码为准。 所有ClassLoader的源码都在`/libcore/dalvik/src/main/java/dalvik/system/`目录下。(**以Android6.0源码为准**) **背景**: 为了解决65535这个问题,Google提出了multidex方案,即一个apk文件可以包含多个dex文件。 不过值得注意的是,除了第一个dex文件以外,其他的dex文件都是以资源的形式被加载的, 换句话说就是在Application初始化前将dex文件注入到系统的ClassLoader中的。 根据Android虚拟机的类加载机制,同一个类只会被加载一次,所以热修复也使用了这样的机制,要让修复后的类替换原有的类就必须让补丁包的类被优先加载,也就是插入到原有dex之前。 首先需要知道的是android中两个主要的Classloader,PathClassLoader和DexClassLoader, 它们都继承自BaseDexClassLoader Android系统通过PathClassLoader来加载系统类和主dex中的类。 而DexClassLoader则用于加载其他dex文件中的类。 他们都是继承自BaseDexClassLoader,具体的加载方法是findClass。 ![](https://box.kancloud.cn/4408a02bf47a53402e6135dfae12479e_1657x622.png) :-: 图1 ClassLoader加载类的整体流程 **为什么说PathClassLoader只能加载系统类和主dex的类呢?**,我们看一下这个类的源代码 **PathClassLoader** ~~~ package dalvik.system; /** * Provides a simple {@link ClassLoader} implementation that operates on a list * of files and directories in the local file system, but does not attempt to * load classes from the network. Android uses this class for its system class * loader and for its application class loader(s). */ public class PathClassLoader extends BaseDexClassLoader { /** * Creates a {@code PathClassLoader} that operates on a given list of files * and directories. This method is equivalent to calling * {@link #PathClassLoader(String, String, ClassLoader)} with a * {@code null} value for the second argument (see description there). * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param parent the parent class loader */ public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } /** * Creates a {@code PathClassLoader} that operates on two given * lists of files and directories. The entries of the first list * should be one of the following: * * <ul> * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as * well as arbitrary resources. * <li>Raw ".dex" files (not inside a zip file). * </ul> * * The entries of the second list should be directories containing * native library files. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } } ~~~ 源码中的注释,大概的意思是说 PathClassLoader被用来加载本地文件系统上的文件或目录,但不能从网络上加载;关键是它被用来加载系统类和我们的应用程序,这也是为什么它的两个构造函数中调用父类构造器的时候第二个参数传null, 可以查看下面 BaseDexClassLoader的源码。 **DexClassLoader** ~~~ package dalvik.system; /** * A class loader that loads classes from {@code .jar} and {@code .apk} files * containing a {@code classes.dex} entry. This can be used to execute code not * installed as part of an application. * * <p>This class loader requires an application-private, writable directory to * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create * such a directory: <pre> {@code * File dexOutputDir = context.getCodeCacheDir(); * }</pre> * * <p><strong>Do not cache optimized classes on external storage.</strong> * External storage does not provide access controls necessary to protect your * application from code injection attacks. */ public class DexClassLoader extends BaseDexClassLoader { /** * Creates a {@code DexClassLoader} that finds interpreted and native * code. Interpreted classes are found in a set of DEX files contained * in Jar or APK files. * * <p>The path lists are separated using the character specified by the * {@code path.separator} system property, which defaults to {@code :}. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; must not be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } } ~~~ **BaseDexClassLoader** ~~~ package dalvik.system; /** * Base class for common functionality between various dex-based * {@link ClassLoader} implementations. */ public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; /** * Constructs an instance. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; may be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ 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; } @Override protected URL findResource(String name) { return pathList.findResource(name); } @Override protected Enumeration<URL> findResources(String name) { return pathList.findResources(name); } @Override public String findLibrary(String name) { return pathList.findLibrary(name); } /** * Returns package information for the given package. * Unfortunately, instances of this class don't really have this * information, and as a non-secure {@code ClassLoader}, it isn't * even required to, according to the spec. Yet, we want to * provide it, in order to make all those hopeful callers of * {@code myClass.getPackage().getName()} happy. Thus we construct * a {@code Package} object the first time it is being requested * and fill most of the fields with dummy values. The {@code * Package} object is then put into the {@code ClassLoader}'s * package cache, so we see the same one next time. We don't * create {@code Package} objects for {@code null} arguments or * for the default package. * * <p>There is a limited chance that we end up with multiple * {@code Package} objects representing the same package: It can * happen when when a package is scattered across different JAR * files which were loaded by different {@code ClassLoader} * instances. This is rather unlikely, and given that this whole * thing is more or less a workaround, probably not worth the * effort to address. * * @param name the name of the class * @return the package information for the class, or {@code null} * if there is no package information available for it */ @Override 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 + "]"; } } ~~~ 看一下构造上的注释,参数解释如下: * dexPath: 需要被加载的dex文件地址,可以多个,用File.pathSeparator分割 * optimizedDirectory: dex文件被加载后会被编译器优化,优化之后的dex存放路径, 不可以为null。注意,注释中也提到需要一个应用私有的可写的一个路径, 以防止应用被注入攻击,并且给出了例子 File dexOutputDir = context.getDir(“dex”, 0); * libraryPath:包含libraries的目录列表,同样用File.pathSeparator分割,如果没有则传null就行了 * parent:父类构造器 到这里估计会问不是说了optimizedDirectory不能传null吗,那PathClassLoader怎么传了null呢? 我们继续看一下BaseClassLoader的findClass,在BaseClassLoader的findClass方法中构造方法中创建了一个叫pathList的DexPathList类型的对象,然后在findClass的时候 代码转移到了pathList的findClass中,DexPathList又是什么呢我们进去看一下。通过下面的DexPathList的构造方法的源码,可以知道第四个参数说optimizedDirectory 如果为空, 那么使用系统默认的文件夹,为什么PathClassLoader传空就行呢,其实是 因为我们的应用已经安装并优化了,优化后的dex存在于/data/dalvik-cache目录下,这就是系统默认的文件夹, 这就是说为什么我们只能用DexClassLoader去加载其他类了 **ClassLoader:loadClass** ~~~ 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) { ClassNotFoundException suppressed = null; try { clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { clazz = findClass(className); } catch (ClassNotFoundException e) { e.addSuppressed(suppressed); throw e; } } } return clazz; } ~~~ 首先通过**findLoadedClass(className)**;判断这个className的类文件是否已经加载过,如果没有加载过,继续判断父类的**parent.loadClass(className, false)**;是否已经加载过,如果都没有加载过,则调用**findClass(className)**;查找这个类,找到这个类,返回这个类。 **ClassLoader:findClass** ~~~ protected Class<?> findClass(String className) throws ClassNotFoundException { throw new ClassNotFoundException(className); } ~~~ 可以看出这个方法是空实现,需要子类来实现。 **BaseDexClassLoader:findClass** ~~~ @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; } ~~~ 这里可以看到`Class c = pathList.findClass(name, suppressedExceptions);`而pathList是BaseDexClassLoader的final变量,在其构造方法中就已经初始化 **BaseDexClassLoader:构造方法** ~~~ private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } ~~~ 可以看到pathList是由DexPathList得到的,再通过DexPathList的findClass方法查找类 **DexPathList:findClass** ~~~ 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; } ~~~ 遍历Element数组中元素(dex文件),得到dex文件,再通过DexFile的loadClassBinaryName方法得到字节码。 dexElements是维护dex文件的数组, 每一个item对应一个dex文件。DexPathList遍历dexElements,从每一个dex文件中查找目标类,在找到后即返回并停止遍历。 **DexPathList的成员变量以及其初始化** ~~~ final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String zipSeparator = "!/"; private final ClassLoader definingContext; private final Element[] dexElements; private final Element[] nativeLibraryPathElements; private final List<File> nativeLibraryDirectories; private final List<File> systemNativeLibraryDirectories; private final IOException[] dexElementsSuppressedExceptions; /** * Constructs an instance. * * @param definingContext the context in which any as-yet unresolved * classes should be defined * @param dexPath list of dex/resource path elements, separated by * {@code File.pathSeparator} * @param libraryPath list of native library directory path elements, * separated by {@code File.pathSeparator} * @param optimizedDirectory directory where optimized {@code .dex} files * should be found and written to, or {@code null} to use the default * system directory for same */ public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, 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<IOException> suppressedExceptions = new ArrayList<IOException>(); this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); this.nativeLibraryDirectories = splitPaths(libraryPath, false); this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true); List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, suppressedExceptions); if (suppressedExceptions.size() > 0) { this.dexElementsSuppressedExceptions = suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); } else { dexElementsSuppressedExceptions = null; } } } ~~~ Element是类DexPathList的一个内部类,它其中重要的一个变量就是DexFile,就是dex文件。 DexPathList的初始化中通过makePathElements方法 ~~~ this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions); this.nativeLibraryDirectories = splitPaths(libraryPath, false); ~~~ makePathElements方法核心作用就是将指定路径中的所有文件转化成DexFile同时存储到到Element[]这个数组中。nativeLibraryDirectories 就是lib库了。 最终在findclass方法中实现。 **DexPathList:makePathElements** ~~~ private static Element[] makePathElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions) { List<Element> elements = new ArrayList<>(); /* * 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.add(new Element(file, true, null, null)); } else if (file.isFile()) { 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 { zip = file; try { dex = loadDexFile(file, optimizedDirectory); } catch (IOException suppressed) { /* * IOException might get thrown "legitimately" by the DexFile constructor if * the zip file turns out to be resource-only (that is, no classes.dex file * in it). * Let dex == null and hang on to the exception to add to the tea-leaves for * when findClass returns null. */ suppressedExceptions.add(suppressed); } } } else { System.logW("ClassLoader referenced unknown path: " + file); } if ((zip != null) || (dex != null)) { elements.add(new Element(dir, false, zip, dex)); } } return elements.toArray(new Element[elements.size()]); } ~~~ **DexPathList:loadDexFile** ~~~ private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException { if (optimizedDirectory == null) { return new DexFile(file); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0); } } ~~~ **DexFile:loadClassBinaryName、defineClass、defineClassNative方法** ~~~ public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { return defineClass(name, loader, mCookie, suppressed); } private static Class defineClass(String name, ClassLoader loader, Object 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, Object cookie) throws ClassNotFoundException, NoClassDefFoundError; ~~~ defineClassNative是一个native层的方法。