Android Gradle 中的使用ASMified插件生成.class的技巧

Posted 好人静

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle 中的使用ASMified插件生成.class的技巧相关的知识,希望对你有一定的参考价值。

前言

       逐步整理的一系列的总结:

        Android Gradle插件开发初次交手(一)

        Android Gradle的基本概念梳理(二)

       Android 自定义Gradle插件的完整流程(三) 

       Android 自定义Task添加到任务队列(四)

       Android 自定义Gradle插件的Extension类(五)

       Android Gradle 中的Transform(六)

       Android Gradle之Java字节码(七)

      Android Gradle 中的字节码插桩之ASM(八)

      android Gradle 中的使用ASMified插件生成.class的技巧(九)

     Android Gradle 中的实例之动态修改AndroidManifest文件(十)


        前面在Android Gradle 中的字节码插桩之ASM(八)在使用ASM进行对.class文件进行修改的时候,通常是需要把要实现的代码编译生成.class文件,然后通过ASMified插件得到具体的ASM框架所需要的代码,本次主要总结下怎么来运用ASMified插件的代码。

  一.初始Java源代码

        初始Java源码对应的方法如下: 

    private int sum(int aa, int bb) 
        System.out.println("Other running code");
        return aa + bb;
    

   二.需要通过ASM框架修改之后的Java源代码

        最终通过ASM框架生成的Java源码如下:

    private int sum(int aa, int bb) 
        //方法刚执行的时候添加的代码
        long beginTime = System.currentTimeMillis();
        //原方法的执行
        System.out.println("Other running code");
        //计算方法执行的时间的相关代码
        long callTime = System.currentTimeMillis() - beginTime;
        Log.d("AUTO", String.format("cost time is [%d]ms", callTime));
        return aa + bb;
    

        可以看到需要在方法体执行之前和方法执行之后,分别获取系统时间,然后计算方法的执行时间。那么怎么根据ASMified插件得到ASM的相关代码呢?

1.将最终的Java源码编译得到.class文件,通过javap -v -p ASM.class查看.class文件

        由于.class文件内容比较多,不在一一罗列,只取部分内容来说明下。找到该sum()方法对应的字节码指令如下:

          分析了一遍之后,自己总结了几条结论:

  • (1)方法的执行都是在操作数栈中,所以当方法执行完有返回值需要赋值给局部变量的时候,需要通过istore_n等相关的指令存储到对应的局部变量表的相应位置;如下:
 0: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
 3: lstore_3

        通过上面两条指令实现了如下代码:

long beginTime = System.currentTimeMillis();
  • (2)方法执行都是在操作数栈中,所以当方法调用需要传入参数的时候,首先要将传入参数从局部变量表加载到操作数栈中。如下
 4: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
 7: ldc           #5                  // String Other running code
 9: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

        第一条指令:getstaic    #4就是将#4对应的静态变量加载到操作数栈中,即System.out加载到操作数栈中;

        第二条指令:ldc           #5 就是将#5对应的字符串加载到操作数栈中,即“String Other running code”字符串

        第三条指令:invokevirtual #6 就是执行println("Other running code")方法,对应的输入参数就是前一个入栈的字符串

        遗留问题:如果有两个输入参数呢?一会在增加一个有两个参数的方法,看看这个地方是如何入栈的

        后面又做了一个测试即Log.v("ASMByte","msg");对应的字节码为:

 19: ldc           #7                  // String ASMByte
 21: ldc           #8                  // String msg
 23: invokestatic  #9                  // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I

        对应的 ASMified插件的代码如下:

  methodVisitor.visitLdcInsn("ASMByte");
  methodVisitor.visitLdcInsn("msg");
  methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "v", "(Ljava/lang/String;Ljava/lang/String;)I", false);

        可以看到第一个输入参数先入栈、第二个输入参数在入栈,包括下面提到的lsub ,也是被减数先入栈,减数再入栈    

          所以初步结论:应该是按照调用顺序先入栈(遗留问题:这个在以后的字节码中多留意下)

  • (3)简单的运算也是在操作数栈中,需要先将被减数入栈,然后再将减数入栈,如下
