Android 性能線上監控實戰篇

聲明:原創文章,轉載請註明出處https://www.jianshu.com/p/70de92815121

之前的兩篇文章Android內存優化實戰篇Android卡頓優化實戰篇分別對內存優化和卡頓優化做了一定的總結,介紹了內存和卡頓問題的檢測和優化,不過這些都是基於本地的,然而很多情況當線上的用戶在使用過程中發生性能問題,我們是不知道,因此如何做到性能的線上監控是一個很重要的問題。所以今天就來總結下實現線上監控的一些重要技術點。

內存線上監控

我們知道我們的手機內存是有限的,這也就導致了我們每個APP都有一個上限的內存,一旦APP中使用的內存超出這個限制,APP就會出現OOM,即 Out of Memery異常。而APP中內存佔用最大的是堆中對象。因此要想監控APP中的內存,其實我們只要監控APP內對象就可以,比如監控對象什麼被創建,使用了多少內存,什麼時候被回收等,知道了這些信息其實就可以較好的監控APP的內存狀態。那麼問題來了,如何才能對對象起到較好的監控呢?這裏就需要用到一個叫JVMTI的技術,即Java虛擬機工具接口

JVMTI(Java虛擬機工具接口)

定義與功能

JVMTI是虛擬機提供給開發者使用的一套API,我們可以看下它有哪些功能:

  • 重新定義類。
  • 跟蹤對象分配和垃圾回收過程。
  • 遵循對象的引用樹,遍歷堆中的所有對象。
  • 檢查 Java 調用堆棧。
  • 暫停(和恢復)所有線程。

可以看到其中第二條跟蹤對象分配和垃圾回收過程就是我們想要的功能。知道了JVMTI可以爲我所用,接下來就看下這個到底該怎麼用。

使用

使用其實很簡單,只需要給虛擬機設置一個代理,設置完之後,JVM中有關對象內存的事件就會回調至這個代理。具體操作是調用Debug的attachJvmtiAgent方法,即Debug.attachJvmtiAgent(String library, String options, ClassLoader classLoader)。這個方法需要傳遞三個參數,第一個參數是代理的庫的路徑,這個庫也就是實現了上面說的JVM接口的庫,是一個so庫,現在我們還沒這個庫,因此下面我們還需要用C++來實現;第二個參數是代理的配置參數,可以爲空;第三個參數需要傳入一個ClassLoader來告訴虛擬機從哪裏加載這個代理庫,這裏我們可以直接通過Application的getClassLoader方法來獲取。這樣第二和第三個參數都有了。接下來就只剩下這個代理庫待實現。我們來看下這個so庫該如何實現。在這之前你需要對C++和JNI有一定的瞭解。當然如果你之前沒怎麼了解過C++也不要緊,下面的例子也非常簡單。

首先我們打開Android Studio新建一個項目叫MonitorDemo, 由於用到NDK開發相關操作,所以還需要配置下項目,首先我們在app/src/main目錄下創建一個cpp文件夾,然後新建一個叫做native-lib.cpp文件,我們具體的jvmti實現就在這裏,然後在app根目錄下新建一個CMakeLists.txt文件,具體內容如下:

cmake_minimum_required(VERSION 3.4.1)
add_library( # Specifies the name of the library.
             native-lib
             # Sets the library as a shared library.
             SHARED
             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )
include_directories(src/main/cpp/include/)
target_link_libraries(native-lib
        android
        log)

具體cmake命令可以參考配置CMake,這裏不再贅述。然後在app的build.gradle文件中添加如下配置:

externalNativeBuild {
  cmake {
    path file('CMakeLists.txt')
  }
}

這樣我們就配置好了NDK開發環境,接下來就是我們的重點即native-lib.cpp的實現。
首先當Debug.attachJvmtiAgent方法被調用後,虛擬機會回調Agent_OnAttach方法,因此我們需要在native-lib.cpp中實現這個方法來初始化一些虛擬機環境。

