Android Gradle3-自定义Plugin实践

作者: shawn_yy | 来源:发表于2017-08-01 19:25 被阅读233次

    因为要做一个无埋点收集数据的功能,需要自定义一个Plugin,搜到的方法大部分都是打印一个HelloWorld,没有任何的参考价值,所以详细记录一下过程。
    如果想对编译的class文件进行字节码注入,hook是一种方式,但是gradle1.5之后android gradle插件也可以通过自定义一个Plugin,调用这段代码来注册一个Transform。

     class GatherPlugin implements Plugin<Project> {
    
        @Override
        void apply(Project project) {
            def android = project.extensions.findByType(AppExtension)
            android.registerTransform(new GatherTransform(project))
        }
    }
    

    Transform是一个抽象类,通过继承这个类可以对字节码进行修改。为了弄这个,经过有些麻烦,踩了一些gradle的坑,特意记录一下。
    整个过程分为下面几步
    创建一个Groovy模块
    创建一个GatherPlugin
    创建一个GatherTransform
    利用ASM扫描所有的类文件,然后在指定地方插入代码

    这个是Gradle的API,方便查看

    创建一个Groovy模块
    • 创建一个Groovy项目
      可以通过创建一个lib项目把里面的文件都删了,处理build.gradle和放源码的目录。
      这里的如果创建本工程自己用的插件文件的目录名字必须是buildSrc,先以本工程用的插件为例。


      Snip20170801_3.png
    • 修改build.gradle文件脚本代码

    apply plugin: 'groovy'
    
    //上传插件到仓库需要 非必要
    apply plugin: 'maven'
    
    dependencies {
        compile gradleApi()//gradle sdk
        compile localGroovy()//groovy sdk
    
        compile 'com.android.tools.build:gradle:2.3.1'
    
        compile 'org.ow2.asm:asm:5.0.3'
        compile 'org.ow2.asm:asm-commons:5.0.3'
    
    }
    
    repositories {
        jcenter()
        mavenCentral()
    }
    
    有个坑
    • jackOptions 为true 会导致自定义的Transform 不能执行
    • 创建的文件必须要以.groovy 为后缀,否则在其他文件中引用会语法错误
    创建GatherPlugin和GatherTransform

    这个很简单

    GatherPlugin.groovy文件,文件后缀一定要有groovy

    class GatherPlugin implements Plugin<Project> {
    
        @Override
        void apply(Project project) {
            def android = project.extensions.findByType(AppExtension)
            android.registerTransform(new GatherTransform(project))
        }
    }
    

    在项目的gradle.build文件里引用插件

    apply plugin: 'com.android.application'
    apply plugin: com.cyy.gather.GatherPlugin
    .....
    

    GatherTransform.groovy文件

    public class GatherTransform extends Transform{
    
       Project project
    
       // 构造函数,我们将Project保存下来备用
       public GatherTransform(Project project) {
           this.project = project
       }
    
       // 设置我们自定义的Transform对应的Task名称
       @Override
       String getName() {
           return "GatherTransform"
       }
    
       // 指定输入的类型,通过这里的设定,可以指定我们要处理的文件类型
       //这样确保其他类型的文件不会传入
       @Override
       Set<QualifiedContent.ContentType> getInputTypes() {
           return TransformManager.CONTENT_CLASS
       }
    
       // 指定Transform的作用范围
       @Override
       Set<QualifiedContent.Scope> getScopes() {
           return TransformManager.SCOPE_FULL_PROJECT
       }
    
       @Override
       boolean isIncremental() {
           return false
       }
    
       @Override
       void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
           super.transform(transformInvocation)
           println(" transform transform ")
       }
    
       @Override
       void transform(Context context, Collection<TransformInput> inputs,
                      Collection<TransformInput> referencedInputs,
                      TransformOutputProvider outputProvider, boolean isIncremental)
               throws IOException, TransformException, InterruptedException {
    
           /**
            * Transform的inputs有两种类型,
            *  一种是目录, DirectoryInput
            *  一种是jar包,JarInput
            *  要分开遍历
            */
           inputs.each { TransformInput input ->
               /**
                * 对类型为“文件夹”的input进行遍历
                */
               input.directoryInputs.each {
                   /**
                    * 文件夹里面包含的是
                    *  我们手写的类
                    *  R.class、
                    *  BuildConfig.class
                    *  R$XXX.class
                    *  等
                    *  根据自己的需要对应处理
                    */
                   println("it == ${it}")
    
                   //注入代码
                   Inject.injectOnClick(it.file.absolutePath)
                   // 获取output目录
                   def dest = outputProvider.getContentLocation(it.name,
                           it.contentTypes, it.scopes,
                           Format.DIRECTORY)
    
                   // 将input的目录复制到output指定目录
                   FileUtils.copyDirectory(it.file, dest)
               }
               //对类型为jar文件的input进行遍历
               input.jarInputs.each { JarInput jarInput ->
    
                   //jar文件一般是第三方依赖库jar文件
    
                   // 重命名输出文件(同目录copyFile会冲突)
                   def jarName = jarInput.name
                   def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                   if (jarName.endsWith(".jar")) {
                       jarName = jarName.substring(0, jarName.length() - 4)
                   }
                   //生成输出路径
                   def dest = outputProvider.getContentLocation(jarName + md5Name,
                           jarInput.contentTypes, jarInput.scopes, Format.JAR)
                   //将输入内容复制到输出
                   FileUtils.copyFile(jarInput.file, dest)
               }
           }
    
       }
    }
    

    这样整个插件就可以运行了。

    利用ASM扫描所有的类文件,然后在指定地方插入代码

    在制定的代码区域注入指定代码主要在Inject,groovy中完成的,这个代码主要就是怎么用Groovy,所以没有贴。

    我是第一次用ASM,对ASM的语法一点不懂,出了很多问题。看了很多的例子代码,基本上都是注入一个输出HelloWorld,属于没有一点参考价值的。

    当然我们只是做一个插件没有必要去花时间去学习ASM,这个东西要学习也不是一天两天的事,踩很多坑之后找到一个工具,非常好用。一个Studio插件 ASM Bytecode Outline , 下载后解压,将复制Studio的图片中的目录,然后重启Studo

    Snip20170801_5.png
    这个插件使用很简单,重启后Studio左边会出现如图所示
    Snip20170801_6.png

    鼠标右击你的某一个类。


    Snip20170801_8.png

    然后就会把你这个类的代码全部转化成ASM语法格式的。66666。如果不会写ASM的语法,把你的代码在一个测试类中先写好,然后利用ASM生成出对应的ASM语法,在把代码copy到Inject.groovy中即可。
    例如GatherClassVisitor.groovy文件中这些代码都是通过这个工具生产的

    methodList.each {
                    if (it == "onResume" || it == "onPause"){
                        MethodVisitor mv = cv.visitMethod(ACC_PUBLIC , it, "()V", null, null)
                        mv.visitVarInsn(ALOAD, 0)
                        mv.visitMethodInsn(INVOKESPECIAL, superName, it, "()V", false)
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitInsn(it == "onResume" ? ICONST_1 : ICONST_0);
                        mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentResumeOrPause", "(Landroid/support/v4/app/Fragment;Z)V", false);
                        mv.visitInsn(RETURN)
                        mv.visitMaxs(1, 1)
                        mv.visitEnd()
                    } else if (it == "onHiddenChanged"){
                        MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "onHiddenChanged", "(Z)V", null, null)
                        mv.visitCode()
                        mv.visitVarInsn(ALOAD, 0)
                        mv.visitVarInsn(ILOAD, 1)
                        mv.visitMethodInsn(INVOKESPECIAL, superName, "onHiddenChanged", "(Z)V", false)
                        mv.visitVarInsn(ALOAD, 0)
                        mv.visitVarInsn(ILOAD, 1);
                        mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onHiddenChanged", "(Landroid/support/v4/app/Fragment;Z)V", false);
                        mv.visitInsn(RETURN)
                        mv.visitMaxs(2, 2)
                        mv.visitEnd()
                    }else if (it == "onViewCreated"){
                        MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "onViewCreated", "(Landroid/view/View;Landroid/os/Bundle;)V", null, null);
                        mv.visitCode();
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitVarInsn(ALOAD, 1);
                        mv.visitVarInsn(ALOAD, 2);
                        mv.visitMethodInsn(INVOKESPECIAL, superName, "onViewCreated", "(Landroid/view/View;Landroid/os/Bundle;)V", false);
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitVarInsn(ALOAD, 1);
                        mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentCreatedView", "(Landroid/support/v4/app/Fragment;Landroid/view/View;)V", false);
                        mv.visitInsn(RETURN);
                        mv.visitMaxs(3, 3);
                        mv.visitEnd();
                    }else if (it == ""){
                        MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "setUserVisibleHint", "(Z)V", null, null);
                        mv.visitCode();
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitVarInsn(ILOAD, 1);
                        mv.visitMethodInsn(INVOKESPECIAL, superName, "setUserVisibleHint", "(Z)V", false);
                        mv.visitVarInsn(ALOAD, 0);
                        mv.visitVarInsn(ILOAD, 1);
                        mv.visitMethodInsn(INVOKESTATIC, INJECT_OWNER, "onFragmentSettUserVisibleHint", "(Landroid/support/v4/app/Fragment;Z)V", false);
    
                        mv.visitInsn(RETURN);
                        mv.visitMaxs(2, 2);
                        mv.visitEnd();
                    }
                }
    

    源码

    相关文章

      网友评论

      • 48aef81ea7ef:请问有没有demo的链接,我参考下,麻烦了
        48aef81ea7ef:@shawn_yy 已学习完毕,非常感谢。关于具体的注入代码部分,也可以使用javassist,它对各种虚拟机指令和字节码操作做了一层封装,功能也很强大,用起来很方便。
        shawn_yy:@nobodyZheng 这几天有点忙,刚看到。https://github.com/cyuanyang/Discipline/tree/master/CyyPlugin,这个是比较早的一个demo,脚本在buildSrc下

      本文标题:Android Gradle3-自定义Plugin实践

      本文链接:https://www.haomeiwen.com/subject/hgjhkxtx.html