這一章主要針對項目中可以用到的一些實用功能來介紹Android Gradle,比如如何隱藏我們的證書文件,降低風險;如何批量修改生成的apk文件名,這樣我們就可以修改成我們需要的,從文件名中就可以看到渠道,版本號以及生成日期等信息,這多方便?。贿€有其他突破65535方法的限制等等。
9.1 使用共享庫
android的包,比如android.app, android.content, android.view, 以及android.widget等,這些是默認就包含在android sdk庫里的,所有的應(yīng)用都可以直接使用它們,系統(tǒng)會幫我們會自動鏈接他們,不會出現(xiàn)找不到相關(guān)類的情況。還有一些庫,比如com.google.android.maps、android.test.runner等,這些庫是獨立的,并不會被系統(tǒng)自動鏈接,所以我們要使用他們的話,就需要單獨進行生成使用,這類庫我們稱之為共享庫。
在AndroidManifest文件中,我們可以通過<uses-library>來指定我們要使用的庫

這樣我們就聲明了我們需要使用maps這個共享庫,聲明之后,在安裝生成的APK包的時候,系統(tǒng)會根據(jù)我們的定義,幫我們檢測我們的手機系統(tǒng)是否有我們需要的共享庫,因為我們設(shè)置的android:required="true",是必須,如果手機系統(tǒng)不滿足,將不能安裝該應(yīng)用。
在Android中,除了我們標準的SDK,還存在兩種庫,一種是add-ons庫,他們位于add-ons目錄下,這些庫大部分第三方廠商或者公司開發(fā)的,一般是為了讓開發(fā)者使用,但是又不想暴漏具體標準實現(xiàn)的;第二類是optional可選庫,他們位于platforms/android-xx/optional目錄下,一般是為了兼容舊版本的API,比如org.apache.http.legacy,這是一個HttpClient的庫,從API23開始,標準的Android SDK中不再包含HttpClient庫,如果還想使用HttpClient庫,就必須使用org.apache.http.legacy這個可選庫。
對第一類add-ons附件庫來說,Android Gradle會自動解析,幫我們添加到classpath里,但是第二類optional可選庫就不會了,我們看下關(guān)于這兩種庫的Android Gradle源碼說明,位于IAndroidTarget.java文件中

這時候我們就需要自己把這個可選庫添加到classpath中,為此,Android Gradle為我們提供了useLibrary方法,讓我們可以把一個庫添加到我們的classpath中,這樣我們才能在代碼中使用他們。
只要知道它的名字,我們就可以使用useLibrary把他們添加到classpath,這樣我們的編譯就可以通過了。useLibrary是一個方法,看下它的源代碼實現(xiàn)
public void useLibrary(String name) {
this.useLibrary(name, true);
}
public void useLibrary(String name, boolean required) {
this.libraryRequests.add(new LibraryRequest(name, required));
}
以上的Android Gradle配置已經(jīng)可以生成Apk安裝運行,但是按照上面的兩類庫的官方源代碼說明文檔,我們最好也要在AndroidManifest文件中配置下uses-library標簽,以防萬一。
對于Api Level低于23的系統(tǒng)來說,默認的標準庫里已經(jīng)包含了Apache HttpClient庫,所以我們這里的Android Gradle配置只是為了保證編譯的通過,那么對于等于或者大于23的系統(tǒng)呢?系統(tǒng)標準包(不是Android 開發(fā)Sdk提供,是手機里)里有沒有Apache HttpClient庫呢?如果沒有,是不是已經(jīng)把他當成一個共享庫呢?試試如果不在AndroidManifest文件中配置下uses-library標簽是否可以運行?友情提示:PackageManager().getSystemSharedLibraryNames()方法。
9.2 批量修改生成的apk文件名
普通的Java比較簡單,因為它有一個有限的任務(wù)集合,而且它的屬性或者方法都是Java Gradle插件添加的,比較固定,而且我們訪問任務(wù)以及任務(wù)里的方法和屬性都比較方便,比如classes這個編譯Java源代碼的任務(wù),我們通過project.tasks.classes就可以訪問它,非??旖荩菍τ贏ndroid工程,就不行了,Android工程相對與Java工程來說,要復雜的多,因為它有很多相同的任務(wù),這些任務(wù)的名字都是通過Build Types和Product Flavors 生成的,是動態(tài)的創(chuàng)建和生成的,而且時機比較靠后,如果你還像原來一樣在某個閉包里通過project.tasks獲取一個任務(wù),會提示找不到該任務(wù),因為還沒有生成。
既然要修改生成的Apk文件名,那么我們就要修改Android Gradle打包的輸出,為了解決這個問題(不限于此),android對象為我們提供了2個屬性:
- applicationVariants (僅僅適用于Android應(yīng)用Gradle插件)
- libraryVariants (僅僅適用于Android庫Gradle插件)
- testVariants (以上兩種Gradle插件都使用)
以上三個屬性返回的都是DomainObjectSet對象集合,里面元素分別是ApplicationVariant、LibraryVariant和TestVariant。這三個元素直譯來看是變體,通俗的講他們就是Android構(gòu)建的產(chǎn)物,比如ApplicationVariant代表google渠道的release包,也可以代表dev開發(fā)用的debug包,我們上面提到了,他們基于Build Types和Product Flavors生成的產(chǎn)物,后面的多渠道打包章節(jié)我們會詳細講解。
特別注意的是,訪問以上這三種集合都會觸發(fā)創(chuàng)建所有的任務(wù),這意味著訪問這些集合后無須重新配置就會產(chǎn)生,也就是說假如我們通過訪問這些集合,修改生成Apk的輸出文件名,那么就會自動的觸發(fā)創(chuàng)建所有任務(wù),此時我們修改后的新的Apk文件名就會起作用,達到可我們修改Apk文件名的目的,因為這些是一個集合,包含里我們所有生成的產(chǎn)物,所以我們只需要進行迭代,就可以達到我們批量修改Apk文件名的目的。
com.android.build.gradle.AppExtension中的getApplicationVariants方法
public DomainObjectSet<ApplicationVariant> getApplicationVariants() {
return this.applicationVariantList;
}
下面我們給出一個批量修改Apk文件名的例子

