使用ASM对生命周期打点

##介绍ASM

ASM是一款基于java字节码层面的代码分析和修改工具。无需提供源代码即可对应用嵌入所需debug代码,用于应用API性能分析。ASM可以直接产生二进制class文件,也可以在类被加入JVM之前动态修改类行为。

##ASM库结构

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/5251070-3fa870a12487ace4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

* Core 为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换

* Tree提供了Java字节码在内存中的表现

* Analysis为存储在tree包结构中的java方法字节码提供基本的数据流统计和类型检查算法

* Commons提供一些常用的简化字节码生成转化和适配器

* Util包含一些帮助类和简单的字节码修改,有利于在开发或者测试中使用

* XML提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化。

### class文件结构

ASM 是基于java字节码层面的代码分析和修改工具。所以学习ASM之前,还得不下class文件结构,java类型,java方法等知识

Class文件结构如下:

| Header| 

| ---------  ------ | 

| Modifiers, name, super class, interfaces | 

| Constant pool: numeric, string and type constants  | 

| Source file name (optional)  |

| Enclosing class reference |

| Annotation* |

| Attribute* |

| member  |  attribute | 

| --------- | ------ | 

| Inner class*  | Name  | 

| Field*  | Modifiers, name, type  | 

|  |Annotation*  |

|  |Attribute*  |

| Method*  | Modifiers, name, return and parameter types    | 

|  |Annotation*  |

|  |Attribute*  |

| |Compiled code |

翻译成中文:

| Header| 

| ---------  ------ | 

| Modifiers, name, super class, interfaces  修饰(public/private等),名称,父类,实现的接口 | 

| Constant pool: numeric, string and type constants 常量池,数字,字符串,类型常量(枚举类型)  | 

| Source file name (optional)  原文件名称,(可选) |

| Enclosing class reference 外部类的引用 |

| Annotation*  Class的注解 |

| Attribute* Class属性 |

| member  |  attribute | 

| --------- | ------ | 

| Inner class* 内部类 | Name 名称 | 

| Field* 成员变量 | Modifiers, name, type 修饰符,名称,类型  | 

|  |Annotation*  注解 |

|  |Attribute*  属性 |

| Method* 方法 | Modifiers, name, return and parameter types Modifiers, name, return and parameter types 修饰符,名称,返回类型,参数类型 | 

|  |Annotation*  注解 |

|  |Attribute*  类型 |

|  |Compiled code 编译的代码 |

* 每个类、字段、方法和方法代码的属性有属于自己的名称记录在类文件格式的JVM规范的部分,这些属性展示了字节码多方面的信息,例如源文件名、内部类、签名、代码行数、本地变量表和注释。JVM规范允许定义自定义属性,这些属性会被标准的VM(虚拟机)忽略,但是可以包含附件信息。

* 方法代码表包含一系列对java虚拟机的指令。有些指令在代码中使用偏移量,当指令从方法代码被插入或者移除时,全部偏移量的值可能需要调整。

###原java类型与class文件内部类型对应关系

| Java type |Type descriptor |

| --------- | ------ | 

|boolean |Z |

|char |C |

|byte |B |

|short |S |

|int |I |

|float |F | 

|long |J |

|double |D | 

|Object |Ljava/lang/Object; |

