Android插桩探索

目录

1.插桩是什么
2.插桩的作用
3.插桩的原理
4.插桩方案对比
5.AspectJ 耗时统计实践
6.ASM 耗时统计实践
7.插桩编译 Gradle Transform
8.ASM的更多用法
9.MethodTraceMan

插桩是什么

插桩就是在代码编译期间修改已有的代码或者生成新代码
Android从编译到加载的过程

插桩的作用

插桩可以做什么?

  • 减少代码的重复编写
  • 无痕埋点
  • 对全局所有class插桩,做UI,内存,网络等等方面的性能监控
  • 修改引入的第三方jar包的class代码

插桩的原理

Java 源文件插桩


AndroidAnnotation、APT(Annotation Processing Tool),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。它们生成的都是 Java 文件,是在编译的最开始介入。

如Greendao、ButterKnife,如下图的项目使用了Greendao开源库,可见 build 目录下有很多 *.java 后缀的文件,build一般都是放置编译生成后的产物,这些文件就是在我们 build 时候通过注解处理器产生的 Java 文件。


字节码插桩

Java字节码
对于 Java 平台,Java 虚拟机运行的是 Class 文件,内部对应的是 Java 字节码

Dalvik 字节码
对于Android 平台,为了优化性能,Android 虚拟机运行的是 Dex 文件。dex 我们可以理解为 Android 为移动设备(受限于早年的手机配置远低于 PC) 研发的 class 的压缩格式。Android SDK 工具包里面有 dx 工具可以将 class 文件打包成 dex,又由 Android 虚拟机的 PathClassLoader 装载到内存中



字节码插桩可以通过修改“.class”的 Java 字节码实现,也可以通过修改“.dex”的 Dalvik 字节码实现,这取决于我们使用的插桩方法。相对于 Java 文件方式,字节码操作方式功能更加强大,应用场景也更广,但是它的使用复杂度更高

插桩方案对比

AspectJ

一个老牌的插桩框架

优点

  • 成熟稳定
  • 使用简单,易上手

缺点

  • 基于规则,所以其切入点相对固定,对于字节码文件的操作自由度以及开发的掌控度大打折扣
  • AspectJ会额外生成一些包装代码,对性能以及包大小有一定影响

ASM

字节码操作框架,可用来动态生成字节码或者对现有的类进行增强

优点

  • 对 Class 文件更直接的修改,操作灵活
  • 功能强大,应用场景广

缺点

  • 需要掌握字节码知识,难上手

Javassist

优点

  • Javassist 源代码级 API 比 ASM 中实际的字节码操作更容易使用
  • 只需要很少的字节码知识,甚至不需要任何实际字节码知识

缺点

  • 使用反射机制,运行速度要比使用 Classworking 技术的ASM慢得多



AspectJ实践

Gradle Version:6.7.1
Android Gradle Plugin Version:4.2.2

项目根目录build.gradle配置

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

plugins {
    id 'com.android.application' version '4.2.2' apply false
    id 'com.android.library' version '4.2.2' apply false
}

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
        maven { url "https://plugins.gradle.org/m2/" }
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

模块的build.gradle中配置

dependencies {
    implementation 'org.aspectj:aspectjrt:1.8.13'
}

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

final def log = project.logger
// 如果是library, 下面一行替换为final def variants = project.android.libraryVariants
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }


创建一个Activity,添加两个方法分别为method()methodAnnotate()
method() 使用匹配类名和方法名来实现,完成在方法前增加打印一行信息
methodAnnotate() 使用注解方式来实现,完成在方法后增加打印一行信息

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        method();
        methodAnnotate();
    }

    /**
     * 以匹配类名和方法名进行aspectJ处理
     */
    public void method(){
        Log.d(TAG, "method()");
    }

    /**
     * 以注解方式进行aspectJ处理
     */
    @AspectAnnotate
    public void methodAnnotate(){
        Log.d(TAG, "methodAnnotate()");
    }
}

创建自定义注解类AspectAnnotate

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AspectAnnotate {

}

创建切面处理类AspectUtil

  • @Aspect 声明配置文件
  • @Pointcut 声明要切入哪个方法
  • @Before/@After 具体插入的代码,插入在方法前/后
