热修复这一功能对很多公司的开发者来说都会有集成,因为测试不是万能的,万一线上遇到一点小bug,就会导致流量流失或者其他造成公司损失的问题,我自己项目中也集成了非常有名的Tinker热修复,具体怎么集成这里就不记录了,主要看官方文档
这里主要记录一下Tinker热修复的源流。
先做个热修复原理的总结,然后再手写实现热修复
-
APK从专业的角度来讲是一些Dex文件的集合,Classloader是专门管理Dex文件的类,在Classloader里面有个findClass方法,在activity或者其他类在加载的时候,会调用这个方法,去Dex文件里面找这个类,如果找到就把这个类放到容器DexElemengs[]里面里面装的是APK里面所有的类。
-
dex文件就是所有类的.dex的集合,里面装载了所有类的dex,热修复的思路就是获取到apk的私有路径,再去遍历所有目录,寻找出classes.dex文件,包括当前应用的dex集合也包括我们写入(已修复的)的dex集合。然后再把两个dex集合的数组拿出来,再合并,合并后的数组给到dexElements,由于我们把修复的dex放在数组的前面,未修复的在后面,所以类加载器在加载的时候会选择加载修复后的dex
-
热修复的原理就是Hook了ClassLoader .pathLish .dexElements[]因为ClassLoader的FindClass是通过遍历DexElements[]中的Dex来寻找类的,比如MainActivity出现bug,只需要找到其对应的Dex文件,然后替换即可。类加载器有个特点就是 如果先找到要找的类dex,就不会再去找了,因此如果一个dex文件里面两个main.dex,只会加载第一个,第二个不会再去处理,因此补丁修复的原理就是把补丁的dex放到前面,原来有bug没修复的dex放到后面
接下来是代码实现,主要是ClassLoader及其子类api的调用
package com.dhd.hotrepair;
import android.content.Context;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashSet;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
public class BugFixManager {
private static HashSet<File> loadedDex = new HashSet<>();
static {
loadedDex.clear();
}
public static void loadDex(Context context) {
if (context == null) {
return;
}
//获取当前上下文的私有路径 也就是Dex的文件目录
File fileDir = context.getDir("odex", Context.MODE_PRIVATE);
//通过私有路径获取到路径目录中所有文件的数据
File[] files = fileDir.listFiles();
//遍历
for (File file : files) {
//判断文件对象的集合是不是符合我们的条件
if (file.getName().startsWith("classes") || file.getName().endsWith(".dex")) {
loadedDex.add(file);
}
}
//创建一个目录,用来装载解压的文件
String optmizeDir = fileDir.getAbsolutePath() + File.separator + "opt_dex";
File fopt = new File(optmizeDir);
if (!fopt.exists()) {
fopt.mkdir();
}
//循环所有找到的Dex文件集合
for (File dex : loadedDex) {
//获取到系统的类加载器
DexClassLoader dexClassLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, context.getClassLoader());
//实例一个类加载器对象
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
//***********************通过反射获取系统的类加载器************************
try {
//获取到DexClassLoader类加载器的父类
Class<?> baseDexClazzloader = Class.forName("dalvik.system.BaseDexClassLoader");
//从父类中获取到pathList的成员变量
Field pathListField = baseDexClazzloader.getDeclaredField("pathList");
pathListField.setAccessible(true);
//获取成员变量对象的值,比如Main里面的有个私有变量a的值
Object pathListObject = pathListField.get(pathClassLoader);
Class<?> systemPathListClass = pathListObject.getClass();
//通过类对象获取里面的dexElements的成员变量
Field dexElementsField = systemPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
//获取到dexElements成员变量的值
Object systemElements = dexElementsField.get(pathListObject);
//*******************************创建自己的类加载器******************************
Class<?> myDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListField = baseDexClazzloader.getDeclaredField("pathList");
myPathListField.setAccessible(true);
Object myPathListObj = myPathListField.get(dexClassLoader);
Field mydexElementsField = myPathListObj.getClass().getDeclaredField("dexElements");
mydexElementsField.setAccessible(true);
Object myElements = mydexElementsField.get(myPathListObj);
//*******************************进行dex文件融合******************************
//获取到systemElements的类对象
Class<?> componentType = systemElements.getClass().getComponentType();
//得到系统(未修复的)的dexElements的长度
int systemLength = Array.getLength(systemElements);
int mylength = Array.getLength(systemElements);
//创建一个新的长度
int newSystemLength = systemLength + mylength;
//生成一个新的数组
Object newElementsArray = Array.newInstance(componentType, newSystemLength);
for (int x = 0; x < newSystemLength; x++) {
if (x < mylength) {
//将自己定义的数组(补丁dex数组)放到前面
Array.set(newElementsArray, x, Array.get(myElements, x));
} else {
//系统未修复bug的dex放到后面
Array.set(newElementsArray, x, Array.get(systemElements, x - mylength));
}
}
//将系统的数组和我们定义的数组融合之后,再放入系统的数组中
Field dexElements = pathListObject.getClass().getDeclaredField("dexElements");
dexElements.setAccessible(true);
dexElements.set(pathListObject, newElementsArray);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
然后修复类已经写好了,怎么调用呢,比如MainActivity里面有bug,可以如下方式调用
public class MainActivity extends AppCompatActivity {
private TextView mResult;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mResult = findViewById(R.id.tv);
}
public void calculate(View view) {
mResult.setText(BugTest.getResult(3, 0)+"");
}
//吧补丁的dex文件全部写入当前应用的私有路径下面,然后去私有路径下面拿到所有dex文件,然后再遍历,再融合
public void repair(View view) {
//获取apk的私有存储路径
File filesDir = this.getDir("odex", Context.MODE_PRIVATE);
//获取到没有bug的dex文件的名字
String name = "out.dex";
//创建一个修复好的dex文件存储路径
String filePth = new File(filesDir, name).getAbsolutePath();
//根据这个路径去创建一个新的file对象
File file = new File(filePth);
if (!file.exists()) {
file.delete();
}
InputStream is = null;
FileOutputStream os = null;
try {
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
os = new FileOutputStream(filePth);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
File f = new File(filePth);
if (f.exists()) {
Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
}
BugFixManager.loadDex(this);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
其中代码里面提到的out.dex文件是我们已经修复后打包的dex文件,通过AS的DeviceFileExplorer将这个out.dex上传到模拟器或者手机的sdcard路径下面,再找到这个路径,就可以实现dex的替换。
那Tinker中是如何替换有bug的dex文件的呢?
用过Tinker的都知道,Tinker有个后台管理,每次发布热补丁都要将patch_signed_7zip.apk上传到服务器,其实这个操作就是Tinker将我们上传的补丁包push到了手机的sdcard目录下面;推送完毕就是上面代码中的流程了