Dexposed源码概述
Dexposed框架可以用来实现热补丁功能和AOP编程功能,在AOP编程功能的基础上我们可以实现事件自动埋点、程序性能监控和日志记录等等功能。整个Dexposed框架中最为重要的功能点是实现Java方法的hook,可以说Java方法的hook是Dexposed框架实现热补丁功能和AOP编程框架的基石。而Dexposed框架通过修改虚拟机内部描述Java方法的数据结构成员的值来实现Java方法的Hook,为此整个框架必须针对Android系统不同虚拟机和同一种虚拟机的不同本版进行适配。
AOP框架的基本思路是在特定的Java方法被调用的时候提供预处理方法和后处理方法。在预处理方法内可以进行事件拦截、日志记录操作,而在后处理方法内可以进行性能统计这类操作。在Dexposed框架中,预处理方法和后处理方法的调用是通过Hook指定的Java方法,将程序对被Hook方法的调用转派到框架指定的一个native函数,该函数实现了预处理方法和后处理方法的调用。
热补丁框架一般来说由补丁程序和宿主程序两部分组成,为此热补丁框架需要定义补丁程序和宿主程序的通讯协议。通讯协议必须能够明确补丁程序想要替换宿主程序内哪些类和哪些方法并且规定了补丁程序编写时应该遵循的规范,而对于宿主程序来说必须要实现指定类或则方法的替换。在Dexposed框架内宿主程序和补丁程序通讯协议的实现是定义在patchloader.jar内,宿主程序实现特定方法的替换则是实现在DexposedBridge.jar内。
Java方法Hook API的基本用法
由于整个框架的的核心是Java方法的hook,所以我们先来了解一下dexposed框架内如何hook一个Java方法。如果读者学习使用过Xposed框架的话,那么对于Dexposed框架hook Java方法的API应该十分熟悉。
下面是一段Dexposed框架hook Java方法的实例代码片段:
DexposedBridge.findAndHookMethod(cls, "showDialog", new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
Activity mainActivity = (Activity) param.thisObject;
AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
builder.setTitle("Dexposed sample")
.setMessage("The dialog is shown from patch apk!")
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).create().show();
return null;
}
});
DexposedBridge类提供了静态方法findAndHookMethod()方法来hook指定的Java方法。该方法的第一个参数是被hook方法所在类的类信息,第二个参数是需要hook的Java方法的名字,接下来是一个参数列表用来传递该Java方法的参数类型列表,而findAndHookMethod()方法最后一个参数必须是Dexposed框架提供的一个回调接口。通过实例化不同回调对象实现Java方法的替换功能或则添加预处理代码和后处理代码功能。
findAndHookMethod()方法的回调类型
在使用Dexposed框架实现Java方法hook的时候,接触比较多的是XC_MethodHook和XC_MethodReplacement这两个回调抽象类。下面将对这个回调类的基本实现进行简单的介绍:
- XC_MethodHook类:
XC_MethodHook回调类一般用于实现AOP编程,该类提供了beforeHookedMethod()
和afterHookedMethod()
两个方法作为指定方法的预处理方法和后处理方法。在beforeHookedMethod()
方法内可以实现事件拦截、事件埋点这类操作,而在afterHookedMethod()
方法内可以实现一些诸如资源释放、数据统计之类的操作。
- XC_MethodReplacement类:
XC_MethodReplacement回调类一般用于实现热补丁功能,该类继承至XC_MethodHook。XC_MethodReplacement提供了replaceHookedMethod()方法用于实现被hook的Java方法的功能的替换。同时该类默认实现了beforeHookedMethod()
和afterHookedMethod()
方法。beforeHookedMethod()
通过调用replaceHookedMethod()
获取用户设置的返回结果后调用参数MethodHookParam的setResult()
阻止框架调用被hook的Java方法。
补丁程序的实现
通过以上对Java方法hook的介绍,我们知道可以在补丁程序中通过DexposedBridge类的findAndHookMethod()方法指定我们需要替换宿主程序中的特定类的方法。下面是补丁程序实现的基本代码片段:
public class DialogPatch implements IPatch {
@Override
public void handlePatch(final PatchParam arg0) throws Throwable {
Class<?> cls = null;
try {
cls = arg0.context.getClassLoader()
.loadClass("com.taobao.dexposed.MainActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return;
}
DexposedBridge.findAndHookMethod(cls, "showDialog",
new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
Activity mainActivity = (Activity) param.thisObject;
AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
builder.setTitle("Dexposed sample")
.setMessage("The dialog is shown from patch apk!")
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).create().show();
return null;
}
});
}
}
实现补丁程序的基本步骤是定义一系列的补丁类,在这些补丁类内指定我们要替换的宿主程序内的方法。现在将实现补丁程序的基本步骤总结如下:
- 导入patchloader.jar和dexposedbridge.jar
- 定义补丁类,并且补丁类必须实现patchloader.jar提供的IPatch接口
- 在补丁类的handlePatch()方法内实现宿主程序Java方法的hook
- 打包补丁程序
在实现补丁程序的时候有两个基本的难点,如何获取宿主程序中需要被hook的Java类的类信息和如何取得将被hook的Java方法的实参。这两个基本问题都可以通过通讯协议框架提供的数据得以实现。宿主程序在加载补丁程序的时候会通过IPatch接口的handlePatch()方法传递宿主程序的Context,补丁程序可以从handlePatch()方法的参数PatchParam获取到宿主程序的Context进而获取到宿主程序的ClassLoader和类信息。宿主程序在实现Java方法的hook的时候会通过回调类XC_MethodReplacement的方法replaceHookedMethod()
参数MethodHookParam传递被hook的Java方法运行时的实参。
MethodHookParam参数的成员如下:
public static class MethodHookParam extends Param {
public Member method; // 被hook方法的描述
public Object thisObject; // 被hook的类的当前对象,静态方法该成员值为空
public Object[] args; // 被hook的方法调用是的实参列表
private Object result = null; // 被hook的方法的返回值
private Throwable throwable = null;
boolean returnEarly = false;
.......
}
从以上MethodHookParam的定义可以看出,我们在实现补丁逻辑的时候可以通过该参数获取到当前的实例、实参列表这些基本的信息。在实现补丁程序的大多数时候都需要使用到被hook的Java方法的实参进行逻辑上的修改,所以MethodHookParam携带的参数完美的解决了这部分需求。
注意: 补丁程序需要引用到patchloader.jar和dexosedbridge.jar中的类和接口,但是这两个库是打包在宿主程序中而非补丁程序中,所以在打包补丁程序的时候不能将这两个库打包进入补丁程序
补丁框架源码分析
补丁框架源码集中在patchloader.jar的实现上,这部分代码的实现比较简洁省略了部分安全方面的校验,在实际项目中需要根据具体情况添加相应的安全校验功能。补丁框架要解决的基本问题可以归纳为以下几个:
1). 定义宿主程序和补丁程序通讯的接口
2). 宿主程序如何加载补丁程序和识别出补丁类
3). 宿主程序如何识别补丁程序的身份和校验数据的完整性
补丁程序和宿主程序通讯接口的定义
宿主程序和补丁程序通讯的接口应该包括宿主程序传递给补丁程序的数据的描述、补丁程序返回给宿主程序的响应信息的描述和宿主程序调用补丁程序中补丁类的约定方法。宿主程序传递给补丁程序的数据包括宿主程序的上下文、被hook的Java方法运行时需要访问的宿主程序类或则实例。补丁程序对宿主程序响应的信息包括补丁操作的结果、出现的错误和异常信息。由于补丁程序中的补丁类实现有多种,所以必须定义这些补丁类实现统一的接口。实现按接口编程可以方便宿主程序调用补丁程序中补丁类的操作。
在Dexposed框架中这三个数据的描述如下:
- PatchParam类:
该类定义了宿主程序传递给补丁程序的数据,在该类中定义的成员context在宿主程序加载补丁程序时赋值为宿主程序的Context实例。PatchParam类中的contentMap成员是一个HashMap,用来保存宿主程序传递给补丁程序的一些基本信息,补丁程序也可按照约定的规则根据指定的key获取宿主程序中的数据。
public class PatchParam {
protected final Object[] callbacks;
public PatchParam(ReadWriteSet<PatchCallback> callbacks) {
this.callbacks = callbacks.getSnapshot();
}
/**
* The context can be get by patch class. The most important is offer classloader.
*/
public Context context;
/**
* This map contains objects that may be used by patch class.
*/
public HashMap<String, Object> contentMap;
}
- PatchResult类
该类定义了宿主程序在加载执行补丁类后,补丁程序返回给宿主程序的结果。该结果包括加载补丁程序是否成功,失败的错误码或则宿主程序应该抛出的异常信息。
public class PatchResult {
private boolean result;
private int erroCode;
private String ErrorInfo;
private Throwable throwable;
/**
* Success
*/
public static int NO_ERROR = 0;
/**
* This device is not support.
*/
public static int DEVICE_UNSUPPORT = 1;
/**
* Exception happened during System.loadLibrary loading so.
*/
public static int LOAD_SO_EXCEPTION = 2;
/**
* The dvm crashed during loading so at last time, so it tell this crash if try to load again.
*/
public static int LOAD_SO_CRASHED = 3;
/**
* Please check the hotpatch file path if correct.
*/
public static int FILE_NOT_FOUND = 4;
/**
* Exception happened during loading patch classes.
*/
public static int FOUND_PATCH_CLASS_EXCEPTION = 5;
/**
* The hotpatch apk doesn't include some classes to patch.
*/
public static int NO_PATCH_CLASS_HANDLE = 6;
/**
* All patched classes run failed. Please check them if correct.
*/
public static int ALL_PATCH_FAILED = 7;
public PatchResult(boolean isSuccess, int code, String info) {
this.result = isSuccess;