Android Studio項(xiàng)目Gradle構(gòu)建實(shí)踐

參考

名詞

  • 構(gòu)建類型(BuildType),編譯時(shí)的類型,如debug, release
  • 產(chǎn)品風(fēng)味(ProductFlavor),不同的產(chǎn)品特征,可以有不同的包名等等。
  • 構(gòu)建變體(BuildVariant),每一個(gè)特定唯一確定版本apk都是一個(gè)構(gòu)建變體的產(chǎn)物,其由構(gòu)建類型和產(chǎn)品風(fēng)味組成。
  • APG,全稱是Android Plugin for Gradle,google為使用gradle構(gòu)建而開發(fā)的插件。

1 一個(gè)典型的Android Studio 項(xiàng)目

1.1 項(xiàng)目結(jié)構(gòu)

一個(gè)新建的Android Stuido項(xiàng)目結(jié)構(gòu)如下:

項(xiàng)目結(jié)構(gòu)

包含三個(gè).gradle文件:

  • settings.gradle 文件對應(yīng)腳本執(zhí)行時(shí)的setting對象,該文件最先被解析和執(zhí)行,一些通用的初始化操作可以放在這里執(zhí)行,項(xiàng)目包含多個(gè)子工程或者模塊時(shí),必須在該文件中include,這也是其最重要的功能之一。新建項(xiàng)目默認(rèn)的settings.gradle
include ':app'

這里我想在腳本剛執(zhí)行時(shí)打印項(xiàng)目的存放路徑操作:

String projectDir = rootProject.projectDir.getAbsolutePath();
println projectDir
include ':app'
  • 項(xiàng)目根目錄下的build.gradle,對應(yīng)腳本執(zhí)行時(shí)的rootProject對象,一般不做具體的模塊構(gòu)建操作,用于指定項(xiàng)目所依賴的遠(yuǎn)程倉庫和使用的Gradle plugin 插件版本,適用與所有的子工程或者模塊。
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    //jcenter一個(gè)著名的遠(yuǎn)程代碼倉庫
    repositories {
        jcenter()
    }
    
    
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  1. 上面指定遠(yuǎn)程倉庫的作用就是在需要依賴的庫在本地找不到時(shí),會到該倉庫中去尋找并自動下載。
  2. 依賴的構(gòu)建插件,注意該插件不是Gradle的版本,是插件的版本,由google開發(fā)
  • 每個(gè)子項(xiàng)目或模塊下單獨(dú)的build.gradle腳本文件,在這里指定各自依賴的SDK,庫屬性等等,這也是我們編譯的腳本的主體。
apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.1"
    defaultConfig {
        applicationId "com.inpor.fmcdevicedemon"
        minSdkVersion 14
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.0.1'
    testCompile 'junit:junit:4.12'
}
  • apply plugin 指定要加載的插件,這里是一個(gè)應(yīng)用,所以加載com.android.application插件,注意這個(gè)插件是我們上面使用的google開發(fā)的com.android.tools.build:gradle:2.2.3中攜帶的。

  • android閉包來自于google插件,這里查看其DSL文檔:http://google.github.io/android-gradle-dsl/

1.2 Gradle Project

我們的AS項(xiàng)目對于Gradle而言就是一個(gè)個(gè)Gradle項(xiàng)目,而Gradle項(xiàng)目對于我們而言就是一個(gè)個(gè)task構(gòu)成的,我們可以點(diǎn)擊Android Studio的最右邊的Gradle工具欄,可以查看其項(xiàng)目結(jié)構(gòu)。

gradle project

比如上面我們點(diǎn)開other目錄,雙擊第一個(gè)任務(wù),此時(shí)就可以直接這個(gè)任務(wù),生成一個(gè)apk。

gradle project2

2 配置基本編譯參數(shù)

2.1 基本使用

這里主要是指設(shè)置編譯時(shí)指定的SDK、bulidtools版本,包名,應(yīng)用版本號等等,注意這里面定義的屬性會覆蓋AndroidMainfest.xml文件中定義的。

//編譯時(shí)的SDK版本號
compileSdkVersion 25

//編譯時(shí)指定的buildtools版本
buildToolsVersion "25.0.1"

