ASM 实现 Hook Lambda 和方法引用 | 数据采集

一、前言

本文是前作「Lambda 设计参考」的实战部分,具体将介绍如何使用 ASM 对 Java 8 Lambda 表达式和方法引用进行 Hook 操作。

在此之前会介绍一些基础概念和字节码相关的知识方便大家对这块内容的理解,最后会给出一个完整的代码供大家参考。

二、脱糖

2.1 概念介绍

Java 脱糖(Desugar):简单地说,就是在编译阶段将语法层面一些底层字节码不支持的特性转换为底层支持的结构。例如:可以在 Android 中使用 Java 8 的 Lambda 特性,就是使用了脱糖。

使用脱糖的最主要原因是 Android 设备并没有提供 Java 8 的运行时环境。下面用一个例子来展示对 Lambda 脱糖需要做的工作。

class Java8 {
  interface Logger {
    void log(String s);
  }
 
  public static void main(String... args) {
    sayHi(s -> System.out.println(s));
  }
 
  private static void sayHi(Logger logger) {
    logger.log("Hello!");
  }
}

首先是将 Lambda 方法体中的内容从 main 方法中移到 Java8 类的内部方法中,改变后的结果如下:

public class Java8 {
    interface Logger {
        void log(String s);
    }
 
    public static void main(String... args) {
        //使用 lambda$main$0 替换原有的逻辑
        sayHi(s -> lambda$main$0(s));
    }
 
    private static void sayHi(Logger logger) {
        logger.log("Hello!");
    }
 
    //方法体中的内容移到这里
    static void lambda$main$0(String str){
        System.out.println(str);
    }
}

接着生成一个类,这个类实现了 Logger 接口,实现的方法中调用 lambdamain0 方法,并且使用实现类替换代码 sayHi(s -> lambdamain0(s)) ,改变后的代码如下:

public class Java8 {
    interface Logger {
        void log(String s);
    }
 
    public static void main(String... args) {
        //这里使用 Logger 的实现类 Java8$1
        sayHi(s -> new Java8$1());
    }
 
    private static void sayHi(Logger logger) {
        logger.log("Hello!");
    }
 
    //方法体中的内容移到这里
    static void lambda$main$0(String str){
        System.out.println(str);
    }
}
 
public class Java8$1 implements Java8.Logger {
    public Java8$1(){
    }
 
    @Override
    public void log(String s) {
        //这里调用 Java8 方法的静态方法
        Java8.lambda$main$0(s);
    }
}

最后,因为 Lambda 并没有捕获外部作用的任何变量,所以这是一个无状态 Lambda。实现类会生成一个单例,在使用的地方用这个单例来替换 new Java8$1(),最终的代码如下:

class Java8 {
  interface Logger {
    void log(String s);
  }
 
  public static void main(String... args) {
    //此处使用单例替换原有代码
    sayHi(Java8$1.INSTANCE);
  }
 
  static void lambda$main$0(String s) {
    System.out.println(s);
  }
 
  private static void sayHi(Logger logger) {
    logger.log("Hello!");
  }
}
 
public class Java8$1 implements Java8.Logger {   
    static final Java8$1 INSTANCE = new Java8$1();
    @Override
    public void log(String s) {
        Java8.lambda$main$0(s);
    }
}

这个例子简单地展示了脱糖的过程,其中 lambdamain0 方法会在编译的时候生成。需要注意的是:方法引用并不会生成额外的方法。

(关于方法引用和 lambdamain0 的生成规则以及上面提到的无状态 lambdas 等知识可以通过「Lambda 设计参考」获取,读者如果对这部分内容不了解可以先看这篇文章)

2.2 Android 中的脱糖

上一节介绍了什么是脱糖以及用一个简单的例子来演示 Lambda 表达式的脱糖逻辑,那么我们为什么要关注 Android 中的脱糖呢?

首先 Android 系统本身并不支持 Java 8,前面说了 Android 设备并没有提供 Java 8 的运行时环境。因此,App 项目使用 Java 8 编译产生的字节码是无法在 Android 设备上解析的,Android 使用 Gradle 在编译时会将 .class 文件中的一些 Java 8 语法特性脱糖成 Java 7 中支持的语法特性。