@Aspect
public class AspectUtil {
    private static final String TAG = AspectUtil.class.getSimpleName();

    /**
     * 以匹配方法名的方式寻找切点
     */
    @Pointcut("execution(* com.ljh.aspectj.MainActivity.method(..))")
    public void pointCut(){

    }

    @Before("pointCut()")
    public void methodBefore(){
        Log.d(TAG,"methodBefore");
    }

//====================================================================

    /**
     * 以注解方式的方式寻找切点
     */
    @Pointcut("execution(@com.ljh.aspectj.AspectAnnotate * *(..))")
    public void pointCut2(){

    }

    @After("pointCut2()")
    public void methodAfter(){
        Log.d(TAG,"methodAfter");
    }
}

切点表达式

切入点有两种,分别是executioncall

  • execution 代码插入在指定方法的内部
  • call 代码插入在指定方法被调用的位置上

第一个*号表示返回值可为任意类型,后跟包名+类名+方法名,括号内表示参数列表,… 表示匹配任意个参数,参数类型为任意类型。
表达式中还可以使用一些条件判断符,比如 !、&&、||。更详细的语法介绍可以查看官网

@Pointcut("execution(* com.ljh.aspectj.MainActivity.method(..))")

代码切入位置

  • @Before 切入点前织入
  • @After 切入点后织入,无论连接点执行如何,包括正常的 return 和 throw 异常
  • @AfterReturning 只有在切入点正常返回之后才会执行,不指定返回类型时匹配所有类型
  • @AfterThrowing 只有在切入点抛出异常后才执行,不指定异常类型时匹配所有类型
  • @Around 替代原有切点,如果要执行原来代码的话,调用 ProceedingJoinPoint.proceed()
切入约束
  • 方法必须为public
  • Before、After、AfterReturning、AfterThrowing 四种类型方法返回值必须为void
  • Around的目标是替代原切入点,它一般会有返回值,这就要求声明的返回值类型必须与切入点方法的返回值保持一致;不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效

运行结果

运行app,查看log打印,可以看到实现了 method() 方法前增加了一行log,methodAnnotate() 方法后增加了一行log

2023-12-01 00:00:50.019 3032-3032/com.ljh.aspectj D/AspectUtil: methodBefore
2023-12-01 00:00:50.019 3032-3032/com.ljh.aspectj D/MainActivity: method()
2023-12-01 00:00:50.019 3032-3032/com.ljh.aspectj D/MainActivity: methodAnnotate()
2023-12-01 00:00:50.019 3032-3032/com.ljh.aspectj D/AspectUtil: methodAfter

我们来看一下编译后的.class文件
execution方式

可以看到在method()methodAnnotate()方法内部开头/末尾插入了我们增加的代码


上面有提到切入点有两种,分别是executioncall,可以看一下call方式的.class

@Pointcut("call(* com.ljh.aspectj.MainActivity.method(..))")
@Pointcut("call(@com.ljh.aspectj.AspectAnnotate * *(..))")

可以看到在目标方法的调用前后,插入了我们新增的代码
call方式


ASM实战

Gradle Version:6.7.1
Android Gradle Plugin Version:4.2.2

ASM在项目中需要配合Gradle Transform一起使用,下图是ASM插入的流程
网图

ASM的角色

ASM有三个重要的角色

  • ClassReader class字节码的读取与分析引擎
  • ClassVisitor ASM 插桩的核心,字节码的插桩修改就是在这一个步骤进行
  • ClassWirter ASM 提供的对字节码修改完以后,将修改完的内容进行写入的工具

通过ClassReader读取字节码

String clazzFilePath = "/G:/AndroidProject/ASM/app/build/intermediates/javac/debug/classes/com/ljh/asm/MainActivity.class";
FileInputStream fileInputStream = new FileInputStream(clazzFilePath);
//ClassReader:class字节码的读取与分析引擎
ClassReader classReader = new ClassReader(fileInputStream);

通过ClassVisitor实现插入字节码

//字节码的插桩的核心
classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);

通过ClassWriter覆盖原来的.class文件

//ClassWriter:写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小
 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//获取执行了插桩后的字节码
byte[] bytes = classWriter.toByteArray();
FileOutputStream fileOutputStream = new FileOutputStream(clazzFilePath);
fileOutputStream.write(bytes);

