热修复 阿里的DexPosed

时间:2021-11-28 13:50:52

介绍

GitHub  

重要提醒:
  • 1、此方案不支持ART(Android 5.0 及之后的Android版本),这一点是致命的!
  • 2、此方案不支持Dalvik 3.0(专为平板设计的Android版本)
  • 3、只支持 ARM 架构
基于此,注定它会逐步失声,再多的优点也是徒劳。

dexposed enable 'god' mode上帝视角 for single android application.
热修复 阿里的DexPosed

基本实现原理简介

下面是一段比较官方的介绍:
Dexposed中的AOP原理来自于Xposed。在Dalvik虚拟机下,主要是通过改变一个方法对象方法在Dalvik虚拟机中的定 义来实现,具体做法就是将该方法的类型改变为native并且将这个方法的实现链接到一个通用的Native Dispatch方法上。这个 Dispatch方法通过JNI回调到Java端的一个统一处理方法,最后在统一处理方法中调用before, after函数来实现AOP。在Art虚拟机上目前也是是通过改变一个 ArtMethod的入口函数来实现。

从上面可以知道基础的实现过程,另外上面提到了两个概念,Dalvik虚拟机和Art虚拟机,这里稍稍做做科普。

什么是Dalvik:
Dalvik是Google公司自己设计用于Android平台的Java虚拟机。Dalvik虚拟机是Google等厂商合作开发的Android移动设备平台的核心组成部分之一。它可以支持已转换为 .dex(即Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

什么是ART:
Android操作系统已经成熟,Google的Android团队开始将注意力转向一些底层组件,其中之一是负责应用程序运行的Dalvik运行时。Google开发者已经花了两年时间开发更快执行效率更高更省电的替代ART运行时。 ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

ART优点:
  • 系统性能的显著提升。
  • 应用启动更快、运行更快、体验更流畅、触感反馈更及时。
  • 更长的电池续航能力。
  • 支持更低的硬件。

ART缺点:
  • 更大的存储空间占用,可能会增加10%-20%。
  • 更长的应用安装时间。

总的来说ART的功效就是“空间换时间”。

google在android4.4之后的版本都用art取代了dalvik,所以要hook android4.4以后的版本就必须去适配art虚拟机的机制。这也解释了为什么会有dexposed和dexposed_l两个so。目前官方表示,为了适配art的dexposed_l只是beta版,所以最好不要在正式的线上产品中使用它。

PS:从代码上看两个 so 文件的区别:
public synchronized static boolean canDexposed(Context context) {
   if (!DeviceCheck.isDeviceSupport(context)) {
      return false;
   }
   return loadDexposedLib(context); //load xposed lib for hook.
}
6
6
 
1
public synchronized static boolean canDexposed(Context context) {
2
   if (!DeviceCheck.isDeviceSupport(context)) {
3
      return false;
4
   }
5
   return loadDexposedLib(context); //load xposed lib for hook.
6
}
private static boolean loadDexposedLib(Context context) {
   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;
   }
}
13
13
 
1
private static boolean loadDexposedLib(Context context) {
2
   try {
3
      if (android.os.Build.VERSION.SDK_INT > 19){
4
         System.loadLibrary("dexposed_l");
5
      } else if (android.os.Build.VERSION.SDK_INT == 10
6
            || android.os.Build.VERSION.SDK_INT == 9 || android.os.Build.VERSION.SDK_INT > 14){
7
         System.loadLibrary("dexposed");
8
      }
9
      return true;
10
   } catch (Throwable e) {
11
      return false;
12
   }
13
}
可以看到,在sdk版本大于19时,会去调用dexposed_l,否则会调用dexposed。

jar包的java部分代码结构很简洁,DexposedBrigde是主要的功能调用类,XposedHelpers则是一个反射功能类,其他则为一些辅助类。
DexposedBrigde中用来hook的方法 findAndHookMethod 的执行过程大致为:
  • 首先会通过 XposedHelpers 这个反射工具类的 findMethodExact 方法来找到对应的 Method。
  • 然后会执行 hookMethod 方法。
  • 然后里面会调用 hookMethodNative 方法,我们需要给 hookMethodNative 传递的参数分别是
    • 根据反射定位到得要hook得Method;
    • 声明定义Method的class;
    • slot标记runtime的标记符;
    • 包含函数入参,返回值类型以及对应回调的自定义类。
  • 这样就完成了对应的hook。

官方文档

What is it?

Dexposed is a powerful yet并且 non-invasive 非侵入性的 runtime AOP (Aspect-oriented Programming) framework for Android app development, based on the work of open-source Xposed framework project.

The AOP of Dexposed is implemented purely纯粹的 non-invasive, without any annotation processor, weaver or bytecode rewriter. The integration is as simple as loading a small JNI library in just one line of code at the initialization phase初始化阶段 of your app.

Not only the code of your app, but also the code of Android framework that running in your app process can be hooked. This feature is extremely useful in Android development as we developers heavily rely on严重依赖 the fragmented old versions of Android platform (SDK).

Together with dynamic class loading, a small piece of compiled Java AOP code can be loaded into the running app, effectively altering the behavior of the target app without restart.
结合动态类加载,可以将一小段编译好的Java AOP代码加载到正在运行的应用程序中,从而有效地改变目标应用程序的行为而无需重新启动。

Typical use-cases 典型使用场景

Classic AOP programming    典型的 AOP 编程
Instrumentation (for testing, performance monitoring and etc.)    仪表化 (测试,性能监控等等)
Online hot patch to fix critical, emergent or security bugs    在线热补丁修复关键,紧急或安全漏洞
SDK hooking for a better development experience    更好的开发体验

