Android免Root无侵入AOP框架Dexposed

时间:2023-03-09 06:10:50
Android免Root无侵入AOP框架Dexposed

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

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

如何在Android Studio集成Dexposed

关于在Android Studio中集成Dexposed,很简单,只需要导入两个jar包和两个so文件,jar包和so文件都可以在github上下载传送门,关于jar的导入大家肯定很清楚,只需要把对应的jar包放入libs目录下,然后添加项目依赖即可,而关于so文件的导入,不同的AS版本便不一样了,较早期的版本可能更麻烦些,而比较新的版本则非常方便,我用的是AS1.4Beta2版的,所以只需要在src/main目录下新建一个jniLibs目录,然后把so文件放入进去即可,当我们打包成apk的时候自到会将so文件添加进去。所以在Android Studio中集成Dexposed的配置如下:

1、添加so文件:

Android免Root无侵入AOP框架Dexposed

2、添加jar依赖:

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

Dexposed支持的SDK版本

Dexposed目前只支持从Android2.3到4.4(除了3.0)的所有Dalvid运行时arm架构的设备,而Android 5.0以后摒弃了Dalvid而使用ART模式,所以说在api19以上的系统目前来说都不支持,不过阿里的团队也正在测试Android5.0加入对ART的支持。

—–支持的系统版本:

Dalvik 2.3

Dalvik 4.0~4.4

—–不支持的系统版本:

Dalvik 3.0

ART 5.1

ART M

ART 6.0

Dexposed应用场景

Dexposed实现的hooking,不仅可以hook应用中的自定义函数,也可以hook应用中调用的Android框架的函数。不过Dexposed只能hook函数和构造器,我们从源码中就可以看出:

Android免Root无侵入AOP框架Dexposed

应用场景:

1、 AOP编程

2、插桩(例如测试,性能监控等)

3、在线热更新,修复严重的,紧急的或者安全性的bug

4、SDK hooking以提供更好的开发体验

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

Dexposed的用法

1、检查当前系统是否支持Dexposed

首先上面我们讲了由于Dexposed框架目前并不是支持所有的系统,所以我们在使用Dexposed框架的时候应该对当前系统的版本进行检查是否支持Dexposed,不过这段检查代码Dexposed内部已经封装好了,所以我们只需要调用DexposedBridge.canDexposed(this)方法即可,我们应该尽可能的在程序一启动时候完成检查,如:

public class MyApplication extends Application {
    private static boolean canDexPosed = false;

    @Override
    public void onCreate() {
        super.onCreate();
        canDexPosed = DexposedBridge.canDexposed(this);
        if(canDexPosed){
            //do something
        }
    }

    public static boolean isCanDexPosed() {
        return canDexPosed;
    }
}

我们可以看看Dexposed源码里面是怎么完成对系统的检查的:

public static synchronized boolean canDexposed(Context context) {
        return !DeviceCheck.isDeviceSupport(context)?false:loadDexposedLib(context);
    }

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 static synchronized boolean isDeviceSupport(Context context) {
        boolean var2;
        try {
            if(!isCheckedDeviceSupport) {
                if(isDalvikMode() && isSupportSDKVersion() && !isX86CPU() && !isYunOS()) {
                    isDeviceSupportable = true;
                    return isDeviceSupportable;
                }

                isDeviceSupportable = false;
                return isDeviceSupportable;
            }

            var2 = isDeviceSupportable;
        } finally {
            Log.d("hotpatch", "device support is " + isDeviceSupportable + "checked" + isCheckedDeviceSupport);
            isCheckedDeviceSupport = true;
        }

        return var2;
    }

2、Dexposed基本功能

Dexposed框架有三个注入点可供选择:函数执行前注入(before),函数执行后注入(after),替换函数执行的代码段(replace)。

其中函数执行前注入和函数执行后注入用的是同一个抽象类:XC_MethodHook,而替换函数执行代码段的抽象类为:XC_MethodReplacement。

这三点注入点对应的抽象类中的方法名分别为:

1、XC_MethodHook抽象类中函数执行前注入和函数执行后注入的两个对应方法为:

 protected void beforeHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
    }

 protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
    }

