At the same time, it should be noted that we use testImplementation, which means that we can only use this framework in Java unit testing, which has no impact on our dependencies in Android.
The Android project using gradle in AS will automatically create Java unit tests and Android unit tests. The test code is in test and Android test respectively.
3.2 preparation of pile to be inserted
Create a Java class under test/java:
public class InjectTest { public static void main(String[] args) { } }
Since we operate bytecode instrumentation, we can go to test/java and use javac to compile this class to generate the corresponding class file.
javac InjectTest.java
3.3 pile insertion
Because there is no output code in the main method, we enter the command: javaInjectTest will not have any output when executing this Class. Next, we use ASM to insert the log output that records the execution time of the function in the starting diagram into the main method.
Write test method in unit test
/** * 1,Prepare class es to be analyzed */ FileInputStream fis = new FileInputStream ("xxxxx/test/java/InjectTest.class"); /** * 2,Perform analysis and pile insertion */ //class bytecode reading and analysis engine ClassReader cr = new ClassReader(fis); // Writer COMPUTE_FRAMES automatically calculates all contents, making subsequent operations easier ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //Analyze and write the processing results to cw EXPAND_FRAMES: the stack graph is accessed in an extended format cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES); /** * 3,Obtain results and output */ byte[] newClassBytes = cw.toByteArray(); File file = new File("xxx/test/java2/"); file.mkdirs(); FileOutputStream fos = new FileOutputStream ("xxx/test/java2/InjectTest.class"); fos.write(newClassBytes); fos.close();
We will not discuss the design of ASM framework itself here. The above code will get the class generated in the previous step, and then the ASM will output the results to the test/java2 directory after pile insertion. The key point is how to insert piles in step 2.
Give the class data to ClassReader for analysis, which is similar to XML parsing. The analysis results will be notified to the first parameter ClassAdapterVisitor of accept in an event driven form.
public class ClassAdapterVisitor extends ClassVisitor { public ClassAdapterVisitor(ClassVisitor cv) { super(Opcodes.ASM7, cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { System.out.println("method:" + name + " autograph:" + desc); MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); return new MethodAdapterVisitor(api,mv, access, name, desc); } }
The analysis results are obtained through ClassAdapterVisitor. There will be methods, annotations and properties in a class. Therefore, ClassReader will call the corresponding visitMethod, visitAnnotation and visitField visitXX methods in ClassAdapterVisitor.
Our purpose is to perform function instrumentation, so we override the visitMethod method, in which we return a MethodVisitor method parser object. The parameters, annotations and method body of a method need to be analyzed and processed in MethodVisitor.
package com.enjoy.asminject.example; import com.enjoy.asminject.ASMTest; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; import org.objectweb.asm.commons.AdviceAdapter; import org.objectweb.asm.commons.Method; /** * AdviceAdapter: Subclass * The methodVisitor is extended to make it easier for us to analyze methods */ public class MethodAdapterVisitor extends AdviceAdapter { private Boolean inject; protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) { super(api, methodVisitor, access, name, descriptor); } /** * Notes above analysis method * What are you doing here??? * <p> * Judge whether the current method uses injecttime. If so, we need to insert piles into this method * If it's not used, it doesn't matter. * * @param desc * @param visible * @return */ @Override public AnnotationVisitor visitAnnotation(String desc, Boolean visible) { if (Type.getDescriptor(ASMTest.class).equals(desc)) { System.out.println(desc); inject = true; } return super.visitAnnotation(desc, visible); } private int start; @Override protected void onMethodEnter() { super.onMethodEnter(); if (inject) { //What if it's done? Record to local variable invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J")); start = newLocal(Type.LONG_TYPE); //Create local LONG type variable //Record the method execution result to the created local variable storeLocal(start); } } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); if (inject){ invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J")); int end = newLocal(Type.LONG_TYPE); storeLocal(end); getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" + "/PrintStream;")); //Allocate memory and dup push it into the top of the stack so that the following INVOKESPECIAL knows whose construction method to execute to create StringBuilder newInstance(Type.getType("Ljava/lang/StringBuilder;")); dup(); invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V")); visitLdcInsn("execute:"); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;")); //subtraction loadLocal(end); loadLocal(start); math(SUB,Type.LONG_TYPE); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;")); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;")); invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V")); } } }
MethodAdapterVisitor inherits from AdviceAdapter, which is actually a subclass of MethodVisitor. AdviceAdapter encapsulates the instruction insertion method, which is more intuitive and simple.
In the above code, onMethodEnter calls back when it enters a method, so inserting an instruction in this method is to add some code at the beginning of the whole method. We need to insert long = system currentTimeMillis();. Insert the output code in onmethodext, that is, at the end of the method.
@Override protected void onMethodEnter() { super.onMethodEnter(); # last **Friends in need[You can get it for free here](https://gitee.com/vip204888/java-p7) this is Daniel's study note~** ![tencent T3 500 pages of Daniel's summary MySQL The actual combat notes exploded unexpectedly, P8 Read the call expert](https://img-blog.csdnimg.cn/img_convert/82aa6bc5be2d181753a68bdcd6a5829b.png) d onMethodEnter() { super.onMethodEnter(); # last **Friends in need[You can get it for free here](https://gitee.com/vip204888/java-p7) this is Daniel's study note~** [External chain picture transfer...(img-pdOmAY6y-1628664744458)] ![tencent T3 500 pages of Daniel's summary MySQL The actual combat notes exploded unexpectedly, P8 Read the call expert](https://img-blog.csdnimg.cn/img_convert/1891635a06ed9fb6d3040e42922ff4f9.png)