0%

Android插件化及热修复

Java类加载器

每个类有自己的类加载器,用户也可以自定义类加载器,同一个类文件被不同加载器加载后他们也不是同一个类,使用 isInstance() 等方法将会得到 false。Java类加载机制相关见:Java类加载机制

Android类加载器

概述

Android_ClassLoader_UML

简单介绍一下部分类加载器:

  • ClassLoader: 抽象类,其中定义了 ClassLoader 的主要功能,子类重写 findClass 方法即可。
  • BootClassLoader: 在 ClassLoader.java 中,使用默认修饰符,因此我们无法直接访问到它。它加载的是系统类,如 HashMap, Intent, Activity 等。
  • BaseDexClassLoader: 是 PathClassLoader 和 DexClassLoader 的父类,其内有一个 DexPathList 属性,实现了 findClass 方法逻辑,PathClassLoader 和 DexClassLoader 都只是在构造函数上对其做了简单封装而已。
  • PathClassLoader: 它是 App 加载自身 Dex 文件所用到的类加载器,如 MainActivity, AppCompatActivity 等,其 parent 是 BootClassLoader。
  • DexClassLoader: 可以加载 dex 或 apk 等文件,可用于执行动态加载,因此很多插件化方案都是采用 DexClassLoader。

相关参数:

  • dexPath: 包含类或者资源的 .jar/.apk/.dex 等路径,如果是多个路径,则用 : 分隔。
  • optimizedDirectory: 在 API 26(Android 8.0)的版本中,它表示 odex(optimized dex) 读写存放目录,如果传 null 则表示使用系统默认的目录来存储。自 Android 8.0 起,这个参数已经被弃用,不再生效,使用系统默认的目录。
  • librarySearchPath: native 库文件存放目录,多个库文件则用 : 分隔。
  • parent: 父类加载器。
  • isTrusted: 当前加载的 dex 是否受信任,如果受信任则可以访问平台隐藏的API,默认为 false。

ClassLoader

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
public abstract class ClassLoader {
// --------系统默认使用的类加载器是PathClassLoader,parent为BootClassLoader
public static ClassLoader getSystemClassLoader() {
return SystemClassLoader.loader;
}

static private class SystemClassLoader {
public static ClassLoader loader = ClassLoader.createSystemClassLoader();
}

private static ClassLoader createSystemClassLoader() {
String classPath = System.getProperty("java.class.path", ".");
String librarySearchPath = System.getProperty("java.library.path", "");
return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance());
}

private final ClassLoader parent;

private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}

protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 判断当前类加载器是否已经加载过指定类,若已加载则直接返回
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果没有加载过,则调用parent的类加载递归加载该类
c = parent.loadClass(name, false);
} else {
// findBootstrapClassOrNull方法return null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 还没加载,则调用当前类加载器来加载
if (c == null) {
c = findClass(name);
}
}
return c;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
}

BaseDexClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
}

// BaseDexClassLoader 实现了 findClass 方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实际上是通过 DexPathList 来加载类
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
// throw Exception;
}
return c;
}
}

BaseDexClassLoader 中有一个 DexPathList 类的 pathList 成员变量,它表示 dexPath 下的 .dex 列表。

PathClassLoader

1
2
3
4
5
6
7
8
9
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

PathClassLoader 构造函数很简单,直接调用父类 BaseDexClassLoader 的构造函数。第二个构造参数始终是 null,表示 optimizedDirectory 始终为 null。

DexClassLoader

1
2
3
4
5
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}

DexClassLoader 跟 PathClassLoader 相似。和前面一样,第二个参数 optimizedDirectory 也从 Android 8.0 开始弃用,不再有效。

实例

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("LLL", "HashMap: " + HashMap.class.getClassLoader());
Log.d("LLL", "Intent: " + Intent.class.getClassLoader());
Log.d("LLL", "Activity: " + Activity.class.getClassLoader());
Log.d("LLL", "AppCompatActivity: " + AppCompatActivity.class.getClassLoader());
Log.d("LLL", "this: " + this.getClassLoader());
}
}

输出:

1
2
3
4
5
HashMap: java.lang.BootClassLoader@e8c0fdc
Intent: java.lang.BootClassLoader@e8c0fdc
Activity: java.lang.BootClassLoader@e8c0fdc
AppCompatActivity: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/base.apk"],nativeLibraryDirectories=[/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/lib/arm64, /system/lib64, /vendor/lib64]]]
this: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/base.apk"],nativeLibraryDirectories=[/data/app/com.hearing.demo-CXVzyNHtPtPvvTGoYDELiA==/lib/arm64, /system/lib64, /vendor/lib64]]]

DexPathList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@hide
public final class DexPathList {
private static final String DEX_SUFFIX = ".dex";
private static final String zipSeparator = "!/";

private Element[] dexElements;
NativeLibraryElement[] nativeLibraryPathElements;

public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}

DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted);

this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);

this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
}
}
  • Element 用来描述一个 dex 文件所代表的元素。字段 dexElements 是 dex 文件元素列表,通过 makeDexElements() 方法来初始化。
  • NativeLibraryElement 用来描述一个库文件所代表的元素,字段 nativeLibraryPathElements 则为库文件元素列表,通过 makePathElements 方法来初始化。

热修复/插件化原理

类加载过程:以 PathClassLoader 加载为例,它会调用父类的 ClassLoader.loadClass() 方法,这个方法在上面已经看了,它遵循双亲委派原则,当 parent 加载不到时则调用自身的 findClass 方法,其父类加载器也是这个逻辑。因此会调用到 BaseDexClassLoader.findClass() 方法,这个方法会借助 DexPathList.findClass() 方法来加载。最终会调用 native 方法来查找 .dex 文件中相应的类,并在 native 层创建目标类的对象并添加到虚拟机列表。

在 DexPathList.findClass() 过程,一个Classloader可以包含多个dex文件,每个dex文件被封装到一个Element对象,这些Element对象排列成有序的数组dexElements。当查找某个类时,会遍历所有的dex文件,如果找到则直接返回,不再继续遍历dexElements。也就是说当两个类不同的dex中出现,会优先处理排在前面的dex文件,这便是热修复的核心精髓,将需要修复的类所打包的dex文件插入到dexElements前面。

热修复示例

此处反射获取PathClassLoader的过程与Android启动Application原理相关,具体过程可见:Android-Application,在理解了这部分源码后,便可知道下面为何要通过反射LoadedApk来获取PathClassLoader了。

反射工具类

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
public final class ReflectUtils {

private static Field getField(Class<?> cls, String fieldName) {
for (Class<?> acls = cls; acls != null; acls = acls.getSuperclass()) {
try {
final Field field = acls.getDeclaredField(fieldName);
setAccessible(field, true);

return field;
} catch (final NoSuchFieldException ex) {
}
}
Field match = null;
for (final Class<?> class1 : cls.getInterfaces()) {
try {
match = class1.getField(fieldName);
} catch (final NoSuchFieldException ex) {
}
}
return match;
}

public static Object readField(Object target, String fieldName) throws Exception {
return readField(target.getClass(), target, fieldName);
}

private static Object readField(Class<?> c, Object target, String fieldName) throws Exception {
Field f = getField(c, fieldName);

return readField(f, target);
}

private static Object readField(final Field field, final Object target) throws Exception {
return field.get(target);
}

public static void writeField(Object target, String fName, Object value) throws Exception {
writeField(target.getClass(), target, fName, value);
}

private static void writeField(Class<?> c, Object object, String fName, Object value) throws Exception {
Field f = getField(c, fName);
writeField(f, object, value);
}

private static void writeField(final Field field, final Object target, final Object value) throws Exception {
field.set(target, value);
}

private static void setAccessible(AccessibleObject ao, boolean value) {
if (ao.isAccessible() != value) {
ao.setAccessible(value);
}
}

public static Object combineArray(Object arrayLhs, Object arrayRhs) throws Exception {
Class<?> clazz = arrayLhs.getClass().getComponentType();
int i = Array.getLength(arrayLhs);
int j = Array.getLength(arrayRhs);
int k = i + j;
Object result = Array.newInstance(clazz, k);
System.arraycopy(arrayLhs, 0, result, 0, i);
System.arraycopy(arrayRhs, 0, result, i, j);
return result;
}
}

