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

jopen 9年前

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

参考资料