自定义Gradle插件学习笔记

这篇博客详细介绍了如何创建并使用自定义Gradle插件,从Hello World开始,逐步深入到记录编译时间、代码插入(AOP)以及使用Javassist修改字节码。通过实例展示了Gradle插件在控制编译过程和项目管理中的价值。
摘要由CSDN通过智能技术生成

插件的价值是什么

不是装逼,也不只是减少一些不太好理解和可能冗余的代码.

核心在于控制编译过程,帮助管理项目,让项目代码更少的实现细节,关注于业务逻辑解耦,管理代码边界.

写这个笔记的原因

我有一句mmp不知……网上要么是进阶教程,要么是漏东西的”入门教程”,找不到一个可以一步步完成的傻瓜教程,让平时几乎不改gradle脚本的我情何以堪,自己写一篇,让找到的人可以省些时间,也作备忘之用.

开撸

Hello World

  • Android Studio 似乎不能直接创建groovy项目,whatever,创建一个java lib module,把全部文件删完就好了~~

  • build.gradle里面的内容也是没用的,全部删掉,添加以下代码:

    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    dependencies {
      compile gradleApi()
      compile localGroovy()
    }
    
    repositories {
      mavenCentral()
    }
    
    //group和version是提供给Maven使用的,在使用maven导入插件的时候用
    group='ml.xuexin.plugins'
    version='1.0.0'
    
    uploadArchives {
      repositories {
          mavenDeployer {
              //提交到远程服务器:
              // repository(url: "http://www.xxx.com/repos") {
              //    authentication(userName: "admin", password: "admin")
              // }
              //本地的Maven地址设置为..:/repos
              repository(url: uri('../repos'))
          }
      }
    }

    很简单,groovy相关的就是必须的,maven相关的就是上传到服务器/本地的东西

  • 跟Java代码类似,新建src/main/groovy/包名/类名.groovy,实现Plugin接口:

    package ml.xuexin
    
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    class MyPlugin implements Plugin<Project> {
    
      @Override
      void apply(Project project) {
          println 'Hello World'
      }
    }

    以上代码作用就是在开始build项目的时候,打印一个Hello World而已

  • 新建resources/META-INF/gradle-plugins/插件名.properties文件

    implementation-class=ml.xuexin.MyPlugin

    这个文件名才是别的地方实际使用的名字:

    apply plugin: '插件名'
  • ok,该上传maven仓库了,用上面的build.gradle,已经可以上传了,Android Studio右边的Gradle窗口,双击upload->uploadArchives就好

  • 引用插件所在的maven库:

    根目录的build.gradle的buildscript的repositories加上(这是本地的,后续改服务器仓库)

    maven {
    url uri('./repos')
    }

    dependencies加上(前面有配置group,version,myplugin为module名)

    classpath 'ml.xuexin.plugins:myplugin:1.0.0'

  • 在想要使用插件的module应用咯,上面有写

  • 然后clean,就可以看到效果了,另外Android Studio显示gradle log的窗口有点坑,太不显眼了,名字叫Gradle Console,目前版本在右下角,如果找不到就用双shift搜索吧

