写点什么

浅谈 Android 热更新的前因后果 _ Android ,Android 面试基础知识

用户头像
Android架构
关注
发布于: 1 小时前

//核心关注点 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++] =


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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 开发者必须要考虑的,下面我们就分析一下各方案的差别。

如何选择热修复框架?

目前市场上的热修复框架很多,从阿里热修复网站找了一个图来对比一下:



用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
浅谈Android热更新的前因后果  _ Android ,Android面试基础知识