Android性能优化(6):浅析类加载机制与热修复技术

1. 类加载与反射

1.1 类加载机制

 类的加载是指将类的.class文件读入到内存中,并为之创建一个java.lang.Class对象,该过程由类加载器(ClassLoader)完成。类的加载过程包含类的加载类的连接类的初始化三个过程,当程序主动使用某个类时,如果该类还未被加载到内存中,系统就会通过这三个步骤来对该类进行初始化。因此,类的加载过程又可称为类的初始化过程,整个过程大致如下图所示:
在这里插入图片描述

1.1.1 类加载器

 类加载器负责将类的.class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。类加载器负责加载所有的类,一旦一个类被载入JVM中,同一个类就不会再次载入。类加载器通常无法等到首次使用该类时才加载类,Java虚拟机规范允许系统预先加载某些类,当然在Android中也会支持预先加载。由于Android虚拟机并没有遵循Java虚拟机规范,因此它们的类加载器并不相同,接下来我们具体分析。

1.1.1.1 Java中的ClassLoader

 Java中的类加载器主要由两种类型,即系统类加载器和自定义类加载器,其中,系统类加载器由Bootstap ClassLoader(引导类加载器)Extension ClassLoader(扩展类加载器)Application ClassLoader(应用程序类加载器)组成,而自定义类加载器为通过继承java.lang.ClassLoader类的方式实现。它们之间的"继承"关系为:
在这里插入图片描述
 需要注意的是,上述的“继承”关系并非我们理解的父类与子类继承关系,这里的“继承”是指类加载器的层级。因为引导类加载器是由C/C++编写,是JVM自带的类加载器,负责Java平台核心库,而扩展类加载器也没有继承引导类加载器,而是继承于java.lang.ClassLoader类,同理系统类加载器也不是继承扩展类加载器,也是继承于java.lang.ClassLoader类。接下来,我们通过一段代码来了解下一个Java程序需要用到几种类加载器。

public class MyClass {
    public static void main(String[] args) {
        // 获取当前类的类加载器
        ClassLoader classLoader = MyClass.class.getClassLoader();
        System.out.println(classLoader);
        // 遍历当前类的父加载器
        while (classLoader != null) {
            classLoader = classLoader.getParent();
            System.out.println(classLoader);
        }
        System.out.println("===========================");
        // 获取系统类加载器加载的路径
        System.out.println(System.getProperty("java.class.path"));
    }
}

打印结果如下:

sun.misc.Launcher A p p C l a s s L o a d e r @ 18 b 4 a a c 2 s u n . m i s c . L a u n c h e r AppClassLoader@18b4aac2 sun.misc.Launcher ExtClassLoader@42a57993
null
===========================
E:\Environment\java\jdk1.8\jre\jre\lib\charsets.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\access-bridge-64.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\cldrdata.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\dnsns.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\jaccess.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\localedata.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\nashorn.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\sunec.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\sunjce_provider.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\sunmscapi.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\sunpkcs11.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\ext\zipfs.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\jce.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\jsse.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\management-agent.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\resources.jar;
E:\Environment\java\jdk1.8\jre\jre\lib\rt.jar;
E:\ComProject\TestProject\lib\build\classes\java\main;
E:\Environment\java\jdk1.8\lib\idea_rt.jar

 从打印的结果可知,MyClass类的类加载器为AppClassLoader,它的父加载器为ExtClassLoader,而ExtClassLoader的父加载器为BootstrapClassLoader。这里之所以讲ExtClassLoader的父加载器打印为null,那是因为BootStrapClassLoader由C/C++编写,并不是一个Java类,因此无法在java的代码中获取它的引用。

1.1.1.2 Android中的ClassLoader

 Java中的ClassLoader加载的是jar文件.class字节码文件,而Android虚拟机加载的是.dex字节码文件,因此Java中的ClassLoader是不能适用在Android中。与Java中的ClassLoader一样,Android中的ClassLoader也分为系统类加载器和自定义类加载器,其中系统类加载器主要包括三种,即BootClassLoaderPathClassLoaderDexClassLoader,需要注意的是,这三种系统类加载器均由Java实现,且它们的父类均为java.lang.ClassLoader类,它们的继承关系(注:这里是父类与子类之间的继承)如下:
