Android Gradle3-自定义Plugin实践

因为要做一个无埋点收集数据的功能,需要自定义一个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();
                }
            }

源码

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