applicationVariants是一個DomainObjectCollection集合,我們可以通過all方法進行遍歷,遍歷的每一個variant都是一個生成的產(chǎn)物,針對示例,共有g(shù)oogleRelease和googleDebug兩個產(chǎn)物,所以遍歷的variant共有g(shù)oogleRelease和googleDebug。
applicationVariants中的variant都是ApplicationVariant,通過查看源代碼,可以看到它有一個outputs作為它的輸出,每一個ApplicationVariant至少有一個輸出,也可以有多個,所以這里的outputs屬性是一個List集合,我們再遍歷它,如果它的名字是以.apk結(jié)尾的話那么就是我們要修改的apk名字了,然后我們就可以根據(jù)需求,修改成我們想要的名字,我這里修改的是以'項目名_渠道名v版本名稱構(gòu)建日期.apk'格式生成的文件名,這樣通過文件名就可以把該apk的基本信息了解,比如什么渠道,什么版本,什么時候構(gòu)建的等等,最后生成的示例apk名字為Example92_google_v1.0_20160229.apk,大家可以運行測試一下,注意buildTime這個我們自定義的返回日期格式的方法。
這一小節(jié)主要介紹批量修改Apk文件名,其中涉及到了對現(xiàn)有生成產(chǎn)出(變體)的操縱,然后引出了多渠道以及操縱任務(wù)等信息的兩個屬性集合,并且對他們做了簡單介紹,后面的多渠道打包一章我會詳細講解,這里大家大概了解下原理,會使用即可。
9.3 動態(tài)生成版本信息
每一個應(yīng)用都會有一個版本號,這樣用戶就知道自己安裝的應(yīng)用是哪個版本,是不是最新版,有了問題,也可以找客服報上自己的版本,讓客服有針對性的幫用戶解決問題。一般的版本有三部分構(gòu)成:major.minor.patch,第一個是主版本號,第二個是副版本號,第三位補丁號,這種我們常見的見識1.0.0這樣的,當然也有兩位的1.0,對應(yīng)major.minor,這里我們以三位為例。
最開始的時候我們都是配置在build文件里的,如下:

這種方式我們直接寫在versionName的后面,比較直觀。但是這種方式有個很大的問題就是修改不方便,特別當我們的build文件中有很多代碼時,不容易找,而且修改容易出錯,代碼版本管理時也容易產(chǎn)生沖突。
9.3.2 分模塊的方式
既然最原始的方式,修改不方便,那么我們可以不可以把版本號的配置單獨的抽取出來的,放在單獨的文件里,供build引用,就像我們在Android里,單獨新建一個存放常量的Java類一樣,供其他類調(diào)用,幸運的是,android是支持基于文件的模塊化的,他就是apply from。
還記得我們應(yīng)用插件的知識吧,我們不光可以應(yīng)用一個插件,也可以把另一個gradle文件引用進來。我們新建一個version.gradle文件,用于專門存放我們的版本。

ext{}塊表明我們要為當前project創(chuàng)建擴展屬性,以供其他腳本引用,他就像我們java里的變量一樣。創(chuàng)建好之后,我們在build.gradle中引用它。

這種方式,我們每次只用修改version.gradle里的版本號即可,方便,容易,也比較清晰,在團隊協(xié)作的過程中,大家看到這個文件,就能猜測出來它大概是做什么的。
9.3.3 從git的tag中獲取
一般jenkins打包發(fā)布的時候,我們都會從我們已經(jīng)打好的一個tag打包發(fā)布,而tag的名字一般就是我們的版本名稱,這時候我們就可以動態(tài)的獲取我們的tag名稱作為我們應(yīng)用的名稱,可能你用的不是git版本控制系統(tǒng),但是大同小異,這里以git為例。
想獲取當前的tag名稱,在git下非常簡單,使用如下命令即可
git describe --abbrev=0 --tags
知道了命令,那么我們?nèi)绾卧趃radle中動態(tài)獲取呢,這就需要我們的exec了,gradle為我們提供了執(zhí)行shell命令非常簡便的方法,這就是Exec,它是一個Task任務(wù),我們可以創(chuàng)建一個繼承Exec的任務(wù)來執(zhí)行我們的shell命令,但是比較麻煩,還好Gradle已經(jīng)為我們想到了這個問題,為我們在Project對象里提供了exec方法。

其參數(shù)接受閉包和Action兩種方式,一般我們都是采用閉包的方式,其閉包的配置是通過ExecSpec對象來配置的。
從ExecSpec源代碼中我們可以看出,Project的exec方法的閉包可以有commandLine屬性、commandLine方法、args屬性以及args方法等配置供我們使用,我們這里只需要commandLine方法就可以達到目的了。

以上示例定義了一個getAppVersionName方法來獲取我們的tag名稱,exec執(zhí)行后的輸出可以用standardOutput獲得,它是BaseExecSpec的一個屬性,ExecSpec繼承了BaseExecSpec,所以我們可以在exec{}閉包中使用。
通過該方法我們獲取了git tag的名稱后,就可以把它作為我們應(yīng)用的版本名稱了,使用非常簡單,只用把我們的versionName配置成這個方法就好了,剛剛我們演示的時候是一個名為appVersionName的擴展屬性。

以上我們通過git tag動態(tài)獲取了版本名稱,那么版本號我們?nèi)绾蝿討B(tài)獲取呢?版本號作為我們內(nèi)部開發(fā)的標識,主要用于控制應(yīng)用進行生成,一般它是+1遞增的,每一次發(fā)版,其值就+1,而每一次發(fā)版我們就會打一個tag,tag的數(shù)量也會增加1個,和我們版本號的遞增邏輯是符合的,那么我們是不是可以把git tag的數(shù)量作為我們的版本號呢?答案是肯定的,這樣打包發(fā)版之前,我們只需打個tag,tag數(shù)量+1,版本號也會跟著+1,達到了我們的目的。

以上示例我們定義一個getAppVersionCode方法來獲取git tag的數(shù)量,用于我們的版本號,然后我們在defaultConfig里使用這個方法即可,替換掉我們的appVersionCode變量。

