Android 虚拟机与ClassLoader类加载笔记

1 Android虚拟机

在介绍Android的虚拟机之前,我们先来看一下JVM虚拟机之下,我们的class文件的字节码指令的Demo:

java 复制代码
public class Demo {
    public static void test() {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
}

将Demo.class文件使用命令:javap -c .\Demo.class 反编译一下字节码文件,得到如下的结果:

到这里,我们的Demo展示完了,随着我们接下来内容的介绍,将会使用到该字节码的内容。我们在介绍Android的虚拟机之前,先了解一下一些相关的概念。

1.1 JVM与Dalvik

Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。

Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基本寄存器的,而后者的指令集是基于堆栈的。

那什么是基于栈的虚拟机,什么又是基于寄存器的虚拟机?

基于栈的虚拟机

对于基于栈的虚拟机来说,每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每有一次方法调用,栈中便会多一个栈桢。最顶部的栈桢称作当前栈桢,其代表着当前执行的方法。基于栈的虚拟机通过操作数栈进行所有操作。

我们上面的字节码文件在基于栈的虚拟机中执行将会产生多次的进出栈操作(具体参考JVM内存管理笔记-CSDN博客):

那这样就导致相对的执行效率会偏低。

基于寄存器的虚拟机

基于寄存器的虚拟机中没有操作数栈,但是有很多虚拟寄存器。其实和操作数栈相同,这些寄存器也存放在运行时栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC和调用栈,方法调用的活动记录以帧为单位保存在调用栈上。

那么我们上面的Demo的test方法在基于寄存器的虚拟机中是如何执行的呢?

我们的虚拟寄存器其实可以理解为就是JVM虚拟机中"局部变量表"和"操作数栈"的结合,减少了栈帧中2个区域的读取与存入操作,没有了对应的操作,那就相对提升了执行的效率。看一下2个对比:

与JVM版相比,可以发现Dalvik版程序的指令数明显减少了,数据移动次数也明显减少了。到这里我们了解了什么基于栈的虚拟机什么是基于寄存器的虚拟机,他们的主要区别就是效率的提升。

我们Android中使用的虚拟机就是基于寄存器的虚拟机,以提升执行的效率。

1.2 ART与Dalvik

Dalvik虚拟机执行的是dex字节码,解释执行。那么什么是dex字节码呢?其实你可以理解为就是多个class文件打包成了一个dex文件。从Android 2.2版本开始,支持JIT即时编译(Just In Time)在程序运行的过程中进行选择热点代码(经常执行的代码)进行编译或者优化。而ART(Android Runtime) 是在 Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认 Android 运行时。ART虚拟机执行的是本地机器码。Android的运行时从Dalvik虚拟机替换成ART虚拟机,并不要求开发者将自己的应用直接编译成目标机器码,APK仍然是一个包含dex字节码的文件。

那么这里就有一个问题,ART执行的机器码是怎么来的呢?

Dalvik下应用在安装的过程,会执行一次优化,将dex字节码进行优化生成odex文件。而Art下将应用的dex字节码翻译成本地机器码的最恰当AOT时机也就发生在应用安装的时候。ART 引入了预先编译机制(Ahead Of Time),在安装时,ART 使用设备自带的 dex2oat 工具来编译应用,dex中的字节码将被编译成本地机器码。

从上面的图可以看出,原来Davilk虚拟机在程序安装的过程中,会将apk中的dex文件转化成OdexFile文件,再加上JIT的即时编译生成的。ART就不一样了,将对应的dex文件在APK的安装过程中直接将文件翻译成了机器码,也就是我们前面介绍的OFT(预编译机制)。

但是你也能想到ART这样搞的话肯定会将APK的安装时间延长,从而导致用户体验不是太好,感觉安装的好慢~~~,那么从Android N开始Android又将虚拟机做了一次改进:

Androd N的虚拟机运作方式

由于纯纯的使用ART虚拟机,会导致安装过程比较慢并且效率低下,所以从Android N开始使用混合方式:

ART 使用预先 (AOT) 编译,并且从 Android N混合使用AOT编译,解释和JIT。

1、最初安装应用时不进行任何 AOT 编译(安装又快了),运行过程中解释执行,对经常执行的方法进行JIT,经过 JIT 编译的方法将会记录到Profile配置文件中。

2、当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行 AOT 编译。待下次运行时直接使用。

到这里Android中的虚拟机我们已经介绍完了,那么到此时,我们是不是疑惑我们平时写的代码逻辑都是java文件或者kt文件,那这些文件又是怎么进入到虚拟机中的呢?虚拟机又是什么时候起来的呢?那带着这几个疑问我们看一下我们的类加载机制:classloader;

2 Classloader(类加载器)

任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载机制。ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

java 复制代码
class Class<T> {
 ...
 private transient ClassLoader classLoader;
 ...
}

首先我们看一下ClassLoader的代码结构:

ClassLoader是一个抽象类,而它的具体实现类主要有:

  • BootClassLoader

用于加载Android Framework层class文件。

  • PathClassLoader

用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex

  • DexClassLoader

用于加载指定的dex,以及jar、zip、apk中的classes.dex

他们之间的关系:

ClassLoader本身就是一个抽象类:

java 复制代码
public abstract class ClassLoader {

    static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }
    // 省略代码.....
}

ClassLoader的代码与JVM中的ClassLoader类是一样的,但是Android自己实现了2个重要的类加载器:java/lang/ClassLoader$BootClassLoader 和 dalvik/system/PathClassLoader。

其中BootClassLoader是ClassLoader中的一个子类,用于加载Android Framework层的class文件,那什么是Andrid framework层的class文件呢?比如:android.app.Activity文件就是framework层的class文件,但是在build.grade文件中引入:

Groovy 复制代码
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

其中我们熟悉的androidx.activity.ComponentActivity就不是android framework层的代码,所以androidx.activity.ComponentActivity类就不是BootClassLoader加载器加载的。

那我们平时自己写的一些java类(com.xxx.MainActivity)或者kt类,都是使用PathClassLoader类加载将java文件编译生成的class文件加载到虚拟机中执行的。

我们可以使用如下的方法获取出加载该类的类加载器:

java 复制代码
import com.donnycoy.classloderdemo.R;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e("MainActivity", "APP getClassLoader: " + getClassLoader());
        Log.e("MainActivity", "Activity: " + Activity.class.getClassLoader());
        Log.e("MainActivity", "AppCompatActivity: " + AppCompatActivity.class.getClassLoader());
    }
}

运行结果:

Groovy 复制代码
2025-02-17 15:53:21.808  5886-5886  MainActivity            com.donnycoy.classloderdemo          E  APP getClassLoader: dalvik.system.PathClassLoader[DexPathList[[dex file "/data/data/com.donnycoy.classloderdemo/code_cache/.overlay/base.apk/classes3.dex", zip file "/data/app/~~cH9o47591Txac7KbsILMrA==/com.donnycoy.classloderdemo-BrBQW_URo0xFxQ7vGd4l_w==/base.apk"],nativeLibraryDirectories=[/data/app/~~cH9o47591Txac7KbsILMrA==/com.donnycoy.classloderdemo-BrBQW_URo0xFxQ7vGd4l_w==/lib/x86, /system/lib, /system_ext/lib]]]
2025-02-17 15:53:21.808  5886-5886  MainActivity            com.donnycoy.classloderdemo          E  Activity: java.lang.BootClassLoader@8597e21
2025-02-17 15:53:21.808  5886-5886  MainActivity            com.donnycoy.classloderdemo          E  AppCompatActivity: dalvik.system.PathClassLoader[DexPathList[[dex file "/data/data/com.donnycoy.classloderdemo/code_cache/.overlay/base.apk/classes3.dex", zip file "/data/app/~~cH9o47591Txac7KbsILMrA==/com.donnycoy.classloderdemo-BrBQW_URo0xFxQ7vGd4l_w==/base.apk"],nativeLibraryDirectories=[/data/app/~~cH9o47591Txac7KbsILMrA==/com.donnycoy.classloderdemo-BrBQW_URo0xFxQ7vGd4l_w==/lib/x86, /system/lib, /system_ext/lib]]]

执行的结果也验证了,我们APK的类加载器是PathClassLoader,而我们系统的class使用的类加载器就是BootClassLoader。

接下来我们看一下类加载的源码分析:

PathClassLoader 与 DexClassLoader 的共同父类是 BaseDexClassLoader 。

