[Android] Bytecode instrumentation technology realizes Caton monitoring

foreword

Bytecode instrumentation is not a new technology, but it is still widely used today. It can change our code like ghosts and ghosts, and realize some functions that we can't see or touch. Frameworks like Arouter, Hilt, Tinker, and Matrix are all using this technology. Bytecode instrumentation is an application of bytecode programming, where bytecode programming technology is mainly used. Bytecode programming can solve many problems, such as automatically generating classes, automatically modifying bytecodes, automatically adding log codes, and automatically inserting codes to achieve freeze monitoring. The author uses the bytecode instrumentation technology to realize the time-consuming automatic statistical method, realize UI freeze monitoring, and understand the whole process of bytecode instrumentation through this case.

Bytecode instrumentation

The so-called bytecode instrumentation is to dynamically inject new bytecodes into class files to enhance the functions of the original bytecodes. Of course, the original bytecode can also be repaired and deleted. It can add functions without writing java code. Generally, the class is filtered out through Gradle's Transform API, and then the bytecode is injected through the ASM framework or the Javaassit framework. Transform is an interceptor-like function of Gradle. It can obtain the output of the previous Transform as the input of the current Transform, and then modify the input content as the output of the current Transform and at the same time as the input of the next Transform. These input and output can be class files, jar files, dex files, etc. Filtering rules can be defined in Transform.

To be familiar with bytecode instrumentation technology, you need to understand common bytecode instructions and the process of Jvm executing bytecode. Bytecode instructions actually convert infix expressions into suffix expressions, also known as reverse Polish expressions. I won't explain it here, and I am interested in consulting professional information.

Dynamic injection of bytecode generally does not require us to write it manually, but first write a template class in Java, then check its bytecode, and copy the corresponding bytecode to Transform. Of course, you must first have a basic understanding of bytecode syntax.

Bytecode instrumentation monitoring stuck principle

The principle of bytecode stub monitoring is to insert code at the entrance and exit of the method to obtain the time, and count the difference between the two times, that is, the execution time of this method is time-consuming. If the time-consuming exceeds a certain value, it can be regarded as a freeze. For example, there are the following initConfigurationmethods, and you need to count its time consumption, you can insert the following code:

    override fun initConfiguration() {
    
    
     	long startTime = System.currentTimeMillis();
      	...
        long endTime = System.currentTimeMillis();
        System.out.println("executeTime=" + (endTime - startTime) + "ms");
    }

Just upload the time difference and method path information to the server. Here is to print out the time difference, just for testing.

If each method needs to manually add such code, it is obviously impractical. Therefore, it needs to be added by dynamically injecting bytecode. Not all methods require time-consuming statistics. We can identify that this method requires bytecode injection by adding custom annotations to the method.

Define annotations to identify methods that need to inject bytecode

First define an annotation named InjectTime:

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.BINARY)
annotation class InjectTime()

This is the grammar of kotlin. AnnotationTarget.FUNCTION means that it only works on methods. AnnotationRetention.BINARY is equivalent to Java language RetentionPolicy.CLASS:

public enum class AnnotationRetention {
    
    
    /** Annotation isn't stored in binary output */
    SOURCE,
    /** Annotation is stored in binary output, but invisible for reflection */
    BINARY,
    /** Annotation is stored in binary output and visible for reflection (default retention) */
    RUNTIME
}

Write a template class and get the bytecode

The author created a new Library with the following structure:
insert image description here
the InjectTestmodule class is used to obtain the required bytecode, and it can also verify whether the bytecode is injected successfully, InjectTimewhich is the annotation defined in the previous step. InjectTimeActivityIt is an Activity, and it is also used to verify whether the bytecode injection is successful.

InjectTestThe content is as follows:

package com.devnn.library3;

public class InjectTest {
    
    
    /**
     * 模板方法,为了获取sleep方法前后的字节码
     */
    public void test() throws InterruptedException {
    
    
        long startTime = System.currentTimeMillis();
        Thread.sleep(2000);
        long endTime = System.currentTimeMillis();
        System.out.println("executeTime=" + (endTime - startTime) + "ms");
    }

    /**
     * 测试方法,会注入新字节码
     */
    @InjectTime
    public void test1() {
    
    
        int i = 5;
        int j = 10;
        int result = test2(i, j);
        System.out.println("result=" + result);
    }

