[轉]ASM插入代碼 visitFieldInsn


今天介紹下ASM3.0,開始之前先思考幾個問題:

1.ASM是什么?
2.ASM 跟傳說中的AOP三劍客APT、aspectJ、Javassit有什么關系?
3.ASM是怎樣修改class文件的?

帶着問題開始今天的分享:

  • 1.ASM是什么?

ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。說白了asm是直接通過字節碼來修改class文件。

  • 2.ASM 跟傳說中的AOP三劍客APT、aspectJ、Javassit有什么關系?

分別解釋下這幾個名詞

APT:APT(Annotation Processing Tool)即注解處理器,是一種處理注解的工具,確切的說它是javac的一個工具,它用來在編譯時掃描和處理注解。注解處理器以Java代碼(或者編譯過的字節碼)作為輸入,生成.java文件作為輸出。簡單來說就是在編譯期,通過注解生成.java文件

aspectJ:AspectJ是一個面向切面的框架,它擴展了Java語言。AspectJ定義了AOP語法,所以它有一個專門的[編譯器]用來生成遵守Java字節編碼規范的Class文件。適合在某一個方法前后插入部分代碼,處理某些邏輯:比如方法運行時間、插入動態權限檢查等。問題會造成很多的冗余代碼,產生很多代理類。簡單來說就是在生成class時動態織入代碼

Javassit: Javassist是一個開源的分析、編輯和創建Java字節碼的類庫。是由東京工業大學的數學和計算機科學系的 Shigeru Chiba(千葉滋)所創建的。簡單來說就是源碼級別的api去修改字節碼

各種方式作用時機

  • 3.ASM是怎樣修改class文件的?

開始這個問題之前我們先學習幾個東西。

  • 字節碼

這里的字節碼主要說的是Java字節碼,看之前的一篇文章java bytecode

  • 訪問者模式

一個稱為元素(Element),另一個稱為訪問者(Visitor)。元素有一個accept方法,該方法接收訪問者作為參數;accept()方法調用訪問者的visit()方法,並且將元素自身作為參數傳遞給訪問者。由元素本身決定是否訪問

在ASM中元素(被訪問者)ClassReader、MethodNode等等,訪問者接口包含ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor

下面我們先簡單實現一個插樁操作
有如下代碼:

public class Main2Activity extends AppCompatActivity {

    private static int MESSAGE_KEY = 0x2019;
    @SuppressLint("HandlerLeak")
    private static Handler sHandler =new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what ==MESSAGE_KEY) {
                if (msg.obj!=null) {
                    Log.i("xmq", String.valueOf(msg.obj));
                }
            }
        }
    };
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sendMessage(getClass().getSimpleName());
    }
    
    private void sendMessage(String string) {
        Message message = new Message();
        message.what =MESSAGE_KEY;
        sHandler.sendMessage(message);
    }
}

我們要在sendMessage方法中添加一行代碼變為下列

private void sendMessage(String string) {
        Message message = new Message();
        message.what =MESSAGE_KEY;
        message.obj = string;
        sHandler.sendMessage(message);
}
  • 1.我們這里使用一個Android studio的plugin (ASM ByteCode Outline)查看Main2Activity的ASM代碼,看主要的sendMessage部分

