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

参考资料

欢迎关注我的微信公众号

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,450评论 25 707
  • title: Android AOP之字节码插桩author: 陶超description: 实现数据收集SDK时...
    陶菜菜阅读 37,823评论 40 182
  • 热更新相关概念 组件化---就是将一个app分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可...
    wangling90阅读 1,688评论 0 0
  • 有天,我问智者:“是不是任何事情都可以自己做?” 他答:“是的。” 我的心情低落到了谷底,因为他这句言简意赅的真相...
    桃花太红李太白呀阅读 311评论 0 0
  • 我讨厌三人行的 我只想和你一起 只属于我一个人的 喜欢你讨厌你 你的我的 放弃你 我会找到只属于我的朋友
    你_ee51阅读 117评论 0 0