概述
热修复即”打补丁“,当一个app上线后,如果发现重大的bug,需要紧急修复。常规的做法是修复bug,然后重新打包,再上线到各个渠道。这种方式的成本高,效率低。
于是热修复技术应运而生,热修复技术一般的做法是应用启动的时候,主动去服务端查询是否有补丁包,有就下载下来,并在下一次启动的时候生效,这样就可以快速解决线上的紧急bug。
Android中的热修复包括:代码修复、资源修复、动态链接库修复。本文主要讲解代码修复。
热修复原理
 代码修复的原理主要是类替换。类的替换就涉及到ClassLoader的使用,Android中可用来动态加载代码的ClassLoader有PathClassLoader、DexClassLoader。
因为PathClassLoader在Dalvik虚拟机中只能用来加载已安装apk的类,而DexClassLoader在Dalvik和ART虚拟机中都能加载未安装apk或者dex中的类,所以热修复使用DexClassLoader来加载补丁包中的类。

ClassLoader.java
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 
 | public abstract class ClassLoader {
 
 
 public Class<?> loadClass(String name) throws ClassNotFoundException {
 return loadClass(name, false);
 }
 
 
 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 
 Class<?> c = findLoadedClass(name);
 if (c == null) {
 try {
 if (parent != null) {
 
 c = parent.loadClass(name, false);
 } else {
 
 c = findBootstrapClassOrNull(name);
 }
 } catch (ClassNotFoundException e) {
 
 
 }
 
 if (c == null) {
 
 c = findClass(name);
 }
 }
 return c;
 }
 
 
 protected Class<?> findClass(String name) throws ClassNotFoundException {
 throw new ClassNotFoundException(name);
 }
 
 
 
 }
 
 | 
DexClassLoader.java
| 12
 3
 4
 5
 6
 
 | public class DexClassLoader extends BaseDexClassLoader {
 public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
 super(dexPath, null, librarySearchPath, parent);
 }
 }
 
 | 
BaseDexClassLoader.java
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | public class BaseDexClassLoader extends ClassLoader {
 
 
 private final DexPathList pathList;
 
 @Override
 protected Class<?> findClass(String name) throws ClassNotFoundException {
 List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
 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;
 }
 
 }
 
 | 
DexPathList.java
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | final class DexPathList {
 
 
 private Element[] dexElements;
 
 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;
 }
 
 
 }
 
 | 
说明:
通过上面几个类的关系,和类的查找过程,我们可以发现最终是通过遍历DexPathList的dexElements数组进行类的查找加载,当找到类就返回;
dexElements数组的每个元素都代表着一个dex文件,所以为了让补丁包中要替换的类抢先于有bug的类被加载,就需要将补丁包dex插入到dexElements数组的头部。
热修复实战
生成补丁dex文件
Step1. 修改待修复的Title类;
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | package com.github.xch168.hotfixdemo;
 
 
 
 public class Title {
 
 public String getTitle() {
 return "hotfix title";
 }
 }
 
 | 
Step2. 编译Title类
| 1
 | javac com/github/xch168/hotfixdemo/Title.java
 | 
Step3. 用d8命令将Title.class打包成patch.dex;
d8命令的位置:<sdk-dir>/build-tools/<versionName>
命令执行完后就会生成一个classes.dex文件,将其重命名为patch.dex
Step4. 将patch.dex上传到七牛云的对象存储服务器上。
patch.dex在七牛对象存储服务器上的外链:http://pm3fh7vxn.bkt.clouddn.com/patch.dex

下载补丁包patch.dex
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 
 | public class HotfixHelper {
 public static void loadPatch(Context context, OnPatchLoadListener listener) {
 File patchFile = new File(context.getCacheDir() + "/patch.dex");
 if (patchFile.exists()) {
 patchFile.delete();
 }
 
 downloadPatch(patchFile, listener);
 }
 
 private static void downloadPatch(final File patchFile, final OnPatchLoadListener listener) {
 OkHttpClient client = new OkHttpClient();
 Request request = new Request.Builder()
 .url("http://pm3fh7vxn.bkt.clouddn.com/patch.dex")
 .get()
 .build();
 client.newCall(request)
 .enqueue(new Callback() {
 @Override
 public void onFailure(Call call, IOException e) {
 if (listener != null) {
 listener.onFailure();
 }
 e.printStackTrace();
 }
 
 @Override
 public void onResponse(Call call, Response response) throws IOException {
 if (response.code() == 200) {
 FileOutputStream fos = new FileOutputStream(patchFile);
 fos.write(response.body().bytes());
 fos.close();
 if (listener != null) {
 listener.onSuccess();
 }
 } else {
 if (listener != null) {
 listener.onFailure();
 }
 }
 }
 });
 }
 }
 
 | 
应用补丁包
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 
 | public class HotfixHelper {
 public static void applyPatch(Context context) {
 
 ClassLoader classLoader = context.getClassLoader();
 Class loaderClass = BaseDexClassLoader.class;
 try {
 
 Object hostPathList = ReflectUtil.getField(loaderClass, classLoader, "pathList");
 
 Object hostDexElement = ReflectUtil.getField(hostPathList.getClass(), hostPathList, "dexElements");
 
 File optimizeDir = new File(context.getCacheDir() + "/optimize");
 if (!optimizeDir.exists()) {
 optimizeDir.mkdir();
 }
 
 DexClassLoader patchClassLoader = new DexClassLoader(context.getCacheDir() + "/patch.dex", optimizeDir.getPath(), null, classLoader);
 
 Object patchPathList = ReflectUtil.getField(loaderClass, patchClassLoader, "pathList");
 
 Object patchDexElement = ReflectUtil.getField(patchPathList.getClass(), patchPathList, "dexElements");
 
 
 Object newDexElements = combineArray(hostDexElement, patchDexElement);
 
 ReflectUtil.setField(hostPathList.getClass(), hostPathList, "dexElements", newDexElements);
 } catch (NoSuchFieldException e) {
 e.printStackTrace();
 } catch (IllegalAccessException e) {
 e.printStackTrace();
 }
 }
 
 
 
 
 
 
 
 private static Object combineArray(Object hostElements, Object patchElements) {
 Class<?> componentType = hostElements.getClass().getComponentType();
 int i = Array.getLength(hostElements);
 int j = Array.getLength(patchElements);
 int k = i + j;
 Object result = Array.newInstance(componentType, k);
 
 System.arraycopy(patchElements, 0, result, 0, j);
 System.arraycopy(hostElements, 0, result, j, i);
 return result;
 }
 }
 
 | 
在Application中应用补丁包,这里是应用启动最新调用的地方。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | public class App extends Application {
 @Override
 protected void attachBaseContext(Context base) {
 super.attachBaseContext(base);
 
 if (HotfixHelper.hasPatch(base)) {
 HotfixHelper.applyPatch(base);
 }
 }
 }
 
 | 
测试流程
Step1.  启动应用,下载补丁包;
Step2. 杀掉应用,然后重启应用。

Demo地址:https://github.com/xch168/HotfixDemo
参考链接
- 一步步手动实现热修复(一)-dex文件的生成与加载
- 一步步手动实现热修复(二)-类的加载机制简要介绍
- 一步步手动实现热修复(三)-Class文件的替换
- Android热修复原理(一)热修复框架对比和代码修复
- Android 热修复,没你想的那么难
- HenCoderPlus