defaultConfig {
    //應(yīng)用的包名
    applicationId "com.inpor.fmcdevicedemon"
    
    //指定應(yīng)用可以安裝到的系統(tǒng)的最低版本,這里14對應(yīng)的是android 4.0
    minSdkVersion 14
    
    //運(yùn)行時(shí)指定使用的sdk的版本,主要針對當(dāng)存在多個(gè)sdk版本時(shí),優(yōu)先使用的SDK版本
    targetSdkVersion 25
    
    //應(yīng)用版本號
    versionCode 1
    versionName "1.0"
    
    //執(zhí)行單元測試時(shí)指定的Runner,在正式打包時(shí)并不會使用到
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

2.2 提取公用字段定義到其他文件中

前面說過我們可以把一個(gè)通用的屬性存放到項(xiàng)目根目錄下的build.gradle中。

//使用ext表示導(dǎo)出
ext {
    compileSdk = 25
    buildTools = "25.0.1"
    targetSdk = 25
    minSdk = 14
}

然后在app的build.gradle文件中使用定義的通用屬性

compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
    applicationId "com.inpor.fmcdevicedemon"
    minSdkVersion rootProject.ext.minSdk
    targetSdkVersion rootProject.ext.targetSdk
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

如果是多項(xiàng)目關(guān)聯(lián),把一些共有的屬性提取出來就很有用了。

2.3 使用resConfigs只打包需要的資源

只打包我們需要的資源。我們知道google給我們的apk提供了國際化支持,如適應(yīng)不同的屏幕分辨率的drawable資源,還有適應(yīng)不同語言的字符串資源等等,但是在很多情況下我們只需要一些指定分辨率和語言的資源就可以了,這個(gè)時(shí)候我們可以使用resConfigs方法來配置。

defaultConfig中添加如下配置之后

defaultConfig {
    .....
    // 過濾,對于國際化支持只打包中文資源,和"xxhdpi"
    // 注意如果在這里指定了dpi,則flavor中不能指定的dpi與這里必須一致否則會報(bào)錯(cuò)
    resConfigs "zh-rCN", "xhdpi"
}

在添加resConfigs之前,反編譯的res目錄截圖:

未過濾圖片

在添加上述resConfigs配置之后,反編譯res目錄:

過濾后的圖片

注意:

  • 使用resConfigs并不會過濾默認(rèn)的drawable, values目錄,這是為了保證App在任何時(shí)候都有一個(gè)默認(rèn)的可選值。
  • resConfigs也可以在后面要講到的productFlavor中也可以使用。

3 signingConfigs(Apk簽名配置)

3.1 配置不同的簽名

在默認(rèn)情況下,AS中編譯apk使用的是SDK中的Debug簽名,不需要顯式的指定簽名配置項(xiàng),在signingConfigs的閉包中我們可以自定義多個(gè)簽名配置,一個(gè)典型的簽名配置:

signingConfigs {
    
    //debug簽名
    debug {
        //簽名秘鑰庫文件的存放的位置,這里使用的是相對路徑
        storeFile file('sign/debug.keystore')
        
        //秘鑰庫的訪問密碼
        storePassword 'android'
        
        //別名,因?yàn)橐粋€(gè)密碼庫可以供多個(gè)項(xiàng)目使用,所以別名不同,最后的簽名也是不同的。
        keyAlias 'androidreleasekey'
        
        //別名的私鑰密碼
        keyPassword 'android'
    }
    
    
    release {
        storeFile file('sign/platform.keystore')
        storePassword 'android'
        keyAlias 'androidreleasekey'
        keyPassword 'android'
    }
}

3.2 從指定文件加載簽名和秘鑰

如果希望不在build.gradle中暴露自己的簽名秘鑰,可以將這些參數(shù)放到一個(gè)專門的文件中,比如在項(xiàng)目的根目錄下添加一個(gè)keystore.properties文件。

//test
debugStoreFile=sign/debug.keystore
debugStorePassword=android
debugKeyAlias=androidreleasekey
debugKeyPassword=android

//release
releaseStoreFile=sign/platform.keystore
releaseStorePassword=android
releaseKeyAlias=androidreleasekey
releaseKeyPassword=android

在app模塊的build.gradle中,解析這個(gè)文件

// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
def keystorePropertiesFile = rootProject.file("keystore.properties")

// Initialize a new Properties() object called keystoreProperties.
def keystoreProperties = new Properties()

// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

android{
    .....
}

修改signConfigs閉包,引用文件中定義的屬性

signingConfigs {
    debug {
        keyAlias keystoreProperties['debugKeyAlias']
        keyPassword keystoreProperties['debugKeyPassword']
        storeFile file(keystoreProperties['debugStoreFile'])
        storePassword keystoreProperties['debugStorePassword']
    }
    
    release{
        keyAlias keystoreProperties['releaseKeyAlias']
        keyPassword keystoreProperties['releaseKeyPassword']
        storeFile file(keystoreProperties['releaseStoreFile'])
        storePassword keystoreProperties['releaseStorePassword']
    }
}

4 編譯類型(buildTypes

在Android studio中我們可以自定義不同的編譯類型,如調(diào)試版本,發(fā)行版本,在不同的版本中可以配置不同的參數(shù)與添加屬性,工程自帶有一個(gè)debug編譯類型,另外用戶無法自定義為testtype,它已經(jīng)被單元測試占用。

比如下面我定義了三個(gè)不同的buildType,分別設(shè)置不同的屬性值

buildTypes {

    debug {
        //指定簽名文件的配置,不指定則使用SDK中默認(rèn)的debug簽名
        signingConfig signingConfigs.debug
        
        //壓縮對齊,提高運(yùn)行時(shí)的效率,也可以使用zipAlignEnabled true
        setZipAlignEnabled(true)
        
        //可以調(diào)試
        debuggable true
        
        //jni可調(diào)試
        jniDebuggable true
        
        //渲染腳本可調(diào)試
        renderscriptDebuggable true
    }


    //在發(fā)行版本中,不允許調(diào)試,并且添加代碼混淆
    release {

        setZipAlignEnabled(true)
        debuggable false
        jniDebuggable false
        renderscriptDebuggable false
        
        //指定簽名文件為release簽名,注意非debug,如果不指定簽名,則打出來的包不會簽名
        signingConfig signingConfigs.release
        
        //minifyEnabled表示代碼是否可以壓縮,裁剪優(yōu)化,需要配合其他的工具一起使用,如proguard
        //添加代碼混淆,注意添加混淆時(shí),必須將minifyEnabled 置為true,否則混淆不生效
        //同樣如果沒有使用代碼混淆必須置為false,否則編譯失敗
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

    }
    
    //自定義一個(gè)type,不使用代碼混淆,并且添加一個(gè)string資源到xml資源文件中
    custom{
        zipAlignEnabled true
        debuggable false
        jniDebuggable false
        renderscriptDebuggable false

        //指定簽名文件為release簽名
        signingConfig signingConfigs.release

        //添加一個(gè)字符資源到values/strings.xml文件中,目前無法指定資源的語言類別
        resValue "string", "custom_name", "測試用戶"
    }

}

上面的minifyEnabled還可以配合shrinkResources屬性使用,移除沒有使用到的資源文件。

buildTypes {
    custom {
        ......
        minifyEnabled true
        shrinkResources true
        ......
    }
}

在實(shí)際測試中發(fā)現(xiàn),上述裁剪可以剪裁布局、圖片、菜單,但是不會移除values。

注意shrinkResources優(yōu)化并不一定會刪除沒有用到的文件,在我的實(shí)際測試中,它會圖片、布局變成最小,沒有刪除它們

當(dāng)我們需要?jiǎng)討B(tài)加載資源時(shí),需要在不要使用該優(yōu)化,否則可能會出現(xiàn)運(yùn)行時(shí)報(bào)錯(cuò)或者顯示效果不正確的問題,如果要使用該優(yōu)化可以在res/raw/keep.xml中進(jìn)行特定資源的保持或優(yōu)化,如下例子不優(yōu)化layout/test_layout

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/test_layout"/>

參考(官網(wǎng)持有資源一章 ):https://developer.android.google.cn/studio/build/shrink-code.html#keep-resources

5 splits(拆分只包含某些需要屬性的apk)

splits

其作用是將當(dāng)前配置的版本的apk分裂生成多個(gè)只包含指定屬性的Apk,目前在google給我們提供根據(jù)language, abi, density進(jìn)行拆分。

//過濾只打包英文和簡體中文的資源
splits{
    //設(shè)置根據(jù)language拆分測試未通過,應(yīng)該是字符串的表現(xiàn)形式不對
//        language{
//            enable true
//            include "values-zh-rCN"
//            include "zh-rCN"
//        }

    density{
        enable true
        reset()  // Clears the default list from all densities to no densities.
        include "mdpi", "xxhdpi" // Specifies the two densities we want to generate APKs for.
    }
}

上面的配置編譯之后會生成3個(gè)Apk:

app-mdpi-custom.apk  //裁剪掉大部分非mdpi資源的apk

app-xxhdpi-custom.apk  //裁減掉大部分非xxdpi資源的apk

app-universal-custom.apk //未做任何裁剪的apk
              

參考Android apk splits 官方文檔:https://developer.android.google.cn/studio/build/configure-apk-splits.html

6 PackagingOptions(指定添加/移除某些文件到最終的apk中)

首先看其DSL結(jié)構(gòu)圖。



PackagingOptions不同于resConfigs,后者過濾某些資源目錄,前者是在打包Apk的時(shí)候(已經(jīng)執(zhí)行過編譯了)排除一些文件,在實(shí)際測試中并不能用于過濾資源文件等等,更多是用于過濾一些與工程沒有直接關(guān)系的文件(聲明、版本控制等等)。

  • First-pick,如果要添加的文件已經(jīng)存在于Apk中,則會被忽略,如果有多個(gè)路徑于指定的pattern,只添加第一個(gè)。
  • Merge,合并的意思,如果文件不存在,則添加,如果文件已經(jīng)存在,則合并內(nèi)容。
  • Exclude,不包含的內(nèi)容,默認(rèn)以下的內(nèi)容不會被打包到Apk中:
/META-INF/LICENCE
/META-INF/LICENCE.txt
/META-INF/NOTICE
/META-INF/NOTICE.txt
/LICENCE
/LICENCE.txt
/NOTICE
/NOTICE.txt
**/.svn/** (all .svn directory contents)
**/CVS/** (all CVS directory contents)
**/SCCS/** (all SCCS directory contents)
**/.* (all UNIX hidden files)
**/.*/** (all contents of UNIX hidden directories)
**/*~ (temporary files)
**/thumbs.db
**/picasa.ini
**/about.html
**/package.html
**/overview.html
**/_*
**/_*/**

PackagingOptions更多的用于去除編譯時(shí)依賴不同的包時(shí),含有相同的文件時(shí),去除編譯時(shí)的重復(fù)錯(cuò)誤中。如:

//在打包時(shí),移除一些許可,注意文檔
packagingOptions {
    exclude 'META-INF/DEPENDENCIES.txt'
    exclude 'META-INF/NOTICE'
    exclude 'META-INF/NOTICE.txt'
    exclude 'META-INF/LICENSE'
    exclude 'META-INF/LICENSE.txt'
}

實(shí)際測試中還沒有發(fā)現(xiàn)有其他的作用,待補(bǔ)充。

官方DSL文檔鏈接:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html

7 lintOptions

lint檢查工具是google開發(fā)的一款代碼掃描工具,其主要用于掃描布局,未使用資源,國際化等等問題,其作用在這里不是我們關(guān)注的重點(diǎn),其使用和配置方法請查看官方文檔:https://developer.android.google.cn/studio/write/lint.html

這里我們要考慮的是link選項(xiàng)對我們打包的影響,要注意link檢查拋出來的錯(cuò)誤,并不會導(dǎo)致編譯時(shí)候的錯(cuò)誤,可能會導(dǎo)致運(yùn)行時(shí)的錯(cuò)誤,以是lintOptions的屬性截圖:

lintOptions

在Android Studio中默認(rèn)下,link檢查報(bào)錯(cuò)會導(dǎo)致編譯中斷,為了避免這個(gè)問題,我們可以在android閉包中添加如下代碼:

android {
    ......
    lintOptions {
        //關(guān)閉編譯release版本的lint檢查
        checkReleaseBuilds false
        
        //關(guān)閉link檢查報(bào)錯(cuò)中斷編譯選項(xiàng)
        abortOnError false
    }
    ......
}

在日常研發(fā)中,我們應(yīng)當(dāng)頻繁執(zhí)行lint檢查,以優(yōu)化代碼和提前暴露一些可能運(yùn)行時(shí)報(bào)錯(cuò)的代碼。

8 productFlavor(產(chǎn)品風(fēng)味)

8.1 基本屬性與方法

productFlavor與其說是產(chǎn)品風(fēng)味還不如說是產(chǎn)品工廠,我們可以根據(jù)不同的需要,最終生成不同的apk。多渠道打包是productFlavor的最常用的功能之一。上面說到的defaultConfig我們可以認(rèn)為一種簡略的默認(rèn)productFlavor,所以我們完全可以在自定義的productFlavor中覆蓋defaultConfig中的任意配置項(xiàng)。

  • 基本屬性
perpties
  • 方法與閉包


    method

參考:http://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.ProductFlavor.html

從上面的屬性與方法中我們發(fā)現(xiàn)可以設(shè)置包名,版本,混淆文件, NDK等等

8.2 示例

現(xiàn)在我創(chuàng)建多個(gè)productFlavor,它們具有不同的包名,不同的版本號

productFlavors{
    sky{
        //直接在原來的包名后加后綴
        applicationIdSuffix ".sky"
        
        //指定不同的版本
        versionName '1.2.0'
        versionCode 120
    }

    gavin{
        //重新命名包名
        applicationId "com.gavin.gradlebuilddemo"
        
        //指定不同的最小編譯版本
        minSdkVersion 17
        targetSdkVersion 21
    }

    smith{
        
        applicationId "com.smith.gradlebuilddemo"
        
        //指定不同的resConfig
        resConfigs "zh-rHK",

        //添加resValue
        resValue 'string', 'smith', 'this is smith'
    }
}

考慮到一種情況,有時(shí)候我們有些公共的資源和配置是某些productFlavors公用的,我們希望把它們提取出來,減少重復(fù),這個(gè)時(shí)候我們可以使用flavorDimensions來實(shí)現(xiàn)我們的需求。

//使用dimensions將一些公共的修改獨(dú)立出來,可以重復(fù)使用,減小代碼的重復(fù)
flavorDimensions 'type', 'common'
    
productFlavors{
    sky{
        dimension 'type'
        
        //直接在原來的包名后加后綴
        applicationIdSuffix ".sky"
        
        //指定不同的版本
        versionName '1.2.0'
        versionCode 120
    }

    gavin{
        dimension 'type'
        
        //重新命名包名
        applicationId "com.gavin.gradlebuilddemo"
        
        //指定不同的最小編譯版本
        minSdkVersion 17
        targetSdkVersion 21
    }

    smith{
        dimension 'type'
        
        applicationId "com.smith.gradlebuilddemo"
        
        //指定不同的resConfig
        resConfigs "en", "hdpi"

        //添加resValue
        resValue 'string', 'smith', 'this is smith'
    }
    
    commonClient{
        dimension 'common'
        
        //添加resValue
        resValue 'string', 'common_client', 'this is common_client'
    }
    
    commonPrivate{
        dimension 'common'
        
        //指定一個(gè)私有的混淆規(guī)則
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules-private.pro'
    }
}

采用上述實(shí)現(xiàn)中,會將兩兩不同的dimension進(jìn)行組合,最終生成我們需要的apk。比如app-sky-commonClient-debug.apk。

現(xiàn)在我們來計(jì)算一下最終可以生成的apk的數(shù)目,我們將flavorDimensions看成數(shù)組的話,最終可以生成的apk的數(shù)目為:

count = BuildType.size * flavorDimensions[0].size * ... flavorDimensions[n].size

這里的flavorDimensions[n].size是指每個(gè)DimensionsproductFlavors中的個(gè)數(shù),比如上面的最終能夠生成的apk個(gè)數(shù)就是:3 * 3(type)* 2(common) = 18。

8.3 資源替換

同樣的我們也可以在src\main的同級目錄下給每個(gè)productFlavors建立目錄存放我們的特定資源。

srcFlavorDir

替換res資源

替換res資源采用的是合并的原則,即最終的資源是所有進(jìn)行合并的資源的并集,出現(xiàn)資源ID重復(fù)的資源,采用優(yōu)先級最高的那個(gè),具體的優(yōu)先級后面會講到。

main/res/values/strings.xml中定義了這樣的資源。

<resources>
    <string name="app_name">GradleBuildDemo</string>
    <string name="hello">hello world</string>
    <string name="enter_button_str">enter</string>
    <string name="cancel_button_str">cancel</string>
    <string name="input_tips">Please input the word you want</string>
    <string name="cancel_tips_msg">button is clicked</string>
</resources>

sky/res/values/strings.xml中重新定義了如下資源。

<resources>
    <string name="app_name">GradleBuildDemo_Sky</string>
    <string name="hello">hello world, sky</string>
    <string name="enter_button_str">enter sky help</string>
    <string name="cancel_button_str">cancel sky</string>
</resources>

最終合并的資源是這樣的。

<resources>
    <string name="app_name">GradleBuildDemo_Sky</string>
    <string name="hello">hello world, sky</string>
    <string name="enter_button_str">enter sky help</string>
    <string name="cancel_button_str">cancel sky</string>
    <string name="input_tips">Please input the word you want</string>
    <string name="cancel_tips_msg">button is clicked</string>
</resources>

注意layout資源是以整個(gè)文件覆蓋的方式合并的。

assets目錄

assets目錄中的文件是以整個(gè)文件覆蓋的方式進(jìn)行合并的。

java原代碼目錄

源碼文件的合并與其他的不同,如果我們想在不同的變體中對一個(gè)類做不同的實(shí)現(xiàn),那么我們就不能在main/java目錄下定義這個(gè)類,只能在每個(gè)變體中單獨(dú)定義,并且路徑要一致,而且對一些變體進(jìn)行組合時(shí),同時(shí)也只能存在一份代碼。

以下示例中,我分別在sky, gavin兩個(gè)flavors中定義了HelpTestActivity類。

sky版本的HelpTestActivity類。

package com.sky.gradlebuilddemo.activity;

import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;

import com.sky.gradlebuilddemo.R;

/**
 * PACKAGE_NAME
 * [function]
 * [detail]
 * Created by Sky on 2016/10/24.
 * modify by
 */

public class HelpTestActivity extends AppCompatActivity {


    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_help_test);
        Button skyButton = (Button) findViewById(R.id.skyButton);

        //點(diǎn)擊按鈕彈出提示文案,
        skyButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final Snackbar snackbar = Snackbar.make(HelpTestActivity.this.getWindow().getDecorView(),
                        "hello snackbar", Snackbar.LENGTH_LONG);
                snackbar.setAction("Change Color", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        snackbar.getView().setBackgroundResource(R.color.colorPrimary);
                    }
                }).show();

            }
        });
    }
}

