再学一次gradle系列——常用技巧(五)

1,367 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

gradle系列——groovy,核心对象(一)

gradle系列——Task和生命周期(二)

gradle系列——Plugin插件(三)

gradle系列——插件应用Transform(四)

gradle系列——常用技巧(五)

Android gradle插件配置

Variants

variant直接翻译叫变体,实际就是指打包输出不同版本的apk

我们随便新建的一个AndroidStudio工程,打包的apk默认都会有两个版本,debug和release,这两个不同版本的apk就称之为变体

在gradle中,变体主要在两个闭包中进行配置,buildTypesproductFlavors

buildTypes

buildTypes是一个域对象,内部声明的每一个闭包都会被处理为一个BuildType对象,例如默认会有debug和release两个,我们也可以完全自定义,例如添加一个local

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
    debug {
        minifyEnabled false
    }
    // 可根据需要随便定义一个新的BuildType
    local {}

}

执行一次assemble任务,就会在outputs文件夹中输出三个文件夹,分别对应了这3个BuildType

productFlavors

productFlavors跟buildTypes是同一个纬度的配置,都用于输出不同的apk包,但是buildTypes偏向于编译上的不同配置,相对来说是偏技术性的,例如接口环境,混淆配置等等,而productFlavors则更偏向于业务产品上的不同,例如不同商店的渠道包,标准版和收费版等

productFlavor是一个通用意义上的产品的变体纬度,但具体是根据什么方向划分,这就需要先细分产品业务具体的维度,例如根据渠道

// 声明一个名为“channel”的维度
flavorDimensions "channel"
// 类似buildTypes,productFlavors中创建的每一个闭包都对应一个ProductFlavor类型
productFlavors {
    xiaomi {
        // 每一个flavor需要指明当前对应的维度
        dimension "channel"
    }
    oppo {
        dimension "channel"
    }
}

从输出的apk可以看出,productFlavors中的配置会和buildTypes中的配置进行组合

首先会根据channel维度,分成对应的两个文件夹,然后在文件夹内,又会按照buildType的类型分成3个文件夹

再继续加上一个产品维度,是否收费的版本类型(标准版和pro收费版)

flavorDimensions "channel","type"
productFlavors {
    xiaomi {
        dimension "channel"
    }
    oppo {
        dimension "channel"
    }
    // 
    stantard {
        dimension "type"
    }
    pro {
        dimension "type"
    }
}

从这次输出的apk可以看出,不同维度的flavor也是会相互组合的

manifestPlaceholders

manifestPlaceholders,顾名思义,用于manifest文件的属性值占位

在我们开发中,有时候会用到一些第三方的库或者工具,需要在AndroidManifest文件中去声明一个meta-data的值

类似这样

<meta-data
    android:name="Third-AppKey"
    android:value="ABCDEFG123456789" />

例如上述我们提到的不同的渠道包,需要在AndroidManifest中去设置不同的channel值,但是如果每打一个渠道包都去手动修改肯定不现实,这种情况就可以借助manifestPlaceholders

manifestPlaceholders是VariantProperties接口的一个属性,是一个Map,而BuildType,ProductFlavor,DefaultConfig这几个类型都实现了这个接口,所以我们defaultConfig,buildTypes下具体的Type中以及productFlavors的具体flavor中都可以访问到这个manifestPlaceholders,并给他设置或添加对应的key-value

defaultConfig {
    applicationId "com.example.myapplication"
    minSdkVersion 19
    targetSdkVersion 30
    versionCode 1
    versionName APP_VERSION

    // 所有的版本默认的值
    manifestPlaceholders = [APP_KEY: "key123456", APP_SECRET: "secret"]

}

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        // release版本指定特殊的key
        manifestPlaceholders.put("APP_KEY","releaseKey")

    }
    debug {
        minifyEnabled false
    }
    local {}

}

flavorDimensions "channel", "type"
productFlavors {
    xiaomi {
        dimension "channel"
        manifestPlaceholders.put("CHANNEL_NAME","xiaomi")
    }
    oppo {
        dimension "channel"
        manifestPlaceholders.put("CHANNEL_NAME","oppo")
    }
    stantard {
        dimension "type"
    }
    pro {
        dimension "type"
    }
}

在AndroidManifest.xml中可直接使用

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.MyApplication">
    <meta-data
        android:name="AppKey"
        android:value="${APP_KEY}"/>
    <meta-data
        android:name="Channel"
        android:value="${CHANNEL_NAME}"/>
    <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:theme="@style/Theme.MyApplication.NoActionBar">
    </activity>