java 复制代码
// dalvik/system/PathClassLoader.java

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 librarySearchPath 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 librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, 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 librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     * @param sharedLibraryLoaders class loaders of Java shared libraries
     * used by this new class loader. The shared library loaders are always
     * checked before the {@code dexPath} when looking
     * up classes and resources.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public PathClassLoader(
            @NonNull String dexPath, @Nullable String librarySearchPath, @Nullable ClassLoader parent,
            @Nullable ClassLoader[] sharedLibraryLoaders) {
        this(dexPath, librarySearchPath, parent, sharedLibraryLoaders, null);
    }

    /**
     * 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 librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     * @param sharedLibraryLoaders class loaders of Java shared libraries
     * used by this new class loader. The shared library loaders are always
     * checked before the {@code dexPath} when looking
     * up classes and resources.
     * @param sharedLibraryLoadersAfter class loaders of Java shared libraries
     * used by this new class loader. These shared library loaders are always
     * checked <b>after</b> the {@code dexPath} when looking
     * up classes and resources.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public PathClassLoader(
            @NonNull String dexPath, @Nullable String librarySearchPath,
            @Nullable ClassLoader parent, @Nullable ClassLoader[] sharedLibraryLoaders,
            @Nullable ClassLoader[] sharedLibraryLoadersAfter) {
        super(dexPath, librarySearchPath, parent, sharedLibraryLoaders, sharedLibraryLoadersAfter);
    }
}
java 复制代码
// dalvik/system/DexClassLoader.java

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 this parameter is deprecated and has no effect since API level 26.
     * @param librarySearchPath 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 librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

可以看到两者唯一的区别在于:创建 DexClassLoader 需要传递一个 optimizedDirectory 参数,并且会将其创建 为 File 对象传给 super ,而 PathClassLoader 则直接给到null。因此两者都可以加载指定的dex,以及jar、zip、apk中的classes.dex

java 复制代码
PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
 

File dexOutputDir = context.getCodeCacheDir();

DexClassLoader dexClassLoader = new 

DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());

PathClassLoader在创建的时候,传入了一个重要的参数(String dexPath)就是我们APK中包含的xxxx.dex文件的路径。那我们看一下对应的类加载器中loadClass()方法是如何实现加载class文件:

那我们到PathClassLoader的父类BaseDexClassLoader中查找看是否有loadClass()方法:

在BaseDexClassLoader中仍然没有loadClass方法,那么我们继续到父类ClassLoader中查找loadClass方法:

找到loadClass方法之后,我们分析一下他的实现:

java 复制代码
// java/lang/ClassLoader.java

protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
		// First, check if the class has already been loaded
        // 在缓存中查找要加载的class文件
		Class<?> c = findLoadedClass(name);

        // 如果之前没有加载过该class对象
		if (c == null) {
			try {
                // 使用双亲委托的方式,再次查找是否已经加载过对应的calss文件
				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.
                // 最后如果在缓存以及双亲中都没有找到已加载的class,就会调用自己的方法去加载该class
				c = findClass(name);
			}
		}
		return c;
}

某个类加载器在加载类时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务或者没有父类加载器时,才自己去加载:

  1. 缓存中查找要加载的class文件
  2. 双亲中查找要加载的class文件
  3. 加载class文件

其实前面2个一直都是防止重复加载在做的操作,也就是已经加载过得calss不会出现重复进行加载。其中有一个概念叫双亲委托查找,这个双亲委托听名字似乎很高大上,说穿了就是创建PathClassLoader类加载器时,通过构造方法传进来的一个成员:

双亲委托机制

可以看到创建 ClassLoader 需要接收一个 ClassLoader parent 参数。这个 parent 的目的就在于实现类加载的双 亲委托。即:某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

java 复制代码
// java/lang/ClassLoader.java

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;

// 省略部分代码....

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) {
			try {
				if (parent != null) {
					//如果parent不为null,则调用parent的loadClass进行加载
					c = parent.loadClass(name, false);
				} else {
					//parent为null,则调用BootClassLoader进行加载
					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.
				// 如果都找不到就自己查找
				c = findClass(name);
			}
		}
		return c;
}

// 省略部分代码......

因此我们自己创建的ClassLoader: new PathClassLoader("/sdcard/xx.dex", getClassLoader()); 并不仅仅 只能加载 xx.dex中的class。

值得注意的是:

java 复制代码
c = findBootstrapClassOrNull(name);

按照方法名理解,应该是当parent为null时候,也能够加载 BootClassLoader 加载的类。

new PathClassLoader("/sdcard/xx.dex", null) ,能否加载Activity.class? 但是实际上,Android当中的实现为:(Java不同)

java 复制代码
// java/lang/ClassLoader.java

/**
 * Returns a class loaded by the bootstrap class loader;
 * or return null if not found.
 */
