【Gradle系列】Gradle在Android上的拓展

2,000 阅读7分钟

背景

上一阶段,我们了解了gradle的任务(Task)相关知识(如:定义/配置等)

知道了安卓在构建过程中,是通过一个个task串联起来的,如何实现的更多细节可以回顾【Gradle系列】Gradle之Task文章

接下来我们将学习gradle在安卓上的一些拓展,比如:setting/SourceSet/自定义插件等

setting

安卓开发过程中,我们经常会碰到 settings.gradle 文件,主要是用来包含模块,如:

include.png

include只是setting的其中一个功能而已,其还有很多功能(如:兼容maven等),只是安卓开发中用的比较少

settings.gradle文件对应的解析类是 org.gradle.api.initialization.Settings

Settings类的执行是在Gradle生命周期的初始化阶段,是初始化阶段最重要的类

翻看源码可以看到,比较常用的include对应的代码为:

void include(String... var1);

void includeFlat(String... var1);

其他部分的代码:

    Settings getSettings();

    @Incubating
    ScriptHandler getBuildscript();

    File getSettingsDir();

    File getRootDir();

    ProjectDescriptor getRootProject();

    ProjectDescriptor project(String var1) throws UnknownProjectException;

    @Nullable
    ProjectDescriptor findProject(String var1);

    ProjectDescriptor project(File var1) throws UnknownProjectException;

    @Nullable
    ProjectDescriptor findProject(File var1);

    StartParameter getStartParameter();

    Gradle getGradle();

    void includeBuild(Object var1);

    void includeBuild(Object var1, Action<ConfigurableIncludedBuild> var2);

    BuildCacheConfiguration getBuildCache();

    void buildCache(Action<? super BuildCacheConfiguration> var1);

    void pluginManagement(Action<? super PluginManagementSpec> var1);

    PluginManagementSpec getPluginManagement();

    @Incubating
    void sourceControl(Action<? super SourceControl> var1);

    @Incubating
    SourceControl getSourceControl();

    @Incubating
    void enableFeaturePreview(String var1);

SourceSet

SourceSet与项目结构有关

安卓默认情况下,代码是放在src/main/java,资源放在src/main/res中,当然还有其他(如:so等)

我们可以通过改变SourceSet属性,自己指定代码/资源放的目录(这个比maven强,因为maven是不可以指定或者添加的)

(1)例子1: 改变so存放位置

默认情况,so存放在app/src/main/jniLibs中,现在改为app/libs下,具体实现如下:

android {
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

结果:

lib.png

这样我们就可以把so放到libs下,编译等时候就会去这里加载

(2)例子2: 资源模块管理

我们日常开发中,资源(如:图片/布局等)一般都是放在src/main/res下面

但是,在大型app中(如:虎牙/CC直播等)涉及到的模块是错综复杂的,如果全部都放在同一个位置,那么维护起来就很费劲

所以,我们可以添加资源的目录,具体实现如下:

android {
    sourceSets {
        main {
            res.srcDirs = ['src/main/res', 'src/main/res-davi']
        }
    }
}

结果:

res.png

可以看到,资源目录由一个变为了两个,这样的话不同模块放不同的资源目录,进而实现 资源模块的管理

自定义插件

前面我们学习了task的使用,可以看到其是写在gradle文件里面的;

随着功能的越来越复杂,那么这种模式下那么gradle文件就会越来越多,这样相比我们写java代码中分模块来比可读性和维护性就没那么好

所以自定义Gradle插件可以比较好的组织这些task,以功能为单元去组织系列task,进而实现,比如我们经常使用的gradle插件:

apply plugin: 'groovy'
apply plugin: 'com.android.application'

下面我们就来看下自定义插件的流程:

新建模块

在AS中新建一个模块,然后把代码/资源删除,gradle文件清空,把目录调整为如下:

调整目录.png

配置编译需要的依赖

在清空的gradle文件中添加如下内容:

//groovy 插件
apply plugin: 'groovy'
//maven仓库上传需要用到的
apply plugin: 'maven'

//引入gradle的API代码
dependencies {
    implementation gradleApi()
}
sourceSets {
    main {
        //指定groovy源码的位置
        groovy {
            srcDir 'src/main/groovy'
        }
        //指定资源的位置
        resources {
            srcDir 'src/main/resources'
        }
    }
}

建立插件入口和相关配置

(3.1)建立代码入口:

package com.davi.pkg
import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project

public class DaviPluDemo implements Plugin<Project> {
    @Override
    void apply(Project project) {
        println '【DaviPluDemo】-- apply start-- ' + project.name
        println '【DaviPluDemo】-- apply end-- ' + project.name
    }
 }

(3.2) 配置插件入口文件:

首先是新建配置文件

插件配置文件.png

红色框的名字比较重要,这个代表的就是在使用的时候的名字:

apply plugin: 'com.demo.plugin'

在使用者找到配置入口后,然后就是配置入口关联代码入口(com.davi.pkg.properties文件里面的内容):

implementation-class=com.davi.pkg.DaviPluDemo

编译插件并且上传maven

编译就不用讲啦,直接模块里面点build

然后就是上传maven,上传之前需要在gradle中配置下上传相关:

uploadArchives {
    repositories {
        mavenDeployer {
            //设置插件的GAV参数
            pom.groupId = 'com.davi.pkg'//你的包名
            pom.artifactId = 'dp'//这个随意起一个,发布后就是这个插件的名称
            pom.version = '1.1.7'//版本号
            //文件发布到下面目录8
            repository(url: uri('../repo'))
        }
    }
}

然后任务中点击uploadArchives来上传,具体按钮如下:

上传maven.png

使用插件

这里我们是在app模块里面使用插件,所以在app模块的gradle文件中添加如下配置:

buildscript {
    repositories {
        maven {
            //本地Maven仓库地址
            //这里是发布在本地文件夹了
            url uri('../repo')
        }
    }
    dependencies {
        //格式为 groupId : artifactId : version
        classpath 'com.davi.pkg:dp:1.1.7'
    }
}
apply plugin: 'com.davi.pkg'

效果

app模块引入成功之后,编译app模块,这个可以看到插件的运行效果:

插件编译成功.png

Gradle自定义插件里添加task

定义

在插件模块里面代码位置,擦创建task类,代码如下:

package com.davi.pkg.task

import com.davi.pkg.bean.ConfigBean
import com.davi.pkg.DaviPluDemo
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction


class DaviTaskPlu extends DefaultTask {

    DaviTaskPlu() {
        group = 'davi'
        description = '自定义插件中的任务:DaviTaskPlu'
    }

    /**
     * TaskAction
     * - 被这个注解过的,那么方法就会在gradle的生命周期中的执行阶段被执行
     * */
    @TaskAction
    void onAction() {
        println '【DaviTaskPlu】【onAction】---> start'
        println '【DaviTaskPlu】【onAction】---> end'
    }
}

插件中调用定义的task

public class DaviPluDemo implements Plugin<Project> {

    public static String NAME_PLU_CONFIG = 'pluConfig'

    @Override
    void apply(Project project) {
        println '【DaviPluDemo】-- apply start-- ' + project.name
        onTask(project)
        println '【DaviPluDemo】-- apply end-- ' + project.name
    }

    void onTask(Project project) {
        /**
         * 自定义task的执行
         * 创建了一个名为 name-davi 的任务,然后映射到我们自定义的任务的类 DaviTaskPlu。
         * */
        project.getTasks().create("name-davi", DaviTaskPlu.class, new Action<DaviTaskPlu>() {
            @Override
            void execute(DaviTaskPlu t) {
                t.onAction()
            }
        })
    }
    
 }

使用效果

插件改动后,同样编译插件,改版本好,然后上传maven; 最后是app模块那边使用地方提高版本号,编译使用,具体效果如下:

task在自定义插件中.png

安卓其他Gradle拓展简要

我们日常开发安卓的时候,经常用到下面的系列配置:

android {
    compileSdkVersion this.compileSdkVersion
    buildToolsVersion this.buildToolsVersion
    defaultConfig {
        applicationId this.applicationId
        minSdkVersion this.rootProject.ext.android.minSdkVersion
        targetSdkVersion mTargetSdkVersion
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

这些都是安卓在gradle上的拓展,如上面的defaultConfig,,buildTypes 除了常用的这些,还有很多其他的配置,具体可以查看gradle的源码:

com.android.build.gradle.AppExtension
com.android.build.gradle.TestedExtension
com.android.build.gradle.BaseExtension

怎么看?比如buildTypes的源码位置:

buildType.png

其他(如:变体)

(1)例子1: 实现改apk输出的名字思路

代码:

this.afterEvaluate {
    //有图.....
    this.android.applicationVariants.each { variant ->
        //debug || release
        println '【applicationVariants】name is : ' + variant.name
        //debug || release
        println '【applicationVariants】baseName is : ' + variant.baseName
        //1.0
        println '【applicationVariants】versionName is : ' + variant.versionName
        /***
         * 【1】实现改apk输出的名字
         * */
        //output.outputFile = apkFile  //重命名后的apk文件指向output,实现apk重命名
        variant.outputs.each { output ->
            //ktplay_android_sdk_CN_v4/DaviPlu/app/build/outputs/apk/debug/app-debug.apk
            //ktplay_android_sdk_CN_v4/DaviPlu/app/build/outputs/apk/release/app-release-unsigned.apk
            File outputFile = output.outputFile
            println '【applicationVariants】【output.outputFile】 path : ' + outputFile.path
            println '【applicationVariants】output name : ' + output.name
        }
    }
}

运行效果:

output.png

根据运行的效果可以看出,output.outputFile指向的就是我们生成的apk(包含debug和release),如果要重新命名apk目录(比如:根据渠道名字命名),只需要重定向下outputFile即可,如:

output.outputFile = apkFile  //重命名后的apk文件指向output,实现apk重命名

这里基于本章重点不在此,所以后续项目中再落实具体的项目实现,这里先留个变体的概念应用先;

另外,有一个比较重要的点需要留意,在访问变体applicationVariants的时候,会触发创建所有的任务;所以我们重定向了outputFile,也就会触发重新构建,进而实现改名字这样

(2)例子2: 变体中,找到某个任务,然后插入自己要执行的任务 代码:

this.afterEvaluate {
    //有图....
    this.android.applicationVariants.each { variant ->
        //debug || release
        println '【applicationVariants】buildType.name is : ' + variant.buildType.name

        /**
         * 【2】变体中,找到某个任务,然后插入自己要执行的任务(利用doLast/doFirst等)
         * */
        Task checkManifest = variant.checkManifest
        checkManifest.doFirst {
            println '【applicationVariants】在 checkManifest 任务执行前插入的代码块 '
        }
    }
}

运行效果:

插入task.png

小结:变体是android在gradle的拓展之一,日常项目应用中是很广泛的,这里先简单介绍下,后续项目使用的时候综合讲

源码地址

基于有些朋友咨询到源码相关,故把代码上传到了github,需要的朋友可以下载来看看

具体戳这里>>>

PS:代码目前还没有体系整理,后面会在项目中的时候再统一整理

结尾

哈哈,该篇就写到这里(一起体系化学习,一起成长)

Tips

更多精彩内容,请关注 “Android热修技术” 微信公众号