Android ASM字节码插桩(上)

一、ASM简介
ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码、添加成员变量、修改父类、添加接口等等。

插桩就是将一段代码插入或者替换原本的代码。字节码插桩就是在编写的java源码编译成class字节码后,在Android下生成dex之前修改class文件,修改或者增强原有代码逻辑的操作。

二、引入ASM库
可以访问ASM官网,https://asm.ow2.io/index.html,更新asm的版本,目前最新版本9.3

在app/build.gradle下引入asm库

dependencies {
    /**
     * 使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,
     * 对我们Android中的依赖关系没有任何影响
     */
    testImplementation 'org.ow2.asm:asm:9.3'
    testImplementation 'org.ow2.asm:asm-commons:9.3'
}

引入asm库之后,就在src\test目录下编写测试代码


cfab9ccb35f2360f5bebe6c71a9ba0f.png

三、使用ASM进行字节码插桩
应用场景:通过字节码插桩计算方法的执行时间。

3.1 首先编写测试类InjectTest.java

package com.xyaty.asmdemo;

/**
 * DESC   : 测试类
 */
public class InjectTest {

    public static void main(String[] args) throws InterruptedException {
        //模拟方法执行的时间
        Thread.sleep(1000);
    }

    public void methodA() {
        System.out.println("methodA");
    }
}

3.2 接下来对InjectTest.java文件通过javac命令进行编译成InjectTest.class
由于我们操作的是字节码插桩,也就是class文件,所以需要进入 test/java下面使用 javac对这个java类进行编译生成对应的class文件,具体操作是:在Android studio底部Terminal窗口,通过cd进入到test/java目录下,然后执行以下命令:

D:\work\plugin\ASMDemo\app\src\test\java>javac com\xyaty\asmdemo\InjectTest.java

执行上面的命令编译后,就会在test/java下面生成对应的InjectTest.class文件,这个class文件就是待插桩的文件。

生成的InjectTest.class文件如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.xyaty.asmdemo;

public class InjectTest {
    public InjectTest() {
    }

    public static void main(String[] var0) throws InterruptedException {
        Thread.sleep(1000L);
    }

    public void methodA() {
        System.out.println("methodA");
    }
}

3.3 期望实现的效果就是利用ASM完成对InjectTest.class字节码的插桩

public static void main(String[] args) throws InterruptedException {
        //方法开始的时间
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        //方法结束的时间
        long end = System.currentTimeMillis();
        //输出方法执行花费的时间
        System.out.println("execute: "+(end - start)+"ms");
    }

3.4 编写测试类InjectUnitTest.java执行插桩

package com.xyaty.asmdemo;

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.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;

/**
 * DESC   :
 */
public class InjectUnitTest {
    /**
     * 单元测试方法,右击test()方法,选择run test()方法即可查看结果
     */
    @Test
    public void test() {
        try {
            //读取待插桩的class
            FileInputStream fis = new FileInputStream(
                    new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));

            /**
             * 执行分析与插桩
             * ClassReader是class字节码的读取与分析引擎
             */
            ClassReader classReader = new ClassReader(fis);
            // 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();
            // 重新写入InjectTest.class中(也可以写入到其他class中,InjectTest1.class),完成插桩
            FileOutputStream fos = new FileOutputStream(
                    new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));
            fos.write(bytes);
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class MyClassVisitor extends ClassVisitor {

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

        @Override
        public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
            System.out.println("visitMethod==>name="+name);
            /**
             * 会输出以下方法:
             * visitMethod==>name=<init>
             * visitMethod==>name=main
             */
            MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
            return new MyMethodVisitor(api, methodVisitor, access, name,descriptor);
        }
    }


    /**
     * 之所以继承自AdviceAdapter,是因为AdviceAdapter是MethodVisitor的子类,
     * AdviceAdapter封装了指令插入方法,更为直观与简单,
     * 要使用其中的onMethodEnter和 onMethodExit方法进行字节码插桩,
     *
     * 继承关系如下:
     * AdviceAdapter extends GeneratorAdapter
     * GeneratorAdapter extends LocalVariablesSorter
     * LocalVariablesSorter extends MethodVisitor
     */
    public class MyMethodVisitor extends AdviceAdapter {
        long start;
        private int startIdentifier;

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

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

        /**
         * 进入方法插入内容
         */
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();

//            start = System.currentTimeMillis();
            /**
             * @Type owner 调用哪个类
             * @Method method 调用某个类的静态方法(参数name: 方法名字,descriptor:方法中参数和方法返回值类型)
             */
            invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
            //调用newLocal创建一个long类型的变量,返回一个int类型索引identifier
            startIdentifier = newLocal(Type.LONG_TYPE);
            //保存到本地变量索引中,用一个本地变量接收上一步执行的结果
            storeLocal(startIdentifier);
        }

        /**
         * 在方法结尾插入内容
         * @param opcode
         */
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);