    /**
     * 测试方法,会注入新字节码
     */
    @InjectTime
    public int test2(int i, int j) {
    
    
        return i + j;
    }
}

The first test()method is a template method, we need to get the bytecode instructions of lines 1, 3, and 4, and then inject them into the target method.

InjectTimeActivity.ktThe content is as follows, and InjectTimethe methods with annotations can get their execution time.

package com.devnn.library3

import android.os.Bundle
import android.util.Log

import androidx.appcompat.app.AppCompatActivity

import com.alibaba.android.arouter.facade.annotation.Route
import com.devnn.library3.databinding.ActivityInjectBinding

@Route(path = "/lib3/InjectTimeActivity")
class InjectTimeActivity : AppCompatActivity() {
    
    

    private lateinit var binding: ActivityInjectBinding

    @InjectTime
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        Log.i("InjectTimeActivity", "onCreate")
        binding = ActivityInjectBinding.inflate(layoutInflater)
        setContentView(binding.root)
        setSupportActionBar(binding.toolbar)
        Thread {
    
    
            test()
        }.start()
    }

    @InjectTime
    fun test() {
    
    
        Log.i("InjectTimeActivity", "test")
        Thread.sleep(1000)
    }

    @InjectTime
    override fun onStart() {
    
    
        Log.i("InjectTimeActivity", "onStart")
        super.onStart()
    }

    @InjectTime
    override fun onResume() {
    
    
        Log.i("InjectTimeActivity", "onResume")
        super.onResume()
    }

    @InjectTime
    override fun onDestroy() {
    
    
        Log.i("InjectTimeActivity", "onDestroy")
        super.onDestroy()
    }

}

Now that we have manually added the code we need to the method of InjectTestthe class , how to get the bytecode of the method of the class ?testInjectTesttest

After building the project, find the compiled InjectTest.class file in the build directory, and then Asm Bytecode Viewercheck its bytecode content, or use other tools such as it javap.
insert image description here

The bytecode content of InjectTest is as follows:

// class version 52.0 (52)
// access flags 0x21
public class com/devnn/library3/InjectTest {
    
    

  // compiled from: InjectTest.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/devnn/library3/InjectTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public test()V throws java/lang/InterruptedException 
   L0
    LINENUMBER 8 L0
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 1
   L1
    LINENUMBER 9 L1
    LDC 2000
    INVOKESTATIC java/lang/Thread.sleep (J)V
   L2
    LINENUMBER 10 L2
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 3
   L3
    LINENUMBER 11 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "executeTime="
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LLOAD 3
    LLOAD 1
    LSUB
    INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    LDC "ms"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 12 L4
    RETURN
   L5
    LOCALVARIABLE this Lcom/devnn/library3/InjectTest; L0 L5 0
    LOCALVARIABLE startTime J L1 L5 1
    LOCALVARIABLE endTime J L3 L5 3
    MAXSTACK = 6
    MAXLOCALS = 5

  // access flags 0x1
  public test1()V
  @Lcom/devnn/library3/InjectTime;() // invisible
   L0
    LINENUMBER 19 L0
    ICONST_5
    ISTORE 1
   L1
    LINENUMBER 20 L1
    BIPUSH 10
    ISTORE 2
   L2
    LINENUMBER 21 L2
    ALOAD 0
    ILOAD 1
    ILOAD 2
    INVOKEVIRTUAL com/devnn/library3/InjectTest.test2 (II)I
    ISTORE 3
   L3
    LINENUMBER 22 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "result="
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ILOAD 3
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 23 L4
    RETURN
   L5
    LOCALVARIABLE this Lcom/devnn/library3/InjectTest; L0 L5 0
    LOCALVARIABLE i I L1 L5 1
    LOCALVARIABLE j I L2 L5 2
    LOCALVARIABLE result I L3 L5 3
    MAXSTACK = 3
    MAXLOCALS = 4

  // access flags 0x1
  public test2(II)I
    // parameter  i
    // parameter  j
  @Lcom/devnn/library3/InjectTime;() // invisible
   L0
    LINENUMBER 30 L0
    ILOAD 1
    ILOAD 2
    IADD
    IRETURN
   L1
    LOCALVARIABLE this Lcom/devnn/library3/InjectTest; L0 L1 0
    LOCALVARIABLE i I L0 L1 1
    LOCALVARIABLE j I L0 L1 2
    MAXSTACK = 2
    MAXLOCALS = 3

