Android 热修复核心原理

  • dexopt

    Dalvik中虚拟机在加载一个dex文件时,对 dex 文件 进行 验证 和 优化的操作,其对 dex 文件的优化结果变成了 odex(Optimized dex) 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码。

  • dex2oat

    ART 预先编译机制 ,在安装时对 dex 文件执行dexopt优化之后再将odex进行 AOT 提前编译操作,编译为OAT(实际上是ELF文件)可执行文件(机器码)。(相比做过ODEX优化,未做过优化的DEX转换成OAT要花费更长的时间)

ClassLoader介绍

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

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

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

  • BootClassLoader

    用于加载Android Framework层class文件。

  • PathClassLoader:系统用的

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

  • DexClassLoader:给开发者用的

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

    很多博客里说PathClassLoader只能加载已安装的apk的dex,其实这说的应该是在dalvik虚拟机上。

    但现在一般不用关心dalvik了。

    java 复制代码
    Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加载");
    Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加载");
    
    
    //输出:
    Activity.class 由:java.lang.BootClassLoader@d3052a9 加载
    
    MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加载

    它们之间的关系如下:

    PathClassLoaderDexClassLoader的共同父类是BaseDexClassLoader,不过二者都并没有复写findClass方法

java 复制代码
public class DexClassLoader extends BaseDexClassLoader {

//dexPath:dex文件以及包含dex的apk文件或jar文件的路径,多个路径用文件分隔符,默认分隔符,":"
//optimizedDirectory:dex文件进行优化后所生成的dex文件的路径,8.0之后已经被废弃,因此8.0之后DexClassLoader和PathClassLoader一模一样
//librarySearchPath:所使用到的c/c++库存放的路径
//parent:保留java中的classloader的委托机制
	
    public DexClassLoader(String dexPath, String optimizedDirectory,
		String librarySearchPath, ClassLoader parent) {
		super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
	}
}

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

	public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
		 super(dexPath, null, librarySearchPath, parent);
	}
}
librarySearchPath:加载ndk文件的目录,so的文件目录

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

odex文件的路径就是optimizedDirectory

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

File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());

​ 其实,optimizedDirectory参数就是dexopt的产出目录(odex)。那PathClassLoader创建时,这个目录为null,就意味着不进行dexopt?并不是,optimizedDirectory为null时的默认路径为:/data/dalvik-cache。optimizedDirectory必须是私有目录(即以data/data目录开头的),不能是sd的目录

在API 26源码中,将DexClassLoader的optimizedDirectory标记为了 deprecated 弃用,实现也变为了:

java 复制代码
public DexClassLoader(String dexPath, String optimizedDirectory,
					String librarySearchPath, ClassLoader parent) {
	super(dexPath, null, librarySearchPath, parent);
}

...和PathClassLoader一摸一样了!

findClass

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

java 复制代码
protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}

​ 其实任何ClassLoader子类,都可以重写loadClassfindClass。一般如果你不想使用双亲委托,则重写loadClass修改其实现。而重写findClass则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class。

而我们的PathClassLoader会自己负责加载MainActivity这样的程序中自己编写的类,利用双亲委托父ClassLoader加载Framework中的Activity。说明PathClassLoader并没有重写loadClass,因此我们可以来看看PathClassLoader中的 findClass 是如何实现的。

java 复制代码
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<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //查找指定的class
    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;
}

​ 实现非常简单,从pathList中查找class。继续查看DexPathList

java 复制代码
public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
	//.........
    // splitDexPath 实现为返回 List<File>.add(dexPath)
    // makeDexElements 会去 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
	//.........
    
}

public Class findClass(String name, List<Throwable> suppressed) {
     //从element中获得代表Dex的 DexFile
	for (Element element : dexElements) {
		DexFile dex = element.dexFile;
		if (dex != null) {
            //查找class
        	Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
            	return clazz;
        	}
    	}
    }
    if (dexElementsSuppressedExceptions != null) {
    	suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
	return null;
}

热修复

PathClassLoader中存在一个Element数组,Element类中存在一个dexFile成员表示dex文件,即:APK中有X个dex,则Element数组就有X个元素。