修复工具类

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
public class FixDexUtil {

private static final String DEX_SUFFIX = ".dex";
private static final String APK_SUFFIX = ".apk";

private static final String PATCH_DIR = "patch";

private static HashSet<File> sLoadedDex = new HashSet<>();

public static boolean needFix(@NonNull Context context) {
sLoadedDex.clear();

// /data/data/包名/files/patch
File fileDir = new File(context.getFilesDir(), PATCH_DIR);
if (!fileDir.exists()) {
fileDir.mkdirs();
return false;
}
if (!fileDir.isDirectory()) {
fileDir.delete();
fileDir.mkdirs();
return false;
}

boolean canFix = false;
File[] listFiles = fileDir.listFiles();
if (listFiles == null) {
return false;
}
for (File file : listFiles) {
if ((file.getName().endsWith(DEX_SUFFIX)
|| file.getName().endsWith(APK_SUFFIX))) {
sLoadedDex.add(file);
canFix = true;
}
}
return canFix;
}

public static void fix(Application application) {
doFix(application, sLoadedDex);
}

private static void doFix(Application application, HashSet<File> loadedDex) {
try {
Context base = application.getBaseContext();
if (base == null) {
Log.d("LLL", "base == null");
return;
}

Object packageInfo = ReflectUtils.readField(base, "mPackageInfo");
if (packageInfo == null) {
Log.d("LLL", "packageInfo == null");
return;
}

// 这里也可以由context.getClassLoader()得到PathClassLoader
// 而loadedDex.getClass().getClassLoader()返回的是BootCLassLoader
ClassLoader classLoader = (ClassLoader) ReflectUtils.readField(packageInfo, "mClassLoader");
if (!(classLoader instanceof PathClassLoader)) {
Log.d("LLL", "classLoader == null");
return;
}

PathClassLoader pathLoader = (PathClassLoader) classLoader;

for (File dex : loadedDex) {
DexClassLoader dexLoader = new DexClassLoader(dex.getAbsolutePath(), null, null, pathLoader);

// 3.开始合并: 合并的目标是Element[], 重新赋值它的值即可

//3.1 准备好pathList的引用
Object dexPathList = ReflectUtils.readField(dexLoader, "pathList");
Object pathPathList = ReflectUtils.readField(pathLoader, "pathList");
//3.2 从pathList中反射出element集合
Object leftDexElements = ReflectUtils.readField(dexPathList, "dexElements");
Object rightDexElements = ReflectUtils.readField(pathPathList, "dexElements");
//3.3 合并两个dex数组
Object dexElements = ReflectUtils.combineArray(leftDexElements, rightDexElements);

Object pathList = ReflectUtils.readField(pathLoader, "pathList");
ReflectUtils.writeField(pathList, "dexElements", dexElements);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

使用方法

1
2
3
4
5
6
7
8
9
10
11
public class PatchApplication extends Application {

@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
if (FixDexUtil.needFix(this)) {
Log.d("LLL", "begin to fix");
FixDexUtil.fix(this);
}
}
}

需要修复的工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 修复前
public class Util {
private static final String TAG = "LLL";

public static void test() {
Log.d(TAG, "first publish.");
}
}

// 修复后
public class Util {
private static final String TAG = "LLL";

public static void test() {
Log.d(TAG, "first publish.");
Log.d(TAG, "patch publish.");
}
}
  • 首先在手机上安装修复前的版本,调用Util.test(),会输出first publish.
  • 然后修改Util类,打包成apk,也可以只将Util类单独打包成dex,push到/data/data/pkg/files/patch下,由于我的测试机已经root,所以直接使用这个目录,否则可以添加从sd卡拷贝到私有目录的逻辑;
  • 重启后会加载修复包,多输出一行patch publish.
  • 打包成dex的命令:dx --dex --output=util.dex com\hearing\gopatch\Util.class

RePlugin

RePlugin原理解析