项目实现

项目根目录gradle配置

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
        maven { url "https://plugins.gradle.org/m2/" }
        google()
        jcenter()
    }
}

模块gradle配置

dependencies {
    implementation 'org.ow2.asm:asm:9.3'
    implementation 'org.ow2.asm:asm-commons:9.3'
}

假设有一个Activity,里面有methodA()methodB(),我们需要计算出每个方法的耗时,我们可以像下面这样写,但是如果要计算APP内所有方法的耗时,我们不可能每个方法都去添加计算方法耗时的代码,这时我们可以使用ASM插桩来实现

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        methodA();
        methodB();
    }

    public void methodA(){
        long start = System.currentTimeMillis();
        SystemClock.sleep(1000);
        long end = System.currentTimeMillis();
        Log.d(TAG , "方法耗时: " + (end - start));
    }

    public int methodB(){
        long start = System.currentTimeMillis();
        systemClock.sleep(2000);
        long end = System.currentTimeMillis();
        Log.d(TAG  ,"方法耗时: " + (end - start));
        return 2;
    }
}

在test目录下创建一个测试类ASMTimeMethodVisitor

import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class ASMTimeMethodVisitor {

    @Test
    public void visit() {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            String clazzFilePath = "/G:/AndroidProject/ASM/app/build/intermediates/javac/debug/classes/com/ljh/asm/MainActivity.class";
            System.out.println("classFilePath = " + clazzFilePath);
            //读取待插桩的class
            fileInputStream = new FileInputStream(clazzFilePath);

            //执行分析与插桩,
            //ClassReader:class字节码的读取与分析引擎
            ClassReader classReader = new ClassReader(fileInputStream);

            //ClassWriter:写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

            /**
             * 执行分析,处理结果写入classWriter, EXPAND_FRAMES表示栈图以扩展格式进行访问
             * 执行插桩的代码在MyClassVisitor中实现
             */
            classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);

            //获取执行了插桩后的字节码
            byte[] bytes = classWriter.toByteArray();

            fileOutputStream = new FileOutputStream(clazzFilePath);
            fileOutputStream.write(bytes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public class MyClassVisitor extends ClassVisitor {

        protected MyClassVisitor(int api) {
            super(api);
        }

        protected MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        @Override  
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
        }
    }

    public class MyMethodVisitor extends AdviceAdapter {
        //todo: 具体代码下面详细说明
    }
}

可以看到上面visitMethod()方法的返回值是MethodVisitor,而我们return的是继承了AdviceAdapterMyMethodVisitor类

AdviceAdapter的继承关系如下,可以看到最终还是继承MethodVisitor

  • AdviceAdapter extends GeneratorAdapter
  • GeneratorAdapter extends LocalVariablesSorter
  • LocalVariablesSorter extends MethodVisitor

为什么是继承AdviceAdapter而不是MethodVisitor呢,因为AdviceAdapter封装了指令插入方法,更为直观与简单


AdviceAdapter封装了两个方法,onMethodEnter()和onMethodExit(),如果要计算方法耗时的话,我们会这样写

    public class MyMethodVisitor extends AdviceAdapter {
        private long start;

        //方法开始插入代码
        @Override
        protected void onMethodEnter() {
            start = System.currentTimeMillis();
            super.onMethodEnter();
        }
        
        //方法末尾插入代码
        @Override
        protected void onMethodExit(int opcode) {
            long end = System.currentTimeMillis();
            Log.d(TAG, "方法耗时: " + (end - start));
            super.onMethodExit(opcode);
        }
    }

但是我们需要将代码转换为字节码插入,如果你对字节码指令很熟悉,那么可以直接撸

private int startIdentifier;
protected void onMethodEnter() {
    //start = System.currentTimeMillis();
    invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
    startIdentifier = newLocal(Type.LONG_TYPE);
    storeLocal(startIdentifier);
}

protected void onMethodExit(int opcode) {
    //long end = System.currentTimeMillis();
    //Log.d(TAG, "方法耗时: " + (end - start));
    //todo: ...
}

但是如果每一行指令都这样手写,对我们的字节码知识的要求以及对 ASM 的 API 的掌握程度有很高的要求,并且容易出现错漏。这时我们可以使用ASM Bytecode Viewer工具来生成字节码指令

