Android热修复之-Frameworks层修复原理分析

说到热修复主要有两种修复方案一种是通过dex替换的方式来达到修复效果、一种是基本native层的修复。dex替换的方式来修复主要的微信团队的Tinker方案,native层修复有阿里的Andfix方案。其中Andfix是实时修复的,而Tinker方案则需要冷启动才能修复。

Native层修复

native层的修复主要是通过替换Method结构体的字段来实现修复,但是国内很多手机厂商都会修改开源代码,这就造成有在部分手机的修复会失效。

Frameworks层修复

frameworks层修复主要是通过替换DexPathList类中的Elemets数组实现修复,这种修复方式会比native层的修复会稳定,一般手机厂商也不会去修改这部分源码,唯一的缺点就是不能做到native层的实现时修复的效果。

Framework层修复原理分析

通过在MainActivity中打印getClassLoder.toString(),可以看到android的类加载器是PathClassLoader这个类,通过查看源码可以看到这个类只有一个构造方法就没有其它的方法了。

package dalvik.system;


public class PathClassLoader extends BaseDexClassLoader {
  PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

DexClassLoader是BaseDexClassLoader的子类,具体的实现都在它的父类中,如果感兴趣的也可以通过查看App的启动流程来分析,具体在ActivityThread.java中的performLaunchActivity方法中开始跟下去就可以看到有一个ClassLoderFactory中看到PathClassLoader的实例过程

    /**
     * Same as {@code createClassLoader} below, except that no associated namespace
     * is created.
     */
    public static ClassLoader createClassLoader(String dexPath,
            String librarySearchPath, ClassLoader parent, String classloaderName) {
        if (isPathClassLoaderName(classloaderName)) {
            return new PathClassLoader(dexPath, librarySearchPath, parent);
        } else if (isDelegateLastClassLoaderName(classloaderName)) {
            return new DelegateLastClassLoader(dexPath, librarySearchPath, parent);
        }

        throw new AssertionError("Invalid classLoaderName: " + classloaderName);
    }

平时使用ClassLoader时通常都是调用loadClasss方法进行dex的加载,所以通过这个方法去查找里面的实现过程

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

从源码中可以看到loadClasss的方法调用了findLoadClass方法进行加载。刚才已经知道了系统的类加载器是PathClassLoader,PathClassLoader中只有构造方法,没有其它的方法所以得到BaseDexClassLoader中查看findClass的具体实现。

    @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中的findClass又调用了pathList中的findClass方法,这个pathList的原型是DexPathList。

  private final DexPathList pathList;

具体的实现都在DexPathList里面

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

从上面可以看到是遍历一个dexElements的数组来查找对应的dex文件

  /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

这个dexElements是在DexPathList里面的一个叫makeDexElements方法创建出来的

/**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    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;
    }

通过上面的分析,可以知道一个apk的dex文件都是存放在DexPathList里面的一个dexElements数组里面让系统使用。apk在打包时可以使用分包技术把class.dex分成若干个,这样在apk有bug的时候可以通过替换掉有bug有dex文件这样就可以达到修复的效果。

推荐阅读更多精彩内容

  • 针对app线上修复技术,目前有好几种解决方案,开源界往往一个方案会有好几种实现。重复的实现会有造轮子之嫌,但分析解...
    石先阅读 3,680评论 2 34
  • 文章目标 Android类加载机制介绍 javassist动态修改字节码 实现热补丁动态修复 Android类加载...
    JxMY阅读 354评论 1 1
  • 什么是热修复 热修复:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。 ​ 早期遇到Bu...
    vson1718阅读 151评论 0 3
  • github地址:https://github.com/Geekholt/HotFix 目录 前言 bug一般是一...
    Geekholt阅读 72评论 0 3
  • 简介 热修复的方案有很多种,其中原理也各不相同。目前开源的比较有名的有阿里AndFix、美团Robust、qq的Q...
    xiasem阅读 710评论 1 7