看完這篇你還不會ASM字節碼插樁,我吃x!

ASM字節碼插樁

1、什麼是插樁

QQ空間曾經發布的熱修復解決方案中利用Javaassist庫實現向類的構造函數中插入一段代碼解決CLASS_ISPREVERIFIED 問題。包括了Instant Run的實現以及參照Instant Run實現的熱修復美團Robus等都利用到了插樁技術。html

插樁就是將一段代碼插入或者替換本來的代碼。字節碼插樁顧名思義就是在咱們編寫的源碼編譯成字節碼(Class)後,在Android下生成dex以前修改Class文件,修改或者加強原有代碼邏輯的操做。java

插樁前

插樁後

咱們須要查看方法執行耗時,若是每個方法都須要本身手動去加入這些內容,當不須要時也須要一個個刪去相應的代碼。1個、兩個方法還好,若是有10個、20個得多麻煩!因此能夠利用註解來標記須要插樁的方法,結合編譯後操做字節碼來幫助咱們自動插入,當不須要時關掉插樁便可。這種AOP思想讓咱們只須要關注插樁代碼自己。android

2、字節碼操做框架

上面咱們提到QQ空間使用了Javaassist來進行字節碼插樁,除了Javaassist以外還有一個應用更爲普遍的ASM框架一樣也是字節碼操做框架,Instant Run包括AspectJ就是藉助ASM來實現各自的功能。web

咱們很是熟悉的JSON格式數據是基於文本的,咱們只須要知道它的規則就可以輕鬆的生成、修改JSON數據。一樣的Class字節碼也有其本身的規則(格式)。操做JSON能夠藉助GSON來很是方便的生成、修改JSON數據。而字節碼Class,一樣能夠藉助Javassist/ASM來實現對其修改。shell

打包流程

字節碼操做框架的做用在於生成或者修改Class文件,所以在Android中字節碼框架自己是不須要打包進入APK的,只有其生成/修改以後的Class才須要打包進入APK中。它的工做時機在上圖Android打包流程中的生成Class以後,打包dex以前。api

3、ASM的使用

因爲ASM具備相對於Javassist更好的性能以及更高的靈活行,咱們這篇文章以使用ASM爲主。在真正利用到Android中以前,咱們能夠先在Java程序中完成對字節碼的修改測試。markdown

3.一、在AS中引入ASM

ASM能夠直接從jcenter()倉庫中引入,因此咱們能夠進入:bintray.com/進行搜索app

jcenter搜索

點擊圖中標註的工件進入,能夠看到最新的正式版本爲:7.1。框架

asm版本查看

所以,咱們能夠在AS中加入:ide

引入asm

同時,須要注意的是:咱們使用testImplementation引入,這表示咱們只能在Java的單元測試中使用這個框架,對咱們Android中的依賴關係沒有任何影響。

AS中使用gradle的Android工程會自動建立Java單元測試與Android單元測試。測試代碼分別在test與androidTest。

3.二、準備待插樁Class

test/java下面建立一個Java類:

public class InjectTest {
	
    public static void main(String[] args) {
        
    }
}

複製代碼

因爲咱們操做的是字節碼插樁,因此能夠進入test/java下面使用javac對這個類進行編譯生成對應的class文件。

javac InjectTest.java
複製代碼

3.三、執行插樁

由於main方法中沒有任何輸出代碼,咱們輸入命令:java InjectTest執行這個Class不會有任何輸出。那麼咱們接下來利用ASM,向main方法中插入一開始圖中的記錄函數執行時間的日誌輸出。

在單元測試中寫入測試方法

/** * 一、準備待分析的class */
        FileInputStream fis = new FileInputStream
                ("xxxxx/test/java/InjectTest.class");

        /** * 二、執行分析與插樁 */
        //class字節碼的讀取與分析引擎
        ClassReader cr = new ClassReader(fis);
        // 寫出器 COMPUTE_FRAMES 自動計算全部的內容,後續操做更簡單
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        //分析,處理結果寫入cw EXPAND_FRAMES:棧圖以擴展格式進行訪問
        cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);


        /** * 三、得到結果並輸出 */
        byte[] newClassBytes = cw.toByteArray();
        File file = new File("xxx/test/java2/");
        file.mkdirs();

        FileOutputStream fos = new FileOutputStream
                ("xxx/test/java2/InjectTest.class");
        fos.write(newClassBytes);

        fos.close();
複製代碼

關於ASM框架自己的設計,咱們這裏先不討論。上面的代碼會獲取上一步生成的class,而後由ASM執行完插樁以後,將結果輸出到test/java2目錄下。其中關鍵點就在於第2步中,如何進行插樁。

把class數據交給ClassReader,而後進行分析,相似於XML解析,分析結果會以事件驅動的形式告知給accept的第一個參數ClassAdapterVisitor

