通过Gradle的Transform配合ASM实战路由框架和统计方法耗时

4,339 阅读7分钟
原文链接: blog.csdn.net

首先,现在世面上的项目基本上都是N多个module并行开发很容易就会出现moduleA想跳转到moduleB某一个界面去如果你没有把moduleB在对应的build.gradle中配置的话,AS就会友好的提示你跳不过去,这时候就需要一个路由来分发跳转操作了。 其次,随着时间的慢慢迭代发现需求功能已经写完了,慢慢开始要各种优化了,常见的优化是速度优化自然而然就需要查看方法的耗时情况,那么解放双手的时候就需要一个正确的姿势来统计方法耗时。

附上Github项目地址:github.com/Neacy/Neacy…

思路

1.采用注解(Annotation)在要跳转的界面和需要统计的地方加上相对应的协议。
2.用groovy语言实现一个Transform的gradle插件来解析相对应的注解。
3.采用ASM框架生成相对应的代码主要是写入或者插入class的字节码。
4.路由框架中需要反射拿到ASM生成的路由表然后代码中调用从而实现跳转。

==============带着这些思路接下来就是拼命写代码了………….

先上两个用到的注释,注释还是比较简单的分分钟写完,需要注意的是我们是class操作所以要选@Retention(RetentionPolicy.CLASS)

/**
 * 用于标记协议
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface NeacyProtocol {
    String value();
}

/**
 * 用于标记方法耗时
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface NeacyCost {
    String value();
}

换个姿势写一个gradle插件,如何写主要参考区长的blog.csdn.net/sbsujjbcy/a…,按着步骤就好,假设我们看完了并设置好了那么就有一个雏形了:

public class NeacyPlugin extends Transform implements Plugin<Project> {

    private static final String PLUGIN_NAME = "NeacyPlugin"

    private Project project

    @Override
    void apply(Project project) {
        this.project = project

        def android = project.extensions.getByType(AppExtension);
        android.registerTransform(this)
    }

    @Override
    String getName() {
        return PLUGIN_NAME
    }

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

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

    @Override
    boolean isIncremental() {
        return true
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {

    }
}

我们要做的就是在transform中扫描相对应的注解并用ASM写入class字节码。我们知道TransformInput对应的有两种可能性一种是目录 一种是jar包所以要分开遍历:

inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput directoryInput ->
                if (directoryInput.file.isDirectory()) {
                    println "==== directoryInput.file = " + directoryInput.file
                    directoryInput.file.eachFileRecurse { File file ->
                        // ...对目录进行插入字节码
                    }
                }
                //处理完输入文件之后,要把输出给下一个任务
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            input.jarInputs.each { JarInput jarInput ->
                println "------=== jarInput.file === " + jarInput.file.getAbsolutePath()
                File tempFile = null
                if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
                    // ...对jar进行插入字节码
                }
                /**
                 * 重名输出文件,因为可能同名,会覆盖
                 */
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                //处理jar进行字节码注入处理
                def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

对于代码中陌生的代码风格可以查阅这篇文章:blog.csdn.net/innost/arti…保证看完之后什么都懂了,好文强烈推荐。

然后,最麻烦的就是字节码注入的部分功能了,先看一下主要的调用代码:

ClassReader classReader = new ClassReader(file.bytes)
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            NeacyAsmVisitor classVisitor = new NeacyAsmVisitor(Opcodes.ASM5, classWriter)
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

调用的主要代码量还是比较少的,主要是自定义一个ClassVisitor。在每一个ClassVisitor中它会分别visitAnnotationvisitMethod

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        NeacyLog.log("=====---------- NeacyAsmVisitor visitAnnotation ----------=====");
        NeacyLog.log("=== visitAnnotation.desc === " + desc);
        AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);

        if (Type.getDescriptor(NeacyProtocol.class).equals(desc)) {// 如果注解不为空的话
            mProtocolAnnotation = new NeacyAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);
            return mProtocolAnnotation;
        }
        return annotationVisitor;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        NeacyLog.log("=====---------- visitMethod ----------=====");
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        mMethodVisitor = new NeacyMethodVisitor(Opcodes.ASM5, mv, access, name, desc);
        return mMethodVisitor;
    }