大功告成,這樣我們在發(fā)版打包之前,只需要打一個tag,然后Android Gradle打包的時候就會自動幫我們生成應(yīng)用的版本名稱和版本號,非常方便,再也不用為維護應(yīng)用的版本信息擔心了,這也是我們使用Gradle構(gòu)建的靈活之處,如果使用Ant,會麻煩的多,有興趣的同學可以思考一下。
9.3.4 從屬性文件中動態(tài)獲取和遞增
其實上一小結(jié)已經(jīng)可以滿足我們大部分的情況了,如果大家不想用,或者想自己更靈活的控制版本信息,可以采用Properties屬性文件的方式,這里我不給出示例代碼了,僅給出思路,大家可以自己練習實現(xiàn)一下,如果遇到問題,可以到通過前言里的聯(lián)系作為給我發(fā)郵件或者加QQ群的方式交流。
大致思路如下:
- 在項目目錄下新建一個version.properties的屬性文件。
- 把版本名稱分成三部分major.minor.patch,版本號分成一部分number,然后在version.properties中新增四個K_V鍵值對,其key就是我們上面分好的,value是對應(yīng)的值。
- 然后在build.gradle里新建兩個方法,用于讀取該屬性文件,獲取對應(yīng)Key的值,然后把major.minor.patch這三個key拼接成版本名稱,number用于版本號。
- 以上就達到了獲取版本信息的目的,獲取使用之后,我們還要更新我們存放在version.properties文件中的信息,這樣就可以達到版本自增的目的,以供下次使用。
- 在更新版本名稱三部分的時候,你可以自定義自己的邏輯,是逢10高位+1呢,還是其他算法,都可以自己靈活定義。
- 使用版本信息,更新version.properties文件的時機,記得doLast這個方法哦,O(∩_∩)O~
- 記得不會在自己運行調(diào)試的時候讓你的版本信息自增哦,如何控制呢?就是要區(qū)分是真正的打包發(fā)版,還是平時的調(diào)試、測試,有很多辦法來區(qū)分的。
這一小結(jié)到這里也寫完了,動態(tài)獲取生成版本信息的思路都大同小異,只是信息來源不一樣,比如git tag,比如version配置等等,你自己的業(yè)務(wù)項目中還可以從其他更多的渠道來生成,這也是因為gradle的靈活,我們才可以隨心所欲的做到這么多。
9.4 隱藏簽名文件信息
很多團隊一開始的成立的時候,十來個人,三五條槍,就開始創(chuàng)業(yè)了,每個組基本上就一個人,扛起所有。開始的時候,大家都不知道這款產(chǎn)品是否可以成功,所以也都沒想那么多,只能小步快跑,快速迭代,占領(lǐng)市場,搶占用戶,這才是最重要的。
隨著產(chǎn)品越做越好,團隊越來越大,組內(nèi)成員越來越多,就開始注重團隊協(xié)作,編碼規(guī)范,性能安全,團隊建設(shè)等等,因為只有做到這些,整個團隊的工作效率和產(chǎn)出才能更高,才能有團隊的威力,越到最后靠的是團隊,而不是一個人。
以前我們都是把App的簽名證書和相關(guān)秘鑰放在項目中,托管在git上,這樣做非常方便,可以直接訪問打包,并且借助git這個代碼管理平臺維護管理。但是簽名信息這個是我們應(yīng)用非常重要的信息,屬于公司重要的資源,所以我們要做到分級管理,保證安全,這也是公司保密措施的一部分,所以基于此,我們講下簽名信息如何隱藏,又能保證每個人可以打正式簽名的包。
簽名信息既然不能放在項目中,那么就需要有個地方存放他們,既然不能在每個開發(fā)者的電腦上,那就只能放到服務(wù)器上了,所以要實現(xiàn)這個,你還得有自己的專門用于打包發(fā)版的服務(wù)器,我們把簽名文件和密鑰信息放到服務(wù)器上,在打包的時候去讀取即可,下面我們以使用環(huán)境變量的方式為例。

然后我們在打包的機器上配置以上環(huán)境變量即可,window和linux的方式不一樣,補關(guān)于配置環(huán)境變量這一塊的知識,大家可以自行g(shù)oogle一下。
如果你是使用Jenkins這類CI打包,以Jenkins,它的配置里就可以指定Jenkins使用的環(huán)境變量,這樣我們就不用區(qū)分linux和window了,只需要在Jenkins里配置即可。
以上配置好之后,我們就可以進行打包使用了,簽名信息也做了隱藏,看到這里,相信大家也意識到了一個問題,那就是每個開發(fā)者電腦上并沒有如上的環(huán)境變量配置,因為簽名信息對他們是隱藏的,那么他們?nèi)绾芜M行打包測試呢?這就需要我們兩個一個debug簽名上場了,我們直接使用android自己提供的debug簽名即可,因為我們需要的是簽名,保證可以生成App測試(非debug調(diào)試)即可,比如給測試。
首先我們要從我們自己的電腦目錄上提取出來Android自帶的debug簽名,一般在你的${HOME}/.android/目錄下,找到后拷貝到我們的工程目錄下,其次找到他們的簽名信息,比如密碼,key等,這是公開的,我們可以參考Android文檔。

關(guān)鍵的邏輯就是在signingConfigs中加了判斷代碼,如果簽名信息四要素中的任何一個沒有獲取到,就使用默認的簽名信息,這樣當我們在打包服務(wù)器進行打包的時候就會使用正式發(fā)布的簽名,因為我們已經(jīng)在服務(wù)器上配置了簽名信息的環(huán)境變量;當每個開發(fā)者自己生成Release包的時候,因為本機沒有配置,就使用默認的簽名。
假如有的開發(fā)者有時候也需要使用正式發(fā)布的簽名打正式的包,用于升級測試等目的,也是可以做到的,比如Jenkins,給每個開發(fā)者開放一個賬號,他們自己新建個Job就可以打正式的包了,打了之后可以在生成的構(gòu)建里下載,關(guān)于Jenkins的具體使用我們后面的章節(jié)會詳細講。
好了,這一小節(jié)講到這里也算是結(jié)束了,這一小節(jié)的目的是如何隱藏我們的簽名信息,既能保證簽名信息的安全性,又可以進行正式的打包,其中的關(guān)鍵點是一個專有的打包服務(wù)器,如果你們公司還沒有的話,趕緊試試吧,優(yōu)點很多,本小節(jié)就是其中之一,還有打包速度快,開發(fā)打包并行,晚上大半夜都可以定時打包等等,打包成功之后還能自動的發(fā)給我們的市場人員,也就是‘小’自動化部署,O(∩_∩)O~。
9.5 動態(tài)配置AndroidManifest文件
動態(tài)配置AndroidManifest文件,顧名思義,就是我們可以在構(gòu)建的過程中,動態(tài)的修改Androidmanifest文件中的一些內(nèi)容。這樣的例子非常多,比如我們使用友盟等第三方分析統(tǒng)計的時候,會要求我們在AndroidManifest文件中指定渠道名稱。