在这里插入图片描述
接下来,我们重点分析下以下三种系统类加载器:

  • BootClassLoader

 BootClassLoader由Java实现,它继承于ClassLoader且是该类的内部类,是Android平台上所有ClassLoader的最终parent,其作用是是预加载常用类,比如Frament、Dialog、DownLoadManager等等。BootClassLoader预加载常用类的过程是在Android系统启动时完成的,具体来说是在Zygote进程启动过程中在ZygoteInit的main方法中被创建使用的。BootClassLoader的源码如下:

// Android8.0\libcore\ojluni\src\main\java\java\lang\ClassLoader$BootClassLoader
class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;
	// 单例模式
    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }

    public BootClassLoader() {
        super(null);
    }
	// 根据指定类全路径名创建其Class对象
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return Class.classForName(name, false, null);
    }
    ...
}
  • PathClassLoader

 PathClassLoader继承于ClassLoader的实现类BaseDexClassLoader,它的作用为加载系统类和应用程序的类,它在SystemServer进程启动时被创建。由于PathClassLoader的构造方法中没有optimizedDirectory参数,因此不支持自定义解压dex文件存储路径,也就意味着PathClassLoader只能用于加载已经安装的apk的dex文件,这个dex文件在APK安装时会被存储到/data/dalvik-cache目录下,也就是说PathClassLoader只能加载/data/dalvik-cache目录中的dex文件。PathClassLoader源码如下:

// Android8.0\libcore\dalvik\src\main\java\dalvik\system\PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    
    public PathClassLoader(String dexPath, String librarySearchPath, 
                           ClassLoader parent) {
        // optimizedDirectory默认为/data/dalvik-cache
        // 因此传入的是null
        super(dexPath, null, librarySearchPath, parent);
    }
}
  • DexClassLoader

 DexClassLoader继承于ClassLoader的实现类BaseDexClassLoader,它的作用为加载dex文件以及包含dex的压缩文件(注:apk和jar文件),其中,dex的压缩文件最终会被解压得到dex文件。由于DexClassLoader的构造参数optimizedDirectory不为null,因此支持自定义解压dex文件存储路径。对比PathClassLoader只能加载已经安装应用的dex或apk文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的.jar和.apk 文件。DexClassLoader源码如下:

// Android8.0\libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
      // 构造方法:
      // dexPath:dex(压缩)文件路径的集合,多个文件以":"分隔符隔开
      // optimizedDirectory:解压dex文件存储路径,为当前应用程序的私有路径(/data/data/pkgName)
      // librarySearchPath:C/C++库的路径集合,多个路径用文件分隔符分隔;
      // parent:父加载器;
      public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

举个例子:

public class SplashActivity extends AppCompatActivity{
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_splash);

        ClassLoader classLoader = SplashActivity.class.getClassLoader();
        Log.d("jiangdg", ""+classLoader);
        while (classLoader != null) {
            classLoader = classLoader.getParent();
            if(classLoader != null) {
                Log.d("jiangdg", ""+classLoader);
            }
        }
    }
}

打印结果如下:

dalvik.system.PathClassLoader
java.lang.BootClassLoader

 从例子打印的结果,可以知道SplashActivity这个类的类加载器为PathClassLoader,这就验证了PathClassLoader确实是用于加载已经安装了的dex或apk文件,而它的父加载器为BootClassLoader,这是Android所有类加载器的最终Parent。

1.1.2 双亲委托模式

 无论是JVM还是Android,一个类的Class加载都遵循双亲委托模式,所谓双亲委托模式是指当类加载器收到要加载一个类的Class请求,它并不会直接加载该Class,而是首先判断缓存中是否已经加载该Class,如果没有则不是自身去查找,而是委托父加载器在其缓存中进行查找,这样依次进行递归,直到委托到最顶层(注:Java中的类加载器最顶层为Bootstrap ClassLoader,Android中的类加载器最顶层为BootClassLoader),如果最顶层的缓存中找到了该Class,就直接返回该Class,如果没有找到则从最顶层继续依次向下查找,其中查找的位置为该层类加载器指定指定加载类的目录,如果还没有找到则最后会交由自身去查找。以Java中系统类加载器为例:
