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