ASM Bytecode Viewer

插件安装

  1. File → Settings → Plugins → Marketplace
  2. 安装ASM Bytecode Viewer Support Kotlin,实测安装前面两个无法使用或出现打不开AndroidStudio的问题
  3. 安装完成,重启AS

插件使用
右键选择ASM Bytecode Viewer,等待插件生成字节码

在这里插入图片描述


选择ASMified,可以看到 methodA() 对应的字节码文件的片段以及 ASM API

在这里插入图片描述


下面我们可以将ASM Bytecode Viewer生成的代码片段copy到methodEnter()onMethodExit()

完整代码

package com.ljh.asm;

import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.AdviceAdapter;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class ASMTimeMethodVisitor {

    @Test
    public void visit() {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            String clazzFilePath = "/G:/AndroidProject/ASM/app/build/intermediates/javac/debug/classes/com/ljh/asm/MainActivity.class";
            System.out.println("classFilePath = " + clazzFilePath);
            //读取待插桩的class
            fileInputStream = new FileInputStream(clazzFilePath);

            //执行分析与插桩,
            //ClassReader:class字节码的读取与分析引擎
            ClassReader classReader = new ClassReader(fileInputStream);

            //ClassWriter:写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

            /**
             * 执行分析,处理结果写入classWriter, EXPAND_FRAMES表示栈图以扩展格式进行访问
             * 执行插桩的代码在MyClassVisitor中实现
             */
            classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);

            //获取执行了插桩后的字节码
            byte[] bytes = classWriter.toByteArray();

            fileOutputStream = new FileOutputStream(clazzFilePath);
            fileOutputStream.write(bytes);
//            fileOutputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public class MyClassVisitor extends ClassVisitor {

        protected MyClassVisitor(int api) {
            super(api);
        }

        protected MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
        }
    }

    /**
     * AdviceAdapter封装了指令插入方法,更为直观与简单
     * 继承关系如下:
     * AdviceAdapter extends GeneratorAdapter
     * GeneratorAdapter extends LocalVariablesSorter
     * LocalVariablesSorter extends MethodVisitor
     */
    public class MyMethodVisitor extends AdviceAdapter {
        private Label start;
        private int startIdentifier;
        private MethodVisitor mv;
        private boolean isInject;

        protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
            mv = methodVisitor;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println("visitAnnotation() methodName = " + getName() + " ; descriptor = " + descriptor);
            return super.visitAnnotation(descriptor, visible);
        }

        /**
         * 方法开始插入代码
         */
        @Override
        protected void onMethodEnter() {
            //long start = System.currentTimeMillis();
            start = new Label();
            mv.visitLabel(start);
            mv.visitLineNumber(19, start);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 1);
            super.onMethodEnter();
        }

        /**
         * 方法末尾插入代码
         */
        @Override
        protected void onMethodExit(int opcode) {
            //long end = System.currentTimeMillis();
            //Log.d("liangjiehao","方法耗时: " + (end - start));
            Label end = new Label();
            mv.visitLabel(end);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 3);

            Label label3 = new Label();
            mv.visitLabel(label3);
            mv.visitFieldInsn(GETSTATIC, "com/ljh/asm/MainActivity", "TAG", "Ljava/lang/String;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("\u65b9\u6cd5\u8017\u65f6: ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, 3);
            mv.visitVarInsn(LLOAD, 1);
            mv.visitInsn(LSUB);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);
            mv.visitEnd();
            super.onMethodExit(opcode);
        }
    }
}

执行插桩

  1. 先到Activity中,把打印方法耗时的代码先注释掉
  2. Make project
  3. 查看编译后的.class文件,可以看到插桩前methodA()和methodB() 没有计算耗时的方法
    在这里插入图片描述
  4. 运行测试程序
    在这里插入图片描述
  5. 重新打开编译后的.class文件,可以看到所有方法都被插入了计算耗时的代码
    在这里插入图片描述

增加注解

上面所有方法都被插桩了,但是我们的需求是只打印methodA()methodB() 的方法耗时,我们可以通过注解来过滤

自定义注解类ASMTimeAnnotation

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ASMTimeAnnotation {
}

