算是记录知识的博客,所以精简为主。
热修复分为两种:
Java层修复,以QZone,Tinker为代表。需要重新启动后才能完成修复。
Native层修复,以阿里系为代表。可以达成及时修复。
实现demo用的是Java层的修复。java层修复的原理是通过类加载器的机制来实现——双亲委派机制。
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
这上面是系统源码,可以看出在加载一个class的时候,他会先让父类去加载,父类如果可以加载,那就父类加载,父类加载不了,那就会抛出一个异常,让子类去加载。这么做的主要原因就是可以避免一个类被重复加载。所以你肯定遇到过使用第三方库,编译时提示你class重复这个问题。那么类替换的热修复方案就是,让fix类先被加载,这样有问题的class就不会被加载了。
在5.0之前会有一个CLASS_ISPREVERIFIED的错误。这个错误的解释是:
- 在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
- 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
- 被打上这个标记的类不能引用其他dex中的类,否则就会报图中的错误
- 在我们的Demo中,MainActivity和Cat本身是在同一个dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我们修复bug的时候却引用了另外一个dex的Cat.class,所以这里就报错了
- 而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED
上文是在其他博客中引用的。如果你的minSDKVersion大于5.0就不用考虑这件事儿了。上面解决方案是插桩,这里不做赘述。
首先需要把一个class变成dex,使用SDK下Tools自带的dx工具就行了。
dx --dex --output out.dex in.class
记住class需要放在相应的包名路径中。
得到了class之后,就用下面的工具类就行。
/**
* 修复的工具类
* on 2020/4/6
*/
public class FixUtil {
/**
* 讲dex存到自己的文件夹下
* @param context
* @param dirName 存放dex的目录
* @param dexName dex名字
*/
public static void loadDex(Context context,String dirName,String dexName) {
//构建存放dex目录
File dexDir = context.getDir(dirName, Context.MODE_PRIVATE);
//拿到dex文件拷贝
String dexPath = dexDir.getAbsoluteFile() + File.separator + dexName;
File dexFile = new File(dexPath);
if (!dexFile.exists()) {
try {
dexFile.createNewFile();
FixUtil.copyFiles(context, dexName, dexFile);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void FixDex(Context context,File dexDir){
//1.获得要修复的dexList
List<File> dexList = getDexList(context, dexDir);
try {
//2.找到pathList对象
Field pathListField = FixUtil.getField(context.getClassLoader(), "pathList");
Object pathList = pathListField.get(context.getClassLoader());
//3.获取系统的dexElements
Field dexElementsField = FixUtil.getField(pathList, "dexElements");
Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
//4.获取makeDexElements
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
Method makeDexElements = FixUtil.getMethod(pathList, "makeDexElements", List.class, File.class
, List.class, ClassLoader.class);
//5.加载修复的dexList
Object[] fixDexElements = (Object[]) makeDexElements.invoke(pathList, dexList, dexDir, suppressedExceptions,
context.getClassLoader());
//6.创建新的Elements数组
Object[] newDexElements = (Object[]) Array.newInstance(oldDexElements.getClass().getComponentType()
, oldDexElements.length + fixDexElements.length);
//7.合并Elements数组
System.arraycopy(fixDexElements, 0, newDexElements, 0, fixDexElements.length);
System.arraycopy(oldDexElements, 0, newDexElements, fixDexElements.length, fixDexElements.length);
//8.将系统的Elements重新设置
dexElementsField.set(pathList, newDexElements);
Log.d("TAG", "修复完成");
} catch (Exception e) {
Log.d("TAG", "修复失败" + e.getMessage());
e.printStackTrace();
}
}
/**
* 获取文件夹下所有.dex文件
* @param context
* @param dexDir
* @return
*/
public static List<File> getDexList(Context context, File dexDir) {
File[] files = dexDir.listFiles();
List<File> fileList = new ArrayList<>();
for (File file : files) {
if (file.getName().endsWith(".dex")) {
fileList.add(file);
}
}
return fileList;
}
/**
* 将文件拷贝到私有目录下
* @param context
* @param fileName
* @param desFile
*/
public static void copyFiles(Context context, String fileName, File desFile) {
InputStream in = null;
OutputStream out = null;
try {
in = context.getAssets().open(fileName);
out = new FileOutputStream(desFile.getAbsolutePath());
byte[] bytes = new byte[1024];
int len = 0;
while ((len = in.read(bytes)) != -1)
out.write(bytes, 0, len);
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null)
in.close();
if (out != null)
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 反射属性
* @param object
* @param name
* @return
* @throws NoSuchFieldException
*/
public static Field getField(Object object, String name) throws NoSuchFieldException {
Class<?> aClass = object.getClass();
while (aClass != null) {
try {
Field declaredField = aClass.getDeclaredField(name);
if (!declaredField.isAccessible()) {
declaredField.setAccessible(true);
}
return declaredField;
} catch (NoSuchFieldException e) {
e.printStackTrace();
aClass = aClass.getSuperclass();
}
}
throw new NoSuchFieldException("找不到对应属性");
}
/**
* 反射方法
* @param object
* @param name
* @param param
* @return
* @throws NoSuchMethodException
*/
public static Method getMethod(Object object, String name, Class... param) throws NoSuchMethodException {
Class<?> aClass = object.getClass();
while (aClass != null) {
try {
Method declaredField = aClass.getDeclaredMethod(name, param);
if (!declaredField.isAccessible()) {
declaredField.setAccessible(true);
}
return declaredField;
} catch (NoSuchMethodException e) {
e.printStackTrace();
aClass = aClass.getSuperclass();
}
}
throw new NoSuchMethodException("找不到对应属性");
}
}
我将dex存放在Asset文件夹下模拟的网络加载,如果是网络加载的话替换下这块就行。然后记住在Application里面去加载你存放dex文件夹下面的所有fix文件。为什么要重新启动才能加载?因为你没法去把一个已经加载的class给卸载咯。还有记住别开混淆!