# DroidAssist `DroidAssist` 是一个轻量级的 Android 字节码编辑插件,基于 `Javassist` 对字节码操作,根据 xml 配置处理 class 文件,以达到对 class 文件进行动态修改的效果。和其他 AOP 方案不同,DroidAssist 提供了一种更加轻量,简单易用,无侵入,可配置化的字节码操作方式,你不需要 Java 字节码的相关知识,只需要在 Xml 插件配置中添加简单的 Java 代码即可实现类似 AOP 的功能,同时不需要引入其他额外的依赖。 > [ Javassist: A Java bytecode engineering toolkit since 1999](http://www.Javassist.org/ "Java bytecode engineering toolkit since 1999") ## 功能 - **替换**:把指定位置代码替换为指定代码 - **插入**:在指定位置的前后插入指定代码 - **环绕**:在指定位置环绕插入指定代码 - **增强**: - **TryCatch** 对指定代码添加 try catch 代码 - **Timing** 对指定代码添加耗时统计代码 ## 特点 * 灵活的配置化方式,使得一个配置就可以处理项目中所有的 class 文件。 * 丰富的字节码处理功能,针对 Android 移动端的特点提供了例如代码替换,添加 try catch,方法耗时等功能。 * 简单易用,只需要依赖一个插件,处理过程以及处理后的代码中也不需要添加额外的依赖。 * 处理速度较快,只占用较少的编译时间。 ## 开发文档 ### DroidAssist 配置文件 DroidAssist 将扫描工程中的每一个单独的 class 以及 jar 中的 class, 并对 class 与配置文件中的规则进行匹配,如果有规则能够匹配到 class,则根据 DroidAssist 配置对此 class 进行字节码修改。 DroidAssist 配置是一个 xml 文件,根节点是 `DroidAssist` , 根节点下包含 `Global` , `Insert` , `Around` , `Replace` , `Enhance` 代码操作配置,完整的 DroidAssist 配置文件格式如下: ```xml ... ... ... ... ... ``` > 为了方便编写配置文件,在 IDE 中能自动提示,请将根目录下 [DTD文件](droidassist.dtd) 拷贝到配置文件第二行。 ### 配置分类: - **Insert**:代码插入类 - **Replace**:代码替换类 - **Around**:代码环绕类 - **Enhance**:代码增强类 ### Source 和 Target Insert、Replace、Around、Enhance 类型代码操作配置中均需要包含 `Source` 和 `Target` 元素: 例: ```xml int android.util.Log.d(java.lang.String,java.lang.String) $_= com.didichuxing.tools.test.LogUtils.log($$); ``` Source 的值 `int android.util.Log.d(java.lang.String,java.lang.String)` 表示需要匹配方法调用 `android.util.Log.d( )` Target 的值 `$_= com.didichuxing.tools.test.LogUtils.log($$); `表示将调用 `android.util.Log.d( )` 方法调用的代码替换为 `com.didichuxing.tools.test.LogUtils.log( )` #### Source 表示需要进行修改的代码位置,用以精确匹配代码位置,Source 按照代码位置类型可以分为方法、构造方法、字段、静态初始化块: ##### 1. 方法 Source 表示方法时,格式为 `returnType className.methodName(argType1,argType2)` : ```xml int android.util.Log.d(java.lang.String,java.lang.String) ``` ##### 2. 构造方法 Source 表示方法时,格式为 `new className(argType1,argType2)` 或者 `className.new(argType1,argType2)`: ```xml new com.didichuxing.tools.test.ExampleSpec(int) ``` 或 ```xml com.didichuxing.tools.test.ExampleSpec.new(int) ``` ##### 3. 字段 Source 表示字段时,格式为 `fieldType className.fieldName` : ```xml int com.didichuxing.tools.test.ExampleSpec.id ``` ##### 4. 静态初始化块 Source 表示静态初始化块时,格式为 `className` : ```xml com.didichuxing.tools.test.ExampleSpec ``` > 注意: > 1. Source 的范围为本类和在子类有效(构造方法和静态初始化块除外),方法和字段如果在子类中可见,则也会被匹配,如果不需要匹配子类只匹配当前配置类,可在` `标签中添加 `extend` 属性:`extend = "false"` > 2. Source 中所有的 class 均需要配置全限定名。 > 3. Source 中 class 如果是内部类,需要使用分隔符 `$` 和外部类隔开,如 `com.didichuxing.tools.test.ExampleSpec$Inner`。 #### Target 需要修改成的目标代码,该值接受一个 Java 表达式或者以大括号`{}`包围的代码块。如果表达式是一个单独的表达式,需要以分号`;`结尾。 例: ```xml java.lang.System.out.println("BeforeMethodCall"); ``` 或: ```xml {System.out.println("Hello"); System.out.println("World");} ``` > 注意: > > 1. 如果 Source 的表达式中 `returnType` 为非 `void` 类型时, Target 中表达式必须 要包含 `$_=` 以保存返回值,否则可能会出现错误。 ##### 扩展变量 基于 Javassist 的支持,可以在 Target 中使用语言扩展,`$` 开头的标识符有特殊的含义: | 符号 | 含义 | scope| | :-------- | --------| --- | |`$0`,`$1`,`$2` .. |`this` 和方法的参数 |runtime | |`$args` |方法参数数组,类型为 `Object[]` |runtime | |`$$` |所有实参, 例如 m(`$$`) 等价于 m(`$1`,`$2`,...) |runtime | |`$proceed` |表示原始的方法、构造方法、字段调用 |runtime | |`$cflow(...)` |cflow 变量 |runtime | |`$r` |返回结果的类型,用于强制类型转换 |runtime | |`$w` |包装器类型,用于强制类型转换 |runtime | |`$_` |返回值 |runtime | |`$sig` |参数类型数组,类型为 `java.lang.Class[]` |runtime | |`$type` |返回值类型,类型为 `java.lang.Class` |runtime | |`$class`|表示当前正在修改的类,类型为 `java.lang.Class` |compile | |`$line` |表示当前正在修改的行号,类型为 `int` |compile | |`$name` |表示当前正在方法或字段名,类型为 `java.lang.String` |compile | |`$file` |表示当前正在修改的文件名,类型为 `java.lang.String` |compile | > 1. Target 中对于 `java.lang` 包中的类可以直接使用,不用添加包名。 > 2. Around 类型配置中 `Target` 分解为 `TargetBefore` 和 `TargetAfter` > 3. Scope 为 `compile` 类型的扩展在编译后将直接替换成结果值,`runtime` 类型的扩展只在运行期有效。 ### 类过滤器 Filter 默认情况下 DroidAssist 将扫描工程中的每一个 class 并进行匹配和处理,为了加快处理速度以及排除某些不需要处理的类,可以使用类过滤器 Filter 配置将不需要处理的类排除。Filter 配置中包含: - **Include**:需要处理的类,支持通配符匹配和精确匹配 - **Exclude**:不需要处理的类,支持通配符匹配和精确匹配 每个 Filter 中可以包含多个 Include、Exclude 配置,支持通配符匹配,class 被匹配的条件是类名可以被 Include 规则匹配但是不能被 Exclude 匹配,即 `(Include & !Exclude)` 。 例: ```xml int android.util.Log.d(java.lang.String,java.lang.String) com.didichuxing.tools.test.LogUtils.log($$); * com.didichuxing.tools.test.Utils android.* com.android.* ``` 该配置中的 Filter 中 有1个 Include 配置,值 `*` 表示将处理所有的 class,有 3 个 Exclude 配置表示将不处理`com.didichuxing.tools.test.Utils` 类,以及类名匹配 `android.*` 和 `com.android.*` 的类。 >1. 每一个代码操作配置规则下都可以添加 Filter 配置(可选) >2. Global 配置中可以包含 Filter,当 Filter 出现在 Global 配置中时,对所有的代码操作配置都生效,如果需要忽略全局 Filter 配置,可在 Filter 标签中添加 ignoreGlobalIncludes="true" 和 ignoreGlobalExcludes="true" 例: ```xml * android.* com.android.* ``` ### Global 配置 Global 配置可以包含类过滤器 Filter: ```xml * android.* com.android.* ``` ### Replace 配置 Replace 类型代码操作配置的作用是将指定代码替换成目标代码,包含以下配置: - **MethodCall** 方法调用 - **MethodExecution** 方法体执行 - **ConstructorCall** 构造方法调用 - **ConstructorExecution** 构造方法体执行 - **InitializerExecution** 静态代码初始化块执行 - **FieldRead** 字段读取 - **FieldWrite** 字段赋值 >Call 表示方法或者构造方法被其他代码调用,Execution 代表方法、构造方法或者静态初始化代码块的方法体本身。 例: ```xml int android.util.Log.d(java.lang.String,java.lang.String) $_= com.didichuxing.tools.test.LogUtils.log($$); new com.didichuxing.tools.test.ExampleSpec(int) {$_= com.didichuxing.tools.test.ExampleSpec.getInstance();} ``` ### Insert 配置 Insert 类型代码操作配置的作用是将指定代码之前或之后插入目标代码,包含以下配置: - **BeforeMethodCall** 方法调用之前 - **AfterMethodCall** 方法调用之后 - **BeforeMethodExecution** 方法体执行之前 - **AfterMethodExecution** 方法体执行之后 - **BeforeConstructorCall** 构造方法体调用之前 - **AfterConstructorCall** 构造方法体调用之后 - **BeforeConstructorExecution** 构造方法体执行之前 - **AfterConstructorExecution** 构造方法体执行之前 - **BeforeInitializerExecution** 静态代码初始化块执行之前 - **AfterInitializerExecution** 静态代码初始化块执行之前 - **BeforeFieldRead** 字段读取之前 - **AfterFieldRead** 字段读取之后 - **BeforeFieldWrite** 字段赋值之前 - **AfterFieldWrite** 字段赋值之后 例: ```xml void com.didichuxing.tools.test.ExampleSpec.run() {java.lang.System.out.println("BeforeMethodCall");} new com.didichuxing.tools.test.ExampleSpec() java.lang.System.out.println("AfterConstructorExecution"); ``` ### Around 配置 Around 类型代码操作配置的作用是将指定代码前后环绕插入目标代码,包含以下配置: - **MethodCall** 方法调用环绕插入代码 - **MethodExecution** 方法体执行环绕插入代码 - **ConstructorCall** 构造方法调用环绕插入代码 - **ConstructorExecution** 构造方法体执行环绕插入代码 - **InitializerExecution** 静态代码初始化块执行环绕插入代码 - **FieldRead** 字段读取环绕插入代码 - **FieldWrite** 字段赋值环绕插入代码 在 Around 类型配置中 Target 配置分解为 `TargetBefore` 和 `TargetAfter`,分别表示 Source 代码之前和之后插入的代码,在 `TargetBefore` 中声明的变量,在 `TargetAfter` 可以直接使用。 例: ```xml void com.didichuxing.tools.test.ExampleSpec.call() java.lang.System.out.println("around before MethodCall"); java.lang.System.out.println("around after MethodCall"); ``` ### Enhance 配置 Enhance 类型代码操作配置的作用是加入增强性代码,可以对 Source 代码添加 `TryCatch` 方法和 `Timing` 耗时统计方法 : #### TryCatch TryCatch 类型配置可以对 Source 代码添加 `try{...} catch(...){...}`代码,包含以下配置: - **TryCatchMethodCall** 方法调用添加 Try Catch 代码 - **TryCatchMethodExecution** 方法体执行添加 Try Catch 代码 - **TryCatchConstructorCall** 构造方法调用添加 Try Catch 代码 - **TryCatchConstructorExecution** 构造方法体执行添加 Try Catch 代码 - **TryCatchInitializerExecution** 静态代码初始化块执行添加 Try Catch 代码 ##### Exception TryCatch 配置默认将捕获 `java.lang.Exception` 类型异常,如果需要捕获其他异常,需要添加 `Exception` 配置,声明需要捕获的异常,在 Target 表达式中可以使用 `$e` 扩展变量接收捕获的异常对象。 例: ```xml void android.content.Context.startActivity(android.content.Intent) android.content.ActivityNotFoundException android.util.Log.d("test", "startActivity error", $e); ``` #### Timing Timing 类型配置可以对 Source 代码添加耗时统计代码,包含以下配置: - **TimingMethodCall** 方法调用添加耗时统计代码 - **TimingMethodExecution** 方法体执行耗时统计代码 - **TimingConstructorCall** 构造方法调用耗时统计代码 - **TimingConstructorExecution** 构造方法体执行耗时统计代码 - **TimingInitializerExecution** 静态代码初始化块执行耗时统计代码 Timing 类型配置会自动在 Source 代码前后添加耗时计算代码,并将耗时毫秒值保存到 `$time` 扩展变量中,可以在 Target 配置中直接使用该扩展变量。 例: ```xml void com.didichuxing.tools.test.ExampleSpec.timing() android.util.Log.d("test", "time cost= "+ $time); ``` > `$time` 扩展变量为 `long` 型,单位为毫秒,如果需要获取耗时的微秒值,可以使用 `$nanotime` 扩展变量。 #### Reparent Reparent 类型配置可以重新设置制定 class 的父类 - **ReparentClass** 重新设置类的父类型 Reparent 类型配置将指定的类型( Source 中配置的类型)的直接子类的父类型设置到另外一个类型( Target 中配置的类型)。 例: ```xml com.didichuxing.tools.test.Parent com.didichuxing.tools.test.ReParent ``` 上面例子中 class 'com.didichuxing.tools.test.Parent' 的直接子类在处理后父类型将被设置为 'com.didichuxing.tools.test.ReParent' > 使用此配置时需要注意 Source 类和 Target 类的 api 兼容性,需要注意子类中构造方法、方法、字段在重新设置到指定的父类后还能否有正确的继承关系。 ## Q & A #### 1. DroidAssist 可以实现什么功能? DroidAssist 提供了一套轻量级的字节码操作方案,可以轻易实现诸如代码替换,代码插入等功能,滴滴出行APP 目前利用DroidAssist 实现了日志输出替换,系统 SharedPreferences 替换,SharedPreferences commit 替换 apply,Dialog show 保护,获取 deviceId 接口替换,getPackageInfo 接口替换,getSystemService 接口替换,startActivity 保护,匿名线程重命名,线程池创建监控,主线程卡顿监控,文件夹创建监控,Activity 生命周期耗时统计,APP启动耗时统计等功能。 #### 2. DroidAssist 和 AspectJ 有什么区别? DroidAssist 采用配置化方案,编写相关配置就可以实现 AOP 的功能,可以完全不用修改 Java 代码,DroidAssist 使用比较简单,不需要复杂的注解配置,DroidAssist 可以比较方便的实现 AspectJ 不容易实现的代码替换功能。