示例中的Channel ID我們要替換成不同渠道的名稱,比如google,baidu,miui等等。
對于這種情況我們不可能定義很多個AndroidManifest文件,因為這種工作繁瑣,而且維護麻煩,所以我們就需要在構(gòu)建的時候,根據(jù)我們正在生成的不同渠道包來為其指定不同的渠道名,對于這種情況Android Gradle為我們提供了非常便捷的方法讓我們來替換AndroidManifest文件中的內(nèi)容,它就是ManifestPlaceholder,Manitest占位符。
manifestPlaceholders是ProductFlavor的一個屬性,他是一個Map<String, Object>類型,所以我們可以同時配置很多個占位符。下面我們就通過這個配置渠道號的例子來演示manifestPlaceholders的用法。

例子中我們定義了兩個渠道google和baidu,并且配置了他們的manifestPlaceholders。留意我們的使用方式,他們的Key都是一樣的,是UMENG_CHANNEL,這個key就是我們在AndroidManifest文件中的占位符變量,在構(gòu)建的時候,它會把AndroidManifest文件文件中所有占位符變量為UMENG_CHANNEL的內(nèi)容替換為我們manifestPlaceholders中對應(yīng)的value值。我們看AndroidManifest文件中具體的使用:

看到以上示例中的meta-data標簽了嗎?其中${UMENG_CHANNEL}就是一個占位符,它的變量名是UMENG_CHANNEL。構(gòu)建的時候${UMENG_CHANNEL}將會被替換為google或者baidu。
通過以上方式我們就可以動態(tài)的配置我們的渠道,非常方便,但是這里也有一個問題,就是我們渠道非常多的時候呢?在中國,你們懂的,一個App很隨意的就有幾十個渠道需要發(fā)布,我們總不能一個個的配置吧,太多也太累,維護也麻煩。假如我們的友盟的渠道名和我們在Android Gradle中配置的ProductFlavor一樣的話就簡單了,我們可以通過迭代productFlavors批量的方式進行修改。