</application>

buildConfigField

buildConfigField的作用是为输出的不同编译版本设置不同的参数值,以便代码中可以动态根据版本进行逻辑控制。例如最常见的,debug版和release版使用不同的api接口,代码中判断是否是付费pro版本等

buildConfigField也是可以在defaultConfig,buildTypes以及productFlavors中使用

buildConfigField函数主要接收3个参数

/**
 * type为字段类型,例如String,int,boolean
 * name位字段名称
 * value为字段取值,tips:如果type为String,对应的value需要在单引号里面使用双引号设置具体字符串
 */
public void buildConfigField(String type,String name, String value)
defaultConfig {
    applicationId "com.example.myapplication"
    minSdkVersion 19
    targetSdkVersion 30
    versionCode 1
    versionName APP_VERSION

    manifestPlaceholders = [APP_KEY: "key123456", APP_SECRET: "secret"]
    buildConfigField 'int','API_VERSION','1'

}

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        manifestPlaceholders.put("APP_KEY","releaseKey")
        buildConfigField('String','HOST','"https://www.release.com"')

    }
    debug {
        minifyEnabled false
        buildConfigField('String','HOST','"https://www.debug.com"')

    }
    local {}

}

flavorDimensions "channel", "type"
productFlavors {
    xiaomi {
        dimension "channel"
        manifestPlaceholders.put("CHANNEL_NAME","xiaomi")
    }
    oppo {
        dimension "channel"
        manifestPlaceholders.put("CHANNEL_NAME","oppo")
    }
    stantard {
        dimension "type"
        buildConfigField 'boolean','isPro','false'
    }
    pro {
        dimension "type"
        buildConfigField 'boolean','isPro','true'
    }
}

定义的这些字段,会以常量的形式添加到编译后生成的BuildConfig类中,在代码中就可以通过BuildConfig使用到这些字段

// type为pro,buildType为debug生成的
public final class BuildConfig {
      public static final boolean DEBUG = Boolean.parseBoolean("true");
      public static final String APPLICATION_ID = "com.example.myapplication";
      public static final String BUILD_TYPE = "debug";
      public static final String FLAVOR = "oppoPro";
      public static final String FLAVOR_channel = "oppo";
      public static final String FLAVOR_type = "pro";
      public static final int VERSION_CODE = 1;
      public static final String VERSION_NAME = "1.0.1";
      // Field from default config.
      public static final int API_VERSION = 1;
      // Field from build type: debug
      public static final String HOST = "https://www.debug.com";
      // Field from product flavor: pro
      public static final boolean isPro = true;
}

//type为stantard,buidType为release生成的
public final class BuildConfig {
      public static final boolean DEBUG = false;
      public static final String APPLICATION_ID = "com.example.myapplication";
      public static final String BUILD_TYPE = "release";
      public static final String FLAVOR = "oppoStantard";
      public static final String FLAVOR_channel = "oppo";
      public static final String FLAVOR_type = "stantard";
      public static final int VERSION_CODE = 1;
      public static final String VERSION_NAME = "1.0.1";
      // Field from default config.
      public static final int API_VERSION = 1;
      // Field from build type: release
      public static final String HOST = "https://www.release.com";
      // Field from product flavor: stantard
      public static final boolean isPro = false;
}

resValue

resValue也是一个函数,它的作用跟buildConfig类似,根据不同的编译版版本输出不同的属性值,但它主要作用于res/values文件夹下定义的资源取值,例如字符串,颜色,尺寸等,可以在defaultConfig,buildTypes以及productFlavors中使用

/**
 * type为字段类型,例如string,dimen,color,与资源文件中定义的类型一致
 * name位字段名称
 * value为字段取值
 */
public void resValue(String type,String name, String value)
defaultConfig {

    manifestPlaceholders = [APP_KEY: "key123456", APP_SECRET: "secret"]
    buildConfigField 'int','API_VERSION','1'
    resValue 'color','gradle_color','#00FF00'
    resValue 'dimen','test_dimen',"10dp"

}
buildTypes {
    release {
        resValue 'string','app_name',"releaseApp"

    }
    debug {
        resValue 'string','app_name',"debugApp"
        resValue 'dimen','test_dimen',"16dp"
        resValue 'color','gradle_color','#FF0000'

    }
}

定义的这些字段都会输出在编译之后生成的gradleResValues文件中

在代码中使用这些资源变量

// 布局文件使用color和dimen
<Button
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/gradle_color"
    android:layout_margin="@dimen/test_dimen"/>

