ASM學習思路

關於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。那麼接下來就是ClassVisitorMethodVisitor

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還是看不太明白

沒關係,對應字節碼看就很清晰了,切換到Bytecodetab

仔細對照一下,從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。好起來了,接下來編譯看一下成果。


沒啥問題,按照這個思路捲一捲豈不是美滋滋。

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