美文网首页Xposedandroid开发android
Android平台免Root无侵入AOP框架Dexposed使用

Android平台免Root无侵入AOP框架Dexposed使用

作者: asce1885 | 来源:发表于2015-08-04 14:09 被阅读24208次

    @author ASCE1885的 Github 简书 微博 CSDN
    本文由于潜在的商业目的,不开放全文转载许可,谢谢!

    阿里巴巴无线事业部最近开源的Android平台下的无侵入运行期AOP框架Dexposed,该框架基于AOP思想,支持经典的AOP使用场景,可应用于日志记录,性能统计,安全控制,事务处理,异常处理等方面。

    针对Android平台,Dexposed支持函数级别的在线热更新,例如对已经发布在应用市场上的宿主APK,当我们从crash统计平台上发现某个函数调用有bug,导致经常性crash,这时,可以在本地开发一个补丁APK,并发布到服务器中,宿主APK下载这个补丁APK并集成后,就可以很容易修复这个crash。

    Dexposed是基于久负盛名的开源Xposed框架实现的一个Android平台上功能强大的无侵入式运行时AOP框架。

    Dexposed的AOP实现是完全非侵入式的,没有使用任何注解处理器,编织器或者字节码重写器。集成Dexposed框架很简单,只需要在应用初始化阶段加载一个很小的JNI库就可以,这个加载操作已经封装在DexposedBridge函数库里面的canDexposed函数中,源码如下所示:

    /**
     * Check device if can run dexposed, and load libs auto.
     */
    public synchronized static boolean canDexposed(Context context) {
        if (!DeviceCheck.isDeviceSupport(context)) {
            return false;
        }
        //load xposed lib for hook.
        return loadDexposedLib(context);
    }
        
    private static boolean loadDexposedLib(Context context) {
        // load xposed lib for hook.
        try {
            if (android.os.Build.VERSION.SDK_INT > 19){
                System.loadLibrary("dexposed_l");
            } else if (android.os.Build.VERSION.SDK_INT == 10
                    || android.os.Build.VERSION.SDK_INT == 9 || 
                    android.os.Build.VERSION.SDK_INT > 14){
                System.loadLibrary("dexposed");
            }
            return true;
        } catch (Throwable e) {
            return false;
        }
    }
    

    Dexposed实现的hooking,不仅可以hook应用中的自定义函数,也可以hook应用中调用的Android框架的函数。Android开发者将从这一点得到很多好处,因为我们严重依赖于Android SDK的版本碎片化。

    基于动态类加载技术,运行中的app可以加载一小段经过编译的Java AOP代码,在不需要重启app的前提下实现修改目标app的行为。

    典型的使用场景

    • AOP编程
    • 插桩(例如测试,性能监控等)
    • 在线热更新,修复严重的,紧急的或者安全性的bug
    • SDK hooking以提供更好的开发体验

    如何集成

    集成方式很简单,只需要将一个jar包加入项目的libs文件夹中,同时将两个so文件添加到jniLibs中对应的ABI目录中即可。Gradle依赖如下所示:

    buildscript {
      repositories {
        mavenCentral()
      }
      dependencies {
        classpath 'com.android.tools.build:gradle:0.10.+'
        classpath 'com.nabilhachicha:android-native-dependencies:0.1'
      }
    }
    
    ...
    
    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')
    }
    

    其中,native_dependencies是一个第三方插件,使用方法可参考《如何在Android Gradle中添加原生so文件依赖》。当然,我们也可以直接把需要用到的so文件直接拷贝到jniLibs目录中,这样的话,可以把上面的native_dependencies代码段注释掉。

    同时应该在应用初始化的地方(尽可能早的添加)添加初始化Dexposed的代码,例如在MyApplication中添加:

    public class MyApplication extends Application {
    
        private boolean mIsSupported = false; // 设备是否支持dexposed
    
        private boolean mIsLDevice = false;  // 设备Android系统是否是Android 5.0及以上
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            // check device if support and auto load libs
            mIsSupported = DexposedBridge.canDexposed(this);
            mIsLDevice = Build.VERSION.SDK_INT >= 21;
        }
    
        public boolean isSupported() {
            return mIsSupported;
        }
    
        public boolean isLDevice() {
            return mIsLDevice;
        }
    }
    

    基本用法

    对于某个函数而言,有三个注入点可供选择:函数执行前注入(before),函数执行后注入(after),替换函数执行的代码段(replace),分别对应于抽象类XC_MethodHook及其子类XC_MethodReplacement中的函数:

    public abstract class XC_MethodHook extends XCallback {
        
        /**
         * Called before the invocation of the method.
         * <p>Can use {@link MethodHookParam#setResult(Object)} and {@link MethodHookParam#setThrowable(Throwable)}
         * to prevent the original method from being called.
         */
        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {}
        
        /**
         * Called after the invocation of the method.
         * <p>Can use {@link MethodHookParam#setResult(Object)} and {@link MethodHookParam#setThrowable(Throwable)}
         * to modify the return value of the original method.
         */
        protected void afterHookedMethod(MethodHookParam param) throws Throwable  {}
    }
    
    public abstract class XC_MethodReplacement extends XC_MethodHook {
    
        @Override
        protected final void beforeHookedMethod(MethodHookParam param) throws Throwable {
            try {
                Object result = replaceHookedMethod(param);
                param.setResult(result);
            } catch (Throwable t) {
                param.setThrowable(t);
            }
        }
    
        protected final void afterHookedMethod(MethodHookParam param) throws Throwable {
        }
    
        /**
         * Shortcut for replacing a method completely. Whatever is returned/thrown here is taken
         * instead of the result of the original method (which will not be called).
         */
        protected abstract Object replaceHookedMethod(MethodHookParam param) throws Throwable;
    }
    

    可以看到这三个注入回调函数都有一个类型为MethodHookParam的参数,这个参数包含了一些很有用的信息:

    • MethodHookParam.thisObject:这个类的一个实例
    • MethodHookParam.args:用于传递被注入函数的所有参数
    • MethodHookParam.setResult:用于修改原函数调用的结果,如果在beforeHookedMethod回调函数中调用setResult,可以阻止对原函数的调用。但是如果有返回值的话仍然需要通过hook处理器进行return操作。

    MethodHookParam代码如下所示:

    public static class MethodHookParam extends XCallback.Param {
        /** Description of the hooked method */
        public Member method;
        /** The <code>this</code> reference for an instance method, or null for static methods */
        public Object thisObject;
        /** Arguments to the method call */
        public Object[] args;
            
        private Object result = null;
        private Throwable throwable = null;
        /* package */ boolean returnEarly = false;
            
        /** Returns the result of the method call */
        public Object getResult() {
            return result;
        }
            
        /**
         * Modify the result of the method call. In a "before-method-call"
         * hook, prevents the call to the original method.
         * You still need to "return" from the hook handler if required.
         */
        public void setResult(Object result) {
            this.result = result;
            this.throwable = null;
            this.returnEarly = true;
        }
            
        /** Returns the <code>Throwable</code> thrown by the method, or null */
        public Throwable getThrowable() {
            return throwable;
        }
            
        /** Returns true if an exception was thrown by the method */
        public boolean hasThrowable() {
            return throwable != null;
        }
            
        /**
         * Modify the exception thrown of the method call. In a "before-method-call"
         * hook, prevents the call to the original method.
         * You still need to "return" from the hook handler if required.
         */
        public void setThrowable(Throwable throwable) {
            this.throwable = throwable;
            this.result = null;
            this.returnEarly = true;
        }
            
        /** Returns the result of the method call, or throws the Throwable caused by it */
        public Object getResultOrThrowable() throws Throwable {
            if (throwable != null)
                throw throwable;
            return result;
        }
    }
    

    例子一:AOP编程

    AOP(Aspect Oriented Programming),也就是面向方面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

    AOP一般应用在日志记录,性能统计,安全控制,事务处理,异常处理等方面,它的主要意图是将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

    例如我们可以在应用中所有的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);
        }
    });
    

    当然也可以替换目标函数原来执行的代码段:

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

    例子二:在线热更新

    在线热更新一般用于修复线上严重的,紧急的或者安全性的bug,这里会涉及到两个apk文件,一个我们称为宿主apk,也就是发布到应用市场的apk,一个称为补丁apk。宿主apk出现bug时,通过在线下载的方式从服务器下载到补丁apk,使用补丁apk中的函数替换原来的函数,从而实现在线修复bug的功能。

    为了实现这个功能,需要再引入一个名为patchloader的jar包,这个函数库实现了一个热更新框架,宿主apk在发布时会将这个jar包一起打包进apk中,而补丁apk只是在编译时需要这个jar包,但打包成apk时不包含这个jar包,以免补丁apk集成到宿主apk中时发生冲突。因此,补丁apk将会以provided的形式依赖dexposedbridge.jar和patchloader.jar,补丁apk的build.gradle文件中依赖部分脚本如下所示:

    dependencies {
        provided files('libs/dexposedbridge.jar')
        provided files('libs/patchloader.jar')
    }
    

    这里我们假设宿主apk的MainActivity.showDialog函数出现问题,需要打补丁,宿主代码如下所示:(类完整路径是com.taobao.dexposed.MainActivity)

    package com.taobao.dexposed;
    
    public class MainActivity extends Activity {
        
        private void showDialog() {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("Dexposed sample")
                    .setMessage(
                            "Please clone patchsample project to generate apk, and copy it to \"/Android/data/com.taobao.dexposed/cache/patch.apk\"")
                    .setPositiveButton("ok", new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int whichButton) {
                        }
                    }).create().show();
        }
    }
    

    补丁apk只有一个名为DialogPatch的类,实现了patchloader函数库中的IPatch接口,IPatch接口代码如下所示:

    /**
     * The interface implemented by hotpatch classes.
     */
    public interface IPatch {
        
        void handlePatch(PatchParam lpparam) throws Throwable;
        
    }
    

    DialogPatch类实现IPatch的handlePatch函数,在该函数中通过反射得到宿主APK中com.taobao.dexposed.MainActivity类实例,然后调用dexposedbridge函数库中的DexposedBridge.findAndHookMethod函数,对MainActivity中的showDialog函数进行Hook操作,替换宿主apk中的相应代码,DialogPatch代码如下所示:

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

    最后宿主apk通过调用patchloader函数库提供的PatchMain.load函数来动态加载下载到的补丁apk,加载代码如下所示:

    // Run patch apk
    public void runPatchApk(View view) {
        Log.d("dexposed", "runPatchApk button clicked.");
        if (isLDevice) {
            showLog("dexposed", "It doesn't support this function on L device.");
            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());
            }
        }
        showDialog();
    }
    

    为便于理解,这里也把load函数体贴出来,更详细内容大家可以看源码:

    /**
     * Load a runnable patch apk.
     *
     * @param context the application or activity context.
     * @param apkPath the path of patch apk file.
     * @param contentMap the object maps that will be used by patch classes.  
     * @return PatchResult include if success or error detail.
     */
    public static PatchResult load(Context context, String apkPath, HashMap<String, Object> contentMap) {
            
        if (!new File(apkPath).exists()) {
            return new PatchResult(false, PatchResult.FILE_NOT_FOUND, "FILE not found on " + apkPath);
        }
    
        PatchResult result = loadAllCallbacks(context, apkPath,context.getClassLoader());
        if (!result.isSuccess()) {
            return result;
        }
    
        if (loadedPatchCallbacks.getSize() == 0) {
            return new PatchResult(false, PatchResult.NO_PATCH_CLASS_HANDLE, "No patch class to be handle");
        }
            
        PatchParam lpparam = new PatchParam(loadedPatchCallbacks);      
        lpparam.context = context;
        lpparam.contentMap = contentMap;
            
        return PatchCallback.callAll(lpparam);
    }
    

    支持的系统版本

    Dexposed支持从Android2.3到4.4(除了3.0)的所有dalvid运行时arm架构的设备,稳定性已经经过实践检验。

    支持的系统版本:

    • Dalvik 2.3
    • Dalvik 4.0~4.4

    不支持的系统版本:

    • Dalvik 3.0
    • ART 5.1
    • ART M

    测试中的系统版本:

    • ART 5.0

    未经测试的系统版本:

    • Dalvik 2.2

    使用Dexposed的项目

    目前阿里系主流app例如手机淘宝,支付宝,天猫都使用了Dexposed支持在线热更新,而开源项目中,在Github上面能搜到的只有一个XLog项目,它的主要功能是方便的打印函数调用和耗时日志,这也是一个了解Dexposed如何使用的很好的例子。

    参考资料

    欢迎关注我的微信公众号

    相关文章

      网友评论

      • 17097fa44b74:你好,想问一下主app做过混淆后,会不会影响补丁的正常使用?
        asce1885:@super_shanks 你想实现什么功能?
        super_shanks:@asce1885 请问现在用什么替代,大神
        asce1885:@Rosinante dexposed已经废弃了,不要用
      • bfcc66a76162:楼主你好,请问在android6.0能否使用这个框架?如果不能的话,6.0应如何使用AOP,谢谢。
      • 58e3baf21353:楼主你好,请问如果我新增了 某一些方法的时候,findAndHookMethod 还能这样做吗?
      • 08624576ed5d: 你好 我想问问 宿主apk 如何调用的 runpathapk 方法
        asce1885:@woshilaomaotao 这个需要根据具体业务需求决定,一般在应用启动时去服务器端检查是否需要patch
        08624576ed5d:runpatchapk 是在什么情况下使用的?? 是宿主apk 开启service 隔一段时间 访问服务器 然后根据返回值 判断是否调用runpachapk 还是服务器 主动推送 。。。 然后在调用还是 怎么 。。。 不太明白 这里
      • 6565ec25845c:楼主好,DexposedBridge能否实现hook经过binder通信的服务端services里面的api呢,主要应该是不在同一个进程里,不知道DexposedBridge能否实现
      • 3c587d8878fa:@asce1885 您好,我看了您的这篇文章动手尝试,但是不知道为何一直抛出异常“Find patch class exception”,我内部跟踪代码【DexFile dexFile = DexFile.loadDex(apkPath, apkPath + "odex", 0);】抛出,但是不知道具体原因,不知道您在尝试的时候是否遇见过这个问题。
        678cd2f05dea:@3c587d8878fa
        odex文件,是优化后的dex文件,一般做动态加载技术时,不会直接拿odex文件做插件;此外,4.0及以后,DexClassLoader是不允许直接用dex或者jar文件来直接进行动态加载解析处理的,需以标准的apk文件格式方可进行处理。最好是贴出完整异常堆栈信息。类动态加载技术应用起来,有一定约束条件,简单死板,但原因通常也就那几个,便于定位问题。阿里这套技术开源出来,很是不错。
        加载前的校验工作是重点的重点,这部分应该是要由框架且在so中来进行保证的,按照@fengcunhan: http://my.oschina.net/fengcunhan/blog/487296 这个中的说法,若是由应用层自行进行校验,网络信息校验且不说,若签名校验也是由应用层且不幸地是在java层进行这个校验,表示后果堪忧啊。

        3c587d8878fa:@asce1885 odex文件是否会生成在你补丁apk文件所在目录?
        asce1885:@3c587d8878fa 这个没遇到过,先确定你的系统版本这个函数库是否支持,支持的话,可以到github上提issue,会有人帮你解答的
      • 0353d4c87012:楼主你好,你能再稍微详细一点说明一下这个框架在AOP中(Android平台上)的应用吗?我之前主要用的是AspectJ,之前想把AspectJ应用到Android应用上,查了些资料,主要是老外写的,都没能很好的实现。最近我也在关注阿里的这个框架,但还没有搞明白,希望你能指点一二,不胜感激。望回复,谢谢!
        asce1885:@changchun_z 官方提供的Demo应该算比较清楚了
        0353d4c87012:@asce1885 好的,谢谢楼主。那请问楼主你现在有没有在Android应用上的一个AOP实例呢?或者是用这个框架写的关于AOP的一个demo。能否给我借鉴一下。谢谢!
        asce1885:@changchun_z 这个库提供的aop能力见文章中aop一节,如何使用这个得具体问题具体分析了
      • asce1885:@fengcunhan good,不过这种方式还是会被攻击者很容易绕过,在apk打包阶段可以增加可以选择加固来进一步提升安全性
      • asce1885:@WeiboLei 是的,app需要集成下载和管理补丁apk的功能,同时要考虑安全性问题,防止补丁apk被非法替换或者修改
        SeyzFq:@asce1885 https://github.com/fengcunhan/hotpatch 做了md5校验和签名一致校验。 详细介绍:http://my.oschina.net/fengcunhan/blog/487296
      • asce1885:@lighters 这个需要结合插件动态加载相关的技术了
      • 5665c3837609:热更新的patch放在我们自己的服务器嘛?如果放我们自己服务器的话,是不是还要在app内集成下载的功能 ?
        SeyzFq:@WeiboLei 嗯,下载功能在Hotpatch包里面已经做好了,你的服务器只需要提供Patch信息(包括Patch包下载url和md5值)
        SeyzFq:@WeiboLei http://my.oschina.net/fengcunhan/blog/487296 做好了,直接用就好了
      • alighters:这个技术真是太叼了,话说这里该如何解决不同apk下的资源加载问题?

      本文标题:Android平台免Root无侵入AOP框架Dexposed使用

      本文链接:https://www.haomeiwen.com/subject/pjvvqttx.html