*本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈java
很久沒有寫博客了,忽然想把這段時間項目中使用到的技術和多渠道相關的認識總結分享一下~android
使用AndroidStudio配合gradle,能夠很方便的輸出多個渠道包,只須要在app Module下的build.gradle中,對productFlavors領域進行配置便可,假設我當前開發的項目,須要上線不一樣的地區,一個是國內版,一個美國版,還有一個免費版,那麼gradle能夠這麼配:api
android {
productFlavors {
china { // 中國版
}
america { // 美國版
}
free { // 免費版
}
}
}
複製代碼
以上多渠道配置完成後,在Android Studio的Build Variants標籤中,就會有不一樣渠道變體供咱們選擇了。當咱們想使用AS直接運行某個渠道的app時,就須要先在Build Variants標籤中選擇好變體,再點擊"運行"按鈕運行項目。微信
在productFlavors中還能夠配置包名(applicationId)、版本號(versionCode)、版本名(versionName)、icon、應用名 等等,舉個例子:架構
free {
applicationId 'com.lqr.demo.free'
versionCode 32
versionName '1.3.2'
manifestPlaceholders = [
app_icon: "@drawable/ic_launcher",
app_name: "菜雞【免費版】",
]
}
複製代碼
注意:
這裏配置的包名是applicationId,而不是清單文件裏的packageName,applicationId與packageName是不同的。
咱們常說,一部Android設備上不能同時安裝2個相同包名的app,指的是applicationId不能同樣。
applicationId與packageName的區別可查閱:《ApplicationId versus PackageName》app
若是工程要求不一樣渠道共存,或者對版本號、icon、應用名等有定製需求的話,那麼這個多渠道配置就顯得很是有用了。其中,app_icon、app_name是放在manifestPlaceholders的,這個實際上是在對AndroidManifest.xml中的佔位符進行變量修改,也就是說,要定製icon或者應用名的話,還須要對清單文件作些小修改才行(增長一些佔位符),如:ide
<application xmlns:tools="http://schemas.android.com/tools" android:icon="${app_icon}" android:label="${app_name}" android:theme="@style/AppTheme" android:largeHeap="true" tools:replace="android:label">
...
</application>
複製代碼
在新增渠道以後,咱們能夠對這些渠道進行一塊兒更多的配置,假設項目代碼須要根據不一樣的渠道,賦予不一樣的數據,固然你能夠選擇在java代碼中經過判斷當前渠道名,配合switch來設置靜態常量,但其實不用那麼煩瑣,並且有些靜態數據經過相似config.gradle或config.properties這類配置文件來配置有比較好,那麼gradle中的applicationVariants徹底能夠幫助到咱們,如下面的配置Demo爲例進行說明:佈局
// 多渠道相關設置
applicationVariants.all { variant ->
buildConfigField("String", "PROUDCT", "\"newapp\"")
buildConfigField("String[]", "DNSS", "{\"http://119.29.29.29\",\"http://8.8.8.8\",\"http://114.114.114.114\"}")
if (variant.flavorName == 'china') {
buildConfigField("String", "DNS", "\"http://119.29.29.29\"")
} else if (variant.flavorName == 'america') {
buildConfigField("String", "DNS", "\"http://8.8.8.8\"")
} else if (variant.flavorName == 'free') {
buildConfigField("String", "DNS", "\"http://114.114.114.114\"")
}
}
複製代碼
經過gradle中提供的buildConfigField(),AndroidStudio會在執行腳本初始化時,根據當前所選變體將對於的配置轉變爲BuildConfig.java中的一個個靜態常量:post
當我切換其餘變體時,BuildConfig中的DNS也會跟着一塊兒改變,這樣,咱們在工程代碼中,就不須要去判斷當前渠道名來爲某些靜態常量賦值了。這裏只是舉例了使用buildConfigField()來生成String和String[]常量,固然也能夠用來生成其它類型的常量數據,有興趣的話,能夠百度瞭解下。學習
上面提到了變體,那麼變體是什麼?能夠這樣理解,變體是由【Build Type】和【Product Flavor】組合而成的,組合狀況有【Build Type】*【Product Flavor】種,舉個例子,有以下2種構建類型,並配置了2種渠道:
Build Type:release debug
Product Flavor:china free
複製代碼
那麼最終會有四種 Build Variant 組成:
chinaRelease chinaDebug freeRelease freeDebug
複製代碼
變體在複雜多渠道工程中是至關有用的,能夠作到資源文件合併以及代碼整合,這裏的合併與整合怎麼理解?咱們使用Android Studio進行項目開發時,會把代碼文件與資源文件都存放在app/src目錄下,一般是main下會有java、res、assets來區分存放代碼文件和資源文件,你能夠把main看做是默認渠道工程文件目錄,也就是說main下存放在代碼文件和資源文件對全部渠道來講都是共同持有的。
那麼,一旦出來了某些代碼文件或者資源文件是個別渠道專屬時,應該怎麼辦呢?由於main是共有的,因此理想狀態下,咱們並不會把這類"不通用"的文件放在main下(這樣作不會出錯,可是作法很low,會增大apk包體積),Android Studio爲變體作了很好的支持,咱們能夠在app/src下,建立一個以渠道名命名的目錄,用於存放這類個別渠道專屬的代碼文件和資源文件,如:
能夠看到,當我選擇freeDebug變體時,app/src/free下的目錄高亮了,說明它們被Android Studio識別,在運行工程時,Android Studio會將free和main下的全部資源文件進行合併,將代碼文件進行整合。同理,若是我選擇的是chinaDebug變體,那麼app/src/china下目錄就會高亮。知道如何建立變體目錄後,下面就開始進行資源合併與代碼整合了。
資源文件有哪些?咱們能夠這樣認爲:
資源文件 = res下的全部文件 + AndroidManifest.xml
複製代碼
變體的資源合併功能簡直是"神器"通常的存在,能夠解決不少業務需求,如不一樣渠道顯示的icon不一樣,應用名不一樣等等。Android Studio在對變體目錄和main目錄進行資源合併時,會遵照這樣的規則,假設當前選中的變體是freeDebug:
針對上述2個規則,這裏以string.xml爲例進行說明,main下的string.xml是:
<resources>
<string name="app_name">Demo</string>
<string name="app_author">Lin</string>
</resources>
複製代碼
free下的string.xml是:
<resources>
<string name="error_append">發生錯誤</string>
<string name="app_author">Lqr</string>
</resources>
複製代碼
那麼最終打出的apk包裏的string.xml是:
<resources>
<string name="app_name">Demo</string>
<string name="error_append">發生錯誤</string>
<string name="app_author">Lqr</string>
</resources>
複製代碼
除了字符串合併外,還有圖片(drawable、mipmap)、佈局(layout)、清單文件(AndroidManifest.xml)的合併,具體能夠本身嘗試一下。其中,清單文件的合併須要提醒一點,若是渠道目錄下的AndroidManifest.xml與main下的AndroidManifest.xml擁有相同的節點屬性,但屬性值不一樣時,那麼就須要對main下的AndroidManifest.xml進行修改了,具體修改要根據編譯時報錯來處理,因此,報錯時不要慌,根據錯誤提示修改就是了。
注意:佈局(layout)文件的合併是對整個文件進行替換的~。
代碼文件,顧名思義就是指java目錄下的.java文件了,爲何代碼叫整合,而資源倒是合併呢?由於代碼文件是沒辦法合併的,只能是整合,整合是什麼意思?假設當前選中的變體是freeDebug,有一個java文件是Test.java,這個Test.java要麼只存在free/java下,要麼只存在於main/java下,如:
能夠看到,一切正常,Test.java被AndroidStudio識別,但若是此時在main/java下也存在Test.java,那麼Android Studio就會報錯了:
代碼整合是一個比較頭痛的事,由於若是你是在渠道目錄free下去引用main下的類,那麼是徹底沒有問題的,但若是反過來,在main下去引用free下的專屬類時,狀況就會變得很糟糕,當你切換其餘變體時(如,切換成chinaDebug),這時工程就會報錯了,由於變體切換,Test.java是free專屬的,在chinaDebug變體下,free不會被識別,因而main就找不到對應的類了。
選擇freeDebug變體時,正常引用Test.java:
選擇chinaDebug變體時,找不到Test.java(只找到junit下的Test.java):
因此,對於代碼整合,須要咱們在開發過程當中慎重考慮,多想一想如何將渠道目錄與main目錄進行解耦。好比可使用Arouter來解耦main與渠道目錄下全部的Activity、Fragment,將類引用轉換爲字符串引用,所有將由Arouter來管理,又或者經過反射來處理,等等,這裏順帶記錄一下,我項目中使用ARouter來判斷Activity、Fragment是否存在,和獲取的相關方法:
/** * 獲取到目標Delegate(僅僅支持Fragment) */
public <T extends CommonDelegate> T getTargetDelegate(String path) {
return (T) ARouter.getInstance().build(path).navigation();
}
/** * 獲取到目標類class(支持Activity、Fragment) */
public Class<?> getTargetClass(String path) {
Postcard postcard = ARouter.getInstance().build(path);
LogisticsCenter.completion(postcard);
return postcard.getDestination();
}
複製代碼
前面只說到了res和java這2個目錄,那麼assets呢,它是屬於哪一種?很惋惜,assets雖然是資源,但它不是合併,而是整合,也就是說,assets文件的處理方式跟java文件的處理方式是同樣的,不能在渠道目錄和main目錄下同時存在相同的assets文件,這將對某些需求實現形成阻礙,舉個例子,假設china與free使用的assets資源是同樣的,而america單獨使用本身的assets資源,而且這些assets資源文件名都是同樣的,那這時要怎麼辦呢?給每一個渠道都放一份各自的assets資源嗎?這種作法可行,但很low,緣由以下:
正確的解決方案是使用sourceSets,對於sourceSets的使用,放到下一節去說明。
強大的gradle,經過sourceSets可讓開發者可以自定義項目結構,如自定義assets目錄、java目錄、res目錄,並且還能夠是多個,但要知道的是,sourceSets並不會破壞變體的合併規則,它們是分開的,sourceSets只是起到了「擴充」的做用。這裏先擺一下sourceSets的常規使用:
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
aidl.srcDirs = ['src']
renderscript.srcDirs = ['src']
res.srcDirs = ['res']
assets.srcDirs = ['assets']
}
}
複製代碼
對於多渠道共用同一套assets資源文件這個問題,結合sourceSets,咱們能夠這麼處理,步驟以下:
sourceSets {
china {
sourceSet.assets.srcDirs = ['src/free/assets']
}
}
複製代碼
這樣配置之後,若是下次須要統一修改china與free的assets資源文件時,你就只須要把free/assets目錄下的資源文件替換掉就行了。雖然這種寫法已經知足前面說的需求了,可是還不夠,還能夠再優化一下,假設你有20個渠道,都使用同一套assets資源的話,按前面的寫法你就要寫19遍sourceSets配置了。
sourceSets {
china {
sourceSet.assets.srcDirs = ['src/free/assets']
}
a{
sourceSet.assets.srcDirs = ['src/free/assets']
}
b{
sourceSet.assets.srcDirs = ['src/free/assets']
}
...
}
複製代碼
能夠想像,在這個gradle文件中,光sourceSets配置就會有多長,你可能會說,一個項目怎麼會有這麼多渠道,很差意思,本人所處公司的業務需求就有20+個渠道的狀況,話很少說,下面就來看看怎麼優化好這段配置,若是你有學習過gradle,就應該知道,gradle是一種腳本,腳本是能夠像寫代碼同樣寫邏輯的,那麼上面的配置就能夠轉化爲一個if-else代碼片斷:
sourceSets {
sourceSets.all { sourceSet ->
// println("sourceSet.name = ${sourceSet.name}")
if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
if (sourceSet.name.contains("china")
|| sourceSet.name.contains("a")
|| sourceSet.name.contains("b")
|| ...) {
sourceSet.assets.srcDirs = ['src/free/assets']
}
}
}
}
複製代碼
如今你可能會以爲這樣寫好像精簡不了多少,不過一旦你的業務複雜起來,像這樣用代碼的邏輯思惟來處理配置,相信這會是一種不錯的選擇。
有興趣的能夠打印下sourceSet.name;if的寫法不必定要用contains(),也能夠用其餘的判斷方式,具體看開發者本身決定。
對於sourceSets的使用,除了針對修改assets之外,java文件、res資源文件、清單文件等等都是能夠用一樣的方式進行「擴充」的,好比不一樣渠道共用一套java代碼邏輯,那麼咱們能夠把這套代碼單獨抽取出來存放在一個其餘目錄下,而後使用sourceSets對其進行添加。這裏就以我親身經從來說明,我是如何經過sourceSets對於java和清單文件進行指定,而且完美解決此類"變態"需求的。
新的app項目開發完成,如今須要將項目定製化後上線,項目總體採用 1個Activity + n個Fragment架構,這個Activity即是程序主入口,由於咱們產品是作機頂盒app開發,產品開發完成後,須要上線到盒子運營商(局方)的應用商店,而後經過盒子推薦位(EPG)啓動咱們開發的app,所以上線後,須要提供app的包名和類名給到局方,假設新app的包名和類名分別以下:
包名:com.lqr.newapp
類名:com.lqr.newapp.MainActivity
複製代碼
把新app的包名和類名改爲跟舊app的同樣,由於局方那邊不想換~~假設舊app的包名和類名以下:
包名:com.lqr.oldapp
類名:com.lqr.oldapp.MainActivity
複製代碼
修改包名很簡單,可是修改入口類名就很麻煩了,若是我在該渠道目錄下新增一個com.lqr.oldapp.MainActivity,並在其清單文件中進行註冊,那麼,在打包時,渠道目錄下的AndroidManifest.xml會與main目錄下的AndroidManifest.xml進行合併。
而main目錄下的AndroidManifest.xml中已經註冊了com.lqr.newapp.MainActivity,這樣就會致使,最終輸出apk包中的清單文件會有2個入口類。
是的,這樣的產品交付出去,確實也能夠應付掉局方的需求,可是,一旦盒子安裝了這個app,那麼盒子Launcher上可能會同時出現2個入口icon,到時又是一頓折騰,畢竟app上線流程比較麻煩,咱們最好是保證產品就一個入口。
由於變體的資源合併規則,只要渠道目錄和main目錄下都存在AndroidManifest.xml,那麼最終apk包裏的清單文件合併出來的就會是2個文件的融合,因此,不能在這2個清單文件中分別註冊入口。能夠抽出2個不一樣入口的AndroidManifest.xml存放到其餘目錄,main下的AndroidManifest.xml只註冊通用組件便可。
在app目錄下,建立一個support/entry目錄(名字隨意),用於存放入口相關功能的代碼及資源文件,將com.lqr.oldapp.MainActivity放到support/entry/java目錄下。
在support目錄下,建立manifest(名字隨意),用於存放各渠道對應的AndroidManifest.xml,如:
其中newapp目錄下的AndroidManifest.xml:
<application>
<activity android:name="com.lqr.newapp.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
複製代碼
oldapp目錄下的AndroidManifest.xml:
<application>
<activity android:name="com.lqr.oldapp.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
複製代碼
通過上面2步後,oldapp的MainActivity與各自的主入口註冊清單文件就被抽離出去了,接下來就是使用sourceSets,根據不一樣的渠道名,指定java與清單文件便可:
sourceSets {
sourceSets.all { sourceSet ->
// project.logger.log(LogLevel.ERROR, "sourceSet.name = " + sourceSet.name)
if (sourceSet.name.contains('Debug') || sourceSet.name.contains('Release')) {
if (sourceSet.name.contains("china")) {
sourceSet.java.srcDirs = ['support/entry/java']
sourceSet.manifest.srcFile 'support/manifest/oldapp/AndroidManifest.xml'
} else {
sourceSet.manifest.srcFile 'support/manifest/newapp/AndroidManifest.xml'
}
}
}
}
複製代碼
至此,最終打出的apk包中的AndroidManifest.xml中就只會保留一個主入口了,完美解決了局方要求。
A:由於oldapp的MainActivity不單只是china這個渠道須要用到,後續還會被其它渠道使用,爲了後續複用考慮,因而就把MainActivity抽離出來。
A:若是你有打印過sourceSet.name的話,你必定會發現輸出的結果不僅僅只是那幾個變體名,還有androidTest、test、main等等這些,但咱們僅僅只是想對工程變體(chinaDebug、chinaRelease、freeDebug、freeRelease)指定java目錄和清單文件而已,若是對test、main這類「東西」也指定的話,結果並非咱們想要的,因此,必定要確保source配置的是咱們想要指定的變體,而非其餘。
A:以java源碼目錄爲例,默認AS工程的java源碼目錄是【src/main/java】,在gradle中經過sourceSets指定了另外一個目錄,好比【support/entry/java】,那麼打包時,AS會認爲這2個目錄均是有效的java目錄,因此,sourceSets指定的java目錄僅僅只是對原來的擴充,而非替換。仍是以java源碼目錄爲例,若是你的項目配置了多渠道,在不考慮sourceSets的狀況下,項目在打包時,由於變體合併的特性,有效的java目錄也是有2個,分別是【src/main/java】和【src/渠道名/java】,變體的合併規則不會由於sourceSets的配置而改變,若是將上述2種狀況一塊兒考慮上的話,那麼最終打包時,有效的java目錄則是3個,分別是【src/main/java】、【src/渠道名/java】、 【support/entry/java】。
咱們知道,要在gradle中添加第三方庫依賴的話,須要在dependencies領域進行配置,常見的configuration有provided(compileOnly)、compile(api)、implementation等等,它們的區別請自行百度查閱瞭解,針對【Build Type】、【Product Flavor】、【Build Variant】,這些configuration也會出現一些組合,如:
debugCompile // 全部的debug變體都依賴
releaseCompile // 全部的release變體都依賴
複製代碼
chinaCompile // china渠道依賴
americaCompile // america渠道依賴
freeCompile // free渠道依賴
複製代碼
chinaDebugCompile // chinaDebug變體依賴
chinaReleaseCompile // chinaRelease變體依賴
americaDebugCompile // americaDebug變體依賴
americaReleaseCompile // americaRelease變體依賴
freeDebugCompile // freeDebug變體依賴
freeReleaseCompile // freeRelease變體依賴
複製代碼
經過上述組合就能夠輕鬆配置好各類狀況下的依賴了,如:
// autofittextview
compile 'me.grantland:autofittextview:0.2.+'
// leakcanary
debugCompile "com.squareup.leakcanary:leakcanary-android:1.6.1"
debugCompile "com.squareup.leakcanary:leakcanary-support-fragment:1.6.1"
releaseCompile "com.squareup.leakcanary:leakcanary-android-no-op:1.6.1"
// gson
chinaCompile 'com.google.code.gson:gson:2.6.2'
americaCompile 'com.google.code.gson:gson:2.6.2'
freeCompile 'com.google.code.gson:gson:2.5.2'
複製代碼
雖然官方給出的多種組合依賴能夠解決幾乎全部的依賴問題,但實際上,當渠道有不少不少時,整個gradle文件將變得冗長臃腫,你能想像20多個渠道中只有1個渠道依賴的gson版本不一樣的狀況嗎?因此,這時候就須要考慮一下,充分利用好gradle做爲腳本的特性,使用代碼方式來進行渠道依賴:
dependencies {
gradle.startParameter.getTaskNames().each { task ->
// project.logger.log(LogLevel.ERROR, "lqr print task : " + task)
if (task.contains('free')) {
compile 'com.google.code.gson:gson:2.5.2'
} else {
compile 'com.google.code.gson:gson:2.6.2'
}
}
}
複製代碼
另外,還有一種方式是我以前項目中使用過的,但這種方式不支持依賴遠程倉庫組件,這裏也記錄一下:
dependencies {
// 配置 插件化庫 依賴
applicationVariants.all { variant ->
if (variant.flavorName == 'china')
||variant.flavorName == 'america') {
dependencies.add("${variant.flavorName}Compile", project(':DroidPluginFix'))
} else {
dependencies.add("${variant.flavorName}Compile", project(':DroidPlugin'))
}
}
}
複製代碼
要知道,如下寫法是正確的,但就是不生效:
dependencies.add("${variant.flavorName}Compile", 'com.google.code.gson:gson:2.6.2')
複製代碼
DroidPluginFix是最新官方適配了Android七、8的DroidPlugin,而DroidPlugin則沒有適配,由於歷史緣由,須要對不一樣的渠道依賴不一樣的DroidPlugin版本。
在公司待了有1年半了,這段時間裏對本身的要求很嚴格,學習了不少新知識,並大膽的用於實踐,收穫頗多,由於公司業務的特殊性,對gradle以及多渠道的掌握要求比較高,因此,這也是我這段時間來重點學習的一部分,可是畢竟是單獨學習這些知識,可能也會存在一些掌握不到位的狀況,因此上面提到的知識若是有誤或有更好的處理方式,歡迎各位指出和分享,thx~