浅谈 Android 热更新的前因后果 _ Android ,Android 面试基础知识
//核心关注点 private final DexPathList pathList;
BaseDexClassLoader 构造函数有四个参数,含义如下:
// dexPath: 需要加载的文件列表,文件可以是包含了 classes.dex 的 JAR/APK/ZIP,也可以直接使用 classes.dex 文件,多个文件用 “:” 分割// optimizedDirectory: 存放优化后的 dex,可以为空// librarySearchPath: 存放需要加载的 native 库的目录// parent: 父 ClassLoaderpublic BaseDexClassLoader(String dexPath, File optimizedDirectory,String librarySearchPath, ClassLoader parent) {//classloader,dex 路径,目录列表,内部文件夹 this(dexPath, optimizedDirectory, librarySearchPath, parent, false);}
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();}}
...
public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {// TODO We should support giving this a library search path maybe.super(parent);this.pathList = new DexPathList(this, dexFiles);}
//核心方法 @Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {//异常处理 List<Throwable> suppressedExceptions = new ArrayList<Throwable>();//这里也只是一个中转,关注点在 DexPathListClass 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.
DexPathList
final class DexPathList {//文件后缀 private static final String DEX_SUFFIX = ".dex";private static final String zipSeparator = "!/";
** class definition context */private final ClassLoader definingContext;
//内部类 Elementprivate Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,String librarySearchPath, File optimizedDirectory) {this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);}
DexPathList(ClassLoader definingContext, String dexPath,String librarySearchPath, File optimizedDirectory, boolean isTrusted) {if (definingContext == null) {throw new NullPointerException("definingContext == null");}
if (dexPath == null) {throw new NullPointerException("dexPath == null");}
if (optimizedDirectory != null) {if (!optimizedDirectory.exists()) {throw new IllegalArgumentException("optimizedDirectory doesn't exist: "
optimizedDirectory);}
if (!(optimizedDirectory.canRead()&& optimizedDirectory.canWrite())) {throw new IllegalArgumentException("optimizedDirectory not readable/writable: "
optimizedDirectory);}}
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();// save dexPath for BaseDexClassLoader//我们关注这个 makeDexElements 方法 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;}}
static class Element {//dex 文件为 null 时表示 jar/dex.jar 文件 private final File path;
//android 虚拟机文件在 Android 中的一个具体实现 private final DexFile dexFile;
private ClassPathURLStreamHandler urlHandler;private boolean initialized;
/**
Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
should be null), or a jar (in which case dexZipPath should denote the zip file).*/public Element(DexFile dexFile, File dexZipPath) {this.dexFile = dexFile;this.path = dexZipPath;}
public Element(DexFile dexFile) {this.dexFile = dexFile;this.path = null;}
public Element(File path) {this.path = path;this.dexFile = null;}
public Class<?> findClass(String name, ClassLoader definingContext,List<Throwable> suppressed) {//核心点,DexFilereturn dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed): null;}
/**
Constructor for a bit of backwards compatibility. Some apps use reflection into
internal APIs. Warn, and emulate old behavior if we can. See b/33399341.
@deprecated The Element class has been split. Use new Element constructors for
*/@Deprecatedpublic Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {System.err.println("Warning: Using deprecated Element constructor. Do not use internal"
" APIs, this constructor will be removed in the future.");if (dir != null && (zip != null || dexFile != null)) {throw new IllegalArgumentException("Using dir and zip|dexFile no longer"
" supported.");}if (isDirectory && (zip != null || dexFile != null)) {throw new IllegalArgumentException("Unsupported argument combination.");}if (dir != null) {this.path = dir;this.dexFile = null;} else {this.path = zip;this.dexFile = dexFile;}}...}
...//主要作用就是将 我们指定路径中所有文件转化为 DexFile,同时存到 Eelement 数组中//为什么要这样做?目的就是为了让 findClass 去实现 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) {if (file.isDirectory()) {//如果存在文件夹,查找文件夹内部查询 elements[elementsPos++] = new Element(file);//如果是文件} else if (file.isFile()) {String name = file.getName();DexFile dex = null;//判断是否是 dex 文件 if (name.endsWith(DEX_SUFFIX)) {// Raw dex file (not inside a zip/jar).try {//创建一个 DexFiledex = 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 {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);}}if (dex != null && isTrusted) {dex.setTrusted();}} else {System.logW("ClassLoader referenced unknown path: " + file);}}if (elementsPos != elements.length) {elements = Arrays.copyOf(elements, elementsPos);}return elements;}
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,Element[] elements)throws IOException {//判断可复制文件夹是否为 nullif (optimizedDirectory == null) {return new DexFile(file, loader, elements);} else {//如果不为 null,则进行解压后再创建 String optimizedPath = optimizedPathFor(file, optimizedDirectory);return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);}}
public Class<?> findClass(String name, List<Throwable> suppressed) {//遍历初始化好的 DexFile 数组,并由 Element 调用 findClass 方法去生成 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;}
上面的代码有点复杂,我摘取了其中一部分我们需要关注的点,便于我们进行分析:
在 BaseDexClassLoader 中,我们发现最终加载类的是由 DexPathList 来进行的,所以我们进入了 DexPathList 这个类中,我们可以发现 在初始化的时候,有一个关键方法需要我们注意 makeDexElements。而这个方法的主要作用就是将 我们指定路径中所有文件转化为 DexFile ,同时存到 Eelement 数组中。
而最开始调用的 DexPathList 中的 findClass() 反而是由 Element 调用的 findClass 方法,而 Emement 的 findClass 方法中实际上又是 DexFile 调用的 loadClassBinaryName 方法,所以带着这个疑问,我们进入 DexFile 这个类一查究竟。
DexFile
public final class DexFile {*If close is called, mCookie becomes null but the internal cookie is preserved if the closefailed so that we can free resources in the finalizer./@ReachabilitySensitiveprivate Object mCookie;
private Object mInternalCookie;private final String mFileName;...DxFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {mCookie = openDexFile(fileName, null, 0, loader, elements);mInternalCookie = mCookie;mFileName = fileName;//System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);}
//关注点在这里 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 {//这里调用了一个 JNI 层方法 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;}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,DexFile dexFile)throws ClassNotFoundException, NoClassDefFoundError;
我们从 loadClassBinaryName 方法中发现,调用了 defineClass 方法,最终又调用了 defineClassNative 方法,而 defineClassNative 方法是一个 JNI 层的方法,所以我们无法得知具体如何。但是我们思考一下,从开始的 BaseDexClassLoader 一直到现在的 DexFile,我们一直从入口找到了最底下,不难猜测,这个 defineClassNative 方法内部就是 C/C++帮助我们以字节码或者别的生成我们需要的 dex 文件,这也是最难的地方所在。
最后我们再用一张图来总结一下 Android 中类加载的过程。
在了解完上面的知识之后,我们来总结一下,Android 中热修复的原理?
Android 中既然已经有了 DexClassLoader 和 PathClassLoader,那么我在加载过程中直接替换我自己的 Dex 文件不就可以了,也就是先加载我自己的 Dex 文件不就行了,这样不就实现了热修复。
真的这么简单吗?热修复的难点是什么?
资源修复
代码修复
so 库修复
抱着这个问题,如何选用一个最合适的框架,是我们 Android 开发者必须要考虑的,下面我们就分析一下各方案的差别。
如何选择热修复框架?
目前市场上的热修复框架很多,从阿里热修复网站找了一个图来对比一下:
评论