ASM實現耗時分析

前言

咱們打開一個Activity的時候是否想知道它徹底加載所須要的時間,若是要分析一個頁面,那咱們直接在代碼中修改就能夠了,那麼若是是多個頁面呢?html

這個時候咱們能夠利用AOP的原理,在既有class文件的基礎上修改生成咱們須要的class文件。java

前面咱們已經會自定義插件了,此次咱們經過ASM來實現編譯插樁的操做。android

原理

打包流程

咱們先來看一下打包的流程: git

以上流程咱們能夠看到:github

  • R文件、source code以及 java代碼都會合併到一塊兒生生java compiler,而後生成.class文件。再將其和其餘內容生成dex文件。
  • kbuilder腳本將資源文件和.dex文件生成未簽名的.apk文件。
  • Jarsigner對apk進行簽名。

因此咱們要作的就是在生成dex以前的.class文件上作文章。這就要用到 Teansformapi

Transform

Android官方從gradle1.5版本開始,提供了Transform來用於在項目構建階段,修改class文件的一個api。Transform會在被註冊以後被Gradle包裝成一個Task,在java compile Task執行完以後執行。數組

咱們來看下它的幾個重要方法bash

/** 指明transform的task名字 */
    @Override
    String getName() {
        return null
    }
    /**
    指明輸入類型:
        CLASSES:class文件,來自jar或者文件夾
        RESOURCES: java資源
    */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return null
    }
    
    /**
    指明輸入文件所屬範圍:
        PROJECT:當前項目代碼,
        SUB_PROJECTS:子工程代碼,
        EXTERNAL_LIBRARIES:外部庫代碼,
        TESTED_CODE:測試代碼,
        PROVIDED_ONLY:provided庫代碼,
    */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return null
    }

    /** 指明是不是增量構建 */
    @Override
    boolean isIncremental() {
        return false
    }
複製代碼

最最重要的方法是transform方法,經過其中的transformInvocation得到TransformInputDirectoryInputJarInput以及TransformOutputProvider框架

  • TransformInput: 輸入文件的抽象,包括DirectoryInput集合以及JarInput集合。
  • DirectoryInput: 表明以源碼方式參與編譯的目錄結構以及下面的源文件,能夠用來修改輸出文件的結構及其字節碼文件。
  • JarInput:全部參與編譯的jar文件包括本地和遠程jar文件。
  • TransformOutputProvider:Transform的輸出,能夠經過它來獲取輸出路徑。

ASM

我這裏使用的是ASM的方式進行編譯時插樁,ASM是一個通用的java字節碼操做和分析框架。能夠生成、轉換和分析已編譯的java class文件,可以使用ASM工具讀、寫、轉換JVM指令集。也就是說來處理jacac編譯以後的class文件。ide

咱們來看下ASM框架的幾個核心類:

  • ClassReader:該類用來解析字節碼class文件,能夠接受一個實現了ClassVisitor接口的對象做爲參數,而後依次調用ClassVisitor接口的各個方法,進行本身的處理。
  • ClassWriter:ClassVisitor的子類,用來對class文件輸出和生成。在對類或者方法進行處理的時候,經過FieldVisitorMethodVisitor進行處理。他們各自都有本身重要的子類:FiledWriterMethodWriter。對於每個方法的調用會建立類的相應部分,例如調用visit方法會建立一個類的聲明部分,調用visitMethod會在這個類中建立一個新的方法,調用visitEnd會代表對該類的建立已經完成了,最終會經過toByteArray方法返回一個數組,這個數組包含了整個class文件的完整字節碼內容。
  • ClassAdapter:實現了ClassVisitor接口,其構造方法須要ClassVisitor隊形,並保存字段爲protected ClassVisitor。在它的實現中,每一個方法都是原裝不動的調用classVisitor對應方法,並傳遞一樣的參數。能夠經過集成ClassAdapter並修改其中的部分方法達到過濾的做用。它能夠堪稱事件的過濾器。

實現

好了,基本的知識咱們已經瞭解了,如今咱們開始一步步實現咱們須要的功能。

首先,咱們先自定義兩個註解以及計算時間的工具類。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnStartTime {
}
複製代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface OnEndTime {
}
複製代碼

只在註解了這兩個的方法中進行耗時統計。

工具類:

public class TimeCache {
    private static volatile TimeCache mInstance;

    private static byte[] mLock = new byte[0];

    private Map<String, Long> mStartTimes = new HashMap<>();

    private Map<String, Long> mEndTimes = new HashMap<>();

    private TimeCache() {}

    public static TimeCache getInstance() {
        if (mInstance == null) {
            synchronized (mLock) {
                if (mInstance == null) {
                    mInstance = new TimeCache();
                }
            }
        }
        return mInstance;
    }
    public void putStartTime(String className, long time) {
            mStartTimes.put(className, time);
    }

    public void putEndTime(String className, long time) {
        mEndTimes.put(className, time);
    }

    public void printlnTime(String className) {
        if (!mStartTimes.containsKey(className) || !mEndTimes.containsKey(className)) {
            System.out.println("className ="+ className + "not exist");
        }
        long currTime = mEndTimes.get(className) - mStartTimes.get(className);
        System.out.println("className ="+ className + ",time consuming " + currTime+ " ns");
    }
}
複製代碼

只有在onStart 和onEnd都註解了以後,纔會計算耗時。

新建Transform類,處理transform邏輯。

@Override
    String getName() {
        return "custom_plugin"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // 輸入類型:class文件
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        // 輸入文件範圍:project包括jar包
        return TransformManager.SCOPE_FULL_PROJECT
    }
複製代碼
@Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        println("//============asm visit start===============//")
        def startTime = System.currentTimeMillis()

        Collection<TransformInput> inputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider

        if (outputProvider != null) {
            outputProvider.deleteAll()
        }

        inputs.each { TransformInput input ->

            input.directoryInputs.each { DirectoryInput directoryInput ->

                handleDirectoryInput(directoryInput, outputProvider)

            }

            input.jarInputs.each { JarInput jarInput ->

                handleJarInput(jarInput, outputProvider)

            }
        }

        def customTime = (System.currentTimeMillis() - startTime) / 1000
        println("plugin custom time = " + customTime + " s")
        println("//============asm visit end===============//")
    }
複製代碼

input分爲兩類:一個是項目中的,一個是jar包中的。咱們目前只處理項目中的。

static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        if (directoryInput.file.isDirectory()) {
            directoryInput.file.eachFileRecurse { File file ->

                def name = file.name
                // 排除不須要修改的類
                if (name.endsWith(".class") && !name.startsWith("R\$") && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                    println("name =="+ name + "===is changing...")
                    ClassReader classReader = new ClassReader(file.bytes)
                    //
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                    //
                    ClassVisitor classVisitor = new CustomClassVisitor(classWriter)

                    classReader.accept(classVisitor, EXPAND_FRAMES)

                    byte [] code = classWriter.toByteArray()

                    FileOutputStream fos = new FileOutputStream(file.parentFile.absolutePath + File.separator + name)

                    fos.write(code)

                    fos.close()
                }

            }
        }

        //處理完輸入文件以後,要把輸出給下一個任務
        def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

        FileUtils.copyDirectory(directoryInput.file, dest)
    }
   
複製代碼

在ClassVisitor中處理咱們要過濾的類,而後對其進行修改。

@Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);

        methodVisitor = new AdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc) {

            private boolean isStart = false;

            private boolean isEnd = false;
            @Override
            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                if ("Lcom/cn/lenny/annotation/OnStartTime;".equals(desc)) {
                    isStart = true;
                }
                if ("Lcom/cn/lenny/annotation/OnEndTime;".equals(desc)) {
                    isEnd = true;
                }
                return super.visitAnnotation(desc, visible);
            }

            @Override
            protected void onMethodEnter() {
                // 方法開始
                if (isStart) {
//                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache", "getInstance", "()Lcom/cn/lenny/annotation/TimeCache;", false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache", "putStartTime", "(Ljava/lang/String;J)V", false);
                }
                super.onMethodEnter();
            }

            @Override
            protected void onMethodExit(int opcode) {
                // 方法結束
                if (isEnd) {
                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache", "getInstance", "()Lcom/cn/lenny/annotation/TimeCache;", false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
                    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache", "putEndTime", "(Ljava/lang/String;J)V", false);
                    mv.visitLdcInsn(name);
                    mv.visitMethodInsn(INVOKESTATIC, "com/cn/lenny/annotation/TimeCache", "getInstance", "()Lcom/cn/lenny/annotation/TimeCache;", false);
                    mv.visitVarInsn(ALOAD, 0);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getSimpleName", "()Ljava/lang/String;", false);
                    mv.visitMethodInsn(INVOKEVIRTUAL, "com/cn/lenny/annotation/TimeCache", "printlnTime", "(Ljava/lang/String;)V", false);
                }
                super.onMethodExit(opcode);
            }
        };
        return methodVisitor;
    }
複製代碼

關於增長字節碼,能夠去看一個關於字節碼的文檔,也能夠經過插件ASM Bytecode Outline來幫助咱們。

咱們來看下編譯以後的類是否達到咱們想要的效果了

public class TestActivity extends Activity {
    public TestActivity() {
    }

    @OnStartTime
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        TimeCache.getInstance().putStartTime(this.getClass().getSimpleName(), System.currentTimeMillis());
        super.onCreate(savedInstanceState);
        this.setContentView(2131296285);
    }

    @OnEndTime
    protected void onResume() {
        super.onResume();
        String var10000 = "onResume";
        TimeCache.getInstance().putEndTime(this.getClass().getSimpleName(), System.currentTimeMillis());
        String var10001 = "onResume";
        TimeCache.getInstance().printlnTime(this.getClass().getSimpleName());
    }
}
複製代碼

哇,成功了。

看到這裏,我以爲你也能夠本身寫一個編譯插樁的代碼了。

總結

利用AOP的思路來統計耗時,避免了對於原有代碼的修改,減小了大量的重複性工做,而且減小了代碼的耦合性;缺點在於ASM操做理解都有必定的難度,而且干預了APK打包的過程,致使編譯速度變慢。

參考

Transform api

深刻理解Android之Gradle

相關文章
相關標籤/搜索