methodA()methodB() 添加注解
在这里插入图片描述

增加注解过滤

  public class MyMethodVisitor extends AdviceAdapter {
        private boolean isInject;
        
        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println("visitAnnotation() methodName = " + getName() + " ; descriptor = " + descriptor);
            //如果方法的注解名字是@ASMTimeAnnotation,则给此方法注入代码
            isInject = ("Lcom/ljh/asm/ASMTimeAnnotation;".equals(descriptor)) ? true : false;
            return super.visitAnnotation(descriptor, visible);
        }
        
        @Override
        protected void onMethodEnter() {
            if(!isInject){
                return;
            }
            
        @Override
        protected void onMethodExit(int opcode) {
            if(!isInject){
                return;
            }
        }

再次运行测试程序,查看.class文件,只有添加了注解的方法才被插桩
在这里插入图片描述

运行APP

在看到目标方法被成功插桩后,满心欢喜地运行APP,发现并没有打印出我们插桩的log ????

再次查看编译后的.class文件,发现刚刚成功插桩的代码都不见了
在这里插入图片描述

哦,原来是运行APP时又重新编译了Activity,把之前通过测试程序生成的.class覆盖了,.class又被还原成最初的样子。
那该怎么让我们插桩的代码最后能生效呢? 这时需要用到Gradle Transform


Gradle Plugin & Transform

Google官方在Android GradleV1.5.0版本以后提供了Transform API,它允许第三方插件在编译后的类文件转换为dex文件之前做处理操作,我们需要做的就是实现Transform来对.class文件便遍历来拿到所有方法,插桩完成后再对原来的.class文件进行替换

在这里插入图片描述

项目配置

创建一个module,java或Libray都可以

module的build.gradle配置

plugins {
    id 'groovy'
    id 'maven'
    id 'java'
}

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    implementation 'org.ow2.asm:asm:9.3'
    implementation 'org.ow2.asm:asm-commons:9.3'

    implementation localGroovy()
    implementation gradleApi()
    implementation 'com.android.tools.build:gradle:4.2.2', {
        exclude group:'org.ow2.asm'
    }
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

uploadArchives {
    repositories {
        mavenDeployer {
            //deploy到maven仓库
            // 调用方式就是 'com.ljh.asm.plugin:timeplugin:1.0.0'

            // 设置 groupId
            pom.groupId = 'com.ljh.asm.plugin'

            // 设置 插件版本号
            pom.version = '1.0.0'

            // 设置 artifactId
            pom.artifactId = 'timeplugin'

            // 本地仓库路径
            repository(url: uri('../repo'))
        }
    }
}


编写一个简单的插件

main 文件下(与 Java 同级) java 文件夹后续没有用到可以删除,创建 groovy 和 resource 资源目录
在这里插入图片描述

新建asm-time-plugin.properties

implementation-class=com.ljh.asm.plugin.TimePlugin

新建TimePlugin.groovy(注意不要新建java文件,否则识别不到groovy语法)
在这里插入图片描述

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin;
import org.gradle.api.Project;

class TimePlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.task("TimePlugin") {
            doLast {
                println("调用了TimePlugin")
            }
        }
    }
}

发布插件

双击我们在gradle定义的uploadArchives Task
在这里插入图片描述

可以看到在本地的maven仓库生成了对应的插件
在这里插入图片描述

插件应用

在项目的根目录的build.gradle添加我们的插件,sync
在这里插入图片描述

很有可能会出现找不到插件的错误,要仔细检查上面的路径有没有写错,例如asm写成ams(就是我)
在这里插入图片描述

可以照着这个路径一级一级往下找
在这里插入图片描述


app模块的gradle配置

id要与插件里定义的名字一样
在这里插入图片描述
在这里插入图片描述


运行插件

sync后查看插件中定义的TimePlugin Task
在这里插入图片描述
在这里插入图片描述

双击运行task,成功打印出我们在task添加的log
在这里插入图片描述


完善插件

  1. 将刚刚单元测试的ASMTimeMethodVisitor类拷贝到插件模块中,我这里为了更好看就重命名为TimeMethodVisitor
    在这里插入图片描述

稍微修改一下,将单元测试的方法去掉

package com.ljh.asm.plugin;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.AdviceAdapter;