//            long end = System.currentTimeMillis();
//            System.out.println("execute: "+(end - start)+"ms");

            invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
            //调用newLocal创建一个long类型的变量,返回一个int类型索引identifier
            int endIdentifier = newLocal(Type.LONG_TYPE);
            //保存到本地变量索引中,用一个本地变量接收上一步执行的结果
            storeLocal(endIdentifier);

            //获取System的静态字段out,类型为PrintStream
            getStatic(Type.getType("Ljava/lang/System;"),
                    "out", Type.getType("Ljava/io/PrintStream;"));

            /**
             * "execute: "+(end - start)+"ms"实际是内部创建StringBuilder来拼接
             * 源码:NEW java/lang/StringBuilder
             * 创建一个对象StringBuilder
             */
            newInstance(Type.getType("Ljava/lang/StringBuilder;"));
            // dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
            dup();
            /**
             * 源码:INVOKESPECIAL java/lang/StringBuilder.<init> ()V
             * 创建StringBuilder的构造方法,用init来代替
             */
            invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("<init>", "()V"));

            visitLdcInsn("execute: ");
            /**
             * 源码:INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
             * 调用append方法
             */
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
            /**
             * 对结束时间和开始时间进行减法操作
             * LLOAD 3 先加载结束时间
             * LLOAD 1 后加载开始时间
             * LSUB    执行减法操作
             */
            loadLocal(endIdentifier);
            loadLocal(startIdentifier);
            //执行减法操作,返回long类型
            math(SUB, Type.LONG_TYPE);

            /**
             * 源码:INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
             * LDC "ms"
             */
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(J)Ljava/lang/StringBuilder;"));
            //拼接毫秒
            visitLdcInsn("ms");

            /**
             * 源码:
             * INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
             * INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
             * INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
             */
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
                    new Method("toString", "()Ljava/lang/String;"));
            invokeVirtual(Type.getType("Ljava/io/PrintStream;"),
                    new Method("println", "(Ljava/lang/String;)V"));
        }

    }
}

在上述代码中可以看到,
其实就是在onMethodEnter()方法中插入了:

long start = System.currentTimeMillis();

在onMethodExit()方法中插入了:

long end = System.currentTimeMillis();
System.out.println("execute: "+(end - start)+"ms");

但是使用字节码指令却写了一大堆代码。

3.5 可以通过android studio的插件ASM(以下三选一),查看指令代码
(1)ASM Bytecode Viewer
(2)ASM Bytecode Viewer Support Kotlin
(3)ASM Bytecode Outline
在AS4.1以上版本使用ASM Bytecode Viewer和ASM Bytecode Outline都报错,可能是AS升级后没有做兼容,使用ASM Bytecode Viewer Support Kotlin即可搞定(亲测)。

a275c0b4bdc548b8f9fec8e1dc19ad5.png

安装好ASM之后,重启AS,在InjectTest.java中右键点击选项“ASM Bytecode Viewer”查看指令代码

InjectTest.java文件

package com.xyaty.asmdemo;


/**
 * Author :
 * Date   : 2022/7/21
 * DESC   :
 */
public class InjectTest {

    @ASMTest
    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println("execute: "+(end - start)+"ms");
    }

    public void methodA() {
        System.out.println("methodA");
    }
}

ASM指令代码截图:


778c04f87130c625967e140f11bf6dc.png

ASM指令代码如下:

// class version 51.0 (51)
// access flags 0x21
public class com/xyaty/asmdemo/InjectTest {

  // compiled from: InjectTest.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V throws java/lang/InterruptedException 
  @Lcom/xyaty/asmdemo/ASMTest;() // invisible
   L0
    LINENUMBER 13 L0
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 1
   L1
    LINENUMBER 14 L1
    LDC 1000
    INVOKESTATIC java/lang/Thread.sleep (J)V
   L2
    LINENUMBER 15 L2
    INVOKESTATIC java/lang/System.currentTimeMillis ()J
    LSTORE 3
   L3
    LINENUMBER 16 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "execute: "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LLOAD 3
    LLOAD 1
    LSUB
    INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
    LDC "ms"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 17 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE start J L1 L5 1
    LOCALVARIABLE end J L3 L5 3
    MAXSTACK = 6
    MAXLOCALS = 5

  // access flags 0x1
  public methodA()V
   L0
    LINENUMBER 20 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "methodA"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 21 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

这些指令涉及到了java类型描述符和方法描述,


f0d7a05a07f4257db0a53660bebb9bd.png
59a8e46f68d9904b087b3e0c49fcafc.png

①类型描述符
Java代码中的类型,在字节码中有相应的表示协议:
(1)Java基本类型的描述符是单个字符,例如Z表示boolean、C表示char
(2)类的类型的描述符是这个类的全限定名,前面加上字符L , 后面跟上一个「;」,例如String的类型描述符为Ljava/lang/String;
(3)数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符,多维数组则使用多个方括号。

借助上面的协议分析,想要看到字节码中参数的类型,就比较简单了。

②方法描述符
方法描述符(方法签名)是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。

方法描述符以左括号开头,然后是每个形参的类型描述符,然后是是右括号,接下来是返回类型的类型描述符,例如,该方法返回void,则是V,要注意的是,方法描述符中不包含方法的名字或参数名。

比如:
void m(int i, float f)对应的方法描述符是(IF)V ,表明该方法会接收一个int和float型参数,且无返回值。
int m(Object o)对应的方法描述符是(Ljava/lang/Object;)I 表示接收Object型参数,返回int。
int[] m(int i, String s)对应的方法描述符是(ILjava/lang/String;)[I 表示接受int和String,返回一个int[]。
Object m(int[] i)对应的方法描述符是 ([I)Ljava/lang/Object; 表示接受一个int[],返回Object。

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

推荐阅读更多精彩内容