滴滴开源的Android 字节码编辑插件!

开发者全社区

共 7798字,需浏览 16分钟

 · 2020-08-06

bf636d5136dd3b84e78776abaee3665e.webp

DroidAssist

DroidAssist 是一个轻量级的 Android 字节码编辑插件,基于 Javassist 对字节码操作,根据 xml 配置处理 class 文件,以达到对 class 文件进行动态修改的效果。和其他 AOP 方案不同,DroidAssist 提供了一种更加轻量,简单易用,无侵入,可配置化的字节码操作方式,你不需要 Java 字节码的相关知识,只需要在 Xml 插件配置中添加简单的 Java 代码即可实现类似 AOP 的功能,同时不需要引入其他额外的依赖。

功能

  • 替换:把指定位置代码替换为指定代码

  • 插入:在指定位置的前后插入指定代码

  • 环绕:在指定位置环绕插入指定代码

  • 增强

    • 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 配置文件格式如下:



    
    
        ...
    


    
    
        ...
    


    
    
        ...
    


    
    
        ...
    


    
    
        ...
    


为了方便编写配置文件,在 IDE 中能自动提示,请将根目录下 DTD文件 拷贝到配置文件第二行。

配置分类:

  • Insert:代码插入类

  • Replace:代码替换类

  • Around:代码环绕类

  • Enhance:代码增强类

Source 和 Target

Insert、Replace、Around、Enhance 类型代码操作配置中均需要包含 Source 和 Target 元素:例:


    
        
            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) :

  int android.util.Log.d(java.lang.String,java.lang.String) 
2. 构造方法

Source 表示方法时,格式为 new className(argType1,argType2) 或者 className.new(argType1,argType2)

  new com.didichuxing.tools.test.ExampleSpec(int) 

  com.didichuxing.tools.test.ExampleSpec.new(int) 
3. 字段

Source 表示字段时,格式为 fieldType className.fieldName :

  int com.didichuxing.tools.test.ExampleSpec.id 
4. 静态初始化块

Source 表示静态初始化块时,格式为 className :

  com.didichuxing.tools.test.ExampleSpec 

注意:

  1. Source 的范围为本类和在子类有效(构造方法和静态初始化块除外),方法和字段如果在子类中可见,则也会被匹配,如果不需要匹配子类只匹配当前配置类,可在标签中添加 extend 属性:extend = "false"

  2. Source 中所有的 class 均需要配置全限定名。

  3. Source 中 class 如果是内部类,需要使用分隔符 $ 和外部类隔开,如 com.didichuxing.tools.test.ExampleSpec$Inner

Target

需要修改成的目标代码,该值接受一个 Java 表达式或者以大括号{}包围的代码块。如果表达式是一个单独的表达式,需要以分号结尾。

例:

java.lang.System.out.println("BeforeMethodCall");

或:

{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.Classruntime
$class表示当前正在修改的类,类型为 java.lang.Classcompile
$line表示当前正在修改的行号,类型为 intcompile
$name表示当前正在方法或字段名,类型为 java.lang.Stringcompile
$file表示当前正在修改的文件名,类型为 java.lang.Stringcompile
  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) 。

例:


    
        
            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 配置中时,对所有的代码操作配置都生效。

Global 配置

Global 配置可以包含类过滤器 Filter:


    
        *
        android.*
        com.android.*
    


Replace 配置

Replace 类型代码操作配置的作用是将指定代码替换成目标代码,包含以下配置:

  • MethodCall 方法调用

  • MethodExecution 方法体执行

  • ConstructorCall 构造方法调用

  • ConstructorExecution 构造方法体执行

  • InitializerExecution 静态代码初始化块执行

  • FieldRead 字段读取

  • FieldWrite 字段赋值

Call 表示方法或者构造方法被其他代码调用,Execution 代表方法、构造方法或者静态初始化代码块的方法体本身。

例:


    
        
            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 字段赋值之后

例:


    
        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 可以直接使用。

例:


    
        
            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 扩展变量接收捕获的异常对象。

例:


    
        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 配置中直接使用该扩展变量。例:


    void com.didichuxing.tools.test.ExampleSpec.timing()
    
        android.util.Log.d("test", "time cost= "+ $time);
    


$time 扩展变量为 long 型,单位为毫秒,如果需要获取耗时的微秒值,可以使用 $nanotime 扩展变量。

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 不容易实现的代码替换功能。

项目地址

github地址:https://github.com/didi/DroidAssist



  开发者全社区 

5T技术资源大放送!包括但不限于:Android,Python,Java,大数据,人工智能,AI等等。关注公众号后回复「2T」,即可免费获取!
浏览 6
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报