public class TimeMethodVisitor extends ClassVisitor {

    protected TimeMethodVisitor(int api) {
        super(api);
    }

    protected TimeMethodVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
    }

    /**
     * AdviceAdapter封装了指令插入方法,更为直观与简单
     * 继承关系如下:
     * AdviceAdapter extends GeneratorAdapter
     * GeneratorAdapter extends LocalVariablesSorter
     * LocalVariablesSorter extends MethodVisitor
     */
    public class MyMethodVisitor extends AdviceAdapter {
        private Label start;
        private MethodVisitor mv;
        private boolean isInject;

        protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor);
            mv = methodVisitor;
        }

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println("visitAnnotation() methodName = " + getName() + " ; descriptor = " + descriptor);
            //如果方法的注解名字是@com.ljh.asm.ASMTimeAnnotation,则给此方法注入代码
            isInject = ("Lcom/ljh/asm/ASMTimeAnnotation;".equals(descriptor)) ? true : false;
            return super.visitAnnotation(descriptor, visible);
        }

        /**
         * 方法开始插入代码
         */
        @Override
        protected void onMethodEnter() {
            if (!isInject) {
                return;
            }

            //long start = System.currentTimeMillis();
            start = new Label();
            mv.visitLabel(start);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 1);
            super.onMethodEnter();
        }

        /**
         * 方法末尾插入代码
         */
        @Override
        protected void onMethodExit(int opcode) {
            if (!isInject) {
                return;
            }

            //long end = System.currentTimeMillis();
            Label end = new Label();
            mv.visitLabel(end);
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitVarInsn(LSTORE, 3);

            Label label3 = new Label();
            mv.visitLabel(label3);
            mv.visitFieldInsn(GETSTATIC, "com/ljh/asm/MainActivity", "TAG", "Ljava/lang/String;");
            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
            mv.visitLdcInsn("\u65b9\u6cd5\u8017\u65f6: ");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
            mv.visitVarInsn(LLOAD, 3);
            mv.visitVarInsn(LLOAD, 1);
            mv.visitInsn(LSUB);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
            mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(POP);
            mv.visitEnd();
            super.onMethodExit(opcode);
        }
    }
}

  1. 新建TimeTransform.groovy,代码套用transform模板,只需要将其的ClassVisitor换成我们的TimeMethodVisitor
package com.ljh.asm.plugin;

import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.Transform;
import com.android.build.api.transform.TransformException;
import com.android.build.api.transform.TransformInput;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.api.transform.TransformOutputProvider;
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes


class TimeTransform extends Transform {


    @Override
    String getName() {
        return "TimeTransform";
    }

    /**
     * 需要处理的数据类型,有两种枚举类型
     * CLASS->处理的java的class文件
     * RESOURCES->处理java的资源
     *
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
     * 1. EXTERNAL_LIBRARIES        只有外部库
     * 2. PROJECT                   只有项目内容
     * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或远程依赖项
     * 5. SUB_PROJECTS              只有子项目。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
     *
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    /**
     * 是否增量编译
     *
     * @return
     */
    @Override
    boolean isIncremental() {
        return false;
    }

    /**
     * @param transformInvocation.getInputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
     * @param transformInvocation.outputProvider 输出路径
     */
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        if (!incremental) {
            //不是增量更新删除所有的outputProvider
            transformInvocation.outputProvider.deleteAll()
        }