private Class<?> findBootstrapClassOrNull(String name)
{
	return null;
}

接下来继续class加载的方法findClass:

findClass

可以看到在所有父ClassLoader无法加载Class时,则会调用自己的 findClass 方法。 findClass 在ClassLoader中的定义为:

java 复制代码
// java/lang/ClassLoader.java

/**
 * Finds the class with the specified <a href="#name">binary name</a>.
 * This method should be overridden by class loader implementations that
 * follow the delegation model for loading classes, and will be invoked by
 * the {@link #loadClass <tt>loadClass</tt>} method after checking the
 * parent class loader for the requested class.  The default implementation
 * throws a <tt>ClassNotFoundException</tt>.
 *
 * @param  name
 *         The <a href="#name">binary name</a> of the class
 *
 * @return  The resulting <tt>Class</tt> object
 *
 * @throws  ClassNotFoundException
 *          If the class could not be found
 *
 * @since  1.2
 */
protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}

其实任何ClassLoader子类,都可以重写 loadClass 与 findClass 。一般如果你不想使用双亲委托,则重写loadClass 修改其实现。而重写 findClass 则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义 自己如何去查找一个Class。而我们的 PathClassLoader 会自己负责加载 MainActivity 这样的程序中自己编写的 类,利用双亲委托父ClassLoader加载Framework中的 Activity 。说明 PathClassLoader 并没有重写loadClass ,因此我们可以来看看PathClassLoader中的 findClass 是如何实现的(PathClassLoader中并重写对应的findClass而是使用其父类BaseDexClassLoader中的findClass方法)。

java 复制代码
// dalvik/system/BaseDexClassLoader.java


/**
 * Hook for customizing how dex files loads are reported.
 *
 * This enables the framework to monitor the use of dex files. The
 * goal is to simplify the mechanism for optimizing foreign dex files and
 * enable further optimizations of secondary dex files.
 *
 * The reporting happens only when new instances of BaseDexClassLoader
 * are constructed and will be active only after this field is set with
 * {@link BaseDexClassLoader#setReporter}.
 */
/* @NonNull */ private static volatile Reporter reporter = null;

@UnsupportedAppUsage
private final DexPathList pathList;


public BaseDexClassLoader(String dexPath,
		String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
		ClassLoader[] sharedLibraryLoadersAfter,
		boolean isTrusted) {
	super(parent);

	this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
}

// 省略部分代码.....

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    // // 省略部分代码.....
	// Check whether the class in question is present in the dexPath that
	// this classloader operates on.
	List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
	Class c = pathList.findClass(name, suppressedExceptions);
	if (c != null) {
		return c;
	}
    // 省略部分代码.....
	return c;
}

实现非常简单,从 pathList 中查找class。pathList 又是DexPathList 类型,所以我们要看继续查看 DexPathList

java 复制代码
// dalvik/system/DexPathList.java


DexPathList(ClassLoader definingContext, String dexPath,
		String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
			// 省略部分代码......
			// save dexPath for BaseDexClassLoader
            // splitDexPath 实现为返回 List<File>.add(dexPath),例如dexPath为:A.dex:B.dex: C.dex .....splitDexPath会以":"为分隔符,分解dexPath字符串为数组列表List,并且将dex文件转换为File
            // makeDexElements 会去 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
			this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
											   suppressedExceptions, definingContext, isTrusted);
			// 省略部分代码......
		}


@UnsupportedAppUsage
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
	List<File> result = new ArrayList<>();

	if (searchPath != null) {
		for (String path : searchPath.split(File.pathSeparator)) {
			if (directoriesOnly) {
				try {
					StructStat sb = Libcore.os.stat(path);
					if (!S_ISDIR(sb.st_mode)) {
						continue;
					}
				} catch (ErrnoException ignored) {
					continue;
				}
			}
			result.add(new File(path));
		}
	}

	return result;
}


