DexPathList
final class DexPathList { //文件后缀 private static final String DEX_SUFFIX = ".dex"; private static final String zipSeparator = "!/"; ** class definition context */ private final ClassLoader definingContext; //内部类 Element private 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) { //核心点,DexFile return 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 * classes and resources, and NativeLibraryElement for the library * search path. */ @Deprecated public 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 { //创建一个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 { 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 { //判断可复制文件夹是否为null if (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 close failed so that we can free resources in the finalizer. / @ReachabilitySensitive private 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开发者必须要考虑的,下面我们就分析一下各方案的差别。
如何选择热修复框架?
目前市场上的热修复框架很多,从阿里热修复网站找了一个图来对比一下:
简单划分就是3大巨头,阿里,腾讯,美团。并不是谁支持的功能多就用谁,在接入方面我们需要综合考虑。
以我个人的体验来说吧:目前体验了Tinker和 Sophix
Tinker
Tinker的集成有点麻烦,我个人觉得挺简单,而且补丁管理系统 TinkerPatch是收费的(有免费额度),补丁下发慢,大概需要5分钟的等待时间。
Tinker有一个免费版后台,Bugly,补丁管理是免费的,热修复用的Tinker,集成很那啥。。。em,建议多读官网教程看视频,因为有补丁上传监测,下发一个补丁需要5-10分钟等待生效,撤回补丁需要10分钟左右生效,而且一次可能不会生效,后台观察日志需要多次才可以实现补丁撤回。(测试设备:小米5s Plus,Android 8.0)
最后总结:
优点:免费,简单
缺点:集成麻烦,出现问题无法第一时间得到解决方案,毕竟免费的理解一下
性能方法:需要冷启动之后才会生效
Sophix
官网教程详细,完全傻瓜式,响应快,出现问题,解决效率高,毕竟花了钱的。
性能方面:冷启动+即时响应(有条件),
有点:功能最多,支持版本最多,解决问题快
缺点:付费
热修复的注意事项
有了热修复,我们就可以为所欲为了吗?
并不是,热修复受限于各种机型设备,而且也有失败的可能性,所以我们开发者,对于补丁包同样也要抱有敬畏之心。
对于热修复同样也由于严格的过程,但是我们日常开发至少要保证以下几点:
debug-> 打补丁包->开发设备测试->灰度下发(条件下发)->全量下发
下面针对我开发中遇到的问题,给出解决方案。
热修复与多渠道
多渠道打包使用 美团 的一键打包方案。补丁包的话,其实并不会影响,因为补丁包一般改动的代码相同,但前提是需要保证我们每个渠道基准包没问题。如果改动代码有区别,那就需要针对这个渠道单独打补了。
自动化构建与热修复
Android开发一般集成了 Jenkins 或者别的自动化打包工具,我们一般基准包都在 app/build/bakApk 目录下,所以我们可以通过编写 shell 命令,在jenkins中打包时,将生成的基准包移动到一个特定的文件夹即可。tinker,Sophix都是支持服务器后台的,所以我们也可以通过自动化构建工具上传补丁包,如果相应的热修复框架不支持服务器管理的话,那么可以将补丁包上传的指定的文件夹,然后我们app打开时,访问我们的服务器接口下拉最新的补丁包,然后在service中合成。不过 Tinker(bugly) , Sophix 都是支持后台管理,所以具体使用那种方案我们自行选择。
关于热修复的到这里就基本写完了,散散落落居然写了这么多,其实难的不是热修复,而是Android中类加载的过程及一些基础相关知识,理解了这些,我们才能真正明白那些优秀的框架到底是怎样去修复的。
如果本文有帮到你的地方,不胜荣幸。如果有什么地方有错误或者疑问,也欢迎大家提出。