Gradle系列(三) Gradle配置構建和渠道包

瀟風寒月發表於2019-12-12

1. 前言

Gradle系列已完成,專注於Gradle,有如下幾篇文章

Android開發,打包的時候可能會打內測包,外側包,release包等,還有就是有時候還需要打不同渠道的包等.這時它們裡面的包名,應用圖示,應用名稱,某些資原始檔,某些java檔案等可能不同,如果通過人工去手動改,改了之後再打包的話,那就太麻煩了.現在有了Gradle,它可以幫到我們.

ps: 請先搞懂Android DSL的基本配置,比如compileSdkVersion是什麼本文不會再介紹.有需要則查官方文件,還有就是皇叔寫的寫給Android開發的Gradle知識體系非常不錯.

demo原始碼: github.com/xfhy/Gradle…

2. 統一配置

2.1 以前的配置方式

今天我來帶大家實現一種很方便的配置專案諸如compileSdkVersion,三方庫引入等.最終的效果如下,可以直接通過Config點出來,並且還可以通過Ctrl+滑鼠左鍵點過去.

QQempF.png

以前,很多很多專案會將一些基本的配置放到Project的build.gradle中,類似

ext {
    compileSdkVersion = 29
    buildToolsVersion = "29.0.0"
    targetSdkVersion = 29
    minSdkVersion = 21
    versionCode = 1
    versionName = "1.0.0"
}
複製程式碼

然後在各個module的build.gradle中進行使用這個配置

android {
    compileSdkVersion rootProject.compileSdkVersion
    defaultConfig {
        versionCode rootProject.versionCode
        versionName rootProject.versionName
        minSdkVersion rootProject.minSdkVersion
        targetSdkVersion rootProject.targetSdkVersion
    }
}
複製程式碼

這種方式可以,但是不夠優雅.我們寫好rootProject,然後再輸入"."的時候AS不會提示你有哪些可用的變數,不能智慧提示.而且即使你一字不差的寫好了,用Ctrl+滑鼠左鍵也點不過去.在ext{}下的那些變數,你用快捷鍵搜尋在哪些地方使用到了,AS也不知道....是不是覺得差點意思.

2.2 推薦的配置方式

ps: 這種配置方式,最開始是看到柯基大佬在使用,覺得太棒了,哈哈.這種配置方式,好像只能是3.5+版本的AS

我們來實現一種更優雅的方式,實現上面的功能.建立一個buildSrc這個名字的module,這個module的名稱必須為buildSrc.因為我們建立的這個module是AS專門用來寫外掛的,會自動參與編譯.建立好之後刪除Android那一堆東西,什麼java程式碼,res,清單檔案等.只剩下build.gradle和.gitignore

QQleEj.png

把build.gradle檔案內容改成

repositories {
    google()
    jcenter()
}
apply {
    plugin 'groovy'
    plugin 'java-gradle-plugin'
}
dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation "commons-io:commons-io:2.6"
}
複製程式碼

然後在main下面建立資料夾groovy,sync一下.沒啥問題的話,應該能編譯過.然後在groovy資料夾下面建立Config.groovy檔案

class Config {

    static applicationId = 'com.xfhy.gradledemo'
    static appName = 'GradleDemo'
    static compileSdkVersion = 29
    static buildToolsVersion = '29.0.2'
    static minSdkVersion = 22
    static targetSdkVersion = 29
    static versionCode = 1
    static versionName = '1.0.0'

}
複製程式碼

可以看到,我們將常用配置全部填入這裡.這個時候去module的build.gradle將這些引數全部替換掉.

android {
    compileSdkVersion Config.compileSdkVersion
    buildToolsVersion Config.buildToolsVersion
    defaultConfig {
        applicationId Config.applicationId
        minSdkVersion Config.minSdkVersion
        targetSdkVersion Config.targetSdkVersion
        versionCode Config.versionCode
        versionName Config.versionName
    }
    ....
}
複製程式碼

完美.同理,將三方庫也可以加進來

class Config {
    static depConfig = [
            support      : [
                    appcompat_androidx   : "androidx.appcompat:appcompat:$appcompat_androidx_version",
                    recyclerview_androidx: "androidx.recyclerview:recyclerview:$recyclerview_androidx_version",
                    design               : "com.google.android.material:material:$design_version",
                    multidex             : "com.android.support:multidex:$multidex_version",
                    constraint           : "com.android.support.constraint:constraint-layout:$constraint_version",
            ],
            kotlin       : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",
            leakcanary   : [
                    android         : "com.squareup.leakcanary:leakcanary-android:$leakcanary_version",
                    android_no_op   : "com.squareup.leakcanary:leakcanary-android-no-op:$leakcanary_version",
                    support_fragment: "com.squareup.leakcanary:leakcanary-support-fragment:$leakcanary_version",
            ],
    ]
}
複製程式碼

