Android热修复简单实现及双亲委派机制

本文深入探讨了Android系统中的类加载器,包括PathClassLoader和DexClassLoader的差异,以及双亲委派机制。同时,文章介绍了Android热修复的实现,通过DexClassLoader动态加载修复bug的dex包,实现类的替换,以此达到修复目的。整个过程涉及反射和类加载的细节,展示了插件化的一种实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文简单介绍 热修复的一种实现,插件化原理也可以参考。

Android系统中的类加载器
Android系统屏蔽了ClassLoader的findClass加载方法,那么它自己的类加载时通过什么样的方式实现的呢?

Android系统中有两个类加载器分别为PathClassLoader和DexclassLoader。

PathClassLoader和DexClassLoader都是继承与BaseDexClassLoader,BaseDexClassLoader继承与ClassLoader。

Android为什么会将自己的类加载器派生出两个不同的子类,它们各自有什么用?
PathClassLoader和DexClassLoader比较

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

public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}

BaseDexClassLoader的构造函数

/**
 * 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);
}

optimizedDirectory:制定输出dex优化后的odex文件,可以为null
libraryPath:动态库路径(将被添加到app动态库搜索路径列表中)
parent:制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。

可以看出 PathClassLoader和DexClassLoader的区别就在于构造函数中optimizedDirectory、libraryPath这两个参数。PathClassLoader中optimizedDirectory、libraryPath为null,DexClassLoader中为new File(optimizedDirectory)、libraryPath。

optimizedDirectory
BaseDexClassLoader的构造函数利用optimizedDirectory创建了一个DexPathList,看看DexPathList中optimizedDirectory:

public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
/部分代码省略/
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
/部分代码省略/
}
private static Element[] makeDexElements(ArrayList files, File optimizedDirectory,
ArrayList suppressedExceptions) {
/部分代码省略/
for (File file : files) {
/部分代码省略/
if (file.isDirectory()) {
/部分代码省略/
} 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) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}

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);
}
}

由以上可以得出以下结论
DexClassLoader:能够加载自定义的jar/apk/dex
PathClassLoader:只能加载系统中已经安装过的apk
所以Android系统默认的类加载器为PathClassLoader,而DexClassLoader可以像JVM的ClassLoader一样提供动态加载。

Android系统中类加载的双亲委派机制
Android5.1源码中ClassLoader的loadClass方法

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;
        }
        //当所有父类节点的类加载器都没有找到该类时,当前加载器调用findClass方法加载。
        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }

想要动态加载类,可以用 自定义ClassLoader和双亲委派机制 中自定义ClassLoader的方法加载自己定义的class文件么?看看Android源码中的ClassLoader的findClass方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
throw new ClassNotFoundException(className);
}
这个方法直接抛出了“ClassNotFoundException”异常,所以在Android中想通过这种方式实现类的加载时不行的。

总结

ClassLoader的loadClass方法保证了双亲委派机。
BaseDexClassLoader提供了两种派生类使我们可以加载自定义类。

----------------------基于以上介绍下面进入热修复的内容-------------------------------

修复流程

两张图 第一张是修复流程,第二张思路都在图片中写的比较详细了,不再啰嗦,下面程序实现以上功能。

第一步先把修复bug的包 classes2.dex 拷贝到程序的私有目录下
/**
* 修复原理
* 使用DexClassLoader:能够加载未安装的jar/apk/dex
* DexClassLoader ->DexPathlist->dexElements[]
* 将修复bug的dex包与dexElement[] 合并成一个新的dex,并把fix.dex 放到 数组的前面
* 通过反射将 原来的dexElements[]用 合并后的的dexElements[] 替换掉,这样就可不会加载旧的带有bug的class文件。
* @param view
*/
public void fixBug(View view) {
//修复bug
//1.修复包 classes2.dex
String dexName = “classes2.dex”;
File sourceFile = new File(Environment.getExternalStorageDirectory(),dexName);

    //DexClassLoader指定的应用程序目录
    File targetFile = new File(getDir("odex", Context.MODE_PRIVATE).getAbsolutePath()+File.separator+dexName);
    if(targetFile.exists()){
        targetFile.delete();
    }
    //拷贝到私有路径
    FileUtil.copyFile(sourceFile,targetFile);

    FixDexUtil.loadFixedDex(this);

}

