热修复原理分析

什么是热修复


热修复是指在用户无感知情况下对已安装的app使用补丁进行紧急修复。优点,效率高,用户体验好。

热修复框架


android热修复技术主要可以分为两类:
一类是利用java hook的技术来替换要修复的方法。代表有阿里的DeXposedAndfix
一类是利用java类加载机制优先返回修复的类。代表有TinkerHotFixNuwaRocooFixRobust
这两类都有着自己的优缺点,事实上从来都没有最好的方案,只有最适合自己的。

热修复原理

热修复实现的利用了Java的类加载机制,关键点是dexElments,代码如下:
  1. /**
  2. * Finds the named class in one of the dex files pointed at by
  3. * this instance. This will find the one in the earliest listed
  4. * path element. If the class is found but has not yet been
  5. * defined, then this method will define it in the defining
  6. * context that this instance was constructed with.
  7. *
  8. * @param name of class to find
  9. * @param suppressed exceptions encountered whilst finding the class
  10. * @return the named class or {@code null} if the class is not
  11. * found in any of the dex files
  12. */
  13. public Class findClass(String name, List<Throwable> suppressed) {
  14. for (Element element : dexElements) {
  15. DexFile dex = element.dexFile;
  16. if (dex != null) {
  17. Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
  18. if (clazz != null) {
  19. return clazz;
  20. }
  21. }
  22. }
  23. if (dexElementsSuppressedExceptions != null) {
  24. suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
  25. }
  26. return null;
  27. }
