大型Android專案的工程化之路:編譯與構建

蘇策發表於2017-12-07

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章目錄

  • 一 Groovy語言基礎
  • 二 Gradle指令碼構建
    • 2.1 root build.gradle
    • 2.2 module build.gradle
    • 2.1 gradle wrapper
  • 三 Gradle混淆與優化
    • 2.1 程式碼壓縮
    • 2.2 資源壓縮
  • 四 Gradle多專案構建
  • 五 Gradle多渠道打包
  • 附錄
    • Gradle常用命令
    • Gradle小技巧

關於文章封面,道理我都懂,你放個妹紙在文章封面上有什麼意義嗎??

情況是這樣的,昨天有個bug困擾了我一天,晚飯時分聽到了T-ara的歌《我怎麼辦》,伴隨著歡快的節奏,忽然思緒大開,解決了那個 bug,說到T-ara,當然要放在她們的主唱樸素妍的照片辣~?

閒話不多說,正文時間到。本篇文章是《大型Android專案的工程化之路》的開篇之作,這個系列的文章主要用來討論伴隨著Android專案越來越大時,如何處理編譯與構建、VCS工作流、模組化、持續整合等問題,以及 一些應用黑科技外掛化、熱更新的實現方案,目前規劃的內容如下:

  • 01大型Android專案的工程化之路:編譯與構建
  • 大型Android專案的工程化之路:VCS工作流
  • 大型Android專案的工程化之路:持續整合
  • 大型Android專案的工程化之路:編碼規範
  • 大型Android專案的工程化之路:專案架構
  • 大型Android專案的工程化之路:SDK設計
  • 大型Android專案的工程化之路:模組化
  • 大型Android專案的工程化之路:外掛化
  • 大型Android專案的工程化之路:熱更新
  • 大型Android專案的工程化之路:異常採集與分析

首先讓我們進入第一個主題,基於Gradle的專案的編譯與構建。

Gradle是一個基於Apache Ant和Apache Maven概念的專案自動化建構工具。它使用一種基於Groovy的特定領域語言來宣告專案設定,大部分功能都通過 外掛的方式實現。

大型Android專案的工程化之路:編譯與構建

官方網站:https://gradle.org/

官方介紹:From mobile apps to microservices, from small startups to big enterprises, Gradle helps teams build, automate and deliver better software, faster.

在正式介紹Gradle之前,我們先了解下Groovy語言的基礎只是,方便我們後面的理解。

一 Groovy語言基礎

Groovy是基於JVM的一種動態語言,語法與Java相似,也完全相容Java。

這裡我們簡單的說一些我們平時用的到的Groovy語言的一些特性,方便大家理解和編寫Gradle指令碼,事實上如果你熟悉Kotlin、JavaScript這些語言,那麼 Groovy對你來說會有種很相似的感覺。

注:Groovy是完全相容Java的,也就意味著如果你對Groovy不熟悉,也可以用Java來寫Gradle指令碼。

  • 單引號表示純字串,雙引號表示對字串求值,例如$取值。
def version = '26.0.0'

dependencies {
    compile "com.android.support:appcompat-v7:$version"
}

複製程式碼
  • Groovy完全相容Java的集合,並且進行了擴充套件。
task printList {
    def list = [1, 2, 3, 4, 5]
    println(list)
    println(list[1])//訪問第二個元素
    println(list[-1])//訪問最後一個元素
    println(list[1..3])//訪問第二個到第四個元素
}

task printMap {
    def map = ['width':720, 'height':1080]
    println(map)
    println(map.width)//訪問width
    println(map.height)//訪問height
    map.each {//遍歷map
        println("Key:${it.key}, Value:${it.value}")
    }
}
複製程式碼
  • Groovy方法的定義方式和Java類似,呼叫方式比Java靈活,有返回值的函式也可以不寫return語句,這個時候會把最後一行程式碼的值作為返回值返回。
def method(int a, int b){
    if(a > b){
        a
    }else {
        b
    }
}

def  callMethod(){
    method 1, 2
}
複製程式碼

可以看到,和Kotlin這些現代程式語言一樣,有很多語法糖。瞭解了Groovy,我們再來看看Gradle工程相關知識。