Integration

patchloader.jar 导入工程

Directly add dexposed aar to your project as compile libraries, it contains a jar file "dexposedbridge.jar" two so files "libdexposed.so libdexposed_l.so" from 'dexposed' directory.
compile 'com.taobao.android:dexposed:0.1.1@aar'
1
1
 
1
compile 'com.taobao.android:dexposed:0.1.1@aar'
注意:这里aar文件只包含了armeabi架构的so文件。
注意:不要忘了加载so库。
System.loadLibrary("dexposed");
1
1
 
1
System.loadLibrary("dexposed");

Insert the following line into the initialization phase of your app, as early as possible:
// Check whether current device is supported (also initialize Dexposed framework if not yet)
if (DexposedBridge.canDexposed(this)) {
   Log.i("bqt", "Use Dexposed to kick off AOP stuffs.");
} else {
   Log.i("bqt", "Not Supported");
}
6
6
 
1
// Check whether current device is supported (also initialize Dexposed framework if not yet)
2
if (DexposedBridge.canDexposed(this)) {
3
   Log.i("bqt", "Use Dexposed to kick off AOP stuffs.");
4
} else {
5
   Log.i("bqt", "Not Supported");
6
}

Basic usage

There are three injection points for a given method: before, after, replace.

在指定方法前后执行方法  纯AOP

Example 1: Attach a piece of code before and after all occurrences of 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);
   }
});
26
26
 
1
// Target class, method with parameter types, followed by the hook callback (XC_MethodHook).
2
DexposedBridge.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodHook() {
3
   
4
   // To be invoked before Activity.onCreate().
5
   @Override
6
   protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
7
      // "thisObject" keeps the reference to the instance of target class.
8
      Activity instance = (Activity) param.thisObject;
9
      
10
      // The array args include all the parameters.
11
      Bundle bundle = (Bundle) param.args[0];
12
      Intent intent = new Intent();
13
      // XposedHelpers provide useful utility methods.
14
      XposedHelpers.setObjectField(param.thisObject, "mIntent", intent);
15
      
16
      // Calling setResult() will bypass the original method body use the result as method return value directly.
17
      if (bundle.containsKey("return"))
18
         param.setResult(null);
19
   }
20
   
21
   // To be invoked after Activity.onCreate()
22
   @Override
23
   protected void afterHookedMethod(MethodHookParam param) throws Throwable {
24
      XposedHelpers.callMethod(param.thisObject, "sampleMethod", 2);
25
   }
26
});
DexposedBridge.findAndHookMethod(Log.class, "d", String.class, String.class, new XC_MethodHook() {
   @Override
   protected void afterHookedMethod(MethodHookParam arg0) throws Throwable {
      String tag = (String) arg0.args[0];
      String msg = (String) arg0.args[1];
      System.out.println(tag + "," + msg);
   }
});
8
8
 
1
DexposedBridge.findAndHookMethod(Log.class, "d", String.class, String.class, new XC_MethodHook() {
2
   @Override
3
   protected void afterHookedMethod(MethodHookParam arg0) throws Throwable {
4
      String tag = (String) arg0.args[0];
5
      String msg = (String) arg0.args[1];
6
      System.out.println(tag + "," + msg);
7
   }
8
});

完全替换某一指定方法  纯AOP

Example 2: Replace the original body of the target method.
可以将原有要执行的函数替换成一个我们需要的新的执行函数, 如:替换  Activity. onCreate( Bundle savedInstanceState ) 方法
DexposedBridge.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodReplacement() {
   @Override
   protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
      Log.i("bqt", "Re-writing the method logic outside the original method context is a bit tricky but still viable");
      return new Object();
   }
});
7
7
 
1
DexposedBridge.findAndHookMethod(Activity.class, "onCreate", Bundle.class, new XC_MethodReplacement() {
2
   @Override
3
   protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
4
      Log.i("bqt", "Re-writing the method logic outside the original method context is a bit tricky but still viable");
5
      return new Object();
6
   }
7
});

用于热修复

使用的前提是,我们需先创建一个热修复工程,打包后在我们项目中动态下载下来。(当然,因为里面基本没什么逻辑,所以包很小,不用担心网络和下载时间的影响)

补丁项目参考: dexposed/sample/patchsample
This project can generate an apk which will be run by dexposedexamples.
The patchloader.jar is export from从...导出的 dexposedexamples "com.taobao.patch" package.

原始项目参考: dexposed/sample/dexposedexamples
原始项目修复bug的核心逻辑:
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.i("bqt", "hotPath load apk success.");
   } else {
      Log.i("bqt", "hotPath load apk error.  " + result.getErrorInfo());
   }
}
10
 
1
File cacheDir = getExternalCacheDir();
2
if (cacheDir != null) {
3
   String fullpath = cacheDir.getAbsolutePath() + File.separator + "patch.apk";//这个是我们下载到本地的热修复包
4
   PatchResult result = PatchMain.load(this, fullpath, null);
5
   if (result.isSuccess()) {
6
      Log.i("bqt", "hotPath load apk success.");
7
   } else {
8
      Log.i("bqt", "hotPath load apk error. " + result.getErrorInfo());
9
   }
10
}

Support

Dexposed support all dalvik runtime arm architecture devices from Android 2.3 to 4.4 (no include 3.0). The stability稳定性 has been proved证实 in our long term product practice实践.

Follow is support status.
Runtime Android Version Support
Dalvik 2.2 Not Test
Dalvik 2.3 Yes
Dalvik 3 No
Dalvik 4.0-4.4 Yes
ART 5 Testing
ART 5.1 No
ART M No

2018-6-9