AndroidStudio中的Gradle插件开发简介

  • 内容
  • 评论
  • 相关

之前我们介绍了HiBeaver的使用,我们发现这个插件可以做很多有意思的事情。那么我们如何自己来实现一个这样的插件功能呢?下面我们就来探索一下Gradle插件的真相。

先来新建一个工程。通常情况下我们会得到以下这些基础的代码:

package com.velsharoon.gradleplugin;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}
apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.velsharoon.gradleplugin"
        minSdkVersion 14
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

现在我们先不管他们,先来想想我们要干什么。我们要自己编写一个插件,这个插件可以在MainActivity启动时弹出一句我们想要显示的话。接下来我们就来进行这个插件的开发。

默认工程已经有了一个app的module,现在我们来开发插件,新建一个自己的module名字叫mylibrary。

创建什么类型的module不重要,重要的是改成下面的目录结构:

其中velsharoon.properties的velsharoon是我们这个插件的名字,可以随意起名字。我们来看一下这个文件中需要写点什么:

implementation-class=com.velsharoon.MyPlugin

可以看到,这个properties里添加的就是我们插件的入口的文件,这里我们写做com.velsharoon.MyPlugin,所以现在我们需要创建MyPlugin这个类。当然了,我们需要说明的是,插件是用groovy写的!!!

package com.velsharoon

import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        project.extensions.create(MyExtension.EXTENSION_NAME, MyExtension)
        //AppExtension就是build.gradle中android{...}这一块
        def android = project.extensions.getByType(AppExtension)
        android.registerTransform(new MyClassTransform(project))
        //project.afterEvaluate {
          //  project.tasks.create(MyTask.NAME,MyTask)
        //}
    }
}

我们来看一下上面都写了啥。

MyPlugin实现了Plugin<Project>接口,这个是插件入口必须实现的方法。之后插件运行,系统会回调apply方法,并把Project参数传进来,有了这个参数就可以干很多事情了。

MyExtension是一个类,它内部有一个EXTENSION_NAME的常量,这个常量的值就是我们之后要在app这个module中的build.gradle文件中写的块,我们现在先记下这个。这个类如下:

package com.velsharoon

class MyExtension {

    static final def EXTENSION_NAME = 'injectConfig'

    String message = 'none'
}

其中message是我们默认提示的文案,之后会用到它。

MyPlugin的注释也写明了def android = project.extensions.getByType(AppExtension),其中AppExtension就是app的module中build.gradle中android{…}这一块。android.registerTransform(new MyClassTransform(project)),这样我们就可以在MyClassTransform这个类中接收到输入的数据了。一会我们重点说这个类。接着我们可以看到MyPlugin中注释了三行代码,这里是说,所有的东西完成后,我们可以继续下一个任务了,由于本次示例没有这个需求,所以先注释掉了,最后我们可以再演示一下。

现在我们来看MyClassTransform类:

package com.velsharoon

import com.android.build.api.transform.Context
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.api.transform.Format
import com.android.build.gradle.internal.pipeline.TransformManager
import org.gradle.api.Project
import org.apache.commons.io.FileUtils
import org.apache.commons.codec.digest.DigestUtils

class MyClassTransform extends Transform {

    private Project mProject

    MyClassTransform(Project p) {
        this.mProject = p
    }

    //transform的名称
    //transformClassesWithMyClassTransformForDebug 运行时的名字
    //transformClassesWith + getName() + For + Debug或Release
    //该方法表示当前Transform在task列表中的名字,
    //返回值最终经过一系列的拼接,具体拼接实现在TransformManager的getTaskNamePrefix()方法中,
    //拼接格式:transform${InputType1}And${InputType2}And${InputTypeN}And${name}For${flavor}${BuildType}
    @Override
    String getName() {
        return "MyClassTransform"
    }

    //需要处理的数据类型,有两种枚举类型
    //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<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 {
        def message = mProject[MyExtension.EXTENSION_NAME].message
        //遍历input
        inputs.each { TransformInput input ->
            //遍历文件夹
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //注入代码
                MyInjects.inject(directoryInput.file.absolutePath, message, mProject)
                // 获取output目录
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
            //遍历jar文件 对jar不操作,但是要输出到out路径
            input.jarInputs.each { JarInput jarInput ->
                // 重命名输出文件(同目录copyFile会冲突)
                def jarName = jarInput.name
                println("jar = " + jarInput.file.getAbsolutePath())
                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)
            }
        }
    }

}

可以看到MyClassTransform类继承自Transform类,并且重写了部分方法。注释很清楚了,我们来看一下transform方法,其中有一段这样的:

MyInjects.inject(directoryInput.file.absolutePath, message, mProject)

MyInjects类是我们自己写的注入的类,这个类负责的真正的代码插桩的任务。其代码如下:

package com.velsharoon

import javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import org.gradle.api.Project

class MyInjects {

