热修复意思就是说不必重新发布新版本来修改线上的错误Bug, 可以通过服务器下发补丁apk到客户端,然后重新排DexElements数组,加载新的补丁类,通过类加载机制来避免再次加载已有的错误代码,实现Bug的热修复。这块就涉及到好些个概念,类加载机制,双亲委派机制,PathClassLoader, DexClassLoader, DexElements等,有了这些概念之后就可以建立起热修复的技术框架。
类加载机制就是利用ClassLoader将Java的字节码文件,即.class文件从磁盘上加载到Java虚拟机内存中,然后进行一系列的加载,验证,准备,解析,初始化操作,然后将一个类的信息保存在Java虚拟机的方法区中。但是还有一些细节需要知道,这个ClassLoader具体在Java中是一个什么样的存在?
日常我们在开发中,写一个类,一般是通过Java系统的AppClassLoader来进行加载的,AppClassLoader是一个内部类,名字就叫应用程序类加载器,定义在JAVA_HOME的lib目录下rt.jar中的sun.misc.Launcher类中,负责加载的是程序员自己写的类的加载,其实还有其他三种类加载器,分别是BootstrapClassLoader, ExtClassLoader, CustomClassLoader(自定义类加载器)。BootstrapClassLoader是Java虚拟机自己实现的一个类加载器,用于加载启动类的加载,使用C++来实现的,它会加载系统的类,加载由系统变量"sun.boot.class.path"所指定的路径下的jar包,除了启动类加载器之外,其他的类加载器都是用Java来实现的,因此我们不能用在程序中。ExtClassLoader是扩展类加载器,用于加载一些JDK提供的扩展类,,它也定义在rt.jar中的sun.misc.Launcher类中,这个类加载器可以用在我们自己的程序中。自定义类加载器是我们自己继承抽象类ClassLoader, 实现loadClass方法,在该方法的最后通过defineClass方法一个类类型的对象来实现自定义类加载器。 BootstrapClassLoader加载类的查找范围 load JRE\lib\rt.jar 或者 -Xbootclasspath选项指定的jar包 ExtClassLoader加载类的查找范围 load JRE\lib\ext*.jar 或者Djava.ext.dirs指定目录下的jar包 AppClassLoader加载类的查找范围 load ClASSPATH 或 -Djava.class.path指定的目录下的类和jar包 CustomClassLoader 加载程序员自己写的经过编译后的class文件
类的整个完成生命周期包括加载,链接,初始化,使用,卸载四大部分,链接部分又可以细化为验证,准备,解析三个小部分。
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
-
验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
-
准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
引用类型的默认值为null。
常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
-
解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。
符号引用就是一组符号来描述目标,可以是任何字面量
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
在类的生命周期执行完加载和连接之后就开始了类的初始化。
在类的初始化阶段,java虚拟机执行类的初始化语句,为类的静态变量赋值。
java类中对类变量指定初始值有两种方式:
1、声明类变量时指定初始值;
2、使用静态初始化块为类变量指定初始值。
public class Test {
public static int a = 10;
public static int b;
static{
b = 2;
}
}
1、假如这个类还没有被加载和连接,则程序先加载并连接该类
2、假如该类的直接父类还没有被初始化,则先初始化其直接父类
3、假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机
1. 创建类的实例。 Test test = new Test();
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
int b = Test.a;
Test.a = b;
3. 调用类的静态方法 Test.doSomething();
4. 反射
Class.forName("com.luckyboy.androidlearn");
5. 初始化一个类的子类
class Parent {
class Child extends Parent {
public static int a = 3;
}
}
Child.a = 4;
6. Java虚拟机启动时被标明为启动类的类
java -Xbootclasspath/a:/usrhome/thirdlib.jar: -jar yourJarExe.jar
上面的操作是将第三方的lib添加到启动类中
在如下几种情况下,Java虚拟机将结束生命周期
-
- 执行了System.exit()方法
-
- 程序正常结束
-
- 程序在执行的过程中遇到了异常或者错误而异常终止
-
- 由于操作系统出现错误而导致Java虚拟机进程终止。
上面讲了些关于类加载的知识,但是有一个问题,如果我们自己写了一个String类,那么就会与系统自带String冲突了,那么系统是怎么处理的呢?这里就是涉及到了双亲委派机制。
双亲委派机制就是说加载一个类,一般向引入该类的加载器的父类加载器查找,如果父类加载器还有父类加载器,那么就会继续向上传递,直到查找到为止。如果没有找到,那么就由当前的类加载器进行加载。这么说来,类加载器是有一个优先级的,自定义的类加载器优先级最低,启动类加载器优先级最高,一般是从自定义类加载器或者应用程序类加载器开始,然后向上查找,扩展类加载器,启动类加载器,先向上传递,如果能够在启动类加载器中找到,那么就加载启动类加载器所能加载的类中去加载,那么最开始的那个类加载器所加载的那个类,就不会被加载。
比如我们自己写了一个HelloWorld类,在加载这个类的时候,不管我们使用的是自定义的类加载器还是系统默认的AppClassLoader,它们都会首先将加载任务向上传递,先让上一层的类加载器尝试加载,一直传递到启动类加载器,启动类加载器会在系统变量"sun.boot.class.path"所指定的路径下查找是否有相应的类,如果有就会加载,如果没有就让下一层的类加载器加载,以此往下,直到最后一个类加载器都无法加载,那么就会抛出ClassNotFoundException,而如果我们自己写了一个和JDK提供的包名和类名一样的类时,那么在启动类加载器和扩展类加载器中,就会先把JDK提供的类加载器加载进来,进而保证了用户定义的类不会影响到JDK提供的类。
在Android的ClassLoader相关类中有BaseDexClassLoader, PathClassLoader, DexClassLoader这三种ClassLoader, 启动BaseDexClassLoader是PathClassLoader和DexClassLoader的父类,PathClassLoader是已有的程序的类加载器,DexClassLoader是可以加载外部的apk文件或者jar文件的类加载器。 BaseDexClassLoader
/**
* 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 + "]";
}
}
/**
* 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);
}
}
/**
* 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);
}
}
从上面的源码可以看到,DexClassLoader能够加载不是作为程序的一部分的apk文件或者jar文件。
好了,有了上面的铺垫,我们知道DexClassLoader能够加载外部的Class, 那么自然就可以加载我们修复Bug的类,那么自然会想到是否能够替换掉原有的Bug, 如果可以,那么就可以不用重新安装APK就可以修复Bug, 这是一个设想,先放在这,我们先研究BaseDexClassLoader, 毕竟PathClassLoader和DexClassLoader没有什么代码,主要的实现都在BaseDexClassLoader中。
// BaseDexClassLoader.java
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;
}
从上面的代码可以看到,如果要查找一个类,就必须通过pathList的findClass方法来查找,那么如果我们将从外部加载的修复Class添加到DexPathList中,是否可以就实现热修复呢?pathList的类型是DexPathList, 从名字上来看是和Dex相关,我们也清楚Android最后使用的就是dex文件来加载到虚拟机中,所以DexPathList和findClass方法是关键。
// 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) {
DexFile dex = element.dexFile;
if (dex != null) {
// 从上面的逻辑来看definingContext可能是PathClassLoader也可能是DexClassLoader
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
private final Element[] dexElements;
从上面的dex.loadClassBinaryName方法可以看出,dexElements就是一个包含Dex文件的元素数组,数组元素Element有个属性dexFile可以用来查找name对应的class,
static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile;
private boolean initialized;
...
}
综上,PathClassLoader和DexClassLoader都有dexElements属性,PathClassLoader是应用程序的类加载器,如果我们能够将外部的dex文件合并到PathClassLoader的dexElements数组中,那么就可以利用系统的类加载方式来修改错误代码类。但是需要考虑的是,需要将修复Dex的Elements添加在PathClassLoader的dexElements的数组前面,这样就可以利用系统的缓存机制来避免加载错误的代码类,因为类缓存机制意思即一旦某个类被加载过,就会被缓存起来,下次再使用的时候,就只会使用已加载过的类,错误的类就没有机会被加载了。
有了上面的知识铺垫,我们可以实现热修复,代码如下
public class FixDexUtil {
private static HashSet<File> loadedDex = new HashSet<>();
static {
loadedDex.clear();
}
public static void loadFixedDex(Context context, String dirName) {
if (context == null) {
return;
}
// 1. 找到所有的修复dex文件 并添加到集合中
File dir = context.getDir(dirName, Context.MODE_PRIVATE);
File[] listFiles = dir.listFiles();
for (File file : listFiles) {
if (file.getName().endsWith(".dex")) {
loadedDex.add(file);
}
}
// 2. 和之前apk里面的dex合并
doDexInject(context, dir, loadedDex);
}
private static void doDexInject(Context context, File dir, HashSet<File> loadedDex) {
// optimizeDir 保存修复后的dex目录
String optimizeDir = dir.getAbsolutePath() + File.separator + "opt_dex";
File fopt = new File(optimizeDir);
if (!fopt.exists()) {
fopt.mkdirs();
}
// 1. 加载应用程序的dex
// 1) 拿到系统的dex 什么是系统的dex 这里说的是被修复前包里面所有的dex文件
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
// 2) 拿到自己的dex -- 自己的dex 就是修复的dex
// String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent
try {
for (File dex : loadedDex) {
DexClassLoader dexClassLoader = new DexClassLoader(
dex.getAbsolutePath(), // 修复dex文件路径
fopt.getAbsolutePath(), // 优化后的dex存储路径
null, //JNI搜索地址 一般为null
pathClassLoader // 父类加载器
);
// 合并
// BaseDexClassLoader ----> DexPathList(变量pathList) ---> Element[] dexElements
// 把 dexElements进行修改
Object pathObj = getPathList(pathClassLoader); // pathClassLoader 对应的 DexPathList
Object dexObj = getPathList(dexClassLoader); // DexClassLoader 对应的 DexPathList
Object pathElements = getDexElements(pathObj);
Object dexElements = getDexElements(dexObj);
// 合并
Object dexElement = combineArrayNew(dexElements, pathElements);
// DexPathList 中的 dexElements的没有修改
// 需要重写赋值给 dexElements
Object pathList = getPathList(pathClassLoader);
// 修改PathClassLoader中的PathList属性的dexElements数组
setField(pathList, pathList.getClass(), "dexElements", dexElement);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static Object getDexElements(Object dexObj) throws NoSuchFieldException, IllegalAccessException {
return getField(dexObj, dexObj.getClass(), "dexElements");
}
// BaseDexClassLoader是 PathClassLoader 和 DexClassLoader的父类 而 BaseDexClassLoader 中有属性
// DexPathList pathList
private static Object getPathList(Object baseDexClassLoader) throws Exception {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getField(Object obj, Class<?> c1, String field)
throws NoSuchFieldException, IllegalAccessException {
// 获取到baseDexClassLoader里面的名字叫field的成员
Field localField = c1.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
private static void setField(Object obj, Class<?> c1, String field, Object value)
throws NoSuchFieldException, IllegalAccessException {
// 获取到baseDexClassLoader里面的名字叫field的成员
Field localField = c1.getDeclaredField(field);
localField.setAccessible(true);
localField.set(obj, value);
}
private static Object combineArray(Object arrayLhs, Object arrayRhs) {
// 获取到数组的字节码对象
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = i + Array.getLength(arrayRhs);
Object result = Array.newInstance(localClass, j);
for (int k = 0; k < j; ++k) {
if (k < i) {
Array.set(result, k, Array.get(arrayLhs, k));
} else {
Array.set(result, k, Array.get(arrayRhs, k - i));
}
}
return result;
}
private static Object combineArrayNew(Object arrayLhs, Object arrayRhs) {
// 获取到数组的字节码对象
Class<?> localClass = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs); // 补丁数组
int j = Array.getLength(arrayRhs); // 原dex数组长度
int k = i + j;
Object result = Array.newInstance(localClass, k);
System.arraycopy(arrayLhs,0 , result, 0, i);
System.arraycopy(arrayRhs,0 , result, i, j);
return result;
}
}
在Application或Activity中加载外部存有dex文件的目录
// 从assets中加载dex文件到 data/data/packagename/plugin目录下
// AssetManagerUtils.getInstance().loadAssetsToFiles(this,
// "yihui-exports-100.dex", "yihui-exports-100.dex","odex")
FixDexUtil.loadFixedDex(this, "plugin")
class文件或者jar文件转化成dex文件
dx --dex --no-strict --output=输出的dex文件路径 class文件所在的全路径
zfz:crash zhangfengzhou$ dx --dex --no-strict --output=./hello.dex CrashMaker.class
//crash目录是在build/tmp/kotlin-classes/debug/com/zhiyunyi/yihui/crash
// dx --dex --no-strict --output=D:\dex\out.dex D:\dex
// dx --dex --no-strict --output=./out.dex yihui-exports-100.jar