我們通過all函數(shù)遍歷每一個ProductFlavor,然后把他們的name作為我們友盟中渠道的名字,非常方便,這里不止可以做這一個事情,在遍歷ProductFlavor的時候,你可以做很多你想做的事情,這就是Gradle的靈活之處,把腳本當成程序?qū)憽?br> Android Gradle為我們提供的manifestPlaceholders占位符的方式,讓我們可以替換AndroidManifest文件中任何${Var}格式的占位符,所以它的使用場景不限于渠道名這一個,比如還有ContentProvider的auth的授權(quán),或者其他動態(tài)想配置meta信息等等,靈活的運用它能幫助我們做很多事情,讓我們的構(gòu)建更靈活,更方便。
9.6 自定義你的BuildConfig
對于BuildConfig這個類,相信大家都不會陌生,我們找到它,在它的頂部會看到“Automatically generated file. DO NOT MODIFY”,說它都是自動生成的不能修改,那么它是如何自動生成的呢?其實并不神秘,它是由我們的Android Gradle構(gòu)建腳本在編譯后生成的,默認情況下是一般是這樣的。
DEBUG用于標記是debug模式還是release模式,剩下的還有包名,當前構(gòu)建的類型是debug還是release,當前構(gòu)建的渠道,當前的版本號以及版本名字。你會發(fā)現(xiàn)這些差不多就是我們當前構(gòu)建渠道的基本應(yīng)用信息,他們都是常量,相比我們獲取這些信息的其他方式,無疑他們是非常方便的。
比如你要獲取當前的包名,一般我們都會使用context.getPackgeName()函數(shù),這個函數(shù)又會有很多實現(xiàn),很麻煩,很復雜,性能也不高,但是我們?nèi)绻苯右肂uildConfig.APPLICATION_ID就方便多了,性能也非??欤酥膺€有渠道、版本號、構(gòu)建類型等信息。
DEBUG這個常量需要著重介紹一下,一般在開發(fā)過程中我們都會輸出日志進行調(diào)試,一般只有在我們自己開發(fā)中才會打印出日志,當我們發(fā)布后就不能打印日志了,也就是我們需要一個標記是debug模式還是release模式的開關(guān),這就是BuildConfig.DEBUG,在debug模式下它的值是true,在release模式下它的值會自動變?yōu)閒alse,不用我們每次去改動這個值,Android Gradle會幫我們自動生成修改,非常方便,你還不用擔心忘記。
既然這個BuildConfig這么好用,我們自己是不是可以自己定義,新增一些常量,讓后動態(tài)的配置他們的值呢,答案是肯定的,對此Android Gradle為我們提供了buildConfigField(String type,String name,String value)讓我們可以添加自己的常量到BuildConfig中,它的函數(shù)原型是
//gradle-core-2.3.0.jar
public class BuildType extends DefaultBuildType implements CoreBuildType, Serializable {
...
public void buildConfigField(String type, String name, String value) {
ClassField alreadyPresent = (ClassField)this.getBuildConfigFields().get(name);
if(alreadyPresent != null) {
this.logger.info("BuildType({}): buildConfigField \'{}\' value is being replaced: {} -> {}", new Object[]{this.getName(), name, alreadyPresent.getValue(), value});
}
this.addBuildConfigField(new ClassFieldImpl(type, name, value));
}
...
第一個參數(shù)type是要生成字段的類型,第二個參數(shù)name是要生成字段的常量名字,第三個參數(shù)value是要生成字段的常量值。最終他們生成的字段格式如下:
<type> <name> = <value>
現(xiàn)在我們具體例子來演示他們的用法。假設(shè)我們有baidu和google兩個渠道,發(fā)布的時候也會有這兩個渠道包,當我們安裝baidu渠道包的時候打開的是baidu的首頁,當我們安裝google渠道包的時候打開的是google的首頁。從這個思路分析,我們只需要添加一個字段WEB_URL,在baidu渠道下它的值是 http://www.baidu.com ,在google渠道下它的值是 http://www.google.com 即可。

看上面的示例代碼,我們定義兩baidu和google兩個渠道,并分別為他們生成了相應(yīng)的BuildConfig常量字段,看我們的BuildConfig類,已經(jīng)生成了這個常量了。
然后我們在代碼中使用這個WEB_URL常量即可,在打包的時候,Android Gradle會幫我們自動生成不同的值。這里需要注意的是,value這個參數(shù),是''這個單引號中間的部分,尤其對于String類型的值,里面的雙引號一定不能省略,不然就會生成如下這樣,報編譯錯誤

value的值是什么就寫什么,要原封不動的放在''這個單引號里。
buildConfigField "boolean", "LOG_DEBUG", "true"
buildConfigField "String", "URL", ' "http://www.ecjtu.jx.cn/" '
以上我們講的都是渠道(ProductFlavor),其實不光渠道可以配置自定義字段,構(gòu)建類型(BuildType)也可以配置,比如針對debug、release甚至其他構(gòu)建類型來自定義配置,構(gòu)建類型的一旦配置,那么所有渠道的這個構(gòu)建類型都會有這個常量字段可以使用,它的使用方法和渠道的一樣,只不過是配置在BuildType里,這里就不舉例子了,類似于

自定義BuildConfig非常靈活,你可以根據(jù)不同的渠道,不同的構(gòu)建類型來靈活配置你的App。
9.7 動態(tài)添加自定義的資源
在我們開發(fā)Android的過程中,我們會用到很多資源,有圖片,動畫、字符串等等,這些資源我們可以在我們的res文件夾里定義,然后在工程里引用即可使用。這里我們講的自定義資源,是專門針對res/values類型資源的,他們不光可以在res/values文件夾里使用xml的方式定義生命,還可以在我們的Android Gradle定義,這大大增加了我們構(gòu)建的靈活性。
實現(xiàn)這一功能的正是resValue方法,他在BuildType和ProductFlavor這兩個對象中都存在,也就是說我們可以分別針對不同的渠道,或者不同的構(gòu)建類型來自定義其特有的資源。以ProductFlavor中的resValue方法為例,我們先看下它的源碼實現(xiàn):
public void resValue(String type, String name, String value) {
ClassField alreadyPresent = (ClassField)this.getResValues().get(name);
if(alreadyPresent != null) {
this.logger.info("BuildType({}): resValue \'{}\' value is being replaced: {} -> {}", new Object[]{this.getName(), name, alreadyPresent.getValue(), value});
}
this.addResValue(new ClassFieldImpl(type, name, value));
}
從其文檔注釋中我們可以看到,它會添加生成一個資源,其效果是和在res/values文件里中定義一個資源是等價的。
resValue方法有三個參數(shù),第一個是type,也就是你要定義資源的類型,比如有string、id、bool等等;第二個是name,也就是你要定義資源的名稱,以便我們在工程中引用它;第三個是value,就是要你要定義資源的值。

當我們使用resValue方法時,Android Gradle幫我們生成的資源在哪里呢?其實都在我們的工程中,以baidu為例,debug模式下,在build/generated/res/resValues/baidu/debug/values/generated.xml這個文件中,我們看下我們生成的這個文件。
有沒有發(fā)現(xiàn),和我們在res/values這個文件夾里定義的xml文件的格式是一樣的,只不過我們通過Gradle配置,Android Gradle幫我們自動做到了,這樣我們控制Android Gradle構(gòu)建的時候更靈活,如果沒有這項功能,在res/values里配置就不太方便了。
以上示例我們演示的是string這個類型,你也可以使用id,bool,dimen,integer,color等這些類型來自定義自己的values資源,總之這個resValue方法和我們上一小節(jié)中講的buildConfigField方法非常相似,參考即可,記得它也可以在BuildType中使用。
9.8 Java編譯選項
有時候我們需要對我們的Java源文件的編碼,源文件使用的JDK版本等等進行調(diào)優(yōu)修改,比如我們需要配置源文件的編碼為UTF-8的編碼,以兼容更多的字符;還比如我們想配置編譯Java源代碼的級別為1.6,這樣我們就可以使用Override接口方法的繼承等特性,為此Android Gradle我們提供了一個非常便捷的入口來讓我們做這些配置。

android對象提供了一個compileOptions方法,它接受一個CompileOptions類型的閉包作為參數(shù),來對Java編譯選項進行配置.
CompileOptions是編譯配置,它提供了三個屬性,分別是encoding、sourceCompatibility、targetCompatibility,通過對他們進行設(shè)置來配置Java相關(guān)的編譯選項。
sourceCompatibility是配置Java源代碼的編譯級別.
從文檔注釋中我們可以看到,它會盡可能的,把所有支持的值轉(zhuǎn)換成一個JavaVersion對象,下面我們直接列出其可用的值
"1.6"1.6JavaVersion.Version_1_6"Version_1_6"
以上列出的這些格式都可以使用,你可以根據(jù)自己的喜好選擇。
targetCompatibility是配置生成的Java字節(jié)碼的版本,其可選值和sourceCompatibility一樣,這里我們就不進行演示和講解了。
9.9 adb操作選項配置
adb,相信大家都非常熟悉了,它是一個Android Debug Bridge,用于連接我們的Android手機進行一些操作,比如調(diào)試Apk,安裝Apk,拷貝文件到手機等等。在Shell中我們可以通過輸入adb來查看其功能和使用說明,在Android Gradle中,也為我們預留了對adb的一些選項的控制配置,它就是adbOptions{}閉包,它和compileOptions一樣也是Android的一個方法。
public void adbOptions(Action<AdbOptions> action) {
this.checkWritability();
action.execute(this.adbOptions);
}
由原型方法可以看到,這是一個AdbOptions類型的閉包,我們所有可以使用的Adb配置選項都在AdbOptions定義好了,所以有什么可以使用的,只需要看下這個AdbOptions類的實現(xiàn)即可。
在講使用之前我們先講下其大概的原理,我們知道adb這個命令,他可以幫助我們連接Android手機,對于Android Gradle這個插件,它也不例外,比如我們運行調(diào)試的時候,Android Gradle插件的底層還是調(diào)用的adb命令,Android Gradle只不過在其之上做了一些包裝,有興趣的可以看到Android Gradle源代碼。既然做了包裝,那么我們的AdbOptions配置就有作用了,在Android Gradle的腳本中,可以通過adbOptions{}閉包對adb的選項進行配置,然后實例化收集到android對象中的一個AdbOptions類型的變量adbOptions中,最后Android Gradle調(diào)用adb命令的時候,把這些配置作為adb命令的參數(shù)傳遞給adb即可,這就是AdbOptions的大概原理,基本上所有的Gradle和Shell命令的配合都是這么做的。
講完了大概的原理,那么我們看下AdbOptions有哪些可供我們配置的。我們來看一下這個類的源碼。
package com.android.build.gradle.internal.dsl;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
public class AdbOptions implements com.android.builder.model.AdbOptions {
int timeOutInMs;
List<String> installOptions;
public AdbOptions() {
}
public int getTimeOutInMs() {
return this.timeOutInMs;
}
public void setTimeOutInMs(int timeOutInMs) {
this.timeOutInMs = timeOutInMs;
}
public void timeOutInMs(int timeOutInMs) {
this.setTimeOutInMs(timeOutInMs);
}
public Collection<String> getInstallOptions() {
return this.installOptions;
}
public void setInstallOptions(String option) {
this.installOptions = ImmutableList.of(option);
}
public void setInstallOptions(String... options) {
this.installOptions = ImmutableList.copyOf(options);
}
public void installOptions(String option) {
this.installOptions = ImmutableList.of(option);
}
public void installOptions(String... options) {
this.installOptions = ImmutableList.copyOf(options);
}
}
我比較喜歡看源碼,這樣能了解的更清楚,以這個AdbOptions為例,如果你看官方的Android Gradle DSL文檔,只能看到介紹的AdbOptions的兩個屬性:installOptions和timeOutInMs,然后你就會很當然的以屬性的方式對他們進行設(shè)值,但是從源代碼中我們可以看到,不僅可以通過屬性的方式進行設(shè)值,還可以方法的方式,因為這里是有三個和其屬性名一樣的方法:

下面我們演示下它的使用以及這兩個配置項的含義

示例中我采用兩種寫法進行了演示,第一種對timeOutInMs的設(shè)置采用屬性的方式,第二種對installOptions的設(shè)置采用的方法的方式,讓大家對這兩種設(shè)置方式都有了解,這樣你就可以根據(jù)自己的喜好進行選了,我本人喜歡方法的方式,簡潔,可讀性強,更有腳本感。
timeOutInMs,從其名字就可以看出來,它是設(shè)置超時時間的,單位是毫秒,這個超時時間是執(zhí)行adb這個命令的超時時間。有時候我們安裝、運行或者調(diào)試的時候,可能會遇到CommandRejectException這樣的異常,這個一般是當我們執(zhí)行一個命令的時候,在規(guī)定的時間內(nèi)沒有返回應(yīng)有的結(jié)果,這時候我們可以通過把超時時間設(shè)置長一些來解決,也就是多等一會,多等一會可能就有相應(yīng)結(jié)果了。如果你經(jīng)常遇到這類異常,可以把adb的超時時間設(shè)置長一些,就是通過timeOutInMs來設(shè)置,記住它的單位是毫秒。
installOptions,從其名字也能看出來,所以我們自己在編碼中,養(yǎng)成好的習慣,命名通俗易懂,合理規(guī)范。它是用來設(shè)置我們adb install安裝這個操作的設(shè)置項的,比如我們是要安裝到sd上,還是要替換安裝等等。我們從adb命令中看下它的功能說明。

adb install以供有l(wèi)rtsdg六個選項。
- -l:鎖定該應(yīng)用程序
- -r:替換已存在的應(yīng)用程序,也就是我們說的強制安裝
- -t:允許測試包
- -s:把應(yīng)用程序安裝到SD卡上
- -d:允許進行降級安裝,也就是安裝的比手機上帶的版本低
- -g:為該應(yīng)用授予所有運行時的權(quán)限
以上就是安裝的六個選項的含義,我們可以根據(jù)自己的需求進行設(shè)置。
adb選項中超時設(shè)置用的比較多,安裝設(shè)置只有在特殊情況下使用,默認的現(xiàn)在基本上夠用。
9.10 dex選項配置
我們都知道,我們的Android中的Java源代碼,被編譯成class字節(jié)碼后,在我們打包成APK的時候又被dx命令優(yōu)化成Android虛擬機可執(zhí)行的DEX文件,DEX文件比較緊湊,Android費勁心思做了這個DEX格式,就是為了能使我們的程序在Android平臺上運行快一些。對于這些生成DEX文件的過程和處理,Android Gradle插件都幫我們處理好了,Android Gradle插件會調(diào)用我們SDK中的dx命令進行處理,但是有的時候我們可能會遇到提示內(nèi)存不足的錯誤,大致提示異常是java.lang.OutOfMemoryError: GC overhead limit exceeded,為什么會提示內(nèi)存不足呢?其實這個dx命令知識一個腳本,它調(diào)用的還是Java編寫的dx.jar庫,是Java程序處理的,所以當內(nèi)存不足的時候,我們會看到這么明顯的Java異常信息,默認情況下給dx分配的內(nèi)存是一個G,也就是1024M
以上就是dx命令的Shell腳本,熟悉的朋友應(yīng)該不會陌生,很容易看的懂,我們注意到,默認內(nèi)存是1024M,但是我們也可以通過-J參數(shù)配置。

現(xiàn)在我們了解了原理了,也知道通過-J參數(shù)重新配置更大的內(nèi)存就可以解決這個問題,但是我們在Android Gradle插件中怎么配置這個內(nèi)存呢?和Adb的選項設(shè)置一樣,Android Gradle插件為我們提供了dexOptions { }閉包,讓我們可以對dx操作進行一些配置,也就是說為我們留了一個配置dx操作的入口,這是一個非常不錯的方法,包括上幾節(jié)我們講的其他選項配置,這也可為我們自己的Gradle插件時,為插件使用者提供可配置項提供一個很好的思路。
dexOptions{}是一個DexOptions類型的閉包,它的配置都是由DexOptions提供的,現(xiàn)在我們看下DexOptions都有哪些可配置項。

- incremental屬性,這是一個boolean類型的屬性,他用來配置是否啟用dx的增量模式,默認值為false,表示不啟用。增量模式雖然速度更快一些,但是目前還有很多限制,也可能會不工作,所以要慎用,要啟用設(shè)置incremental為true即可。
- javaMaxHeapSize屬性,剛剛我們前面已經(jīng)提了,他是配置我們執(zhí)行dx命令是為其分配的最大堆內(nèi)存,主要用來解決dx時內(nèi)存不夠用情況。它接受一個字符串格式的參數(shù),比如1024M,代表是1個G,當然你也可以直接配置為1g,也是支持的,和1024M效果一樣。
這里我配置4g,如果不夠用你還可以再添加,前提是你的電腦有那么多內(nèi)存夠使用O(∩_∩)O~ - jumboMode屬性,boolean類型,它可以用來配置是否開啟jumbo模式,有時候我們的工程比較多,代碼量太大,函數(shù)超過了65535個,那么就需要強制開啟jumbo模式才可以構(gòu)建成功,下一節(jié)我們再詳細講如何在Android5.0以下系統(tǒng)上突破65535方法的限制。
- preDexLibraries屬性,boolean類型,用來配置是否預dex Libraries庫工程,開啟后會大大提高增量構(gòu)建的速度,不過這可能會影響clean構(gòu)建的速度。默認值為true,是開啟的。有時候我們需要關(guān)閉這個選項,比如我們需要使用dx的--multi-dex選項生成多個dex導致和庫工程有沖突的時候,需要將該選項設(shè)置為false。
- threadCount屬性,Integer類型,用來配置我們Android Gradle運行dx命令時使用的線程數(shù)量,適當?shù)臄?shù)量可以提供dx的效率。
以上就是關(guān)于Dex選項設(shè)置的5個可以配置選項,我們可以根據(jù)我們具體項目中的需求來配置這些選項,達到項目構(gòu)建的目的。
9.11 突破65535方法限制
隨著業(yè)務(wù)越來越復雜,代碼量會越來越多,尤其是大量集成第三方Jar庫,你很快就要遇到如下錯誤:

