Android平臺免Root無侵入AOP框架Dexposed使用詳解

jopen 9年前發布 | 49K 次閱讀 Dexposed Android開發 移動開發

<p>@author ASCE1885的 Github 簡書 微博 CSDN </p>

阿里巴巴無線事業部最近開源的Android平臺下的無侵入運行期AOP框架Dexposed,該框架基于AOP思想,支持經典的AOP使用場景,可應用于日志記錄,性能統計,安全控制,事務處理,異常處理等方面。

針對Android平臺,Dexposed支持函數級別的在線熱更新,例如對已經發布在應用市場上的宿主APK,當我們從crash統計平臺上發現某個函數調用有bug,導致經常性crash,這時,可以在本地開發一個補丁APK,并發布到服務器中,宿主APK下載這個補丁APK并集成后,就可以很容易修復這個crash。

</blockquote>

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

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

參考資料

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!