Android ASM字节码插桩(下)

由于内容篇幅限制,紧接着上篇
https://www.jianshu.com/p/c975081b43fd?u_atoken=de4d112a-6864-4703-ad69-f82922d505de&u_asession=01X_C9UMmFccLMXpIfjRM8FYXsq1g_CrgffyPiHHUpgZGBrWiw193wtdhvbyaJ4chEX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K_OI3-HIFVcmZbMNunYRvYCslvTX-jMTLEIhdGFg3rxgWBkFo3NEHBv0PZUm6pbxQU&u_asig=05e9XZPq2Q7yJkKdk86YtiyFfiNPAdPSg-Voed-oPaPvY2QwBnCAT6a9bvIxBzozUzQDvsYuznxmEgCniYpEFR8BE5VfTwKn3vCIpIwgp7gRXcA22dI9OX-SjB0FJAYc2ww3dPL_NNCbygKg7jU_P9L1CtmgFQaXXOFbG5UybdQYj9JS7q8ZD7Xtz2Ly-b0kmuyAKRFSVJkkdwVUnyHAIJzet9GpoGT6ubHEJ3SNQzl3mpaIfenFB8MFcEbsDwJo4a6FPw117USKdEPc8n7HkzU-3h9VXwMyh6PgyDIVSG1W-IddwkY7v-CAh1IBL0sYO20tCNasH120WVmgCT0pzm_wUd5n3s6RPzq9rJ9ybbMWH4abeDNZysnYRhxU5ybSOYmWspDxyAEEo4kbsryBKb9Q&u_aref=%2FnUp0P8E0Oe%2BxeO5Bz5i0j215Eo%3D

3.6 执行InjectUnitTest.java的test()方法查看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() {
        long var1 = System.currentTimeMillis();
        long var3 = System.currentTimeMillis();
        System.out.println("execute: " + (var3 - var1) + "ms");
    }

    public static void main(String[] var0) throws InterruptedException {
        long var1 = System.currentTimeMillis();
        Thread.sleep(1000L);
        long var3 = System.currentTimeMillis();
        System.out.println("execute: " + (var3 - var1) + "ms");
    }

    public void methodA() {
        long var1 = System.currentTimeMillis();
        System.out.println("methodA");
        long var3 = System.currentTimeMillis();
        System.out.println("execute: " + (var3 - var1) + "ms");
    }
}

发现在每个方法中都加入了

long var1 = System.currentTimeMillis();
和
long var3 = System.currentTimeMillis();
System.out.println("execute: " + (var3 - var1) + "ms");

如果仅仅在main()方法才注入代码,就需要引入自定义注解来标记指定的方法

四、引入自定义注解,标记方法才注入代码
4.1 注解类ASMTest.java,并通过javac编译成ASMTest.class(略)

package com.xyaty.asmdemo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * DESC   :
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ASMTest {

}

4.2 在InjectTest.java的main()方法上加上注解@ASMTest,标记此方法,并通过javac生成class

package com.xyaty.asmdemo;

/**
 * DESC   :
 */
public class InjectTest {

    @ASMTest
    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(1000);
    }

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

4.3 在上述MyMethodVisitor的下列方法中加入注解判断

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

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

            if (!isInject) {
                return;
            }
        }

@Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);

            if (!isInject) {
                return;
            }
}

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;
        private boolean isInject = false;//是否注入代码

        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            System.out.println("visitAnnotation===>methodName="+getName()+", descriptor="+descriptor);
            //如果方法的注解名字是@ASMTest,则给此方法注入代码
            if ("Lcom/xyaty/asmdemo/ASMTest;".equals(descriptor)) {
                isInject = true;
            } else {
                isInject = false;
            }
            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();

            if (!isInject) {
                return;
            }

//            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);

            if (!isInject) {
                return;
            }

//            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"));
        }

    }
}

再次运行InjectTest.class结果如下,可以看到只有main()方法注入了代码

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

package com.xyaty.asmdemo;

public class InjectTest {
    public InjectTest() {
    }

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

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

到此结束。

参考文章:
https://blog.csdn.net/zenmela2011/article/details/125586333
https://blog.csdn.net/huangbin123/article/details/123322667

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容