在build.gradle中使用

dependencies {
    implementation Config.depConfig.support.recyclerview_androidx
    ....
}
複製程式碼

3. 渠道包

3.1 productFlavors

productFlavors直譯為產品風味,Android這邊用它來做多渠道.在app的build.gradle中加入如下配置

android {
    flavorDimensions "channel"
    productFlavors {
        free {
            dimension "channel"
            //程式包名
            applicationId "com.xfhy.free"
            //替換清單檔案中的標籤
            manifestPlaceholders = [
                    APP_ICON: "@drawable/ic_launcher",
                    APP_NAME: "xx免費版",
            ]
            //versionName
            versionName "2.0.0"
            //versionCode
            versionCode 2
        }
        vip {
            dimension "channel"
            //程式包名
            applicationId "com.xfhy.vip"
            //替換清單檔案中的標籤
            manifestPlaceholders = [
                    APP_ICON: "@drawable/ic_launcher",
                    APP_NAME: "xxVip版",
            ]
            //versionName
            versionName "3.0.0"
            //versionCode
            versionCode 3
        }
        svip {
            dimension "channel"
        }
    }
}

複製程式碼

如程式碼所示,我們配置了3種型別的風味,在productFlavors中可以配置包名(applicationId)、版本號(versionCode)、版本名(versionName)、icon、應用名.並且可以在裡面配置各種你之前在defaultConfig裡面配置的東西.還可以配置src程式碼目錄,res目錄之類的.並且這個時候Build Variants裡面有了多種型別,比如:freeDebug,freeRelease,vipDebug,vipRelease等.你在Build Variants裡面選擇freeDebug,則是使用free風味,並且是debug時使用的配置.

<application
    xmlns:tools="http://schemas.android.com/tools"
    android:icon="${APP_ICON}"
    android:label="${APP_NAME}"
    android:theme="@style/AppTheme"
    android:largeHeap="true"
    tools:replace="android:label">
    ...
</application>
複製程式碼

3.2 渠道變數

首先來介紹一個關鍵詞擴充套件:applicationVariants,它是在AppExtension裡面的,它的官方文件,它意思是返回應用程式專案包含的構建變體的集合,是用all關鍵詞進行遍歷.我們拿到了這些變體之後,可以根據當前是哪個變體來構建出相應變體所特殊的變數.比如內測和外測它們的地址肯定不一樣的,那麼通過這種方式可以很方便地整出來.構建的變數會存在於相應的BuildConfig中,然後在java程式碼中直接引用就行,替換地址時也不需要動java程式碼,只需在gradle中改一下,然後它編譯的時候就會自動構建BuildConfig,自動將地址搞成最新的了.說了這些多,show me the code!

android {
    applicationVariants.all { variant ->
        //構建變體專屬變數
        switch (variant.flavorName) {
            case 'free':
                buildConfigField("String", "BASE_URL", "\"http://31.13.66.23\"")
                buildConfigField("String", "TOKEN", "\"dhaskufguakfaskfkjasjhbfree\"")
                break
            case 'vip':
                buildConfigField("String", "BASE_URL", "\"http://31.13.66.24\"")
                buildConfigField("String", "TOKEN", "\"dhaskfagafkjasjhbvip\"")
                break
            case 'svip':
                buildConfigField("String", "BASE_URL", "\"http://31.13.66.25\"")
                buildConfigField("String", "TOKEN", "\"dhaskufgufgsdagajasjhbsvip\"")
                break
        }
    }
}
複製程式碼

將上面的程式碼寫在app的build.gradle中,在上面的gradle程式碼中我們定義了2個變數,不同的變體會構建不同的值,比如上面的BASE_URL我們會在free變體編譯的時候就會在BuildConfig生成一個變數,值是http://31.13.66.23.我們來看一下BuildConfig中是些什麼內容:

//build\generated\source\buildConfig\free\debug\com\xfhy\gradledemo\BuildConfig.java
public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.xfhy.free";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "free";
  public static final int VERSION_CODE = 2;
  public static final String VERSION_NAME = "2.0.0";
  // Fields from the variant
  public static final String BASE_URL = "http://31.13.66.23";
  public static final String TOKEN = "dhaskufguakfaskfkjasjhbfree";
}
複製程式碼

這個檔案是gradle構建時自動為我們建立的,不需要去修改.我們構建的變體變數在最下面,這裡面的值確實是我們在gradle程式碼中寫的那樣.這個裡面已經有一些不是我們搞出來的變數了,比如是否是DEBUG,APPLICATION_ID,VERSION_CODE之類的.我們在java程式碼中使用的時候,直接BuildConfig.BASE_URL這種方式進行使用即可,它就是一個普通的java類,裡面定義了一些變數而已.

