目录

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

插桩是什么

插桩就是在代码编译期间修改已有的代码或者生成新代码

Android rom插桩 android插桩测试_android

插桩的作用

插桩可以做什么?

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

插桩的原理

Java 源文件插桩

Android rom插桩 android插桩测试_java_02


AndroidAnnotation、APT(Annotation Processing Tool),可以在代码编译期解析注解,并且生成新的 Java 文件,减少手动的代码输入。它们生成的都是 Java 文件,是在编译的最开始介入。如Greendao、ButterKnife,如下图的项目使用了Greendao开源库,可见 build 目录下有很多 *.java 后缀的文件,build一般都是放置编译生成后的产物,这些文件就是在我们 build 时候通过注解处理器产生的 Java 文件。

Android rom插桩 android插桩测试_java_03


字节码插桩

Java字节码

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

Android rom插桩 android插桩测试_java_04

Dalvik 字节码

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

Android rom插桩 android插桩测试_字节码_05




字节码插桩可以通过修改“.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文件

Android rom插桩 android插桩测试_字节码_06

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

Android rom插桩 android插桩测试_字节码_07


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

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



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

Android rom插桩 android插桩测试_java_08



ASM实战

Gradle Version:6.7.1
Android Gradle Plugin Version:4.2.2

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

Android rom插桩 android插桩测试_Android rom插桩_09

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,等待插件生成字节码

Android rom插桩 android插桩测试_android_10



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

Android rom插桩 android插桩测试_Android rom插桩_11



下面我们可以将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() 添加注解

Android rom插桩 android插桩测试_Android rom插桩_12


增加注解过滤

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文件,只有添加了注解的方法才被插桩

Android rom插桩 android插桩测试_java_13


运行APP

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

再次查看编译后的.class文件,发现刚刚成功插桩的代码都不见了

Android rom插桩 android插桩测试_android_14

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



Gradle Plugin & Transform

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

Android rom插桩 android插桩测试_Java_15


项目配置

创建一个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 资源目录

Android rom插桩 android插桩测试_字节码_16

新建asm-time-plugin.properties

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

新建TimePlugin.groovy(注意不要新建java文件,否则识别不到groovy语法)

Android rom插桩 android插桩测试_android_17

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

Android rom插桩 android插桩测试_Java_18

可以看到在本地的maven仓库生成了对应的插件

Android rom插桩 android插桩测试_Android rom插桩_19


插件应用

在项目的根目录的build.gradle添加我们的插件,sync

Android rom插桩 android插桩测试_Android rom插桩_20

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

Android rom插桩 android插桩测试_android_21

可以照着这个路径一级一级往下找

Android rom插桩 android插桩测试_Java_22




app模块的gradle配置id要与插件里定义的名字一样

Android rom插桩 android插桩测试_字节码_23


Android rom插桩 android插桩测试_android_24



运行插件

sync后查看插件中定义的TimePlugin Task

Android rom插桩 android插桩测试_android_25


Android rom插桩 android插桩测试_android_26

双击运行task,成功打印出我们在task添加的log

Android rom插桩 android插桩测试_android_27



完善插件

  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文件

Android rom插桩 android插桩测试_Android rom插桩_28



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配置文件,并在里面对插桩范围进行配置

Android rom插桩 android插桩测试_java_29


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 插件



Android rom插桩 android插桩测试_java_30

安装后toolbar会多出一个黄色的小灯泡

Android rom插桩 android插桩测试_java_31



使用介绍

  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

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