12: invokestatic  #3                  // Method java/lang/System.currentTimeMillis:()J
15: lload_3
16: lsub
17: lstore        5

        第一条指令: invokestatic  #3就是直接调用了 System.currentTimeMillis(),并且返回值直接在操作栈中;

        第二条指令: lload_3就是将局部变量表中的索引值(槽点)为3的值加载到操作栈中,即此时beginTime对应的值已经入栈

        第三条指令:lsub就是将操作栈中的上两个进行相减,此时是被减数先入栈,减数再入栈(遗留问题:不知道这个结论是不是正确。大胆猜测下:执行iadd的时候,先是第一个加数入栈,第二个加数在入栈,是不是入栈的顺序就是按照方法调用参数的前后顺序入栈呢?
        第四条指令: lstore        5就是将此时栈顶的值保存到局部变量表的索引值(槽点)的为5的位置,即callTime上。

        遗留问题:这个方法在执行的时候:

Log.d("AUTO", String.format("cost time is [%d]ms", callTime));

    没怎么看懂这两个常量字符串加载到操作栈的过程:像这两条指令理解的就是把注释中对应的两个字符串加载到操作栈中,如下:

        19: ldc           #7                  // String AUTO
        21: ldc           #8                  // String cost time is [%d]ms

但是下面这几条指令作用又是什么呢?除去“anewarray来创建了Object类型的数组并加载到操作栈中”、“dup是在创建对象之后都要执行下dup(将创建的对象复制一次到栈顶)”外,其他的iconst_1和iconst_0的作用是什么呢?因为没有搞清楚此时操作栈中的内容,所以对于Log.d在调用的时候两个传入参数是怎么赋值的呢?后面简化下这个逻辑在分析下。

        23: iconst_1
        24: anewarray     #9                  // class java/lang/Object
        27: dup
        28: iconst_0

        常量池的内容如下: 

Constant pool:
   #1 = Methodref          #9.#43         // java/lang/Object."<init>":()V
   #2 = Fieldref           #14.#44        // com/android/androidplugin/ASMByte.a:I
   #3 = Methodref          #45.#46        // java/lang/System.currentTimeMillis:()J
   #4 = Fieldref           #45.#47        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #48            // Other running code
   #6 = Methodref          #49.#50        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = String             #51            // AUTO
   #8 = String             #52            // cost time is [%d]ms
   #9 = Class              #53            // java/lang/Object
  #10 = Methodref          #54.#55        // java/lang/Long.valueOf:(J)Ljava/lang/Long;
  #11 = Methodref          #56.#57        // java/lang/String.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
  #12 = Methodref          #58.#59        // android/util/Log.d:(Ljava/lang/String;Ljava/lang/String;)I
  #13 = Fieldref           #14.#60        // com/android/androidplugin/ASMByte.c:I
  #14 = Class              #61            // com/android/androidplugin/ASMByte
  #15 = Class              #63            // android/view/View$OnClickListener
  #16 = Utf8               a
  #17 = Utf8               I
  #18 = Utf8               c
  #19 = Utf8               name
  #20 = Utf8               Ljava/lang/String;
  #21 = Utf8               <init>
  #22 = Utf8               ()V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               LocalVariableTable
  #26 = Utf8               this
  #27 = Utf8               Lcom/android/androidplugin/ASMByte;
  #28 = Utf8               sum
  #29 = Utf8               (II)I
  #30 = Utf8               aa
  #31 = Utf8               bb
  #32 = Utf8               beginTime
  #33 = Utf8               J
  #34 = Utf8               callTime
  #35 = Utf8               MethodParameters
  #36 = Utf8               onClick
  #37 = Utf8               (Landroid/view/View;)V
  #38 = Utf8               v
  #39 = Utf8               Landroid/view/View;
  #40 = Utf8               <clinit>
  #41 = Utf8               SourceFile
  #42 = Utf8               ASMByte.java
  #43 = NameAndType        #21:#22        // "<init>":()V
  #44 = NameAndType        #16:#17        // a:I
  #45 = Class              #66            // java/lang/System
  #46 = NameAndType        #67:#68        // currentTimeMillis:()J
  #47 = NameAndType        #69:#70        // out:Ljava/io/PrintStream;
  #48 = Utf8               Other running code
  #49 = Class              #71            // java/io/PrintStream
  #50 = NameAndType        #72:#73        // println:(Ljava/lang/String;)V
  #51 = Utf8               AUTO
  #52 = Utf8               cost time is [%d]ms
  #53 = Utf8               java/lang/Object
  #54 = Class              #74            // java/lang/Long
  #55 = NameAndType        #75:#76        // valueOf:(J)Ljava/lang/Long;
  #56 = Class              #77            // java/lang/String
  #57 = NameAndType        #78:#79        // format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
  #58 = Class              #80            // android/util/Log
  #59 = NameAndType        #81:#82        // d:(Ljava/lang/String;Ljava/lang/String;)I
  #60 = NameAndType        #18:#17        // c:I
  #61 = Utf8               com/android/androidplugin/ASMByte
  #62 = Class              #83            // android/view/View
  #63 = Utf8               android/view/View$OnClickListener
  #64 = Utf8               OnClickListener
  #65 = Utf8               InnerClasses
  #66 = Utf8               java/lang/System
  #67 = Utf8               currentTimeMillis
  #68 = Utf8               ()J
  #69 = Utf8               out
  #70 = Utf8               Ljava/io/PrintStream;
  #71 = Utf8               java/io/PrintStream
  #72 = Utf8               println
  #73 = Utf8               (Ljava/lang/String;)V
  #74 = Utf8               java/lang/Long
  #75 = Utf8               valueOf
  #76 = Utf8               (J)Ljava/lang/Long;
  #77 = Utf8               java/lang/String
  #78 = Utf8               format
  #79 = Utf8               (Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
  #80 = Utf8               android/util/Log
  #81 = Utf8               d
  #82 = Utf8               (Ljava/lang/String;Ljava/lang/String;)I
  #83 = Utf8               android/view/View

2.ASMified插件中对应的内容

         根据上面蓝色标记的数字划分的5部分来看下具体生成的ASMified插件里面的代码实现。

        (1)第一部分

        

       (2)第二部分

        

         (3)第三部分

        

      (4)第四部分

               

        (5)第五部分

          

        从两边的对比上可以看到,其实ASMified插件中的内容就是完全对应的字节码。所以在使用ASM框架进行修改.class文件的时候,可以直接将代码复制到对应的回调方法中即可。

        遗留问题:Label的作用到底是用来做什么的? 

3.Label

        在ASMified插件中生成的代码中有一个Label对象,这个是用来做什么呢?官方API中描述如下:

        

         从描述中可以看出,该Label就是用来标记在方法对应的字节码的一个位置。这个Label主要用来jump、goto、switch和try catch。所以这个Label主要用来逻辑跳转。如果一个代码从起始位置执行到目标位置的时候,中间有多少行代码无法确认,那么对于起始位置和目标位置的索引值也是会时刻发生变化的,所以就可以用Label来代表这个抽象的位置,用来实现“if siwtch for while try catch”。

        所以对于上部分提到的这种顺序执行的代码,其实在编写ASM框架的代码的时候,其实作用是不大的。

4.visitLocalVariable()

        在ASMified插件中代码中还有几行methodVisitor.visitLocalVariable()调用,那么在编写ASM框架的代码的时候,这个是否也需要呢?

         visitLocalVariable(name, descriptor, signature, start, end, index);描述的是定义在字节码的Code属性中的LocalVariableTable和LocalVariableTypeTable属性中的调试信息(存储有关源码的局部变量的名词、类型以及作用范围)。不是正常操作所必需的,与StackMapTable的信息(存储的是JVM的局部变量表和操作栈数栈的类型信息)不同。

        其中start和end是该变量的起始和结束的作用域范围。也就是在字节码中标记的Label的位置。

        首先看下没有调用该方法的时候,最后经过ASM添加相应的代码之后,所生成的Java源码的局部变量如下:

    public void onClick(View v) 
        long var3 = System.currentTimeMillis();
        long var5 = System.currentTimeMillis() - var3;
        Log.d("AUTO", String.format("[%s] cost time is [%d] ms", var5));
    

        在字节码文件的Code属性下的 LocalVariableTable是没有对应这两个局部变量:

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      31     0  this   Lcom/android/androidplugin/ASMByte;
            4      31     1     v   Landroid/view/View;

        但是在打包成apk的时候,会将.class文件转换成.dex文件的那个Task中,输出对应的3的位置无局部变量,但不影响最后打包成apk。

> Task :app:dexBuilderHuaweiDebug
AGPBI: "kind":"warning","text":"Invalid stack map table at 28: lload 3, error: No local at index 3.","sources":["file":"/Users/j1/Documents/android/code/studio/AndroidPlugin/app/build/intermediates/transforms/AutoLogTransformTask/huawei/debug/34/com/android/androidplugin/MainActivity.class"],"tool":"D8"

        而添加visitLocalVariable的调用(仅对赋值一个beginTime做验证。实现方案:需要通过Label定义该beginTime的作用域的起始位置和结束位置,然后通过visitLocalVariable()进行对该变量进行命名和设置作用域),最后生成的Java源码的局部变量如下:

    public void onClick(View v) 
        long beginTime = System.currentTimeMillis();
        long var5 = System.currentTimeMillis() - beginTime;
        Log.d("AUTO", String.format("[%s] cost time is [%d] ms", var5));
    

        在查看字节码文件的Code属性下已经添加了beginTime这个局部变量:

      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      34     3 beginTime   J
            4      31     0  this   Lcom/android/androidplugin/ASMByte;
            4      31     1     v   Landroid/view/View;

         所以如果想要提供调试信息,则可以通过visitLocalVariable()为局部变量进行命名,否则可以不调用。

        上面不管是否调用visitLocalVariable(),因为为 ClassWriter设置了ClassWriter.COMPUTE_MAXS,会自动计算maxStack和maxLocals的个数。

三 总结

        在使用ASM框架编写字节码框架的时候,首先要把java源代码写好,然后将java源代码编译成.class文件,将ASMified插件中对应的代码放到ASM框架对应的回调方法中即可。

        后面还要在这个插件上在增加相应的功能来加深对java字节码的理解。加油

以上是关于Android Gradle 中的使用ASMified插件生成.class的技巧的主要内容,如果未能解决你的问题,请参考以下文章

android中的build.gradle是干啥用的

Android Gradle 插件Gradle 扩展属性 ② ( 定义在根目录 build.gradle 中的扩展属性 | 使用 rootProject.扩展属性名访问 | 扩展属性示例 )

Android Gradle 插件Gradle 扩展属性 ② ( 定义在根目录 build.gradle 中的扩展属性 | 使用 rootProject.扩展属性名访问 | 扩展属性示例 )

使用 gradle 7.0.0 与 Android 中的华为 HMS 插件冲突

在 Gradle 中为 Android 中的库项目构建变体

gradle build 未检测到 build.gradle 中的 android ndkVersion