修复原理:
首先要了解 android 加载classes.dex文件的流程或原理。
1、android 如何加载classes.dex文件的?
// 用来加载apk 的dex文件:PathClassLoader
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;
}
通过了解PathClassLoader 加载dex文件得知,将修复好的dex文件插入到dexElements集合中,
根据类的加载机制,一个类只能被加载一次,所以当虚拟机加载 修复好的dex文件时,就会加载
修复好的类,而有问题的类将不被加载。因此达到修复的目的。
实现步骤:
1: 总网络下载补丁包,在app启动的时候加载补丁包,就到达了修复有问题的类的目的。
这里 用本地拷贝的方式来模拟从网络下载。将补丁包放在assets资源目录下.
2:由于PathClassLoader 和 DexClassLoader 都继承自BaseClassLoader,所以这里 通过反射拿到
BaseClassLoader中的DexPathList pathList 。
3: 拿到DexPathList pathList对象后,再获取pathList中的存放dex的数组 Element[] dexElements。
/** list of dex/resource (class path) elements */
private final Element[] dexElements;
4:通过DexClassLoader 拿到补丁包中dex文件的 pathList,最终拿到补丁包中的Element[] dexElements。
5:用补丁包中的Element[] dexElements 替换 app中的Element[] dexElements。
6:重新启动app即可
完整实现:
崩溃代码所在类:将本类代码修复后 重新打包成dex文件,放入assets目录下 来测试。
//生成新的 classes2.dex
//dx --dex --output=D:\classes2.dex class路径(包含完整包名)
package example.nzh.om.myhandler.hotfix;
public class CommonUtil {
public static String getStr() {
String[] array = {"123", "456", "789"};
return array[3];
}
}
修复方法:onclick
package example.nzh.om.myhandler.hotfix;
import android.app.Activity;
import android.content.Context;
import android.net.http.HttpResponseCache;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import dalvik.system.BaseDexClassLoader;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import example.nzh.om.myhandler.R;
public class HotFixTestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hot_fix_test);
}
public void test(View view) {
String s = CommonUtil.getStr();
if (!TextUtils.isEmpty(s)) {
Toast.makeText(this, s, Toast.LENGTH_LONG).show();
}
}
// 存放修复好的dex
HashSet<File> fixDex = new HashSet<>();
public void fix(View view) {
//1:将下载好的classes.dex修复补丁包 拷贝到 /data/data/packageName/odex 目录。
try {
String testFile = "classes2.dex";
String newDexDir = "odex";
String dir = File.separator + "data" +
File.separator + "data" +
File.separator + "example.nzh.om.myhandler" +
File.separator + newDexDir;
File dexDir = new File(dir);
if (!dexDir.exists()) {
dexDir.mkdir();
}
InputStream in = getAssets().open(testFile);
FileOutputStream fos = new FileOutputStream(dir + File.separator + testFile);
Toast.makeText(this, dir + File.separator + testFile, Toast.LENGTH_LONG).show();
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.flush();
fos.close();
//?2:遍历该目录下的所有修复好的dex文件
File dir2 = dexDir;//getApplicationContext().getDir(newDexDir, Context.MODE_PRIVATE);
File[] files = dir2.listFiles();
for (File file : files) {
if (file.getName().startsWith("classes") && file.getName().endsWith(".dex")) {
fixDex.add(file);
}
}
if (fixDex.size() == 0) {
Toast.makeText(this, "修复包拷贝失败!", Toast.LENGTH_SHORT).show();
return;
} else {
Toast.makeText(this, "修复包拷贝成功!", Toast.LENGTH_SHORT).show();
}
//2.1 加载应用程序的dex,通过( 用context.get---> PathClassLoader)
// 得到的classloader就是PathClassLoader
//2.2 加载修复后的dex文件 用new DexClassLoader
//2.3 合并两个文件:反射拿到baseClassLoader中的pathList,再从DexPathList中反射获取Element[]
// 最终替换Element[] elements变量。
// 修复:
PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader();
String optimizedDir = dir2.getAbsolutePath() + File.separator + "optimized_dir";
File opt = new File(optimizedDir);
if (!opt.exists()) {
opt.mkdir();
}
for (File file : fixDex) {
DexClassLoader dexClassLoader = new DexClassLoader(file.getAbsolutePath()
, optimizedDir
, null
, this.getClassLoader());
//反射第一步:拿到pathList对象(BaseClassLoader-->pathList(DexPathList))
Class baseClassLoaderCls = Class.forName("dalvik.system.BaseDexClassLoader");
String feildNamePathList = "pathList";
Object dexObj = reflectField(dexClassLoader, baseClassLoaderCls, feildNamePathList);//修复后的dex 的DexPathList
Object pathObj = reflectField(pathClassLoader, baseClassLoaderCls, feildNamePathList);// 源程序的dex的DexPathList
//反射第二步:得到DexPathList-->Element[]
String feildNameElement = "dexElements";
//修复后的ClassLoader里面的Elements 数据
Object fixedDexArray = reflectField(dexObj, dexObj.getClass(), feildNameElement);
//源程序中的ClassLoader里面的Elements 数据
Object pathDexArray = reflectField(pathObj, pathObj.getClass(), feildNameElement);
//合并数组
Object fianlDex = combineArray(fixedDexArray, pathDexArray);
//更换DexPathList中的Element[] elments
setFeild(pathObj, pathObj.getClass(), "dexElements", fianlDex);
}
//生成新的 classes2.dex
//dx --dex --output=D:\classes2.dex class路径(包含完整包名)
Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();
}
}
//合并数组,将修复dex 插入数组最前面。
public Object combineArray(Object arrayNew, Object arrayOld) {
int i = Array.getLength(arrayNew);
int j = i + Array.getLength(arrayOld);
//复制数组,长度为两个数组长度之和。类型和新的数组一样。
Object result = Array.newInstance(arrayNew.getClass().getComponentType(), j);
for (int k = 0; k < j; k++) {
if (k < i) {
//新数组的0-i个元素 插入新的数组数据
Array.set(result, k, Array.get(arrayNew, k));
} else {
// i-end 插入旧数组的数据
Array.set(result, k, Array.get(arrayOld, k - i));
}
}
return result;
}
/**
* 反射成员变量
*
* @param obj
* @param clazz
* @param fieldName
*/
public Object reflectField(Object obj, Class clazz, String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
public void setFeild(Object obj, Class clazz, String fieldName, Object value) throws Exception {
Field f = clazz.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(obj, value);
}
}
最后使用:Manifest.xml 引用MyApplication
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
}
@Override
protected void attachBaseContext(Context base) {
MultiDex.install(base);
super.attachBaseContext(base);
}
}