原文:http://www.cnblogs.com/zjoch/p/6018432.html
Dexposed是基于久負(fù)盛名的開源Xposed框架實(shí)現(xiàn)的一個(gè)Android平臺(tái)上功能強(qiáng)大的無侵入式運(yùn)行時(shí)AOP框架。
Dexposed的AOP實(shí)現(xiàn)是完全非侵入式的,沒有使用任何注解處理器,編織器或者字節(jié)碼重寫器。集成Dexposed框架很簡(jiǎn)單,只需要在應(yīng)用初始化階段加載一個(gè)很小的JNI庫(kù)就可以,這個(gè)加載操作已經(jīng)封裝在DexposedBridge函數(shù)庫(kù)里面的canDexposed函數(shù)中,源碼如下所示:
/** * 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實(shí)現(xiàn)的hooking,不僅可以hook應(yīng)用中的自定義函數(shù),也可以hook應(yīng)用中調(diào)用的Android框架的函數(shù)。Android開發(fā)者將從這一點(diǎn)得到很多好處,因?yàn)槲覀儑?yán)重依賴于Android SDK的版本碎片化。
基于動(dòng)態(tài)類加載技術(shù),運(yùn)行中的app可以加載一小段經(jīng)過編譯的Java AOP代碼,在不需要重啟app的前提下實(shí)現(xiàn)修改目標(biāo)app的行為。
典型的使用場(chǎng)景
- AOP編程
- 插樁(例如測(cè)試,性能監(jiān)控等)
- 在線熱更新,修復(fù)嚴(yán)重的,緊急的或者安全性的bug
- SDK hooking以提供更好的開發(fā)體驗(yàn)
如何集成
集成方式很簡(jiǎn)單,只需要將一個(gè)jar包加入項(xiàng)目的libs文件夾中,同時(shí)將兩個(gè)so文件添加到j(luò)niLibs中對(duì)應(yīng)的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是一個(gè)第三方插件,使用方法可參考《如何在Android Gradle中添加原生so文件依賴》。當(dāng)然,我們也可以直接把需要用到的so文件直接拷貝到j(luò)niLibs目錄中,這樣的話,可以把上面的native_dependencies代碼段注釋掉。
同時(shí)應(yīng)該在應(yīng)用初始化的地方(盡可能早的添加)添加初始化Dexposed的代碼,例如在MyApplication中添加:
public class MyApplication extends Application { private boolean mIsSupported = false; // 設(shè)備是否支持dexposed private boolean mIsLDevice = false; // 設(shè)備Android系統(tǒng)是否是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; } }
基本用法
對(duì)于某個(gè)函數(shù)而言,有三個(gè)注入點(diǎn)可供選擇:函數(shù)執(zhí)行前注入(before),函數(shù)執(zhí)行后注入(after),替換函數(shù)執(zhí)行的代碼段(replace),分別對(duì)應(yīng)于抽象類XC_MethodHook及其子類XC_MethodReplacement中的函數(shù):
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; }
可以看到這三個(gè)注入回調(diào)函數(shù)都有一個(gè)類型為MethodHookParam的參數(shù),這個(gè)參數(shù)包含了一些很有用的信息:
- MethodHookParam.thisObject:這個(gè)類的一個(gè)實(shí)例
- MethodHookParam.args:用于傳遞被注入函數(shù)的所有參數(shù)
- MethodHookParam.setResult:用于修改原函數(shù)調(diào)用的結(jié)果,如果在beforeHookedMethod回調(diào)函數(shù)中調(diào)用setResult,可以阻止對(duì)原函數(shù)的調(diào)用。但是如果有返回值的話仍然需要通過hook處理器進(jìn)行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),也就是面向方面編程,是通過預(yù)編譯方式和運(yùn)行期動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。
AOP一般應(yīng)用在日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理等方面,它的主要意圖是將日志記錄,性能統(tǒng)計(jì),安全控制,事務(wù)處理,異常處理等代碼從業(yè)務(wù)邏輯代碼中劃分出來,通過對(duì)這些行為的分離,我們希望可以將它們獨(dú)立到非業(yè)務(wù)邏輯的方法中,進(jìn)而改變這些行為的時(shí)候不影響業(yè)務(wù)邏輯的代碼。
例如我們可以在應(yīng)用中所有的Activity.onCreate(Bundle)函數(shù)調(diào)用之前和之后增加一些相同的處理:
// 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); } });
當(dāng)然也可以替換目標(biāo)函數(shù)原來執(zhí)行的代碼段:
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. ... } });
例子二:在線熱更新
在線熱更新一般用于修復(fù)線上嚴(yán)重的,緊急的或者安全性的bug,這里會(huì)涉及到兩個(gè)apk文件,一個(gè)我們稱為宿主apk,也就是發(fā)布到應(yīng)用市場(chǎng)的apk,一個(gè)稱為補(bǔ)丁apk。宿主apk出現(xiàn)bug時(shí),通過在線下載的方式從服務(wù)器下載到補(bǔ)丁apk,使用補(bǔ)丁apk中的函數(shù)替換原來的函數(shù),從而實(shí)現(xiàn)在線修復(fù)bug的功能。
為了實(shí)現(xiàn)這個(gè)功能,需要再引入一個(gè)名為patchloader的jar包,這個(gè)函數(shù)庫(kù)實(shí)現(xiàn)了一個(gè)熱更新框架,宿主apk在發(fā)布時(shí)會(huì)將這個(gè)jar包一起打包進(jìn)apk中,而補(bǔ)丁apk只是在編譯時(shí)需要這個(gè)jar包,但打包成apk時(shí)不包含這個(gè)jar包,以免補(bǔ)丁apk集成到宿主apk中時(shí)發(fā)生沖突。因此,補(bǔ)丁apk將會(huì)以provided的形式依賴dexposedbridge.jar和patchloader.jar,補(bǔ)丁apk的build.gradle文件中依賴部分腳本如下所示:
dependencies { provided files('libs/dexposedbridge.jar') provided files('libs/patchloader.jar') }
這里我們假設(shè)宿主apk的MainActivity.showDialog函數(shù)出現(xiàn)問題,需要打補(bǔ)丁,宿主代碼如下所示:(類完整路徑是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(); } }
補(bǔ)丁apk只有一個(gè)名為DialogPatch的類,實(shí)現(xiàn)了patchloader函數(shù)庫(kù)中的IPatch接口,IPatch接口代碼如下所示:
/** * The interface implemented by hotpatch classes. */ public interface IPatch { void handlePatch(PatchParam lpparam) throws Throwable; }
DialogPatch類實(shí)現(xiàn)IPatch的handlePatch函數(shù),在該函數(shù)中通過反射得到宿主APK中com.taobao.dexposed.MainActivity類實(shí)例,然后調(diào)用dexposedbridge函數(shù)庫(kù)中的DexposedBridge.findAndHookMethod函數(shù),對(duì)MainActivity中的showDialog函數(shù)進(jìn)行Hook操作,替換宿主apk中的相應(yīng)代碼,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通過調(diào)用patchloader函數(shù)庫(kù)提供的PatchMain.load函數(shù)來動(dòng)態(tài)加載下載到的補(bǔ)丁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函數(shù)體貼出來,更詳細(xì)內(nèi)容大家可以看源碼:
/** * 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); }
支持的系統(tǒng)版本
Dexposed支持從Android2.3到4.4(除了3.0)的所有dalvid運(yùn)行時(shí)arm架構(gòu)的設(shè)備,穩(wěn)定性已經(jīng)經(jīng)過實(shí)踐檢驗(yàn)。
支持的系統(tǒng)版本:
不支持的系統(tǒng)版本:
測(cè)試中的系統(tǒng)版本:
未經(jīng)測(cè)試的系統(tǒng)版本:
使用Dexposed的項(xiàng)目
目前阿里系主流app例如手機(jī)淘寶,支付寶,天貓都使用了Dexposed支持在線熱更新,而開源項(xiàng)目中,在Github上面能搜到的只有一個(gè)XLog項(xiàng)目,它的主要功能是方便的打印函數(shù)調(diào)用和耗時(shí)日志,這也是一個(gè)了解Dexposed如何使用的很好的例子。
參考資料
文/asce1885(簡(jiǎn)書作者)
原文鏈接:http://www.jianshu.com/p/14edcb444c51
著作權(quán)歸作者所有,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),并標(biāo)注“簡(jiǎn)書作者”。