【Android】字节码插桩技术实现卡顿监控

ASM字节码插桩(AOP思想) 什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加 从技术上来说,字节码插桩是 自定义Gradle插件+ASM+Java字节码+切面编程 的综合应用 字节码插桩可以做什么 举个例子,APP全量统计的时候,经常需要建立很多埋点。这是个很大重复性工作,那么可以通过字节码插桩,在apk打包之前,对class文件需要的地方进行埋点。这样就可以实现无埋点的全量统计。 步骤 1、自定义Gradle插件 作用:将处理java字节码的Transform注 阅读详情

前言

字节码插桩早已不是什么新鲜的技术了,但时至今日仍然在广泛应用。它能鬼使神差一般改变我们的代码,实现一些功能让我们看不见摸不着。像Arouter、Hilt、Tinker、Matrix这些框架都在使用这项技术。字节码插桩是字节码编程的一个应用,这里主要是使用字节码编程技术。字节码编程能解决很多问题,比如自动生成class,自动修改字节码,自动添加日志代码,自动插入代码实现卡顿监控。笔者通过字节码插桩技术实现自动统计方法耗时,实现UI卡顿监控,通过这个案例了解字节码插桩的全过程。

字节码插桩

所谓字节码插桩,就是在class文件里动态注入新的字节码,增强原有字节码的功能。当然也可以修复和删除原有字节码。它可以不用写java代码来实现功能的新增。一般是通过Gradle的Transform API来过滤出class,然后通过ASM框架或者Javaassit框架注入字节码。Transform是Gradle的一个类似拦截器的功能,可以获取上一个Transform的输出,作为当前Transform的输入,然后修改输入内容,作为当前Transform的输出,同时作为下一个Transform的输入。这些输入输出可以是class文件、jar文件、dex文件等,在Transform中可以定义过滤规则。

熟悉字节码插桩技术需要先了解常见的字节码指令以及Jvm执行字节码的过程,字节码指令其实是将中缀表达式转化成了后缀表达式,也叫逆波兰表达式,在这里就不讲解了,有兴趣查阅专业资料。

动态注入字节码,一般不需要我们手动去写,而是先用Java写好一个模板类,然后查看它的字节码,将相应的字节码复制到Transform中。当然先要对字节码语法有基本的了解。

字节码插桩监控卡顿原理

字节码插桩监控卡顿的原理就是在方法的入口和出口的地方插入代码获取时间,统计两个时间差,即是这个方法的执行耗时,耗时超过一定值可视为卡顿。例如有以下initConfiguration方法,需要统计它的耗时,可以插入如下代码:

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

将时间差及方法路径信息上传到服务器即可。这里是将时间差打印出来,仅供测试。

如果每个方法都需要手动去添加这样的代码,很显然是不切实际的。因此需要通过通过动态注入字节码的方式来添加。不是所有方法都需要统计耗时,我们可以通过给方法添加自定义注解的方式来标识此方式需要注入字节码。

定义注解,标识方法需要注入字节码

先定义一个注解,命名为InjectTime:

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

这是kotlin的语法,AnnotationTarget.FUNCTION表示只作用在方法上,AnnotationRetention.BINARY等同于Java语言的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
}

编写模板类,获取字节码

笔者新建了一个Library,结构如下:
在这里插入图片描述
其中InjectTest是模块类,用于获取需要的字节码,同时也可以验证字节码是否注入成功,InjectTime即是上一步定义的注解。InjectTimeActivity是一个Activity,也是用来验证字节码是否注入成功。

InjectTest 内容如下:

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;
    }
}

其中第一个test()方法是一个模板方法,我们需要获取到第1、3、4行的字节码指令,然后注入到目标方法中。

InjectTimeActivity.kt的内容如下,添加了InjectTime注解的方法都可以获取它们的执行时间。

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()
    }

}

