Android 方法耗时打印插件

最近在做启动优化,我需要打印出所有耗时的方法,或者是打印出所有耗时超过指定时间的方法,为此我写了这个工具,主要用到的技术有自定义gradle插件和asm字节码插桩。

具体效果如下, 插桩前:

  private void c() {
        try {
            Thread.sleep(80);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

插桩后:

private void c() {
    TimeMonitor.i("com/maove/methodcosttimesample/MainActivity", "c", "()V");

    try {
        Thread.sleep(80L);
    } catch (InterruptedException var2) {
        var2.printStackTrace();
    }

    TimeMonitor.o("com/maove/methodcosttimesample/MainActivity", "c", "()V");
}

所有的插桩都发生在gradle 构建期间,对我们的代码是没有侵入的。

1、自定义grade plugin

1、首先在项目中新建目录buildSrc,buildSrc不需要在 setting.gradle 中进行include,它会被自动识别为一个模块。

2、在buildSrc中新建build.gradlew脚本和src/main/groovysrc/main/javasrc/main/resources/META-INF/gradle-plugins

  • java源代码目录是可选的,因为这里我使用了java代码,你也可以使用kotlin。

  • resources文件用来配置插件描述,如下:

    gradle-plugins文件下的属性文件名,就是我们的插件id,通过implementation-class来指定插件实现类的全限定类名
    在这里插入图片描述

buildSrc的build脚本中配置我们编写插件需要的配置

plugins {
    id 'groovy'//使用groovy插件
}

repositories {
    mavenCentral()
    google()
    jcenter()
}

//指定编写插件需要使用的依赖
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.1.0'
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'
    testImplementation 'junit:junit:4.13.2'
}

//指定源码路径
sourceSets {
    main {
        groovy {
            srcDir '../src/main/groovy'
        }

        java {
            srcDir '../src/main/java'
        }

        resources {
            srcDir '../src/main/resources'
        }
    }
}
java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

自定义插件需要实现Plugin接口

class MethodTimePlugin implements Plugin<Project>{

    @Override
    void apply(Project project) {
        println "apply MethodTimePlugin..."
    }
}

当我们在app下的build脚本通过插件id来应用我们的插件,其apply方法就会被调用

apply plugin: 'org.maove.methodTimeBeat'

3、我们还可以为插件添加属性扩展

首先自定义一个类

class MethodTimeExt{
    public boolean isOpen = true;
    public String injectClass
    public String methodIn ="i";
    public String methodOut ="o";
}

然后创建Extension

project.extensions.create("methodTime", MethodTimeExt.class)

methodTime就是我们自定义的 Extension 了,它里面能配置的属性与类 MethodTimeExt 中的字段是一致的

这样我们就可以在使用插件时候对其进行配置

apply plugin: 'org.maove.methodTimeBeat'
methodTime{
    isOpen = true
    injectClass = "com/maove/libutil/TimeMonitor"
}

4、注册Transform

Gradle Transform 执行在.class转为.dex阶段,我们可以利用它的执行时机来修改class文件。

class MethodTimeTransform extends Transform{

    private Project project

    public MethodTimeTransform(Project project){
        this.project = project
    }

    //当前transform的名称
    @Override
    String getName() {
        return MethodTimeTransform.simpleName
    }

    //告知编译器,当前transform处理的输入类型,通常是class
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    //告知编译器,当前transform处理范围
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    //是否支持增量
    @Override
    boolean isIncremental() {
        return false
    }

    //编译器会把所有的class收集封装到TransformInvocation中,然后传给这个方法
    /**
     * 1、遍历所有的输入
     * 2、对input进行二次处理
     * 3、将input拷贝到目标目录
     */
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
				//code...
    }

}

在gradle中task是最小可执行单元。transform也会被转为一个task。

gradle生命周期分为3个阶段

  • 1、初始化阶段,该阶段主要是执行settings.gradle,确定哪些子模块会被include进来参与本次的构建。

  • 2、配置阶段,该阶段主要执行各个模块的build.gradle,创建task,建立task之间的依赖关系。

  • 3、执行阶段,根据依赖关系决定Task执行顺序。

插件定义完成只需要在transform获取到所有的class文件,对其利用ASM进行字节码注入就好了。

2、ASM介绍

使用实例:

//把类文件交给ClassReader进行读取
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor =  closure.call(classWriter)
//accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
//ClassWriter 是将修改后的类的字节码以字节数组的形式输出
byte[] code = classWriter.toByteArray()
  • ClassReader

ClassReader会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法

  • ClassWriter

ClassWriter 和 ClassReader 对应,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码以字节数组的形式输出

  • ClassVisitor 访问器,当读取到class中对应信息时,对应方法会被调用。
public class LogMethodTimeClassVisitor extends ClassVisitor {

    private MethodConfig methodConfig;

    public LogMethodTimeClassVisitor(ClassVisitor classVisitor, MethodConfig methodConfig) {
        //当前使用的ASM API版本
        super(Opcodes.ASM6, classVisitor);
        this.methodConfig = methodConfig;
    }

    /**
     * 访问类头部信息
     * @param version class版本
     * @param access class方法标识符
     * @param name class名称
     * @param signature 类签名
     * @param superName 父类名称
     * @param interfaces 实现的接口
     */
    @java.lang.Override
    public void visit(int version, int access, java.lang.String name, java.lang.String signature, java.lang.String superName, java.lang.String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        methodConfig.setClassName(name);
    }

    /**
     * 访问方法
     * @param access 方法访问标识符
     * @param name 方法名称
     * @param descriptor 方法描述符
     * @param signature 方法签名
     * @param exceptions 方法异常信息
     * @return
     */
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (methodVisitor==null){
            return null;
        }
        return new LogMethodTimeMethodVisitor(methodVisitor,access,name,descriptor,methodConfig);
    }

    /**
     * 访问类上的注解
     * @param descriptor 描述
     * @param visible 运行时是否可见
     * @return
     */
    @java.lang.Override
    public AnnotationVisitor visitAnnotation(java.lang.String descriptor, boolean visible) {
        return super.visitAnnotation(descriptor, visible);
    }

    /**
     * 访问类字段
     * @return
     */
    @java.lang.Override
    public FieldVisitor visitField(int access, java.lang.String name, java.lang.String descriptor, java.lang.String signature, java.lang.Object value) {
        return super.visitField(access, name, descriptor, signature, value);
    }

    /**
     * 在访问类的过程中最后一个被调用的方法,我们可以在这个方法中为类追加信息
     */
    @java.lang.Override
    public void visitEnd() {
        super.visitEnd();
    }
}

关于ClassVisitor还有其他方法我没有列出,常用的也就这几个。

当ClassReader读取到类方法时,就会调用ClassVisitor的visitMethod方法,我们可以构造一个MethodVisitor来参与方法的处理。

通常我们不用直接继承MethodVisitor,使用MethodVisitor的复杂性也更大,我么可以使用它的子类AdviceAdapter

public class LogMethodTimeMethodVisitor extends AdviceAdapter {

    protected LogMethodTimeMethodVisitor(MethodVisitor methodVisitor, int access, String name, String descriptor,MethodConfig methodConfig) {
        super(Opcodes.ASM6, methodVisitor, access, name, descriptor);
        //code..
    }

    /**
     * 进入方法时调用
     */
    @Override
    protected void onMethodEnter() {
        //code...
    }

    /**
     * 即将从方法退出时调用
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
        //code...
    }

    /**
     * 在访问方法的过程中最后一个被调用的方法
     */
    @java.lang.Override
    public void visitEnd() {
        super.visitEnd();
    }
}

当ClassReader读取到字段时,就会调用ClassVisitor的visitField方法,我们可以构造一个FieldVisitor来参与字段的处理,这里就不细说了。
在实例代码中我只是打印了方法的耗时。我们还可以结合Trace,在每个方法的开始结尾进行插桩,这样可以看到更加详细的信息。

通过plugin和asm结合对class进行插桩,我么可以监控代码耗时、代码替换、埋点、修复三方代码简单bug、性能监控、快速点击处理等等

具体代码:github

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android利用ASM(字节码操作框架)监控方法耗时的步骤如下: 1. 添加ASM依赖库 在build.gradle文件中添加ASM依赖库: ```gradle dependencies { implementation 'org.ow2.asm:asm:9.1' } ``` 2. 编写ASM代码 使用ASM编写一个ClassVisitor,用于修改字节码,在方法的开头和结尾插入计时代码: ```java public class TimeClassVisitor extends ClassVisitor { public TimeClassVisitor(int api, ClassVisitor cv) { super(api, cv); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); if (!name.equals("<init>") && !name.equals("<clinit>")) { mv = new TimeMethodVisitor(api, mv, access, name, descriptor); } return mv; } static class TimeMethodVisitor extends MethodVisitor { private final String mName; private final String mDesc; private final Label mStartLabel = new Label(); private final Label mEndLabel = new Label(); public TimeMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) { super(api, mv); mName = name; mDesc = desc; } @Override public void visitCode() { super.visitCode(); mv.visitLabel(mStartLabel); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); mv.visitFieldInsn(PUTSTATIC, "com/example/MyClass", "sStartTime", "J"); } @Override public void visitInsn(int opcode) { if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) { mv.visitLabel(mEndLabel); mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false); mv.visitFieldInsn(GETSTATIC, "com/example/MyClass", "sStartTime", "J"); mv.visitInsn(LSUB); mv.visitFieldInsn(PUTSTATIC, "com/example/MyClass", "sStartTime", "J"); } super.visitInsn(opcode); } @Override public void visitMaxs(int maxStack, int maxLocals) { mv.visitMaxs(maxStack + 4, maxLocals); } } } ``` 3. 在应用中使用ASM代码 在应用中调用ASM代码,在Activity的onCreate方法中插入以下代码: ```java try { ClassReader cr = new ClassReader(getClass().getName()); ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); TimeClassVisitor tcv = new TimeClassVisitor(Opcodes.ASM7, cw); cr.accept(tcv, ClassReader.EXPAND_FRAMES); byte[] code = cw.toByteArray(); Class<?> clazz = defineClass(getClass().getName(), code, 0, code.length); clazz.newInstance(); } catch (Exception e) { e.printStackTrace(); } ``` 以上代码将读取当前Activity的字节码,使用TimeClassVisitor修改字节码,然后重新定义类并实例化,以便在方法中添加计时代码。 4. 打印方法耗时 在Activity的onDestroy方法打印方法耗时: ```java Log.i("Time", "onCreate: " + (sStartTime / 1000000f) + "ms"); ``` 以上代码将打印onCreate方法耗时,单位为毫秒。 通过以上步骤,就可以使用ASM监控方法耗时了。需要注意的是,ASM会修改字节码,可能会导致应用崩溃或出现其他问题,所以需要谨慎使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值