美文网首页Aop
Android 中使用ASM,对Activity生命周期打点统计

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

作者: 编程小猪 | 来源:发表于2017-03-23 20:35 被阅读0次

    介绍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。这里就不在写了。
    以上所有代码链接

    相关文章

      网友评论

        本文标题:Android 中使用ASM,对Activity生命周期打点统计

        本文链接:https://www.haomeiwen.com/subject/pxwtottx.html