        transformInvocation.inputs.each { TransformInput input ->
            //遍历目录
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectoryInput(directoryInput, transformInvocation.outputProvider)
            }
            // 遍历jar 第三方引入的 class
            //不能省略,否则运行应用可能会崩溃
            input.jarInputs.each { JarInput jarInput ->
                handleJarInput(jarInput, transformInvocation.outputProvider)
            }
        }
    }

    /**
     * 处理文件目录下的class文件
     */
    static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        //是否是目录
        if (directoryInput.file.isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            directoryInput.file.eachFileRecurse { File file ->
                def name = file.name
                if (filterClass(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new TimeMethodVisitor(Opcodes.ASM9, classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }
        //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
        // 获取output目录
        def dest = outputProvider.getContentLocation(
                directoryInput.name,
                directoryInput.contentTypes,
                directoryInput.scopes,
                Format.DIRECTORY)
        //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
        //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

    static void handleJarInput(JarInput jarInput, TransformOutputProvider outputProvider){
        //是否是目录
        if (jarInput.file.isDirectory()) {
            //列出目录所有文件(包含子文件夹,子文件夹内文件)
            jarInput.file.eachFileRecurse { File file ->
                def name = file.name
                if (filterClass(name)) {
                    ClassReader classReader = new ClassReader(file.bytes)
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    ClassVisitor classVisitor = new TimeMethodVisitor(Opcodes.ASM9, classWriter)
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                    byte[] code = classWriter.toByteArray()
                    FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)
                    fos.write(code)
                    fos.close()
                }
            }
        }

        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR);
        //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
        //将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
        FileUtils.copyFile(jarInput.getFile(), dest);
    }


    /**
     * 检查class文件是否需要处理
     * @param fileName
     * @return
     */
    static boolean filterClass(String name) {
        return (name.endsWith(".class")
                && !name.startsWith("R\$")
                && "R.class" != name
                && "BuildConfig.class" != name)
    }
}


  1. 修改TimePlugin
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin;
import org.gradle.api.Project;

class TimePlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        def android = project.extensions.findByType(AppExtension)
        android.registerTransform(new TimeTransform())
    }
}
  1. 更新插件

    插件模块sync后,执行uploadArchives上传插件
    app模块sync后,运行APP

    查看.class文件,已经插桩成功
    在这里插入图片描述

    查看日志,也有了方法耗时打印。但是发现只打印耗时,不知道是哪个方法
    在这里插入图片描述

    其实在前面介绍AdviceAdapter时,里面有提供方法名的接口。

    public class MyMethodVisitor extends AdviceAdapter {
    
        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println("visitAnnotation() methodName = " + getName() + " ; descriptor = " + descriptor);
            //如果方法的注解名字是@com.ljh.asm.ASMTimeAnnotation,则给此方法注入代码
            isInject = ("Lcom/ljh/asm/ASMTimeAnnotation;".equals(descriptor)) ? true : false;
            return super.visitAnnotation(descriptor, visible);
        }
    

    但是方法名不是我们的重点,因为后面我会介绍一个统计方法耗时的插件MethodTraceMan


ASM的更多用法

修改已存在的类(增加字段、增加方法、删除方法、修改方法等)

import android.os.Bundle;
import android.os.SystemClock;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        methodA();
        methodB();
    }

    //1.删除methodA()方法
    public void methodA(){
        SystemClock.sleep(1000);
    }

    //2.将methodB() private改成public
    private int methodB(){
        SystemClock.sleep(1000);
        return 2;
    }

//    //3.增加一个字段
//    private String name;
//
//    //4.增加getName()方法
//    public String getName() {
//        return name;
//    }
}

编写测试类

public class ASMMethodModifyVisitor {
    private ClassWriter mClassWriter;
    private ClassReader mClassReader;