/**
 * 虛擬機回調程序
 */
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
    vm->GetEnv(reinterpret_cast<void **>(&_jvmtiEnv), JVMTI_VERSION);
    //獲取jvmti能力
    jvmtiCapabilities capabilities;
    _jvmtiEnv->GetPotentialCapabilities(&capabilities);
    //開啓能力
    _jvmtiEnv->AddCapabilities(&capabilities);
    return JNI_OK;
}

這裏第一句我們通過GetEnv方法拿到了jvmti環境(_jvmtiEnv),然後通過調用GetPotentialCapabilities方法獲取jvmti的能力(capabilities),然後調用AddCapabilities方法來開啓能力。

接着我們需要定義兩個方法,一個是初始化,就是告訴虛擬機你想監聽哪些事件,比如對象的分配事件、對象的回收事件等;另一個是釋放資源。因爲這兩個方法需要我們手動調用,所以我們還需要定義一個Monitor類,並在Monitor類中定義如下兩個方法:

//初始化
private native static void nativeInit();
//釋放資源
private native static void nativeRelease();

然後再看下在native-lib.cpp的具體實現:

extern "C"
JNIEXPORT void JNICALL
Java_com_example_monitordemo_Monitor_nativeInit(JNIEnv *env, jclass clazz) {
    jvmtiEventCallbacks callbacks;
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.VMObjectAlloc = &objectAlloc;
    callbacks.ObjectFree = &objectFree;
    callbacks.GarbageCollectionStart = &gcStart;
    callbacks.GarbageCollectionFinish = &gcFinish;
    _jvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
    _jvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
    _jvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_FREE, NULL);
    _jvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_START, NULL);
    _jvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_GARBAGE_COLLECTION_FINISH, NULL);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_example_monitordemo_Monitor_nativeRelease(JNIEnv *env, jclass clazz) {
    _jvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, NULL);
    _jvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_OBJECT_FREE, NULL);
    _jvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_GARBAGE_COLLECTION_START, NULL);
    _jvmtiEnv->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_GARBAGE_COLLECTION_FINISH, NULL);
}

上面在初始化方法中,我們給jvmtiEventCallbacks設置四個監聽方法,這兩個方法分別會在虛擬機創建對象、回收對象、開始垃圾回收以及結束垃圾回收時候回調。這四個具體的方法實現如下:

void JNICALL objectAlloc
        (jvmtiEnv *jvmti_env,
         JNIEnv *jni_env,
         jthread thread,
         jobject object,
         jclass object_klass,
         jlong size) {
    __android_log_print(ANDROID_LOG_INFO, LOG_TAG, "創建對象!!!!");
}
void JNICALL objectFree
        (jvmtiEnv *jvmti_env,
         jlong tag) {
    __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "回收對象!!!!");
}

void JNICALL gcStart(jvmtiEnv *jvmti_env) {
    __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "開始垃圾回收!!!!");
}
void JNICALL gcFinish(jvmtiEnv *jvmti_env) {
    __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "結束垃圾回收!!!!");
}

很簡單,我們分別在虛擬機開始回收和結束回收的時候打印下相應的日誌。接下來還需要通過SetEventNotificationMode方法來打開監聽,然後在nativeReleas方法中來關閉監聽。另外jvmtiEventCallbacks除了上面四個事件可以監聽外還有很多可以監聽,我們點進去看下:

typedef struct {
    /*   50 : 虛擬機初始化事件 */
    jvmtiEventVMInit VMInit;
    /*   51 : 虛擬機銷燬事件 */
    jvmtiEventVMDeath VMDeath;
    /*   52 : 線程開始 */
    jvmtiEventThreadStart ThreadStart;
    /*   53 : 線程結束 */
    jvmtiEventThreadEnd ThreadEnd;
    /*   54 : Class File Load Hook */
    jvmtiEventClassFileLoadHook ClassFileLoadHook;
    /*   55 : 類加載完成 */
    jvmtiEventClassLoad ClassLoad;
    /*   56 : 類準備階段完成 */
    jvmtiEventClassPrepare ClassPrepare;
    /*   57 : 虛擬機開始事件*/
    jvmtiEventVMStart VMStart;
    /*   58 : 異常 */
    jvmtiEventException Exception;
    /*   59 : 異常捕獲 */
    jvmtiEventExceptionCatch ExceptionCatch;
    /*   60 : Single Step */
    jvmtiEventSingleStep SingleStep;
    /*   61 : Frame Pop */
    jvmtiEventFramePop FramePop;
    /*   62 : 斷點 */
    jvmtiEventBreakpoint Breakpoint;
    /*   63 : 字段訪問 */
    jvmtiEventFieldAccess FieldAccess;
    /*   64 : 字段修改 */
    jvmtiEventFieldModification FieldModification;
    /*   65 :方法進入 */
    jvmtiEventMethodEntry MethodEntry;
    /*   66 : 方法退出 */
    jvmtiEventMethodExit MethodExit;
    /*   67 : 原生方法綁定*/
    jvmtiEventNativeMethodBind NativeMethodBind;
    /*   68 : 編譯方法加載 */
    jvmtiEventCompiledMethodLoad CompiledMethodLoad;
    /*   69 : 編譯方法卸載 */
    jvmtiEventCompiledMethodUnload CompiledMethodUnload;
    /*   70 : 動態代碼生成 */
    jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
    /*   71 : 數據轉儲要求 */
    jvmtiEventDataDumpRequest DataDumpRequest;
    /*   72 */
    jvmtiEventReserved reserved72;
    /*   73 : 監聽器等待 */
    jvmtiEventMonitorWait MonitorWait;
    /*   74 : 監聽器等待完成 */
    jvmtiEventMonitorWaited MonitorWaited;
    /*   75 : Monitor Contended Enter */
    jvmtiEventMonitorContendedEnter MonitorContendedEnter;
    /*   76 : Monitor Contended Entered */
    jvmtiEventMonitorContendedEntered MonitorContendedEntered;
    /*   77 */
    jvmtiEventReserved reserved77;
    /*   78 */
    jvmtiEventReserved reserved78;
    /*   79 */
    jvmtiEventReserved reserved79;
    /*   80 : Resource Exhausted */
    jvmtiEventResourceExhausted ResourceExhausted;
    /*   81 : 垃圾回收開始 */
    jvmtiEventGarbageCollectionStart GarbageCollectionStart;
    /*   82 : 垃圾回收結束 */
    jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
    /*   83 : 對象內存釋放 */
    jvmtiEventObjectFree ObjectFree;
    /*   84 : 對象內存分配 */
    jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;

接下來我們來完善下Monitor中的代碼,完整代碼如下:

public class Monitor {
    private static final String LIB_NAME = "native-lib";
    public static void init(Application application) {
        // 最低支持 Android 8.0
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return;
        }
        String agentPath = createAgentLib(application);
        //加載指定位置的so
        System.load(agentPath);
        //加載jvmti agent
        attachAgent(agentPath, application.getClassLoader());
        nativeInit();
    }
    private static void attachAgent(String agentPath, ClassLoader classLoader) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                Debug.attachJvmtiAgent(agentPath, null, classLoader);
            } else {
                Class vmDebugClazz = Class.forName("dalvik.system.VMDebug");
                Method attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String.class);
                attachAgentMethod.setAccessible(true);
                attachAgentMethod.invoke(null, agentPath);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    @RequiresApi(api = Build.VERSION_CODES.O)
    private static String createAgentLib(Context context) {
        try {
            //1、獲得so的地址
            ClassLoader classLoader = context.getClassLoader();
            Method findLibrary = ClassLoader.class.getDeclaredMethod("findLibrary",
                    String.class);
            String jvmtiAgentLibPath = (String) findLibrary.invoke(classLoader, LIB_NAME);
            //2、創建目錄:/data/data/packagename/files/monitor
            File filesDir = context.getFilesDir();
            File jvmtiLibDir = new File(filesDir, "monitor");
            if (!jvmtiLibDir.exists()) {
                jvmtiLibDir.mkdirs();
            }
            //3、將so拷貝到上面的目錄中
            File agentLibSo = new File(jvmtiLibDir, "agent.so");
            if (agentLibSo.exists()) {
                agentLibSo.delete();
            }
            Files.copy(Paths.get(new File(jvmtiAgentLibPath).getAbsolutePath()),
                    Paths.get((agentLibSo).getAbsolutePath()));
            return agentLibSo.getAbsolutePath();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    public static void release() {
        nativeRelease();
    }
    private native static void nativeInit();
    private native static void nativeRelease();
}

上面代碼比較簡單,首先定義了一個init方法中,判斷當前系統版本是否小於Android 8.0,因爲jvmti在Android 8.0及以上才支持,所以如果小於8.0就直接返回。接着我們定義一個createAgentLib方法來獲取so庫的絕對路徑,也就是上面我們說的第一個參數。接着調用System.load(agentPath)方法加載so,只有加載後我們纔可以在程序中調用so庫中的方法,比如nativeInit方法等。接着定義了一個attachAgent方法來使我們的虛擬機掛載我們寫的代理so庫。由於在Android 8.0不能直接調用Debug的attachJvmtiAgent方法,所以可以通過反射的形式調用。加載完後就可以調用nativeInit方法進行so庫的初始化。另外還定義了一個release方法來關閉虛擬機監聽。
然後我們創建一個自己的Application,在onCreate中調用Monitor方法:

class Application : Application() {
    override fun onCreate() {
        super.onCreate()
        Monitor.init(this)
    }
}

別忘了在AndroidManifest.xml中聲明這個Application。

這樣我們就可以運行這個項目,當我們跑起這個APP的時候我們在日誌窗口中可以看到一些關於對象的事件:


注意事項

到這裏我們介紹了有關內存線上監控的關鍵技術:JVMTI的作用和使用,當然這裏離我們一個完整的線上監控系統還有點距離,由於attachJvmtiAgent方法只能在Debug包中調用,如果在release中使用的話,你還需要通過hook技術改變gJdwpAllowed、IsJavaDebuggable等變量,讓系統誤以爲這個release包是一個Debug包。另外你需要將上述的事件實時記錄到日誌文件中,然後在合適的時機將文件上傳至服務器,然後後臺可以做可視化界面,這樣就便於分析。另外爲了減小日誌文件大小,提高傳輸效率,還可以對文件進行壓縮處理,以及在日誌中將一些符號做一些映射以減小文件大小。另外由於寫文件的操作非常頻繁,這裏可以將平時用io來寫文件改爲mmap寫入以避免io操作帶來的耗時影響。由於篇幅有限,有關上述的這些操作這裏不展開談論,可以參考其他文章。

卡頓線上監控

上面我們簡單分析了內存線上監控的相關技術,接下來看下卡頓該如何監控,其實上面JVMTI也能實現卡頓監控,我們可以監聽jvmtiEventCallbacks中的MethodEntry(方法進入)和MethodExit(方法退出)事件,有了這兩個事件我們就可以算出一個方法的執行耗時。不過下面我們介紹另一種監聽方法耗時的技術即字節碼插樁技術。我們知道要想監控一個方法耗時,可以在方法執行前記錄當前時間,然後在方法執行後記錄下時間,然後計算前後時間差就可以得到方法耗時,不過我們程序中有大量的方法,如果給每個方法都加上這些計時的代碼,那也太蛋疼了。。。那有什麼其他方法既能實現方法的計時,又不需要添加這麼多代碼呢?答案是肯定的,用字節碼插樁就可以實現。

字節碼插樁

定義

我們寫的Java源代碼是.java格式,虛擬機是不能直接執行的,需要將.java文件編譯成.class文件纔可以被執行。那麼我們能不能直接修改.class文件,在.class中相應方法執行前後增加計時代碼,這個當然可以,只要你遵守.class文件格式。具體的.class文件格式可以參考官方文檔:class文件格式

我們把上述直接修改.class文件的操作叫做字節碼插樁

ASM

如果你看過上面class文件格式的說明文檔,相信你已經頭暈了。如果再讓你按照上面格式對文件進行修改我想你一定會不知所措了。不過不要慌,問題不大,我們可以藉助一些框架來完成這一操作,這些框架對class的修改進行了很好的封裝,目前比較常用的框架有ASM、AspectJ等。下面我們就用ASM框架來演示下字節碼插樁。
首先我們創建一個類,裏面有一個main方法,方法也很簡單,就是打印一條語句:

class TestJVMCode {
    public static void main(String[] args) {
        System.out.println("Test ASM");
    }
}

接下來我們就用ASM框架在這個main方法前後分別增加一句獲取當前時間戳的代碼。
我們創建一個ASMTest類來實現這一操作,具體如下:

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.FileInputStream;
import java.io.FileOutputStream;

class ASMTest {
    public static void main(String[] args) throws Exception{
        FileInputStream fileInputStream = new FileInputStream("要被修改的class文件路徑(/.../.../TestJVMCode.class)");
        ClassReader classReader = new ClassReader(fileInputStream);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        classReader.accept(new ClassVisitor(Opcodes.ASM9,classWriter) {
            @Override
            public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
                System.out.println("method name is "+name);
                return new MyMethodVisitor(api,super.visitMethod(access, name, descriptor, signature, exceptions),access,name,descriptor);
            }
        },ClassReader.EXPAND_FRAMES);
        byte[] bytes = classWriter.toByteArray();
        FileOutputStream fileOutputStream = new FileOutputStream("生成新的class文件路徑(/.../.../TestJVMCode.class)");
        fileOutputStream.write(bytes);
        fileOutputStream.close();
    }
    static class MyMethodVisitor extends AdviceAdapter{
        private int startIndex;
        protected MyMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
        }
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter();
            invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis","()J"));
            startIndex = newLocal(Type.LONG_TYPE);
            storeLocal(startIndex,Type.LONG_TYPE);
        }
        @Override
        protected void onMethodExit(int opcode) {
            super.onMethodExit(opcode);
            invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis","()J"));
            int index = newLocal(Type.LONG_TYPE);
            storeLocal(index,Type.LONG_TYPE);
            loadLocal(index,Type.LONG_TYPE);
            loadLocal(startIndex,Type.LONG_TYPE);
        }
    }
}

