個(gè)人博客:
http://www.milovetingting.cn
Android Gradle 高級(jí)自定義
使用共享庫(kù)
Android的包,如android.app,android.content,android.view,android.widget等,是默認(rèn)包含在Android SDK庫(kù)里的,所有應(yīng)用都可以直接使用它們。還有一些庫(kù),如com.google.android.maps,android.test.runner等,這些庫(kù)是獨(dú)立的,并不會(huì)被系統(tǒng)自動(dòng)鏈接,所以如果要使用的話,就需要單獨(dú)進(jìn)行生成使用,這類(lèi)庫(kù)我們稱(chēng)為共享庫(kù)。
在AndroidManifest.xml中,我們可以指定要使用的庫(kù):
<uses-library
android:name="com.google.android.maps"
android:required="true"
/>
這樣我們就聲明了需要使用maps這個(gè)共享庫(kù)。聲明之后,在安裝生成的apk包的時(shí)候,系統(tǒng)會(huì)根據(jù)我們的定義,幫助檢測(cè)手機(jī)系統(tǒng)是否有我們需要的共享庫(kù)。因?yàn)槲覀冊(cè)O(shè)置的android:required="true",如果手機(jī)系統(tǒng)不滿足,將不能安裝該應(yīng)用。
在Android中,除了標(biāo)準(zhǔn)的SDK,還存在兩種庫(kù):一種是add-ons庫(kù),它們位于add-ons目錄下,這些庫(kù)大部分是第三方廠商或者公司開(kāi)發(fā)的,一般是為了開(kāi)發(fā)者使用,但又不想暴露具體標(biāo)準(zhǔn)實(shí)現(xiàn);第二類(lèi)是optional可選庫(kù),它們位于platforms/androi-xx/optional目錄下,一般是為了兼容舊版本的API,比如org.apache.http.legacy。
對(duì)于第一類(lèi)add-ons附件庫(kù)來(lái)說(shuō),Android Gradle會(huì)自動(dòng)解析,幫我們添加到classpath里。第二類(lèi)optional可選庫(kù)就不會(huì),需要自己將這個(gè)可選庫(kù)添加到classpath中。Android Gradle提供了useLibrary方法,讓我們把一個(gè)庫(kù)添加到classpath中。
android{
useLibrary 'org.apache.http.legacy'
}
以上的配置已經(jīng)可以生成APK,并能安裝運(yùn)行。但最好也要在AndroidManifest文件中配置一下uses-library標(biāo)簽,以防出現(xiàn)問(wèn)題。
批量修改生成的apk文件名
Android對(duì)象為我們提供了3個(gè)屬性:applicationVariants(僅僅適用于Android應(yīng)用Gradle插件),libraryVariants(僅僅適用于Android庫(kù)Grdle插件),testVariants(以上兩種Gradle插件都適用)。
以上3個(gè)屬性返回的都是DomainObjectSet對(duì)象集合,訪問(wèn)它們都會(huì)觸發(fā)創(chuàng)建所有的任務(wù)。
以下為批量修改apk名稱(chēng)的示例:
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.wangyz.gradle"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
flavorDimensions "default"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
productFlavors {
baidu {
}
huawei {
}
}
applicationVariants.all {
variant ->
variant.outputs.all {
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 fileName = "channel_${flavorName}_${variant.versionName}.apk"
outputFileName = fileName
}
}
}
}
動(dòng)態(tài)生成版本信息
一般的版本由3部分組成:major.minor.patch,第一個(gè)是主版本號(hào),第二個(gè)是副版本號(hào),第三個(gè)是補(bǔ)丁號(hào)。
最原始的方式
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.wangyz.channel"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
分模塊的方式
可以把版本號(hào)的配置單獨(dú)抽取出來(lái),放在單獨(dú)的文件里,供build引用。
新建一個(gè)vesion.gradle文件:
ext{
appVersionCode = 1
appVersionName = "1.0.0"
}
在build.gradle中引用它:
apply from:'version.gradle'
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.wangyz.channel"
minSdkVersion 21
targetSdkVersion 28
versionCode appVersionCode
versionName appVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
先使用apply from加載version.gradle腳本文件,這樣它里面定義的擴(kuò)展屬性就可以使用了。
從git的tag中讀取
想獲取當(dāng)前的tag名稱(chēng),在git下非常簡(jiǎn)單,使用以下命令即可:
git describe --abbrev=0 --tags
知道了命令,那么如何在Gradle中動(dòng)態(tài)獲取呢?這就需要exec。Gradle提供了執(zhí)行shell命令非常簡(jiǎn)便的方法,即exec。它是一個(gè)Task任務(wù)。
def getAppVersionName(){
def stdout = new ByteArrayOutputStream()
exec{
commandLine 'git','describe','--abbrev=0','--tags'
standardOutput = stdout
}
return stdout.toString()
}
以上定義了一個(gè)獲取版本名稱(chēng)的方法,通過(guò)該方法獲取了git tag的名稱(chēng)后,就可以把它作為應(yīng)用的版本名稱(chēng),只要把versionName配置成這個(gè)方法就好了。
android {
compileSdkVersion 28
defaultConfig {
applicationId "com.wangyz.channel"
minSdkVersion 21
targetSdkVersion 28
versionCode 1
versionName getAppVersionName()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
從屬性文件中動(dòng)態(tài)獲取和遞增
1、在項(xiàng)目目錄下新建一個(gè)version.properties的屬性文件
2、把版本名稱(chēng)分為3部分major.minor.patch,版本號(hào)分為一部分number,然后在properties新增4個(gè)KV鍵值對(duì)
3、在build.gradle新建一個(gè)方法用于讀取該屬性文件
隱藏簽名文件信息
定義一個(gè)文件,用來(lái)保存簽名的相關(guān)信息,如sign.properties,這個(gè)文件加入.gitignore,不上傳到git中。通過(guò)讀取這個(gè)文件來(lái)獲取配置信息。
android{
signingConfigs{
release{
def signPropertiesFile = 'sign.properties'
storeFile file(readSignProperties(signPropertiesFile,'storeFile'))
storePassword readSignProperties(signPropertiesFile,'storePassword')
keyAlias readSignProperties(signPropertiesFile,'keyAlias')
keyPassword readSignProperties(signPropertiesFile,'keyPassword')
}
}
}
buildTypes{
release{
signingConfig signingConfigs.release
}
}
def readSignProperties(String filePath,String key){
File file = file(filePath)
if(file.exists())
{
def Properties properties = new Properties()
properties.load(new FileInputStream(file))
return properties[key]
}
return "file not exist!"
}
sign.properties內(nèi)容如下:
storeFile=android.keystore
storePassword=android
keyAlias=android
keyPassword=android
動(dòng)態(tài)配置AndroidManifest文件
Android Gradle 提供了非常便捷的方法讓我們來(lái)替換AndroidManifest文件中的內(nèi)容,它就是manifestPlaceholder,Manifest占位符。
android{
productFlavors{
google{
manifestPlaceholders = [APP_CHANNEL:"google"]
}
baidu{
manifestPlaceholders = [APP_CHANNEL:"baidu"]
}
}
}
在AndroidManifest文件中使用
<application>
<meta-data
android:name="APP_CHANNEL"
android:value="${APP_CHANNEL}"
/>
</application>
如果需要批量修改(假設(shè)需要將名稱(chēng)改為和渠道名一樣),可以通過(guò)productFlavors迭代方法:
android{
productFlavors{
google{
}
baidu{
}
}
productFlavors.all{
flavor->
manifestPlaceholder = [APP_CHANNEL:name]
}
}
自定義BuildConfig
Android Gradle 提供了buildConfigField(String type,String name,String value)讓我們添加自己的常量到BuildConfig中。使用示例:
android{
productFlavors{
google{
buildConfigField 'String','URL','"http://www.google.com"'
}
baidu{
buildConfigField 'String','URL','"http://www.baidu.com"'
}
}
}
需要注意,value這個(gè)參數(shù),是單引號(hào)中間的部分,尤其對(duì)于String類(lèi)型的值,里面的雙引號(hào)不能省略。value是什么就寫(xiě)什么,原封不動(dòng)地放在單引號(hào)里。
上面是渠道,productFlavor,其實(shí)不光渠道可以配置自定義字段,構(gòu)建類(lèi)型BuildType也可以配置。如:
android{
buildTypes{
debug{
buildConfigField 'String','NAME','"zhangsan"'
}
}
}
動(dòng)態(tài)添加自定義的資源
實(shí)現(xiàn)這一功能的方法是resValue方法。它在BuildType和ProductFlavor這兩個(gè)對(duì)象中存在。它會(huì)生成一個(gè)資源,效果和在res/values文件中定義一個(gè)資源是等價(jià)的。
resValue方法有三個(gè)參數(shù),第一個(gè)是type,也就是你要定義資源的類(lèi)型,比如有string,id,bool等;第二個(gè)是name,也就是定義資源的名稱(chēng),以便在工程中引用;第三個(gè)是value,就是定義資源的值。
android{
productFlavors{
google{
resValue 'string','tip','hello'
}
baidu{
resValue 'string','tip','hi'
}
}
}
Java編譯選項(xiàng)
Android對(duì)象提供了一個(gè)compileOptions方法,接受一個(gè)CompileOptions類(lèi)型的閉包作為參數(shù),來(lái)對(duì)Java編譯選項(xiàng)進(jìn)行配置:
android{
compileOptions{
encoding 'utf-8'
sourceCompatibility JavaVersion.VERSION_1_6
targetCompatibility JavaVersion.VERSION_1_6
}
}
CompileOptions是編譯配置,它提供三個(gè)屬性,分別是encoding,sourceCompatibility,targetCompatibility,通過(guò)對(duì)它們進(jìn)行設(shè)置來(lái)配置Java相關(guān)的編譯選項(xiàng)。
sourceCompatibility是配置Java源代碼的編譯級(jí)別
targetCompatibility是配置生成的Java字節(jié)碼的版本
adb操作選項(xiàng)配置
在Android Gradle 中,為我們預(yù)留了對(duì)adb的一些選項(xiàng)的控制配置,它就是adbOptions{}閉包。
android{
adbOptions{
timeOutInMs 5*1000
installOptions '-r','-s'
}
}
DEX選項(xiàng)配置
Android Gradle 提供了dexOptions{}閉包,讓我們可以對(duì)dx操作進(jìn)行一些配置。
android{
dexOptions{
incremental true
}
}
突破65535方法限制
Java源文件被打包成一個(gè)DEX文件,這個(gè)文件就是優(yōu)化過(guò)的、Dalvik虛擬機(jī)可執(zhí)行的文件,Dalvik虛擬機(jī)在執(zhí)行DEX文件時(shí),使用了short類(lèi)型來(lái)索引DEX文件中的方法,這就意味著單個(gè)DEX文件可以被定義的方法最多只有是65535,當(dāng)定義的方法數(shù)量超過(guò)時(shí),就會(huì)出錯(cuò)。
Android官方給出的解決方案:Multidex。對(duì)于Android5.0以后的版本,使用了ART的運(yùn)行方式,可以天然支持App有多個(gè)DEX文件,ART在安裝App的時(shí)候執(zhí)行預(yù)編譯,把多個(gè)DEX文件合并成一個(gè)oat文件執(zhí)行。對(duì)于Android5.0之前的版本,Dalvik虛擬機(jī)限制每個(gè)App只能有一個(gè)class.dex,要使用它們,就得使用Android提供的Multidex庫(kù)。
要在項(xiàng)目中使用Multidex,首先要修改Gradle build配置文件,啟用Multidex,并同時(shí)配置Multidex需要的jar依賴。
android{
defaultConfig{
multiDexEnabled true
}
}
dependencies{
implementation 'com.android.support:multidex:1.0.1'
}
配置好之后,開(kāi)啟了Multidex,會(huì)讓我們的方法多于65535個(gè)的時(shí)候生成多個(gè)DEX文件,名字為classes.dex,classes(...n)這樣的形式。但是對(duì)于Android5.0以前的系統(tǒng)虛擬機(jī),它只認(rèn)識(shí)一個(gè)DEX,名字還是classes.dex,所以想達(dá)到程序可以正常運(yùn)行的目的,也要讓虛擬機(jī)把其它幾個(gè)生成的classes加載進(jìn)來(lái)。要做到這步,就必須在App程序啟動(dòng)的入口控制,這個(gè)入口就是Application。
Multidex提供現(xiàn)成的Application,名字是MultiDexApplication,如果我們沒(méi)有自定義Applicaiton的話,直接使用MultiDexApplication即可,在Manifest清單文件中配置:
<application
...
android:name="android.support.multidex.MultiDexApplication"
>
...
</application>
如果有自定義的Application,并且是直接繼承自Application,那么只需要把繼承改為我們的MultiDexApplication即可。
如果自定義的Application是繼承自第三方提供的Application,就不能改繼承了,這個(gè)時(shí)候可以重寫(xiě)attachBaseContext方法來(lái)實(shí)現(xiàn):
@Override
protected void attachBaseContext(Context base){
super.attachBaseContext(base);
MultiDex.install(this);
}
雖然有了解決65535問(wèn)題的方法,但還是要盡量避免我們工程中的方法超過(guò)65535.首先不能濫用第三方庫(kù),如果引用,最好也要自己精簡(jiǎn)。精簡(jiǎn)后,還要使用ProGuard減小DEX的大小。還有因?yàn)镈alvik linearAlloc的限制,尤其在2.2和2.3版本上,只有5MB,到Android 4.0的時(shí)候升級(jí)到8MB,所以低于4.0的系統(tǒng)上dexopt的時(shí)候可能會(huì)崩潰。
自動(dòng)清理未使用的資源
Android Gradle 為我們提供了在構(gòu)建打包時(shí)自動(dòng)清理未使用的資源的方法,這個(gè)就是Resource Shrinking。
Resource Shringking要結(jié)合Code Shringking一起使用,即我們開(kāi)發(fā)中經(jīng)常使用的ProGuard,也就是我們要啟用minifyEnabled,是為了減縮代碼。
android{
buildTypes{
release{
minifyEnabled true
shringkResources true
...
}
}
}
自動(dòng)清理未使用的資源這個(gè)功能雖然好用,但是有時(shí)候會(huì)誤刪有用的程序,因?yàn)槲覀冊(cè)诖a編寫(xiě)的時(shí)候,可能會(huì)使用反射去引用資源,尤其很多的第三方庫(kù)會(huì)這么做,這個(gè)時(shí)候Android Gradle就區(qū)分不出來(lái),可能會(huì)誤認(rèn)為這些資源不有被使用。針對(duì)這種情況,Android Gradle提供了keep方法來(lái)讓我們配置哪些資源不被清理。
keep方法使用非常簡(jiǎn)單,我們要新建一個(gè)xml文件來(lái)配置,這個(gè)文件是res/raw/keep.xml,然后通過(guò)tools:keep屬性來(lái)設(shè)置。這個(gè)tools:keep接受一個(gè)以逗號(hào)分隔的配置資源列表,并且支持星號(hào)*通配符。
<?xml version="1.0" encoding="utf-8">
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/layout_a,@layout/layout_b" />
keep.xml還有一個(gè)屬性是tools:shrinkMode,用于配置自動(dòng)清理資源的模式,默認(rèn)是false,是安全的。
除了shrinkResources之外,Android Gradle 還提供了一個(gè)resConfigs,它屬于ProductFlavor的一個(gè)方法,可以讓我們配置哪些類(lèi)型的資源才會(huì)被打包進(jìn)APK中。
android{
defaultConfig{
resConfigs 'zh'
}
}
上面代碼表示,只保留zh資源。