2、XC_MethodReplacement抽象类中替换执行的代码段的方法为:

protected abstract Object replaceHookedMethod(MethodHookParam var1) throws Throwable;

所以我们只要实现上述中相应的方法,便可以在某个函数执行前、函数执行后或者在要替换的函数中执行相应的代码。那么怎么使用呢?就是通过DexposedBridge.findAndHookMethod(Class<?> clazz, String methodName, Object... parameterTypesAndCallback)

该方法的参数意思为:

1、clazz - 就是hook的方法所在的类的字节码对象

2、methodName - 就是hook的方法名

3、parameterTypesAndCallback - 可变参数,如果hook的方法中有参数的话,需要在此传入参数类型,如String则传入String.class,这和反射一样的,如果没有参数则不传。在最后还需要传入XC_MethodHook或者XC_MethodReplacement对象

如:

1、在getMsg方法执行前或者执行后做相应的操作:

DexposedBridge.findAndHookMethod(clazz, "getMsg", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                super.beforeHookedMethod(param);
                //do something
            }

            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                //do something
            }
        });

2、替换getMsg方法中的代码:

DexposedBridge.findAndHookMethod(clazz, "getMsg", new XC_MethodReplacement() {
            @Override
            protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                //replace code
                return null;
            }
        });

我们可以看到在上面三个方法中,都会传入一个MethodHookParam类型的参数,这个参数包含了一些很重要的信息:

  • MethodHookParam.thisObject:被hook方法所在类的一个实例(相当于clazz类的一个实例)
  • MethodHookParam.args:用于传递被hook方法的所有参数,它返回的是一个Object[]对象,通过它如果hook方法有参数传入的话,用这个可以获取到这些参数的值
  • MethodHookParam.setResult:用于修改hook方法调用的结果,如果在beforeHookedMethod回调函数中调用setResult,可以阻止对hook方法的调用。但是如果有返回值的话仍然需要通过hook处理器进行return操作

Dexposed的应用示例

AOP编程

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

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

比如我们可以在应用的某些Activity中onCreate(Bundle)方法调用之前和之后做一些处理:

在MainActivity中onCreate方法调用之前预分配一些值或者调用一些方法:

public class MyApplication extends Application {
    private static boolean canDexPosed = false;

    @Override
    public void onCreate() {
        super.onCreate();
        canDexPosed = DexposedBridge.canDexposed(this);
        if(canDexPosed){
            Class<?> clazz = null;
            try {
                clazz = getClassLoader().loadClass("com.sunzxyong.dexposeddemo.MainActivity");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            DexposedBridge.findAndHookMethod(clazz, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                    Log.v("zxy", "before onCreate ");

                    //为Activity预先分配一些值
XposedHelpers.setObjectField(param.thisObject,"content","sunzxyong");
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    Log.v("zxy", "after onCreate ");
                }
            });
        }
    }

    public static boolean isCanDexPosed() {
        return canDexPosed;
    }
}

然后在MainActivity:

public class MainActivity extends Activity {
    private String content;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.v("zxy", "in onCreate ");
        Log.v("zxy","content="+content);
}

运行后可以看到打印的Log为:

09-10 13:16:34.950  28373-28373/? V/zxy﹕ before onCreate
09-10 13:16:35.265  28373-28373/? V/zxy﹕ in onCreate
09-10 13:16:35.265  28373-28373/? V/zxy﹕ content=sunzxyong
09-10 13:16:35.265  28373-28373/? V/zxy﹕ after onCreate

可以看到预分配值的方法beforeHookedMethod()确实是在onCreate方法执行前执行,而onCreate方法执行后又执行了afterHookedMethod()方法。

其中XposedHelpers这个帮助类提供了很多方法,比如调用某个类中的某个方法,设置变量的值等等。

当然也可以使用如下代码替换onCreate方法中的代码:

 DexposedBridge.findAndHookMethod(clazz, "onCreate", Bundle.class, new XC_MethodReplacement() {
                @Override
                protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                    //relpace code
                    return null;
                }
            });