我們來簡單分析下上面代碼,首先用原class文件的輸入流fileInputStream創建一個ClassReader對象,然後創建一個ClassWriter對象,接着調用classReader的accept方法,在accept方法中傳入一個ClassVisitor的匿名類,這個ClassVisitor的構造方法需要傳入兩個參數,一個是ASM版本號,另一個爲上面創建的ClassWriter對象。由於我們是要修改class文件中的方法,所以需要覆寫visitMethod方法,這個方法返回一個MethodVisitor對象,在這裏我們返回一個我們自己的MethodVisitor對象即MyMethodVisitor,這個類繼承自AdviceAdapter類,需要繼承兩個方法onMethodEnter和onMethodExit,這兩個看名字就可以知道分別是方法進入和退出時你要添加代碼的地方。具體如何添加我們稍後再說。添加完代碼我們就可以調用上面創建的ClassWriter對象的toByteArray方法,這個方法返回一個字節數組也就是我們修改完後新文件的數據流,有了數據流我們就可以用FileOutputStream輕鬆的寫入文件。這樣就生成了我們修改之後的.class文件。我們來運行下上面的程序就可以得到一個新的.class文件,我們直接用Android Studio打開可以看到對應的源碼:

class TestJVMCode {
    TestJVMCode() {
        long var1 = System.currentTimeMillis();
        long var3 = System.currentTimeMillis();
    }
    public static void main(String[] var0) {
        long var1 = System.currentTimeMillis();
        System.out.println(2333333);
        long var3 = System.currentTimeMillis();
    }
}

