编写Gradle插件配合ASM框架实战

转载 2017年12月08日 00:00:00

640?wx_fmt=png&wxfrom=5&wx_lazy=1

今日科技快讯


昨日11时左右,美团服务器出现大面积崩溃,外卖订单付款出现延迟,部分用户付款后系统仍提示尚未付款;团购页面内容也无法正常显示。对此美团方面负责人回应称:因技术原因导致平台部分订单出现支付故障,经紧急修复后,现已陆续恢复,由此给用户带来的不便我们深感抱歉。针对此次故障受到影响的订单,已在陆续解决中,我们将确保故障期间用户权益不会受到任何影响。


作者简介


明天就是周六啦,提前祝大家周末愉快!

本篇文章来自 左手木亽 的投稿。分享了通过编写Gradle插件来配合ASM框架的使用,具有很强的实用价值,希望对大家有所帮助!

左手木亽 的博客地址:

/neacy_zz


前言


首先,现在世面上的项目基本上都是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插件,如何写主要参考区长:

/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 钱柜娱乐开户 = project.extensions.getByType(AppExtension); 
        钱柜娱乐开户.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) 
            } 
        }

对于代码中陌生的代码风格可以查阅这篇文章:

/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 中它会分别 visitAnnotation 和 visitMethod

@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) 判断是否是我们需要的处理的注解,像这里我们主要处理前面定义好的注解 NeacyProtocol 和 NeacyCost 两个注解就好。

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

0?wx_fmt=png

注入成功的耗时代码:

0?wx_fmt=jpeg

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

0?wx_fmt=jpeg

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

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

ddd

/** 
 * 生成路由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 钱柜娱乐开户 = project.extensions.getByType(AppExtension); 
    钱柜娱乐开户.registerTransform(this) 

    project.afterEvaluate { 
        def extension = project.extensions.findByName("neacy") as NeacyExtension 
        def debugOn = extension.debugOn 
        project.logger.error '========= debugOn = ' + debugOn 
        project.钱柜娱乐开户.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实战统计方法耗时


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

640.png?

640?wx_fmt=jpeg

Gradle强制依赖解析策略

遇到过这样一个问题: 使用第三方的插件,但是插件在定义自身依赖时,使用[version+]策略,导致永远下载最新的依赖,但是该插件所指定仓库并没有最新版本的依赖,从而导致构建失败。 ...
  • BenW1988
  • BenW1988
  • 2016年02月07日 10:12
  • 3984

升级Gradle 3.0遇到的坑

升级gradle 3.0遇到的坑
  • fengrui_sd
  • fengrui_sd
  • 2017年11月28日 18:54
  • 776

安卓开发通过自定义Gradle插件实现自动化埋点

结合gradle开发的一款自动化埋点的插件,使用简单,零代码入侵,不需要开发配合,维护一份埋点文档即可...
  • rnZuoZuo
  • rnZuoZuo
  • 2016年11月09日 14:17
  • 3033

动态生成Java字节码之java字节码框架ASM的学习

原文链接:/qq_27376871/article/details/51613066 一、什么是ASM   ASM是一个java字节码操纵框架,它能被...
  • zhushuai1221
  • zhushuai1221
  • 2016年08月10日 10:56
  • 4213

使用ASM来书写Java代码

原文地址:http://blog.sina.com.cn/s/blog_4b38e200010008to.html 小巧而神奇的ASM ASM是一套JAVA字节码生成架构。它可以动态生成二进制格式...
  • Mr__fang
  • Mr__fang
  • 2017年02月03日 13:47
  • 1719

使用钱柜娱乐开户Studio创建自定义gradle插件并被引用实战例子

钱柜娱乐开户studio自定义gradle插件并被其他项目引用
  • tiandiwuya
  • tiandiwuya
  • 2017年04月14日 18:15
  • 814

插件化 第三方框架实战

本文基于DroidPlugin实现插件化 类库使用步骤: 1. 使用PluginHelper初始化      2.实现ServiceConnection ,连接服务,并实现onServiceConne...
  • u011840744
  • u011840744
  • 2017年01月23日 13:45
  • 327

为钱柜娱乐开户 Studio编写自定义Gradle插件的教程

Google已经建议钱柜娱乐开户开发全部转向钱柜娱乐开户 Studio开发,钱柜娱乐开户 Studio 是使用gradle编译、打包的,那么问题来了,gradle可是有一堆东西...,为了彻底了...
  • LANGZI7758521
  • LANGZI7758521
  • 2016年06月27日 18:21
  • 1122

Gradle 1.12用户指南翻译——第五十八章. 编写自定义插件

Gradle 插件打包了可以复用的构建逻辑块,这些逻辑可以在不同的项目和构建中使用。Gradke 允许你实现你自己的自定义插件,因此你可以重用你的构建逻辑,并且与他人分享。 你可以使用你喜欢的任何一...
  • maosidiaoxian
  • maosidiaoxian
  • 2017年04月23日 21:44
  • 755

jQuery框架学习第十一天:实战jQuery表单验证及jQuery自动完成提示插件

jQuery框架学习第一天:开始认识jQuery jQuery框架学习第二天:jQuery中万能的选择器 jQuery框架学习第三天:如何管理jQuery包装集  jQuery框架学习第四天:使...
  • GoodShot
  • GoodShot
  • 2013年03月08日 21:11
  • 1121
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:编写Gradle插件配合ASM框架实战
举报原因:
原因补充:

(最多只允许输入30个字)