  // access flags 0x1
  public test3()V
   L0
    LINENUMBER 34 L0
    ICONST_5
    ISTORE 1
   L1
    LINENUMBER 35 L1
    BIPUSH 10
    ISTORE 2
   L2
    LINENUMBER 36 L2
    ILOAD 1
    ILOAD 2
    IADD
    ISTORE 3
   L3
    LINENUMBER 37 L3
    RETURN
   L4
    LOCALVARIABLE this Lcom/devnn/library3/InjectTest; L0 L4 0
    LOCALVARIABLE i I L1 L4 1
    LOCALVARIABLE j I L2 L4 2
    LOCALVARIABLE result I L3 L4 3
    MAXSTACK = 2
    MAXLOCALS = 4
}

What we want to get is the bytecode of the 8th, 10th, and 11th lines of the test method:
insert image description here

long startTime = System.currentTimeMillis();The corresponding bytecode of line 8 :

  INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 1

long endTime = System.currentTimeMillis();The bytecode corresponding to line 10 :

  INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 3

System.out.println("executeTime=" + (endTime - startTime) + "ms");The bytecode corresponding to line 11 :

  GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "executeTime="
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LLOAD 3
    LLOAD 1
    LSUB
    INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    LDC "ms"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V

After obtaining these bytecodes, we can inject them into the target method through Transform.

Here we inject bytecode through the ASM framework. There are two ways. One is to inject bytecode by calling the ASM API by hand, which is not conducive to modification. One is to generate ASM syntax codes in batches, copy and paste. You can view the ASM syntax through the plug-in, and then copy them to the corresponding callback in ASM.

insert image description here

Custom Gradle plugin to write Transform

For how to customize the Gradle plug-in, you can check another article of the author
Customizing the Gradle Plug-in - Realizing Cloud Configuration Project Dependencies

The plug-in project structure is as follows:
insert image description here

The plugin library needs to depend on gradle:

apply plugin: 'java-gradle-plugin'
apply plugin: 'maven'

dependencies{
    
    
    // gradle sdk
    implementation gradleApi()
    // groovy sdk
    implementation localGroovy()
    //gradle sdk
    implementation 'com.android.tools.build:gradle:4.2.1'
}

ASM is already included in gradle, so there is no need to introduce ASM.

MyAsmPluginThat is, the plugin implementation class:

package com.devnn.plugin;

import com.android.build.gradle.BaseExtension;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyAsmPlugin implements Plugin<Project>{
    
    

    @Override
    public void apply(Project project) {
    
    
        System.out.println("This is MyAsmPlugin");
        BaseExtension baseExtension = project.getExtensions().getByType(BaseExtension.class);
        baseExtension.registerTransform(new MyAsmTransform());
    }
}

This plug-in class is very simple, and a Transform is registered for Gradle in the apply method.

The content of the Transform implementation class MyAsmTransformis as follows:

public class MyAsmTransform extends Transform {
    
    


    /**
     * Transform的名称,会显示在build/intermediates/transforms目录下
     * @return
     */
    @Override
    public String getName() {
    
    
        return "MyAsmTransform";
    }

    /**
     * 接受的输入类型,这里只接受class类
     * @return
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
    
    
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 作用范围,SCOPE_FULL_PROJECT表示全工程,如果是作用在library中,只能选择PROJECT_ONLY
     * @return
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
    
    
        return TransformManager.PROJECT_ONLY;
    }

    /**
     * 是否增量编译
     * @return
     */
    @Override
    public boolean isIncremental() {
    
    
        return false;
    }

    /**
     * Transform变换的核心方法
     * @param transformInvocation
     * @throws TransformException
     * @throws InterruptedException
     * @throws IOException
     */
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    
    
        System.out.println("MyAsmTransform transform");
        super.transform(transformInvocation);
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();//获取当前transform的输出
        outputProvider.deleteAll();//清理文件
        Collection<TransformInput> inputs = transformInvocation.getInputs();//获取当前transform的输入