在线热更新

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

我们假设刚刚上线的apk中的MainActivity类中的getMsg(String str)方法出现了bug,然后我们采用在线热更新的方法去修复这个bug。

getMsg()方法:

public void getMsg(String str){
        new AlertDialog.Builder(this,AlertDialog.THEME_HOLO_LIGHT).setTitle("提示").setMessage(str).setPositiveButton("确定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                dialog.dismiss();
            }
        }).create().show();
    }

1、首先我们需要一个补丁apk

我们创建一个叫PatchApk的Module作为补丁apk的工程,然后为它添加jar包依赖:

dependencies {
    provided fileTree(include: ['*.jar'], dir: 'libs')
    provided files('libs/patchloader.jar')
    provided files('libs/dexposedbridge.jar')
}

这里需要注意的是,由于补丁apk也添加了jar包依赖,而我们的宿主apk中也有jar包依赖,如果补丁apk中添加的jar包依赖是以compile的方式添加的话,则会报重复包引用错误,错误如下:

Caused by: java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation

所以我们在补丁apk中需要以provided提供的方式添加jar包依赖。

然后我们创建一个PatchMsg类,意思就是getMsg方法的补丁类,此时我们需要将PatchMsg补丁类实现IPatch接口,然后实现该接口的handlePatch方法,顾名思义,这个方法就是处理补丁的方法。然后通过DexposedBridge.findAndHookMethod()方法来hook在MainActivity中的getMsg方法,然后实现replaceHookedMethod方法进而替换掉宿主apk中getMsg()中的代码。其中MainActivity的字节码对象可以由包名+类名反射得到。

整个PatchMsg类实现如下:

PatchMsg.java

public class MsgPatch implements IPatch {
    @Override
    public void handlePatch(PatchParam patchParam) throws Throwable {

        Class<?> clazz = patchParam.context.getClassLoader().loadClass("com.sunzxyong.dexposeddemo.MainActivity");
        DexposedBridge.findAndHookMethod(clazz, "getMsg", String.class, new XC_MethodReplacement() {
            @Override
            protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {
                Activity activity = (Activity) methodHookParam.thisObject;
                new AlertDialog.Builder(activity, AlertDialog.THEME_HOLO_LIGHT).setTitle("提示").setMessage("这是补丁信息--->使用的是无侵入AOP插件Dexposed!").setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                }).create().show();

                String msg = (String) methodHookParam.args[0];//得到宿主apk中getMsg方法传递过来的参数值
                Log.v("zxy", msg);//Dexposed hello!
                //由于getMsg方法没有返回值则返回null
                return null;
            }
        });
    }
}

好了,补丁类已经创建好了,此时我们可以把补丁apk中的无关类删了,比如MainActivity。

2、宿主apk加载补丁apk从而修复bug

我们假如宿主apk已经在线下下载好了补丁apk,并将补丁apk放在了cache目录中。

最后我们在宿主apk中通过调用PatchMain.load()方法来加载补丁apk,代码如下:

public void click(View view){
        if(!MyApplication.isCanDexPosed()){
            Log.v("zxy","can not support DexPosed!");
            return;
        }

        String patchPath = this.getCacheDir().getPath()+ File.separator+"patch.apk";
        PatchResult result = PatchMain.load(this, patchPath, null);
        if(result.isSuccess()){
            new AlertDialog.Builder(this,AlertDialog.THEME_HOLO_LIGHT).setTitle("提示").setMessage("加载MsgPatch补丁成功!").setPositiveButton("确定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    dialog.dismiss();
                }
            }).create().show();
        }
    }

这样,当加载补丁成功后,即用补丁中的代码替换了宿主apk中getMsg中的代码,从而修复了bug。

效果图:

这个demo用genymotion演示不了,只能用真机,然后360录屏太渣渣,录的有点不好,鼠标点击顺序依次是:展示信息->加载补丁->展示信息->退出->再次打开,展示信息。这个效果充分说明了在线热更新修复bug非常方便

Android免Root无侵入AOP框架Dexposed

Demo源码下载

参考资料: