IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android 方法耗时打印插件 -> 正文阅读

[移动开发]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

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-12 17:41:04  更:2022-03-12 17:41:39 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 18:47:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码