稍微有一点点功能的插件,记录编译时间

  • 首先,来个找了半天的傻逼问题,这个插件应该应用到哪儿?

    根bulid.gradle对不对,我真是脑抽……

  • groovy本质是Java,所以实在不知道语法的时候,就直接打Java代码吧^_^

  • 写这么一个类:

    class TimeListener implements TaskExecutionListener, BuildListener {
    
      private long now
      private times = []
    
      @Override
      void beforeExecute(Task task) {
          now = System.currentTimeMillis()
      }
    
      @Override
      void afterExecute(Task task, TaskState taskState) {
          def ms = System.currentTimeMillis() - now
          times.add([ms, task.path])
      }
    
      void printTime() {
          long totalTime = 0
          println "耗时超过100ms的项目:"
          for (time in times) {
              totalTime += time[0]
              if (time[0] >= 100) {
                  printf "%7sms %s\n", time
              }
          }
          printf "总耗时:%.2fs\n", totalTime / 1000.0
      }
    
      @Override
      void buildStarted(Gradle gradle) {
    
      }
    
      @Override
      void settingsEvaluated(Settings settings) {
    
      }
    
      @Override
      void projectsLoaded(Gradle gradle) {
    
      }
    
      @Override
      void projectsEvaluated(Gradle gradle) {
    
      }
    
      @Override
      void buildFinished(BuildResult buildResult) {
          printTime()
      }
    }
  • ok,在插件类添加下Listener一下就完了:

    class MyPlugin implements Plugin<Project> {
    
      @Override
      void apply(Project project) {
          project.gradle.addListener(new TimeListener())
      }
    }
  • 大功告成,完成的时候会打印这样的信息:

    耗时超过100ms的项目:
      225ms :app:processDebugManifest
      177ms :mylibrary:processDebugResources
      163ms :mylibrary2:processDebugResources
     1127ms :app:processDebugResources
      247ms :mylibrary:compileDebugJavaWithJavac
      171ms :mylibrary2:compileDebugJavaWithJavac
     2815ms :app:compileDebugKotlin
      476ms :app:compileDebugJavaWithJavac
    19868ms :app:transformClassesWithDexBuilderForDebug
     3263ms :app:transformDexArchiveWithExternalLibsDexMergerForDebug
      742ms :app:transformDexArchiveWithDexMergerForDebug
      531ms :app:transformNativeLibsWithMergeJniLibsForDebug
      712ms :app:transformResourcesWithMergeJavaResForDebug
      444ms :app:packageDebug
    总耗时:31.41s

插入代码(最直接的应用场景:AOP)

Transform

  • 偷人家的图

  • 偷来的图上可以看到,编译的过程就是不断in/out的过程.transform就是中间一环啦

  • 在插件build.gradle添加依赖:

    compile 'com.android.tools.build:gradle:3.0.1'
  • 继承Transform:

    class MyTransform extends Transform {
      private Project project
    
      MyTransform(Project project) {
          super()
          this.project = project
      }
    
      //transform的名称
      //transformClassesWithMyClassTransformForDebug 运行时的名字
      //transformClassesWith + getName() + For + Debug或Release
      @Override
      String getName() {
          return this.class.simpleName
      }
    
      //需要处理的数据类型,有两种枚举类型
      //CLASSES和RESOURCES,CLASSES代表处理的java的class文件,RESOURCES代表要处理java的资源
      @Override
      Set<QualifiedContent.ContentType> getInputTypes() {
          return TransformManager.CONTENT_CLASS
      }
    
      //    指Transform要操作内容的范围,官方文档Scope有7种类型:
      //
      //    EXTERNAL_LIBRARIES        只有外部库
      //    PROJECT                   只有项目内容
      //    PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
      //    PROVIDED_ONLY             只提供本地或远程依赖项
      //    SUB_PROJECTS              只有子项目。
      //    SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
      //    TESTED_CODE               由当前变量(包括依赖项)测试的代码
      @Override
      Set<? super QualifiedContent.Scope> getScopes() {
          return TransformManager.SCOPE_FULL_PROJECT
      }
    
      //当前Transform是否支持增量编译
      @Override
      boolean isIncremental() {
          return false
      }
    
      //    Transform中的核心方法,
      //    inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
      //    outputProvider 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
      @Override
      void transform(Context context,
                     Collection<TransformInput> inputs,
                     Collection<TransformInput> referencedInputs,
                     TransformOutputProvider outputProvider,
                     boolean isIncremental) throws IOException, TransformException, InterruptedException {
    
      }
    }
  • 在插件中注册这个transform:

    project.android.registerTransform(new MyTransform(project))

    这就完成了一个只要apply就编译不过,但是很难找原因的插件233

  • 修复它:

    void transform(Context context,
                     Collection<TransformInput> inputs,
                     Collection<TransformInput> referencedInputs,
                     TransformOutputProvider outputProvider,
                     boolean isIncremental) throws IOException, TransformException, InterruptedException {
          // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
          inputs.each { TransformInput input ->
              //对类型为“文件夹”的input进行遍历
              input.directoryInputs.each { DirectoryInput directoryInput ->
                  //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
    
                  // 获取output目录
                  def dest = outputProvider.getContentLocation(directoryInput.name,
                          directoryInput.contentTypes, directoryInput.scopes,
                          Format.DIRECTORY)
    
                  // 将input的目录复制到output指定目录
                  FileUtils.copyDirectory(directoryInput.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)
              }
          }
      }

    这里用了apache的开源IO库,DigestUtils,FileUtils又需要添加依赖:

    compile group: 'commons-codec', name: 'commons-codec', version: '1.4'

    不知道代码提示什么鬼,反正我没提示,得自己手动添加import:

    import org.apache.commons.codec.digest.DigestUtils
    import org.apache.commons.io.FileUtils

    ok,绕了这么大弯,终于在插入IDE黑盒子的情况下能正确编译了,马上就是见证奇迹的时候了