在这里插入图片描述
 由于Java中系统类加载器都无法加载E盘目录下的test.class文件,因此,我们需要通过继承ClassLoader的方式实现一个自定义类加载器。但是,实现的自定义类加载器并不会执行加载test.class类,而是依次向上(AppClassLoader->BootstrapClassLoader)进行委托父类加载器去查找它们的缓存中是否有加载过test.class,如果有就直接返回该class,如果没有就依次向下(BootstrapClassLoader->AppClassLoader)去这些类加载器指定的路径中查找是否包含test.class,如果有则直接加载,如果都没有则最终由自定义类加载器加载E盘目录下的test.class文件。class文件被容加载到内存中,它的字节码内容(静态数据)会被转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象。那么问题来了,为什么自定义的ClassLoader不直接加载test.class文件?原因有两点:一是避免重复加载。如果已经加载过一次Class,就不需要再次加载,而是直接读取已经加载的Class;二是增强系统安全。假如不使用双亲委托模式,我们随便定义一个String类就能替换系统的String类,从而造成安全隐患。但如果使用双亲委托模式,只要加载该Class是同一个类加载器,那么系统始终就会认为这两个Class是同一个,由于系统启动时已经加载过了,因为我们自定义的String将不会被加载。除非使用另外一个类加载器,但是此时这两个Class就不是同一个了,也就没有对原始的那个造成影响。

1.1.3 ClassLoader的加载过程

 ClassLoader加载过程时序图如下:
在这里插入图片描述
 在Android中,加载一个类的Class文件是从调用ClassLoader的loadClass方法开始的,该方法源码如下:

// Android8.0\libcore\ojluni\src\main\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);
            if (c == null) {
                try {
                	// 判断父类加载器是否存在
                	// 如果存在则调用父类加载器的loadClass
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                		// 查看BootstrapClassLoader     
                        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 l o a d C l a s s C l a s s L o a d e r f i n d L o a d e d C l a s s C l a s s p a r e n t p a r e n t l a o d C l a s s p a r e n t = n u l l B o o t s t r a p C l a s s L o a d e r C l a s s c = n u l l C l a s s L o a d e r loadClass方法源码可知,当我们需要加载指定名称的类时,它首先会去调用ClassLoader的findLoadedClass方法来检查该类是否已经被加载过,如果已经被加载,则直接返回该类的Class对象;如果没有被加载,则进入双亲委托模式进行查找。具体来说就是先判断父类加载器parent是否为空,如果不为空则调用parent的laodClass依次向上委托查找缓存是否存在,当parent=null时,说明已经委托到最顶层类加载器BootstrapClassLoader,如果检查到了,就直接返回Class对象,如果没有则说明向上委托流程没有检查出类已经被加载,即c=null。接下来,就会调用ClassLoader findClass方法进入具体的查找流程。该方法源码如下:

// Android8.0\libcore\ojluni\src\main\java\java\lang\ClassLoader.java
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

 从ClassLoader$findClass方法源码可知,该方法只是抛出一个ClassNotFoundException异常,可见它的具体实现应该是在ClassLoader的子类中,由于前面我们说到虽然ClassLoader有很多子类,但是它的具体实现类是BaseDexClassLoader。接下来,我们看下BaseDexClassLoader的findClass方法,源码如下:

// Android8.0\libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // 调用DexPathList的findClass方法
    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;
}

 该方法实现很简单,就是去调用DexPathList的findClass方法执行查找操作,从DexPathList类的源码可知,该类的描述的是dex文件路径集合,DexPathList 的作用和 JVM 中的 classpath 的作用类似,JVM 根据 classpath 来查找类,而 Dalvik/ART利用 DexPathList 来查找并加载类。DexPathList 包含的路径可以是 .dex文件的路径,也可以是包含了 dex 的 .jar 和 .zip 文件的路径(本文统称dex文件)。DexPathList$findClass方法源码如下:

// Android8.0\libcore\dalvik\src\main\java\dalvik\system\DexPathList.java
final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String zipSeparator = "!/";
    /** class definition context */
    private final ClassLoader definingContext;
    // 描述dex/压缩文件路径列表
    // static class Element {
    //    private final File path;       // dex文件路径
    //    private final DexFile dexFile; // 用于加载dex文件
    //    ...
    // }
    private Element[] dexElements;
    // native库路径列表
    private final NativeLibraryElement[] nativeLibraryPathElements;
    // application(应用)本地库目录列表
    private final List<File> nativeLibraryDirectories;
    // system(系统)本地库目录列表
    private final List<File> systemNativeLibraryDirectories;
    // 存放创建dexElemnts列表抛出的异常
    private IOException[] dexElementsSuppressedExceptions;
    ...
        
    public Class<?> findClass(String name, List<Throwable> suppressed) {
        // 遍历dexElements,即所有dex文件
        // 查找是否包含名为name的class文件
        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;
    }
}

 DexPathList$findClass方法只做了一件事情,就是通过循环去遍历dexElements保存的所有dex文件,并通过调用Element的findClass方法来查找当前dex文件中是否包含我们要加载的class文件,如果找到就加载它并返回一个与之相关的Class对象,其中,Element类封装了一个dex文件的路径和DexFile对象,其中这个DexFile用于加载dex相关文件。接下来,我们看下Element的findClass方法,该方法源码如下:

// Android8.0\libcore\dalvik\src\main\java\dalvik\system\DexPathList$Element
public Class<?> findClass(String name, ClassLoader definingContext,
                          List<Throwable> suppressed) {
    return dexFile != null ?
           dexFile.loadClassBinaryName(name, definingContext, suppressed)
           : null;
}

 Element$findClass方法实现很简单,就是先判断dexFile是否为空,如果为空就直接返回null,说明查找失败;如果不为空,则调用DexFile的loadClassBinaryName来加载dex文件,该方法源码如下:

// \Android8.0\libcore\dalvik\src\main\java\dalvik\system\DexFile.java
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
    return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                 DexFile dexFile, List<Throwable> suppressed) {
    Class result = null;
    try {
        // native方法
        // 加载dex文件
        result = defineClassNative(name, loader, cookie, dexFile);
    } catch (NoClassDefFoundError e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    } catch (ClassNotFoundException e) {
        if (suppressed != null) {
            suppressed.add(e);
        }
    }
    return result;
}

 在loadClassBinaryName方法中会继续调用DexFile的defineClass方法,该方法最终调用defineClassNative执行dex文件加载流程,至此,查找加载流程由Java层转入到Native层,考虑到篇幅原因,这里就不再继续分析下去了。最后总结下ClassLoader的加载过程,即首先会遵循双亲委托模式检查此前是否已经加载过传入的类(指的是该类的.class文件),如果没有检查到就调用ClassLoader的findClass方法进行查找流程,Java层最终会调用DexFile的defineClassNative方法来执行查找流程。

1.1.4 类的链接

 当类被类加载器加载到内存之后,系统就会为之生成一个对应的Class对象,接着就会进入类的链接阶段,该阶段的作用为是把类的二进制数据合并到JRE中。类的生命周期示意图:
在这里插入图片描述
类的链接分为如下三个阶段:

(1)验证

 验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。

(2)准备

 类准备阶段主要负责为类的静态属性分配内存,并设置默认初始值。

(3)解析

 将类的二进制数据中的符号引用替换成"直接引用"。

1.1.5 类的初始化

 对于每个类而言,JRE都为其保留一个不变的Class类型的对象,一个Class对象包含了特定某个结构(class(类)/interface(接口)/enum(枚举)/annotation/primitive type/void/[])的有关信息,也就是说,只有这些结构才有对应的Class对象。Class本身也是一个类,一个加载的类在JVM中只会有一个Class实例,这个Class实例对应的是一个加载到JVM中的一个.class文件,并且每个类的实例都会记得自己是由哪个Class实例所生成。通过Class可以完整地得到一个类中所有被加载的结构,Class类是反射的根源,针对任何我们希望动态加载运行的类,唯有先获得相应的Class对象。获取一个类对应的Class类的实例主要有四种方式:

