Android 中使用ASM,对Activity生命周期打点统计

介绍ASM

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

ASM库结构

Paste_Image.png
  • 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

网友画的时序图:

Paste_Image.png

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

  • ASM中提供一个ClassReader类,调用accept方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移。
  • 这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。

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

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

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。这里就不在写了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容