1. 程式人生 > >使用Gradle管理你的Android Studio工程

使用Gradle管理你的Android Studio工程

Gradle簡介

Gradle 是一個基於Ant和Maven概念的專案自動化建構工具。它使用一種基於Groovy的特定領域語言(DSL)來宣告專案設定,這比我們的ANT使用XML構建配置要靈活的多。在編寫配置時,你可以像程式設計一樣靈活,Gradle是基於Groovy的DSL語言,完全相容JAVA

Gradle入門

projects 和 tasks是Gradle中最重要的兩個概念,任何一個Gradle構建都是由一個或者多個project組成,每個project可以是一個jar包,一個web應用,或者一個android app等,每個project又由多個task構成,一個task其實就是構建過程中一個原子性的操作,比如編譯、拷貝等。

一個build.gradle檔案是一個構建指令碼,當執行gradle命令的時候會從當前目錄查詢build.gradle檔案來執行構建。下面我們來看下gradle的Hello World。在build.gradle構建檔案中輸入以下構建指令碼:

1
2
3
4
5
task hello {
    doLast {
        println 'Hello world!'
    }
}

task定義了一個任務,這個任務名字是hello。doLast是Task的方法,意思是在該hello任務執行之後作的事情,可以用一個閉包配置它,這裡是輸出Hello world!字串。我們在終端裡執行如下命令執行檢視結果:

1
2
$gradle hello -q
Hello world!

其他關於Gradle的更多介紹請參考Gradle使用指南

Android Studio入門

使用Android Studio新建一個工程之後,其目錄結構是這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├── app #Android App目錄
│   ├── app.iml
│   ├── build #構建輸出目錄
│   ├── build.gradle #構建指令碼
│   ├── libs #so相關庫
│   ├── proguard-rules.pro #proguard混淆配置
│ └── src #原始碼,資源等 ├── build │ └── intermediates ├── build.gradle #工程構建檔案 ├── gradle │ └── wrapper ├── gradle.properties #gradle的配置 ├── gradlew #gradle wrapper linux shell指令碼 ├── gradlew.bat ├── LibSqlite.iml ├── local.properties #配置Androod SDK位置檔案 └── settings.gradle #工程配置

settings.gradle用於配置project,標明其下有幾個module,比如這裡包含一個:app module

1
include ':app'

