QZone热修复

2,537 阅读5分钟

前言

大家好,我是猪猪侠。热修复是近几年比较火的话题,它可以在不更新版本的前提下,使用户在无感的情况下修复应用的bug。从而避免了因发因版本时间较长,而导致因为bug对用户的体验感大大降低情况。

市面上比较有名的热修复框架

其中AndFix(阿里)已经不再维护了。今天我们来看下QZone热修复原理,相对于其他三种热修复框架,QZone热修复相对较简单,也比较容易理解。

类加载

我们知道普通的java程序是使用ClassLoader执行class文件,而Android虽然是使用java开发,但是Android却不能直接执行class文件,而是执行的dex文件。所以加载dex就需要一些特殊的类加载器。Android中常见的类加载器有BootClassLoader、BaseDexClassLoader、PathClassLoader、DexClassLoader。

BootClassLoader是加载Android系统源码,例如Activity,AMS等。PathClassLoader和DexClassLoader都是继承于BaseDexClassLoader,两者的区别在于构造方法参数不同。默认情况下,PathClassLoader是用于加载三方库,比如AppCompatActivity等这些代码。DexClassLoader是加载外部的dex文件,其实使用PathClassLoader去加载外部的dex文件也是没问题的。

双亲委托机制

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 {
                    if (parent != null) {
                    	//父亲加载过的Class
                        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.
                    //自己加载Class
                    c = findClass(name);
                }
            }
            return c;
    }

从上述源码我们可以看出,类加载的大致流程如下:

  1. 获取已经加载过的Class。
  2. 从父类加载器中获取Class。
  3. 如果parent为null,则调用BootstrapClassLoader进行加载。
  4. 如果class依旧没有找到,则调用当前类加载器的findClass方法进行加载。

双亲委托机制的优点

  1. 避免重复的类加载,可以节省资源(这也是热修复的前提)
  2. 安全性较高,防止核心API被随意篡改
public class BaseDexClassLoader extends ClassLoader {
	private final DexPathList pathList;
    
    @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;
    }
}
final class DexPathList{
	//dex文件数组
	private Element[] dexElements;
    
    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
      Element[] elements = new Element[files.size()];
      int elementsPos = 0;
      /*
       * Open all files and load the (direct or contained) dex files up front.
       */
      for (File file : files) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  DexFile dex = null;
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }
}

原理分析

如上图所示,类加载会依次从class.dex到class2.dex。例如class.dex和class.dex中都有个类A,但是类只会从class.dex加载一次A类,后面class1.dex中的A类不会被加载。

如上图所以,假设class1.dex中有个存在Bug的A.class,那我们就可以将修复后的类插到Element数组的最前面,那当程序运行时就只会加载fix.dex中的A.class,而不会加载有Bug的A.class了。

那我们现在要做的就是修改将fix.dex插入到DexPathList中的dexElements数组的索引第一位,然后替换原来的dexElements数组。

那用什么方式呢?没错,就是强大的反射。下面看下主要代码

fun install(){
	......
	//将新的dex转换为dexElements数组
                val makePathElements =
                    makePathElements(pathList, list, context.cacheDir, exceptions)
                val newElements = java.lang.reflect.Array.newInstance(
                    dexElements.javaClass.componentType,
                    dexElements.size + makePathElements.size
                )
                //将新的dexElements和旧的dexElement数组合并
                System.arraycopy(makePathElements, 0, newElements, 0, makePathElements.size)
                System.arraycopy(
                    dexElements,
                    0,
                    newElements,
                    makePathElements.size,
                    dexElements.size
                )
                //将合并后的dexElements数组重新设置给DexPathList中的dexElements
                dexElementsField.set(pathList, newElements)
	}

	/**
     * 将dex转化为Element数组
     */
    @Suppress("UNCHECKED_CAST")
    @Throws(
        IllegalAccessException::class,
        InvocationTargetException::class,
        NoSuchMethodException::class
    )
    private fun makePathElements(
        dexPathList: Any,
        files: ArrayList<File>,
        optimizedDirectory: File,
        exceptions: ArrayList<IOException>
    ): Array<Any> {
        val makePathElementsMethod = dexPathList.javaClass.getDeclaredMethod(
            "makePathElements",
            List::class.java,
            File::class.java,
            List::class.java
        )
        if (!makePathElementsMethod.isAccessible) {
            makePathElementsMethod.isAccessible = true
        }
        return makePathElementsMethod.invoke(
            dexPathList,
            files,
            optimizedDirectory,
            exceptions
        ) as Array<Any>
    }

有需要看详细代码的,请看这里

存在的问题

  1. 版本兼容,Android每个版本系统源码都有所不同,所以反射的时候需要对每个版本进行兼容。
  2. CLASS_ISPREVERIFIED标志问题。例如MainAcivity和Utils类存在于同一个dex中,这个时候MainActivity会被打上CLASS_ISPREVERIFIED标志,大概意思就是当MainActivity使用Utils类的时候,会直接从该dex中加载,而不会从其他dex中加载,这个时候就会出现问题。具体解决方案请参考安卓App热补丁动态修复技术介绍
  3. Android N混合编译与对热补丁影响解析

总结

这篇文章只是说了QZone热修复的大概原理,以及手撸了一个简单的热修复Demo,不可用于真实项目中,只是为了让自己加深对类加载和热修复原理的印象。好了,今天就到这里了,大家再见。