有些Android的操作系統(tǒng)會遇到如下錯誤:

他們雖然提示的錯誤信息不一樣,但是都是同一個問題,這個錯誤是告訴我們整個App應(yīng)用的方法超過了限制,為什么會這樣呢,這要從Android中的虛擬機Dalvik說起。我們上一節(jié)也提到,我們的Java源文件都被打包成了一個DEX文件,這個文件就是優(yōu)化過的Dalvik虛擬機可執(zhí)行文件,Dalvik虛擬機在執(zhí)行DEX文件的時候,它使用了short這個類型來索引DEX文件中的方法,這就意味著單個DEX文件可以被定義的方法最多只能是65535個,當我們定義的方法超過這個數(shù)時,就會出現(xiàn)如上的錯誤提示信息。
那么我們?nèi)绾蝸斫鉀Q這個問題呢?我們注意到單個DEX文件的方法超過65535個,那么我們解決的辦法就是生成多個DEX文件,這樣每個DEX文件的方法數(shù)量都沒有超過65535,這樣我們就可以解決這個問題了。
Facebook發(fā)展的很快,他們的Android App中的方法很快就達到了這個限制,他們的解決辦法是采用打補丁的方式,有興趣的可以參考下 Facebook Dalvik補丁。Android開發(fā)者博客也有一篇通過自定義類的加載過程的文章來解決該問題,有興趣的也可以參考一下,雖然他們有點復雜,但是在當時來說是不錯的解決辦法,并且可以了解一些對類加載,Dalvik虛擬機等技術(shù)。
隨著出現(xiàn)該問題的App越來越多,Android官方終于給出了官方解決該問題的方法,這個就是Multidex。對于Android5.0之后的版本,使用了ART的運行時方式,可以天然支持App有多個dex文件,ART在安裝App的時候執(zhí)行預編譯,把多個dex文件合并成一個oat文件執(zhí)行;對于Android5.0之前的版本,Dalvik虛擬機限制每個App只能有一個class.dex,要使用他們,就得使用Android為我們提供的Multidex庫,下面我們就重點講針對Android5.0之前的版本的處理。
首先你得升級你的Android Build Tools和Android Support Repository 到21.1,這是支持這個Multidex功能的最低支持版本,目前我們升級到最新即可。
要在我們的項目中使用Multidex,首先我們要修改我們的gradle build配置文件,啟用Multidex,并同時配置Multidex需要的Jar依賴。

