这是我参与更文挑战的第1天,活动详情查看: 更文挑战java
首先,咱们须要持有如下几个问题:android
上面一共有7个问题,若是是新同窗的话,后面两条可能不会很了解,建议自行补课学习。因而最基本的5个问题,咱们必须明白,这是咱们每一个开发者学习一个新知识的基本须要作到的。git
其实简单来讲,热修复就是一种动态加载技术,好比你线上某个产品此时出现了bug:shell
传统流程:debug->测试->发布新版 ->用户安装(各平台审核时间不一,并且用户须要手动下载或者更新)数组
集成热修复状况下:dubug->测试->推送补丁->自动下载补丁修复 (用户不知状况,自动下载补丁并修复)缓存
对比下来,咱们不难发现,传统流程存在这几大弊端:服务器
上面三个缘由中,咱们主要来谈一下 Instant Run:markdown
Android Studio2.0时,新增了一个 Instant Run的功能,而各大厂的热修复方案,在代码,资源等方面的实现都是很大程度上参考了Instant Run的代码。因此能够说 Instant Run 是推动Android 热修复的主因。cookie
那 Instant Run内部是如何作到这一点呢?app
- 构建一个新的 AssetManager(资源管理框架),并经过反射调用这个 addAssetPath,把这个完整的新资源加入到 AssetManager中,这样就获得了一个含有全部新资源的 AssetManager.
- 找到全部以前引用到原有AssetManager的地方,经过反射,把引用出替换为新的AssetManager.
参考自 <深刻探索Android热修复技术原理>
咱们都知道热修复都至关于动态加载,那么动态加载到底动态在哪里了呢。
说到这个就躲不过一个关键点 ClassLoader(类加载器) ,因此咱们先从Java开始。
咱们都知道Java的类加载器有四种,分别为:
类加载过程以下:
过程: 加载-链接(验证-准备-解析)-初始化
加载
将类的信息(字节码)从文件中获取并载入到JVM的内存中
链接
验证:检查读入的结构是否符合JVM规范
准备:分配一个结构来存储类的信息
解析:将类的常量池中的全部引用改变成直接引用
初始化
执行静态初始化程序,把静态变量初始化成指定的值
其中用到的三个主要机制:
- 双亲委托机制
- 全盘负责机制
- 缓存机制
其实后面的两个机制都是主要从双亲委托机制延续而来。详细的Java类加载请参考个人另外一篇博客
在说明了Java 的ClassLoader以后,咱们接下来开始Android的ClassLoader,不一样于Java的是,Java中的ClassLoader能够加载 jar 文件和 Class文件,而Android中加载的是Dex文件,这就须要从新设计相关的ClassLoader类。因此Android 的ClassLoader 咱们会说的详细一点
在这里,顺便提一下,这里贴的代码版本是Android 9.0,在8.0之后,PathClassLoader和DexClassLoader并无什么区别,由于惟一的一个区别参数 optimizedDirectory已经被废弃。
首先是 loadClass,也就是咱们类加载的核心方法方法:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//查找当前类是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//查看父加载器是否加载过
c = parent.loadClass(name, false);
} else {
//若是没有加载过,调用根加载器加载,双亲委托模式的实现
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//找到根加载器依然为null,只能本身加载了
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
复制代码
这里有个问题,JVM双亲委托机制能够被打破吗?先保留疑问。
咱们主要去看他的 findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
复制代码
这个方法是一个null实现,也就是须要咱们开发者本身去作。
从上面基础咱们知道,在Android中,是有 PathClassLoader和 DexClassLoader,而它们又都继承与 BaseDexClassLoader,而这个BaseDexClassLoader又继承与 ClassLoader,并将findClass方法交给子类本身实现,因此咱们从它的两个子类 PathClassLoader和 DexClassLoader入手,看看它们是怎么处理的。
这里碍于Android Studio没法查看相关具体实现源码,因此咱们从源码网站上查询:
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// dexPath: 须要加载的文件列表,文件能够是包含了 classes.dex 的 JAR/APK/ZIP,也能够直接使用 classes.dex 文件,多个文件用 “:” 分割
// librarySearchPath: 存放须要加载的 native 库的目录
// parent: 父 ClassLoader
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
复制代码
由注释看能够发现PathClassLoader被用来加载本地文件系统上的文件或目录,由于它调用的 BaseDexClassLoader的第二个参数为null,即未传入优化后的Dex文件。
注意:Android 8.0以后,BaseClassLoader第二个参数为(optimizedDirectory)为null,因此DexClassLoader与PathClassLoader并没有区别
public class DexClassLoader extends BaseDexClassLoader {
// dexPath: 须要加载的文件列表,文件能够是包含了 classes.dex 的 JAR/APK/ZIP,也能够直接使用 classes.dex 文件,多个文件用 “:” 分割
// optimizedDirectory: 存放优化后的 dex,能够为空
// librarySearchPath: 存放须要加载的 native 库的目录
// parent: 父 ClassLoader
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
复制代码
DexClassLoader用来加载jar、apk,其实还包括zip文件或者直接加载dex文件,它能够被用来执行未安装的代码或者未被应用加载过的代码,也就是咱们修复过的代码。
注意:Android 8.0以后,BaseClassLoader第二个参数为(optimizedDirectory)为null,因此DexClassLoader与PathClassLoader并没有区别
从上面咱们能够看到,它们都继承于BaseDexClassLoader,而且它们真正的实现行为都是调用的父类方法,因此咱们来看一下BaseDexClassLoader.
public class BaseDexClassLoader extends ClassLoader {
private static volatile Reporter reporter = null;
//核心关注点
private final DexPathList pathList;
BaseDexClassLoader 构造函数有四个参数,含义以下:
// dexPath: 须要加载的文件列表,文件能够是包含了 classes.dex 的 JAR/APK/ZIP,也能够直接使用 classes.dex 文件,多个文件用 “:” 分割
// optimizedDirectory: 存放优化后的 dex,能够为空
// librarySearchPath: 存放须要加载的 native 库的目录
// parent: 父 ClassLoader
public 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);
}
//核心方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//异常处理
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//这里也只是一个中转,关注点在 DexPathList
Class 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.
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这个类一查究竟。
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文件不就好了,这样不就实现了热修复。
抱着这个问题,如何选用一个最合适的框架,是咱们Android开发者必需要考虑的,下面咱们就分析一下各方案的差异。
目前市场上的热修复框架不少,从阿里热修复网站找了一个图来对比一下:
平台 | Sophix | AndFix | Tinker | Qzone | Robust |
---|---|---|---|---|---|
即时生效 | yes | yes | no | no | yes |
性能损耗 | 较小 | 较小 | 较大 | 较大 | 较小 |
侵入式打包 | 无侵入式打包 | 无侵入式打包 | 依赖侵入式打包 | 依赖侵入式打包 | 依赖侵入式打包 |
Rom体积 | 较小 | 较小 | 较大 | 较小 | 较小 |
接入复杂度 | 傻瓜式接入 | 比较简单 | 复杂 | 比较简单 | 复杂 |
补丁包大小 | 较小 | 较小 | 较小 | 较大 | 通常 |
全平台支持 | yes | yes | yes | yes | yes |
类替换 | yes | yes | yes | yes | no |
so替换 | yes | no | yes | no | no |
资源替换 | yes | no | yes | yes | no |
简单划分就是3大巨头,阿里,腾讯,美团。并非谁支持的功能多就用谁,在接入方面咱们须要综合考虑。
详细的技术对比请参考 Android热修复技术选型——三大流派解析
以我我的的体验来讲吧:目前体验了Tinker和 Sophix
Tinker
Tinker的集成有点麻烦,我我的以为挺简单,并且补丁管理系统 TinkerPatch是收费的(有免费额度),补丁下发慢,大概须要5分钟的等待时间。
Tinker有一个免费版后台,Bugly,补丁管理是免费的,热修复用的Tinker,集成很那啥。。。em,建议多读官网教程看视频,由于有补丁上传监测,下发一个补丁须要5-10分钟等待生效,撤回补丁须要10分钟左右生效,并且一次可能不会生效,后台观察日志须要屡次才能够实现补丁撤回。(测试设备:小米5s Plus,Android 8.0)
最后总结:
优势:免费,简单
缺点:集成麻烦,出现问题没法第一时间获得解决方案,毕竟免费的理解一下
性能方法:须要冷启动以后才会生效
Sophix
官网教程详细,彻底傻瓜式,响应快,出现问题,解决效率高,毕竟花了钱的。
性能方面:冷启动+即时响应(有条件),
有点:功能最多,支持版本最多,解决问题快
缺点:付费
别的框架没有体验,也就不妄自评价了。关于以上方案的实现原理,你们能够点击Android热修复技术选型——三大流派解析,或者百度搜索。简单了解并不困难。
有了热修复,咱们就能够随心所欲了吗?
开始讲骚话:
并非,热修复受限于各类机型设备,并且也有失败的可能性,因此咱们开发者,对于补丁包一样也要抱有敬畏之心。
对于热修复一样也因为严格的过程,可是咱们平常开发至少要保证如下几点:
debug-> 打补丁包->开发设备测试->灰度下发(条件下发)->全量下发
下面针对我开发中遇到的问题,给出解决方案。
多渠道打包使用 美团 的一键打包方案。补丁包的话,其实并不会影响,由于补丁包通常改动的代码相同,但前提是须要保证咱们每一个渠道基准包没问题。若是改动代码有区别,那就须要针对这个渠道单独打补了。
Android开发通常集成了 Jenkins 或者别的自动化打包工具,咱们通常基准包都在 app/build/bakApk目录下,因此咱们能够经过编写 shell 命令,在jenkins中打包时,将生成的基准包移动到一个特定的文件夹便可。tinker,Sophix都是支持服务器后台的,因此咱们也能够经过自动化构建工具上传补丁包,若是相应的热修复框架不支持服务器管理的话,那么能够将补丁包上传的指定的文件夹,而后咱们app打开时,访问咱们的服务器接口下拉最新的补丁包,而后在service中合成。不过 **Tinker(bugly) **, Sophix 都是支持后台管理,因此具体使用那种方案咱们自行选择。
关于热修复的到这里就基本写完了,散散落落竟然写了这么多,其实难的不是热修复,而是Android中类加载的过程及一些基础相关知识,理解了这些,咱们才能真正明白那些优秀的框架究竟是怎样去修复的。
若是本文有帮到你的地方,不胜荣幸。若是有什么地方有错误或者疑问,也欢迎你们提出。