private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
		List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
  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) {
	  if (file.isDirectory()) {
		  // We support directories for looking up resources. Looking up resources in
		  // directories is useful for running libcore tests.
		  elements[elementsPos++] = new Element(file);
	  } else if (file.isFile()) {
		  String name = file.getName();

		  DexFile dex = null;
		  if (name.endsWith(DEX_SUFFIX)) {
			  // Raw dex file (not inside a zip/jar).
			  try {
				  dex = loadDexFile(file, optimizedDirectory, loader, elements);
				  if (dex != null) {
					  elements[elementsPos++] = new Element(dex, null);
				  }
			  } catch (IOException suppressed) {
				  System.logE("Unable to load dex file: " + file, suppressed);
				  suppressedExceptions.add(suppressed);
			  }
		  } else {
			  try {
				  dex = loadDexFile(file, optimizedDirectory, loader, elements);
			  } 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);
			  }

			  if (dex == null) {
				  elements[elementsPos++] = new Element(file);
			  } else {
				  elements[elementsPos++] = new Element(dex, file);
			  }
		  }
		  if (dex != null && isTrusted) {
			dex.setTrusted();
		  }
	  } else {
		  System.logW("ClassLoader referenced unknown path: " + file);
	  }
  }
  if (elementsPos != elements.length) {
	  elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}

以上为findClass的遍历参数dexElements 的实现,说穿了dexElements 是什么呢?就是我们APK中xxx.dex文件的一个集合,只不过是封装了Element而已。那接下来就是重点:

java 复制代码
// dalvik/system/DexPathList.java


/**
 * Finds the named class in one of the dex files pointed at by
 * this instance. This will find the one in the earliest listed
 * path element. If the class is found but has not yet been
 * defined, then this method will define it in the defining
 * context that this instance was constructed with.
 *
 * @param name of class to find
 * @param suppressed exceptions encountered whilst finding the class
 * @return the named class or {@code null} if the class is not
 * found in any of the dex files
 */
public Class<?> findClass(String name, List<Throwable> suppressed) {
	for (Element element : dexElements) {
		Class<?> clazz = element.findClass(name, definingContext, suppressed);
		if (clazz != null) {
			return clazz;
		}
	}

	if (dexElementsSuppressedExceptions != null) {
		suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
	}
	return null;
}


public Class<?> findClass(String name, ClassLoader definingContext,
		List<Throwable> suppressed) {
	return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
			: null;
}

DexPathList的findClass方法就是遍历的Dex文件中的所有class,找到对应的class文件后直接返回,这样就完成了从我们的APK中加载class文件到虚拟机中的过程。

那么我们总结一下:

到这里类是通过加载器加载完了。介绍到这里是不是自然而然的就能够了解通过类加载来实现代码修复功能,也就是我们常说的热修复。

3 热修复

类加载热修复原理:利用类加载器每个class只加载一次的(上面有分析)特性,那我们如果将有问题的类修复之后的class文件打包成XXX.dex文件,然后插入到刚才介绍的dexElements数组的前面的话,那么类加载器加载了我们已经修复了的class之后,就不会再加载有问题的class文件了,这样就做到了使用类加载机制实现代码的热修复:

那么来实现一下:

java 复制代码
public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //执行热修复。 插入补丁dex
        // /data/data/xxx/files/xxxx.dex
        // /sdcard/xxx.dex
        Hotfix.installPatch(this,new File("/sdcard/patch.dex"));
    }
}
java 复制代码
public class Hotfix {

    private static final String TAG = "Hotfix";

    public static void installPatch(Application application, File patch) {
        //1、获得classloader,PathClassLoader
        ClassLoader classLoader = application.getClassLoader();


        List<File> files = new ArrayList<>();
        if (patch.exists()) {
            files.add(patch);
        }
        File dexOptDir = application.getCacheDir();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            try {
                NewClassLoaderInjector.inject(application, classLoader, files);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        } else {
            try {
                //23 6.0及以上
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    V23.install(classLoader, files, dexOptDir);
                } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                    V19.install(classLoader, files, dexOptDir); //4.4以上
                } else {  // >= 14
                    V14.install(classLoader, files, dexOptDir);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static final class V23 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                IOException {
            //找到 pathList
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);

            ArrayList<IOException> suppressedExceptions = new ArrayList<>();
            // 从 pathList找到 makePathElements 方法并执行
            // 得到补丁创建的 Element[]
            Object[] patchElements = makePathElements(dexPathList,
                    new ArrayList<>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions);

            //将原本的 dexElements 与 makePathElements生成的数组合并
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", patchElements);
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

        /**
         * 把dex转化为Element数组
         */
        private static Object[] makePathElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            //通过阅读android6、7、8、9源码,都存在makePathElements方法
            Method makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements",
                    List.class, File.class,
                    List.class);
            return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

    private static final class V19 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
                IOException {
            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements",
                    makeDexElements(dexPathList,
                            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                            suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                    throw e;
                }
            }
        }

        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
            Method makeDexElements = ShareReflectUtil.findMethod(dexPathList, "makeDexElements",
                    ArrayList.class, File.class,
                    ArrayList.class);


            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

