个人博客
http://www.milovetingting.cn
热修复
前言
最近在熟悉Android热修复方面的知识,纸上得来终觉浅,因此写了一个基于dex分包方案的简单Demo。
热修复是什么
在热修复技术出现前,对于已经发布的应用,如果遇到BUG,需要再次发布版本,用户需要更新应用版本,才可以解决问题。这种方式,存在新版本覆盖所需要的时间较长、需要全量更新的问题。而基于热修复技术,可以打包出修复的补丁包,推送给客户端或者客户端拉取,可以减少修复BUG所需时间、减少更新包大小。
热修复分类
基于Dex分包的热修复方案原理
在Android中,类加载器的结构如下:
加载Dex的流程
PathClassLoader与DexClassLoader都可以加载Dex,但最终都是通过他们的父类BaseDexClassLoader的findClass方法加载的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); }
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) { super(dexPath, null, librarySearchPath, parent); } }
public class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), librarySearchPath, parent); } }
|
BaseDexClassLoader中的findClass方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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; }
|
可以看到,BaseDexClassLoader中的findClass方法又是通过DexPathList的findClass方法来具体实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
private Element[] dexElements;
public Class findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { DexFile dex = element.dexFile;
if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; }
|
通过遍历dexElements中的元素来查找class,如果找到就不再往后查找。
1 2 3 4 5 6
| public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, }
|
dexElements是在构造方法中赋值的。
基于上面的分析,如果在dexElements数组的开始位置插入补丁dex,那么系统则会应用补丁包中的class,从而达到替换原来的class的效果。
由于dex在应用启动加载过后,不会再次重复加载。因此,这种方案只有在冷启动后,再次加载dex才会生效。
实现方案
在Application中,加载补丁dex,通过反射,将补丁dex插入到BaseDexClassLoader的属性:pathList中的dexElements数据开始位置。
实现代码:
1 2 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 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| public class App extends Application {
@Override public void onCreate() { super.onCreate(); try { PatchUtil.loadPatch(getApplicationContext(), "/sdcard/patch.dex"); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
public class PatchUtil {
public static void loadPatch(Context context, String patch) throws NoSuchFieldException, IllegalAccessException {
File patchFile = new File(patch); if (!patchFile.exists()) { return; }
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Field pathListField = pathClassLoader.getClass().getSuperclass().getDeclaredField( "pathList"); pathListField.setAccessible(true); Object pathListObject = pathListField.get(pathClassLoader);
Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements"); dexElementsField.setAccessible(true); Object dexElementsObject = dexElementsField.get(pathListObject);
File odex = context.getDir("odex", Context.MODE_PRIVATE); DexClassLoader dexClassLoader = new DexClassLoader(patch, odex.getAbsolutePath(), null, context.getClassLoader()); Field patchPathListField = dexClassLoader.getClass().getSuperclass().getDeclaredField( "pathList"); patchPathListField.setAccessible(true); Object patchPathListObject = patchPathListField.get(dexClassLoader);
Field patchDexElementsField = patchPathListObject.getClass().getDeclaredField( "dexElements"); patchDexElementsField.setAccessible(true); Object patchDexElementsObject = patchDexElementsField.get(patchPathListObject);
Class<?> elementClazz = dexElementsObject.getClass().getComponentType(); int dexElementsSize = Array.getLength(dexElementsObject); int patchDexElementsSize = Array.getLength(patchDexElementsObject); int newDexElementsSize = dexElementsSize + patchDexElementsSize; Object newDexElements = Array.newInstance(elementClazz, newDexElementsSize); for (int i = 0; i < newDexElementsSize; i++) { if (i < patchDexElementsSize) { Array.set(newDexElements, i, Array.get(patchDexElementsObject, i)); } else { Array.set(newDexElements, i, Array.get(dexElementsObject, i - patchDexElementsSize)); } }
dexElementsField.set(pathListObject, newDexElements); }
}
|
模拟发布应用中出现的BUG
1 2 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
| public class Foo {
public static void showToastShort(Context context, String text) { Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); }
}
public class MainActivity extends AppCompatActivity {
private Foo foo;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); foo = new Foo(); foo.showToastShort(getApplicationContext(), "出现BUG啦~~~"); } }
|
生成修复补丁
1 2 3 4 5 6 7 8 9 10 11 12
| public class MainActivity extends AppCompatActivity {
private Foo foo;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); foo = new Foo(); foo.showToastShort(getApplicationContext(), "BUG修复啦~~~"); } }
|
在Android Studio中,先Build-Clean Project,然后Build-Rebuild Project,在项目的对应模块的\build\intermediates\javac\debug\classes目录下,将生成的对应class复制出来,放在其它位置,如D:\HotFix,复制出来的class文件要放在对应的包结构下,如:
使用SDK中自带的dx工具生成dex文件
打开CMD窗口,定位到SDK中的build-tools文件夹中对应的版本,如28.0.0
也可以将这个路径加入到系统的环境变量中,就可以在任何位置调用dx命令
输入以下命令生成dex:–dex –output=D:\HotFix\patch.dex D:\HotFix\
这里为简化操作,只是简单将文件推到/sdcard/下,对应具体的业务,可以通过网络下载回来。这里由于用到了sdcard,6.0以上的设备,需要申请存储的运行时权限。
结束应用的进程,再次打开应用,就会加载补丁dex,运行修复后的代码。
应用补丁前
应用补丁后
CLASS_ISPREVERIFIED问题
这个问题只在Dalvik虚拟机之下出现(Android 4.4以下默认使用dalvik,5.0以后默认使用art虚拟机)。出现的原因:
apk在安装时,Dalvik虚拟机如果发现一个类A引用了其它类B,如果这个类B和类A位于同一个dex里,那么类A就会打上CLASS_ISPREVERIFIED标记。因此,如果类A引用了一个有BUG的类C,修复时用multidex热修复方案加载一个patch.dex,由于这个类已经被打上标记,而重启应用后,再次加载dex时,这个类C又位于另一个dex中,程序就会报错。
目前网上用的比较多的解决方案是,在类的构造函数中动态引入一个位于其它dex中的类,即字节码插桩。这块内容在下篇文章会展现。
源码地址:https://github.com/milovetingting/Samples/tree/master/HotFix