美文网首页
热修复原理解析

热修复原理解析

作者: 还是昵称啊 | 来源:发表于2020-07-05 22:40 被阅读0次

1. 类加载机制

1.1 类加载UML图

ClassLoader继承图

1.2 loadClass流程

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) {
          if (parent != null) {
            //使用父ClassLoader加载
            c = parent.loadClass(name, false);
          } else {
            c = findBootstrapClassOrNull(name);
          }
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}
  • 首先先判断是否已经加载过这个类

  • 如果没有加载过就让父加载器来加载

  • 如果父加载器没有加载到才使用当前的ClasLoader来加载----双亲委派机制

    • 避免多次加载
    • 避免系统class被自定义的ClasLoader篡改
  • findClass方法由ClassLoader的子类实现,主要由BaseDexClassLoader类实现

    • @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类中包含一个DexPathList对象

    • 在这个pathList中查找对应的加载类

    DexPathList中通过一个Element数组保存加载过的类,然后遍历这个数组当找到从找到一个和当前类名匹配的Class对象时就停止遍历,返回这个Class

    public Class<?> findClass(String name, List<Throwable> suppressed) {
            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;
    }
    

2. 实现原理

主要流程

  • 将新的dex文件转换成Element数组
  • 获取新生成的Element数组中的所有对象
  • 获取旧的Element数组中的对象
  • 将新的对象插入到旧的对象之前,在遍历时就能获取到最新的class文件
  • 把组合的Element数组设置到DexPathList对象中

2.1 从类加载器中获取DexPathList对象

Class<?> cl = null;
Field pathListFiled = null;
for (cl = classLoader.getClass(); cl != null; cl = cl.getSuperclass()) {
    try {
        //从BaseDexClassLoader中找pathList属性
        pathListFiled = cl.getDeclaredField("pathList");
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    }
    if (pathListFiled != null) {
        pathListFiled.setAccessible(true);
        break;
    }
}

因为只有BaseDexClassCloader中才持有这个对象,因此需要对当前ClassLoader的父类进行遍历,通过反射拿到DexPathList属性对象

2.2 获取makePathElements方法

通过反射从DexPathList对象中获取makePathElements这个方法,通过这个方法把传入的Dex文件转换成Element数组。

Object pathList = pathListFiled.get(classLoader);

Class<?> pathListClass = pathList.getClass();
Method makePathListMethod = pathListClass.getDeclaredMethod("makePathElements",
        List.class, File.class, List.class);
makePathListMethod.setAccessible(true);

//要插入的新的文件集合
List<File> dexFile = new ArrayList<>();
File f = new File(file);
dexFile.add(f);
List<IOException> suppressedException = new ArrayList<>();
//获得新的dexElements数组
Object[] elements = (Object[]) makePathListMethod.invoke(pathList, dexFile,new File(optPath),
        suppressedException);
Log.d("sxhLog", "fix: " + elements.length);

2.3 获取PathList中原有的Element数组

直接通过反射拿到PathList对象中的dexElement对象。

Field oldElementsFiled = pathListClass.getDeclaredField("dexElements");
oldElementsFiled.setAccessible(true);
Object[] oldElements = (Object[]) oldElementsFiled.get(pathList);
Log.d("sxhLog", "fix: " + oldElements.length);

2.4 将新的数组插入旧的数组

//合并两个数组
Object[] newArrayElement = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
        elements.length + oldElements.length);

//将两个数组中的元素拷贝到新的数组中
System.arraycopy(elements, 0, newArrayElement, 0, elements.length);
System.arraycopy(oldElements,0, newArrayElement, elements.length, oldElements.length);

//把原有的Element数组中的内容替换为新的
oldElementsFiled.set(pathList, newArrayElement);

先新建一个数组,然后把新的Element数组插入再把旧的Element数组插入,这里因为Element类是对外不可见的,因此需要使用Array.newInstance来创建对应的数组。

2.5 最后用组装的新Element数组替换旧的数组

//把原有的Element数组中的内容替换为新的
oldElementsFiled.set(pathList, newArrayElement);

3. 制作dex文件

先使用jar命令把指定的class文件打成jar包jar cvf fix.jar com/example/hotfix/Bug.class

使用dx命令(build-tools/29.0.3/目录下)把刚才的jar打包成dex文件 dx --dex --output=output.dex --fix.jar

然后把这个dex文件push到手机目录下,在修复时把这个文件传入就可以加载对应的Class。

相关文章

网友评论

      本文标题:热修复原理解析

      本文链接:https://www.haomeiwen.com/subject/ppibqktx.html