    static void inject(String path, String message, Project project) {
        ClassPool pool = ClassPool.getDefault()
        //将当前路径加入类池,不然找不到这个类
        pool.appendClassPath(path)
        //project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        pool.appendClassPath(project.android.bootClasspath[0].toString())
        //引入android.os.Bundle包,因为onCreate方法参数有Bundle
        pool.importPackage("android.os.Bundle")

        File dir = new File(path)
        if (dir.isDirectory()) {
            //遍历文件夹
            dir.eachFileRecurse { File file ->
                if (file.getName().equals("MainActivity.class")) {
                    CtClass ctClass = pool.getCtClass("com.velsharoon.gradleplugin.MainActivity")
                    //解冻
                    if (ctClass.isFrozen()){
                        ctClass.defrost()
                    }
                    CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
                    String insetBeforeStr = """ android.widget.Toast.makeText(this,"${message}",android.widget.Toast.LENGTH_SHORT).show();"""
                    ctMethod.insertBefore(insetBeforeStr)
                    ctClass.writeFile(path)
                    ctClass.detach()
                }
            }
        }

    }
}

是不是很眼熟了?对,用的就是我们之前说到的javassist工具。经过这段代码后,MainActivity的onCreate方法中就有Toast相关的代码了。

最后,我们需要在我们创建的这个module中的build.gradle中添加需要的内容(当然,这部分实际可以放到最一开始写,这样更好):

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    mavenCentral()
}

dependencies {
    compile gradleApi()
    compile localGroovy()

    compile 'com.android.tools.build:gradle:3.1.4'
    compile 'com.android.tools.build:transform-api:1.5.0'
    compile 'org.javassist:javassist:3.22.0-GA'
    compile 'org.lucee:commons-io:2.6.0'
}

group='com.velsharoon'
version='1.0.0'

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: uri('../repo'))
        }
    }
}

其中group是我们这个插件对应的组,也就是我们插件下载父路径,如果我们以后有更多的插件,可以用同一个组,例如com.velsharoon:a,com.velsharoon:b等等。version就是本插件的版本。如上写的话我们导入这个插件时就这样写:com.velsharoon:mylibrary:1.0.0,其中com.velsharoon是父路径,mylibrary子路径,1.0.0是版本,这些合起来就组成了我们插件的全路径。

通常来说,我们的插件都会上传到maven或者jcenter,本例是为了演示用的,所以就不上传了,我们通过uploadArchives将本次的插件到出到本地,路径即为repo,它于本module处于同一级目录。这些都准备齐全后大概就是这个样子:

我们同步一下插件module或工程的gradle,即可生成repo结果。

之后我们使用的插件的来源就是这里了,如果没有猜错的话,上传到maven或者jcenter应该也是这样的结构。

接下来就是如何使用这个东西了。

在我们app的module下的build.gradle中添加一些内容:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.velsharoon.gradleplugin"
        minSdkVersion 14
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}
//-------以下是添加的部分---------
apply plugin: 'velsharoon'
injectConfig {
    message 'has been injected'
}
buildscript {
    repositories {
        maven {
            url uri('../repo')
        }
    }
    dependencies {
        classpath 'com.velsharoon:mylibrary:1.0.0'
    }
}

如上,apply plugin: 'velsharoon'是应用我们的插件,injectConfig就是我们前面MyExtension内的变量,message即我们要提示的文案。下面repositories中的maven的url指定了仓库的位置,通常我们都是网络的位置,这里我们写的是本地的位置。最后dependencies中的classpath就是我们之前说过的插件路径。

这时候编译工程,运行apk,就可以看到MainActivity弹出Toast了。

最后我们提供一下上面讲到的MyTask,这里给一些示例代码,方便以后参考。

package com.velsharoon

import com.android.build.gradle.AppExtension
import com.android.build.gradle.AppPlugin
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class MyTask extends DefaultTask {

    static final String NAME = "injectTask"

    @TaskAction
    def start() {
        if (project.plugins.hasPlugin(AppPlugin)) {
            def android = project.extensions.getByType(AppExtension)
            android.applicationVariants.all { variant ->
                //获取到scope,作用域
                def variantData = variant.variantData
                def scope = variantData.scope
                //创建一个task
                def createTaskName = scope.getTaskName("CeShi", "MyTestPlugin")
                def createTask = project.task(createTaskName)
                //设置task要执行的任务
                createTask.doLast {
//                    def message = project[MyExtension.EXTENSION_NAME]
                    def injectConfig = project.extensions.getByName(MyExtension.EXTENSION_NAME)
                    def content = """package com.velsharoon.plugindemo;
                        public class MyPluginTestClass {
                            public static final String str = "${injectConfig.message}";
                        }
                        """
                    //获取到BuildConfig类的路径
                    File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
                    def javaFile = new File(outputDir, "MyPluginTestClass.java")
                    javaFile.write(content, 'UTF-8')
                }
                //设置task依赖于生成BuildConfig的task,然后在生成BuildConfig后生成我们的类
                String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
                def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                if (generateBuildConfigTask) {
                    createTask.dependsOn generateBuildConfigTask
                    generateBuildConfigTask.finalizedBy createTask
                }
            }
        }
    }
}

评论

0条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注