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

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

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

思路

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插件,如何写主要参考区长的http://blog.csdn.net/sbsujjbcy/article/details/50782830,按着步骤就好,假设我们看完了并设置好了那么就有一个雏形了:

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)
            }
        }

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

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

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............
感谢巴神的文章:http://www.wangyuwei.me/2017/03/05/ASM实战统计方法耗时/#more

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,046评论 25 707
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,678评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,494评论 18 139
  • 先上demo地址:https://github.com/JeasonWong/CostTime 需求 实际业务开发...
    神来一巴掌阅读 5,126评论 10 9
  • 准备好基本的知识之后正式参与百人计划,与其说是组织的,不如说是自己对自己的一个要求计划的启动。一直以来,做一...
    肖聖钦阅读 264评论 2 5