{
            mv = cw.visitMethod(ACC_PRIVATE, "sendMessage", "(Ljava/lang/String;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(33, l0);
            mv.visitTypeInsn(NEW, "android/os/Message");
            mv.visitInsn(DUP);
            mv.visitMethodInsn(INVOKESPECIAL, "android/os/Message", "<init>", "()V", false);
            mv.visitVarInsn(ASTORE, 2);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(34, l1);
            mv.visitVarInsn(ALOAD, 2);
            mv.visitFieldInsn(GETSTATIC, "com/lucky/lib/studyapp/Main2Activity", "MESSAGE_KEY", "I");
            mv.visitFieldInsn(PUTFIELD, "android/os/Message", "what", "I");
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLineNumber(36, l2);
            mv.visitFieldInsn(GETSTATIC, "com/lucky/lib/studyapp/Main2Activity", "sHandler", "Landroid/os/Handler;");
            mv.visitVarInsn(ALOAD, 2);
            mv.visitMethodInsn(INVOKEVIRTUAL, "android/os/Handler", "sendMessage", "(Landroid/os/Message;)Z", false);
            mv.visitInsn(POP);
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLineNumber(37, l3);
            mv.visitInsn(RETURN);
            Label l4 = new Label();
            mv.visitLabel(l4);
            mv.visitLocalVariable("this", "Lcom/lucky/lib/studyapp/Main2Activity;", null, l0, l4, 0);
            mv.visitLocalVariable("string", "Ljava/lang/String;", null, l0, l4, 1);
            mv.visitLocalVariable("message", "Landroid/os/Message;", null, l1, l4, 2);
            mv.visitMaxs(2, 3);
            mv.visitEnd();
        }

看起來很懵逼,其實這里只不過是ASM幫助我們調用java bytecode罷了。

    1. 修改你想要的代碼,同樣使用ASM ByteCode Outlineplugin對比差異代碼

***
mv.visitFieldInsn(GETSTATIC, "com/lucky/lib/studyapp/Main2Activity", "MESSAGE_KEY", "I");
mv.visitFieldInsn(PUTFIELD, "android/os/Message", "what", "I");
Label l2 = new Label();
mv.visitLabel(l2);
mv.visitLineNumber(35, l2);
mv.visitVarInsn(ALOAD, 2);
mv.visitVarInsn(ALOAD, 1);
mv.visitFieldInsn(PUTFIELD, "android/os/Message", "obj", "Ljava/lang/Object;");
Label l3 = new Label();
mv.visitLabel(l3);
mv.visitLineNumber(36, l3);
***

看的出我們在mv.visitFieldInsn(PUTFIELD, "android/os/Message", "what", "I");后邊插入了三行代碼(其中的Label以及行數設置可以不用理),那么這三行代碼什么意思呢?這里就用到了上邊提到的java bytecode知識,意思是:將變量2,1分別入棧,並將2變量賦值給message的obj。

好了那么我們開始寫ASM,這里我們使用android transform api作為前置條件掃描文件。(有關於transform api后期分享)

開始之前先解釋幾個類:

1.Opcodes接口定義了一些常量,尤其是版本號,訪問標示符,字節碼等信息;
2.ClassReader用於讀取Class文件,主要用於Class文件的分析,可接受一個ClassVisitor;ClassReader會將解析過程中產生的類的部分信息,比如訪問標識符,字段,方法逐個送入ClassVisitor,后者在接收到對應的信息后,進行各自的處理;
3.ClassVisitor的子類ClassWriter: 負責進行Class文件的輸出和生成。ClassVisitor在進行字段和方法處理的時候,會委托給FieldVistor和MethodVisitor進行處理;在類的處理過程中,會創建對應的FieldVisitor和MethodVisitor對象;FieldVisitor和MethodVisitor類也各自有1個重要的子類,FieldWriter和MethodWriter;當ClassWriter進行字段和方法的處理時,也是依賴這兩個類進行的;
4.ClassVisitor,FieldVisitor,MethodVisitor都可以使用委托的方式,將實際的處理工作交給內部的委托類進行;它們內部有一些列的visitXXX方法,這些方法就是ASM 的實際方法code。

創建元素跟訪問者

private static byte [] scanClass(InputStream inputStream) {
        //被訪問者(元素)
        ClassReader cr = new ClassReader(inputStream)
        //訪問者
        ClassWriter cw = new ClassWriter(cr, 0)
        ScanClassVisitor cv = new ScanClassVisitor(Opcodes.ASM5,cw) //ClassWriter 的代理類
        cr.accept(cv, ClassReader.EXPAND_FRAMES)
        return cw.toByteArray()
    }

這里的訪問者ScanClassVisitor繼承了ClassVisitor,這里我們要修改某一個方法,所以實現了visitMethod方法,並篩選其中的sendMessage方法

    static class ScanClassVisitor extends ClassVisitor{
        ScanClassVisitor(int api, ClassVisitor cv) { //這里很奇怪我無法使用繼承Opcodes,內部直接調用ASM5,只能傳參數
            super(api, cv)
        }
        @Override
        MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv =  cv.visitMethod(access, name, desc, signature, exceptions)
            if (name == "sendMessage") {
                mv = new ScanMethodVisitor(Opcodes.ASM5,mv)
            }
            return mv
        }
    }

上邊的ScanMethodVisitor是實現了MethodVisitor訪問者,看ASM代碼,我們需要在mv.visitFieldInsn(PUTFIELD, "android/os/Message", "what", "I");行后插入代碼,所以需要實現visitFieldInsn方法

opcode: PUTFIELD
owner: "android/os/Message"
name:"what"
desc:"I"

static class ScanMethodVisitor extends MethodVisitor {
        ScanMethodVisitor(int api, MethodVisitor mv) {
            super(api, mv)
        }
        @Override
        void visitFieldInsn(int opcode, String owner, String name, String desc) {
            if (opcode == Opcodes.PUTFIELD && owner.equals("android/os/Message")) {
                mv.visitFieldInsn(opcode, owner, name, desc)
                mv.visitVarInsn(Opcodes.ALOAD, 2)
                mv.visitVarInsn(Opcodes.ALOAD, 1)
                mv.visitFieldInsn(Opcodes.PUTFIELD, "android/os/Message", "obj", "Ljava/lang/Object;")
            } else {
                mv.visitFieldInsn(opcode, owner, name, desc)
            }
        }
    }

好了接下來運行代碼transform看效果

public class Main2Activity extends AppCompatActivity {
    private static int MESSAGE_KEY = 8217;
    @SuppressLint({"HandlerLeak"})
    private static Handler sHandler = new Handler() {
        public void handleMessage(Message msg) {
            if (msg.what == Main2Activity.MESSAGE_KEY && msg.obj != null) {
                Log.i("xmq", String.valueOf(msg.obj));
            }

        }
    };

    public Main2Activity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2131296284);
        this.sendMessage(this.getClass().getSimpleName());
    }

    private void sendMessage(String string) {
        Message message = new Message();
        message.what = MESSAGE_KEY;
        message.obj = string;
        sHandler.sendMessage(message);
    }
}

總結:ASM直接修改class文件確實效率很高,但因直接操作字節碼,需要有字節碼知識,不適合直接上手,相比較來Javassit源碼級修改class文件更方便些。
demo

作者:heiheiwanne
鏈接:https://www.jianshu.com/p/a1e6b3abd789
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM