首页 > 代码库 > alibaba dexposed初步解析

alibaba dexposed初步解析

alibaba新出了一个非侵入的aop库,感觉不错。那么楼主这次就来学习一下这个库的详细应用,原理以及能够达到的效果。


这里先给出相应的githubproject传送门:https://github.com/alibaba/dexposed


1.首先来讲讲,dexposed的详细使用方法怎么用,怎么引入到我们的project中来。

这个事实上在dexposed的githubproject上说明的非常清楚,这里我来重述下。

首先我们要将其引入到project中:

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这种方法事实上非常easy看出来:

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;
        }
    }

在sdk版本号大于19。及为5.0以上时。会去调用dexposed_l。否则会调用dexposed。至于为什么须要两套不同的native实现,后面会解释到。


集成到我们的project后。假设想应用它。仅仅须要简单地一句话:

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.那么我们既然已经将其引入了project,接下来就是研究怎么去使用这个库了

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)编译器去解释字节码。

开发人员编译后的应用代码须要通过一个解释器在用户的设备上执行。这一机制并不高效,但让应用能更easy在不同硬件和架构上运 行。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则是一个反射功能类,其它则为一些辅助类。

project用来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");
        }
    }

首先会通过XposedHelpers这个反射工具类的findMethodExact方法来找到相应的Method。


然后会运行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);
        }
    }

这里我们须要给hookMethodNative传递的參数各自是1.依据反射定位到得要hook得Method;2.声明定义Method的class;3.slot标记runtime的标记符;4.包括函数入參,返回值类型以及相应回调的自己定义类。然后就完毕了相应的hook,java端的代码还是比較简单清晰的。至于native中的具体实现。楼主就不在这里具体解析了。这里能够给出hookMethodNative相应的c代码。

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这样的client的应用。一旦出现线上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,即补丁文件的目录路径可按自己需求定义。PatchMain.load实现的主要功能是去遍历patch.apk中的ipatch类,并调用当中的handlePatch方法。注意:最好将该方法加在application的oncreate方法中,保证初始化程序时已打上补丁。那么当然你得app必须有一个去后台检查是否须要下载补丁的接口,如若须要最好用静默下载的方式。进行静默修复。

下载完毕后记得,动态调用runPatchApk这种方法,否则补丁不会生效。


那么再来看看相应的patch.apkproject应该是怎么构建的。

技术分享

仅仅须要引入dexposedbridge.jar和patchloader.jar,然后com.taobao.patch的包名下。去加入相应的Ipatch类就可以。


那么我们想象线上版本号出现了图片显示有问题,我们怎么去应用dexposed的热补丁功能去修复呢。

project中有问题的函数:

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();
        }
    }

这里我们如果对iv_start_bottom这个imageview载入错了图片。那么修复的方法例如以下:

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函数时,也会对使用者的反射知识有较高的要求。尤其在热补丁的应用场景下,仅仅能通过反射去获取应用相应的函数和成员变量,以及对它们进行相应的操作。


alibaba dexposed初步解析