可以看到我們main方法前後添加獲取時間的代碼執行成功了,並且我們的構造方法中也新增了耗時代碼。這樣我們就將給每個方法增加耗時代碼的操作統一用ASM來處理,大大提高了效率。我們也叫這種編程方式爲面向切面編程

面向切面的程序設計(Aspect-oriented programming,AOP,又譯作面向方面的程序設計剖面導向程序設計)是計算機科學中的一種程序設計思想,旨在將橫切關注點與業務主體進行進一步分離,以提高程序代碼的模塊化程度。

上面的定義可能比較抽象,舉個簡單點的例子,比如你現在要在項目中對每個網絡接口的請求體和返回體進行日誌輸出,如果不用aop思想,你可能會在項目中每調用一處網絡請求就進行相關日誌的打印,這顯然很繁瑣,如果你的網絡請求用的OkHttp的庫,你就可以自己創建一個OkHttp的攔截器,對每個請求進行攔截再進行日誌打印,這樣項目中每處的日誌打印就都統一到攔截器中處理,使日誌模塊和原有主體程序分離,這就是AOP思想。

接下來我們來分析下上面MyMethodVisitor中是如何在方法前後加代碼的。首先要說明的是Java虛擬機並不是直接執行.class文件,它會把.class文件解析成類似彙編的虛擬機指令集,然後根據指令集的順序逐行執行。我們可以用javap命令來看下.class文件對應的虛擬機指令。可以執行如下指令:

javap -v TestJVMCode.class

執行結果:

Classfile /.../.../TestJVMCode.class
  Last modified Jul 21, 2021; size 449 bytes
  MD5 checksum 9c2ad4ac2a0f5c56b72e74f1928b97ec
  Compiled from "TestJVMCode.java"
class cn.....monitordemo.TestJVMCode
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Test ASM
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // cn/.../monitordemo/TestJVMCode
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               TestJVMCode.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Test ASM
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               cn/.../monitordemo/TestJVMCode
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V
{
  cn.....monitordemo.TestJVMCode();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Test ASM
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
}
SourceFile: "TestJVMCode.java"

上面的信息很多有常量池、代碼區等,我們主要看下main方法中的Code部分,這裏也是虛擬機執行指令的地方。首先通過執行getstatic指令,獲取PrintStream靜態類字段,然後執行ldc指令把字符串常量Test ASM壓入棧頂,接着執行invokevirtual指令,調用PrintStream的println方法,最後執行return返回。如果你對這些虛擬機執行不是很熟悉可以參考官方文檔:Java虛擬機指令集。對虛擬機指令有一定的瞭解後,再看上面MyMethodVisitor中的代碼會比較好理解,它的接口調用的設計就根據虛擬機指令來的,由於篇幅有限這裏就不展開來講了。

到這我們就知道了如何用ASM框架來爲字節碼插樁,不過我們還有個問題就是我們一個項目中肯定不止一個源碼文件,編譯完後會有大量的.class文件,面對這麼多.class文件我們肯定不能一個個文件處理,這樣工作量也太大了,和每個方法增加代碼一樣麻煩。那該如何解決這個問題呢,沒錯這時就該Gradle登場了,我們可以用gradle的Transform來完成.class文件的批量插樁,有關這方面的內容等我下次有空再來總結(手動狗頭)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章