现在已经手动给InjectTest类的test方法添加了我们需要的代码,如何获取InjectTest类的test方法的字节码呢?

将项目build之后,在build目录下找到编译好的InjectTest.class文件,然后通过Asm Bytecode Viewer查看它的字节码内容,也可以通过其它工具比如javap也行。
在这里插入图片描述

InjectTest的字节码内容如下:

// 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
}

我们要获取的是test方法第8行、第10行、第11行代码的字节码:
在这里插入图片描述

第8行的long startTime = System.currentTimeMillis();对应的字节码:

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

第10行 long endTime = System.currentTimeMillis();对应的字节码:

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

第11行 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

获取到这些字节码之后,我们就可以通过Transform注入到目标方法中。

这里我们通过ASM框架来注入字节码,有两种方式,一种是通过手写调用ASM API注入字节码,这样不利于修改。一种是批量生成ASM语法代码,复制粘贴。通过插件可以查看ASM语法,到时候将它们复制到ASM中的对应回调中即可。

在这里插入图片描述

自定义Gradle插件编写Transform

关于如何自定义Gradle插件,可以查看笔者的另一篇文章
自定义Gradle插件——实现云端配置项目依赖

插件工程结构如下:
在这里插入图片描述

插件library需要依赖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'
}

gradle中已经包含了ASM,不需要额外再引入ASM。

MyAsmPlugin即是插件实现类:

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());
    }
}

这个插件类很简单,在apply方法里给Gradle注册了一个Transform。

Transform实现类MyAsmTransform内容如下:

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();
        }
    }
}

注意要加一个class文件的过滤:

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

因为kotlin代码编译后有META-INF下的非class文件也被输入进了transform:
在这里插入图片描述
不过滤的话,后面获取类名时会报错:
java.lang.ArrayIndexOutOfBoundsException
报错代码:className = fileName.substring(0, fileName.lastIndexOf(“.”));

MyInjectTimeClassVisitor内容如下:

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是最后需要注入字节码的类,主要是在onMethodEnter()和
onMethodExit()方法中编写注入代码。内容如下:

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);
    }

}

方法执行时的局部变量表的下标号不能写死,因为每个方法不一样,所以需要修改成动态的,通过start和end两个变量保存计算好的startTime和endTime在局部变量表的下标。

这里除了打印方法耗时,还打印了方法名和类名,通过#号分隔了,方便验证。

验证字节码注入

下面将插件打包并引入到项目的gradle之后,开始build,即可看到通过transform修改之后的类。

在build/intermediates/transforms目录下即可查看变换之后的类:
在这里插入图片描述

InjectTest.class反编译内容如下:

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的java代码对比下:

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;
    }


}

可以看到已经成功注入了方法耗时统计的代码了。

下面再验证一下InjectTimeActivity页面运行时打印的日志:
在这里插入图片描述
大功告成,可以看到,已经成功通过字节码插桩实现了方法耗时统计!

实现卡顿监控的原理就是这样,将方法名、类名、耗时保存到日志,上传到服务器即可。保存到日志的代码也是需要动态注入的,原理跟上面一样。

字节码插桩 -- 入门篇 Android Studio --> File --> New --> New Module --> Java or Kotlin Library --> plugin(名字自取)/***/api: Int,/*** 方法开始执行*//*** 方法执行结束*/"append",",false"append",",false",false"d",)I",false。 阅读详情

相关推荐

App流畅度优化:利用字节码插桩实现一个快速排查高耗时方法的工具

    我们产线的主流程页面中有几个比较复杂的页面在版本迭代中流畅度频繁出现反复,经常由于开发的不注意导致变卡,主要是对流畅度缺少必要的监控和可持续的优化手段,这个系列是对上半年实践App流畅度监控、优化过程中的一点总结,希望可以给需要的同学一点小参考。 当然App内存上的优化,尽量减少内存抖动也能显著提升流畅度,内存优化方面可以参考之前的文章:实践App...