配置好之后,只完成了一半,開啟了multidex,會讓我們的方法多余65535個的時候生成多個dex文件,其名字為classes.dex,classes(...n).dex這樣的樣式,但是對于Android5.0之前的系統(tǒng)虛擬機,它只認識一個dex,其名字還得是classes.dex,所以要想達到程序可以正常的目的,也要讓虛擬機把其他幾個生成的classes加載進來,要想做到這一步,必須在App程序啟動的入口控制,這個入口就是Application。
Multidex為我們提供了現(xiàn)成的Application,其名字是MultiDexApplication,如果我們沒有自定義的Application的話,直接使用MultiDexApplication即可,在Mainftest清單里配置。

如果你的有自定義的Application,并且是直接繼承自Application,那么只需要把繼承改為我們的MultiDexApplication即可。
如果你的自定義的Application是繼承其他第三方提供的Application,就不能改變繼承了,這時候我們通過重寫attachBaseContext方法實現(xiàn)。

到了這里,我們對65535的限制都解決完了,這時我們打包的時候,Android Gradle會自動判斷你的方法有沒有超過65535個,如果沒有,還是生成一個classes.dex文件,如果超過了,那么就會生成1個classes.dex文件,這個是入口主文件,然后還會生成若干個附屬dex文件,比如classes2.dex, classes3.dex,打包系統(tǒng)會把他們一起打包到Apk里發(fā)布。
雖然我們有了解決65535方法的辦法,但是還是應(yīng)該盡量的避免我們工程的方法超過65535個,要達到這個目的,首先我們不能濫用第三方庫,因為你自己的代碼一般不會有這么多,如果要引用,最好也要自己進行精簡。精簡之后,還要使用ProGuard減小DEX的大小,因為DEX安裝到機器上的過程比較復雜,尤其是有第二個DEX文件并且過大的時候,可能會造成ANR異常。還有因為Dalvik linearAlloc的限制,尤其在Android2.2和2.3上,只有5M,到Android4.0的時候還好點,升級到8M了,所以在低于4.0的系統(tǒng)上dexopt的時候可能會崩潰。
到了這里我們這一節(jié)要結(jié)束了,有興趣的可以看下MultiDex的實現(xiàn)原理,尤其是加載classes2.dex,classes3.dex等等這幾段,可以幫助我們理解動態(tài)的加載DEX文件原理。最后提出一些其他方法比較好,但是較為復雜的65535方法限制的解決辦法--插件化。
插件化可以參考幾個不錯的開源工程:
- https://github.com/singwhatiwanna/dynamic-load-apk
- https://github.com/DroidPluginTeam/DroidPlugin
- https://github.com/alibaba/AndFix
- https://github.com/wequick/Small
9.12 自動清理未使用的資源
隨著工程越來越大,功能越來越多,開發(fā)人員越來越多,代碼越來越復雜,不可避免的會產(chǎn)生一些不在使用的資源,這類資源如果沒有清理的話,會增加我們Apk的包大小,也會增加構(gòu)建的時候。
要清理這些無用的資源,第一個辦法是我們在開發(fā)的過程中,把不再使用的資源清理掉,這個靠開發(fā)人員的自覺以及對程序代碼邏輯的了解程度,而且清理成本也比較大。第二個辦法是使用Android Lint,它會幫我們檢測出哪些資源沒有被使用,然后我們按照檢測出來的列表清理即可,這種辦法需要我們隔一段時間就要清理一次,不然就可能會有無用的資源遺留,做不到及時性。以上兩個方式還有一個不能解決的問題,他就是第三方庫里的資源的問題。如果你引用的第三方庫里也含有無用的資源,那么這兩種辦法都不能做到清理他們,因為他們被打包在第三方庫里,沒有辦法做刪除。
針對以上情況,Android Gradle為我們提供了在構(gòu)建打包時自動清理掉未使用資源的方法,這個就是Resource Shrinking。他是一種在構(gòu)建時,打包成Apk之前,會檢測所有資源,看看是否被引用,如果沒有,那么這些資源就不會被打包到Apk包中,因為是在這個過程中(構(gòu)建時),Android Gradle構(gòu)建系統(tǒng)會拿到所有的資源,不管是你項目自己的,還是引用的第三方的,它都一視同仁的處理,所以這個時機點可以控制哪些資源可以被打包,所以能解決第三方不使用的資源的問題。比如我們常用的Google Play Service,這個是一個比較大的庫,它支持很多Google的服務(wù),比如Google Drive,Google Sign In等等,如果你在你的應(yīng)用中只使用了Google Drive這個服務(wù),并沒有使用到Google Sign In服務(wù),那么在構(gòu)建打包的時候,會自動的處理Google Sign In功能相關(guān)的無用資源圖片。
Resource Shrinking要結(jié)合著Code Shrinking一起使用,什么是Code Shrinking呢?就是我們經(jīng)常使用的ProGuard,也就是我們要啟用minifyEnabled,是為了縮減代碼的;我們上面已經(jīng)講了,自動清理未使用的資源的原理很簡單,就是判斷有沒有用到這些資源,如果你的代碼還在使用,那么自然不會被清理,所以要和代碼清理結(jié)合使用,先清理掉無用的代碼,這樣這些無用的代碼引用的資源才能被清理掉。那么我們?nèi)绾闻渲檬褂媚?,看下面的示例,如下Gradle配置來啟用Resource Shrinking:

