Android Gradle 中的使用ASMified插件生成.class的技巧
Posted 好人静
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle 中的使用ASMified插件生成.class的技巧相关的知识,希望对你有一定的参考价值。
前言
逐步整理的一系列的总结:
Android 自定义Gradle插件的Extension类(五)
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 Gradle 插件Gradle 扩展属性 ② ( 定义在根目录 build.gradle 中的扩展属性 | 使用 rootProject.扩展属性名访问 | 扩展属性示例 )
Android Gradle 插件Gradle 扩展属性 ② ( 定义在根目录 build.gradle 中的扩展属性 | 使用 rootProject.扩展属性名访问 | 扩展属性示例 )