gavin版本的HelpTestActivity

package com.sky.gradlebuilddemo.activity;

import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;

import com.sky.gradlebuilddemo.R;


/**
 * PACKAGE_NAME
 * [function]
 * [detail]
 * Created by Sky on 2016/10/24.
 * modify by
 */

public class HelpTestActivity extends AppCompatActivity {


    private ListView listView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_help_test);
        listView = (ListView) findViewById(R.id.msgListView);
        String[] msgs = getResources().getStringArray(R.array.listMsg);
        listView.setAdapter(new SimpleListAdapter(this, msgs));
    }

    private static class SimpleListAdapter extends BaseAdapter{

        private String[] data;

        private Context context;

        SimpleListAdapter(Context context, String[]data){
            this.data = data;
            this.context = context;
        }

        @Override
        public int getCount() {
            return data.length;
        }

        @Override
        public Object getItem(int position) {
            return data[position];
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if(convertView == null){
                convertView = LayoutInflater.from(context).inflate(R.layout.list_item_layout, null);
            }
            TextView  textView = (TextView) convertView.findViewById(R.id.itemTextView);
            textView.setText(data[position]);
            return convertView;
        }
    }


}

MainActivity中調(diào)用它。

package com.sky.gradlebuilddemo;

import android.content.Intent;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.sky.gradlebuilddemo.activity.HelpTestActivity;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button enterButton = (Button) findViewById(R.id.enter_button);

        enterButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MainActivity.this.startActivity(new Intent(MainActivity.this, HelpTestActivity.class));
            }
        });

        .......
    }
}