當我們開啟了shrinkResources后,打包構(gòu)建的時候,Android Gradle就會自動的處理未使用的資源,不把他們打包到生成的Apk中,我們可以在我們構(gòu)建輸出的日志中看到處理結(jié)果,以我們當前的示例代碼為例,我們運行./gradlew :example912:assembleRelease 就可以看到如下日志:

自動清理未使用的資源這個功能雖好,但是有時候會誤刪,為什么呢,因為我們在代碼編寫的時候可能會使用反射去引用資源文件,尤其很多你引用的第三方庫會這么做,這時候Android Gradle就區(qū)分不出來了,可能會誤認為這些資源沒有被使用。針對這中情況,Android Gradle為我們提供了keep方法來讓我們配置哪些資源不被清理。
keep方法使用非常簡單,我們要新建一個xml文件來配置,這個文件是 res/raw/keep.xml,然后通過tools:keep屬性來配置,這個tools:keep接受一個以逗號(,)分割的配置資源列表,并且支持星號(*)通配符,有沒有覺得它和我們用ProGuard的配置文件是一樣的,我們在ProGuard配置文件里配置保存一些不被混淆的類也是這么做的。此外,對于res/raw/keep.xml這個文件我們不用擔心,Android Gradle構(gòu)建系統(tǒng)最終打包的時候會清理它,不會把它打包進Apk中的,除非你在代碼中通過R.raw.keep引用了它。
以下是res/raw/keep.xml示例,引用自Android Tech Docs

keep.xml還有一個屬性是 tools:shrinkMode,用于配置自動清理資源的模式,默認是safe,是安全的,這種情況下,Android Gradle可以識別代碼中類似于如下示例的引用

這類代碼也被構(gòu)建系統(tǒng)認為是使用了資源文件,不會被清理。如果把清理模式改為strict,那么就沒有辦法識別了,這個資源會被認為沒有被引用,也會被清理掉。
除了shrinkResources之外,Android Gradle還為我們 提供了一個resConfigs,它屬于ProductFlavor的一個方法,可以讓我們配置哪些類型的資源才被打包到Apk中,比如只有中文的,只有hdpi格式的圖片等等,這是非常重要的,比如我們引用的第三方庫,特別是Support Library 和 Google Play Services這兩個主要的大庫,因為國際化的問題,他們都支持了幾十種語言,但是對于我們的App來說,我們并不需要這么多,比如我們只用中文的語言就可以了,其他的都不需要;比如我們支持hdpi格式的圖片就好了,其他的都不需要,這時候我們就可以通過resConfigs方法來配置:

這樣我們就只保留了zh資源,其他非zh資源都不會被打包到Apk文件中。
其實這個resConfig的配置有3中辦法,一般常用的是resConfigs這個方法,因為可以同時指定多個配置,你也可以使用resConfig(后面沒有s)來指定一個配置,它一次只能添加一個,如果要添加多個,要么調(diào)用多次,要么使用resConfigs方法。我們看下他們的方法原型,了解他們的方法原理:
resConfig的使用非常廣泛,它的參數(shù)就是我們在Android開發(fā)時的資源限定符,不止于我們上面描述的語言和密度,還包括Api Level,分辨率等等,具體的可以參考Android Doc文檔。
以上自動清理資源只是在打包的時候,不打包到Apk中,實際上并沒有刪除我們工程中的資源,如果我們在使用的時候發(fā)現(xiàn)有大量的無用資源被清理,那么我們自己最好還是把這些資源文件從我們的工程中刪除吧,這樣也好維護一些。
到這里這一章就結(jié)束了,這一章主要是介紹Android Gradle的一些高級用戶,基本上都是現(xiàn)實項目中遇到的,整理出來讓大家參考,可以根據(jù)自己的實際情況選擇使用,也可以在這些的基礎(chǔ)上發(fā)散自己的思維,摸索出其他的更適用于你的項目的用法。
本文屬自學歷程, 僅供參考
詳情請支持原書 Android Gradle權(quán)威指南