寻找大致过程PathClassLoader ->BaseDexClassLoader.findClass(String name)->PathList.findClass(name, suppressedExceptions).
大致相关的5个类:
PathClassLoader 用来加载应用程序的dex
  1. /**
  2. * Provides a simple {@link ClassLoader} implementation that operates on a list
  3. * of files and directories in the local file system, but does not attempt to
  4. * load classes from the network. Android uses this class for its system class
  5. * loader and for its application class loader(s).
  6. */
  7. public class PathClassLoader extends BaseDexClassLoader {

DexClassLoader  这个类是可以用来从.jar文件和.apk文件中加载classed.dex。(必须要在应用程序的目录下面。最终是加载jar、apk文件里的dex文件)
  1. /**
  2. * A class loader that loads classes from {@code .jar} and {@code .apk} files
  3. * containing a {@code classes.dex} entry. This can be used to execute code not
  4. * installed as part of an application.
  5. *
  6. * <p>This class loader requires an application-private, writable directory to
  7. * cache optimized classes. Use {@code Context.getCodeCacheDir()} to create
  8. * such a directory: <pre> {@code
  9. * File dexOutputDir = context.getCodeCacheDir();
  10. * }</pre>
  11. *
  12. * <p><strong>Do not cache optimized classes on external storage.</strong>
  13. * External storage does not provide access controls necessary to protect your
  14. * application from code injection attacks.
  15. */
  16. public class DexClassLoader extends BaseDexClassLoader {

BaseDexClassLoader PathClassLoader和DexClassLoader继承这个类。BaseDexClassLoader查找类的方法:
  1. /**
  2. * Base class for common functionality between various dex-based
  3. * {@link ClassLoader} implementations.
  4. */
  5. public class BaseDexClassLoader extends ClassLoader {
  6. private final DexPathList pathList;
  7. //.... code
  8. @Override
  9. protected Class<?> findClass(String name) throws ClassNotFoundException {
  10. List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
  11. Class c = pathList.findClass(name, suppressedExceptions);
  12. if (c == null) {
  13. ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
  14. for (Throwable t : suppressedExceptions) {
  15. cnfe.addSuppressed(t);
  16. }
  17. throw cnfe;
  18. }
  19. return c;
  20. }
findClass()方法中用到了pathList.findClass(name),附DexPathList类代码
  1. final class DexPathList {
  2. /**
  3. * List of dex/resource (class path) elements.
  4. * Should be called pathElements, but the Facebook app uses reflection
  5. * to modify 'dexElements' (http://b/7726934).
  6. */
  7. private Element[] dexElements;
  8. /**
  9. * Finds the named class in one of the dex files pointed at by
  10. * this instance. This will find the one in the earliest listed
  11. * path element. If the class is found but has not yet been
  12. * defined, then this method will define it in the defining
  13. * context that this instance was constructed with.
  14. *
  15. * @param name of class to find
  16. * @param suppressed exceptions encountered whilst finding the class
  17. * @return the named class or {@code null} if the class is not
  18. * found in any of the dex files
  19. */
  20. public Class findClass(String name, List<Throwable> suppressed) {
  21. for (Element element : dexElements) {
  22. DexFile dex = element.dexFile;
  23. if (dex != null) {
  24. Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
  25. if (clazz != null) {
  26. return clazz;
  27. }
  28. }
  29. }
  30. if (dexElementsSuppressedExceptions != null) {
  31. suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
  32. }
  33. return null;
  34. }

DexFile.loadClassBinaryName方法:
  1. public final class DexFile {
  2. /**
  3. * See {@link #loadClass(String, ClassLoader)}.
  4. *
  5. * This takes a "binary" class name to better match ClassLoader semantics.
  6. *
  7. * @hide
  8. */
  9. public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
  10. return defineClass(name, loader, mCookie, this, suppressed);
  11. }
  12. private static Class defineClass(String name, ClassLoader loader, Object cookie,
  13. DexFile dexFile, List<Throwable> suppressed) {
  14. Class result = null;
  15. try {
  16. result = defineClassNative(name, loader, cookie, dexFile);
  17. } catch (NoClassDefFoundError e) {
  18. if (suppressed != null) {
  19. suppressed.add(e);
  20. }
  21. } catch (ClassNotFoundException e) {
  22. if (suppressed != null) {
  23. suppressed.add(e);
  24. }
  25. }
  26. return result;
  27. }
  28. private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
  29. DexFile dexFile)
  30. throws ClassNotFoundException, NoClassDefFoundError;
  31. }

类所在路径:
热修复原理分析
总结:一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。(出自安卓App热补丁动态修复技术介绍)

热修复原理:
ClassLoader加载类会遍历dexElments数组,从dex文件中不断寻找你要的class,找到后立即返回class,后面的dex文件不再读取。热修复就是把有问题class打包成fixDex文件,插入dexElements靠前的位置,让修复的class优先被找到。

手动实现热修复

1步:通过使用dx.bat将修复的class打包成fix.dex。
命令 : dx --dex --output=E:\patch\dex\fix.dex  E:\patch\cls
 --output=E:\patch\dex\fix.dex dex输出路径
E:\patch\cls 打包指定目录下面的class字节文件(注意要包括全路径的文件夹,也可以有多个class)

2歩:将fix.dex拷贝到SD卡目录下(模拟fix.dex已从服务器上下载到SD卡)

3歩:将fix.dex文件搬到应用目录下,如/data/data/pkgName/odex/

4歩:通过反射将fix.dex添加到dexElements比较靠前的位置(要在bug class所在的dex前面)。
这里借助工具类FixDexUtils.

5歩:自定义MyApplication,onCreate方法中调用FixDexUtils.loadFixedDex(this);

FixDexUtils代码:
  1. public class FixDexUtils {
  2. private static final String DEX_DIR = "odex";
  3. private static HashSet<File> loadedDex = new HashSet<File>();
  4. static {
  5. loadedDex.clear();
  6. }
  7. public static void loadFixedDex(Context context) {
  8. if (context == null) {
  9. return;
  10. }
  11. //遍历所有的修复的dex
  12. File fileDir = context.getDir(DEX_DIR, Context.MODE_PRIVATE);
  13. File[] listFiles = fileDir.listFiles();
  14. for (File file : listFiles) {
  15. if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
  16. loadedDex.add(file);
  17. }
  18. }
  19. //合并之前的dex
  20. doDexInject(context, fileDir, loadedDex);
  21. }
  22. private static void doDexInject(final Context appContext, File filesDir, HashSet<File> loadedDex) {
  23. String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
  24. File fopt = new File(optimizeDir);
  25. if (!fopt.exists()) {
  26. fopt.mkdirs();
  27. }
  28. //1.加载应用程序的dex
  29. //2.加载指定的修复的dex文件。
  30. //3.合并
  31. try {
  32. PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
  33. for (File dex : loadedDex) {
  34. //2.加载指定的修复的dex文件。
  35. DexClassLoader classLoader = new DexClassLoader(
  36. dex.getAbsolutePath(),
  37. fopt.getAbsolutePath(),
  38. null,
  39. pathLoader
  40. );
  41. ///通过反射获得classLoader、pathLoader的pathList
  42. Object dexObj = getPathList(classLoader);
  43. Object pathObj = getPathList(pathLoader);
  44. //通过反射获得classLoader、pathLoader的DexElements
  45. Object mDexElementsList = getDexElements(dexObj);
  46. Object pathDexElementsList = getDexElements(pathObj);
  47. //将classLoader、pathLoader的DexElements合并
  48. Object dexElements = combineArray(mDexElementsList, pathDexElementsList);
  49. //重新给pathLoader的PathList里面的dexElements赋值
  50. setField(pathObj, pathObj.getClass(), "dexElements", dexElements);
  51. }
  52. } catch (Exception e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. private static void setField(Object obj, Class<?> cl, String field, Object value) throws Exception {
  57. Field localField = cl.getDeclaredField(field);
  58. localField.setAccessible(true);
  59. localField.set(obj, value);
  60. }
  61. private static Object getField(Object obj, Class<?> cl, String field)
  62. throws Exception {
  63. Field localField = cl.getDeclaredField(field);
  64. localField.setAccessible(true);
  65. return localField.get(obj);
  66. }
  67. //反射获得BaseDexClassLoader的pathList
  68. private static Object getPathList(Object baseDexClassLoader) throws Exception {
  69. return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
  70. }
  71. //反射DexPathList的dexElements
  72. private static Object getDexElements(Object obj) throws Exception {
  73. return getField(obj, obj.getClass(), "dexElements");
  74. }
  75. /**
  76. * 两个数组合并
  77. *
  78. * @param arrayLhs
  79. * @param arrayRhs
  80. * @return
  81. */
  82. private static Object combineArray(Object arrayLhs, Object arrayRhs) {
  83. Class<?> localClass = arrayLhs.getClass().getComponentType();
  84. int i = Array.getLength(arrayLhs);
  85. int j = i + Array.getLength(arrayRhs);
  86. Object result = Array.newInstance(localClass, j);
  87. for (int k = 0; k < j; ++k) {
  88. if (k < i) {
  89. Array.set(result, k, Array.get(arrayLhs, k));
  90. } else {
  91. Array.set(result, k, Array.get(arrayRhs, k - i));
  92. }
  93. }
  94. return result;
  95. }
  96. }



参考

Android dex分包方案
安卓App热补丁动态修复技术介绍

微信Android热补丁实践演进之路

Alibaba-AndFix Bug热修复框架原理及源码解析

Android热补丁之AndFix原理解析

微信Tinker的一切都在这里,包括源码(一)

Android 热修复Nuwa的原理及Gradle插件源码解析

Android中热修复框架Robust原理解析+并将框架代码从"闭源"变成"开源"(下篇)