二 Gradle指令碼構建

一個標準的Android Gradle工程如下所示,我們分別來看看裡面每個檔案的作用。

大型Android專案的工程化之路:編譯與構建

2.1 root build.gradle

root build.gradle是根目錄的build.gradle檔案,它主要用來對整體工程以及各個Module進行一些通用的配置。

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        //遠端倉庫
        google()
        jcenter()
    }
    dependencies {
        //Android Studio Gradle外掛
        classpath 'com.android.tools.build:gradle:3.0.0'

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

//對所有工程進行遍歷和配置
allprojects {
    repositories {
        //遠端倉庫
        jcenter()
        google()
    }
}

//對單個工程進行遍歷和配置
subprojects{

}

task clean(type: Delete) {
    delete rootProject.buildDir
}


ext{
    //定義module通用的版本號,這樣module裡就可以通過$rootProject.ext.supportLibraryVersion
    //的方式訪問
    supportLibraryVersion = '26.0.0'
}
複製程式碼

2.2 module build.gradle

module build.gradle用於module的配置與編譯。

這裡有很多常用的配置選項,你並不需要都把它們記住,有個大致的印象就行,等到用的時候再回來查一查。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    buildToolsVersion '26.0.2'


    defaultConfig {
        //應用包名
        applicationId "com.guoxiaoxing.software.engineering.demo"
        //最低支援的Android SDK 版本
        minSdkVersion 15
        //基於開發的Android SDK版本
        targetSdkVersion 26
        //應用版本號
        versionCode 1
        //應用版本名稱
        versionName "1.0"

        //單元測試時使用的Runner
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

    signingConfigs{

        debug{
            storeFile file("debugKey.keystore")
            storePassword '123456'
            keyAlias 'debugkeyAlias'
            keyPassword '123456'
        }

        release{
            storeFile file("releaseKey.keystore")
            storePassword '123456'
            keyAlias 'releasekeyAlias'
            keyPassword '123456'
        }
    }

    //Java編譯選項
    compileOptions{

        //編碼
        encoding = 'utf-8'

        //Java編譯級別
        sourceCompatibility = JavaVersion.VERSION_1_6

        //生成的Java位元組碼版本
        targetCompatibility = JavaVersion.VERSION_1_6
    }

    //ADE配置選項
    adbOptions{

        //ADB命令執行的超時時間,超時時會返回CommandRectException異常。
        timeOutInMs = 5 * 1000//5秒

        //ADB安裝選項,例如-r代表替換安裝
        installOptions '-r', '-s'
    }

    //DEX配置選項
    dexOptions{

        //是否啟動DEX增量模式,可以加快速度,但是目前這個特性不是很穩定
        incremental false

        //執行DX命令是為其分配的最大堆記憶體,主要用來解決執行DX命令是記憶體不足的情況
        javaMaxHeapSize '4g'

        //執行DX開啟的執行緒數,適當的執行緒數量可以提高編譯速度
        threadCount 2

        //是否開啟jumbo模式,有時方法數超過了65525,需要開啟次模式才能編譯成功
        jumboMode true
    }

    lintOptions{

        //lint發現錯誤時是否退出Gradle構建
        abortOnError false
    }

    //構建的應用型別。用於指定生成的APK相關屬性
    buildTypes {

        debug{

            //是否可除錯
            debuggable true

            //是否可除錯jni
            jniDebuggable true

            //是否啟動自動拆分多個DEx
            multiDexEnabled true

            //是否開啟APK優化,zipAlign是Android提供的一個整理優化APK檔案的
            //工具,它可以提高系統和應用的執行效率,更快的讀寫APK裡面的資源,降低
            //記憶體的優化
            zipAlignEnabled true

            //簽名資訊
            signingConfig signingConfigs.debug

            //是否自動清理未使用的資源
            shrinkResources true

            //是否啟用混淆
            minifyEnabled true

            //指定多個混淆檔案
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        release {
            //簽名資訊
            signingConfig signingConfigs.release

            //是否啟用混淆
            minifyEnabled true

            //指定多個混淆檔案
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

//依賴
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    testCompile 'junit:junit:4.12'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
}
複製程式碼

2.3 Gradle Wrapper

Gradle Wrapper是對Gradle的一層包裝,目的在於團隊開發中統一Gradle版本,一般可以通過gradle wrapper命令構建,會生成以下檔案:

  • gradle-wrapper.jar
  • gradle-wrapper.properties

檔案用來進行Gradle Wrapper進行相關配置。如下所示:

#Fri Nov 24 17:39:29 CST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
複製程式碼

我們通常關心的是distributionUrl,它用來配置Gradle的版本,它會去該路徑下載相應的Gradle包。

注:如果官方的gradle地址下載比較慢,可以去國內的映象地址下載。

三 Gradle混淆與優化

3.1 程式碼壓縮

程式碼壓縮通過 ProGuard 提供,ProGuard 會檢測和移除封裝應用中未使用的類、欄位、方法和屬性,包括自帶程式碼庫中的未使用項(這使其成為以變通方式解決 64k 引用限制的有用工具)。 ProGuard 還可優化位元組碼,移除未使用的程式碼指令,以及用短名稱混淆其餘的類、欄位和方法。混淆過的程式碼可令您的 APK 難以被逆向工程,這在應用使用許可驗證等安全敏感性功能時特別 有用。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
    ...
}
複製程式碼

除了 minifyEnabled 屬性外,還有用於定義 ProGuard 規則的 proguardFiles 屬性:

  • getDefaultProguardFile('proguard-android.txt') 方法可從 Android SDK tools/proguard/ 資料夾獲取預設的 ProGuard 設定。 提示:要想做進一步的程式碼壓縮,請嘗試使用位於同一位置的 proguard-android-optimize.txt 檔案。它包括相同的 ProGuard 規則,但還包括其他在位元組碼一級(方法內和方法間)執行分析的優化,以進一步減小 APK 大小和幫助提高其執行速度。
  • proguard-rules.pro 檔案用於新增自定義 ProGuard 規則。預設情況下,該檔案位於模組根目錄(build.gradle 檔案旁)。

我們可以在專案裡的proguard-rules.pro定義我們的混淆規則,

常用的混淆命令如下所示:

proguard 引數

  • -include {filename} 從給定的檔案中讀取配置引數

  • -basedirectory {directoryname} 指定基礎目錄為以後相對的檔案名稱

  • -injars {class_path} 指定要處理的應用程式jar,war,ear和目錄

  • -outjars {class_path} 指定處理完後要輸出的jar,war,ear和目錄的名稱

  • -libraryjars {classpath} 指定要處理的應用程式jar,war,ear和目錄所需要的程式庫檔案

  • -dontskipnonpubliclibraryclasses 指定不去忽略非公共的庫類。

  • -dontskipnonpubliclibraryclassmembers 指定不去忽略包可見的庫類的成員。

保留選項

  • -keep {Modifier} {class_specification} 保護指定的類檔案和類的成員

  • -keepclassmembers {modifier} {class_specification} 保護指定類的成員,如果此類受到保護他們會保護的更好

  • -keepclasseswithmembers {class_specification} 保護指定的類和類的成員,但條件是所有指定的類和類成員是要存在。

  • -keepnames {class_specification} 保護指定的類和類的成員的名稱(如果他們不會壓縮步驟中刪除)

  • -keepclassmembernames {class_specification} 保護指定的類的成員的名稱(如果他們不會壓縮步驟中刪除)

  • -keepclasseswithmembernames {class_specification} 保護指定的類和類的成員的名稱,如果所有指定的類成員出席(在壓縮步驟之後)

  • -printseeds {filename} 列出類和類的成員- -keep選項的清單,標準輸出到給定的檔案

壓縮

  • -dontshrink 不壓縮輸入的類檔案

  • -printusage {filename}

  • -whyareyoukeeping {class_specification}

優化

  • -dontoptimize 不優化輸入的類檔案

  • -assumenosideeffects {class_specification} 優化時假設指定的方法,沒有任何副作用

  • -allowaccessmodification 優化時允許訪問並修改有修飾符的類和類的成員

混淆

  • -dontobfuscate 不混淆輸入的類檔案

  • -printmapping {filename}

  • -applymapping {filename} 重用對映增加混淆

  • -obfuscationdictionary {filename} 使用給定檔案中的關鍵字作為要混淆方法的名稱

  • -overloadaggressively 混淆時應用侵入式過載

  • -useuniqueclassmembernames 確定統一的混淆類的成員名稱來增加混淆

  • -flattenpackagehierarchy {package_name} 重新包裝所有重新命名的包並放在給定的單一包中

  • -repackageclass {package_name} 重新包裝所有重新命名的類檔案中放在給定的單一包中

  • -dontusemixedcaseclassnames 混淆時不會產生形形色色的類名

  • -keepattributes {attribute_name,...} 保護給定的可選屬性,例如LineNumberTable, LocalVariableTable, SourceFile, Deprecated, Synthetic, Signature, and InnerClasses.

  • -renamesourcefileattribute {string} 設定原始檔中給定的字串常量

另外關於具體的混淆規則,可以使用Android Stduio外掛AndroidProguardPlugin,它幫我們收集了主要第三方庫的混淆規則,可以 參考下。

混淆完成後都會輸出下列檔案:

  • dump.txt:說明 APK 中所有類檔案的內部結構。
  • mapping.txt:提供原始與混淆過的類、方法和欄位名稱之間的轉換。
  • seeds.txt:列出未進行混淆的類和成員。
  • usage.txt:列出從 APK 移除的程式碼。 t 這些檔案儲存在 /build/outputs/mapping/release/ 中,這些檔案是很有用的,我們還可以利用在SDK的安裝目錄下\tools\proguard\lib的proguardgui程式再結合 mapping.txt對APK進行反混淆,以及利用etrace 指令碼解碼混淆過後的應用程式堆疊資訊,這通常是用來來分析混淆後的線上應用的bug。

retrace 指令碼(在 Windows 上為 retrace.bat;在 Mac/Linux 上為 retrace.sh)。它位於 /tools/proguard/ 目錄中。該指令碼利用 mapping.txt 檔案來生成應用程式堆疊資訊。

具體做法:

retrace.sh -verbose mapping.txt obfuscated_trace.txt
複製程式碼

另外,還要提一點,如果想要混淆支援Instant Run,可以使用Android內建的程式碼壓縮器,Android內建的程式碼壓縮器也可以使用 與 ProGuard 相同的配置檔案來配置 Android 外掛壓縮器。 但是,Android 外掛壓縮器不會對您的程式碼進行混淆處理或優化,它只會刪除未使用的程式碼。因此,它應該僅將其用於除錯構建,併為釋出構建啟用 ProGuard,以便對釋出 APK 的程式碼進行混淆 處理和優化。

要啟用 Android 外掛壓縮器,只需在 "debug" 構建型別中將 useProguard 設定為 false(並保留 minifyEnabled 設定 true),如下所示:

android {
    buildTypes {
        debug {
            minifyEnabled true
            useProguard false
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}
複製程式碼

3.2 資源壓縮

資源壓縮通過適用於 Gradle 的 Android 外掛提供,該外掛會移除封裝應用中未使用的資源,包括程式碼庫中未使用的資源。它可與程式碼壓縮發揮協同效應,使得在移除未使 用的程式碼後,任何不再被引用的資源也能安全地移除。

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}
複製程式碼

同樣地,我們也可以自定義保留的資源,我們可以在專案中建立一個包含 標記的 XML 檔案,並在 tools:keep 屬性中指定每個要保留的資源,在 tools:discard 屬性中指 定每個要捨棄的資源。這兩個屬性都接受逗號分隔的資源名稱列表。當然我們也可以使用星號字元作為萬用字元。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:shrinkMode="strict"
    tools:discard="@layout/unused2" />
複製程式碼

然後將該檔案儲存在專案資源中,例如,儲存在 res/raw/keep.xml。構建不會將該檔案打包到 APK 之中。上面提到可以用discard指定需要刪除的資源,

這裡有人可能會疑惑,直接刪了不就完了,還要指定刪除?。這個其實通常用在多構建應用變體之中,同一個應用可能包打包成不同的變體,不同變體需要的資原始檔是不一樣的,這樣 可以通過為不同變體定義不同的keep.xml來解決這個問題。

另外,上面還有個tools:shrinkMode="strict",即啟用嚴格模式進行資源壓縮。正常情況下,資源壓縮器可準確判定系統是否使用了資源,但有些動態引用資源的情況,例如:

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
複製程式碼

這種情況下,資源壓縮器就會將img_開頭的資源都標記為已使用,不會被移除。這是一種預設情況下的防禦行為,要停用這種行為只需要加上tools:shrinkMode="strict"即可。

最後,我們還可以通過resConfigs指定我們的應用只支援哪些語言的資源。

例如將語言資源限定為僅支援英語和法語:

android {
    defaultConfig {
        ...
        resConfigs "en", "fr"
    }
}
複製程式碼

四 Gradle多專案構建

Android的專案一般分為應用專案、庫專案和測試專案,它們對應的Gradle外掛型別分別為:

  • com.android.application
  • com.android.library
  • com.android.test

我們一般只有一個應用專案,但是會有多個庫專案,通過新增依賴的方式引用庫專案。

例如:

compile ('commons-httpclient:commons-httpclient:3.1'){
    exclude group:'commons-codec',module:'commons-codec'//排除該group的依賴,group是必選項,module可選
}

//選擇1以上任意一個版本
compile 'commons-httpclient:commons-httpclient:1.+'

//選擇最新的版本,避免直接指定版本號 
compile 'commons-httpclient:commons-httpclient:latest.integration'
複製程式碼

依賴型別主要分為五種:

  • compile:原始碼(src/main/java)編譯時的依賴,最常用
  • runtime:原始碼(src/main/java)執行時依賴
  • testCompile:測試程式碼(src/main/test)編譯時的依賴
  • testRuntime:測試程式碼(src/main/java)執行時的依賴
  • archives:專案打包(e.g.jar)時的依賴

注:Gradle 3.0已經廢棄了compile,並新增了implementation與api兩個命令,它們的區別如下:

  • api:完全等同於compile指令,沒區別,你將所有的compile改成api,完全沒有錯。
  • implementation:這個指令的特點就是,對於使用了該命令編譯的依賴,對該專案有依賴的專案將無法訪問到使用該命令編譯的依賴中的任何程式,也就是將該依賴隱藏在內部,而不對外部公開。

在編譯庫的時候,我們通常選擇的遠端庫是jcenter(包含maven),google也推出了自己的遠端倉庫google()(新的gradle外掛需要從這個遠端倉庫上下載),這些國外的遠端倉庫在編譯的時候 有時候會非常慢,這個時候可以換成國內的阿里雲映象。

修改專案根目錄下的檔案 build.gradle :

buildscript {
    repositories {
        maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
    }
}

allprojects {
    repositories {
        maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
    }
}
複製程式碼

另外,如果我們想把自己的專案提交到jcenter上,可以使用bintray-release,具體使用方式很簡單,專案文件上說的也很清楚,這裡就 不再贅述。

五 Gradle多渠道打包

根據釋出的渠道或者客戶群的不同,同一個應用可能會有很多變體,不同變體的應用名字、渠道等很多資訊都會不一樣,這個時候就要使用Gradle多渠道打包。 多渠道打包主要是通過productFlavor進行定製。

例如下面針對google、baidu批量配置了UMENG_CHANNEL。

apply plugin: 'com.android.application'

android {

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled true
        }
    }
    
    productFlavors {
        xiaomi {
            //manifestPlaceholders定義了AndroidManifest裡的佔位符,
            //AndroidManifest可以通過$UMENG_CHANNEL_VALUE來獲取
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
        }
        _360 {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "_360"]
        }
        baidu {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
        }
        wandoujia {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
        }
    }
}
複製程式碼

當然我們也可以批量修改:

productFlavors {
    xiaomi {}
    _360 {}
    baidu {}
    wandoujia {}
}  

//通過all函式遍歷每一個productFlavors然後把它作為UMENG_CHANNEL的名字,這種做法
//適合渠道名稱非常多的情況
productFlavors.all { 
    flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] 
}
複製程式碼