(1)通过某个类的class属性获取。该方法最为安全可靠,程序性能最高。

// Person是一个类
Class clazz = Person.class;

(2)通过某个类的实例的getClass()方法获取

// person是Person的一个实例
Class clazz = person.getClass();

(3)通过Class类的静态方法forName()获取

// 获取失败会抛出ClassNotFoundException
Class clazz = Class.forName("com.jiangdg.test.Person");

(4)通过ClassLoader加载一个类

Class clazz = ClassLoader.loadClass("com.jiangdg.test.Person");

 对于使用Class.forName()ClassLoader.loadClass()获取指定类的Class对象时,它们之间还是有一定的区别的,具体表现为ClassLoader.loadClass()加载某个类,该方法只会加载该类,并不会执行该类的初始化;Class.forName()加载某个类时,默认还会执行类的初始化操作。一旦初始化,就会触发目标对象的
static块代码执行,static参数也会被初始化。这两点可以通过源码容易看出。

1.2 Java反射机制

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。在日常的第三方应用开发过程中,经常会遇到某个类的某个成员变量、方法或是属性是私有的或是只对系统应用开放,这时候就可以利用Java的反射机制通过反射来获取所需的私有成员或是方法。与Java反射相关的类如下:

类名 用途
Class类 代表类的实体,在运行的Java应用程序中表示类和接口,它是反射的基础。
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

 下图罗列了与反射相关的知识点:
在这里插入图片描述

2. 热修复技术

 对于已经发布的APP出现严重bug时,我们通常的解决做法是,将bug修复后重新打包一个升级版本并发布到应用市场,然后提示用户更新APP。这种传统的修复方式虽然能够解决问题,但是要发布一个新版往往需要大量的测试,并且从上一个版本到下一个版本时间间隔也会较长,这就会导致bug修复代价过大以及用户体验大大下降。为了缓解上述问题,热修复技术(或称热修复补丁)应运而生。热修复补丁(hotfix),又称为Patch,指能够修复软件漏洞的一些代码,是一种快速、低成本修复产品软件版本bug的方式。热修复补丁是一种包含信息的独立的累积更新包,通常表现为一个或多个文件。

2.1 热修复方案

 热修复框架种类繁多,但绝大多数是基于类加载机制底层替换机制Instant Run热插拔机制三种方式实现,并且这些热修复框架的主要提供代码修复资源修复动态链接库修复三种核心技术(功能)。目前主流的框架:Tinker(腾讯系)QZone(腾讯系)AndFix(阿里系)Robust(美团),我们可以根据具体的业务来选择合适的热修复框架,它们的主要区别如下表所示:

Tinker QZone AndFix Robust
类替换 yes yes no no
So替换 yes no no no
资源替换 yes yes no no
全平台支持 yes yes yes yes
即时生效 no no yes yes
性能损耗 较小 较大 较小 较小
补丁包大小 较小 较大 一般 一般
开发透明 yes yes no no
复杂度 较低 较低 复杂 复杂
gradle支持 yes no no no
Rom体积 较大 较小 较小 较小
成功率 较高 较高 一般 最高

2.1.1 Tinker

Tinker是微信团队开源的一个热修复框架,它针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。该方法主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是以差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX,达到修复的目的。Tinker是一种类替换方案,无法及时生效,需要重启APP实现修复生效。Tinker方案流程如如下:
在这里插入图片描述

2.1.2 QZone超级补丁

QZone超级补丁基于dex分包(multidex dex)技术实现,它是一种java层类替换方案,无法及时生效,需要重启APP实现修复生效。该方案大致过程:把BUG方法修复以后,放到一个单独的dex补丁文件,让程序在运行期间加载dex补丁,即将dex文件插入到dexElements数组的最前面,然后再让虚拟机执行修复后的方法。其中,这个加载dex补丁的过程是基于类加载机制实现的,根据Android虚拟机的类加载机制,同一个类只会被加载一次(基于双亲委托机制),所以要让修复后的类替换原有的类就必须让补丁包的类被优先加载,而这个dexElements数组就存储了所有要被加载的dex文件,我们要做的就是将dex补丁文件插入到这个数组的前面,从而实现补丁包的优先加载。QZone超级补丁方案流程图:
在这里插入图片描述

