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

相关文章
相关标签/搜索