productFlavors裡還可以自定義變數,自定義的定了可以在BuildConfig裡獲取。自定義變數通過以下方法完成:

buildConfigField 'String','WEB_URL','"http://www.baidu.com"'
複製程式碼

渠道productFlavors和編譯型別裡都可以自定義變數。

apply plugin: 'com.android.application'

android {

    buildTypes {
        
        debug{
            buildConfigField 'String','WEB_URL','"http://www.baidu.com"'
        }
        
        release {
            buildConfigField 'String','WEB_URL','"http://www.google.com"'
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled true
        }
    }
    
    productFlavors {
        google {
            buildConfigField 'String','WEB_URL','"http://www.google.com"'
        }
        baidu {
            buildConfigField 'String','WEB_URL','"http://www.baidu.com"'
        }
    }
}
複製程式碼

附錄

Gradle常用命令

強制重新整理依賴

gradle --refresh-dependencies assemble
複製程式碼

檢視app所有依賴庫

gradle dependencies :app

複製程式碼

檢視編譯時依賴

gradle dependencies -configuration compile
複製程式碼

檢視執行時依賴

gradle dependencies -configuration runtime
複製程式碼

Gradle小技巧

批量修改生成的APK檔名

有些時候想改變輸入APK的檔名。

apply plugin: 'com.android.application'

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

