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