Android中dex文件加载原理解析

栏目: IOS · Android · 发布时间: 5年前

内容简介:说明:该篇博客参考https://juejin.im/post/5a0ad2b551882531ba1077a2,只是为了自己的学习做记录,如有侵权请联系删除。为了能够对热修复的原理理解的更加深入有必要对Android中dex文件的加载机制进行解析。在Andorid中有两个专门的类加载器用于加载Andorid的dex文件中的class文件,分别是PathClassLoader和DexClassLoader;PathClassLoader只能加载已经安装到Andorid系统中的apk文件(data/aap目录

说明:该篇博客参考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列表中,只是没有机会被加载到了而已。具体可以参考文章开头中这位大神的实现。


以上所述就是小编给大家介绍的《Android中dex文件加载原理解析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

强化学习精要

强化学习精要

冯超 / 电子工业出版社 / 2018-6 / 80

《强化学习精要:核心算法与TensorFlow 实现》用通俗幽默的语言深入浅出地介绍了强化学习的基本算法与代码实现,为读者构建了一个完整的强化学习知识体系,同时介绍了这些算法的具体实现方式。从基本的马尔可夫决策过程,到各种复杂的强化学习算法,读者都可以从本书中学习到。本书除了介绍这些算法的原理,还深入分析了算法之间的内在联系,可以帮助读者举一反三,掌握算法精髓。书中介绍的代码可以帮助读者快速将算法......一起来看看 《强化学习精要》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具