ASM代碼插樁監(jiān)控方法耗時

前言

我們會通過ASM+Transform 代碼插樁來實現(xiàn)方法耗時監(jiān)控 另外 用到了一個比較好用的插件
ASM Bytecode Outline 這樣在不會寫操作碼的時候 也可以去實現(xiàn)

我們主要分為三個部分來實現(xiàn)代碼插樁

  • 注解 (標記需要插樁的方法)
  • Plugin+Transform實現(xiàn)代碼掃描 尋找插樁點
  • ASM實現(xiàn)代碼生成注入

國際慣例 先貼個源碼 show me the code,no BB

注解

很簡單,直接上代碼

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CostTime {
}

Plugin+Transform

首先看一眼結構目錄 有一個比較坑的點 坑了我半天的時間


19207E7A-87EC-4EF0-B6EE-C8B7E7152855.png

之前項目自動生成 是java 結果打出來的jar包 只包含了java文件 好氣啊

1.聲明Plugin

public class CostTimePlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.android.registerTransform(new CostTimeTransform())
    }
}

2.實現(xiàn)Transform

已經將注釋都寫在代碼中 直接看代碼就可以 很簡單易懂

package com.dsg.CostTImePlugin

import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import org.apache.commons.codec.digest.DigestUtils
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter


class CostTimeTransform extends Transform {

    @Override
    String getName() {
        //Transform名稱
        return "CostTime"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        //遍歷輸入
        for (TransformInput input in inputs) {
            //遍歷Directioy
            for (DirectoryInput dirInput in input.directoryInputs) {
                //處理需要插樁的文件
                modifyClassWithPath(dirInput.file)
                //Copy修改之后的文件
                File dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes,
                        dirInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(dirInput.file, dest)
            }
            //遍歷JarInput 因為我們這里只對自己的方法插樁 所以不對JarInput做處理
            for (JarInput jarInput : input.jarInputs) {//jar(第三方庫,module)
                if (jarInput.scopes.contains(QualifiedContent.Scope.SUB_PROJECTS)) {//module library
                    //從module中獲取注解信息
//                    readClassWithJar(jarInput)
                }
                //雖然不做處理 但是還是要記得重新拷貝回去 不然會有問題
                copyFile(jarInput, outputProvider)
            }
        }
    }

    void modifyClassWithPath(File dir) {
        def root = dir.absolutePath
        dir.eachFileRecurse { File file ->
            def filePath = file.absolutePath
            //過濾非class文件
            if (!filePath.endsWith(".class")) return
            def className = getClassName(root, filePath)
            //過濾系統(tǒng)文件
            if (isSystemClass(className)) return
            //hook關鍵代碼
            hookClass(filePath, className)
        }
    }

    void hookClass(String filePath, String className) {
        //1.聲明ClassReader
        ClassReader reader = new ClassReader(new FileInputStream(new File(filePath)))
        //2聲明 ClassWriter
        ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
        //3聲明ClassVisitor
        CostTimeMethodAdapter adapter = new CostTimeMethodAdapter(writer)
        //4調用accept方法 傳入classVisitor
        reader.accept(adapter, ClassReader.EXPAND_FRAMES)
        if (adapter.changed) {
            println className + "is changed:" + adapter.changed
            byte[] bytes = writer.toByteArray()
            FileOutputStream fos = new FileOutputStream(new File(filePath))
            fos.write(bytes)
        }


    }


    //默認排除
    static final DEFAULT_EXCLUDE = [
            '^android\\..*',
            '^androidx\\..*',
            '.*\\.R$',
            '.*\\.R\\$.*$',
            '.*\\.BuildConfig$',
    ]

    //獲取類名
    String getClassName(String root, String classPath) {
        return classPath.substring(root.length() + 1, classPath.length() - 6)
                .replaceAll("/", ".")       // unix/linux
                .replaceAll("\\\\", ".")    //windows
    }

    boolean isSystemClass(String fileName) {
        for (def exclude : DEFAULT_EXCLUDE) {
            if (fileName.matches(exclude)) return true
        }
        return false
    }
    void copyFile(JarInput jarInput, TransformOutputProvider outputProvider) {
        def dest = getDestFile(jarInput, outputProvider)
        FileUtils.copyFile(jarInput.file, dest)
    }

    static File getDestFile(JarInput jarInput, TransformOutputProvider outputProvider) {
        def destName = jarInput.name
        // 重名名輸出文件,因為可能同名,會覆蓋
        def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath)
        if (destName.endsWith(".jar")) {
            destName = destName.substring(0, destName.length() - 4)
        }
        // 獲得輸出文件
        File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
        return dest
    }

}

關鍵代碼就在hookClass方法

ASM插樁

簡單講一下ASM各類的作用

ClassReader會讀取java字節(jié)碼
ClassWriter通過toByteArray可以生成修改之后的字節(jié)碼
ClassWriter是ClassVisitor的子類 我們一般會代理ClassWriter的實現(xiàn)
MethodVisitor 讀取Method方法
MethodVisitor 的子類很多 具體可以參考 ASM官網 有很多不同功能的MethodVisitor

我們會通過ClassReader讀取字節(jié)碼 然后通過ClassVisitor進行字節(jié)碼的修改 
然后再通過ClassWriter生成我們修改之后的字節(jié)碼 
大致的思路就是這樣 接下來看一下源碼
package com.dsg.CostTImePlugin;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.LocalVariablesSorter;

import static org.objectweb.asm.Opcodes.*;

/**
 * @author DSG
 * @Project ASMCostTime
 * @date 2020/6/22
 * @describe
 */
public class CostTimeClassAdapter extends ClassVisitor {
    public boolean changed; //是否修改過 
    private String owner;
    private boolean isInterface;

    public CostTimeClassAdapter(ClassVisitor visitor) {
        super(ASM4, visitor);
    }

    public CostTimeClassAdapter(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (!isInterface && mv != null && !name.equals("<init>")) {
            //將MethodVisitor交由CostTimeMethodAdapter代理
            mv = new CostTimeMethodAdapter(access, name, descriptor, mv);
        }
        return mv;

    }

    //繼承自LocalVariablesSorter 有序遍歷素有方法 
    class CostTimeMethodAdapter extends LocalVariablesSorter {
        private String name;
        private boolean isAnnotationed;
        private int time;

        public CostTimeMethodAdapter(int access, String name, String descriptor, MethodVisitor methodVisitor) {
            super(ASM4, access, descriptor, methodVisitor);
            this.name = name;
        }


        /**
         * 遍歷代碼的開始
         */
        @Override
        public void visitCode() {
            super.visitCode();
            if (isAnnotationed) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                time = newLocal(Type.LONG_TYPE);
                mv.visitVarInsn(LSTORE, time);
            }
        }

        @Override
        public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
            super.visitFieldInsn(opcode, owner, name, descriptor);
        }

        @Override
        public void visitIntInsn(int opcode, int operand) {
            super.visitIntInsn(opcode, operand);
        }


        /**
         * 遍歷操作碼 判斷是否是return語句 如果是return 就插入我們的代碼
         *
         * @param opcode 操作碼
         */
        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                if (isAnnotationed) {
                    //這里的代碼都可以由ASM插件生成
                    //Label可以生成局部變量
                    Label l1 = new Label();
                    mv.visitLabel(l1);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                    mv.visitVarInsn(LLOAD, time);
                    mv.visitInsn(LSUB);
                    mv.visitVarInsn(LSTORE, 3);
                    Label l2 = new Label();
                    mv.visitLabel(l2);
                    mv.visitLdcInsn(owner);
                    mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                    mv.visitInsn(DUP);
                    mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                    mv.visitLdcInsn("func " + name + " cost Time:");
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                    mv.visitVarInsn(LLOAD, 3);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                    mv.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
                }
            }
            super.visitInsn(opcode);
        }


        /**
         * @param descriptor 最先執(zhí)行 判斷是否存在注解 如果存在 就進行插樁
         * @param visible
         * @return
         */
        @Override
        public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
            isAnnotationed = ("Lcom/dsg/annotations/CostTime;".equals(descriptor));
            if (!changed && isAnnotationed) {
                changed = true;
            }
            return super.visitAnnotation(descriptor, visible);

        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            super.visitMaxs(maxStack, maxLocals);
        }

        @Override
        public void visitEnd() {
            super.visitEnd();
        }
    }
}

代碼也寫的很詳細了 主要思路就是

  • 判斷是否存在注解 是否需要插樁
  • 在頭部插入當前時間
  • 在return之前計算方法耗時

總結

其實ASM相對感覺還是比較簡潔明了的 只要我們找好注入點 生成我們需要的代碼就可以 還可以通過ASM插件來生成字節(jié)碼

感覺ASM+Transform的方式還是比較常見的 比如之前分析的Robust原理一樣 而且ASM基本沒有性能上的損耗 所以我們還是有必要深度學習一下

參考的大佬的文章

深入探索編譯插樁技術(四阀圾、ASM 探秘)

使用javassist和ASM修改class,并實現(xiàn)方法耗時檢測插件

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叙淌,更是在濱河造成了極大的恐慌键闺,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淑际,死亡現(xiàn)場離奇詭異,居然都是意外死亡扇住,警方通過查閱死者的電腦和手機春缕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來台囱,“玉大人淡溯,你說我怎么就攤上這事〔狙担” “怎么了咱娶?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵米间,是天一觀的道長。 經常有香客問我膘侮,道長屈糊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任琼了,我火速辦了婚禮逻锐,結果婚禮上,老公的妹妹穿的比我還像新娘雕薪。我一直安慰自己昧诱,他們只是感情好,可當我...
    茶點故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布所袁。 她就那樣靜靜地躺著盏档,像睡著了一般。 火紅的嫁衣襯著肌膚如雪燥爷。 梳的紋絲不亂的頭發(fā)上蜈亩,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天,我揣著相機與錄音前翎,去河邊找鬼稚配。 笑死,一個胖子當著我的面吹牛港华,可吹牛的內容都是我干的道川。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼立宜,長吁一口氣:“原來是場噩夢啊……” “哼愤惰!你這毒婦竟也來了?” 一聲冷哼從身側響起赘理,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎扇单,沒想到半個月后商模,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡蜘澜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年施流,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鄙信。...
    茶點故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞪醋,死狀恐怖,靈堂內的尸體忽然破棺而出装诡,到底是詐尸還是另有隱情银受,我是刑警寧澤践盼,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站宾巍,受9級特大地震影響咕幻,放射性物質發(fā)生泄漏。R本人自食惡果不足惜顶霞,卻給世界環(huán)境...
    茶點故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一肄程、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧选浑,春花似錦蓝厌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至描函,卻和暖如春崎苗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舀寓。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工胆数, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人互墓。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓必尼,卻偏偏與公主長得像,于是被迫代替她去往敵國和親篡撵。 傳聞我的和親對象是個殘疾皇子判莉,可洞房花燭夜當晚...
    茶點故事閱讀 45,851評論 2 361