def buildTime() {
    def date = new Date()
    def formattedDate = date.format('yyyyMMdd')
    return formattedDate
}
複製程式碼

動態獲取應用版本號和版本名稱

一般來說在打包的時候都會從git選擇一個tag來打包釋出,以tag來作為應用的名稱。

git獲取tag的命令

git describe --abbrev=0 --tags
複製程式碼

這個時候就需要利用Gradle執行shell命令,它為我們提供了exec這樣簡便的方式來執行shell命令。


apply plugin: 'com.android.application'

android {
    defaultConfig {
        applicationId "com.guoxiaoxing.software.demo"
        minSdkVersion 14
        targetSdkVersion 23
        versionCode getAppVersionCode()
        versionName getAppVersionName()
    }
}
/**
 * 以git tag的數量作為其版本號
 * @return tag的數量
 */
def getAppVersionCode(){
    def stdout = new ByteArrayOutputStream()
    exec {
        commandLine 'git','tag','--list'
        standardOutput = stdout
    }
    return stdout.toString().split("\n").size()
}

/**
 * 從git tag中獲取應用的版本名稱
 * @return git tag的名稱
 */
def getAppVersionName(){
    def stdout = new ByteArrayOutputStream()
    exec {
        commandLine 'git','describe','--abbrev=0','--tags'
        standardOutput = stdout
    }
    return stdout.toString().replaceAll("\n","")
}
複製程式碼

