一.热修复的产生概述

在开发中我们会遇到如下的情况:

1.刚发布的版本出现了严重的bug,这就需要去解决bug、测试并打渠道包在各个应用市场上重新发布,这会耗费大量的人力物力,代价会比较大。

2.已经改正了此前发布版本的bug,如果下一个版本是一个大版本,那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复bug,这样此前版本的bug会长期的影响用户。

3.版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的bug就会一直影响不升级版本的用户。

4.有一个小而重要的功能,需要短时间内完成版本覆盖,比如节日活动。

为了解决上面的问题,热修复框架就产生了。对于Bug的处理,开发人员不要过于依赖热修复框架,在开发的过程中还是要按照标准的流程做好自测、配合测试人员完成测试流程。

二、什么是热修复?

定义

热修复说白了就是”打补丁”,比如你们公司上线一个app,用户反应有重大bug,需要紧急修复。如果按照通 常做法,那就是程序猿加班搞定bug,然后测试,重新打包并发布。这样带来的问题就是成本高,效率低。于是,热 修复就应运而生.一般通过事先设定的接口从网上下载无Bug的代码来替换有Bug的代码。这样就省事多了,用 户体验也好。

用户不用重新下载一个新的apk安装,而是直接下载一个补丁包,通过补丁来替换一些出现bug的类, 当然下载补丁的过程用户一般是感觉不到的,表面上看是直接修复了bug。

在早期的android系统中,为了优化dex,所有的method会存放在一张表里面,表的大小位short,也就是65535(65K)现在android代码非常多,超过65K很正常,这个时候就需要一种解决方案来解决这个问题。简单来说就是将编译好的class文件分拆成2个dex文件,绕过65k的限制。

Android 如何实现热修复 安卓热修复_加载

Android 如何实现热修复 安卓热修复_android 热修复_02

三. 部分热修复框架

Android 如何实现热修复 安卓热修复_Android 如何实现热修复_03

虽然热修复框架很多,但热修复框架的核心技术主要有三类,分别是代码修复、资源修复和动态链接库修复。

四. 热修复的原理

1.Android的类加载机制

Android 如何实现热修复 安卓热修复_Android 如何实现热修复_04

java中的ClassLoader是加载class文件,而Android中的虚拟机无论是dvm还是art都只能识别dex文件。因此Java中的ClassLoader在Android中不适用。Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。

BootClassLoader

BootClassLoader实例在Android系统启动的时候被创建,用于加载一些Android系统框架的类,其中就包括APP用到的一些系统类。BootClassLoader是ClassLoader的一个内部类。与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。

PathClassLoader:

用来加载系统类和应用类。

继承自BaseDexClassLoader。

在应用启动的时候创建PathClassLoader实例,只能加载系统中已经安装过的apk;

官方文档对PathClassLoader描述,大概意思是:
PathClassLoader简单实现了ClassLoader,可以操作本地文件系统的文件和目录,但是不能从网络中加载类。Android使用PathClassLoader作为系统类加载器和应用程序类加载器。

DexClassLoader:

用来加载jar、apk、dex文件,加载jar、apk也是最终抽取里面的Dex文件进行加载,可以从SD卡中加载未安装的apk。

PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中。

上图展示了Android中的ClassLoader中的继承体系,其中SecureClassLoader和UrlClassLoader是在Java中的类加载器,在Android中是没法办使用的。

2.热修复机制

在Android中我们主要关心的是PathClassLoader和DexClassLoader。

PathClassLoader用来操作本地文件系统中的文件和目录的集合。并不会加载来源于网络中的类。Android采用这个类加载器一般是用于加载系统类和它自己的应用类。这个应用类放置在data/data/包名下。

看一下PathClassLoader的源码,只有2个构造方法:

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源码

DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。DexClassLoader需要一个对应用私有且可读写的文件夹来缓存优化后的class文件。而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击。看一下它的源码,只有1个构造方法:

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

两个ClassLoader就两三行代码,只是调用了父类的构造函数.

可以看到,PathClassLoader和DexClassLoader除了构造方法传参不同,其它的逻辑都是一样的。要注意的是DexClassLoader构造方法第2个参数指的是dex优化缓存路径,这个值是不能为空的。而PathClassLoader对应的dex优化缓存路径为null是因为Android系统自己决定了缓存路径。

接下来我们看一下BaseDexClassLoader这个类:

public class BaseDexClassLoader extends ClassLoader {
    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;
    }

BaseDexClassLoader的构造方法有四个参数:

dexPath,指的是在Androdi包含类和资源的jar/apk类型的文件集合,指的是包含dex文件。多个文件用“:”分隔开,用代码就是File.pathSeparator。
optimizedDirectory,指的是odex优化文件存放的路径,可以为null,那么就采用默认的系统路径。
libraryPath,指的是native库文件存放目录,也是以“:”分隔。
parent,parent类加载器

在BaseDexClassLoader 构造函数中创建一个DexPathList类的实例,这个DexPathList的构造函数会创建一个dexElements 数组也来存放dex文件。

public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
        ... 
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //创建一个数组
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
        ... 
    }

然后BaseDexClassLoader 重写了findClass方法,调用了pathList.findClass,跳到DexPathList类中.

/* package */final class DexPathList {
    ...
    public Class findClass(String name, List<Throwable> suppressed) {
            //遍历该数组
        for (Element element : dexElements) {
            //初始化DexFile
            DexFile dex = element.dexFile;
 
            if (dex != null) {
                //调用DexFile类的loadClassBinaryName方法返回Class实例
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }       
        return null;
    }
    ...
}

pathList.findClass()会遍历这个数组,然后初始化DexFile,如果DexFile不为空那么调用DexFile类的loadClassBinaryName方法返回Class实例.

归纳上面的话就是:

ClassLoader会遍历这个数组,然后加载这个数组中的dex文件. 而ClassLoader在加载到正确的类之后,就不会再去加载有Bug的那个类了,我们把这个正确的类放在Dex文件中,让这个Dex文件排在dexElements数组前面即可.

热修复就是利用android中的 DexClassLoader 类加载器,动态加载补丁dex,替换有bug的类。

加载类的过程

跟Java类加载器加载类类似,都是通过ClassLoader用loadClass方法来加载我们需要的类,但是它们两的代码实现不太一样。

下面是Android中ClassLoader的loadClass方法代码:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

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

从loadClass可以看出,与Java类似,都是使用双亲委托机制来加载类。

加载过程:

1.会先查找当前ClassLoader是否加载过此类,有就返回;

2.如果没有,查询父ClassLoader是否已经加载过此类,如果已经加载过,就直接返回Parent加载的类;

3.如果整个类加载器体系上的ClassLoader都没有加载过,才由当前ClassLoader加载,整个过程类似循环链表一样。

双亲委托机制的作用

1.共享功能,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。

2.隔离功能,保证java/Android核心类库的纯净和安全,防止恶意加载。

使用ClassLoader一些需要注意的问题

我们都知道,我们可以通过动态加载获得新的类,从而升级一些代码逻辑,这里有几个问题要注意一下。

如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类。

如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载。

不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。

总结

Android中的类加载器是BootClassLoader、PathClassLoader、DexClassLoader,其中BootClassLoader是虚拟机加载系统类需要用到的,PathClassLoader是App加载自身dex文件中的类用到的,DexClassLoader可以加载直接或间接包含dex文件的文件,如APK等。

无论是热修复还是插件化技术中都利用了类加载机制,所以深入理解Android中的类加载机制对于理解这些技术的原理很有帮助。

Android热修复(2):AndFix热修复框架的使用

Android热修复(3):Tinker的使用