9 sourceSets(資源集合的集合)

9.1 sourceSet與優(yōu)先級

sourceSet就是所謂的源集,包含特定源代碼和所需資源,每一個(gè)源集不是任意命名的,每一個(gè)源集對應(yīng)一個(gè)BuildTypeProductFlavorBuildVariant,看官方的文檔描述。

sourceSet

參考:https://developer.android.google.cn/studio/build/index.html#sourcesets

Android Studio在編譯某個(gè)構(gòu)建變體的時(shí)候,并不是單獨(dú)的使用某個(gè)源集,而是merge不同的源集,比如有一個(gè)SkyCommonClientCustom的構(gòu)建變體,并且定義了skyCommonClientCustom, custom, sky, commonClient這四個(gè)源集,那么在構(gòu)建的時(shí)候就會合并上述的四個(gè)源集和默認(rèn)的main源集的源代碼和資源。

上面提到了merge合并資源,那么合并的優(yōu)先級是怎樣的呢?


SourceSetPority

需要補(bǔ)充一點(diǎn),如果在ProductFlavor使用了flavorDimensions,比如:

flavorDimensions 'type', 'common'

sourceSets{
    sky{
       .....
    }
    
    commonClient{
       .....
    }
}

productFlavors{
    sky{
        dimension 'type'
        //直接在原有包名后面添加
        applicationIdSuffix ".sky"
    }

    commonClient{
        dimension 'common'
    }
}