    /**
     * 14, 15, 16, 17, 18.
     */
    private static final class V14 {


        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
                throws IllegalArgumentException, IllegalAccessException,
                NoSuchFieldException, InvocationTargetException, NoSuchMethodException {

            Field pathListField = ShareReflectUtil.findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);

            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements",
                    makeDexElements(dexPathList,
                            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
        }

        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory)
                throws IllegalAccessException, InvocationTargetException,
                NoSuchMethodException {
            Method makeDexElements =
                    ShareReflectUtil.findMethod(dexPathList, "makeDexElements", ArrayList.class,
                            File.class);
            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
        }
    }

}
java 复制代码
public class ShareReflectUtil {


    /**
     * 从 instance 到其父类 找 name 属性
     *
     * @param instance
     * @param name
     * @return
     * @throws NoSuchFieldException
     */
    public static Field findField(Object instance, String name) throws NoSuchFieldException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                //查找当前类的 属性(不包括父类)
                Field field = clazz.getDeclaredField(name);

                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                return field;
            } catch (NoSuchFieldException e) {
                // ignore and search next
            }
        }
        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }

    /**
     * 从 instance 到其父类 找  name 方法
     *
     * @param instance
     * @param name
     * @return
     * @throws NoSuchFieldException
     */
    public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
            throws NoSuchMethodException {
        for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
            try {
                Method method = clazz.getDeclaredMethod(name, parameterTypes);

                if (!method.isAccessible()) {
                    method.setAccessible(true);
                }

                return method;
            } catch (NoSuchMethodException e) {
                // ignore and search next
            }
        }
        throw new NoSuchMethodException("Method "
                + name
                + " with parameters "
                + Arrays.asList(parameterTypes)
                + " not found in " + instance.getClass());
    }


    /**
     * @param instance
     * @param fieldName
     * @param patchElements 补丁的Element数组
     * @throws NoSuchFieldException
     * @throws IllegalArgumentException
     * @throws IllegalAccessException
     */
    public static void expandFieldArray(Object instance, String fieldName, Object[] patchElements)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        //拿到 classloader中的dexelements 数组
        Field dexElementsField = findField(instance, fieldName);
        //old Element[]
        Object[] dexElements = (Object[]) dexElementsField.get(instance);


        //合并后的数组
        Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
                dexElements.length + patchElements.length);

        // 先拷贝新数组
        System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
        System.arraycopy(dexElements, 0, newElements, patchElements.length, dexElements.length);

        //修改 classLoader中 pathList的 dexelements
        dexElementsField.set(instance, newElements);
    }


}

以上热修复代码文件请看资源绑定)那么我们如何将一个calss文件打包成dex文件呢?

相关推荐
张风捷特烈4 小时前
Flutter 伪3D绘制#03 | 轴测投影原理分析
android·flutter·canvas
omegayy7 小时前
Unity 2022.3.x部分Android设备播放视频黑屏问题
android·unity·视频播放·黑屏
mingqian_chu7 小时前
ubuntu中使用安卓模拟器
android·linux·ubuntu
自动花钱机7 小时前
Kotlin问题汇总
android·开发语言·kotlin
行墨10 小时前
Kotlin 主构造函数
android
前行的小黑炭10 小时前
Android从传统的XML转到Compose的变化:mutableStateOf、MutableStateFlow;有的使用by有的使用by remember
android·kotlin
_一条咸鱼_10 小时前
Android Compose 框架尺寸与密度深入剖析(五十五)
android
在狂风暴雨中奔跑10 小时前
使用AI开发Android界面
android·人工智能
行墨10 小时前
Kotlin 定义类与field关键
android
信徒_11 小时前
Mysql 在什么样的情况下会产生死锁?
android·数据库·mysql