Android热修复和插件化Android技术知识Android知识

Android Gradle3-自定义Plugin实践

2017-08-01  本文已影响233人  shawn_yy

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

源码

上一篇下一篇

猜你喜欢

热点阅读