Javassist

  • 这是什么呢,简单的说,就是提供傻瓜化修改字节码的开源库,有了他,我们就可以不用看着字节码一脸懵逼了,说这个做不了了,世界多美好

  • 添加依赖:

    compile 'org.javassist:javassist:3.22.0-GA'
  • 新建一个类来专门处理注入:

    package ml.xuexin.insertcode
    
    import javassist.ClassPool
    import javassist.CtClass
    import javassist.CtConstructor
    
    class MyInject {
    
      private static ClassPool pool = ClassPool.getDefault()
      private static String injectStr = "System.out.println(\"插入的代码\" ); "
    
      static void injectDir(String path, String packageName) {
          pool.appendClassPath(path)
          File dir = new File(path)
          if (dir.isDirectory()) {
              dir.eachFileRecurse { File file ->
                  String filePath = file.absolutePath
                  //确保当前文件是class文件,并且不是系统自动生成的class文件
                  if (filePath.endsWith(".class")
                          && !filePath.contains('R$')
                          && !filePath.contains('R.class')
                          && !filePath.contains("BuildConfig.class")) {
                      // 判断当前目录是否是在我们的应用包里面
                      int index = filePath.indexOf(packageName)
                      boolean isMyPackage = index != -1
                      if (isMyPackage) {
                          int end = filePath.length() - 6 // .class = 6
                          String className = filePath.substring(index, end)
                                  .replace('\\', '.').replace('/', '.')
                          //开始修改class文件
                          CtClass c = pool.getCtClass(className)
    
                          if (c.isFrozen()) {
                              c.defrost()
                          }
    
                          CtConstructor[] cts = c.getDeclaredConstructors()
                          if (cts == null || cts.length == 0) {
                              //手动创建一个构造函数
                              CtConstructor constructor = new CtConstructor(new CtClass[0], c)
                              constructor.insertBeforeBody(injectStr)
                              c.addConstructor(constructor)
                          } else {
                              cts[0].insertBeforeBody(injectStr)
                          }
                          c.writeFile(path)
                          c.detach()
                      }
                  }
              }
          }
      }
    
    
    }

  • Transform中调用:

    @Override
      void transform(Context context,
                     Collection<TransformInput> inputs,
                     Collection<TransformInput> referencedInputs,
                     TransformOutputProvider outputProvider,
                     boolean isIncremental) throws IOException, TransformException, InterruptedException {
          // Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历
          inputs.each { TransformInput input ->
              //对类型为“文件夹”的input进行遍历
              input.directoryInputs.each { DirectoryInput directoryInput ->
                  //文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等
                  MyInject.injectDir(directoryInput.file.absolutePath,"ml/xuexin/learnplugin")
                  // 获取output目录
                  def dest = outputProvider.getContentLocation(directoryInput.name,
                          directoryInput.contentTypes, directoryInput.scopes,
                          Format.DIRECTORY)
    
                  // 将input的目录复制到output指定目录
                  FileUtils.copyDirectory(directoryInput.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)
              }
          }
      }

    跟之前其实只差了一句话:

    MyInject.injectDir(directoryInput.file.absolutePath,"ml/xuexin/learnplugin")

    其中包名,看的文章用的windows,是”\\”,我是mac,换成了”/”,印象中在windows中用”/”也是可以的,待测试

  • MainActivity:

    class MainActivity : AppCompatActivity() {
    
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          Log.e("--->", "===================")
          Test()
          Log.e("--->", "===================")
      }
    }
    
    class Test
  • 见证奇迹的时刻到了:

    12-21 08:52:22.465 28669-28669/ml.xuexin.learnplugin E/--->: ===================
    12-21 08:52:22.465 28669-28669/ml.xuexin.learnplugin I/System.out: 插入的代码
    12-21 08:52:22.465 28669-28669/ml.xuexin.learnplugin E/--->: ===================

主要参考文章:

http://blog.csdn.net/huachao1001/article/details/51819972

http://blog.csdn.net/yulong0809/article/details/77752098

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值