那么源集sky的優(yōu)先級高于commonClient,所以如果把flavorDimensions看做一個(gè)數(shù)組的話,最終的優(yōu)先級是:

BuildVariant > BuidlType > flavorDimensions[0] > ... > flavorDimensions[x] > main > 內(nèi)容庫依賴項(xiàng)(aar等)

9.2 sourceSet的基本屬性

sourceSet簡單來說就是指定在編譯指定了某些特定源代碼和資源的集合,在Android中使用的是googleAndroidSourceSet

android閉包中的sourceSets就是由上面用戶定義的一系列的AndroidSourceSet的集合。

先看AndroidSourceSet的DSL屬性結(jié)構(gòu)圖:

AndroidSourceSet

從上面的圖中我們可以看出,針對每個(gè)AndroidSourceSet可以配置不同的:

//跨進(jìn)程通信聲明文件(.aidl)
aidl

//assets文件下文件
assets

//.java文件目錄
java

//.c, .cpp文件位置
jni 

//.so文件路徑,注意該路徑只需要指定到所包含平臺的外層,不需要指定到具體的平臺如`armeabi`,否則無法找到SO
jnilibs

//mainfest文件
manifest

//Android resource
res

//(java resource)
resource

//渲染腳本
rendersript

最終根據(jù)這些不同的資源集合生成不同的apk

以下四個(gè)屬性為只讀屬性:

//sourceSet的名稱,如custom
name 

//編譯時(shí)的配置名稱,如customCompile,與后面要講的dependencies的配置項(xiàng)compile相對應(yīng)。
compileConfigurationName

//如customApk,與后面要講的dependencies的配置項(xiàng)apk相對應(yīng)。
packageConfigurationName

//如customProvided,與后面要講的dependencies的配置項(xiàng)provided相對應(yīng)。 
providedConfigurationName

9.3 示例

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

    custom{
        //指定一個(gè)新的jnilibs為根目錄下的jnilibs
        jniLibs.srcDirs=[rootProject.projectDir.absolutePath + '/jnilibs']

        //指定一個(gè)新的assets為根目錄下的skyTestAssets目錄
        assets.srcDirs = [rootProject.projectDir.absolutePath + '/assets/skyTestAssets']
    }
}

上面的例子中我們給custom這個(gè)源集指定了新的jniLibs和Assets目錄,上面的源集也可以采用閉包的形式。

custom{
    
    jniLibs{
        //指定一個(gè)新的jnilibs為根目錄下的jnilibs
        srcDirs=[rootProject.projectDir.absolutePath + '/jnilibs']
    }

    
    assets{
        //指定一個(gè)新的assets為根目錄下的skyTestAssets目錄
        srcDirs = [rootProject.projectDir.absolutePath + '/assets/skyTestAssets']
    }
}

兩點(diǎn)注意:

  • srcDirsrcDirs的區(qū)別,當(dāng)使用srcDir指定文件目錄時(shí),不允許將要合并的源集的同一目錄下有一樣名稱的資源,提示重復(fù)資源異常,而srcDirs則會根據(jù)前面所說的優(yōu)先級進(jìn)行覆蓋。

  • 如果我們在src/main的同級目錄下,也建立一個(gè)如下的文件目錄:

buildTypeCustonDir

當(dāng)在源集中指定了asserts的目錄時(shí),custom/assets目錄會直接失效。

9.4 未解決的問題

在上面的源集中,Android官方構(gòu)建指南沒有提及一點(diǎn),就是如何過濾源集目錄下的一些文件不編譯或打包到最后的apk中,我使用如下方式,希望過濾掉src/main/assets/mainIngoreTest.txt文件不打包到最終的apk中。

sourceSets
{
    .....
    custom{
        //指定一個(gè)新的jnilibs為根目錄下的jnilibs
        jniLibs.srcDirs=[rootProject.projectDir.absolutePath + '/jnilibs']

        //指定一個(gè)新的assets為根目錄下的skyTestAssets目錄
        assets.srcDirs = [rootProject.projectDir.absolutePath + '/assets/skyTestAssets']
        
        //意圖過濾掉`src/main/assets/mainIngoreTest.txt`文件
        assets.exclude (project.projectDir.absolutePath + "\\src\\main\\assets\\mainIngoreTest.txt")
        
        //或者采用閉包
        assets.exclude{
            File f = it.file
            println f.absolutePath
            f.absolutePath.endsWith("mainIngoreTest.txt")
        }
        
    }
    .....
}

采用上述方式并不能成功過濾掉文件,并且上面的閉包中的代碼也沒有執(zhí)行,目前還沒有找到原因。

