Android Studio 自定义Gradle Plugin

作者: 蹲街式等待 | 来源:发表于2019-07-19 11:24 被阅读19次

    一、简介

    之前公司的一个项目需要用到Gradle插件来修改编译后的class文件,今天有时间就拿出来整理一下,学习一下Gradle插件的编写还是一件十分有意义的事。

    二、Gradle插件类型

    • 一种是直接在项目中的gradle文件里编写,这种方式的缺点是无法复用插件代码,在其他项目中还得复制一遍代码(或者说说复制一遍文件)

    • 另一种是在独立的项目里编写插件,然后发布到中央仓库,之后直接引用就可以了,优点就是可复用。

    今天我们主要来讲解下第二种。

    三、Gradle插件

    Gradle插件是使用Groovy进行开发的,而Groovy其实是可以兼容Java的。Android Studio其实除了开发Android App外,完全可以胜任开发Gradle插件这一工作,下面来讲讲具体如何开发。

    1、创建插件步骤

    第一步:新建一个Android工程

    第二步:在该工程中新建一个Android Module项目,类型选择Android Library

    创建Module
    第三步:将Module里的内容删除,只保留build.gradle文件和src/main目录,同时移除build.gradle文件里的内容

    第四步:建立Gradle插件目录

    由于gradle是基于groovy,因此,我们开发的gradle插件相当于一个groovy项目。所以需要在main目录下新建groovy目录,这时候groovy文件夹会被Android识别为groovy源码目录。除了在main目录下新建groovy目录外,你还要在main目录下新建resources目录,同理resources目录会被自动识别为资源文件夹。在groovy目录下新建项目包名,就像Java包名那样。resources目录下新建文件夹META-INFMETA-INF文件夹下新建gradle-plugins文件夹。这样,就完成了gradle 插件的项目的整体搭建。目前项目的结构是这样的:

    插件目录结构

    第五步:修改build.gradle文件

    内容如下:

    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    dependencies{
        // gradle sdk
        compile gradleApi()
        // groovy sdk
        compile localGroovy()
        compile 'com.android.tools.build:gradle:1.5.0'
    }
    
    repositories{
        mavenCentral()
    }
    

    第六步:在com.davisplugins包名下通过new -> file ->创建PluginImpl.groovy文件

    内容如下:

    package com.davisplugins
    
    import com.android.build.gradle.AppExtension
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    
    public class PluginImpl implements Plugin<Project>{
    
        void apply(Project project){
            System.out.println("========================");
            System.out.println("hello gradle plugin!");
            System.out.println("========================");
        }
    }
    

    第七步:定义插件名称

    resources/META-INF/gradle-plugins目录下新建一个properties文件,注意该文件的命名就是你使用插件的名字,这里命名为davis.properties,那么你在其他build.gradle文件中使用自定义的插件时候则需写成:

    apply plugin: 'davis'
    

    davis.properties文件内容:

    implementation-class=com.davisplugins.PluginImpl
    

    注意包名需要替换为你自己的包名。

    现在你的目录结构如下:


    插件目录结构

    2、插件发布

    前面我们已经自定义好了插件,接下来就是要打包到Maven库里面去了,你可以选择打包到本地,或者是远程服务器中。

    (1)打包到本地Maven仓库

    在我们自定义Module目录下的build.gradle添加如下代码:

    uploadArchives {
        repositories {
            mavenDeployer {
                pom.groupId = 'com.davisplugins'
                pom.artifactId = 'davis'
                pom.version = 1.0
                // maven本地仓库的目录
                repository(url: uri('../DavisPlugin'))
            }
        }
    }
    

    这时候,右侧的gradle Toolbar就会在module下多出一个task

    Task任务
    点击uploadArchives这个Task,就会在项目下多出一个DavisPlugin目录,里面存着这个gradle插件。
    本地插件包

    (2)发布到远程Jcenter仓库

    内容更新中...

    3、插件的使用

    我们来看下,发布到本地maven仓库的插件如何使用,在项目根目录下的gradle.build的文件中加入:

    buildscript {
        repositories {
            // maven插件目录
            maven{
                url uri('DavisPlugin')
            }
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:2.1.0'
            // 使用自定义插件
            classpath 'com.davisplugins:davis:1.0'
        }
    }
    
    allprojects {
        repositories {
            jcenter()
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    

    app目录下的build.gradle文件中加入:

    apply plugin: 'davis'
    

    然后我们就可以使用该插件了,执行一次打包命令看看会发生啥吧!


    日志输出信息

    在打包之前输出了我们打印的日志信息。

    4、最佳实践

    (1)修改编译后的class文件

    我们回到如何修改class文件,首先我们得知道什么时候编译完成,并且我们要赶在class文件被转化为dex文件之前去修改。从1.5.0-beta1开始,android的gradle插件引入了com.android.build.api.transform.Transform,可以点击 http://tools.android.com/tech-docs/new-build-system/transform-api 查看相关内容。Transform每次都是将一个输入进行处理,然后将处理结果输出,而输出的结果将会作为另一个Transform的输入,过程如下:

    Transform输入输出
    注意:输出地址不是由你任意指定的。而是根据输入的内容、作用范围等由TransformOutputProvider生成,比如,你要获取输出路径:
    String dest = outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, 
                            directoryInput.scopes, 
                            Format.DIRECTORY)
    

    Transform是一个抽象类,我们先自定义一个Transform,如下:

    package com.davisplugins
    
    import com.android.build.api.transform.*
    import com.android.build.gradle.internal.pipeline.TransformManager
    import org.apache.commons.codec.digest.DigestUtils
    import org.apache.commons.io.FileUtils
    
    public class InsertTransform extends Transform {
    
        //设置我们自定义的Transform对应的Task名称
        @Override
        String getName() {
            return "DavisPlugin"
        }
    
        //指定输入的类型,通过这里设定,可以指定我们要处理的文件类型
        //这样确保其他类型的文件不会传入
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        //指定Transfrom的作用范围
        @Override
        Set<QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        @Override
        boolean isIncremental() {
            return false
        }
    
        @Override
        void transform(Context context, Collection<TransformInput> inputs,
                       Collection<TransformInput> referencedInputs,
                       TransformOutputProvider outputProvider,
                       boolean isIncremental) throws IOException,
                       TransformException, InterruptedException {
            
        }
    }
    

    看到函数transform,我们还没有具体实现这个函数。这个函数就是具体如何处理输入和输出。可以运行一下看看,注意,这里的运行时直接编译执行我们的apk,而不是像之前那样直接rebuild,因为rebuild并没有执行到编译这一步。由于我们没有实现transform这个函数,导致没有输出!使得整个过程中断了!最终导致apk运行时找不到MainActivity,所以会报错。接下来我们去实现以下这个函数,我们啥也不干,就是把输入内容写入到作为输出内容,不做任何处理:

        @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等
                    // 获取output目录
                    def dest = outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, directoryInput.scopes,
                            Format.DIRECTORY)
                    //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
                    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)
                    }
                    //生成输出路径 + md5Name
                    def dest = outputProvider.getContentLocation(jarName + md5Name,
                            jarInput.contentTypes, jarInput.scopes, Format.JAR)
    
                    //这里执行字节码的注入,不操作字节码的话也要将输入路径拷贝到输出路径
                    FileUtils.copyFile(jarInput.file, dest)
                }
            }
        }
    

    注意input的类型,分为“文件夹”和“jar文件”,”文件夹”里面的就是我们写的类对应的class文件,jar文件一般为第三方库。此时,能成功运行,但是这里我们没有注入任何代码。

    Transform类我们实现了,那么如何调用的呢?调用方式如下:

    public class PluginImpl implements Plugin<Project>{
    
        void apply(Project project){
            def android = project.extensions.findByType(AppExtension);
            android.registerTransform(new InsertTransform())
        }
    }
    

    (2)监控每一个Task任务执行

    在我们的工程目录中我们可以看到还有一个TaskListener.groovy类,内容如下:

    package com.davisplugins
    
    import org.gradle.BuildListener
    import org.gradle.BuildResult
    import org.gradle.api.Task
    import org.gradle.api.execution.TaskExecutionListener
    import org.gradle.api.initialization.Settings
    import org.gradle.api.invocation.Gradle
    import org.gradle.api.tasks.TaskState
    
    public class TaskListener implements TaskExecutionListener, BuildListener {
    
        private static final String TAG = "[DAVIS] ";
        /**
         * 此类可以监控每一个task的执行开始和结束,以及工程build的情况
         */
        public TaskListener(){
    
        }
    
        @Override
        void beforeExecute(Task task) {
            println(TAG + "task before : " + task.getName())
        }
    
        /**
         * 比如,我们要在packageRelease这个task任务执行完后,做一些操作,
         * 我们就可以在此方法中判断
         * @param task
         * @param taskState
         */
        @Override
        void afterExecute(Task task, TaskState taskState) {
            println(TAG + "task after : " + task.getName())
            if(task.getName().equals("packageRelease")){
                //做自己的任务
            }
        }
    
        @Override
        void buildFinished(BuildResult result) {
            //项目build完成之后,会调用此方法
            println(TAG + "build finished.")
        }
    
        @Override
        void buildStarted(Gradle gradle) {
            println(TAG + "build started.")
        }
    
        @Override
        void projectsEvaluated(Gradle gradle) {
            println(TAG + "project evaluated.")
        }
    
        @Override
        void projectsLoaded(Gradle gradle) {
            println(TAG + "project loaded.")
        }
    
        @Override
        void settingsEvaluated(Settings settings) {
            println(TAG + "setting evaluated.")
        }
    }
    

    调用方式:

    public class PluginImpl implements Plugin<Project>{
    
        void apply(Project project){
            project.gradle.addListener(new TaskListener())
        }
    }
    

    这个类是做啥用的呢,此类可以用来监控每一个Task任务的执行情况,比如我们在打apk包的过程中,其实就是调用了一连串的Task任务。下面是我们在未使用插件的情况下打一个release包过程中Gradle Console输出的日志:

    Executing tasks: [:app:assembleRelease]
    
    Configuration on demand is an incubating feature.
    Incremental java compilation is an incubating feature.
    :app:preBuild UP-TO-DATE
    :app:preReleaseBuild UP-TO-DATE
    :app:checkReleaseManifest
    :app:prepareReleaseDependencies
    :app:compileReleaseAidl UP-TO-DATE
    :app:compileReleaseRenderscript UP-TO-DATE
    :app:generateReleaseBuildConfig UP-TO-DATE
    :app:mergeReleaseShaders UP-TO-DATE
    :app:compileReleaseShaders UP-TO-DATE
    :app:generateReleaseAssets UP-TO-DATE
    :app:mergeReleaseAssets UP-TO-DATE
    :app:generateReleaseResValues UP-TO-DATE
    :app:generateReleaseResources UP-TO-DATE
    :app:mergeReleaseResources UP-TO-DATE
    :app:processReleaseManifest UP-TO-DATE
    :app:processReleaseResources UP-TO-DATE
    :app:generateReleaseSources UP-TO-DATE
    :app:incrementalReleaseJavaCompilationSafeguard UP-TO-DATE
    :app:compileReleaseJavaWithJavac UP-TO-DATE
    :app:compileReleaseNdk UP-TO-DATE
    :app:compileReleaseSources UP-TO-DATE
    :app:lintVitalRelease
    :app:prePackageMarkerForRelease
    :app:transformClassesWithDexForRelease
    To run dex in process, the Gradle daemon needs a larger heap.
    It currently has approximately 1365 MB.
    For faster builds, increase the maximum heap size for the Gradle daemon to more than 2048 MB.
    To do this set org.gradle.jvmargs=-Xmx2048M in the project gradle.properties.
    For more information see https://docs.gradle.org/current/userguide/build_environment.html
    :app:mergeReleaseJniLibFolders UP-TO-DATE
    :app:transformNative_libsWithMergeJniLibsForRelease UP-TO-DATE
    :app:processReleaseJavaRes UP-TO-DATE
    :app:transformResourcesWithMergeJavaResForRelease UP-TO-DATE
    :app:validateExternalOverrideSigning
    :app:packageRelease UP-TO-DATE
    :app:zipalignRelease UP-TO-DATE
    :app:assembleRelease
    
    BUILD SUCCESSFUL
    
    Total time: 5.557 secs
    
    

    那么我们使用了该插件之后输出的日志是怎样的那,如下:

    Executing tasks: [:app:assembleRelease]
    
    Configuration on demand is an incubating feature.
    Incremental java compilation is an incubating feature.
    [DAVIS] project evaluated.
    :app:preBuild
    [DAVIS] task before : preBuild
    :app:preBuild UP-TO-DATE
    [DAVIS] task after : preBuild
    :app:preReleaseBuild
    [DAVIS] task before : preReleaseBuild
    :app:preReleaseBuild UP-TO-DATE
    [DAVIS] task after : preReleaseBuild
    :app:checkReleaseManifest
    [DAVIS] task before : checkReleaseManifest
    [DAVIS] task after : checkReleaseManifest
    :app:prepareReleaseDependencies
    [DAVIS] task before : prepareReleaseDependencies
    [DAVIS] task after : prepareReleaseDependencies
    :app:compileReleaseAidl
    [DAVIS] task before : compileReleaseAidl
    :app:compileReleaseAidl UP-TO-DATE
    [DAVIS] task after : compileReleaseAidl
    :app:compileReleaseRenderscript
    [DAVIS] task before : compileReleaseRenderscript
    :app:compileReleaseRenderscript UP-TO-DATE
    [DAVIS] task after : compileReleaseRenderscript
    :app:generateReleaseBuildConfig
    [DAVIS] task before : generateReleaseBuildConfig
    :app:generateReleaseBuildConfig UP-TO-DATE
    [DAVIS] task after : generateReleaseBuildConfig
    :app:mergeReleaseShaders
    [DAVIS] task before : mergeReleaseShaders
    :app:mergeReleaseShaders UP-TO-DATE
    [DAVIS] task after : mergeReleaseShaders
    :app:compileReleaseShaders
    [DAVIS] task before : compileReleaseShaders
    :app:compileReleaseShaders UP-TO-DATE
    [DAVIS] task after : compileReleaseShaders
    :app:generateReleaseAssets
    [DAVIS] task before : generateReleaseAssets
    :app:generateReleaseAssets UP-TO-DATE
    [DAVIS] task after : generateReleaseAssets
    :app:mergeReleaseAssets
    [DAVIS] task before : mergeReleaseAssets
    :app:mergeReleaseAssets UP-TO-DATE
    [DAVIS] task after : mergeReleaseAssets
    :app:generateReleaseResValues
    [DAVIS] task before : generateReleaseResValues
    :app:generateReleaseResValues UP-TO-DATE
    [DAVIS] task after : generateReleaseResValues
    :app:generateReleaseResources
    [DAVIS] task before : generateReleaseResources
    :app:generateReleaseResources UP-TO-DATE
    [DAVIS] task after : generateReleaseResources
    :app:mergeReleaseResources
    [DAVIS] task before : mergeReleaseResources
    :app:mergeReleaseResources UP-TO-DATE
    [DAVIS] task after : mergeReleaseResources
    :app:processReleaseManifest
    [DAVIS] task before : processReleaseManifest
    :app:processReleaseManifest UP-TO-DATE
    [DAVIS] task after : processReleaseManifest
    :app:processReleaseResources
    [DAVIS] task before : processReleaseResources
    :app:processReleaseResources UP-TO-DATE
    [DAVIS] task after : processReleaseResources
    :app:generateReleaseSources
    [DAVIS] task before : generateReleaseSources
    :app:generateReleaseSources UP-TO-DATE
    [DAVIS] task after : generateReleaseSources
    :app:incrementalReleaseJavaCompilationSafeguard
    [DAVIS] task before : incrementalReleaseJavaCompilationSafeguard
    :app:incrementalReleaseJavaCompilationSafeguard UP-TO-DATE
    [DAVIS] task after : incrementalReleaseJavaCompilationSafeguard
    :app:compileReleaseJavaWithJavac
    [DAVIS] task before : compileReleaseJavaWithJavac
    :app:compileReleaseJavaWithJavac UP-TO-DATE
    [DAVIS] task after : compileReleaseJavaWithJavac
    :app:compileReleaseNdk
    [DAVIS] task before : compileReleaseNdk
    :app:compileReleaseNdk UP-TO-DATE
    [DAVIS] task after : compileReleaseNdk
    :app:compileReleaseSources
    [DAVIS] task before : compileReleaseSources
    :app:compileReleaseSources UP-TO-DATE
    [DAVIS] task after : compileReleaseSources
    :app:lintVitalRelease
    [DAVIS] task before : lintVitalRelease
    [DAVIS] task after : lintVitalRelease
    :app:prePackageMarkerForRelease
    [DAVIS] task before : prePackageMarkerForRelease
    [DAVIS] task after : prePackageMarkerForRelease
    :app:transformClassesWithDavisPluginForRelease
    [DAVIS] task before : transformClassesWithDavisPluginForRelease
    :app:transformClassesWithDavisPluginForRelease UP-TO-DATE
    [DAVIS] task after : transformClassesWithDavisPluginForRelease
    :app:transformClassesWithDexForRelease
    [DAVIS] task before : transformClassesWithDexForRelease
    To run dex in process, the Gradle daemon needs a larger heap.
    It currently has approximately 1365 MB.
    For faster builds, increase the maximum heap size for the Gradle daemon to more than 2048 MB.
    To do this set org.gradle.jvmargs=-Xmx2048M in the project gradle.properties.
    For more information see https://docs.gradle.org/current/userguide/build_environment.html
    [DAVIS] task after : transformClassesWithDexForRelease
    :app:mergeReleaseJniLibFolders
    [DAVIS] task before : mergeReleaseJniLibFolders
    :app:mergeReleaseJniLibFolders UP-TO-DATE
    [DAVIS] task after : mergeReleaseJniLibFolders
    :app:transformNative_libsWithMergeJniLibsForRelease
    [DAVIS] task before : transformNative_libsWithMergeJniLibsForRelease
    :app:transformNative_libsWithMergeJniLibsForRelease UP-TO-DATE
    [DAVIS] task after : transformNative_libsWithMergeJniLibsForRelease
    :app:processReleaseJavaRes
    [DAVIS] task before : processReleaseJavaRes
    :app:processReleaseJavaRes UP-TO-DATE
    [DAVIS] task after : processReleaseJavaRes
    :app:transformResourcesWithMergeJavaResForRelease
    [DAVIS] task before : transformResourcesWithMergeJavaResForRelease
    :app:transformResourcesWithMergeJavaResForRelease UP-TO-DATE
    [DAVIS] task after : transformResourcesWithMergeJavaResForRelease
    :app:validateExternalOverrideSigning
    [DAVIS] task before : validateExternalOverrideSigning
    [DAVIS] task after : validateExternalOverrideSigning
    :app:packageRelease
    [DAVIS] task before : packageRelease
    :app:packageRelease UP-TO-DATE
    [DAVIS] task after : packageRelease
    :app:zipalignRelease
    [DAVIS] task before : zipalignRelease
    :app:zipalignRelease UP-TO-DATE
    [DAVIS] task after : zipalignRelease
    :app:assembleRelease
    [DAVIS] task before : assembleRelease
    [DAVIS] task after : assembleRelease
    
    BUILD SUCCESSFUL
    
    Total time: 3.6 secs
    [DAVIS] build finished.
    
    

    从上面的日志我们可以看出,我们可以在项目打包前、某个Task任务执行前或执行后以及整个项目打包完成后来做自己想做的事了。

    GitHub源码地址:https://github.com/881205wzs/GradlePluginDemo
    源码下载地址:https://download.csdn.net/download/wangzhongshun/11010210

    相关文章

      网友评论

        本文标题:Android Studio 自定义Gradle Plugin

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