转载

Android中dex文件加载原理解析

说明:该篇博客参考https://juejin.im/post/5a0ad2b551882531ba1077a2,只是为了自己的学习做记录,如有侵权请联系删除。

1、简述

  为了能够对热修复的原理理解的更加深入有必要对Android中dex文件的加载机制进行解析。

2、源码

  在Andorid中有两个专门的类加载器用于加载Andorid的dex文件中的class文件,分别是PathClassLoader和DexClassLoader;PathClassLoader只能加载已经安装到Andorid系统中的apk文件(data/aap目录),是Android系统默认的类加载器,DexClassLoader可以加载任意目录下的dex、jar、zip、apk文件,比PathClassLoader更加灵活,因此这也成为了实现热修复的一个突破点。下面对他们的代码分别进行讲解。

2.1 PathClassLoader源码讲解

  该类继承了BaseDexClassLoader类,并且在仅有的两个构造方法中也调用到了父类的构造方法中。

public class PathClassLoader extends BaseDexClassLoader {
    /**
    * dexPath:要加载的dex、jar、apk或者zip文件string路径列表,并且每一个dex路径用:分隔开
    * parent:父类加载
    **/
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
    /**
    *  librarySearchPath:加载程序文件时需要用到的库路径,有可能为null
    **/
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}
复制代码

2.2 DexClassLoader源码讲解

  该类也是继承了BaseDexClassLoader了,并且在仅有的一个构造方法中调用到了父类的构造方法。

public class DexClassLoader extends BaseDexClassLoader {
    /**
    * optimizedDirectory:dex文件的输出目录,因为在加载zip、apk、jar格式的程序文件的时候会解压出其中的dex文件,该目录
    *就是专门用于存放这些被解压出来的dex文件,但是从api26开始就失效了,即使传入了具体的值也不会被使用。
    **/
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}
复制代码

2.3 BaseDexClassLoader源码讲解

  DexClassLoader和PathClassLoader类加载器最终都会调用到BaseDexClassLoader类中,也就是具体的实现都在该类中。

public BaseDexClassLoader (String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent, boolean isTrusted) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);

    if (reporter != null) {
        reportClassLoaderChain();
    }
}
复制代码

  从源码可以看出BaseDexClassLoader类继承了ClassLoader类,并且在BaseDexClassLoader的构造方法中首先会调用父类的构造方法,下面对ClassLoader中的构造方法进行分析。

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
}
复制代码

  在ClassLoader构造方法中会对父加载器进行初始化。接下来继续看BaseDexClassLoader的构造方法,初始化了成员变量pathList,继续看DexPathList中的构造方法。

/**
* definingContext:当前的类加载器
* dexPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
* librarySearchPath:加载程序文件的库文件
* optimizedDirectory:dex文件的解压目录,但是在api26以后就不在使用了
**/
DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
    ..........//判断数据的合法性
    
    this.definingContext = definingContext;
    //初始化IO异常列表
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    //将dex文件构造为Elements对象
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions, definingContext, isTrusted);
    //对库文件路径进行解析
    this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    this.systemNativeLibraryDirectories =
            splitPaths(System.getProperty("java.library.path"), true);
    List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
    allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
    if (suppressedExceptions.size() > 0) {
        this.dexElementsSuppressedExceptions =
            suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    } else {
        dexElementsSuppressedExceptions = null;
    }
}
复制代码

3.2 splitDexPath函数讲解

/**
*  searchPath:要加载的dex、jar、apk或者zip文件string路径列表,每一个dex路径用:分隔开
**/
private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
    List<File> result = new ArrayList<>();
    if (searchPath != null) {
        for (String path : searchPath.split(File.pathSeparator)) {
            if (directoriesOnly) {
                try {
                    StructStat sb = Libcore.os.stat(path);
                    if (!S_ISDIR(sb.st_mode)) {
                        continue;
                    }
                } catch (ErrnoException ignored) {
                    continue;
                }
            }
            //将string列表中单个dex、jar、apk或者zip文件路径存放到list中
            result.add(new File(path));
        }
    }
    return result;
}
复制代码

3.3 makeDexElements函数讲解