10 Dependencies(依賴內(nèi)容庫)

依賴內(nèi)容庫指的是不是當(dāng)前工程的代碼或資源,它們存在于其他的項(xiàng)目中,它們可以以源碼庫,jar,arr形式存在。

10.1 聲明依賴項(xiàng)的三種方式

先看聲明方式。

android {...}
...
dependencies {


    // 添加含有源碼的模塊依賴
    compile project(":mylibrary")

    // 遠(yuǎn)程二進(jìn)制庫依賴
    compile 'com.android.support:appcompat-v7:25.1.0'

    // 本地庫(jar)依賴
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

下面逐一介紹,以下來自官方文檔。

模塊依賴項(xiàng)

  • compile project(':mylibrary')行聲明了一個(gè)名為mylibrary的本地Android庫模塊作為依賴項(xiàng),這樣的庫可能是另外某個(gè)工程的一部分,此時(shí)是具有源代碼的,注意依賴本地庫模塊時(shí),必須在根目錄下的settings.gradle文件中include它。

遠(yuǎn)程二進(jìn)制依賴項(xiàng)

  • compile 'com.android.support:appcompat-v7:25.1.0' 行通過指定其 JCenter 遠(yuǎn)程倉庫中的標(biāo)識,當(dāng)本地不存在該依賴庫時(shí),則自動從遠(yuǎn)程下載,默認(rèn)存放在sdk/extras/目錄下,當(dāng)然我們也可以在 SDK 管理器下載和安裝特定的依賴項(xiàng)。

本地二進(jìn)制依賴項(xiàng)

  • 簡單來說就是依賴已經(jīng)打包好的jar庫,compile fileTree(dir: 'libs', include: ['*.jar'])的意思就是依賴app/libs目錄下的所有的以.jar結(jié)尾的文件。

10.2 配置依賴項(xiàng)

當(dāng)我們希望對依賴項(xiàng)在編譯和打包時(shí)做一些特殊處理的時(shí)候,通過使用不同的關(guān)鍵詞,google給我們提供三種配置方式:

  • compile,最常見的配置項(xiàng),編譯時(shí)依賴,Gradle將此配置依賴項(xiàng)添加到類路徑和最終的apk中,其實(shí)就是說在編譯時(shí)和最終的apk中都需要。

  • apk,其指定的依賴項(xiàng)只在打包最終的apk的時(shí)候才需要,此配置只能和JAR二進(jìn)制依賴項(xiàng)一起使用,而不能與其他庫模塊依賴項(xiàng)或 AAR 二進(jìn)制依賴項(xiàng)一起使用。

  • provided,其指定的依賴項(xiàng),此配置依賴項(xiàng)將添加到類路徑中,只在編譯的時(shí)候需要,不打包到最終的apk中(也就是說運(yùn)行時(shí)無須該依賴項(xiàng)),比如我們編譯時(shí)使用的SDK就屬于這一類,同樣的此配置只能和JAR二進(jìn)制依賴項(xiàng)一起使用,而不能與其他庫模塊依賴項(xiàng)或 AAR 二進(jìn)制依賴項(xiàng)一起使用。

示例:

dependencies { 

    .....

    // 依賴app/apklib下的jar文件,只在apk打包的時(shí)候依賴
    apk fileTree(dir: 'apklib', include: ['*.jar'])
    
    // 依賴app/rovidedlib下的jar文件,只在編譯的時(shí)候依賴
    provided fileTree(dir: 'providedlib', include: ['*.jar'])
}

10.3 指定特定的構(gòu)建變體的依賴項(xiàng)

在實(shí)際構(gòu)建中,我們常常遇到有這樣的需求,我們希望某些依賴項(xiàng)只有在某些特定構(gòu)建變體編譯時(shí)才被依賴,在Android Studio中我們可以指定依賴項(xiàng)在以下特定的構(gòu)建類型下才依賴:

  • BuildTypes
  • BuildVariant
  • ProductFlavors

也就是說我們可以以上述任意一種方式指定特定的依賴項(xiàng),我們知道每一個(gè)buildType, flavor都有一個(gè)與之名字相同的的sourceSet,所以我們要指定依賴項(xiàng)為某特定類型的方式為:

  • sourceSet.compileConfigurationName,如skyCompile
  • sourceSet.packageConfigurationName,如skyApk
  • sourceSet.providedConfigurationName,如skyProvided

以下是具體示例。

dependencies {

    // BuildTypes為AndroidTest,做單元測試時(shí)才編譯
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    
    
    // BuildTypes為test的 依賴項(xiàng)junit
    testCompile 'junit:junit:4.12'

    // BuildTypes為debug的添加含有源碼的模塊依賴
    debugCompile project(":mylibrary")
    
    // flavor為sky的指定jar庫,且不使用`okhttp-3.2.0.jar`庫,使用skyCommon目錄下的okhttp-3.3.1.jar
    skyCompile fileTree(include: ['*.jar'], dir: 'jar/sky', excludes: ['okhttp-3.2.0.jar'])
    
    // 構(gòu)建變體依賴項(xiàng)指定
    skyCommonClientCustomCompile fileTree(include: ['*.jar'], dir: 'jar/skyCommonClientCustom')

    //以下是正常的依賴
    // 遠(yuǎn)程二進(jìn)制庫依賴
    compile 'com.android.support:appcompat-v7:25.1.0'

    // 本地庫(jar)依賴
    compile fileTree(dir: 'libs', include: ['*.jar'])
    
    compile 'com.android.support:design:24.2.1'
}

注意,在上述使用xxxCompile時(shí),有時(shí)會提示找不到對應(yīng)的xxxCompile方法的錯(cuò)誤:

Error:(180, 0) Could not find method skyCommonClientCustomCompile() for arguments [directory 'jar/skyCommonClientCustom'] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
<a href="openFile:E:\Source\GitHub\GradleBuildDemo\app\build.gradle">Open File</a>

解決辦法為,我們在sourceSets閉包下,新建一個(gè)空的對應(yīng)的soruceSet就可以了。

sourceSets{
    .....

    // 此處增加一個(gè)空的sourceSet,是為了解決在dependencies中使用
    // skyCommonClientCustomCompile 指定依賴項(xiàng)時(shí)提示找不到方法的錯(cuò)誤
    skyCommonClientCustom{

    }

}

資源合并的規(guī)則同樣適用與依賴合并,所以在指定特定依賴后,構(gòu)建某個(gè)特定變體時(shí)(flavorType),其編譯時(shí)最終的依賴項(xiàng)(不考慮provided)就變?yōu)椋?/p>