​ 在PathClassLoader中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class 位于patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得dexElements中的DexFile,查找到了Key.class 则立即返回,不会再管后续的element中的DexFile是否能加载到Key.class了。

​ 因此实际上,一种热修复实现可以将出现Bug的class单独的制作一份fix.dex文件(补丁包),然后在程序启动时,从服务器下载fix.dex保存到某个路径,再通过fix.dex的文件路径,用其创建Element对象,然后将这个Element对象插入到我们程序的类加载器PathClassLoaderpathList中的dexElements数组头部。这样在加载出现Bug的class时会优先加载fix.dex中的修复类,从而解决Bug。

注意:热修复的类必须是没有被加载过的。如果类已经被加载了,那热修复就没有什么意义的了,时间已经迟了。

热修复的方式不止这一种,并且如果要完整实现此种热修复可能还需要注意一些其他的问题(如:反射兼容)。

双亲委托机制

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

  • 加载.class文件时,以递归的形式逐级向上委托给父加载器parentClassLoader去加载,如果加载过了,就不用再加载一遍。
  • 如果父加载器也没有加载过,则继续委托给父加载器去加载,一直到这条链路的顶级,顶级classloader判断如果没加载过,则尝试加载,加载失败,则逐级向下交还调用者来加载。说到底,双亲委派的核心机制就是递归。
go 复制代码
public abstract class ClassLoader {
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), 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) {
                        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.
                    c = findClass(name);
                }
            }
            return c;
    }

}
  • 双亲委派的作用

    • 防止同一个class文件重复加载
    • 对于任意一个类确保在虚拟机中的唯一性。由加载它的类加载器和这个类的全类名一同确立其在jvm中的唯一性
    • 保证系统类的class文件不被篡改

Class文件加载

类的加载是指将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区的数据结构,并且提供了访问方法区内的数据结构的方法。

  • 通过Class.forName方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

类加载分为三个步骤:1.装载(Load) 2.链接(Link) 3.初始化(Init)

  • 1.装载(load)查找并加载类的二进制数据(查找并导入Class文件)

    • 通过一个类的全限定名来获取其定义的二进制字节流
    • 将这个字节流转化为方法区的运行时数据结构
    • 在Java堆中生成一个代表这个类的Class对象,作为对方法区中这些数据的访问入口
  • 2.链接

    • 验证:确保被加载的类的正确性

      • 文件格式验证:验证字节流是否符合class文件格式的规范;例如:主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
      • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求;例如,这个类是否有父类,除了Object之外。
      • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的。
    • 准备:为类的静态变量分配内存,并将其初始化为默认值

      • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在java堆中。这也是为什么不能在静态方法中访问非静态变量的原因。
      • 这里所设置的初始值通常情况下是数据类型默认的零值(如0,0L,null,false等),而不是被在java代码中显式赋予的值。
    • 解析:把类中的符号引用转换为直接引用

      • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类、接口、字段、类方法、接口方法、方法类型。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的内存地址指针。
  • 3.初始化:执行类的方法,对类的静态变量,静态代码块执行初始化操作(不是必须的),只有下面这几种方式才会触发类的初始化。

    • 创建类的实例,也就是new一个对象
    • 访问某个类或接口的静态变量,或者对该静态变量赋值
    • 调用类的静态方法
    • 反射Class.forName("xxx.xxx")
    • 初始化一个类的子类(会首先初始化子类的父类)
    • jvm启动时标明的启动类,即文件名和类名相同的那个类

class.forname和classloader.loadclass 有何不同

Class.forName()加载的类会被初始化,类中的静态成员变量会被初始化,静态代码块会被执行通过ClassLoader.loadClass加载的类不进行解析操作,不进行解析操作就意味着初始化也不会进行,那么其类的静态参数就不会初始化,静态代码块也不会被执行。

相关推荐
大白要努力!16 分钟前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟1 小时前
Android音频采集
android·音视频
小白也想学C2 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程2 小时前
初级数据结构——树
android·java·数据结构
闲暇部落5 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX7 小时前
Android 分区相关介绍
android
大白要努力!8 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee8 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood8 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-11 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记