注册

Android asm加注解实现自动Log打印

Android asm加注解实现自动Log打印

前言

在Android开发中有时候调试问题要给方法加很多的log,很麻烦,所以结合asm用注解的方式来自动在方法中插入log,这样方便开发时候调试。当然通过asm插入的log应该需要包含方法的参数,方法的返回值,有时候也需要获取对象里面的变量值等。

hanno

_    _
| | | |
| |__| | __ _ _ __ _ __ ___
| __ |/ _` | '_ \| '_ \ / _ \
| | | | (_| | | | | | | | (_) |
|_| |_|\__,_|_| |_|_| |_|\___/
复制代码

通过字节码插件实现注解打印log,注解可以加在类上面,也可以加在方法上面,当加在类上面时会打印全部方法的log,当加在方法上面时打印当前方法的log

使用方法

1、类中全部方法打印log

@HannoLog
class MainActivity : AppCompatActivity() {
// ...
}
复制代码

只要在类上面加上@HannoLog注解就可以在编译的时候给这个类中所有的方法插入log,运行时输出log。

2、给类中的某些方法加log

class MainActivity : AppCompatActivity() {
@HannoLog(level = Log.INFO, enableTime = false,watchField=true)
private fun test(a: Int = 3, b: String = "good"): Int {
return a + 1
}
}
复制代码

通过在方法上面添加注解可以在当前方法中插入log。 3、打印的log

//D/MainActivity: ┌───────────────────────────────────------───────────────────────────────────------
//D/MainActivity: │ method: onCreate(android.os.Bundle)
//D/MainActivity: │ params: [{name='savedInstanceState', value=null}]
//D/MainActivity: │ time: 22ms
//D/MainActivity: │ fields: {name='a', value=3}{name='b', value=false}{name='c', value=ccc}
//D/MainActivity: │ thread: main
//D/MainActivity: └───────────────────────────────────------───────────────────────────────────------
复制代码

其中method是当前方法名,params是方法的参数名和值,time方法的执行时间,fields是当前对象的fields值,thread当前方法执行的线程。

HannoLog参数解释

可以通过level来设置log的级别,level的设置可以调用Log里面的INFO,DEBUG,ERROR等。enableTime用来设置是否打印方法执行的时间,默认是false,如果要打印设置enableTime=true. tagName用于设置log的名称,默认是当前类名,也可以通过这个方法进行设置。

1、level控制log打印的等级,默认是log.d,可以通过@HannoLog(level = Log.INFO)来设置等级,支持Log.DEBUG,Log.ERROR等。

2、enableTime控制是否输出方法的执行时间,默认是false,如果要打印可以通过@HannoLog(enableTime=true)来设置。

3、tagName设置tag的名称,默认是当前类名,也可以通过 @HannoLog(tagName = "test")来设置。

4、watchField用于观察对象中的field值,通过@HannoLog(watchField = true)设置,由于静态方法中不能调用非静态的field所以这个参数在静态方法上统一不生效。

重要的类

1、HannoLog HannoLog是注解类,里面提供了控制参数。对应上面的HannoLog参数解释

/**
*
*
*
* create by 胡汉君
* date 2021/11/10 17:38
* 定义一个注解,用于标注当前方法需要打印log
*/

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface HannoLog {
//定义一下log的级别,默认是3,debug级别
int level() default Log.DEBUG;
/**
* @return 打印方法的运行时间
*/

boolean enableTime() default false;

/**
* @return tag的名称,默认是类名,也可以设置
*/

String tagName() default "";

/**
* @return 是否观察field的值,如果观察就会就拿到对象里面全部的field值
*/

boolean watchField() default false;
}
复制代码

2、HannoExtension

public class HannoExtension {
//控制是否使用Hanno
boolean enable;
//控制是否打印log
boolean openLog = true;

public boolean isEnableModule() {
return enableModule;
}

public void setEnableModule(boolean enableModule) {
this.enableModule = enableModule;
}

//设置这个值为true可以给整个module的方法增加log
boolean enableModule = false;

public boolean isEnable() {
return enable;
}

public boolean isOpenLog() {
return openLog;
}

public void setOpenLog(boolean openLog) {
this.openLog = openLog;
}

public void setEnable(boolean enable) {
this.enable = enable;
}
}
复制代码

HannoExtension提供gradle.build文件是否开启plugin 和打印执行plugin的log 默认情况下添加HannoLog之后会进行asm插装,也可以通过在module的build.gradle文件中添加以下配置使在编译时不执行字节码插装提高编译速度

apply plugin: 'com.hanking.hanno'
hannoExtension{
enable=false
openLog=false
}
复制代码

实现原理

hanno是通过asm字节码插桩方式来实现的。Android项目的编译过程如下图: 在这里插入图片描述 java编译器会将.java类编译生成.class类,asm可以用来修改.class类,通过对.class类的修改就可以达到往已有的类中加入代码的目的。一个.java文件经过Java编译器(javac)编译之后会生成一个.class文件。 在.class文件中,存储的是字节码(ByteCode)数据,如下图所示。 在这里插入图片描述 ASM所的操作对象是是字节码(ByteCode)的类库。ASM处理字节码(ByteCode)数据的流程是这样的:

第一步,将.class文件拆分成多个部分;

第二步,对某一个部分的信息进行修改;

第三步,将多个部分重新组织成一个新的.class文件。

ClassFile

.class文件中,存储的是ByteCode数据。但是,这些ByteCode数据并不是杂乱无章的,而是遵循一定的数据结构。

ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
复制代码

字节码的类库和ClassFile之间关系 在这里插入图片描述

asm的组成

从组成结构上来说,ASM分成两部分,一部分为Core API,另一部分为Tree API。

  • 其中,Core API包括asm.jar、asm-util.jar和asm-commons.jar;
  • 其中,Tree API包括asm-tree.jar和asm-analysis.jar。

在这里插入图片描述

asm中重要的类

  • ClassReader类,负责读取.class文件里的内容,然后拆分成各个不同的部分。
  • ClassVisitor类,负责对.class文件中某一部分里的信息进行修改。
  • ClassWriter类,负责将各个不同的部分重新组合成一个完整的.class文件。

在这里插入图片描述

.class文件 --> ClassReader --> byte[] --> 经过各种转换 --> ClassWriter --> byte[] --> .class文件
复制代码

ClassVisitor类

ClassVisitor是一个抽象类,实现类有ClassWriter类(Core API)和ClassNode类(Tree API)。

public abstract class ClassVisitor {
protected final int api;
protected ClassVisitor cv;
}
复制代码
  • api字段:int类型的数据,指出了当前使用的ASM API版本。
  • cv字段:ClassVisitor类型的数据,它的作用是将多个ClassVisitor串连起来

在这里插入图片描述

classVisitor的方法

visit()、visitField()、visitMethod()和visitEnd()。

visitXxx()方法与ClassFile ClassVisitor的visitXxx()方法与ClassFile之间存在对应关系。在ClassVisitor中定义的visitXxx()方法,并不是凭空产生的,这些方法存在的目的就是为了生成一个合法的.class文件,而这个.class文件要符合ClassFile的结构,所以这些visitXxx()方法与ClassFile的结构密切相关。 1、visit()方法 用于生成类或者接口的定义,如下生成一个为printField的类,因为如果类默认继承的父类是Object类,所以superName是” java/lang/Object “。

cw.visit(52, ACC_PUBLIC + ACC_SUPER, "com/hank/test/PrintField", null, "java/lang/Object", null);
复制代码

2、visitField()方法 对应classFile中的field_info,用于生成对象里面的属性值。通过visitField生成一个属性,如下:

FieldVisitor fv;
{
fv = cw.visitField(ACC_PRIVATE + ACC_FINAL + ACC_STATIC, "a", "I", null, new Integer(2));
fv.visitEnd();
}
复制代码

3、visitMethod()方法 用于生成一个方法,对应classFile中的method_info

ClassWriter类

ClassWriter的父类是ClassVisitor,因此ClassWriter类继承了visit()、visitField()、visitMethod()和visitEnd()等方法。 toByteArray方法 在ClassWriter类当中,提供了一个toByteArray()方法。这个方法的作用是将对visitXxx()的调用转换成byte[],而这些byte[]的内容就遵循ClassFile结构。 在toByteArray()方法的代码当中,通过三个步骤来得到byte[]:

  • 第一步,计算size大小。这个size就是表示byte[]的最终的长度是多少。
  • 第二步,将数据填充到byte[]当中。
  • 第三步,将byte[]数据返回。

3、使用ClassWriter类 使用ClassWriter生成一个Class文件,可以大致分成三个步骤:

  • 第一步,创建ClassWriter对象。
  • 第二步,调用ClassWriter对象的visitXxx()方法。
  • 第三步,调用ClassWriter对象的toByteArray()方法。
import org.objectweb.asm.ClassWriter;

import static org.objectweb.asm.Opcodes.*;

public class GenerateCore {
public static byte[] dump () throws Exception {
// (1) 创建ClassWriter对象
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// (2) 调用visitXxx()方法
cw.visit();
cw.visitField();
cw.visitMethod();
cw.visitEnd(); // 注意,最后要调用visitEnd()方法

// (3) 调用toByteArray()方法
byte[] bytes = cw.toByteArray();
return bytes;
}
}
复制代码

Hanno源码分析

上面已经先回顾一下asm相关的基础知识,下面对hanno源码进行分析。主要针对三个方面:

1、如何在方法中插入Log语句。

2、如何获取对象中的field值。

3、如何获取到方法的参数


原文链接:https://juejin.cn/post/7037369790100406309?utm_source=gold_browser_extension

0 个评论

要回复文章请先登录注册