flavorTypeCompile + typeCompile + flavorCompile + compile

指定依賴項(xiàng)的構(gòu)建類型

我們可以直接指定依賴項(xiàng)的構(gòu)建類型,這里的構(gòu)建類型可以是BuildVariant, buildType, flavor

dependencies {
    ...
    // relase構(gòu)建時(shí)指定依賴的`library`也是release
    releaseCompile project(path: ':library', configuration: 'release')
    
    // debug構(gòu)建時(shí)指定依賴的`library`的構(gòu)建也是`debug`
    debugCompile project(path: ':library', configuration: 'debug')
    ......
}

10.4 transitive, force, exclude的使用與依賴沖突解決

通過gradle命令查看依賴樹,在模塊所在的目錄(app目錄),執(zhí)行gradle dependencies,執(zhí)行結(jié)果如圖(以androidTest為例)。

android_test_dependenice

transitive

transitive用于自動處理子依賴項(xiàng)。默認(rèn)為true,gradle自動添加子依賴項(xiàng),形成一個(gè)多層樹形結(jié)構(gòu);設(shè)置為false,則需要手動添加每個(gè)依賴項(xiàng)。

  • 為所有的配置指定自動添加子依賴項(xiàng)為false
configurations.all {
   transitive = false
}
  • 為單獨(dú)的某個(gè)依賴項(xiàng)指定字典添加子依賴項(xiàng)為false
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
       transitive = false 
    })

force

即強(qiáng)制設(shè)置某個(gè)模塊的版本。

configurations.all {
   resolutionStrategy {
       force 'com.android.support.test:runner:0.2'
   }
}

以上設(shè)置之后所有對com.android.support.test:runner模塊有依賴的其他庫都被強(qiáng)制使用0.2版本。

exclude

排除依賴項(xiàng)中的某些子依賴項(xiàng),這在解決依賴庫版本沖突或者重復(fù)時(shí)特別有用,我們可以通過如下兩種方式進(jìn)行排除:

  • groupmaven項(xiàng)目的GroupId,GroupID是項(xiàng)目組織唯一的標(biāo)識符,對于小型的項(xiàng)目,常常對應(yīng)JAVA的包的結(jié)構(gòu),是main目錄里java的目錄結(jié)構(gòu),但是也可以很多個(gè)項(xiàng)目共用一個(gè)GroupID,如com.android.support下就有很多個(gè)子項(xiàng)目。

  • module, maven項(xiàng)目的ArtifactID,ArtifactID就是具體某個(gè)項(xiàng)目的唯一的標(biāo)識符,實(shí)際常常對應(yīng)項(xiàng)目的名稱,就是項(xiàng)目根目錄的名稱,如support-annotations

groupmodule可以配合一起使用也可以單獨(dú)使用。

  • 配合使用
//移除所有依賴項(xiàng)中,組織為`com.android.support`項(xiàng)目為`support-annotations`的子依賴項(xiàng)
configurations {
   all*.exclude group: 'com.android.support', module: 'support-annotations'
}

//移除單個(gè)依賴項(xiàng)中,組織為`com.android.support`項(xiàng)目為`support-annotations`的子依賴項(xiàng)
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
})
  • 單獨(dú)使用groupmodule
//移除所有依賴項(xiàng)中,組織為`com.android.support`的子依賴項(xiàng)
configurations {
   all*.exclude group: 'com.android.support'
}

//移除單個(gè)依賴項(xiàng)中,組織為`com.android.support`的子依賴項(xiàng)
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support'
})

  • 單獨(dú)使用module
//移除所有依賴項(xiàng)中名為`support-annotations`的子依賴項(xiàng)
configurations {
   all*.exclude module: 'support-annotations'
}

//移除單個(gè)依賴項(xiàng)中名為`support-annotations`的子依賴項(xiàng)
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude module: 'support-annotations'
})

依賴項(xiàng)的版本沖突

gradle在同一個(gè)配置下(例如androidTestCompile),某個(gè)模塊的不同版本同時(shí)被依賴時(shí),默認(rèn)使用最新版,gradle同步時(shí)不會報(bào)錯(cuò),例如:

dependencies {
   androidTestCompile('com.android.support.test:runner:0.4')
   androidTestCompile('com.android.support.test:rules:0.2')
   androidTestCompile('com.android.support.test.espresso:espresso-core:2.1')
}

上面espresso:espresso-core依賴runner不同與上面我們上面指定的版本的runner,此時(shí)gradle會自動同步最新版本。

對于不同的配置下,出現(xiàn)的依賴項(xiàng)不一樣,gradle就會直接報(bào)錯(cuò),這時(shí)我們就可以使用上面提到的force, exclude來解決。

10.5 參考

11 綜合

11.1 過濾不需要生成的apk

當(dāng)我們添加自定義的BuildType, flavor時(shí),必然會組合出很多我們不需要的apk,google也給我們提供了解決方案。

核心就是使用variantFilter這個(gè)方法過濾滿足我們特定條件的所有構(gòu)建變體。

// 移除不需要打包的apk
variantFilter { variant ->
    String buildTypeName = variant.buildType.name
    String flavors0Name = variant.getFlavors().get(0).name

    //對于編譯類型為`release 或者 custom` 并且 flavors0類型為`smith 或 gavin`的構(gòu)建類型直接忽略,不用編譯
    if((buildTypeName.equals('release') || buildTypeName.equals('custom'))
            && (flavors0Name.equals('smith') || flavors0Name.equals('gavin'))) {
        variant.setIgnore(true);
    }
}

11.2 apk名稱修改

Android studio 構(gòu)建生成的apk的默認(rèn)名稱為app-flavor[0]-...-flavor[n]-buildType.apk,google給我們提供了修改Apk名稱的方法。

applicationVariants包含了所有可能構(gòu)建變體的集合,我們使用閉包遍歷所有的輸出,修改我們想修改的apk的名稱,以下是一個(gè)示例。

