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 initConfiguration
methods, 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:
the InjectTest
module class is used to obtain the required bytecode, and it can also verify whether the bytecode is injected successfully, InjectTime
which is the annotation defined in the previous step. InjectTimeActivity
It is an Activity, and it is also used to verify whether the bytecode injection is successful.
InjectTest
The 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.kt
The content is as follows, and InjectTime
the 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 InjectTest
the class , how to get the bytecode of the method of the class ?test
InjectTest
test
After building the project, find the compiled InjectTest.class file in the build directory, and then Asm Bytecode Viewer
check its bytecode content, or use other tools such as it javap
.
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:
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.
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:
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.
MyAsmPlugin
That 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 MyAsmTransform
is 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
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(“.”));
MyInjectTimeClassVisitor
The 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/transforms
You can view the transformed class in the directory :
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;
}
InjectTest
Compare 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 InjectTimeActivity
the log printed when the page is running:
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.