當然除了上面的渠道變數之外,還有一些變數是公用的,每個變體都是一樣的那種.我們可以寫到defaultConfig下面.

android {
    defaultConfig {
        buildConfigField("String", "APP_DESCRIPTION", "\"你沒有見過的船新版本\"")
        buildConfigField("String[]", "TAB", "{\"首頁\",\"排行榜\",\"我的\"}")
    }
}
複製程式碼

3.3 打包檔案命名

還是利用上面的applicationVariants,當我們拿到了變體之後,在打包的時候動態的將打包之後的檔名改一下.比如改成下面這種形式

applicationVariants.all { variant ->
    variant.outputs.all {
        def type = variant.buildType.name
        def channel = variant.flavorName
        outputFileName = "demo_${variant.versionName}_${channel}_${type}.apk"
    }
}
複製程式碼

最後它打出來的包是這樣的demo_2.0.0_free_debug.apk,寫完之後可以使用gradlew assembleFreeDebug命令試一下.命令執行之後會在app\build\outputs\apk\free\debug目錄下產生相應的apk檔案.

3.4 簽名

可以在gradle中指定打包時的簽名檔案,密碼啥的

signingConfigs {
    debug {
        storeFile file('../keys/xfhy.jks')
        storePassword "qqqqqq"
        keyAlias "xfhy"
        keyPassword "qqqqqq"
        v1SigningEnabled true
        v2SigningEnabled true
    }
    release {
        storeFile file('../keys/xfhy.jks')
        storePassword "qqqqqq"
        keyAlias "xfhy"
        keyPassword "qqqqqq"
        v1SigningEnabled true
        v2SigningEnabled true
    }
}
複製程式碼

指定了簽名以及密碼之後,打包的時候就只需要在命令列執行gradlew assembleVipRelease即可,不用開啟Android Studio了.

3.5 資源

Android Studio提供了程式碼整合功能.只需要建立app/src/xxFlavorName/assets,app/src/xxFlavorName/src,app/src/xxFlavorName/res即可.當在Build Variants中切換切換變體之後,AS就只會編譯對應變體的資源+main下面的資源.

Qr9JqP.md.png

可以看到,free下面的資料夾自動變色了,這些是free變體特殊的東西,只有在free編譯的時候才會被用到.java程式碼,res資源等,到時是需要和main下面的一起合併的.

假如我在free變體下建立了Test.java,然後可以在main下面引用到,就和平時使用一樣.但是如果相同包名下如果free中有Test.java,main中也有,那麼是編譯不過的. 還有就是當main裡面用到了Test.java的時候,在Build Variants中切換成了vip,而vip中剛好沒有Test.java,就會報錯的,因為找不到這個檔案.

上面這個問題,可以用sourceSets來解決,sourceSets可以指定程式碼資原始檔的位置.雖然上面建立的free,vip等變體資料夾下面也是放這些東西的,但是用sourceSets比他們優先順序高.

下面來看它的普通用法,一看就懂.

sourceSets {
    main {
        manifest.srcFile 'AndroidManifest.xml'
        java.srcDirs = ['src']
        aidl.srcDirs = ['src']
        renderscript.srcDirs = ['src']
        res.srcDirs = ['res']
        assets.srcDirs = ['assets']
    }
}
複製程式碼

然後我們除了在src/main/java下有java程式碼,還可以指定在其他地方有java程式碼.比如下面這樣.可以在src下面建立common/java資料夾,用於存放公共的程式碼.

sourceSets {
    sourceSets.main.java.srcDirs = ['src/main/java', 'src/common/java']
}
複製程式碼

上面的Test.java問題,可以用sourceSets解決.在common資料夾建立一個公共的Test.java,然後其他變體可以使用.在free在使用自己特殊的Test.java,只拿給free用.

專案的結果是這樣的,這是變體是vip的時候:

QszS5F.md.png

sourceSets {
    main {
        java.srcDirs = ['src/main/java']
    }
    free {
        java.srcDirs = ['src/free/java']
    }

    svip {
        java.srcDirs = ['src/common/java']
    }

    vip {
        java.srcDirs = ['src/common/java']
    }
}
複製程式碼

4. 總結

又學到了一大波乾貨內容.對於渠道包,可能不一定會用得到,但是其實還是挺有用的. 同一套程式碼可以產出多個app,俗稱馬甲包,可能很多公司都在搞這種.如果用得上,希望能幫到你.

參考:

相關文章