一、代码修复
1. 原理
原理是比较简单的,本质就是App在运行时,会有个dex数组,当要用到的class不存在时,就顺序从dex数组中寻找,只要找到就停止,因此可以把修复好的dex插入到这个dex数组最前面,那么找的时候就会找到修复好dex中的class,而不再是旧的有问题的class。
2. 动手写代码
public class App extends Application {
private static final String TAG = "App";
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
try {
ClassLoader classLoader = getClassLoader();
Log.i(TAG, "classLoader:" + classLoader);
Field pathListField = classLoader.getClass().getSuperclass().getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
Log.i(TAG, "把补丁复制到私有目录(省略)");
File patchFile = getDir("patch", Context.MODE_PRIVATE);
if (!patchFile.isDirectory()) {
patchFile.mkdirs();
}
Log.i(TAG, patchFile.getAbsolutePath());
// 加载补丁
DexClassLoader dexClassLoader = new DexClassLoader(
new File(patchFile, "patch.dex").getAbsolutePath(),
"/sdcard/odex/", null, getClassLoader());
Object[] patchDexElements = hookDexElements(dexClassLoader);
// 合并
Object[] newDexElements = (Object[]) Array.newInstance(dexElements[0].getClass(), dexElements.length + patchDexElements.length);
int index = 0;
for (Object element : patchDexElements) {
newDexElements[index++] = element;
}
for (Object element : dexElements) {
newDexElements[index++] = element;
}
// 设置回去
dexElementsField.set(pathList, newDexElements);
} catch (Throwable t) {
t.printStackTrace();
}
}
private static Object[] hookDexElements(ClassLoader classLoader) {
try {
Field pathListField = classLoader.getClass().getSuperclass().getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(classLoader);
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] dexElements = (Object[]) dexElementsField.get(pathList);
return dexElements;
} catch (Throwable t) {
t.printStackTrace();
}
return null;
}
}
这里分为几个步骤:
先通过反射拿到dexElements数组。
使用dx命令将class打包成补丁dex,如下:
dx --dex --output=patch.dex ./
注意要在包的最顶级目录中执行。
有了补丁dex后,通过网络下载到私有目录下。
使用DexClassLoader加载补丁dex,同样也拿到对应的dexElements数组。
两个dexElements数组合并,补丁的在前面,原有的在后面。
把合并好的新dexElements数组设置回ClassLoader中。
注意这里dex会有缓存,因此这个补丁操作必须尽早执行,如果已经用过某个class后再执行补丁操作,就不会生效了。
二、资源修复
1. 原理
ActivityThread中有个ArrayMap<String, WeakReference>变量mPackages,保存包名和LoadedApk的键值对,一般都是只有一个,我们只需要变量这些键值对,拿到对于的LoadedApk对象,然后修改其中的mResDir字符串的值为新的资源包的路径,后面在创建AssetManager时就会指定为新的资源路径,也就实现了资源的替换。
2. 动手写代码
private void doPatchResources() {
try {
File patchResDir = getDir("patch_res", Context.MODE_PRIVATE);
if (!patchResDir.isDirectory()) {
patchResDir.mkdirs();
}
File patchResFile = new File(patchResDir, "fix.apk");
if (!patchResFile.isFile()) {
// 没有补丁资源
return;
}
// printFields();
// 获取ContextImpl对象
Field mBaseField = this.getClass().getSuperclass().getSuperclass().getDeclaredField("mBase");
mBaseField.setAccessible(true);
Object mBase = mBaseField.get(this);
Log.i(TAG, "mBase:" + mBase);
// 获取ActivityThread对象
Field mMainThreadField = mBase.getClass().getDeclaredField("mMainThread");
mMainThreadField.setAccessible(true);
Object mMainThread = mMainThreadField.get(mBase);
Log.i(TAG, "mMainThread:" + mMainThread);
// 获取mPackages
Field mPackagesField = mMainThread.getClass().getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
ArrayMap<String, WeakReference<?>> mPackages = (ArrayMap<String, WeakReference<?>>) mPackagesField.get(mMainThread);
for (String key : mPackages.keySet()) {
Log.i(TAG, "key:" + key);
Object value = mPackages.get(key).get();
Log.i(TAG, "value:" + value);
Field mResDirField = value.getClass().getDeclaredField("mResDir");
mResDirField.setAccessible(true);
String mResDir = (String) mResDirField.get(value);
Log.i(TAG, "mResDir:" + mResDir);
// 修改mResDir的值
String newPath = patchResFile.getAbsolutePath();
mResDirField.set(value, newPath);
}
} catch (Throwable t) {
t.printStackTrace();
}
}
这里也是分为几个步骤:
打包好新的资源包fix.apk,手动放到私有目录下。
拿到Application对象的ContextImpl对象,再拿到其中的ActivityThread对象,一个进程对应有一个。
拿到ActivityThread对象中的mPackages,也就是ArrayMap<String, WeakReference>表。
遍历该表,获取到LoadedApk对象,修改其中的mResDir为fix.apk路径。
这样后面在创建AssetManager对象时,资源路径就是这个fix.apk的路径,获取到的资源也就是新的。