關於做者php
郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。java
文章目錄android
關於文章封面,道理我都懂,你放個妹紙在文章封面上有什麼意義嗎?🙄git
狀況是這樣的,昨天有個bug困擾了我一天,晚飯時分聽到了T-ara的歌《我怎麼辦》,伴隨着歡快的節奏,突然思緒大開,解決了那個 bug,說到T-ara,固然要放在她們的主唱樸素妍的照片辣~🤓程序員
閒話很少說,正文時間到。本篇文章是《大型Android項目的工程化之路》的開篇之做,這個系列的文章主要用來討論伴隨着Android項目愈來愈大時,如何處理編譯與構建、VCS工做流、模塊化、持續集成等問題,以及 一些應用黑科技插件化、熱更新的實現方案,目前規劃的內容以下:github
首先讓咱們進入第一個主題,基於Gradle的項目的編譯與構建。shell
Gradle是一個基於Apache Ant和Apache Maven概念的項目自動化建構工具。它使用一種基於Groovy的特定領域語言來聲明項目設置,大部分功能都經過 插件的方式實現。編程
官方網站:https://gradle.org/api
官方介紹:From mobile apps to microservices, from small startups to big enterprises, Gradle helps teams build, automate and deliver better software, faster.安全
在正式介紹Gradle以前,咱們先了解下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"
}
複製代碼
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}")
}
}
複製代碼
def method(int a, int b){
if(a > b){
a
}else {
b
}
}
def callMethod(){
method 1, 2
}
複製代碼
能夠看到,和Kotlin這些現代編程語言同樣,有不少語法糖。瞭解了Groovy,咱們再來看看Gradle工程相關知識。
一個標準的Android 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'
}
複製代碼
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' } 複製代碼
Gradle Wrapper是對Gradle的一層包裝,目的在於團隊開發中統一Gradle版本,通常能夠經過gradle wrapper命令構建,會生成如下文件:
文件用來進行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地址下載比較慢,能夠去國內的鏡像地址下載。
代碼壓縮經過 ProGuard 提供,ProGuard 會檢測和移除封裝應用中未使用的類、字段、方法和屬性,包括自帶代碼庫中的未使用項(這使其成爲以變通方式解決 64k 引用限制的有用工具)。 ProGuard 還可優化字節碼,移除未使用的代碼指令,以及用短名稱混淆其他的類、字段和方法。混淆過的代碼可令您的 APK 難以被逆向工程,這在應用使用許可驗證等安全敏感性功能時特別 有用。
android {
buildTypes {
release {
minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } ... } 複製代碼
除了 minifyEnabled 屬性外,還有用於定義 ProGuard 規則的 proguardFiles 屬性:
咱們能夠在項目裏的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,它幫咱們收集了主要第三方庫的混淆規則,能夠 參考下。
混淆完成後都會輸出下列文件:
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' } } } 複製代碼
資源壓縮經過適用於 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"
}
}
複製代碼
Android的項目通常分爲應用項目、庫項目和測試項目,它們對應的Gradle插件類型分別爲:
咱們通常只有一個應用項目,可是會有多個庫項目,經過添加依賴的方式引用庫項目。
例如:
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'
複製代碼
依賴類型主要分爲五種:
注:Gradle 3.0已經廢棄了compile,並新增了implementation與api兩個命令,它們的區別以下:
在編譯庫的時候,咱們一般選擇的遠程庫是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多渠道打包。 多渠道打包主要是經過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 --refresh-dependencies assemble
複製代碼
查看app全部依賴庫
gradle dependencies :app
複製代碼
查看編譯時依賴
gradle dependencies -configuration compile
複製代碼
查看運行時依賴
gradle dependencies -configuration runtime
複製代碼
有些時候想改變輸入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 } } } 複製代碼