        for (TransformInput input : inputs) {
    
    
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
    
    
                String dirName = directoryInput.getName();
                System.out.println("dirName:" +dirName);
                File srcDir = directoryInput.getFile();
                System.out.println("src目录:" + srcDir.getAbsolutePath());
                String md5Name = DigestUtils.md5Hex(srcDir.getAbsolutePath());
                System.out.println("md5Name:" +md5Name);
                File destDir = outputProvider.getContentLocation(dirName + md5Name, directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                System.out.println("dest目录:" + destDir.getAbsolutePath());
                processInject(srcDir, destDir);
            }
        }

    }

    private void processInject(File srcDir, File destDir) throws IOException {
    
    
        System.out.println("MyAsmTransform processInject");
        FluentIterable<File> allSrcFiles = FileUtils.getAllFiles(srcDir);
        for (File srcFile : allSrcFiles) {
    
    
            //System.out.println("MyAsmTransform processInject file:" + absPath);
            if (!srcFile.getAbsolutePath().endsWith(".class")) {
    
    //kotlin写的代码有非class的文件
                continue;
            }
           FileInputStream fis = new FileInputStream(srcFile);
            //class读入器
            ClassReader classReader = new ClassReader(fis);

            //class写出器
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            
            //分析,将结果写入classWriter中 
            classReader.accept(new MyInjectTimeClassVisitor(classWriter, srcFile.getName()), ClassReader.EXPAND_FRAMES);

            //将classWriter中的字节码保存到文件中
            byte[] newClassBytes = classWriter.toByteArray();
            String absolutePath = srcFile.getAbsolutePath();
            String fullClassPath = absolutePath.replace(srcDir.getAbsolutePath(), "");
            File outFile = new File(destDir, fullClassPath);
            FileUtils.mkdirs(outFile.getParentFile());
            FileOutputStream fos = new FileOutputStream(outFile);
            fos.write(newClassBytes);
            fos.close();
        }
    }
}

Note that you need to add a class file filter:

  if (!srcFile.getAbsolutePath().endsWith(".class")) {
    
    //kotlin写的代码有非class的文件
      continue;

Because after the kotlin code is compiled, non-class files under META-INF are also input into transform: if
insert image description here
it is not filtered, an error will be reported when the class name is obtained later:
java.lang.ArrayIndexOutOfBoundsException
error code: className = fileName.substring(0, fileName .lastIndexOf(“.”));

MyInjectTimeClassVisitorThe content is as follows:

public class MyInjectTimeClassVisitor extends ClassVisitor {
    
    
    private String className;

    public MyInjectTimeClassVisitor(ClassVisitor classVisitor, String fileName) {
    
    
        super(Opcodes.ASM7, classVisitor);
        if (fileName != null && !fileName.isEmpty()) {
    
    
            className = fileName.substring(0, fileName.lastIndexOf("."));
        }
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
    
    
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new MyAsmAdviceAdapter(methodVisitor, access, name, descriptor, className);
    }
}

MyAsmAdviceAdapter is the last class that needs to inject bytecode, mainly to write injection code in onMethodEnter()and method.
onMethodExit()The content is as follows:

package com.devnn.plugin;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;

public class MyAsmAdviceAdapter extends AdviceAdapter {
    
    

    private String className;
    private String methodName;
    private boolean inject;
    private int index;
    private int start, end;

    protected MyAsmAdviceAdapter(MethodVisitor methodVisitor, int access, String name, String descriptor, String className) {
    
    
        super(Opcodes.ASM7, methodVisitor, access, name, descriptor);
        methodName = name;
        this.className = className;
        System.out.println("methodName=" + methodName);
        System.out.println("className=" + className);
    }
    
 	/**
     * 如果方法头上带有InjectTime注解,inject标记为true
     * @param desc
     * @param visible
     * @return
     */
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
    
    
        System.out.println("visitAnnotation|" + getName() + "|" + desc);
        if ("Lcom/devnn/library3/InjectTime;".equals(desc)) {
    
    
            inject = true;
        }
        return super.visitAnnotation(desc, visible);
    }
    
 	 /**
     * 方法入口回调
     */
    @Override
    protected void onMethodEnter() {
    
    
        if (!inject) {
    
    
            return;
        }
        /**
         long startTime = System.currentTimeMillis();
         INVOKESTATIC java/lang/System.currentTimeMillis ()J
         LSTORE 1
         */
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        index = newLocal(Type.LONG_TYPE);
        start = index;
        mv.visitVarInsn(LSTORE, start);
    }

 	 /**
     * 方法出口回调
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
    
    
        if (!inject) {
    
    
            return;
        }
        /**
         以下是"long endTime = System.currentTimeMillis();"对应的字节码

         INVOKESTATIC java/lang/System.currentTimeMillis ()J
         LSTORE 3

         以下是"System.out.println("ExecuteTime=" + (endTime - startTime) + "ms");"的字节码
         GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
         NEW java/lang/StringBuilder
         DUP
         INVOKESPECIAL java/lang/StringBuilder.<init> ()V
         LDC "ExecuteTime"
         INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
         LLOAD 3
         LLOAD 1
         LSUB
         INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
         LDC "ms"
         INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
         INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
         INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
         */

		//转化为ASM语法:
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        index = newLocal(Type.LONG_TYPE);
        end = index;
        mv.visitVarInsn(LSTORE, end);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);

        mv.visitLdcInsn(className);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

        mv.visitLdcInsn("#");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

        mv.visitLdcInsn(methodName);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

        mv.visitLdcInsn("#");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

        mv.visitLdcInsn("executeTime=");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(LLOAD, end);
        mv.visitVarInsn(LLOAD, start);
        mv.visitInsn(LSUB);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        mv.visitLdcInsn("ms");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

}