2.1.3 AndFix

AndFix基于在native层动态替换java层的方法技术实现,它是一种方法替换方案,能够及时生效,无需重启APP。该方法大致过程:首先,打开链接库的操作句柄,获取native层的内部函数,得到ClassObject对象。(setup方法);然后,修改访问权限属性为public。(setFieldFlag方法);最后,得到新旧方法的指针,使新方法指向目标方法,从而实现方法的替换(replaceMethod方法)。 AndFix方案流程图:
在这里插入图片描述

注:由于AndFix基本处于停止维护状态,且只能支持到Android 7.0版本,而目前大部分机型为Android9.0+,因此该方法只作原理了解,不建议使用。

2.1.4 Robust

Robust借鉴Instant Run热插拔技术实现,它是一种方法替换方案,能够及时生效,无需重启APP。该方案大致过程:Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明。类似于代理,将方法执行的代码重定向到其他方法中。Robust方案流程图
在这里插入图片描述

2.2 热修复实战

 俗话说:“纵使有千言万语,却抵不过认真撸一次“。为了加深对热修复技术的理解,本小将以基于multidex方案为例,实现一个简化版的热修复框架。该框架实现流程如下:
在这里插入图片描述
 根据之前对类加载过程的分析,基于multidex方案的热修复核心是获取当前应用的PathClassLoader类加载器,然后得到该对象的DexPathList类型属性pathList,最后修改pathList对象的dexElements属性的值。具体来说,可分为如下几步:

  • 首先,获取当前应用的PathClassLoader,这个当前应用的类加载器;
  • 其次,通过反射获得DexPathList属性对象pathList;
  • 最后,反射修改pathList的dexElemnts。细分为如下三步:
    • (1)把补丁包patch.dex转化为Elment[] (path);
    • (2)获得pathList的dexElements属性,类型为Elment[] (old);
    • (3)将path Elment[]和old Elment[]合并,并反射赋值给pathList的dexElements;

 实现代码如下:

/** 自定义热修复框架
 * author : jiangdg
 * date   : 2019/12/29 13:53
 * desc   : 基于multidex方案
 * version: 1.0
 */
public class HotFix {

