Android Studio已經成爲現在Android 開發的主要工具,在開發過程中學習使用gradle顯得格外重要,本篇博客,我們一起學習gradle打包的一些知識。
Gradle 安裝
1.安裝JDK,並配置JAVA_HOME環境變量。因爲Gradle是用Groovy編寫的,而Groovy基於JAVA。另外,Java版本要不小於1.5.
2.下載。地址是:http://www.gradle.org/downloads
選擇Download,選擇相應的complete版本即可
3.解壓。如果你下載的是gradle-xx-all.zip的完整包,它會有以下內容:
二進制文件
用戶手冊(包括PDF和HTML兩種版本)
DSL參考指南
API手冊(包括Javadoc和Groovydoc)
樣例
源代碼,僅供參考使用。
4.配置環境變量。配置GRADLE_HOME到你的gradle根目錄當中,然後把%GRADLE_HOME%/bin(linux或mac的是$GRADLE_HOME/bin)加到PATH的環境變量。
配置完成之後,運行gradle -v,檢查一下是否安裝無誤。如果安裝正確,它會打印出Gradle的版本信息,包括它的構建信息,Groovy, Ant, Ivy, 當前JVM和當前系統的版本信息。
另外,可以通過GRADLE_OPTS或JAVA_OPTS來配置Gradle運行時的JVM參數。不過,JAVA_OPTS設置的參數也會影響到其他的JAVA應用程序。
Gradle構建基礎
Projects和tasks
先介紹兩個概念,projects和tasks,它們是Gradle中的兩個重要概念。
任何一個Gradle構建,都是由一個或多個projects組成的。Project就是你想要用Gradle做什麼,比如構建一個jar包,構建一個web應用。Project也不單指構建操作,部署你的應用或搭建一個環境,也可以是一個project。
一個project由多個task組成。每個task代表了構建過程當中的一個原子性操作,比如編譯,打包,生成javadoc,發佈等等這些操作。
編寫第一個構建腳本
新建一個文件build.gradle,然後添加以下代碼:
task hello {
doLast {
println 'Hello, Gradle!'
}
}
- 1
- 2
- 3
- 4
- 5
這是一個非常簡單的構建腳本,它定義了一個叫hello的task,task的內容是在最後打印出“Hello, Gradle!”。
輸入命令gradle hello來執行它:
Gradle是領域驅動設計的構建工具,在它的實現當中,Project接口對應上面的project概念,Task接口對應上面的task概念,實際上除此之外還有一個重要的領域對象,即Action,對應的是task裏面具體的某一個操作。一個project由多個task組成,一個task也是由多個action組成。
當執行gradle hello的時候,Gradle就會去調用這個hello task來執行給定操作(Action)。這個操作其實就是一個用Groovy代碼寫的閉包,代碼中的task是Project類裏的一個方法,通過調用這裏的task方法創建了一個Task對象,並在對象的doLast方法中傳入println ‘Hello, Gradle!’這個閉包。這個閉包就是一個Action。
Task是Gradle裏定義的一個接口,表示上述概念中的task。它定義了一系列的諸如doLast, doFirst等抽象方法,具體可以看gradle api裏org.gradle.api.Task的文檔。
在上面執行了gradle hello後,除了輸出“Hello, Gradle!”之外,我們發現像“:hello”這樣的其他內容。這其實是Gradle打印出來的日誌,如果不想輸出這些內容,可以在gradle後面加上參數 -q。即:gradle -q hello。
快速定義任務
上面的代碼,還有一種更簡潔的寫法,如下:
task hello << {
println 'Hello, Gradle!'
}
- 1
- 2
- 3
task hello << {
println ‘Hello, Gradle!’
}
代碼即腳本
Gradle腳本是採用Groovy編寫的,所以也像Groovy一樣,以腳本方式來執行代碼,如下面例子:
task upper << {
String someString = 'myName'
println "Original: " + someString
println "Upper case: " + someString.toUpperCase()
}
- 1
- 2
- 3
- 4
- 5
- 6
執行結果如下,它將定義的字符串轉爲大寫:
D:\testGradle>gradle -q hello
Hello, Gradle!
D:\testGradle>gradle -q upper
Original: myName
Upper case: MYNAME
- 1
- 2
- 3
- 4
- 5
我們在寫Gradle腳本的時候,可以像寫Groovy代碼一樣。而Groovy是基於Java的,兼容Java語法
任務依賴
我們可以通過以下方式創建依賴:
task hello << {
print 'Hello, '
}
task intro(dependsOn: hello) << {
println "Gradle!"
}
- 1
- 2
- 3
- 4
- 5
- 6
定義一個任務hello,輸出“Hello, ”,然後定義一個任務intro,並依賴hello,輸出“Gradle!”。結果是打印出“Hello, Gradle!”,如下:
D:\testGradle>gradle -q intro
Hello, Gradle!
- 1
- 2
另外,被依賴的task不必放在前面聲明,在後面也是可以的,這一點在後面將會用到。
動態任務
藉助於強大的Groovy,我們還可以動態地創建任務。如下代碼:
我們還可以動態地創建任務。如下代碼:
4.times { counter ->
task "task$counter" << {
println "I'm task number $counter"
}
}
- 1
- 2
- 3
- 4
- 5
我們定義了4個task,分別是task0, task1, task2, task3。我們來執行task1,如下:
D:\testGradle>gradle -q task1
I'm task number 1
- 1
- 2
任務操縱
在Gradle當中,任務創建之後可以通過API進行訪問,這是Gradle與Ant的不同之處。
增加依賴
還是以上面的例子,但是我們添加一行代碼,如下:
4.times { counter ->
task "task$counter" << {
println "I'm task number $counter"
}
}
task1.dependsOn task0, task3
- 1
- 2
- 3
- 4
- 5
- 6
然後還是執行 gradle -q task1
gradle -q task1
I'm task number 0
I'm task number 3
I'm task number 1
- 1
- 2
- 3
- 4
它先執行了task0和task3,因爲task1依賴於這兩個。
增加任務行爲
task hello << {
println 'Hello, Gradle!'
}
hello.doFirst {
println 'I am first.'
}
hello.doLast {
println 'I am last.'
}
hello << {
println 'I am the the last'
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
執行後的輸出:
gradle -q hello
I am first.
Hello, Gradle!
I am last.
I am the the last
- 1
- 2
- 3
- 4
- 5
短標記法
如果你對groovy有一定了解,那你也許會注意到,每個task都是一個構建腳本的屬性,所以可以通過“$”這種短標記法來訪問任務。如下:
task hello << {
println 'Hello, Gradle!'
}
hello.doLast {
println "Greetings from the $hello.name task."
}
- 1
- 2
- 3
- 4
- 5
- 6
執行結果:
gradle -q hello
Hello, Gradle!
Greetings from the hello task.
- 1
- 2
- 3
注意,通過這種方法訪問的任務一定是要已經定義的。
增加自定義屬性
task myTask {
ext.myProperty = "myValue"
}
task printTaskProperties << {
println myTask.myProperty
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
輸出結果:
gradle -q printTaskProperties
myValue
- 1
- 2
- 3
- 4
定義默認任務
defaultTasks 'clean', 'run'
task clean << {
println 'Default Cleaning!'
}
task run << {
println 'Default Running!'
}
task other << {
println "I'm not a default task!"
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
執行結果:
gradle -q
Default Cleaning!
Default Running!
- 1
- 2
- 3
- 4
DAG配置
Gradle使用DAG(Directed acyclic graph,有向非循環圖)來決定任務執行的順序。通過這一特性,我們可以實現依賴任務做不同輸出。
如下代碼:
task distribution << {
println "We build the zip with version=$version"
}
task release(dependsOn: 'distribution') << {
println 'We release now'
}
gradle.taskGraph.whenReady {taskGraph ->
if (taskGraph.hasTask(release)) {
version = '1.0'
} else {
version = '1.0-SNAPSHOT'
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
執行結果如下:
gradle -q
Default Cleaning!
Default Running!
D:\testGradle>gradle -q distribution
We build the zip with version=1.0-SNAPSHOT
D:\testGradle> gradle -q release
We build the zip with version=1.0
We release now
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在上面的腳本代碼中,whenReady會在release任務執行之前影響它,即使這個任務不是主要的任務(即不是通過命令行傳入參數來調用)。
————–接下來是Android Gradle打包的小技巧———–
替換AndroidManifest中的佔位符
把配置中的${app_label}替換爲@string/app_name
android{
defaultConfig{
manifestPlaceholders = [app_label:"@string/app_name"]
}
}
- 1
- 2
- 3
- 4
- 5
如果只想替換debug版本:
android{
buildTypes {
debug {
manifestPlaceholders = [app_label:"@string/app_name_debug"]
}
release {
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
更多的需求是替換渠道編號:
android{
productFlavors {
// 把dev產品型號的apk的AndroidManifest中的channel替換dev
"dev"{
manifestPlaceholders = [channel:"dev"]
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
獨立配置簽名信息
對於簽名相關的信息,直接寫在gradle當然不好,特別是一些開源項目,可以添加到gradle.properties:
RELEASE_KEY_PASSWORD=xxxx
RELEASE_KEY_ALIAS=xxx
RELEASE_STORE_PASSWORD=xxx
RELEASE_STORE_FILE=../.keystore/xxx.jks
- 1
- 2
- 3
- 4
然後在build.gradle中引用即可:
android {
signingConfigs {
release {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
如果不想提交到版本庫,可以添加到local.properties中,然後在build.gradle中讀取。
多渠道打包
多渠道打包的關鍵之處在於,定義不同的product flavor, 並把AndroiManifest中的channel渠道編號替換爲對應的flavor標識:
android {
productFlavors {
dev{
manifestPlaceholders = [channel:"dev"]
}
official{
manifestPlaceholders = [channel:"official"]
}
// ... ...
wandoujia{
manifestPlaceholders = [channel:"wandoujia"]
}
xiaomi{
manifestPlaceholders = [channel:"xiaomi"]
}
"360"{
manifestPlaceholders = [channel:"360"]
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
注意一點,這裏的flavor名如果是數字開頭,必須用引號引起來。
構建一下,就能生成一系列的Build Variant了:
devDebug
devRelease
officialDebug
officialRelease
wandoujiaDebug
wandoujiaRelease
xiaomiDebug
xiaomiRelease
360Debug
360Release
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
其中debug, release是gradle默認自帶的兩個build type, 下一節還會繼續說明。選擇一個,就能編譯出對應渠道的apk了。
自定義Build Type
前面說到默認的build type有兩種debug和release,區別如下:
// release版本生成的BuildConfig特性信息
public final class BuildConfig {
public static final boolean DEBUG = false;
public static final String BUILD_TYPE = "release";
}
// debug版本生成的BuildConfig特性信息
public final class BuildConfig {
public static final boolean DEBUG = true;
public static final String BUILD_TYPE = "debug";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
現在有一種需求,增加一種build type,介於debug和release之間,就是和release版本一樣,但是要保留debug狀態(如果做過rom開發的話,類似於user debug版本),我們稱爲preview版本吧。
其實很簡單:
android {
signingConfigs {
debug {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
preview {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
release {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
buildTypes {
debug {
manifestPlaceholders = [app_label:"@string/app_name_debug"]
}
release {
manifestPlaceholders = [app_label:"@string/app_name"]
}
preview{
manifestPlaceholders = [app_label:"@string/app_name_preview"]
}
}
}
- 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
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
另外,build type還有一個好處,如果想要一次性生成所有的preview版本,執行assemblePreview即可,debug和releae版本同理。
build type中的定製參數
上面我們在不同的build type替換${app_label}爲不同的字符串,這樣安裝到手機上就能明顯的區分出不同build type的版本。
除此之外,可能還可以配置一些參數,我這裏列幾個我在工作中用到的:
android {
debug {
manifestPlaceholders = [app_label:"@string/app_name_debug"]
applicationIdSuffix ".debug"
minifyEnabled false
signingConfig signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
manifestPlaceholders = [app_label:"@string/app_name"]
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
preview{
manifestPlaceholders = [app_label:"@string/app_name_preview"]
applicationIdSuffix ".preview"
debuggable true // 保留debug信息
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.preview
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
- 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
- 26
這些都用的太多了,稍微解釋一下:
// minifyEnabled 混淆處理
// shrinkResources 去除無用資源
// signingConfig 簽名
// proguardFiles 混淆配置
// applicationIdSuffix 增加APP ID的後綴
// debuggable 是否保留調試信息
// ... ...
- 1
- 2
- 3
- 4
- 5
- 6
- 7
多工程全局配置
隨着產品渠道的鋪開,往往一套代碼需要支持多個產品形態,這就需要抽象出主要代碼到一個Library,然後基於Library擴展幾個App Module。
相信每個module的build.gradle都會有這個代碼:
android {
compileSdkVersion 22
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 10
targetSdkVersion 22
versionCode 34
versionName "v2.6.1"
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
當升級sdk、build tool、target sdk等,幾個module都要更改,非常的麻煩。最重要的是,很容易忘記,最終導致app module之間的差異不統一,也不可控。
強大的gradle插件在1.1.0支持全局變量設定,一舉解決了這個問題。
先在project的根目錄下的build.gradle定義ext全局變量:
ext {
compileSdkVersion = 22
buildToolsVersion = "23.0.1"
minSdkVersion = 10
targetSdkVersion = 22
versionCode = 34
versionName = "v2.6.1"
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
然後在各module的build.gradle中引用如下:
Android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.xxx.xxx"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
然後每次修改project級別的build.gradle即可實現全局統一配置。
自定義導出的APK名稱
默認android studio生成的apk名稱爲app-debug.apk或者app-release.apk,當有多個渠道的時候,需要同時編出50個渠道包的時候,就麻煩了,不知道誰是誰了。
這個時候,就需要自定義導出的APK名稱了,不同的渠道編出的APK的文件名應該是不一樣的。
android {
// rename the apk with the version name
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile = new File(
output.outputFile.parent,
"lol-${variant.buildType.name}-${variant.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
當apk太多時,如果能把apk按debug,release,preview分一下類就更好了(事實上,對於我這樣經常發版的人,一編往往就要編四五十個版本的人,debug和release版本全混在一起沒法看,必須分類),簡單:
android {
// rename the apk with the version name
// add output file sub folder by build type
applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFile = new File(
output.outputFile.parent + "/${variant.buildType.name}",
"lol-${variant.buildType.name}</span>-<span class="hljs-subst">${variant.versionName}-${variant.productFlavors[0].name}.apk".toLowerCase())
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
現在生成了類似於lol-dev-preview-v2.4.0.0.apk這樣格式的包了,preview的包自然就放在preview的文件夾下,清晰明瞭。
混淆技巧
混淆能讓反編譯的代碼可讀性變的很差,而且還能顯著的減少APK包的大小。
第一個技巧
相信很多朋友對混淆都覺得麻煩,甚至說,非常亂。因爲添加混淆規則需要查詢官方說明文檔,甚至有的官方文檔還沒說明。當你引用了太多庫後,添加混淆規則將使一場噩夢。
這裏介紹一個技巧,不用查官方文檔,不用逐個庫考慮添加規則。
首先,除了默認的混淆配置(android-sdk/tools/proguard/proguard-android.txt), 自己的代碼肯定是要自己配置的:
接下來是麻煩的第三方庫,一般來說,如果是極光推的話,它的包名是cn.jpush, 添加如下代碼即可:
dontwarn cn.jpush.**
-keep class cn.jpush.** { *; }
- 1
- 2
其他的第三庫也是如此,一個一個添加,太累!其實可以用第三方反編譯工具(比如jadx:https://github.com/skylot/jadx ),打開apk後,一眼就能看到引用的所有第三方庫的包名,把所有不想混淆或者不確定能不能混淆的,直接都添加又有何不可:
#####################################
### 第三方庫或者jar包
#
-dontwarn cn.jpush.**
-keep class cn.jpush.* { ; }
-dontwarn com.squareup.**
-keep class com.squareup.* { ; }
-dontwarn com.octo.**
-keep class com.octo.* { ; }
-dontwarn de.**
-keep class de.* { ; }
-dontwarn javax.**
-keep class javax.* { ; }
-dontwarn org.**
-keep class org.* { ; }
-dontwarn u.aly.**
-keep class u.aly.* { ; }
-dontwarn uk.**
-keep class uk.* { ; }
-dontwarn com.baidu.**
-keep class com.baidu.* { ; }
-dontwarn com.facebook.**
-keep class com.facebook.* { ; }
-dontwarn com.google.**
-keep class com.google.* { ; }
## ... ...
- 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
- 26
第二個技巧
一般release版本混淆之後,像友盟這樣的統計系統如果有崩潰異常,會記錄如下:
java.lang.NullPointerException: java.lang.NullPointerException
at com.xxx.TabMessageFragment$7.run(Unknown Source)
- 1
- 2
這個Unknown Source是很要命的,排除錯誤無法定位到具體行了,大大降低調試效率。
當然,友盟支持上傳Mapping文件,可幫助定位,mapping文件的位置在:
project > module
> build > outputs > {flavor name} > {build type} > mapping.txt
- 1
- 2
如果版本一多,mapping.txt每次都要重新生成,還要上傳,終歸還是麻煩。
其實,在proguard-rules.pro中添加如下代碼即可:
-keepattributes SourceFile,LineNumberTabl
- 1
當然apk包會大那麼一點點(我這裏6M的包,大個200k吧),但是再也不用mapping.txt也能定位到行了,爲了這種解脫,這個代價是值的!
動態設置一些額外信息
假如想把當前的編譯時間、編譯的機器、最新的commit版本添加到apk,而這些信息又不好寫在代碼裏,強大的gradle給了我創造可能的自信:
android {
defaultConfig {
resValue "string", "build_time", buildTime()
resValue "string", "build_host", hostName()
resValue "string", "build_revision", revision()
}
}
def buildTime() {
return new Date().format("yyyy-MM-dd HH:mm:ss")
}
def hostName() {
return System.getProperty("user.name") + "@" + InetAddress.localHost.hostName
}
def revision() {
def code = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = code
}
return code.toString()
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
上述代碼實現了動態的添加了3個字符串資源: build_time、build_host、build_revision, 然後在其他地方可像如引用字符串一樣使用如下:
// 在Activity裏調用
getString(R.string.build_time)
getString(R.string.build_host)
getString(R.string.build_revision)
- 1
- 2
- 3
- 4
給自己留個”後門”: 點七下
爲了調試方便,我們往往會在debug版本留一個顯示我們想看的界面,如何進入到一個界面,我們可以仿照android開發者選項的方式,點七下才顯示,我們來實現一個:
private int clickCount = 0;
private long clickTime = 0;
sevenClickView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (clickTime == 0) {
clickTime = System.currentTimeMillis();
}
if (System.currentTimeMillis() - clickTime > 500) {
clickCount = 0;
} else {
clickCount++;
}
clickTime = System.currentTimeMillis();
if (clickCount > 6) {
// 點七下條件達到,跳到debug界面
}
}
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
release版本肯定是不能暴露這個界面的,也不能讓人用am在命令行調起,如何防止呢,可以在release版本把這個debug界面的exported設爲false。