南朝烟雨的博客 2850

“ 字节码插桩 “ 技术简介

性能优化 ,插件化 ,热修复 ,等技术都需要用到" 字节码插桩 "相关技术;OOP 面向对象编程, 主要针对业务逻辑进行开发 ,大特点封装继承多态;大特点 封装 , 继承 , 多态;大特点封装继承多态;AOP 面向切面编程, 主要针对某个动作进行开发;如 : 给 Android 中所有的 Activity 添加安全策略 , 如果每个页面逐个手动添加 , 可能存在遗漏 , 并且会造成代码冗余;通过 AOP 面向切面编程 , 完成上述操作;

MayMatrix 的博客 1802

Android字节码插桩:轻松掌握技巧

字节码插桩是指在编译期或运行期修改Java或Kotlin源代码编译生成的字节码的过程。通过字节码插桩,我们可以在不改变源代码的情况下,对应用程序的行为进行定制和增强。性能优化:通过插入计时代码或者统计代码执行次数,我们可以对应用程序进行性能分析和优化。功能增强:通过在特定的方法中插入额外的代码,我们可以增加应用程序的功能,如添加日志记录、异常捕获等。安全增强:通过在敏感方法中插入安全检查代码,我们可以提高应用程序的安全性,例如防止数据泄露或者篡改。

JaDentenet的博客 303

Android 字节码插桩全流程解析

在[Android进阶宝典 -- Handler应用于线上卡顿监控]中,我简单介绍了一下关于ASM实现字节码插桩来实现方法耗时的监控,但是当时只是找了一个特定的class文件,针对某个特定的方法进行插桩,但是真正的开发中不可能这么做的,因为整个工程中会有成百上千的方法,而且存储的位置也各有不同

Eqiqi的博客 1613

手把手教你用ZEMAX复现贝塞尔光束:从Thorlabs锥透镜到Edmund透镜的完整光路搭建

本文详细介绍了如何在ZEMAX光学设计软件中复现贝塞尔光束,从Thorlabs锥透镜到Edmund透镜的完整光路搭建过程。通过逐步指导,帮助研究人员和工程师掌握光学设计技巧,实现无衍射特性的贝塞尔光束生成,适用于激光加工和生物成像等领域。

weixin_33737774的博客 176

银河麒麟高级服务器操作系统V10SP2(X86)PXE无人值守批量部署实战指南

本文详细介绍了银河麒麟高级服务器操作系统V10SP2(X86)PXE无人值守批量部署的实战指南。通过PXE网络引导技术,实现全自动化、高效率的系统部署,特别适用于企业级数据中心和国产化替代项目。文章涵盖基础环境准备、关键服务配置、Kickstart高级定制及部署测试等核心内容,帮助用户快速掌握批量部署技巧。

weixin_42525637的博客 382

Android 字节码插桩技术实现卡顿监控

字节码插桩早已不是什么新鲜的技术了,但时至今日仍然在广泛应用。它能鬼使神差一般改变我们的代码,实现一些功能让我们看不见摸不着。像Arouter、Hilt、Tinker、Matrix这些框架都在使用这项技术。字节码插桩是字节码编程的一个应用,这里主要是使用字节码编程技术。字节码编程能解决很多问题,比如自动生成class,自动修改字节码,自动添加日志代码,自动插入代码实现卡顿监控。笔者通过字节码插桩技术实现自动统计方法耗时,实现UI卡顿监控,通过这个案例了解字节码插桩的全过程。

Dream It Possible 289

STM32(HAL库)软件IIC驱动OLED

本文通过STM32F103C8T6单片机(HAL库)通过软件IIC方式对OLED进行驱动。

weixin_44597885的博客 9755

Android进阶宝典 -- Handler应用于线上卡顿监控