和settings.gradle在同一目錄下的build.gradle是一個頂級的build配置檔案,在這裡可以為所有project以及module配置一些常用的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()//使用jcenter庫
    }
    dependencies {
        // 依賴android提供的1.1.0的gradle build
        classpath 'com.android.tools.build:gradle:1.1.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}
//為所有的工程的repositories配置為jcenters
allprojects {
    repositories {
        jcenter()
    }
}

Android Gradle基本配置

下面著重說一下Android的Gradle,畢竟對Android開發來說,這才是重中之重。這裡以初始化好的build.gradle為例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "22.0.1"

    defaultConfig {
        applicationId "org.flysnow.demo"
        minSdkVersion 9
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.0.0'
}

開頭第一行apply plugin: ‘com.android.application’,這表示該module是一個app module,應用了com.android.application外掛,如果是一個android library,那麼這裡的是apply plugin: ‘com.android.library’。

其次是基於哪個SDK編譯,這裡是API LEVEL,是21,buildToolsVersion是基於哪個構建工具版本進行構建的。defaultConfig是預設配置,如果沒有其他的配置覆蓋,就會使用這裡的。看其屬性的名字就可以知道其作用,比如applicationId是配置包名的,versionCode是版本號,versionName是版本名稱等。

buildTypes是構建型別,常用的有release和debug兩種,可以在這裡面啟用混淆,啟用zipAlign以及配置簽名信息等。

dependencies就不屬於Android專有的配置了,它定義了該module需要依賴的jar,aar,jcenter庫資訊。

配置應用的簽名信息

在android.signingConfigs{}下定義一個或者多個簽名信息,然後在buildTypes{}配置使用即可。比如這裡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
android {

    signingConfigs {
        release {
            storeFile file("release.keystore")
            keyAlias "release"
            keyPassword "123456"
            storePassword "123456"
        }
        debug {
            ...
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
        debug {
            signingConfig signingConfigs.debug
        }
    }
  }

storeFile是簽名證書檔案,keyAlias是別名,keyPassword是key的密碼,storePassword是證書的密碼。配好好相關資訊即可在buildTypes配置使用。

啟用proguard混淆

我們可以為不同的buildTypes選擇是否啟用混淆,一般release釋出版本是需要啟用混淆的,這樣別人反編譯之後就很難分析你的程式碼,而我們自己開發除錯的時候是不需要混淆的,所以debug不啟用混淆。對release啟用混淆的配置如下:

1
2
3
4
5
6
7
8
9
android {

    buildTypes {
        release {
            minifyEnabled true
            proguardFile 'proguard.cfg'
        }
   }
}

minifyEnabled為true表示啟用混淆,proguardFile是混淆使用的配置檔案,這裡是module根目錄下的proguard.cfg檔案

啟用zipAlign

這個也是比較簡單的,同樣也是在buildTypes裡配置,可以為不用的buildTypes選擇時候開啟zipAlign

1
2
3
4
5
6
7
8
android {

    buildTypes {
        release {
            zipAlignEnabled true
        }
   }
}

多渠道打包

東西到了國內就變了,做什麼都是一窩蜂,比如Android App市場就是,所以才有了多渠道打包,每次發版幾十個渠道包。還好Android Gradle給我們提供了productFlavors,讓我們可以對生成的APK包進行定製,所以就有了多渠道。

1
2
3
4
5
6
7
8
9
10
11
12
13
android  {
    productFlavors {
        dev{

        }
        google{

        }
        baidu{

        }
    }
}

這樣當我們執行assembleRelease的時候就會生成3個release包,分別是dev、google以及baidu的。目前看這三個包除了檔名沒有什麼不一樣,因為我們還沒有定製,使用的都是defaultConfig配置。這裡的flavor和defaultConfig是一樣的,可以自定義其applicationId、versionCode以及versionName等資訊,比如區分不同包名:

1
2
3
4
5
6
7
8
9
10
11
12
13
android  {
    productFlavors {
        dev{
            applicationId "org.flysnow.demo.dev"
        }
        google{
            applicationId "org.flysnow.demo.google"
        }
        baidu{
            applicationId "org.flysnow.demo.baidu"
        }
    }
}

批量修改生成的apk檔名

在我們打包發版的時候,一次性打幾十個包,這時候我們就想讓生成的apk檔名有區分,比如一眼就能看出這個apk是哪個版本的,哪個渠道的,是哪天打的包等等,這就需要我們在生成apk檔案的時候動態修改生成的apk檔名達到這一目的。這裡以我們的產品隨手記為例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def buildTime() {
    def date = new Date()
    def formattedDate = date.format('yyyyMMdd')
    return formattedDate
}

android {
    buildTypes {
        release {
            applicationVariants.all { variant ->
                variant.outputs.each { output ->
                    if (output.outputFile != null && output.outputFile.name.endsWith('.apk')
                        &&'release'.equals(variant.buildType.name)) {
                        def apkFile = new File(
                                output.outputFile.getParent(),
                                "Mymoney_${variant.flavorName}_v${variant.versionName}_${buildTime()}.apk")
                        output.outputFile = apkFile
                    }
                }
            }
        }
    }
}

以baidu渠道為例,以上的程式碼會生成一個名字為Mymoney_baidu_v9.5.2.6_20150330.apk安裝包。下面我們分析一下,Android Gradle任務比較複雜,它的很多工都是自動生成的,為了可以更靈活的控制,Android Gradle提供了applicationVariants、libraryVariants以及testVariants,他們分別適用於app、library、app和library都適用。

這裡是迴圈處理每個applicationVariant,當他們的輸出檔名以apk結尾並且buildType是release時,重新設定新的輸出檔名,這樣就達到了我們批量修改生成的檔名的目的。

AndroidManifest裡的佔位符

AndroidManifest.xml這是一個很重要的檔案,我們的很多配置都在這裡定義。有時候我們的一些配置資訊,比如一個第三方應用的key,第三方統計分析的渠道號等也要在這裡進行配置。這裡以友盟統計分析平臺為例,演示這一功能的使用。在友盟統計分析中,我們需要根據渠道進行統計,比如google,百度,應用寶等渠道的活躍新增等,友盟的SDK是在AndroidManifest裡配置一個name為UMENG_CHANNEL的meta-data,這樣這個meta-data的值就表示這個apk是哪個渠道,我們版本釋出有幾十個渠道,以前ant打包的時候是採用文字替換的辦法,現在Gradle有更好的處理辦法,那就是manifestPlaceholders,它允許我們動態替換我們在AndroidManifest檔案裡定義的佔位符。

1
<meta-data android:value="${UMENG_CHANNEL_VALUE}" android:name="UMENG_CHANNEL"/>

如上${UMENG_CHANNEL_VALUE}就是一個佔位符,然後我們在gradle的defaultConfig;裡這樣定義指令碼:

1
2
3
4
5
android {
    defaultConfig {
        manifestPlaceholders = [UMENG_CHANNEL_VALUE: 'dev']
    }
}

以前的意思就是我們的預設配置裡AndroidManifest的${UMENG_CHANNEL_VALUE}佔位符會被dev這個字串所替換,也就說預設執行的版本是一個開發板。以此類推,我們其他渠道的版本就可以這樣定義:

1
2
3
4
5
6
7
8
9
10
11
12
android  {
    productFlavors {
        google{
            applicationId "org.flysnow.demo.google"
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE",'google')
        }
        baidu{
            applicationId "org.flysnow.demo.baidu"
            manifestPlaceholders.put("UMENG_CHANNEL_VALUE",'baidu')
        }
    }
}

這樣有多少個渠道就做多少次這樣的定義,即可完成分渠道統計。但是如果上百個渠道,這樣一個個寫的確太累,很麻煩,我們繼續研究,同學們有沒有發現,我們的渠道名字和我們的flavorName一樣,我們用這個flavorName作為UMENG_CHANNEL_VALUE不就好了嗎,可以批量的替換嗎?當然可以,這又體現了我們Gradle的強大和靈活之處。

1
2
3
productFlavors.all { flavor ->
        manifestPlaceholders.put("UMENG_CHANNEL_VALUE",name)
    }

迴圈每個flavor,並把他們的UMENG_CHANNEL_VALUE設定為他們自己的name名字,ok,搞定。

自定義你的BuildConfig

BuildConfig.java是Android Gradle自動生成的一個java類檔案,無法手動編譯,但是可以通過Gradle控制,也就是說他是動態可配置的,有了這個功能就很好玩了,這裡以生產環境和測試環境為例來說明該功能的使用。

我們在開發App的時候免不了要和伺服器進行通訊,我們的伺服器一般都有生產和測試環境,當我們處理開發和測試的時候使用測試環境進行除錯,正式釋出的時候使用生成環境。以前的時候我們通過把不同的配置檔案打包進APK中來控制,現在不一樣了,我們有更簡便的方法,這就是buildConfigField。

1
2
3
4
5
6
7
8
9
10
11
12
13
android {
    defaultConfig {
        buildConfigField 'String','API_SERVER_URL','"http://test.flysnow.org/"'
    }
    productFlavors {
        google{
            buildConfigField 'String','API_SERVER_URL','"http://www.flysnow.org/"'
        }
        baidu{
            buildConfigField 'String','API_SERVER_URL','"http://www.flysnow.org/"'
        }
    }
}

buildConfigField 一共有3個引數,第一個是資料型別,就是你定義的常量值是一個什麼型別,和Java的型別是對等的,這裡是String。第二個引數是常量名,這裡是API_SERVER_URL。第三個引數是常量值。如此定義之後,就會在BuildConfig.java中生成一個常量名為API_SERVER_URL的常量定義。預設配置的生成是:

1
public final static String API_SERVER_URL = "http://test.flysnow.org/"

當是baidu和google渠道的時候生成的就是http://www.flysnow.org/了。這個常量可以在我們編碼中引用。在我們進行打包的時候會根據Gradle配置動態替換。

我們發現一般渠道版本都是用來發布的,肯定用的是生產伺服器,所以我們可以使用批處理來搞定這個事情,而不用在一個個渠道里寫這些配置。

1
2
3
productFlavors.all { flavor ->
        buildConfigField 'String','API_SERVER_URL','"http://www.flysnow.org/"'
    }

此外,比如Gradle的resValue,也是和buildConfigField,只不過它控制生成的是資源,比如我們在android的values.xml定義生成的字串。可以用它來動態生成我們想要的字串,比如應用的名字,可能一些渠道會不一樣,這樣就可以很靈活的控制自動生成,關於resValue詳細介紹請參考相關文件,這裡不再舉例說明。

插裝測試覆蓋率程式碼

程式碼覆蓋率現在已經成為檢驗單元測試是否覆蓋到的一種手段,Android Gradle提供了原生的用於單元測試的程式碼覆蓋率,這個就是jacoco。今天我們不談這個,我想要的是在我們生成的APK包中已經包含了檢測程式碼覆蓋率的程式碼,這樣當我們安裝APK後執行進行一些測試的時候,這些檢測程式碼覆蓋率的程式碼就會被執行到,這樣最後我們匯出一份程式碼測試覆蓋率的檔案,然後生成檢視測試覆蓋率報告看哪些覆蓋到,哪些沒有覆蓋到。這種場景在檢測測試工程師測試功能以及Android UI自動化測試是否完全覆蓋尤為有效。這裡程式碼覆蓋率框架我選擇的是emma,一來這個在Ant打包的時候一直在用,二來它具有很方便的插裝功能。

emma插裝的是class檔案,所以我們只能在編譯完java檔案生成class檔案後進行插裝,這是我們進行覆蓋率程式碼插裝的最好時機。找到了時機,那麼具體對應在Gradle指令碼上是哪呢?還記不記得我們上面講的applicationVariants,每一個applicationVariant都有一個javaCompile屬性,javaCompile是一個JavaCompile型別的Task,這個就是負責編譯java程式碼的。是Task就有doLast方法,就是在這個任務本身完成之後要做的事情,我們就是在這個方法裡進行我們的程式碼覆蓋率的安裝。一般我們這個插裝只是在特性情況下,那麼我們新增一個特殊的flavor好了,專門做這個使用,這裡我姑且叫feature。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
applicationVariants.all { variant ->
    //為feature 版本加上程式碼覆蓋率
    if('feature'.equals(variant.flavorName)){
        variant.javaCompile.doLast {
            def coverageFile=file('out/coverage.em')
            if(coverageFile.exists()){
                coverageFile.delete()
            }
            javaexec {
                main 'emma'
                args 'instr','-ip',variant.javaCompile.destinationDir,'-m','overwrite','-out','out/coverage.em'
                classpath files(new File(getSdkDirectory(),'tools/lib/emma.jar'))
            }
        }
    }
}

非常簡單,我們使用javaexec命令執行java應用程式程序插裝,插裝模式使用的是overwrite,就是插裝後覆蓋原始檔。getSdkDirectory()函式獲取你電腦上的Android SDK目錄,這裡我們使用SDK自帶的emma,保持每個人的統一。另外注意進行程式碼覆蓋率插裝的APK不能進行程式碼混