    @Test
    public void visit() {
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        try {
            String clazzFilePath = "/G:/AndroidProject/ASM/app/build/intermediates/javac/debug/classes/com/ljh/asm/MainActivity.class";
            System.out.println("classFilePath = " + clazzFilePath);
            
            fileInputStream = new FileInputStream(clazzFilePath);
            mClassReader = new ClassReader(fileInputStream);
            mClassWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
            mClassReader.accept(new MyClassVisitor(Opcodes.ASM9, mClassWriter), ClassReader.EXPAND_FRAMES);
            
            byte[] bytes = mClassWriter.toByteArray();

            fileOutputStream = new FileOutputStream(clazzFilePath);
            fileOutputStream.write(bytes);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    public class MyClassVisitor extends ClassVisitor{
        private ClassVisitor cv;

        protected MyClassVisitor(int api) {
            super(api);
        }

        protected MyClassVisitor(int api, ClassVisitor classVisitor) {
            super(api, classVisitor);
            cv = classVisitor;
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            System.out.println("visitMethod() methodName = " + name + " ; descriptor = " + descriptor);

            /**
             * 删除methodA()方法
             */
            if ("methodA".equals(name)){
                return null;
            }

            /**
             * 修改methodB()方法的private为public
             */
            if("methodB".equals(name)){
                access = Opcodes.ACC_PUBLIC;
            }

            return super.visitMethod(access, name, descriptor, signature, exceptions);
        }

        @Override
        public void visitEnd() {
            /**
             * 如果要为类增加属性和方法,放到visitEnd中,避免破坏之前已经排列好的类结构
             */

            /**
             * 增加一个字段
             */
            FieldVisitor fieldVisitor = cv.visitField(Opcodes.ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
            fieldVisitor.visitEnd();

            /**
             * 增加一个方法
             */
            MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC, "getName", "()Ljava/lang/String;", null, null);
            mv.visitCode();
            Label label0 = new Label();
            mv.visitLabel(label0);
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitFieldInsn(Opcodes.GETFIELD, "com/ljh/asm/MainActivity", "name", "Ljava/lang/String;");
            mv.visitInsn(Opcodes.ARETURN);
            mv.visitEnd();
            super.visitEnd();
        }
    }
}

执行测试类,查看编译后的.class文件
在这里插入图片描述


MethodTraceMan

MethodTraceMan也是通过gradle plugin+ASM实现可配置范围的方法插桩来统计所有方法的耗时,还增加了在浏览器上展示方法耗时数据,并支持耗时筛选、线程筛选、方法名搜索等功能

Gradle Version:6.7.1 (gradle 7.0以上用不了)
Android Gradle Plugin Version:4.2.2

项目根目录build.gradle配置

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath "gradle.plugin.cn.cxzheng.methodTracePlugin:tracemanplugin:1.0.4"
    }
}

plugins {
    id 'com.android.application' version '4.2.2' apply false
    id 'com.android.library' version '4.2.2' apply false
}

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
        maven { url "https://plugins.gradle.org/m2/" }
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

模块build.gradle配置

android {
    packagingOptions {
        exclude 'META-INF/androidx.*'
    }
}

dependencies {
    debugImplementation 'com.github.zhengcx:MethodTraceMan:1.0.7'
    releaseImplementation 'com.github.zhengcx:MethodTraceMan:1.0.5-noop'
}

apply plugin: "cn.cxzheng.asmtraceman"
traceMan {
    open = true //这里如果设置为false,则会关闭插桩
    logTraceInfo = true //这里设置为true时可以在log日志里看到所有被插桩的类和方法
    traceConfigFile = "${project.projectDir}/traceconfig.txt"
}
}

模块的根目录下创建traceconfig.txt配置文件,并在里面对插桩范围进行配置
在这里插入图片描述
com/gvs/method_trace_man 换成自己的路径

#配置需插桩的包,如果为空,则默认所有文件都进行插桩(config the package need to trace,If they are empty, all files are traced by default.)
-tracepackage com/gvs/method_trace_man

#在需插桩的包下设置无需插桩的包(Setting up traceless packages under packages that require trace)
#-keeppackage com/gvs/method_trace_man

#在需插桩的包下设置无需插桩的类(Setting up traceless classes under packages that require trace)
#-keepclass com/gvs/method_trace_man/MainActivity

#插桩代码所在类,这里固定配置为:cn/cxzheng/tracemanui/TraceMan(Fixed configuration here: cn/cxzheng/tracemanui/TraceMan)
-beatclass cn/cxzheng/tracemanui/TraceMan

AndroidManifest.xml配置

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

安装 MethodTraceMan 插件


在这里插入图片描述

安装后toolbar会多出一个黄色的小灯泡
在这里插入图片描述


使用介绍

  1. 启动APP后,此时点击AndroidStduio顶部栏的MethodTraceMan灯泡小图标,则会在浏览器上打开MethodTraceMan的UI界面如下,点击开始方法耗时统计
    在这里插入图片描述

  2. 随意操作app,操作完成后点击 结束方法耗时统计
    在这里插入图片描述

  3. 此时会输出所有方法的耗时统计,你可以进行耗时筛选、线程筛选、方法名搜索等进行筛查
    在这里插入图片描述


参考资料

https://www.yuque.com/amingdexiaohudie/sdp0g1/ggg0gg?#C3970
https://www.jianshu.com/p/c975081b43fd
https://github.com/zhengcx/MethodTraceMan

注:文中部分图片引用来自互联网,如有侵权随时联系笔者删除

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值