本篇文章主要在之前學習的基礎上,從實際開發的角度學習如何對 Android Gradle 來進行自定義以滿足不同的開發需求,下面是 Gradle 系列的幾篇文章:
- Gradle 系列之初識 Gradle
- Gradle 之 Groovy 基礎
- Gradle 系列之構建腳本基礎
- Gradle 系列之 Gradle 任務
- Gradle 系列之 Gradle 插件
- Gradle 系列之 Java Gradle 插件
- Gradle 系列之 Android Gradle 插件
- Gradle 系列之 Android Gradle 基礎配置
下面是主要內容:
- 修改生成的 Apk 文件名
- 版本信息統一管理
- 隱藏簽名文件信息
- 動態配置 AndroidManifest 文件
- 自定義 BuildConfig
- 動態添加自定義資源
- Java 編譯選項
- adb 操作選項配置
- DEX 選項配置
- 自動清理未使用的資源
- 突破 65535 方法限制
修改生成的 Apk 文件名#
修改打包輸出的 Apk 的文件名主要用到三個屬性:
applicationVariants //Android應用Gradle插件
libraryVariants //Android庫Gradle插件
testVariants //上述兩種插件都適用
下面是修改打包生成的 Apk 文件名的代碼,參考如下:
android{
//...
/**
* 修改打包生成的apk的文件名
*/
applicationVariants.all { variant ->
variant.outputs.all { output ->
if (output.outputFile != null && output.outputFile.name.endsWith('.apk') &&
'release' == variant.buildType.name) {
//輸出文件名
outputFileName = "AndroidGradleProject_v${variant.versionName}_${buildTime()}.apk"
}
}
}
}
//當前時間
def static buildTime() {
def date = new Date()
return date.format("yyyMMdd")
}
此時,執行 release 模式構建 Apk 的任務,生成的 Apk 的名字就修改了,當然還可以配置在 debug 模式下生成對應的文件名等。
版本信息統一管理#
每個應用都有一個版本,版本一般由三部分組成:major.minor.patch,第一個是主版本號,第二個是副版本號,第三個是補丁號,如 1.0.0 這種格式的版本號,在 Android 開發中最原始的版本配置方式就是在 build.gradle 中在 defaultConfig 中配置對應的版本號和版本名稱,參考如下:
//最原始的版本配置方式
android{
defaultConfig {
versionCode 1
versionName "1.0"
//...
}
}
實際開發中一般將這種版本相關的信息單獨定義在一個獨立的版本管理文件中進行統一管理,定義 version.gradle 文件如下:
ext{
//應用版本號、版本名稱
appversionCode = 1
appVersionName = "1.0"
//其他版本號...
}
然後在 build.gradle 中使用 version.gradle 文件中定義的版本號、版本名稱即可,參考如下:
//引入version.gradle文件
apply from: "version.gradle"
android {
//...
defaultConfig {
//使用version.gradle裡定義的版本號
versionCode appversionCode
//使用version.gradle裡定義的版本名稱
versionName appVersionName
//...
}
}
當然不只是應用的版本號,還有使用到的一些第三方的庫的版本也可以使用這樣的方式來統一管理。
隱藏簽名文件信息#
簽名文件信息是非常重要的信息,如果將簽名文件信息直接配置到項目中將是不安全的,那麼簽名文件如何能夠安全呢,簽名文件放在本地是不安全的,那麼只能放在服務器上才是安全的,打包的時候從服務器上讀取簽名文件信息即可,當然這個服務器也可以是一台專門用於打包正式 Apk 的電腦,將簽名文件和密鑰信息配置成環境變量,打包是直接從環境變量中讀取簽名文件和密鑰信息即可。
配置四個環境變量 STORE_FILE、STORE_PASSWORD、KEY_ALIAS、KEY_PASSWORD 分別對應簽名文件、簽名文件密碼、簽名文件密鑰別名、簽名文件密鑰密碼,環境變量的配置就不具體說了,代碼參考如下:
android {
//簽名文件配置
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')
//如果獲取不到相關簽名文件信息,則使用默認的簽名文件
if(!appStoreFile || !appStorePassword || !keyAlias || !keyPassword){
appStoreFile = "debug.keystore"
appStorePassword = "android"
appKeyAlias = "androiddebugkey"
appKeyPassword = "android"
}
release {
storeFile file(appStoreFile)
storePassword appStorePassword
keyAlias appKeyAlias
keyPassword appKeyPassword
}
debug {
//默認情況下,debug模式下的簽名已配置為Android SDK自動生成的debug簽名文件證書
//.android/debug.keystore
}
}
}
注意一點,配置好環境變量後,如果不能讀取到新配置的環境變量,重啟電腦後就能讀取到了,至於如何使用專用的服務器進行打包、讀取簽名文件信息實踐後再來介紹。
動態配置 AndroidManifest 文件#
動態配置 AndroidManifest 配置就是動態的去修改 AndroidManifest 文件中的一些內容,如友盟等第三方統計平台分析統計的時候,一般會要求要在 AndroidManifest 文件中指定渠道名稱,如下所示:
<meta-data android:value="CHANNEL_ID" android:name="CHANNEL"/>
這裡 CHANNEL_ID 要替換成不同渠道的名稱,如 baidu、miui 等各個渠道名稱,那麼如何動態的修改這些變化的參數呢,這裡需要用到 Manifest 占位符和 manifestPlaceholder,manifestPlaceholder 是 ProductFlavor 的一個屬性,是一個 Map 類型,可以配置多個占位符,具體代碼參考如下:
android{
//維度
flavorDimensions "channel"
productFlavors{
miui{
dimension "channel"
manifestPlaceholders.put("CHANNEL","google")
}
baidu{
dimension "channel"
manifestPlaceholders.put("CHANNEL","baidu")
}
}
}
上述代碼中配置了 flavorDimensions 屬性,這個屬性可以理解為維度,比如 release 和 debug 是一個維度、不同的渠道是另一個維度、免費版本還是付費版本又是另一個維度,如果這三個維度都要考慮,那麼生成 Apk 的格式就是 2 * 2 * 2 供 8 個不同的 Apk,從 Gradle 3.0 開始不管是一個維度還是多個維度,都必須使用 flavorDimensions 來約束,上面代碼中定義了一個維度 channel,再加上 buildType 中的 debug 和 release,故此時生成不同 Apk 的個數是 4 個,如下圖所示:
當然,如果沒有配置 flavorDimensions 則會出現如下錯誤,具體如下:
Error flavors must now belong to a named flavor dimension.
實際開發中根據實際情況配置對應的 flavorDimensions 即可。
然後,在 AndroidManifest 文件中使用占位符介紹打包時傳遞過來的參數,在 AndroidManifest 文件中添加 如下:
<meta-data android:value="${CHANNEL}" android:name="channel"/>
最後,執行對應的渠道包任務,如執行 assembleBaiduRelease 將會將 AndroidManifest 中的渠道替換成 baidu,可使用命令執行也可使用 Android Studio 選擇對應的 task 來執行,執行命令如下:
gradle assembleBaiduRelease
如果使用 Android Studio , 打開右側 Gradle 控制面板,找到對應的 task 來執行相應的任務,如下圖所示:
選擇對應的 task 執行就會生成對應的 Apk,使用 Android Killer 反編譯打開生成的 Apk ,查看 AndroidManifest 文件如下:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.manu.androidgradleproject">
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" roundIcon="@mipmap/ic_launcher_round">
<!--AndroidManifest文件修改成功-->
<meta-data android:name="channel" android:value="baidu"/>
<activity android:name="com.manu.androidgradleproject.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data android:name="android.support.VERSION" android:value="26.1.0"/>
<meta-data android:name="android.arch.lifecycle.VERSION" android:value="27.0.0-SNAPSHOT"/>
</application>
</manifest>
上述案例中,渠道的名稱是一致的,可以通過遍歷很方便的完成渠道名稱的替換,參考如下:
productFlavors.all{ flavor ->
manifestPlaceholders.put("CHANNEL",name)
}
這一小節重要的一點就是關於 manifestPlaceholders 占位符的使用。
自定義 BuildConfig#
BuildConfig 是一個在 Android Gradle 構建腳本編譯後生成的類,默認構建生成的 BuildConfig 內容如下:
/**
* Automatically generated file. DO NOT MODIFY
*/
package com.manu.androidgradleproject;
public final class BuildConfig {
public static final boolean DEBUG = false;
public static final String APPLICATION_ID = "com.manu.androidgradleproject";
public static final String BUILD_TYPE = "release";
public static final String FLAVOR = "baidu";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
}
上面 BuildConfig 中的一些常量都是關於應用的一些關鍵信息,其中 DEBUG 在 debug 模式下為 true,release 模式下為 false,此外還有應用包名、構建類型、構建渠道、版本號及版本名稱,所以如果開發中需要用到這些值可以在 BuildConfig 中直接獲取,比如包名的獲取一般是 context.getPackageName (),如果直接從 BuildConfig 中獲取是不是不僅方便而且有利於應用性能提升,所以,可在構建時在該文件中添加一些額外的有用的信息,可以使用 buildConfigField 方法,具體如下:
/**
* type:生成字段的類型
* name:生成字段的常量名稱
* value:生成字段的常量值
*/
public void buildConfigField(String type, String name, String value) {
//...
}
下面使用 buildConfigField 方法為每個渠道配置一個相關地址,參考如下:
android{
//維度
flavorDimensions "channel"
productFlavors{
miui{
dimension "channel"
manifestPlaceholders.put("CHANNEL","miui")
buildConfigField 'String' ,'URL','"http://www.miui.com"'
}
baidu{
dimension "channel"
manifestPlaceholders.put("CHANNEL","baidu")
//buildConfigField方法參數value中的內容是單引號中的,如果value是String,則String的雙引號不能省略
buildConfigField 'String' ,'URL','"http://www.baidu.com"'
}
}
}
再打包時就會自動生成添加的字段,構建完成後查看 BuildConfig 文件,生成了上面添加的字段,參考如下:
/**
* Automatically generated file. DO NOT MODIFY
*/
package com.manu.androidgradleproject;
public final class BuildConfig {
public static final boolean DEBUG = false;
public static final String APPLICATION_ID = "com.manu.androidgradleproject";
public static final String BUILD_TYPE = "release";
public static final String FLAVOR = "baidu";
public static final int VERSION_CODE = -1;
public static final String VERSION_NAME = "";
// Fields from product flavor: baidu
public static final String URL = "http://www.baidu.com";
}
至此,自定義 BuildConfig 的學習就到此為止,當然 buildConfigField 也可以使用到構建類型中,關鍵就是 buildConfigField 方法的使用。
動態添加自定義資源#
Android 開發中資源文件都是放置在 res 目錄下,還可以在 Android Gradle 中定義,自定義資源需要使用到 resValue 方法,該方法在 BuildType 和 ProductFlavor 對象中可以使用,使用 resValue 方法會生成相對應的資源,使用方式和在 res/values 文件中定義的一樣
android{
//...
productFlavors {
miui {
//...
/**
* resValue(String type,String name,String value)
* type:生成字段的類型(id、string、bool等)
* name:生成字段的常量名稱
* value:生成字段的常量值
*/
resValue 'string', 'welcome','miui'
}
baidu {
//...
resValue 'string', 'welcome','baidu'
}
}
}
當生成不同的渠道包時,通過 R.string.welcome 獲取的值是不相同的,如生成的百度的渠道包時 R.string.welcome 的值為 baidu、生成小米渠道包時 R.string.welcome 的值為 miui,構建時生成的資源的位置在 build/generated/res/resValues/baidu/... 下面的 generated.xml 文件中,文件內容參考如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Automatically generated file. DO NOT MODIFY -->
<!-- Values from product flavor: baidu -->
<string name="welcome" translatable="false">baidu</string>
</resources>
Java 編譯選項#
在 Android Gradle 中還可以配置 Java 源代碼的編譯版本,這裡使用到 compileOptions 方法, compileOptions 可配置三個屬性:encoding、sourceCompatibility 和 targetCompatibility,通過這些屬性來配置 Java 相關的編譯選項,具體參考如下:
//配置Java編譯選項
android {
compileSdkVersion 26
buildToolsVersion '26.0.2'
compileOptions{
//設置源文件的編碼
encoding = 'utf-8'
//設置Java源代碼的編譯級別()
sourceCompatibility = JavaVersion.VERSION_1_8
// sourceCompatibility "1.8"
// sourceCompatibility 1.8
// sourceCompatibility "Version_1_8"
//設置Java字節碼的版本
targetCompatibility = JavaVersion.VERSION_1_8
}
}
adb 操作選項設置#
adb 的全稱是 Android Debug Bridge,adb 主要用來連接手機來進行一些操作,比如調試 Apk、安裝 Apk、複製文件等操作,在 Android Gradle 中可借助 adpOptions 來配置,可配置的有兩個屬性:installOptions 和 timeOutInMs,也可以通過相應的 setter 方法來設置,具體參考如下:
android{
//adb配置選項
adbOptions{
//設置執行adb命令的超時時間
timeOutInMs = 5 * 1000
/**
* 設置adb install安裝這個操作的設置項
* -l:鎖定應用程序
* -r:替換已存在的應用程序
* -t:允許測試包
* -s:把應用程序安裝到SD卡上
* -d:允許應用程序降級安裝
* -g:為該應用授予所有運行時的權限
*/
installOptions '-r', '-s'
}
}
installOptions 的配置對應 adb install [-lrtsdg] 命令,如果安裝、運行或調試 Apk 的時候,如果出現 CommandRejectException 可以嘗試設置 timeOutInMs 來解決,單位是毫秒。
DEX 選項配置#
Android 中的源代碼被編譯成 class 字節碼,在打包成 Apk 的時候又被 dx 命令優化成 Android 虛擬機可執行的 DEX 文件,DEX 格式的文件是專為 Android 虛擬機設計的,在一定程度上會提高其運行速度,默認情況下給 dx 分配的內存是 1024M,在 Android Gradle 中可以通過 dexOptions 的五個屬性:incremental、javaMaxHeapSize、jumboMode、threadCount 和 preDexLibraries 來對 DEX 進行相關配置,具體參考如下:
android{
//DEX選項配置
dexOptions{
//設置是否啟用dx增量模式
incremental true
//設置執行dx命令為其分配的最大堆內存
javaMaxHeapSize '4g'
//設置是否開啟jumbo模式,如果項目方法數超過65535,需要開啟jumbo模式才能構建成功
jumboMode true
//設置Android Gradle運行dx命令時使用的線程數量,可提高dx執行的效率
threadCount 2
/**
* 設置是否執行dex Libraries庫工程,開啟後會提高增量構建的速度,會影響clean的速度,默認為true
* 使用dx的--multi-dex選項生成多個dex,為避免和庫工程衝突,可設置為false
*/
preDexLibraries true
}
}
自動清理未使用資源#
Android 開發中打包 Apk 總是希望在相同功能的情況下 Apk 體積儘量小,那就要在打包之前刪除沒有使用的資源文件或打包時不將無用的資源打包到 Apk 中,可以使用 Android Lint 檢查未使用的資源,但是無法清除一些第三方庫中的無用資源,還可以使用 Resource Shrinking,可在打包之前檢查資源,如果沒有使用則不會被打包到 Apk 中,具體參考如下:
//自動清理未使用資源
android{
buildTypes {
release {
//開啟混淆,保證某些資源在代碼中未被使用,以便於自動清理無用資源,兩者配合使用
minifyEnabled true
/**
* 打包時會檢查所有資源,如果沒有被引用,則不會被打包到Apk中,會處理第三方庫不使用的資源
* 默認不啟用
*/
shrinkResources true
//開啟zipalign優化
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug{
}
}
//...
}
為防止有用資源未被打包到 Apk 中,Android Gradle 提供了 keep 方法來配置那些資源不被清理,在 res/raw/ 下新建一個 xml 文件來使用 keep 方法,參考如下:
<!--keep.xml文件-->
<?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:discard="@layout/l_used"
tools:shrinkMode="safe"/>
可配置的三個屬性:keep 表示要保留的資源文件,可使用以 (,) 分割的資源列表,可使用 (*) 作為通配符,discard 表示要移除的資源,和 keep 類似,shrinkMode 用於設置自動清理資源的模式,一般設置為 safe 即可,如果設置為 strict 則有可能清除可能會使用的資源。
此外,還可以使用 ProductFlavor 提供的方法 resConfigs 和 resConfig,可配置那些資源打包到 Apk 中,使用方式如下:
android{
defaultConfig{
//參數可以是Android開發時的資源限定符
resConfigs 'zh'
//...
}
}
上述自動清理資源的方式只是不打包到 Apk 中,在實際的項目中並沒有被清除,可通過日誌查看哪些資源被清理了,然後決定要不要在項目中清除。
突破 65535 方法限制#
在 Android 開發中總會遇到方法數大於 65535 時出現異常,那為什麼會有這個限制呢,因為 Java 源文件被打包成一個 DEX 文件,這個文件是優化過的、可在 Dalvik 虛擬機上可執行的文件,由於 Dalvik 在執行 DEX 文件的時候,使用了 short 來索引 DEX 文件中的方法,這就意味著單個 DEX 文件可被定義的方法最多只有 65535 個。解決辦法自然是當方法數超過 65535 個的時候創建多個 DEX 文件。
從 Android 5.0 開始的 Android 系統使用 ART 的運行方式,原生支持多個 DEX 文件,ART 在安裝 App 的時候執行預編譯,把多個 DEX 文件合併成一個 oat 文件執行,在 Android 5.0 之前,Dalvik 虛擬機只支持單個 DEX 文件,要想突破單個 DEX 方法數超過 65535 的限制,需使用 Multidex 庫,這裡就不再贅述了。
總結#
本篇文章的很多內容都可以用到實際開發中,這篇文章也是在邊學習邊驗證的情況下完成的,斷斷續續花了一周時間,距離上次更文已有一周時間,希望閱讀此文能夠對你有所幫助。