隱藏簽名檔案資訊

很多團隊在開發初期都是直接把簽名檔案放在git上(我司現在還是這麼幹的T_T),這樣的做法在開發團隊越來越大的時候會有安全問題,解決方式是將簽名檔案放在打包伺服器中,然後動態獲取。

例如你可以把簽名資訊配置在打包機器環境變數中,然後通過System.getenv("STORE_FILE")來獲取。

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    signingConfigs {
        def appStoreFile = System.getenv("STORE_FILE")
        def appStorePassword = System.getenv("STORE_PASSWORD")
        def appKeyAlias = System.getenv("KEY_ALIAS")
        def appKeyPassword = System.getenv("KEY_PASSWORD")

        //當不能從環境變數裡獲取到簽名資訊的時候,則使用本地的debug.keystore,這一般是
        //針對研發自己打包測試的情況
        if(!appStoreFile||!appStorePassword||!appKeyAlias||!appKeyPassword){
            appStoreFile = "debug.keystore"
            appStorePassword = "android"
            appKeyAlias = "androiddebugkey"
            appKeyPassword = "android"
        }
        release {
            storeFile file(appStoreFile)
            storePassword appStorePassword
            keyAlias appKeyAlias
            keyPassword appKeyPassword
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            zipAlignEnabled true
        }
    }
}
複製程式碼

相關文章