編輯:關於Android編程
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構建文件中輸入以下構建腳本:
task hello { doLast { println 'Hello world!' } }
task定義了一個任務,這個任務名字是hello。doLast是Task的方法,意思是在該hello任務執行之後作的事情,可以用一個閉包配置它,這裡是輸出Hello world!字符串。我們在終端裡執行如下命令運行查看結果:
$gradle hello -q Hello world!
其他關於Gradle的更多介紹請參考Gradle使用指南
Android Studio入門
使用Android Studio新建一個工程之後,其目錄結構是這樣的:
├── 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
include ':app'
和settings.gradle在同一目錄下的build.gradle是一個頂級的build配置文件,在這裡可以為所有project以及module配置一些常用的配置。
// 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為例。
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{}配置使用即可。比如這裡
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啟用混淆的配置如下:
android { buildTypes { release { minifyEnabled true proguardFile 'proguard.cfg' } } }
minifyEnabled為true表示啟用混淆,proguardFile是混淆使用的配置文件,這裡是module根目錄下的proguard.cfg文件
啟用zipAlign
這個也是比較簡單的,同樣也是在buildTypes裡配置,可以為不用的buildTypes選擇時候開啟zipAlign
android { buildTypes { release { zipAlignEnabled true } } }
多渠道打包
東西到了國內就變了,做什麼都是一窩蜂,比如Android App市場就是,所以才有了多渠道打包,每次發版幾十個渠道包。還好Android Gradle給我們提供了productFlavors,讓我們可以對生成的APK包進行定制,所以就有了多渠道。
android { productFlavors { dev{ } google{ } baidu{ } } }
這樣當我們運行assembleRelease的時候就會生成3個release包,分別是dev、google以及baidu的。目前看這三個包除了文件名沒有什麼不一樣,因為我們還沒有定制,使用的都是defaultConfig配置。這裡的flavor和defaultConfig是一樣的,可以自定義其applicationId、versionCode以及versionName等信息,比如區分不同包名:
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文件名達到這一目的。這裡以我們的產品隨手記為例:
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文件裡定義的占位符。
<meta-data android:value="${UMENG_CHANNEL_VALUE}" android:name="UMENG_CHANNEL"/>
如上${UMENG_CHANNEL_VALUE}就是一個占位符,然後我們在gradle的defaultConfig;裡這樣定義腳本:
android { defaultConfig { manifestPlaceholders = [UMENG_CHANNEL_VALUE: 'dev'] } }
以前的意思就是我們的默認配置裡AndroidManifest的${UMENG_CHANNEL_VALUE}占位符會被dev這個字符串所替換,也就說默認運行的版本是一個開發板。以此類推,我們其他渠道的版本就可以這樣定義:
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的強大和靈活之處。
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。
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的常量定義。默認配置的生成是:
public final static String API_SERVER_URL = "http://test.flysnow.org/"
當是baidu和google渠道的時候生成的就是http://www.flysnow.org/了。這個常量可以在我們編碼中引用。在我們進行打包的時候會根據Gradle配置動態替換。
我們發現一般渠道版本都是用來發布的,肯定用的是生產服務器,所以我們可以使用批處理來搞定這個事情,而不用在一個個渠道裡寫這些配置。
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。
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不能進行代碼混淆,這個很簡單,為feature flavor指定不混淆的proguardFile覆蓋默認的proguardFile即可。最後該APK需要emma的框架代碼,所以要配置feature flavor的特殊依賴信息。
dependencies { compile fileTree(dir: 'libs', include: '*.jar') featureCompile files(new File(android.getSdkDirectory(),'tools/lib/emma_device.jar')) } dexOptions javaMaxHeapSize
在Gradle 進行dex的可能會遇到內存不夠用的情況,錯誤信息大概是java.lang.OutOfMemoryError: GC overhead limit exceeded。這個時候只需要配置dexOptions的javaMaxHeapSize大小即可,我這裡配置4g:
dexOptions { javaMaxHeapSize "4g" }