ASM实现耗时分析

3,600 阅读6分钟

前言

我们打开一个Activity的时候是否想知道它完全加载所需要的时间,如果要分析一个页面,那我们直接在代码中修改就可以了,那么如果是多个页面呢?

这个时候我们可以利用AOP的原理,在既有class文件的基础上修改生成我们需要的class文件。

前面我们已经会自定义插件了,这次我们通过ASM来实现编译插桩的操作。

原理

打包流程

我们先来看一下打包的流程:

以上流程我们可以看到:

  • R文件、source code以及 java代码都会合并到一起生生java compiler,然后生成.class文件。再将其和其他内容生成dex文件。
  • kbuilder脚本将资源文件和.dex文件生成未签名的.apk文件。
  • Jarsigner对apk进行签名。

所以我们要做的就是在生成dex之前的.class文件上做文章。这就要用到 Teansform

Transform

Android官方从gradle1.5版本开始,提供了Transform来用于在项目构建阶段,修改class文件的一个api。Transform会在被注册之后被Gradle包装成一个Task,在java compile Task执行完之后执行。

我们来看下它的几个重要方法

    /** 指明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文件。

我们来看下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