    public static void fix(Application context, File patchFile) {
        if(! patchFile.exists()) {
            return;
        }
        ArrayList<File> patches = new ArrayList<>();
        patches.add(patchFile);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        File mOdexDirectory = context.getDir("odex", Context.MODE_PRIVATE);

        // 1. 获取当前应用的PathClassLoader,这个当前应用的类加载器;
        ClassLoader pathClassLoader = context.getClassLoader();
        // 注释1
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            try {
                pathClassLoader = NewClassLoaderInjector.
                    inject(context, pathClassLoader);
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
        // 2. 通过反射获得DexPathList属性对象pathList;
        Field pathListField = findField(pathClassLoader, "pathList");

        // 3. 反射修改pathList的dexElemnts,即重新赋值
        try{
            Object pathList = pathListField.get(pathClassLoader);
            // 3.1 把补丁包patch.dex转化为Elment[],即patchElements
            // 通过调用pathList对象的makePathElement方法实现,该方法需要存储三个参数
            //    files: 补丁dex文件列表
            //    optimizedDirectory: 存储dex优化后的odex文件目录,我们可以自定义
            //    suppressedExceptions: new一个即可
            Method makePathElementsMthod = findMethod(pathList, "makePathElements", 
                                          List.class,File.class, List.class);
            Object[] patchElements = (Object[]) makePathElementsMthod.
                invoke(pathList, patches, mOdexDirectory, suppressedExceptions);

            // 3.2 获得pathList的dexElements属性,类型为Elment[]
            // 即 oldElements
            Field dexElementsField = findField(pathList, "dexElements");
            Object[] oldElements = (Object[]) dexElementsField.get(pathList);

            // 3.3 将path Elment[]和old Elment[]合并,并反射赋值给pathList的dexElements;
            Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().
                    getComponentType(), patchElements.length + oldElements.length);
            
            System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
            System.arraycopy(oldElements, 0, newElements, 
                             patchElements.length, oldElements.length);
            dexElementsField.set(pathList, newElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Field findField(Object instance, String fieldName) {
        // 获取instance的Class对象
        Class<?> clz = instance.getClass();
        // 得到Class对象的field属性
        // 需要考虑继承问题,有可能field在父类中
        while(clz != null) {
            try {
                Field filed = clz.getDeclaredField(fieldName);
                if(filed != null) {
                    // 设置访问权限
                    filed.setAccessible(true);
                    return filed;
                }
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            }
            clz = clz.getSuperclass();
        }
        throw new IllegalArgumentException("instance:"+ 
                                           instance.getClass().getSimpleName()
                                           +"do not find "+ fieldName);
    }

    private static Method findMethod(Object instance, String methodName, 
                                     Class<?>... paramters) {
        Class<?> clz = instance.getClass();

        while (clz != null) {
            try {
                Method method = clz.getDeclaredMethod(methodName, paramters);
                if(method != null) {
                    method.setAccessible(true);
                    return method;
                }
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }

            clz = clz.getSuperclass();
        }

        throw new IllegalArgumentException("instance:"+ 
                                           instance.getClass().getSimpleName()
                                           +"do not find "+ methodName);
    }
}

 需要注意的是,在注释1处有个Android版本判断,目的是解决ART模式在Android N(7.0)之前安装APK时会采用AOT(Ahead of time:提前编译、静态编译)预编译为机器码。而在Android N使用混合模式的运行时。应用在安装时不做编译,而是运行时解释字节码,同时在JIT编译了一些代码后将这些代码信息记录至Profile文件,等到设备空闲的时候使用AOT(All-Of-the-Time compilation:全时段编译)编译生成称为app_image的base.art(类对象映像)文件,这个art文件会在apk启动时自动加载(相当于缓存)。根据类加载原理,类被加载了无法被替换,即无法修复。接着,我们看下如何使用HotFix工具类实现热修复,即在Application类中加载补丁包,相关代码如下:

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        hotFixBug();
    }

    private void hotFixBug() {
        // 根目录下
        File file = new File("sdcard/patch.jar");
        if(! file.exists()) {
            return;
        }
        HotFix.fix(this, file);
    }
}

 当我们没有生成和指定补丁包时,HotFix并不起作用,运行测试APP仍然会报错,这里如果我们将修复生成的补丁包放置到sdcard目录下时,然后重启测试APP将不会再报错。其中,我们可以使用dx.bat工具生成补丁包,该工具位于Android SDK\build-tools\28.03目录下,执行命令:

dx --dex --output=patch.jar com\jiangdg\hotfix\MyUtils.class

其中,我们需要将下图中com目录拷贝到E:\盘目录下,然后再在E:\盘下执行上述cmd命令。另外,由于是冷启动,因此我们需要重启APP使热修复生效。
在这里插入图片描述

Github项目地址:HotFix

 最后,对目前主流的热修复框架作个小结:Android平台的热修复框架种类繁多,根据实现原理大体可分为三类,即基于multidex、基于native hook方案以及基于Instant Run热插拔机制。其中,基于multidex的热修复框架,如NuWa、Tinker、Qzone超级补丁等,这种方案兼容性高,但是需要反射更改DexElements,改变Dex的加载顺序,这使得patch需要在下次启动时才能生效,实时性就受到了影响;基于native hook的热修复框架,如Andfixd等,这种方法能够实时生效且几乎无性能损耗,但是需要针对dalvik虚拟机和art虚拟机做适配,需要考虑指令集的兼容问题,需要native代码支持,兼容性上会有一定的影响。基于Instant Run热插拔机制,如Robust,这种方案兼容性高且实时生效,但是会增加包体积,且暂时不支持so文件和资源替换。

发布了83 篇原创文章 · 获赞 293 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/AndrExpert/article/details/103756056