//修改生成的apk的名稱,命名為demo-flavorsName-buildType-versionName.apk
applicationVariants.all { variant ->
    //遍歷所有的輸出文件
    variant.outputs.each { output ->
        File tempFile = output.outputFile
        //對于包含`commonClient` flavor的我們在名稱中去掉它
        if (variant.productFlavors[1].name.contains("commonClient")) {

            output.outputFile = new File(tempFile.parent, tempFile.name.replace(tempFile.name,
                    "demo" + variant.productFlavors[0].name + "_" + variant.buildType.name + "_${variant.versionName}.apk"))
        } else {
            output.outputFile = new File(tempFile.parent, tempFile.name.replace(tempFile.name,
                    "demo" + variant.productFlavors[0].name + "_" + variant.productFlavors[1].name + "_" + variant.buildType.name + "_${variant.versionName}.apk"))
        }
    }
}

11.3 mainfest文件添加屬性

當(dāng)我們打多渠道包或需要給不同的構(gòu)建變體加入的不同的屬性時(shí),此時(shí)我們就需要修改mainfest文件,gradle給我們提供了兩種方式動態(tài)的修改mainfest文件。

在變體對應(yīng)src目錄下添加一個(gè)mainfest文件

在添加的mainfest文件中添加/修改配置項(xiàng),此種方式與動態(tài)合并res/values/strings.xml的方式一致,其遵守的規(guī)則也和它們保持一致,這種方式不僅能添加屬性,還能添加四大組件等等,具體可以參考:

Mainfest合并規(guī)則:https://developer.android.google.cn/studio/build/manifest-merge.html

使用APG中的manifestPlaceholders屬性

manifestPlaceholdersbuild.gradle中以鍵值對的方式給mainfest中對應(yīng)鍵設(shè)置相應(yīng)的值的方式來實(shí)現(xiàn),看如下示例,我在mainfest文件中添加以下需要?jiǎng)討B(tài)設(shè)置的屬性。

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        
        .....
        <!--渠道配置信息AppKey  -->
        <meta-data
            android:name="APP_KEY"
            android:value="${APP_KEY_VALUE}"/>

        <!-- 產(chǎn)品ID -->
        <meta-data
            android:name="APP_ID"
            android:value="${APP_ID_VALUE}"/>
    
        .....

   </application>

接下來我分別在defaultCofig, sky, gavin三個(gè)變體中從不同路徑的配置文件config.xml中讀取屬性,這里我把解析xml文件放在根目錄下自定義的utils.gradle文件中。


/**
 * 解析XML文件
 * */
def parseXml(String path) {
    println ("parseXml called, path = " + path)
    return new XmlParser().parse(path)
}


ext {
    parseXml       = this.&parseXml
}

然后在模塊的build.gradle文件中引用它。

// 加載自定義的utils.gradle
apply from: rootProject.projectDir.getAbsolutePath() + File.separator + "utils.gradle"

最后在三個(gè)變體中解析相應(yīng)的配置文件。

defaultConfig {
    
    ......

    def defaultConfig = parseXml("app/config/main/config.xml")
    manifestPlaceholders = [
            APP_KEY_VALUE : defaultConfig.appKey[0].text(),
            APP_ID_VALUE  : defaultConfig.id[0].text()
    ]
}

flavor{

    sky{
        ......
        def skyConfig = parseXml("app/config/sky/config.xml")
        manifestPlaceholders = [
            APP_KEY_VALUE : skyConfig.appKey[0].text(),
            APP_ID_VALUE  : skyConfig.id[0].text()
        ]
    }
    
    gavin{
        ......
        def gavinConfig = parseXml("app/config/gavin/config.xml")
        manifestPlaceholders = [
                APP_KEY_VALUE : gavinConfig.appKey[0].text(),
                APP_ID_VALUE  : gavinConfig.id[0].text()
        ]
        
    }
    
    ......
}

這樣最后構(gòu)建出來的apk使用的就是上面配置的不同的值。

11.4 APG動態(tài)生成的BuildConfig類的使用

使用BuildConfig

使用APG構(gòu)建apk,會動態(tài)自動生成一個(gè)BuildConfig類,里面會包含當(dāng)前構(gòu)建變體的一些基本屬性,如版本號等等,一下是一個(gè)默認(rèn)下的示例(當(dāng)前構(gòu)建變體是skyCommonClientDebug)。

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.sky.gradlebuilddemo;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.inpor.fmcdevicedemon.sky";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "skyCommonClient";
  public static final int VERSION_CODE = 120;
  public static final String VERSION_NAME = "1.2.0";
  public static final String FLAVOR_type = "sky";
  public static final String FLAVOR_common = "commonClient";
}

一種最典型的用法,就是在源代碼中判斷當(dāng)前是不是debug版本,然后做某些操作,如果是其他版本又做什么操作等等。

一個(gè)示例,在MainActivity中添加如下代碼。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    
    .....

    private void checkBuildConfig(){
        if(BuildConfig.DEBUG){
            Log.i(TAG, "now this is debug build");
        }
    }
}

自定義BuildConfig屬性

APG給我們提供了自定義BuildConfig屬性的方法buildConfigField,注意,添加Field時(shí),最好在defaultConfig中給要添加的Field設(shè)置一個(gè)默認(rèn)值,否則當(dāng)編譯其他沒有設(shè)置該Field的變體時(shí),會編譯報(bào)錯(cuò),我們可以在buildTypes, flavors中復(fù)寫它。

在下面的示例中我添加一個(gè)字段。

defaultConfig {
    
    ......
    buildConfigField 'int', 'ID', '0'
}

flavor{

    sky{
        ......
        buildConfigField 'int', 'ID', '1'
    }
    
    ......
}

最終生成的BuildConfig是這樣的。

/**
 * Automatically generated file. DO NOT MODIFY
 */
package com.sky.gradlebuilddemo;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.inpor.fmcdevicedemon.sky";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "skyCommonClient";
  public static final int VERSION_CODE = 120;
  public static final String VERSION_NAME = "1.2.0";
  public static final String FLAVOR_type = "sky";
  public static final String FLAVOR_common = "commonClient";
  // Fields from product flavor: sky
  public static final int ID = 1;
  // Fields from default config.
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容