alibaba新出了一个非侵入的aop库,感觉不错,那么楼主这次就来学习一下这个库的具体应用,原理以及可以达到的效果。
这里先给出对应的github工程传送门:https://github.com/alibaba/dexposed
1.首先来讲讲,dexposed的具体用法怎么用,怎么引入到我们的工程中来。
这个其实在dexposed的github工程上说明的很清楚,这里我来重述下。
首先我们要将其引入到工程中:
native_dependencies {
artifact 'com.taobao.dexposed:dexposed_l:0.2+:armeabi'
artifact 'com.taobao.dexposed:dexposed:0.2+:armeabi'
}
dependencies {
compile files('libs/dexposedbridge.jar')
}
看到libdexposed.so和libdexposed_l.so,可能会想到他们的差异是什么,看loadDexposedLIb这个方法其实很容易看出来:
private static boolean loadDexposedLib(Context context) {
try {
if(VERSION.SDK_INT != 10 && VERSION.SDK_INT != 9) {
if(VERSION.SDK_INT > 19) {
System.loadLibrary("dexposed_l");
} else {
System.loadLibrary("dexposed");
}
} else {
System.loadLibrary("dexposed2.3");
}
return true;
} catch (Throwable var2) {
return false;
}
}
public class MyApplication extends Application {
@Override public void onCreate() {
// Check whether current device is supported (also initialize Dexposed framework if not yet)
if (DexposedBridge.canDexposed(this)) {
// Use Dexposed to kick off AOP stuffs.
...
}
}
...
}
参考git上给出的代码,官方应该是推荐在application层去调用这个方法。值得注意的是DexposedBridge.canDexposed(this)是一个对应boolean类型返回值的函数,若为false则是不能去实现hook函数的,所以要注意记录这个返回值,以便再其他地方调用hook时用来判断是否可以执行。
2.那么我们既然已经将其引入了工程,接下来就是研究怎么去使用这个库了
Example 1: Attach a piece of code before and after all occurrences of Activity.onCreate(Bundle)
.
// Target class, method with parameter types, followed by the hook callback (XC_MethodHook).
DexposedBridge.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodHook() {
// To be invoked before Activity.onCreate().
@Override protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
// "thisObject" keeps the reference to the instance of target class.
Activity instance = (Activity) param.thisObject;
// The array args include all the parameters.
Bundle bundle = (Bundle) param.args[0];
Intent intent = new Intent();
// XposedHelpers provide useful utility methods.
XposedHelpers.setObjectField(param.thisObject, "mIntent", intent);
// Calling setResult() will bypass the original method body use the result as method return value directly.
if (bundle.containsKey("return"))
param.setResult(null);
}
// To be invoked after Activity.onCreate()
@Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
XposedHelpers.callMethod(param.thisObject, "sampleMethod", 2);
}
});
Example 2: Replace the original body of the target method.
DexposedBridge.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodReplacement() {
@Override protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
// Re-writing the method logic outside the original method context is a bit tricky but still viable.
...
}
});
以上两个example是git上给出的两个最基础的用法。其他用法的延生都会基于这两个基础的用法。
example1中,我们可以看到,用dexposed可以实现不改变原函数的执行,但是在原函数执行前后去做一些其他的额外处理,例如改变入参和返回值等等的一些事情。
example2中,则是可以将原有要执行的函数替换成一个我们需要的新的执行函数。
基于以上两个实现,我们可以衍生出什么样的应用场景呢?有人说hook api具体可以实现什么,全凭使用者的想象力,实际上确实如此。
官方列出的几种应用场景正式当今比较需要的:
典型的 AOP 编程
仪表化 (测试,性能监控等等)
在线热修复(重要,关键,安全漏洞等等)
SDK hooking,更好的开发体验
3.既然要应用一个库,我们必须对它的实现以及原理有一定的了解
下面是一段比较官方的介绍:
Dexposed中的AOP原理来自于Xposed。在Dalvik虚拟机下,主要是通过改变一个方法对象方法在Dalvik虚拟机中的定 义来实现,具体做法就是将该方法的类型改变为native并且将这个方法的实现链接到一个通用的Native Dispatch方法上。这个 Dispatch方法通过JNI回调到Java端的一个统一处理方法,最后在统一处理方法中调用before, after函数来实现AOP。在Art虚拟机上目前也是是通过改变一个 ArtMethod的入口函数来实现。
从上面可以知道基础的实现过程,另外上面提到了两个概念,Dalvik虚拟机和Art虚拟机,这里稍稍做做科普。
什么是Dalvik:
Dalvik是Google公司自己设计用于Android平台的Java虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
什么是ART:
Android操作系统已经成熟,Google的Android团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。 ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。
ART优点:
1、系统性能的显著提升。
2、应用启动更快、运行更快、体验更流畅、触感反馈更及时。
3、更长的电池续航能力。
4、支持更低的硬件。
ART缺点:
1、更大的存储空间占用,可能会增加10%-20%。
2、更长的应用安装时间。
总的来说ART的功效就是“空间换时间”。
google在android4.4之后的版本都用art取代了dalvik,所以要hook android4.4以后的版本就必须去适配art虚拟机的机制。这也解释了上面为什么会有dexposed和dexposed_l两个so。目前官方表示,为了适配art的dexposed_l只是beta版,所以最好不要在正式的线上产品中使用它。
接下来,稍微来分析下对应的jar包的java部分代码结构:
其实很简洁,DexposedBrigde是主要的功能调用类,XposedHelpers则是一个反射功能类,其他则为一些辅助类。
工程用来hook的方法DexposedBridge.findAndHookMethod,那么这里来看看这个函数java部分的执行过程:
public static Unhook findAndHookMethod(Class> clazz, String methodName, Object... parameterTypesAndCallback) {
if(parameterTypesAndCallback.length != 0 && parameterTypesAndCallback[parameterTypesAndCallback.length - 1] instanceof XC_MethodHook) {
XC_MethodHook callback = (XC_MethodHook)parameterTypesAndCallback[parameterTypesAndCallback.length - 1];
Method m = XposedHelpers.findMethodExact(clazz, methodName, parameterTypesAndCallback);
Unhook unhook = hookMethod(m, callback);
if(!(callback instanceof XC_MethodKeepHook) && !(callback instanceof XC_MethodKeepReplacement)) {
ArrayList var6 = allUnhookCallbacks;
synchronized(allUnhookCallbacks) {
allUnhookCallbacks.add(unhook);
}
}
return unhook;
} else {
throw new IllegalArgumentException("no callback defined");
}
}
然后会执行hookMethod:
public static Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
if(!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor)) {
throw new IllegalArgumentException("only methods and constructors can be hooked");
} else {
boolean newMethod = false;
Map declaringClass = hookedMethodCallbacks;
DexposedBridge.CopyOnWriteSortedSet callbacks;
synchronized(hookedMethodCallbacks) {
callbacks = (DexposedBridge.CopyOnWriteSortedSet)hookedMethodCallbacks.get(hookMethod);
if(callbacks == null) {
callbacks = new DexposedBridge.CopyOnWriteSortedSet();
hookedMethodCallbacks.put(hookMethod, callbacks);
newMethod = true;
}
}
callbacks.add(callback);
if(newMethod) {
Class declaringClass1 = hookMethod.getDeclaringClass();
int slot = runtime == 1?XposedHelpers.getIntField(hookMethod, "slot"):0;
Class[] parameterTypes;
Class returnType;
if(hookMethod instanceof Method) {
parameterTypes = ((Method)hookMethod).getParameterTypes();
returnType = ((Method)hookMethod).getReturnType();
} else {
parameterTypes = ((Constructor)hookMethod).getParameterTypes();
returnType = null;
}
DexposedBridge.AdditionalHookInfo additionalInfo = new DexposedBridge.AdditionalHookInfo(callbacks, parameterTypes, returnType, (DexposedBridge.AdditionalHookInfo)null);
hookMethodNative(hookMethod, declaringClass1, slot, additionalInfo);
}
callback.getClass();
return new Unhook(callback, hookMethod);
}
}
static void com_taobao_android_dexposed_DexposedBridge_hookMethodNative(JNIEnv* env, jclass clazz, jobject reflectedMethodIndirect,
jobject declaredClassIndirect, jint slot, jobject additionalInfoIndirect) {
// Usage errors?
if (declaredClassIndirect == NULL || reflectedMethodIndirect == NULL) {
dvmThrowIllegalArgumentException("method and declaredClass must not be null");
return;
}
// Find the internal representation of the method
ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);
Method* method = dvmSlotToMethod(declaredClass, slot);
if (method == NULL) {
dvmThrowNoSuchMethodError("could not get internal representation for method");
return;
}
if (dexposedIsHooked(method)) {
// already hooked
return;
}
// Save a copy of the original method and other hook info
DexposedHookInfo* hookInfo = (DexposedHookInfo*) calloc(1, sizeof(DexposedHookInfo));
memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));
hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));
hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));
// Replace method with our own code
SET_METHOD_FLAG(method, ACC_NATIVE);
method->nativeFunc = &dexposedCallHandler;
method->insns = (const u2*) hookInfo;
method->registersSize = method->insSize;
method->outsSize = 0;
if (PTR_gDvmJit != NULL) {
// reset JIT cache
MEMBER_VAL(PTR_gDvmJit, DvmJitGlobals, codeCacheFull) = true;
}
}
至于有兴趣研究的童鞋可以去github上查看完整代码,并深入研究,这里涉及比较多得android底层知识。
4.既然也了解部分的实现和原理,dexposed还有一个最吸引人的地方就是拥有热补丁的功能
android开发人员都知道,对于android这种客户端的应用,一旦出现线上bug,唯一的解决方法就是发修复包来升级修复,这是很不灵活,而且体验很差的。那么能够在不发版的情况下,动态修复线上bug是被急切需求的。下面来演示一下如何应用dexposed来修复线上的bug。
github上有给出相应的例子,楼主在此基础上进行了拓展和完善。
首先在你得app中添加此方法:
// Run taobao.patch apk
public void runPatchApk() {
if (android.os.Build.VERSION.SDK_INT == 21) {
return;
}
if (!isSupport) {
Log.d("dexposed", "This device doesn't support dexposed!");
return;
}
File cacheDir = getExternalCacheDir();
if (cacheDir != null) {
String fullpath = cacheDir.getAbsolutePath() + File.separator + "patch.apk";
PatchResult result = PatchMain.load(this, fullpath, null);
if (result.isSuccess()) {
Log.e("Hotpatch", "patch success!");
} else {
Log.e("Hotpatch", "patch error is " + result.getErrorInfo());
}
}
}
那么再来看看对应的patch.apk工程应该是怎么构建的。
只需要引入dexposedbridge.jar和patchloader.jar,然后com.taobao.patch的包名下,去添加对应的Ipatch类即可。
那么我们想象线上版本出现了图片显示有问题,我们怎么去应用dexposed的热补丁功能去修复呢。
工程中有问题的函数:
private void initView() {
iv_start_top = (ImageView) findViewById(R.id.iv_start_top);
iv_start_middle = (ImageView) findViewById(R.id.iv_start_middle);
iv_start_bottom = (ImageView) findViewById(R.id.iv_start_bottom);
// int height = Utils.getHeight(MizheApplication.getApp()) / 3;
// RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, height);
// iv_start_top.setLayoutParams(layoutParams);
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inDensity = DisplayMetrics.DENSITY_XHIGH;
if (getResources().getDisplayMetrics().densityDpi > DisplayMetrics.DENSITY_XHIGH) {
options.inTargetDensity = DisplayMetrics.DENSITY_XHIGH;
}
topImg = BitmapFactory.decodeResource(getResources(), R.drawable.default_startup_top, options);
// middleImg = BitmapFactory.decodeResource(getResources(), R.drawable.default_startup_middle, options);
bottomImg = BitmapFactory.decodeResource(getResources(), R.drawable.default_startup_bottom, options);
iv_start_top.setImageBitmap(topImg);
// iv_start_middle.setImageBitmap(middleImg);
iv_start_bottom.setImageBitmap(bottomImg);
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
}
public class ViewPatch implements IPatch {
@Override
public void handlePatch(PatchParam patchParam) throws Throwable {
Class> cls = null;
try {
cls = patchParam.context.getClassLoader()
.loadClass("com.husor.mizhe.activity.SplashActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return;
}
DexposedBridge.findAndHookMethod(cls, "initView",
new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Activity mainActivity = (Activity) param.thisObject;
ImageView bottomView = (ImageView) XposedHelpers.getObjectField(mainActivity, "iv_start_bottom");
bottomView.setImageResource(0x7f020175);
}
});
}
}
我们可以在initView这个方法执行完后用对的图片来覆盖原先设置的图片,以此为思路。我们先通过loadClass来定位到出问题的class,然后通过hook的方法,将SplashActivity的initView方法中设置图片错误的imageview,设置成我们想要得正确地resourceId.
最近项目中有碰到这样一个问题,会导致app奔溃:
private Address validateInputs() {
String name, phone, region, detail, post;
name = etPeople.getText().toString();
if (TextUtils.isEmpty(name)) {
Toast.makeText(AddressItemActivity.this, getString(R.string.tip_input_receiver), Toast.LENGTH_SHORT).show();
return null;
}
phone = etPhone.getText().toString();
if (TextUtils.isEmpty(phone)) {
Toast.makeText(AddressItemActivity.this, getString(R.string.tip_input_mobile_number), Toast.LENGTH_SHORT).show();
return null;
}
if (!Utils.validatePhone(phone)) {
Toast.makeText(AddressItemActivity.this, getString(R.string.tip_mobile_number_illegal), Toast.LENGTH_SHORT).show();
return null;
}
region = (String)tvRegion.getText();
if (TextUtils.isEmpty(region)) {
Toast.makeText(AddressItemActivity.this, getString(R.string.tip_input_province_city), Toast.LENGTH_SHORT).show();
return null;
}
detail = etDetail.getText().toString();
if (TextUtils.isEmpty(detail)) {
Toast.makeText(AddressItemActivity.this, getString(R.string.tip_input_detail_address), Toast.LENGTH_SHORT).show();
return null;
}
//可以不用填写邮编
post = etPost.getText().toString();
if (!TextUtils.isEmpty(post) && (!Utils.validatePost(post) || post.equals("000000"))) {
Toast.makeText(AddressItemActivity.this, getString(R.string.label_postalcode_format), Toast.LENGTH_SHORT).show();
return null;
}
mAddress.mName = name;
mAddress.mPhone = phone;
mAddress.mDetail = detail;
mAddress.mIsDefault = (cbDefault.isChecked() ? 1 : 0);
mAddress.mZip = post;
return mAddress;
}
region = (String)tvRegion.getText();
这一句代码在有些机型上会出现类型转换失败的问题,那么这里来写一个对应的补丁来修复这个问题。修复的办法是将该句转换为
region = tvRegion.getText().toString();
补丁对应的代码为:
public class AddressPatch implements IPatch {
@Override
public void handlePatch(final PatchParam patchParam) throws Throwable {
Class> cls = null;
try {
cls = patchParam.context.getClassLoader()
.loadClass("com.husor.mizhe.activity.AddressItemActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return;
}
DexposedBridge.findAndHookMethod(cls, "validateInputs",
new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
Activity activity = (Activity) methodHookParam.thisObject;
String name, phone, region, detail, post;
EditText etPeople = (EditText) XposedHelpers.getObjectField(activity, "etPeople");
EditText etPhone = (EditText) XposedHelpers.getObjectField(activity, "etPhone");
TextView tvRegion = (TextView) XposedHelpers.getObjectField(activity, "tvRegion");
EditText etDetail = (EditText) XposedHelpers.getObjectField(activity, "etDetail");
EditText etPost = (EditText) XposedHelpers.getObjectField(activity, "etPost");
CheckBox cbDefault = (CheckBox) XposedHelpers.getObjectField(activity, "cbDefault");
Object mAddress = XposedHelpers.getObjectField(activity, "mAddress");
Class> utilCls = null;
try {
utilCls = patchParam.context.getClassLoader()
.loadClass("com.husor.mizhe.utils.Utils");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
}
name = etPeople.getText().toString();
if (TextUtils.isEmpty(name)) {
Toast.makeText(activity, "请输入收货人", Toast.LENGTH_SHORT).show();
return null;
}
phone = etPhone.getText().toString();
if (TextUtils.isEmpty(phone)) {
Toast.makeText(activity, "请输入手机号码", Toast.LENGTH_SHORT).show();
return null;
}
boolean isPhone = (boolean) XposedHelpers.callStaticMethod(utilCls, "validatePhone", phone);
if (!isPhone) {
Toast.makeText(activity, "手机号码不合法", Toast.LENGTH_SHORT).show();
return null;
}
region = tvRegion.getText().toString();
if (TextUtils.isEmpty(region)) {
Toast.makeText(activity, "请输入省市地址", Toast.LENGTH_SHORT).show();
return null;
}
detail = etDetail.getText().toString();
if (TextUtils.isEmpty(detail)) {
Toast.makeText(activity, "请输入详细地址", Toast.LENGTH_SHORT).show();
return null;
}
//可以不用填写邮编
post = etPost.getText().toString();
boolean isPost = (boolean) XposedHelpers.callStaticMethod(utilCls, "validatePost", post);
if (!TextUtils.isEmpty(post) && (!isPost || post.equals("000000"))) {
Toast.makeText(activity, "邮政编码格式有误", Toast.LENGTH_SHORT).show();
return null;
}
XposedHelpers.setObjectField(mAddress, "mName", name);
XposedHelpers.setObjectField(mAddress, "mPhone", phone);
XposedHelpers.setObjectField(mAddress, "mDetail", detail);
XposedHelpers.setIntField(mAddress, "mIsDefault", (cbDefault.isChecked() ? 1 : 0));
XposedHelpers.setObjectField(mAddress, "mZip", post);
return mAddress;
}
});
}
}
楼主这边只能想到用覆写这个方法来修复问题,可能还有不用那么复杂的做法,这里暂时没有想到。但是其实主要需要我们关注的是在patch代码中,我们只能通过java的反射机制来获取你要修改的对象或者调用的方法,dexposed提供的XposedHelpers这个类提供了比较丰富的反射功能函数,需要我们灵活地利用好这个类,才能更好更快地实现修复代码。
总结:
这套库在java层面,主要应用到得是Java的反射机制,而且在应用这套库hook函数时,也会对使用者的反射知识有较高的要求,尤其在热补丁的应用场景下,只能通过反射去获取应用对应的函数和成员变量,以及对它们进行相应的操作。