声明:原创文章,转载请注明出处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(&_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."":()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
#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 // "":()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."":()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文件的批量插桩,有关这方面的内容等我下次有空再来总结(手动狗头)。