visitAnnotation中就是我们扫描相对应的注解的地方类似Type.getDescriptor(NeacyProtocol.class).equals(desc)判断是否是我们需要的处理的注解,像这里我们主要处理前面定义好的注解NeacyProtocolNeacyCost两个注解就好。

这里我要展示一下注入成功之后的class中的代码是什么模样: 生成好的路由表:

这里写图片描述

注入成功的耗时代码:
这里写图片描述

看一眼logcat打印出来的耗时时间,感觉离成功不远了。可是是怎么注入的呢,首先要看一眼class结构 这里推荐使用IntelliJ IDEA然后装个插件叫Bytecode outline这里距离看一眼耗时的生成的class文件字节码。
这里写图片描述

左边是我们对应的java文件,右边是编译之后生成的class字节码。对于右边一般是看不懂的但是神奇的ASM就能看的懂而且提供了一系列的api供我们调用,我们只要对着编写就好了,按照上面的操作很大程度上减少了巨大的工作难度,再次感谢巴掌大神

所以我们路由框架的代码字节生成,我把整个类贴上来吧代码量不是很多:

/**
 * 生成路由class文件
 */
public class NeacyRouterWriter implements Opcodes {

    public byte[] generateClass(String pkg, HashMap<String, String> metas) {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        // 生成class类标识
        cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, pkg, null, "java/lang/Object", null);

        // 声明一个静态变量
        fv = cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "map", "Ljava/util/HashMap;", "Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
        fv.visitEnd();

        // 默认的构造函数<init>
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);

        // 生成一个getMap方法
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getMap", "()Ljava/util/HashMap;", "()Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
        mv.visitCode();
        mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
        mv.visitInsn(Opcodes.ARETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        // 将扫描到的注解生成相对应的路由表 主要写在静态代码块中
        mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
        mv.visitCode();
        mv.visitTypeInsn(Opcodes.NEW, "java/util/HashMap");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false);
        mv.visitFieldInsn(Opcodes.PUTSTATIC, pkg, "map", "Ljava/util/HashMap;");

        for (Map.Entry<String, String> entrySet : metas.entrySet()) {
            String key = entrySet.getKey();
            String value = entrySet.getValue();
            NeacyLog.log("=== key === " + key);
            NeacyLog.log("=== value === " + value);
            mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
            mv.visitLdcInsn(key);
            mv.visitLdcInsn(value);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", false);
            mv.visitInsn(Opcodes.POP);
        }
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(3, 0);
        mv.visitEnd();

        cw.visitEnd();

        return cw.toByteArray();
    }
}

然后对方法耗时的进行的代码插入主要代码有:

    @Override
    protected void onMethodEnter() {
        if (isInject) {
            NeacyLog.log("====== 开始插入方法 = " + methodName);

            /** 
            NeacyCostManager.addStartTime("xxxx", System.currentTimeMillis());
            */
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addStartTime", "(Ljava/lang/String;J)V", false);
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (isInject) {
            /** 
            NeacyCostManager.addEndTime("xxxx", System.currentTimeMillis());
            */
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addEndTime", "(Ljava/lang/String;J)V", false);

            /**
             NeacyCostManager.startCost("xxxx");
            */
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "startCost", "(Ljava/lang/String;)V", false);

            NeacyLog.log("==== 插入结束 ====");
        }
    }

基本上这样子相对应的路由表相对应的代码插入都写完,然后只需要在gradle插件中进行调用一下即可,而对于遍历目录的时候没有什么难点就是直接覆盖当前class即可:

               if (isDebug) {// 只有Debug才进行扫描const耗时
                                // 扫描耗时注解 NeacyCost
                                byte[] bytes = classWriter.toByteArray()
                                File destFile = new File(file.parentFile.absoluteFile, name)
                                project.logger.debug "========== 重新写入的位置->lastFilePath = " + destFile.getAbsolutePath()
                                FileOutputStream fileOutputStream = new FileOutputStream(destFile)
                                fileOutputStream.write(bytes)
                                fileOutputStream.close()
                            }

