關於AOP、Plugin、Transform的概念,大家都捲到這個份上了,就不再細說了。擼個經典的demo,方法耗時檢測,提供一下學習思路。相信按照下面這個流程走下來,肯定是能入門了,畢竟我都能學會。
進入正題,說實話直接用asm api織入字節碼還是很懵逼的。工欲善其事必先利其器,下載字節碼插件ASM Bytecode Viewer
。
首先需要一個輔助類,保存方法起始時間、結束時間,還需要計算耗時的方法current
。
public class TimeManager {
private static final HashMap<String, Long> startMap = new HashMap<>();
private static final HashMap<String, Long> endMap = new HashMap<>();
public static void start(String key, long timeMills) {
startMap.put(key, timeMills);
}
public static void end(String key, long timeMills) {
endMap.put(key, timeMills);
}
public static long current(String key) {
long startTimeMills = 0L;
long endTimeMills = 0L;
if (startMap.get(key) != null) {
startTimeMills = startMap.get(key);
}
if (endMap.get(key) != null) {
endTimeMills = endMap.get(key);
}
return endTimeMills - startTimeMills;
}
}
沒什麼好多說的,以方法名爲key。那麼接下來就是ClassVisitor
、MethodVisitor
。
class TrackClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, classVisitor) {
override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val mv = cv.visitMethod(access, name, descriptor, signature, exceptions)
return TrackMethodVisitor(mv, access, name, descriptor)
}
}
object TrackMethodVisitor {
operator fun invoke(
mv: MethodVisitor,
access: Int,
name: String?,
descriptor: String?,
): MethodVisitor {
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, descriptor) {
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
return super.visitAnnotation(descriptor, visible)
}
override fun onMethodEnter() {
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
}
}
}
}
asm的代碼怎麼寫呢,別急,弄個工具類TimeUtil
,啓動插件。
public class TimeUtil {
public void handler() {
TimeManager.start("", System.currentTimeMillis());
TimeManager.end("", System.currentTimeMillis());
long currentTimeMills = TimeManager.current("");
}
}
點個錘子編譯一下,查看build
中編譯後的TimeUtil
,右鍵ASM Bytecode Viewer
。
直接看生成的asm api還是看不太明白
沒關係,對應字節碼看就很清晰了,切換到
Bytecode
tab仔細對照一下,從
LDC ""
開始對比,asm api和字節碼能一一對應。接下來的工作就很簡單了,對照字節碼去掉無用的label,然後cv大法。
因爲這裏MethodVisitor使用的AdviceAdapter,onMethodEnter()
、onMethodExit()
就可以很方便的在方法前後插樁。
方法前插入
TimeManager.start("", System.currentTimeMillis());
onMethodEnter()
override fun onMethodEnter() {
mv.visitLdcInsn(name);//方法名
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/chenxuan/annotation/TimeManager", "start", "(Ljava/lang/String;J)V", false);
super.onMethodEnter()
}
方法後插入
TimeManager.end("", System.currentTimeMillis());
long currentTimeMills = TimeManager.current("");
Log.d("chenxuan----->", "Method name total time: " + currentTimeMills + "ms");
onMethodExit()
override fun onMethodExit(opcode: Int) {
mv.visitLdcInsn(name)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitMethodInsn(INVOKESTATIC, "com/chenxuan/annotation/TimeManager", "end", "(Ljava/lang/String;J)V", false)
mv.visitLdcInsn(name)
mv.visitMethodInsn(INVOKESTATIC, "com/chenxuan/annotation/TimeManager", "current", "(Ljava/lang/String;)J", false)
mv.visitVarInsn(LSTORE, 1)
mv.visitLdcInsn("chenxuan----->")
mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
mv.visitInsn(DUP)
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
mv.visitLdcInsn("Method $name total time: ")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitVarInsn(LLOAD, 1)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
mv.visitLdcInsn("ms")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
mv.visitInsn(POP)
super.onMethodExit(opcode)
}
加個註解Track
,只對註解過的方法進行插樁。
@Target({ElementType.METHOD})
public @interface Track {
}
TrackMethodVisitor
最終代碼
object TrackMethodVisitor {
operator fun invoke(
mv: MethodVisitor,
access: Int,
name: String?,
descriptor: String?,
): MethodVisitor {
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, descriptor) {
var needTrack = false
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
if (Type.getDescriptor(Track::class.java) == descriptor) needTrack = true
return super.visitAnnotation(descriptor, visible)
}
override fun onMethodEnter() {
if (needTrack){
mv.visitLdcInsn("");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitMethodInsn(INVOKESTATIC, "com/chenxuan/annotation/TimeManager", "start", "(Ljava/lang/String;J)V", false);
}
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
if (needTrack){
mv.visitLdcInsn(name)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitMethodInsn(INVOKESTATIC, "com/chenxuan/annotation/TimeManager", "end", "(Ljava/lang/String;J)V", false)
mv.visitLdcInsn(name)
mv.visitMethodInsn(INVOKESTATIC, "com/chenxuan/annotation/TimeManager", "current", "(Ljava/lang/String;)J", false)
mv.visitVarInsn(LSTORE, 1)
mv.visitLdcInsn("chenxuan----->")
mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
mv.visitInsn(DUP)
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
mv.visitLdcInsn("Method $name total time: ")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitVarInsn(LLOAD, 1)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
mv.visitLdcInsn("ms")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
mv.visitInsn(POP)
}
super.onMethodExit(opcode)
}
}
}
}
測試一下MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
trackMethod()
}
@Track
private fun trackMethod() {
val data = mutableListOf<String>()
Thread.sleep(50)
}
}
編譯後查看build下transforms文件夾
Tools->kotlin反編譯成Java
插樁成功,但是有個問題,TimeManager工具類以方法名爲key保存方法調用的起始時間、結束時間,不太優雅。而且方法名會重複,當然也可以做一下優化,key拼接下類名,但是同一個類多個對象還是會重複,雖然可以繼續優化,但是最好的做法肯定是用局部變量。
public class TimeUtil {
public void handler() {
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
long currentTimeMills = end - start;
Log.d("chenxuan----->", "Method name total time: " + currentTimeMills + "ms");
}
}
老樣子,編譯後查看TimeUtil.class,然後使用插件。
嗯,對照Bytecode,cv到TrackMethodVisitor
object TrackMethodVisitor {
operator fun invoke(
mv: MethodVisitor,
access: Int,
name: String?,
descriptor: String?,
): MethodVisitor {
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, descriptor) {
var needTrack = false
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
if (Type.getDescriptor(Track::class.java) == descriptor) needTrack = true
return super.visitAnnotation(descriptor, visible)
}
override fun onMethodEnter() {
if (needTrack) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 1);
}
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
if (needTrack) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 3);
mv.visitVarInsn(LLOAD, 3);
mv.visitVarInsn(LLOAD, 1);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, 5);
mv.visitLdcInsn("chenxuan----->");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("Method name total time: ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, 5);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn("ms");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(POP);
}
super.onMethodExit(opcode)
}
}
}
}
build不出意外會報錯。MainActivity中的track()有一行
val data = mutableListOf<String>()
回看上面的asm中插入兩個局部變量的代碼
override fun onMethodEnter() {
if (needTrack) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 1);
}
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
if (needTrack) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 3);
}
super.onMethodExit(opcode)
}
}
LSTORE在方法前插入1的位置,方法後插入3,這個可沒有考慮到方法體本身也有局部變量,在方法後插入局部變量表的理想操作是先拿到表的長度N,然後mv.visitVarInsn(LSTORE, N+2);
。理想很豐滿現實很骨感,搜索一番沒有找到獲取局部變量表長度的api,有點裂開了。好在之前有不少大佬寫過這種東西,面向google不丟人,“借鑑”了大佬們的寫法,最後用newLocal搞定了,先上代碼。
object TrackMethodVisitor {
operator fun invoke(
mv: MethodVisitor,
access: Int,
name: String?,
descriptor: String?,
): MethodVisitor {
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, descriptor) {
var needTrack = false
var startTimeMills = -1
override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
if (Type.getDescriptor(Track::class.java) == descriptor) needTrack = true
return super.visitAnnotation(descriptor, visible)
}
override fun onMethodEnter() {
if (needTrack) {
startTimeMills = newLocal(Type.LONG_TYPE)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitIntInsn(LSTORE, startTimeMills)
}
super.onMethodEnter()
}
override fun onMethodExit(opcode: Int) {
if (needTrack) {
val durationTimeMills = newLocal(Type.LONG_TYPE)
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
mv.visitVarInsn(LLOAD, startTimeMills)
mv.visitInsn(LSUB)
mv.visitVarInsn(LSTORE, durationTimeMills)
mv.visitLdcInsn("chenxuan----->")
mv.visitTypeInsn(NEW, "java/lang/StringBuilder")
mv.visitInsn(DUP)
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false)
mv.visitLdcInsn("Method $name total time: ")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitVarInsn(LLOAD, durationTimeMills)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false)
mv.visitLdcInsn("ms")
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false)
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false)
mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false)
}
super.onMethodExit(opcode)
}
}
}
}
說人話:startTimeMills記錄了插入的位置mv.visitIntInsn(LSTORE, startTimeMills)
,然後計算結束時間end;取出startTimeMills,end與其相減,插入局部變量表,記錄一下durationTimeMills;後面拼接string的時候mv.visitVarInsn(LLOAD, durationTimeMills)
取出duration。好起來了,接下來編譯看一下成果。
沒啥問題,按照這個思路捲一捲豈不是美滋滋。