devnn

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

已于 2023-08-30 12:35:19 修改
2022-10-29 18:51:51

阅读量2.3k

收藏 5

2赞

前言

字节码插桩早已不是什么新鲜的技术了,但时至今日仍然在广泛应用。它能鬼使神差一般改变我们的代码,实现一些功能让我们看不见摸不着。像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页面运行时打印的日志:
在这里插入图片描述
大功告成,可以看到,已经成功通过字节码插桩实现了方法耗时统计!

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

写评论
5
文章收藏成功
前往CSDN APP阅读全文
CSDN APP记录你的成长
微信小程序收藏浏览更方便
截图/长按 保存本地,用微信扫码打开
进入小程序随时浏览/收藏技术文章
需要前往CSDN APP登录即可继续互动
成就一亿技术人!
拼手气红包6.0元
发红包
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
前往CSDN APP阅读全文
阅读体验更佳

CSDN

成就一亿技术人

浏览器
分享
请升级应用版本