而对于jar遍历的时候需要做的是先拆jar然后注入代码完成之后需要再生产一个jar,所以我们需要创建一个临时地址来存放新的jar。

                    if (isDebug) {
                        // 将jar包解压后重新打包的路径
                        tempFile = new File(jarInput.file.getParent() + File.separator + "neacy_const.jar")
                        if (tempFile.exists()) {
                            tempFile.delete()
                        }
                        fos = new FileOutputStream(tempFile)
                        jarOutputStream = new JarOutputStream(fos)

                        // 省略一些代码....

ZipEntry zipEntry = new ZipEntry(entryName)
                                jarOutputStream.putNextEntry(zipEntry)
                                // 扫描耗时注解 NeacyCost
                                byte[] bytes = classWriter.toByteArray()
                                jarOutputStream.write(bytes)
                    }

这里有必要插入一个插件配置,因为对于方法耗时统计只要开发的时候debug模式下使用就好其他模式禁止使用了,这就是为什么上面有if(debugOn)的判断。
先定义一个Extension:

/**
 * 配置
 */
public class NeacyExtension {
    boolean debugOn = true

    public NeacyExtension(Project project) {

    }
}

然后在transfrom中进行读取:

    void apply(Project project) {
        this.project = project
        project.extensions.create("neacy", NeacyExtension, project)

        def android = project.extensions.getByType(AppExtension);
        android.registerTransform(this)


        project.afterEvaluate {
            def extension = project.extensions.findByName("neacy") as NeacyExtension
            def debugOn = extension.debugOn

            project.logger.error '========= debugOn = ' + debugOn

            project.android.applicationVariants.each { varient ->
                project.logger.error '======== varient Name = ' + varient.name
                if (varient.name.contains(DEBUG) && debugOn) {
                    isDebug = true
                }
            }
        }
    }

最后在build.gradle中进行配置就可以愉快的使用了..

apply plugin: com.neacy.plugin.NeacyPlugin
neacy {
    debugOn true
}

当然更多的代码可以参考demo的git库了解更多。

最后路由库要怎么让代码调用呢,这就是前面讲到的反射因为是编译生成的class无法直接调用唯有反射大法,反射会稍微影响性能所以我们一开始就直接做好这些初始化工作就可以了。

    /**
     * 初始化路由
     */
    public void initRouter() {
        try {
            Class clazz = Class.forName("com.neacy.router.NeacyProtocolManager");
            Object newInstance = clazz.newInstance();
            Field field = clazz.getField("map");
            field.setAccessible(true);
            HashMap<String, String> temps = (HashMap<String, String>) field.get(newInstance);
            if (temps != null && !temps.isEmpty()) {
                mRouters.putAll(temps);
                Log.w("Jayuchou", "=== mRouters.Size === " + mRouters.size());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根据协议找寻路由实现跳转
     */
    public void startIntent(Context context, String protocol, Bundle bundle) {
        if (TextUtils.isEmpty(protocol)) return;
        String protocolValue = mRouters.get(protocol);
        try {
            Class destClass = Class.forName(protocolValue);
            Intent intent = new Intent(context, destClass);
            if (bundle != null) {
                intent.putExtras(bundle);
            }
            context.startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最最最后,怎么使用呢?

@NeacyProtocol("Neacy://app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    @NeacyCost("MainActivity.onCreate")
    protected void onCreate(Bundle savedInstanceState) {

根据上面的注解标识之后,方法耗时就已经完成当然路由还需要哪里需要哪里传协议进行跳转就好了,当然也是一句代码的事。

NeacyRouterManager.getInstance().startIntent(TestActivity.this, "Neacy://neacymodule/NeacyModuleActivity", bundle);

这样一个完整的路由框架以及方法耗时统计V1.0版本就打完收工了。

Thanks…………
感谢巴神的文章:www.wangyuwei.me/2017/03/05/…