使用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混淆配置 |
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不能進行程式碼混