The subscript of the local variable table cannot be hard-coded when the method is executed, because each method is different, so it needs to be modified to be dynamic, and the calculated startTime and endTime are saved in the subscript of the local variable table through the two variables of start and end.

Here, in addition to the time-consuming printing method, the method name and class name are also printed, which are separated by # for easy verification.

Verify bytecode injection

After packaging the plug-in and introducing it into the gradle of the project, start building, and you can see the class modified by transform.

build/intermediates/transformsYou can view the transformed class in the directory :
insert image description here

InjectTest.class decompilation content is as follows:

package com.devnn.library3;

public class InjectTest {
    
    
    public InjectTest() {
    
    
    }

    public void test() throws InterruptedException {
    
    
        long startTime = System.currentTimeMillis();
        Thread.sleep(2000L);
        long endTime = System.currentTimeMillis();
        System.out.println("executeTime=" + (endTime - startTime) + "ms");
    }

    @InjectTime
    public void test1() {
    
    
        long var1 = System.currentTimeMillis();
        int i = 5;
        int j = 10;
        int result = this.test2(i, j);
        System.out.println("result=" + result);
        long var6 = System.currentTimeMillis();
        System.out.println("InjectTest" + "#" + "test1" + "#" + "executeTime=" + (var6 - var1) + "ms");
    }

    @InjectTime
    public int test2(int i, int j) {
    
    
        long var3 = System.currentTimeMillis();
        int var10000 = i + j;
        long var5 = System.currentTimeMillis();
        System.out.println("InjectTest" + "#" + "test2" + "#" + "executeTime=" + (var5 - var3) + "ms");
        return var10000;
    }

InjectTestCompare with the original java code:

public class InjectTest {
    
    
    /**
     * 模板方法,为了获取sleep方法前后的字节码
     */
    public void test() throws InterruptedException {
    
    
        long startTime = System.currentTimeMillis();
        Thread.sleep(2000);
        long endTime = System.currentTimeMillis();
        System.out.println("executeTime=" + (endTime - startTime) + "ms");
    }

    /**
     * 测试方法,会注入新字节码
     */
    @InjectTime
    public void test1() {
    
    
        int i = 5;
        int j = 10;
        int result = test2(i, j);
        System.out.println("result=" + result);
    }

    /**
     * 测试方法,会注入新字节码
     */
    @InjectTime
    public int test2(int i, int j) {
    
    
        return i + j;
    }


}

You can see that the code for method time-consuming statistics has been successfully injected.

Next, verify InjectTimeActivitythe log printed when the page is running:
insert image description here
you're done, you can see that the time-consuming statistics of the method have been successfully implemented through bytecode instrumentation!

This is the principle of realizing stuck monitoring. Save the method name, class name, and time-consuming to the log and upload it to the server. The code saved to the log also needs to be dynamically injected, and the principle is the same as above.

Guess you like

Origin blog.csdn.net/devnn/article/details/127588667