个人博客
热修复
前言
最近在熟悉Android热修复方面的知识,纸上得来终觉浅,因此写了一个基于dex分包方案的简单Demo。
热修复是什么
在热修复技术出现前,对于已经发布的应用,如果遇到BUG,需要再次发布版本,用户需要更新应用版本,才可以解决问题。这种方式,存在新版本覆盖所需要的时间较长、需要全量更新的问题。而基于热修复技术,可以打包出修复的补丁包,推送给客户端或者客户端拉取,可以减少修复BUG所需时间、减少更新包大小。
热修复分类
基于Dex分包的热修复方案原理
在Android中,类加载器的结构如下:
加载Dex的流程
PathClassLoader与DexClassLoader都可以加载Dex,但最终都是通过他们的父类BaseDexClassLoader的findClass方法加载的
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方法
//BaseDexClassLoader中的代码
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方法来具体实现的
//DexPathList中的代码
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,如果找到就不再往后查找。
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数据开始位置。
实现代码:
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 {
/**
* 加载patch
*
* @param context
* @param patch
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public static void loadPatch(Context context, String patch) throws NoSuchFieldException,
IllegalAccessException {
//如果patch不存在,直接返回
File patchFile = new File(patch);
if (!patchFile.exists()) {
return;
}
//获取系统的PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//获取BaseDexClassLoader中DexPathList类型的属性:pathList
Field pathListField = pathClassLoader.getClass().getSuperclass().getDeclaredField(
"pathList");
pathListField.setAccessible(true);
Object pathListObject = pathListField.get(pathClassLoader);
//获取DexPathList中Element[]类型的dexElements
Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object dexElementsObject = dexElementsField.get(pathListObject);
//设置optimizedDirectory
File odex = context.getDir("odex", Context.MODE_PRIVATE);
//创建自定义的DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(patch, odex.getAbsolutePath(), null,
context.getClassLoader());
//获取BaseDexClassLoader中DexPathList类型的属性:pathList
Field patchPathListField = dexClassLoader.getClass().getSuperclass().getDeclaredField(
"pathList");
patchPathListField.setAccessible(true);
Object patchPathListObject = patchPathListField.get(dexClassLoader);
//获取DexPathList中Element[]类型的dexElements
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));
}
}
//替换原来的dexElements
dexElementsField.set(pathListObject, newDexElements);
}
}
模拟发布应用中出现的BUG
public class Foo {
/**
* 显示Toast
*
* @param context
* @param text
*/
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啦~~~");
}
}
生成修复补丁
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,在项目的对应模块的uildintermediatesjavacdebugclasses目录下,将生成的对应class复制出来,放在其它位置,如D:HotFix,复制出来的class文件要放在对应的包结构下,如:
使用SDK中自带的dx工具生成dex文件
打开CMD窗口,定位到SDK中的build-tools文件夹中对应的版本,如28.0.0
也可以将这个路径加入到系统的环境变量中,就可以在任何位置调用dx命令
输入以下命令生成dex:--dex --output=D:HotFixpatch.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