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

@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如何使用的很好的例子。

参考资料

文末摄影鉴赏

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-08-04 00:50:29

Android平台免Root无侵入AOP框架Dexposed使用详解的相关文章

Android新技术学习——阿里巴巴免Root无侵入AOP框架Dexposed

阿里巴巴无线事业部最近开源的Android平台下的无侵入运行期AOP框架Dexposed,该框架基于AOP思想,支持经典的AOP使用场景,可应用于日志记录,性能统计,安全控制,事务处理,异常处理等方面. 针对Android平台,Dexposed支持函数级别的在线热更新,例如对已经发布在应用市场上的宿主APK,当我们从crash统计平台上发现某个函数调用有bug,导致经常性crash,这时,可以在本地开发一个补丁APK,并发布到服务器中,宿主APK下载这个补丁APK并集成后,就可以很容易修复这个c

Dexposed:android免Root无侵入Aop框架

在网上看到了阿里推出的一个android开源项目,名为Dexposed, 是一个Android平台下的无侵入运行期AOP框架.旨在解决像性能监控.在线热补丁等移动开发常见难题,典型使用场景为: AOP编程 插桩 (如测试.性能监控等) 在线热补丁 SDK hooking以提供更好的开发体验 它基于ROOT社区著名开源项目Xposed改造剥离了ROOT部分,演化为服务于所在应用自身的AOP框架,并在Apache 2.0协议下开源. Xposed是XDA社区用户rovo89开发并管理的一个项目,它通

SSH框架的整合详解

"-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> SSH框架的整合详解 - super_YC的博客 - 博客频道 - CSDN.NET super_YC的博客 记录我生活的一点一滴!我很开心拥有这样一个自己心事的笔记本 目录视图 摘要视图 订阅 [活动]2017 CSDN博客专栏评选 &nbsp [5月书讯]流畅的P

Android 热更新——非侵入AOP框架

Android 客户端应用上线以后,一旦出现Bug,一般的解决思路是发修复包升级应用,这种方式不仅耗时,更重要的是用户需要频繁的升级版本,体验不好,所以优化的思路是在不发版本的情况下热更新,以期提高用户体验. 近期GitHub新出一种非侵入运行期AOP框架Dexposed, 下面简单了解一下这个框架,GitHub地址. 简要说明: 该框架基于AOP思想,支持经典的AOP使用场景,可应用于日志记录,性能统计,安全控制,事务处理,异常处理等方面. 针对Android平台,Dexposed支持函数级别

Android进阶中级教程——1.1 Git的本地使用详解

Android进阶中级教程--1.1 Git的本地使用详解 标签(空格分隔): Android进阶 1.引言 在之前的Android基础入门系列我们就讲解过Git的简单使用了,进阶系列我们 系统地对Git进行讲解,基本命令的使用:工作区,暂存区,历史仓库,远程仓库的概念, 团队协作中的分支管理,Android Studio中使用Git等:Git是一个快速的分布式的版本 控制系统,和其他的版本控制系统的差别在于Git直接记录快照,而不是差异比较! 差异比较的版本控制系统只关心文件内容的具体差异,然

Android基础入门教程——2.3.1 TextView(文本框)详解

Android基础入门教程--2.3.1 TextView(文本框)详解 标签(空格分隔): Android基础入门教程 本节引言: 学习完Android中的六大布局,从本节开始我们来一个个讲解Android中的UI控件,本节给大家带来的UI控件是:TextView(文本框),用于显示文本的一个控件,另外声明一点,我不是翻译API文档,不会一个个属性的去扣,只学实际开发中常用的,有用的,大家遇到感觉到陌生的属性可以查询对应的API!当然,每一节开始都会贴这一节对应API文档的链接:TextVie

java的集合框架最全详解

java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作的方法. 在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类).所有抽象出来的数据结构和操作(算法)统称为Java集合框架(JavaCollectionFramework). Java程序员在具体应用时,不必考虑数据结构和算法实现细节,只需要用这

Java的集合框架最全详解(图)

纯个人整理,如有错误请指正. java的集合框架最全详解(图) 前言:数据结构对程序设计有着深远的影响,在面向过程的C语言中,数据库结构用struct来描述,而在面向对象的编程中,数据结构是用类来描述的,并且包含有对该数据结构操作的方法. 在Java语言中,Java语言的设计者对常用的数据结构和算法做了一些规范(接口)和实现(具体实现接口的类).所有抽象出来的数据结构和操作(算法)统称为Java集合框架(JavaCollectionFramework). Java程序员在具体应用时,不必考虑数据

Spring MVC 框架搭建及详解

一.Spring MVC环境搭建:(Spring 2.5.6 + Hibernate 3.2.0) 1. jar包引入 Spring 2.5.6:spring.jar.spring-webmvc.jar.commons-logging.jar.cglib-nodep-2.1_3.jar Hibernate 3.6.8:hibernate3.jar.hibernate-jpa-2.0-api-1.0.1.Final.jar.antlr-2.7.6.jar.commons-collections-3