/**
* files:dex、jar、zip或者apk文件路径列表
* optimizedDirectory: dex解压路径,在api26以后为null
* suppressedExceptions:IO异常列表
**/
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
        List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
  Element[] elements = new Element[files.size()];
  int elementsPos = 0;
  for (File file : files) {
      //文件是一个目录,则直接添加到elements列表中,后续解析的时候直接从目录中找到dex文件
      if (file.isDirectory()) {
          elements[elementsPos++] = new Element(file);
      } else if (file.isFile()) {
          String name = file.getName();
          DexFile dex = null;
          //如果该文件是.dex结尾的文件则将该文件包装为DexFile对象
          if (name.endsWith(DEX_SUFFIX)) {
              try {
                  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 {
              //如果该文件是jar、apk或者zip文件,则从这些文件中提取出dex文件并包装太DexFile对象,具体的提取是在DexFile
              //中通过native方法进行提取
              try {
                  dex = loadDexFile(file, optimizedDirectory, loader, elements);
              } catch (IOException suppressed) {
                  suppressedExceptions.add(suppressed);
              }
              if (dex == null) {
                  elements[elementsPos++] = new Element(file);
              } else {
                  elements[elementsPos++] = new Element(dex, file);
              }
          }
          if (dex != null && isTrusted) {
            dex.setTrusted();
          }
      } else {
          System.logW("ClassLoader referenced unknown path: " + file);
      }
  }
  //如果实际的长度和理论的长度不等,则将elements的长度变更为实际长度
  //实际长度<=理论长度
  if (elementsPos != elements.length) {
      elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}
复制代码

  从PathClassLoader和DexClassLoader的构造方法开始,最后会在BaseClassLoader中将包含dex的文件或者文件夹构造成一个个的Element对象,并且最后会通过findClass方法从构造出的Element列表中解析出与传入的class名相同的class文件。下面对findClass方法进行分析。

/**
* 遍历dexElements列表,找到与传入的className相对应的第一个class并返回;正因为这个特性成为了热修复的突破点,我们只需要
* 将需要修复的bug类编译成dex文件然后放到dexElements列表的第一个元素位置,当系统在查找类的时候就会只加载我们插入的dex
* 文件
* name: 需要寻找的class名
* suppressed: 异常列表
**/
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;
}
复制代码

  在BaseClassLoader的findClass方法中最终会调用到Element的findClass方法。

//最终会从dex文件查找该class,如果找到了则直接返回,没有找到则直接返回null
public Class<?> findClass(String name, ClassLoader definingContext,
            List<Throwable> suppressed) {
        return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                : null;
}
复制代码

  最终会调用到DexFile中的loadClassBinaryName方法

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文件中查找到与name相对应的clas文件
        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;
}
复制代码

3、热修复实现步骤

  文章最开始就讲到Android中存在两个类加载器PathClassLoader和DexClassLoader,它们虽然都是为了将一个个dex文件构造成Element对象,并从dex文件中加载出对应的class文件,但是它们的使用方式却不相同。PathClassLoader是Android默认的dex文件加载器,DexClassLoader则是为了能够加载没有被初始化在apk中的代码,它可以加载Android中任意目录下包含dex的jar、apk、zip等文件,而这也成为了我们实现热修复的突破点。根据这种思路实现热修复大致步骤如下:

  (1)将需要加入到原有apk的java文件编译为dex文件格式;

  (2)获取到默认的PathClassLoader实例对象;

  (3)获取指定目录下面所有包含dex文件的apk、jar、zip等文件;

  (4)根据获取到的文件构造出DexClassLoader;

  (5)获取到DexClassLoader中的dexElements列表,并存储到集合中;

  (6)获取PathClassLoader中的dexElements列表;

  (7)将获取到的dexElements列表集合按先后顺序存储到PathClassLoader中dexElements列表中的头部;

  当app重新启动之后就会加载最新的dex文件,这样就会将Bug修复了,不过老的dex文件依旧存在于dexElements列表中,只是没有机会被加载到了而已。具体可以参考文章开头中这位大神的实现。

原文  https://juejin.im/post/5d15ca8c5188255cfe0de8f8
正文到此结束
Loading...