|int[] |[I |

|Object[][] |[[Ljava/lang/Object; |

### 原java方法声明与class文件内部声明的对应关系

| Method declaration in source file | Method descriptor | 

| ------------ | ------------- | 

| void m(int i, float f) | (IF)V  |

| int m(Object o) |(Ljava/lang/Object;)I | 

|int[] m(int i, String s) |(ILjava/lang/String;)[I|

| Object m(int[] i)|([I]Ljava/lang/Object;|

参数描述在前面,返回值描述在后面

## ASM的处理流程,生产者消费者模式

在Core包中逻辑上分为2部分:

* 字节码生产者,例如ClassReader

* 字节码消费者,例如writers(ClassWriter, FieldWriter, MethodWriter和AnnotationWriter),adapters(ClassAdapter和MethodAdapter)

下图是生产者和消费者交互的时序图:

官网提供的时序图:

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/5251070-4ba0865b79cc47f7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

网友画的时序图:

![Paste_Image.png](http://upload-images.jianshu.io/upload_images/5251070-325e8dddd5882892.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

通过时序图可以看出ASM在处理class文件的整个过程。ASM通过树这种数据结构来表示复杂的字节码结构,并利用Push模型来对树进行遍历。

* ASM中提供一个ClassReader类,调用accept方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。

* 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。

##示例:拦截Android中 Activity生命周期方法,执行的时长。

首先定义一个ActivityTimeManager记录方法的使用时长,以onCreate方法为例。

``` java

public class ActivityTimeManger {

public static HashMap<String, Long> startTimeMap = new HashMap<>();

public static void onCreateStart(Activity activity) {

startTimeMap.put(activity.toString(), System.currentTimeMillis());

}

public static void onCreateEnd(Activity activity) {

Long startTime = startTimeMap.get(activity.toString());

if (startTime == null) {

return;

}

long coastTime = System.currentTimeMillis() - startTime;

System.out.println(activity.toString() + " onCreate coast Time" + coastTime);

startTimeMap.remove(activity.toString());

… …

}

```

在Activity编译的时候,在onCreate方法中,前后各插入ActivityTimeManger. onCreateStart() 和

ActivityTimeManger. onCreateEnd() 方法

原始的Activity,onCreate方法:

```

public class TestActivity extends Activity{

public void onCreate() {

System.out.println("onCreate");

}

}

```

使用javap –c 命令 查看class文件的字节码,如下:

```

public com.test.aop.main.TestActivity();

    Code:

      0: aload_0

      1: invokespecial #8                  // Method android/app/Activity."<init>":()V

      4: return

  public void onCreate();

    Code:

      0: getstatic    #15                // Field java/lang/System.out:Ljava/io/PrintStream;

      3: ldc          #21                // String onCreate

      5: invokevirtual #22                // Method java/io/PrintStream.println:(Ljava/lang/String;)V

      8: return

```

加上ActivityTimeManger. onCreateStart(),ActivityTimeManger. onCreateEnd()之后的源码如下:

```

public class TestActivity extends Activity{

public void onCreate() {

ActivityTimeManger.onCreateStart(this);

System.out.println("onCreate");

ActivityTimeManger.onCreateEnd(this);

}

```

使用javap –c 命令 查看class文件的字节码,如下:

```

public com.test.aop.main.TestActivity();

    Code:

      0: aload_0

      1: invokespecial #8                  // Method android/app/Activity."<init>":()V

      4: return

  public void onCreate();

    Code:

      0: aload_0

      1: invokestatic  #15                // Method com/test/aop/tools/ActivityTimeManger.onCreateStart:(Landroid/app/Activity;)V

      4: getstatic    #21                // Field java/lang/System.out:Ljava/io/PrintStream;

      7: ldc          #27                // String onCreate

      9: invokevirtual #28                // Method java/io/PrintStream.println:(Ljava/lang/String;)V

      12: aload_0

      13: invokestatic  #34                // Method com/test/aop/tools/ActivityTimeManger.onCreateEnd:(Landroid/app/Activity;)V

      16: return

```

红色部分是增加ActivityTimeManger. onCreateStart(),ActivityTimeManger. onCreateEnd()2个方法后,增加的字节码。

所以我们怎么使用ASM,对class文件进行修改。把红色部分的字节码插入到class文件中呢?

先输入文件。把class文件重命名为.opt文件,修改完后,再重命名回去。

```

public static void processClass(File file) {

System.out.println("start process class " + file.getPath());

File optClass = new File(file.getParent(), file.getName() + ".opt");

FileInputStream inputStream = null;

FileOutputStream outputStream = null;

try {

inputStream = new FileInputStream(file);

outputStream = new FileOutputStream(optClass);

byte[] bytes = referHack(inputStream);

outputStream.write(bytes);

} catch (Exception e) {

e.printStackTrace();

} finally {

if (inputStream != null) {

try {

inputStream.close();

} catch (IOException e) {

e.printStackTrace();

}

}

if (outputStream != null) {

try {

outputStream.close();

} catch (IOException e) {

e.printStackTrace();

}

}

}

if (file.exists()) {

file.delete();

}

optClass.renameTo(file);

}

```

referHack 方法

```

private static byte[] referHack(InputStream inputStream) {

try {

ClassReader classReader = new ClassReader(inputStream);

ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);

ClassVisitor changeVisitor = new ChangeVisitor(classWriter);

classReader.accept(changeVisitor, ClassReader.EXPAND_FRAMES);

return classWriter.toByteArray();

} catch (IOException e) {

e.printStackTrace();

} finally {

}

return null;

}

```

创建ClassReader,生产者,读出class字节码,输出给ClassWriter消费。

自定义ChangeVisitor 来处理class字节码。

```

public static class ChangeVisitor extends ClassVisitor {

  // 记录文件名

private String owner;

private ActivityAnnotationVisitor fileAnnotationVisitor = null;

public ChangeVisitor(ClassVisitor cv) {

super(Opcodes.ASM5, cv);

}

@Override

public void visit(int version, int access, String name, String signature, String superName,

String[] interfaces) {

super.visit(version, access, name, signature, superName, interfaces);

this.owner = name;

}

@Override

// 处理class文件的注解

public AnnotationVisitor visitAnnotation(String desc, boolean visible) {

System.out.println("visitAnnotation: desc=" + desc + " visible=" + visible);

AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);

if (desc != null) {

// 如果注解不是空,传递给ActivityAnnotationVisitor处理。

fileAnnotationVisitor = new ActivityAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);

return FileAnnotationVisitor;

}

return annotationVisitor;

}

@Override

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

// 获取到原始的MethodVisitor

MethodVisitor mv = this.cv.visitMethod(access, name, desc, signature, exceptions);

// 如果文件的注解不为空,说明文件要进行修改。则创建RedefineAdvice,修改方法

if (fileAnnotationVisitor!= null) {

return new RedefineAdvice(mv, access, owner, name, desc);

}

return mv;

}

}

```

ChangeVisitor,继承ClassVisitor,class文件的访问,可以重写

visitAnnotation(), 获取或者修改注解

visitMethod(),获取或者修改方法

visitField(),获取或者修改成员变量

这段代码的逻辑是:先判断这个class文件是否有注解。如果有注解,则先解析注解。如果注解不为空,则说明有方法需要修改则创建RedefineAdvice,访问和修改方法。

看下ActivityAnnotationVisitor,对注解的访问和解析。

```

public static class ActivityAnnotationVisitor extends AnnotationVisitor {

public String desc;

public String name;

public String value;

public ActivityAnnotationVisitor(int api, AnnotationVisitor av, String paramDesc) {

super(api, av);

this.desc = paramDesc;

}

public void visit(String paramName, Object paramValue) {

this.name = paramName;

this.value = paramValue.toString();

System.out.println("visitAnnotation: name=" + name + " value=" + value);

}

}

```

记录注解的名称和值,描述。

RedefineAdvice,对方法的修改

```

public static class RedefineAdvice extends AdviceAdapter {

String owner = "";

ActivityAnnotationVisitor activityAnnotationVisitor = null;

protected RedefineAdvice(MethodVisitor mv, int access, String className, String name, String desc) {

super(Opcodes.ASM5, mv, access, name, desc);

owner = className;

}

@Override

public AnnotationVisitor visitAnnotation(String desc, boolean visible) {

System.out.println("visitAnnotation: desc=" + desc + " visible=" + visible);

AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);

// 先判断方法上是否有注解,如果有注解,则使用ActivityAnnotationVisitor解析注解

if (desc != null) {

activityAnnotationVisitor = new ActivityAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);

return activityAnnotationVisitor;

}

return annotationVisitor;

}

@Override

// 修改方法入口,在方法执行前,插入字节码

protected void onMethodEnter() {

if (activityAnnotationVisitor == null) {

return;

}

super.onMethodEnter();

//插入字节码,ALOAD

mv.visitVarInsn(ALOAD, 0);

//插入字节码INVOKESTATIC,调用ActivityTimeManger.onCreateStart().

// onCreate使用注解写入

mv.visitMethodInsn(INVOKESTATIC, "com/test/aop/tools/ActivityTimeManger",

activityAnnotationVisitor.value+"Start",

"(Landroid/app/Activity;)V");

}

//在方法执行结束前,插入字节码

@Override

protected void onMethodExit(int opcode) {

if (activityAnnotationVisitor == null) {

return;

}

super.onMethodExit(opcode);

//插入字节码,ALOAD

mv.visitVarInsn(ALOAD, 0);

//插入字节码INVOKESTATIC,调用ActivityTimeManger.onCreateEnd().

// onCreate使用注解写入

mv.visitMethodInsn(INVOKESTATIC, "com/test/aop/tools/ActivityTimeManger", 

activityAnnotationVisitor.value+"End",

"(Landroid/app/Activity;)V");

}

}

```

整段代码逻辑是:

先查找方法上的注解,如果方法上有注解,则获取注解的value。在方法执行前后,插入字节码。通过重写onMethodEnter和onMethodExit方法。

所以在原来的TestActivity上,增加注解class注解和方法注解,然后通过processClass()处理,就能在记录Activity方法执行的时间。

```

@FileAnnotation("TestActivity")

public class TestActivity extends Activity{

@ActivityAnnotation("onCreate")

public void onCreate() {

System.out.println("onCreate");

}

```

编译后的class文件,反编译后,结果如下:

```

@FileAnnotation

public class TestActivity extends Activity {

    @ActivityAnnotation

    public void onCreate() {

        ActivityTimeManger.onCreateStart(this);

        System.out.println("onCreate");

        ActivityTimeManger.onCreateEnd(this);

    }

}

```

备注:Android App目前大部分都是通过gradle编译。所以以上字节码处理代码,都需要写在自定义的gradle插件中,自定义一个Transform处理。

怎么自定义gradle插件和Transform ,可以百度,或者google。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值