我们看下图 2-1 描述的 Android 处理 Java 文件的流程,注意图中的 “Third-party plugins” 是 Android 为我们提供的可以在编译期有机会处理 .class 文件。关于插件开发,可以参考我司出版的《Android 全埋点解决方案》一书。


2-1

图 2-1 Android 处理 Java 文件的流程(来源:https://developer.android.com/studio/write/java8-support

根据图 2-1 所示,自定义的 Android 插件是在 D8/R8 之前先操作 .class 文件。D8 是 Android 提供的脱糖工具,这就导致自定义插件获取的 .class 是原始未脱糖的 .class(注:多个自定义插件执行顺序跟引入顺序有关,我们自定义的插件获取到的 .class 可能是其他插件处理过的 )。现在我们来分析下面这段代码:

View.setOnClickListener(System.out::println)

Android 开发者对这段代码很容易理解。现在我们对这段代码进行处理,希望在执行点击事件的时候,除了执行 println 方法,同时还能够加入一些其他的逻辑,如下面代码的描述:

View.setOnClickListener(view->{
    System.out.println(view); // 方法引用
    SensorsDataAutoTrackHelper.trackViewClick(view); //添加的额外逻辑
})

因为这里是一个方法引用,并不会像 Lambda 表达式那样在编译时生成一个 lambd$ 开头的方法(注:关于这块的描述请参考 「Lambda 设计参考」),而且我们也不能在 println 方法中插入代码,本文就是给大家介绍如何处理这种情况。

注意:

Android 可以选择在工程中关闭 D8 的脱糖功能(可以通过在 gradle.properties 里配置 android.enableD8.desugaring=false 来关闭),那么 .class 文件的处理流程会变成:.class → desugar → third-party plugins → dex

三、invokedynamic 指令

在正式介绍如何使用 ASM 处理 Lambda 和方法引用之前,我们首先了解一下字节码指令** invokedynamic**。

invokedynamic 指令是在 JDK 7 引入的,用来实现动态类型语言功能,简单来说就是能够在运行时去调用实际的代码。

在进一步介绍 invokedynamic 指令之前,我们先熟悉几个类:

MethodTypeMethodHandleCallSite

在介绍这几个类之前我们先来了解一个方法的构成:

  • 方法名;

  • 方法签名(参数类型和返回值类型);

  • 方法所在的类;

  • 方法体(方法中的代码)。

根据上面方法的构成,我们来依次介绍上面的几个类的用法。

3.1 MethodType

MethodType 代表一个方法所需的参数签名和返回值签名,MethodType 类有多个静态方法来构造 MethodType 对象,示例如下:

MethodType methodType = MethodType.methodType(String.

上面这个 MethodType 描述的是返回值为 String 类型,参数是一个 int 类型的方法签名,例如:int foo(String) 这个方法就符合这个描述。

3.2 MethodHandle

MethodHandle 翻译过来就是方法句柄,通过这个句柄可以调用相应的方法,MethodType 描述了方法的参数和返回值,MethodHandle 则是根据类名、方法名并且配合 MethodType 来找到特定方法然后执行它;MethodType 和 MethodHandle 配合起来完整表达了一个方法的构成。例如:我们调用 String.valueOf(int) 方法,可以这么做:

//声明参数和返回值类型
MethodType methodType = MethodType.methodType(String.class, int.class);
MethodHandles.Lookup lookup = MethodHandles.lookup();
//声明一个方法句柄:这里说明的是 String 类里面的 valueOf 方法,方法签名需要符合 methodType
MethodHandle methodHandle = lookup.findStatic(String.class, "valueOf", methodType);
//执行这个方法
String result = (String) methodHandle.invoke(99);
System.out.println(result);

这个跟反射很类似,从这个例子可以看出方法句柄里包含了需要执行的方法信息,只要传入所需的参数就可以执行这个方法了。

3.3 CallSite

CallSite 是方法调用点,调用点中包含了方法句柄信息,通常 invokedynamic 指令所描述的内容会使用 CallSite 来链接,关于这块内容的介绍也可以在 「Lambda 设计参考」找到。可以从调用点上获取 MethodHandle ,代码如下所示:

CallSite callSite = new ConstantCallSite(methodHandle);
MethodHandle mh = callSite.getTarget();

3.4 invokedynamic

前面介绍了一些跟 Lambda 相关的 API,下面正式介绍 invokedynamic,先看下面这段代码对应的字节码:

//源码部分
public class TestMain2 {
    public void test() {
        final Date date = new Date();
        Consumer<String> consumer = (String s) -> System.out.println(s + date.toString());
    }
}
 
//对应的部分字节码
Constant pool:
   #4 = InvokeDynamic      #0:#30         // #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;
   #5 = Fieldref           #31.#32        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Class              #33            // java/lang/StringBuilder
   #7 = Methodref          #6.#23         // java/lang/StringBuilder."<init>":()V
   #8 = Methodref          #6.#34         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #9 = Methodref          #2.#35         // java/util/Date.toString:()Ljava/lang/String;
  #10 = Methodref          #6.#35         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #11 = Methodref          #36.#37        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #12 = Class              #38            // cn/curious/asm/method_ref/TestMain2
  #19 = Utf8               lambda$test$0
  #20 = Utf8               (Ljava/util/Date;Ljava/lang/String;)V
  #21 = Utf8               SourceFile
  #22 = Utf8               TestMain2.java
  #23 = NameAndType        #14:#15        // "<init>":()V
  #24 = Utf8               java/util/Date
  #25 = Utf8               BootstrapMethods
  #26 = MethodHandle       #6:#40         // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #27 = MethodType         #41            //  (Ljava/lang/Object;)V
  #28 = MethodHandle       #6:#42         // invokestatic cn/curious/asm/method_ref/TestMain2.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V
  #29 = MethodType         #43            //  (Ljava/lang/String;)V
  #30 = NameAndType        #44:#45        // accept:(Ljava/util/Date;)Ljava/util/function/Consumer;
  #31 = Class              #46            // java/lang/System
  #32 = NameAndType        #47:#48        // out:Ljava/io/PrintStream;
  #33 = Utf8               java/lang/StringBuilder
  #34 = NameAndType        #49:#50        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #35 = NameAndType        #51:#52        // toString:()Ljava/lang/String;
  #36 = Class              #53            // java/io/PrintStream
  #37 = NameAndType        #54:#43        // println:(Ljava/lang/String;)V
  #38 = Utf8               cn/curious/asm/method_ref/TestMain2
  #39 = Utf8               java/lang/Object
  #40 = Methodref          #55.#56        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #41 = Utf8               (Ljava/lang/Object;)V
  #42 = Methodref          #12.#57        // cn/curious/asm/method_ref/TestMain2.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V
  #43 = Utf8               (Ljava/lang/String;)V
  #44 = Utf8               accept
  #45 = Utf8               (Ljava/util/Date;)Ljava/util/function/Consumer;=
{
  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/Date
         3: dup
         4: invokespecial #3                  // Method java/util/Date."<init>":()V
         7: astore_1
         8: aload_1
         9: invokedynamic #4,  0              // InvokeDynamic #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;
        14: astore_2
        15: return
 
  private static void lambda$test$0(java.util.Date, java.lang.String);
    descriptor: (Ljava/util/Date;Ljava/lang/String;)V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=3, locals=2, args_size=2
         0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #6                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
        10: aload_1
        11: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        14: aload_0
        15: invokevirtual #9                  // Method java/util/Date.toString:()Ljava/lang/String;
        18: invokevirtual #8                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
}
InnerClasses:
     public static final #61= #60 of #64; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #26 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #27 (Ljava/lang/Object;)V
      #28 invokestatic cn/curious/asm/method_ref/TestMain2.lambda$test$0:(Ljava/util/Date;Ljava/lang/String;)V
      #29 (Ljava/lang/String;)V

上面是部分主要的字节码信息,可以看下关键代码:

1. 首先可以看第 58 行的 invokedynamic 指令:invokedynamic #4, 0 // invokeDynamic #0:accept:

(Ljava/util/Date;)Ljava/util/function/Consumer,其中,0 是预留字段,#4 表示的是常量池中的字段;

2. 第 11 行 #4 = InvokeDynamic #0:#30 // #0:accept:

(Ljava/util/Date;)Ljava/util/function/Consumer,这里的 #0 表示的是第一个引导方法,假如有多个 Lambda 可能会有多个引导方法。所谓的引导方法指的是在执行 invokedynamic 指令时,该指令所指向的、需要去执行的 Java 方法,通常在执行引导方法的时候会生成一些额外的类,例如前面介绍脱糖的时候 Java8.Logger 的实现类 Java8$1,这个类会在第一次执行引导方法的时候生成,大家有兴趣可以看一下引导方法的源码;

3. 第 83 行,这可以看到这个引导方法是 LambdaMetafacotry.metafactory,此方法的定义如下:

public static CallSite metafactory(MethodHandles.Lookup caller,
                                      String invokedName,
                                      MethodType invokedType,
                                      MethodType samMethodType,
                                      MethodHandle implMethod,
                                      MethodType instantiatedMethodType)

这个方法会返回一个 Callsite 调用点,调用点中包括了方法句柄信息,我们现在来详细解释下这个方法的参数,其中前三个参数不需要关注,系统会自动生成,主要是看后面三个参数:

1. samMethodType:

函数式接口中抽象方法的签名描述信息,关于 MethodType 前面的章节有介绍,这里的方法签名是 Consumer#apply 的签名,因为泛型参数,泛型 T 统一被转换成 Object(注:这里的 sam 指的是 Single Abstract Method,大家可以理解为函数式接口);

2. implMethod:

是一个方法句柄,这个在前面也介绍了,方法句柄包含了具体需要执行的方法,从上面的字节码可以看到,这个方法句柄的内容是#23 invokestatic cn/curious/asm/method_ref/TestMain2.lambdamain0:(Ljava/lang/String;)V,意思是调用静态方法 lambdamain0,在前面有介绍 Lambda 脱糖的时候我们知道,Lambda 会生成一个方法,此方法默认是隐藏的,如果想查看,可以使用 java 的 javap -p -v xxx.class 命令查看这个方法;

3. instantiatedMethodType:

samMethodType 的具体实现,源码传入的泛型类型是 String,所以这里就是 String。

接下来再看 invokedynamic 指令执行的前后代码:

public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/Date
         3: dup
         4: invokespecial #3                  // Method java/util/Date."<init>":()V
         7: astore_1
         8: aload_1
         9: invokedynamic #4,  0              // InvokeDynamic #0:accept:(Ljava/util/Date;)Ljava/util/function/Consumer;
        14: astore_2
        15: return

从上面的指令可以看到,invokedynamic 指令在执行的时候将创建的 Date 对象加载到栈顶。

invokedynamic 指令对应的 accept:(Ljava/util/Date;)Ljava/util/function/Consumer; 中的 Date 就是动态参数。

这个参数会添加在编译时生成的方法 lambdatest0(java.util.Date, java.lang.String) 参数列表的前面。

脱糖的具体规则在「Lambda 设计参考」中的 Lambda 方法体脱糖章节有详细的介绍。

四、使用 ASM 实现

综合前三节的知识,我们知道方法句柄中包含了方法调用的信息,而且我们也说明了方法引用并不会生成一个 lambda$ 开头的中间方法,同时我们知道 MethodHandle 包含了方法调用的信息。

因此,如果要去 Hook Lambda 和方法引用,我们可以创建一个新的 MethodHandle 替换原有的

具体做法是:我们会生成一个新的方法,新的方法中会实现 invokedynamic 指令中描述的代码逻辑。然后创建新的 MethodHandle,将这个 MethodHandle 替换原本的 MethodHandle。

现在整体的思路和方案已经有了,接下来就是使用 ASM 编写代码来实现,具体的实现如下:

`public` `class` `MethodReferenceAdapter ``extends` `ClassNode {`

`private` `final` `AtomicInteger counter = ``new` `AtomicInteger(``0``);`

`private` `List<MethodNode> syntheticMethodList = ``new` `ArrayList<>();`

`public` `MethodReferenceAdapter(ClassVisitor classVisitor) {`

`super``(Opcodes.ASM7);`

`this``.cv = classVisitor;`

`}`

`@Override`

`public` `void` `visitEnd() {`

`super``.visitEnd();`

`this``.methods.forEach(methodNode -> {`

`ListIterator<AbstractInsnNode> iterator = methodNode.instructions.iterator();`

`while` `(iterator.hasNext()) {`

`AbstractInsnNode node = iterator.next();`

`if` `(node ``instanceof` `InvokeDynamicInsnNode) {`

`InvokeDynamicInsnNode tmpNode = (InvokeDynamicInsnNode) node;`

`//形如:(Ljava/util/Date;)Ljava/util/function/Consumer;   可以从 desc 中获取函数式接口,以及动态参数的内容。`

`//如果没有参数那么描述符的参数部分应该是空。`

`String desc = tmpNode.desc;`

`Type descType = Type.getType(desc);`

`Type samBaseType = descType.getReturnType();`

`//sam 接口名`

`String samBase = samBaseType.getDescriptor();`

`//sam 方法名`

`String samMethodName = tmpNode.name;`

`Object[] bsmArgs = tmpNode.bsmArgs;`

`//sam 方法描述符`

`Type samMethodType = (Type) bsmArgs[``0``];`

`//sam 实现方法实际参数描述符`

`Type implMethodType = (Type) bsmArgs[``2``];`

`//sam name + desc,可以用来辨别是否是需要 Hook 的 lambda 表达式`

`String bsmMethodNameAndDescriptor = samMethodName + samMethodType.getDescriptor();`

`//中间方法的名称`

`String middleMethodName = ``"lambda$"` `+ samMethodName + ``"$sa"` `+ counter.incrementAndGet();`

`//中间方法的描述符`

`String middleMethodDesc = ``""``;`

`Type[] descArgTypes = descType.getArgumentTypes();`

`if` `(descArgTypes.length == ``0``) {`

`middleMethodDesc = implMethodType.getDescriptor();`

`} ``else` `{`

`middleMethodDesc = ``"("``;`

`for` `(Type tmpType : descArgTypes) {`

`middleMethodDesc += tmpType.getDescriptor();`

`}`

`middleMethodDesc += implMethodType.getDescriptor().replace(``"("``, ``""``);`

`}`

`//INDY 原本的 handle,需要将此 handle 替换成新的 handle`

`Handle oldHandle = (Handle) bsmArgs[``1``];`

`Handle newHandle = ``new` `Handle(Opcodes.H_INVOKESTATIC, ``this``.name, middleMethodName, middleMethodDesc, ``false``);`

`InvokeDynamicInsnNode newDynamicNode = ``new` `InvokeDynamicInsnNode(tmpNode.name, tmpNode.desc, tmpNode.bsm, samMethodType, newHandle, implMethodType);`

`iterator.remove();`

`iterator.add(newDynamicNode);`

`generateMiddleMethod(oldHandle, middleMethodName, middleMethodDesc);`

`}`

`}`

`});`

`this``.methods.addAll(syntheticMethodList);`

`accept(cv);`

`}`

`private` `void` `generateMiddleMethod(Handle oldHandle, String middleMethodName, String middleMethodDesc) {`

`//开始对生成的方法中插入或者调用相应的代码`

`MethodNode methodNode = ``new` `MethodNode(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC ``/*| Opcodes.ACC_SYNTHETIC*/``,`

`middleMethodName, middleMethodDesc, ``null``, ``null``);`

`methodNode.visitCode();`

`// 此块 tag 具体可以参考: [https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic)`

`int` `accResult = oldHandle.getTag();`

`switch` `(accResult) {`

`case` `Opcodes.H_INVOKEINTERFACE:`

`accResult = Opcodes.INVOKEINTERFACE;`

`break``;`

`case` `Opcodes.H_INVOKESPECIAL:`

`//private, this, super 等会调用`

`accResult = Opcodes.INVOKESPECIAL;`

`break``;`

`case` `Opcodes.H_NEWINVOKESPECIAL:`

`//constructors`

`accResult = Opcodes.INVOKESPECIAL;`

`methodNode.visitTypeInsn(Opcodes.NEW, oldHandle.getOwner());`

`methodNode.visitInsn(Opcodes.DUP);`

`break``;`

`case` `Opcodes.H_INVOKESTATIC:`

`accResult = Opcodes.INVOKESTATIC;`

`break``;`

`case` `Opcodes.H_INVOKEVIRTUAL:`

`accResult = Opcodes.INVOKEVIRTUAL;`

`break``;`

`}`

`Type middleMethodType = Type.getType(middleMethodDesc);`

`Type[] argumentsType = middleMethodType.getArgumentTypes();`

`if` `(argumentsType.length > ``0``) {`

`int` `loadIndex = ``0``;`

`for` `(Type tmpType : argumentsType) {`

`int` `opcode = tmpType.getOpcode(Opcodes.ILOAD);`

`methodNode.visitVarInsn(opcode, loadIndex);`

`loadIndex += tmpType.getSize();`

`}`

`}`

`methodNode.visitMethodInsn(accResult, oldHandle.getOwner(), oldHandle.getName(), oldHandle.getDesc(), ``false``);`

`Type returnType = middleMethodType.getReturnType();`

`int` `returnOpcodes = returnType.getOpcode(Opcodes.IRETURN);`

`methodNode.visitInsn(returnOpcodes);`

`methodNode.visitEnd();`

`syntheticMethodList.add(methodNode);`

`}`

`}`

我们对前面介绍的示例中的 .class 文件使用 ASM 运行后输出的结果如下:

public class TestMain2 {
    public TestMain2() {
    }
 
    public void test() {
        Date var1 = new Date();
        Consumer var2 = TestMain2::lambda$accept$sa1;
    }
     
    private static void lambda$accept$sa1(Date var0, String var1) {
        //TODO 可以在此插桩
        lambda$test$0(var0, var1);
    }
}

其中,lambdaacceptsa1 是我们使用 ASM 生成的方法。在这个方法中,我们替换了原本的 lambdatest0(此方法的 tag 是 acc_synthetic,表示代码是自动生成的,反编译默认不显示)方法,在我们生成的方法中调用编译器生成的 lambdatest0 方法。

这里需要再提醒一下,方法引用并不会生成类似 lambdatest0 这样的方法,我们需要将方法引用的代码放在我们生成的方法中,这个读者可以写一个方法引用测试一下结果。

至此,如果我们想要对 Lambda 或者方法引用的代码进行插桩,只要在我们生成的方法中插入即可

五、总结

整体的原理是:我们自己生成一个中间方法,如果是 Lambda,那么我们在方法中调用这个 Lambda 编译时生成的中间方法;

如果是方法引用,就把方法引用里的内容放到我们生成的中间方法中,然后将自定义的 MethodHandle 指向生成的方法;

最后替换掉 Bootstrap Method 中的 MethodHandle,达到偷梁换柱的效果。

不过,这种方式的弊端是会多生成一些中间方法。

至此,用了两篇文章介绍了 ASM Hook Lambda 和方法引用的知识,希望对大家有所帮助。

参考资料:

  • D8 & R8:

https://developer.android.com/studio/releases#r8-default

  • Android's Java 8 Support:

https://jakewharton.com/androids-java-8-support/

  • JVM invokeydynamic instruction:

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.invokedynamic

本文作者

张伟

神策数据 | Android 研发工程师

我是张伟,神策数据 Android 研发工程师,主要从事神策 Android SDK 和 Android Plugin 的开发工作,希望通过开源社区这个平台与大家共同学习进步。生活中热爱篮球、看书、旅游,希望我们相聚神策,一起维护神策开源社区和打篮球。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,737评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,103评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,710评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,909评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,794评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,557评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,939评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,572评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,852评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,871评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,692评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,490评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,939评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,114评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,409评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,971评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,176评论 2 339