public class ClassAdapterVisitor extends ClassVisitor {

        public ClassAdapterVisitor(ClassVisitor cv) {
            super(Opcodes.ASM7, cv);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            System.out.println("方法:" + name + " 簽名:" + desc);

            MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                    exceptions);
            return new MethodAdapterVisitor(api,mv, access, name, desc);
        }
}
複製代碼

分析結果經過ClassAdapterVisitor得到,一個類中會存在方法、註解、屬性等,所以ClassReader會將調用ClassAdapterVisitor中對應的visitMethodvisitAnnotationvisitField這些visitXX方法。

咱們的目的是進行函數插樁,所以重寫visitMethod方法,在這個方法中咱們返回一個MethodVisitor方法分析器對象。一個方法的參數、註解以及方法體須要在MethodVisitor中進行分析與處理。

package com.enjoy.asminject.example;

import com.enjoy.asminject.ASMTest;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;

/** * AdviceAdapter: 子類 * 對methodVisitor進行了擴展, 能讓咱們更加輕鬆的進行方法分析 */
public class MethodAdapterVisitor extends AdviceAdapter {

    private boolean inject;

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


    /** * 分析方法上面的註解 * 在這裏幹嗎??? * <p> * 判斷當前這個方法是否是使用了injecttime,若是使用了,咱們就須要對這個方法插樁 * 沒使用,就無論了。 * * @param desc * @param visible * @return */
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        if (Type.getDescriptor(ASMTest.class).equals(desc)) {
            System.out.println(desc);
            inject = true;
        }
        return super.visitAnnotation(desc, visible);
    }

    private int start;

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        if (inject) {
            //執行完了怎麼辦? 記錄到本地變量中
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));

            start = newLocal(Type.LONG_TYPE); //建立本地 LONG類型變量
            //記錄 方法執行結果給建立的本地變量
            storeLocal(start);
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        if (inject){
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
            int end = newLocal(Type.LONG_TYPE);
            storeLocal(end);

            getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
                    "/PrintStream;"));

            //分配內存 並dup壓入棧頂讓下面的INVOKESPECIAL 知道執行誰的構造方法建立StringBuilder
            newInstance(Type.getType("Ljava/lang/StringBuilder;"));
            dup();
            invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));


            visitLdcInsn("execute:");
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));

            //減法
            loadLocal(end);
            loadLocal(start);
            math(SUB,Type.LONG_TYPE);


            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)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"));

        }
    }
}
複製代碼

MethodAdapterVisitor繼承自AdviceAdapter,其實就是MethodVisitor 的子類,AdviceAdapter封裝了指令插入方法,更爲直觀與簡單。

上述代碼中onMethodEnter進入一個方法時候回調,所以在這個方法中插入指令就是在整個方法最開始加入一些代碼。咱們須要在這個方法中插入long s = System.currentTimeMillis();。在onMethodExit中即方法最後插入輸出代碼。

@Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        if (inject) {
            //執行完了怎麼辦? 記錄到本地變量中
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));

            start = newLocal(Type.LONG_TYPE); //建立本地 LONG類型變量
            //記錄 方法執行結果給建立的本地變量
            storeLocal(start);
        }
    }
複製代碼

這裏面的代碼怎麼寫?其實就是long s = System.currentTimeMillis();這句代碼的相對的指令。咱們能夠先寫一份代碼

void test(){
    //插入的代碼
    long s = System.currentTimeMillis();
    /** * 方法實現代碼.... */ 
    //插入的代碼
	long e = System.currentTimeMillis();
	System.out.println("execute:"+(e-s)+" ms.");
}
複製代碼

而後使用javac編譯成Class再使用javap -c查看字節碼指令。也能夠藉助插件來查看,就不須要咱們手動執行各類命令。 插件安裝

安裝完成以後,能夠在須要插樁的類源碼中點擊右鍵:

查看字節碼

點擊ASM Bytecode Viewer以後會彈出

字節碼

因此第20行代碼:long s = System.currentTimeMillis();會包含兩個指令:INVOKESTATICLSTORE

再回到onMethodEnter方法中

@Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        if (inject) {
            //invokeStatic指令,調用靜態方法
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
		   //建立本地 LONG類型變量
            start = newLocal(Type.LONG_TYPE); 
            //store指令 將方法執行結果從操做數棧存儲到局部變量
            storeLocal(start);
        }
    }
複製代碼

onMethodExit也一樣根據指令去編寫代碼便可。最終執行完插樁以後,咱們就能夠得到修改後的class數據。

4、Android中的實現

在Android中實現,咱們須要考慮的第一個問題是如何得到全部的Class文件來判斷是否須要插樁。Transform就是幹這件事情的。

相關文章
相關標籤/搜索