我详细介绍了对于线上App内存监控的方案策略,其实除了内存指标之外,经常有用户反馈卡顿问题,其实这种问题是最难定位的,因为不像Crash有完整的堆栈信息,而且卡顿问题可能转瞬即逝,那么如何健全完整的线上卡顿监控,可能就需要我们对于Android系统的消息处理有一个清晰的认知。

m0_59162559的博客 734

未连接互联网的银河麒麟v10安装g++,各种报错,如何处理?

🏆本文收录于《全栈Bug调优(实战版)》专栏,主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!! 备注:部分问题/疑难杂症搜集于互联网。

**My Coding Family** 1803

Xilinx高级调试方法--远程调试

本文主要介绍Xilinx的一些高级调试方法,以及如何使用Xilinx的相关IP。

斌赋的博客 3307

从OutSiderAPM看Android APM技术演进:字节码插桩与性能监控的未来趋势

> 这篇文章为你梳理Android性能监控(APM)的发展脉络,并以OutSiderAPM这个开源项目为样本,剖析**Android APM字节码插桩**的核心原理、技术选型与未来趋势。无论你是刚接触性能优化的新手,还是想深入理解APM架构的进阶开发者,都能从中获得一套完整的认知框架。 ## 一、为什么Android应用离不开APM性能监控? 在移动互联网时代,应用卡顿、ANR、内存泄漏、流量

gitblog_01148的博客 996

Python与网络爬虫案例:电影评论抓取

接下来,我们就可以通过分析该页面中的HTML代码,来确定需要抓取的内容。在这段代码中,我们首先对每个评论div元素进行for循环,然后通过find方法获取class为'title'、'text'、'rating-other-user-rating'的元素,并将结果分别赋值给title、content和rating变量。在这段代码中,我们首先定义了目标网站的URL和headers信息,然后通过requests.get方法向目标网站发送了一个GET请求,并将返回的响应赋值给了response变量。

吃不胖. 1262

Android网络性能监控方案 android线上性能监测

因为在一个类中,会存在很多属性,例如变量、方法、注解等等,所以在ASM中的ClassVisitor类中,提供了这些属性的访问权利,例如visitMethod可以访问方法,假如我们想要对funA进行插桩,那么就需要做一些自定义的操作,这里就可以使用ASM提供的AdviceAdapter来完成方法执行过程中代码的插入。首先我们看如果按照我们这种加代码的方式,当然没问题,但是在进行插桩的时候,将会写很多的字节码指令,看下面的代码,我仅仅贴出L2代码块就需要这么多,写的多通常就会出问题。

hcgeng的专栏 1542

39、信号流图:解决线性代数方程组的图论方法

本文介绍了信号流图理论在求解线性代数方程组中的应用,重点阐述了Coates方法和Mason方法的原理、步骤及对比分析。通过图论视角,将矩阵运算转化为对有向图结构的分析,利用1-因子、1-阶连接、前向路径和反馈回路等概念,推导出方程组解的拓扑公式。文章结合具体实例演示两种方法的计算过程,并总结其在电路分析、控制系统和通信系统等领域的拓展应用,展示了信号流图理论的直观性与有效性。

pear55的博客 87

全网最详细的 Ubuntu 18.04 安装Livox mid-360驱动,测试 fast_lio2

Livox mid-360需要使用Livox-SDK2,而非Livox-SDK,以及对应的。并需要修改FAST_LIO中部分代码。Faster—LIO;Faster-LIO:快速激光IMU里程计。Faster-LIO:快速激光IMU里程计PDF。大部分Lidar SLAM都是基于loam进行改造的,基本上是通过线特征edge和面特征(面点)分割进行配准得到里程计的,而fast-lio2是通过一个迭代扩展卡尔曼滤波器去优化帧内位姿,然后通过输入点云与地图进行配准得到里程计,精度和鲁棒性都不错。

努力努力努力 6234

卡顿、死锁、ANR原理,线上监控方案分析

一、前言 最近参加了几轮面试,发现很多5-7年工作经验的候选人在性能优化这一块,基本上只能说出传统的分析方式,例如ANR分析,是通过查看/data/anr/ 下的log,分析主线程堆栈、cpu、锁信息等, 然而,这种方法有一定的局限性,并不是每次都奏效,很多时候是没有堆栈信息给你分析的,例如有些高版本设备需要root权限才能访问/data/anr/ 目录,或者是线上用户的反馈,只有一张ANR的截图加上一句话描述。 假如你的App没有实现ANR监控上报,那么你大概率会把这个问题当成“未复现”处理掉,而没有真正

Androiddddd的博客 1159

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

OutSiderAPM Demo实战教程:6个示例页面快速上手卡顿、线程与网络监控

OutSiderAPM 是一款功能全面的 Android 应用性能监控平台(APM),覆盖卡顿、线程、网络、内存、ANR、流量、电量等核心维度,并以"非侵入式"的 Gradle 插件 + ASM 字节码插桩方式接入,对业务代码零侵入。本文带你跑一遍官方 Demo,用 6 个示例页面快速上手卡顿监控、线程监控与网络监控的完整玩法,新手也能 10 分钟看懂每个示例背后的监控原理。 ## 第一步:获取

gitblog_01089的博客 209

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Matrix Android性能监控深度解析:Trace Canary模块

Matrix Android性能监控深度解析:Trace Canary模块 【免费下载链接】matrix Matrix is a plugin style, non-invasive APM system developed by WeChat. ...

gitblog_00200的博客 791

ArgusAPM 源码解析与学习

ArgusAPM 移动性能监控平台是 360 开源的一款 APM 监测工具。 目前已经停止免费的服务端接入,但是对于我们从客户端的角度来搭建 监测还是有不错的学习参照意义。目前支持如下性能指标:本文也将围绕 总体框架和支持的功能点进行分析。项目分为三个目录,分别是:工程定义了一个名为 的 ,主要有以下两个作用:关于实现一个 的过程这里就不赘述了,下面看一下整体的框架。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vt1jfbtf-1655453421261)(https:/

m0_56255097的博客 919

卡顿,附答案+考点

不过呢,高版本的Handler的构造方法可以通过传async=true,来使用异步消息 public Handler(@Nullable Callback callback, boolean async) {} 然后在Handler发送消息的时候,都会走到 enqueueMessage 方法,如下代码块所示,每个消息都带了异步属性,有优先处理权 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message ms

m0_61408959的博客 484

腾讯开源matrix使用

Matrix是一个全面的Android性能监控框架,提供APK安装包分析(APKChecker)、内存泄漏检测(ResourceCanary)、卡顿追踪(TraceCanary)、SQLite优化(SQLiteLint)、IO问题检测(IOCanary)、耗电监控(BatteryCanary)等功能。其特点包括:1) 支持持续集成,2) 高性能低侵入检测,3) 提供详细的可视化报告。通过动态配置和插件机制,开发者只需简单初始化即可监控关键性能指标,并通过日志或埋点上报问题。特别适用于开发测试阶段发现性能隐患

u011324501的专栏 245

卡顿、ANR、死锁,线上如何监控?

在这里小编整理了一份Android大厂常见面试题,和一些Android架构视频解析,都已整理成文档,全部都已打包好了,希望能够对大家有所帮助,在面试中能顺利通过。喜欢本文的话,不妨顺手给我点个小赞、评论区留言或者转发支持一下呗《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取![外链图片转存中…(img-TktMEYfi-1712383169875)][外链图片转存中…(img-rqO7PQZn-1712383169875)]

bfhye16552的博客 1182
上一篇: 【Android】深入理解Android View的绘制流程
下一篇: 【Kotlin】从字节码角度理解kotlin构造函数、成员变量、init代码块执行顺序
devnn
博客等级 码龄12年 2005粉丝 90原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值