Android HotFix 热修复研究

Android的apk安装包,主要有dex二进制文件library库resources资源manifest清单文件,所以,热修复也将根据这些文件来研究分析。

dex二进制文件

通过在上一篇文件中的分析,可以知道,我们可以通过反射来改变BaseDexClassLoader中的pathList成员变量的dexElements成员变量,来达到动态修复的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class HotFix {
final static String TAG = "HotFix";
public static void hotFixDex(ClassLoader loader, List<File> additionalDexes,
File optimizedDirectory) {
try {
V19.hotFixDex(loader, additionalDexes, optimizedDirectory);
} catch (Exception e) {
e.printStackTrace();
}
}
private static final class V19 {
private static void hotFixDex(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory) {
/* loader是dalvik.system.BaseDexClassLoader的子类,
* 通过反射修改它的pathList(DexPathList)属性,来添加额外的dex文件入口;
*/
Field pathListField = findField(loader, "pathList");
// 获取到pathList
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
Object[] elements = makeDexElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions);
printElementDir(elements);
expandFieldArray(dexPathList, "dexElements", elements);
}
/**
* A wrapper around
* {@code private static final dalvik.system.DexPathList#makeDexElements}.
*/
private static Object[] makeDexElements(Object dexPathList,
ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
// 获取makePathElements方法
Method makeDexElements = findMethod(dexPathList,
"makePathElements", List.class, File.class, List.class);
makeDexElements.setAccessible(true);
return (Object[]) makeDexElements.invoke(null, files, optimizedDirectory,
suppressedExceptions);
}
private static void printElementDir(Object[] elements) {
for (int i = 0; i < elements.length; i++) {
try {
Field f = findField(elements[i], "dir");
f.setAccessible(true);
File file = (File) f.get(elements[i]);
Log.e(TAG, "dir = " + file.getAbsolutePath());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
// 通过反射查找成员变量
private static Field findField(Object instance, String name) {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Field field = clazz.getDeclaredField(name);
if (!field.isAccessible()) {
field.setAccessible(true);
}
return field;
} catch (NoSuchFieldException e) {
// ignore and search next
}
}
throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
}
// 通过反射查找方法
private static Method findMethod(Object instance, String name, Class<?>... parameterTypes)
throws NoSuchMethodException {
for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {
try {
Method method = clazz.getDeclaredMethod(name, parameterTypes);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException e) {
// ignore and search next
}
}
throw new NoSuchMethodException("Method " + name + " with parameters " +
Arrays.asList(parameterTypes) + " not found in " + instance.getClass());
}
private static void expandFieldArray(Object instance, String fieldName,
Object[] extraElements) {
Field jlrField = findField(instance, fieldName);
Object[] original = (Object[]) jlrField.get(instance);
Object[] combined = (Object[]) Array.newInstance(
original.getClass().getComponentType(), original.length + extraElements.length);
System.arraycopy(original, 0, combined, 0, original.length);
System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
jlrField.set(instance, combined);
}
}

参考系统源码: frameworks/multidex/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
boolean fixed = false;
private void hotFixDex() {
if (fixed)
return;
File opt = new File("/data/data/com.xxx.dede/code_cache");
ArrayList<File> additionList = new ArrayList<File>();
File target2 = new File("/data/data/com.xxx.dede/classes2.zip");
File target3 = new File("/data/data/com.xxx.dede/classes3.zip");
additionList.add(target2);
additionList.add(target3);
HotFix.hotFixDex(MainActivity.this.getClassLoader(), additionList, opt);
// 往当前的classLoader里面添加两个dex文件。
fixed = true;
}

注意:classes.dex 文件不要直接放到外部存储中,应该放到应用程序安装目录中,以防止dex文件攻击。

library库

library文件热加载和dex文件热加载基本类似,也是通过反射去改变BaseDexClassLoader的加载目录,以达到动态加载的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* dirs: so库文件所在目录
* loader: BaseDexClassLoader实例
*/
private static void hotFixLibrary(List<File> dirs, ClassLoader loader) {
Log.e(TAG, "changeLibPath = ");
try {
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
Field f = findField(dexPathList, "nativeLibraryPathElements");
f.setAccessible(true);
Object[] oldArray = (Object[]) f.get(dexPathList);
Method m = findMethod(dexPathList, "makePathElements",
List.class, File.class, List.class);
m.setAccessible(true);
List<IOException> suppressedExceptions = new ArrayList<IOException>();
Object[] newArray = (Object[]) m.invoke(null, dirs, null, suppressedExceptions);
Object[] libArray = (Object[]) Array.newInstance(oldArray
.getClass().getComponentType(), oldArray.length
+ newArray.length);
System.arraycopy(oldArray, 0, libArray, 0, oldArray.length);
System.arraycopy(newArray, 0, libArray, oldArray.length,
newArray.length);
Log.e(TAG, "libArray.length = " + libArray.length);
f.set(dexPathList, libArray);
} catch (Exception e) {
Log.e(TAG, "ex = " + e);
}
}
1
2
3
4
5
6
7
8
// so库存放目录
File folder = new File(getFilesDir(), "arm64-v8a");
folder.mkdirs();
List<File> libPaths = new ArrayList<File>();
libPaths.add(folder);
// 热加载library
HotFix.hotFixLibrary(getClassLoader(), libPaths);

注意:library库文件不同于dex文件,放在sdcard目录中也可以加载(强烈不建议这样做!!!),library文件必须放到应用程序文件目录中。

Link

resources资源

Activity的工作主要是由ContextImpl来完成的, 它在Activity中是一个叫做mBase的成员变量。Context中有如下两个抽象方法通过它们来获取资源,这两个抽象方法的真正实现在ContextImpl中。

1
2
3
4
5
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();

AssetManager中有一个私有方法addAssetPath(String assetPath),用于添加资源文件,可以是一个zip文件的地址,也可以是资源目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected void newResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = getResources();
Resources mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
return mResources;
}
  • 使用生成的Resources获取apk中的资源文件。
1
2
3
Resources resources = newResources();
//
Drawable drawable = resources.getDrawable(resources.getIdentifier("icon_zz", "drawable", "com.cf.splitapk1"));

参考:
Link1
Link2
Link3
Link4