第二步就是进行合并dex数组,将我们的修复后的包classes2.dex,与当前app的 dexElement 进行合并,生成一个新的DexElement。
以下主要基于反射来实现替换出bug的class 。

获取 DexPathList 中的 DexElement ,然后将合并后的 NewDexElement 替换掉原来的。这样就达到了修复的目的,这也是插件化的一种实现原理。

 //dex文件需要被写入的目录
    String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
    File fileOpt = new File(optimizeDir);
    if(!fileOpt.exists()){
        fileOpt.mkdirs();
    }
    //1.获得加载应用程序dex的PathClassLoader
    PathClassLoader systemClassLoader = (PathClassLoader) context.getClassLoader();
    for (File dexFile : loadedDex) {
        //DexClassLoader构造参数
        //String dexPath,dex路径
        // String optimizedDirectory, 制定输出dex优化后的odex文件,可以为null
        // String librarySearchPath, so路径 动态库路径(将被添加到app动态库搜索路径列表中)
        // ClassLoader parent 制定父类加载器,以保证双亲委派机制从而实现每个类只加载一次。

        DexClassLoader dexClassLoader = new DexClassLoader(
                dexFile.getAbsolutePath(),
                optimizeDir,
                null,
                systemClassLoader
                );

        //3.合并dex
        try {
            Object dexObj = ReflectUtil.getPathList(dexClassLoader);
            Object pathObj = ReflectUtil.getPathList(systemClassLoader);
            Object fixDexElements = ReflectUtil.getDexElements(dexObj);
            Object pathDexElements = ReflectUtil.getDexElements(pathObj);
            //合并两个数组
            Object newDexElements = ReflectUtil.combineArray(fixDexElements,pathDexElements);
            LogUtil.log("combineArray>>>");
            //重新赋值给PathClassLoader 中的exElements数组

            Object pathList = ReflectUtil.getPathList(systemClassLoader);

            ReflectUtil.setField(pathList,pathList.getClass(),"dexElements",newDexElements);
            LogUtil.log("修复完成>>>");
            LogUtil.log("修复完成>>>"+pathList+"   ");
        } catch (Exception e) {
            e.printStackTrace();
            LogUtil.log("doDexInject Exception>>>"+e.toString());
        }

    }

什么是双亲委托机制
要了解双亲委托机制要先了解JAVA的类加载器。

类加载器
​ JVM设计者把类加载阶段中的“通过’类全名’来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。即:类加载器是jre的一部分,负责动态将类添加到Java虚拟机。

​ 简单的说:类加载就是把我们写好并编译成的 class字节码文件从硬盘中加载到内存,而类加载器就是JVM用来完成类加载的工具。

类加载器的类别
BootstrapClassLoader(启动类加载器)
c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

ExtClassLoader (标准扩展类加载器)
Bootstrploader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrploader,

ExtClassLoader是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
java编写,加载扩展库,如classpath中的jre ,javax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)
Bootstrploader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。
java编写,加载程序所在的目录,如user.dir所在的位置的class

CustomClassLoader(用户自定义类加载器)
java编写,用户自定义的类加载器,可加载指定路径的class文件

源码分析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
long t1 = System.nanoTime();
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass©;
}
return c;
}
}

双亲委派机制
​ 双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有在父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。

双亲委派模型工作工程:
1.当Application ClassLoader收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器Extension ClassLoader去完成。

2.当Extension ClassLoader收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器Bootstrap ClassLoader去完成。

3.如果Bootstrap ClassLoader加载失败(在<JAVA_HOME>\lib\rt.jar中未找到所需类),就会让Extension ClassLoader尝试加载。

4.如果Extension ClassLoader也加载失败(在<JAVA_HOME>\lib\ext\*.jar中未找到所需类),就会使用Application ClassLoader加载。

5.如果Application ClassLoader也加载失败(在$CLASSPATH中未找到所需类,你说配置的classpath),就会使用自定义加载器去尝试加载。

6.如果均加载失败,就会抛出ClassNotFoundException异常。

双亲委派机制的作用
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

总结
“双亲委派”机制只是Java推荐的机制,并不是强制的机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值