// AndroidManifest文件使用string
<application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:theme="@style/Theme.MyApplication">
</application>   
    

不难看出resValue,BuildConfig,manifestPlaceHolders都是跟上面的Variant有关系,作用很简单,就是针对不同版本的apk,进行差异化配置

修改输出的APK名字

有时候,我们需要根据自定的规则命名apk,这种可以通过applicationVariants属性去指定,具体用法可以看下注释

android {
    // 遍历applicationVariants拿到的是每一个变体variant
    applicationVariants.all { variant ->
        // 每一个变体有一个outputs属性,代表该变体的所有输出,也是一个集合,可能不仅仅包含apk文件
        variant.outputs.all { output ->
            if (output.outputFile != null || output.outputFile.name.endWith('.apk')) {
                def info = ''
                variant.productFlavors.each {
                    info = info + it.name + '_'
                }
                // 为outputFileName属性重新赋值,就自定义了对应apk的名字
                output.outputFileName = "$info${variant.buildType.name}_${buildTime()}.apk"
            }
        }

    }

}

// 获取当前日期
def buildTime() {
    return new Date().format("yyyyMMdd")
}

输出的apk就是按照编译类型+时间日期组成的

资源文件分包

在Android开发中,随着长时间迭代,项目会越来越大,资源文件也越来越多,一般我们的资源文件都放在同一个res目录下,管理起来就很混乱,不过现在可以借助gradle的sourceSets,将这些资源文件按照一定的业务逻辑进行分包处理

首页在main目录下按照业务逻辑拆成几个res相关目录

然后在build.gradle的android闭包中通过sourceSets把这几个文件夹的路径配置上,这样就实现了资源分包,可以方便后续资源文件维护

android{
    sourceSets {
        main {
            res.srcDirs (
                'src/main/res',
                'src/main/res_common',
                'src/main/res_home'
            )
        }
    }
}

gradle模块化

gradle模块其实在前面系列文章中讲ext属性的时候有提到过,模块化无非就是抽象封装,提高代码的复用性,便于维护

提取公共属性

例如可以提取一个config.gradle文件,用于声明一些在各个build.gradle文件中都会用到的一些参数

// config.gradle文件
ext {
    android = [
            compileVersion: 31,
            buildVersion  : "30.0.3"
    ]

    version = [
            appcompat:'1.2.0',
            constraintlayout:'2.0.1'
    ]

    dependenciesMap = [
            appcompat:"androidx.appcompat:appcompat:${version.appcompat}",
            constraintlayout:"androidx.constraintlayout:constraintlayout:${version.constraintlayout}"
    ]
}

// 在module的build.gradle中引入使用
apply from: rootProject.file('config.gradle')
android {

    compileSdkVersion android.compileVersion
    buildToolsVersion android.buildVersion
}

提取功能脚本

我们一个项目会有多个module,每个module会输出aar,这些aar需要上传到maven上,我们就可以写一个upload.gradle,其他需要上传的模块引入就可以实现上传maven的功能

// upload.gradle
apply plugin: 'maven'

uploadArchives {
    repositories.mavenDeployer {
        repository(url: uri('../repo'))   // 本地仓库路径
        pom.groupId = GROUP_ID// 唯一标识(通常为模块包名,也可以任意)
        pom.artifactId = ARTIFACT_ID// 项目名称(通常为类库模块名称,也可以任意)
        pom.version = VERSION// 版本号
    }
}

// 在module下也增加一个gradle.properties,用于配置区分不同aar的groupId,version等信息
GROUP_ID = com.tu.example
ARTIFACT_ID = library2
VERSION = 1.0.0

// 在具体module的build.gradle中引入
apply from:rootProject.file('upload.gradle')

以上配置之后,就会在每个moudule的task中,增加uploadArchives的Task

提取base基础脚本

如果项目有很多个module,且module有很多的共性,则可以考虑类似java,kotlin开发中,抽象出一个base基类,抽象出一个base.gradle

apply plugin:'com.android.library'
apply plugin: 'kotlin-android'
apply from:rootProject.file('config.gradle')

// 只作演示,抽离了部分属性,实际项目中,所有变量值都可抽离到config.gradle中统一管理
android {
    compileSdkVersion android.compileVersion
    buildToolsVersion android.buildVersion

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 31
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.4.0'
    implementation 'com.google.android.material:material:1.4.0'
}

然后module下的gradle脚本引入,这里面的内容都是基于base.gradle脚本中的内容进行添加

apply from:rootProject.file('base.gradle')

// 同样可添加自己特有的依赖配置等
dependencies {
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
}