前言
最近有個需求,老板讓開發(fā)一個新的app,新的app上的功能和老的app基本上完全一致,差異化的地方很少,那按照慣性思維,復制出一個老的app,然后改改色值,icon,string不就可以了么。但是,還要求以后保持倆app數(shù)據(jù)同步,產(chǎn)品同步。換句話說,老app上有需求,新的app上也要同樣去支持。這樣的話,復制出的新app想要同步支持老app的需求,就比較難了,反之亦然。其實,google官方早就給出了類似問題的解決方案,多渠道打包。
build配置及目錄結(jié)構(gòu)
①新增productFlavors
在app的build中新增productFlavors來標記多渠道:
flavorDimensions "app"
productFlavors {
main {
applicationId "com.zdu.client"
dimension "app"
}
app2 {
applicationId "com.zdu.test"
dimension "app"
}
}
flavorDimensions官方文檔上介紹的很清楚,這是一個維度標識。這么講肯定難以理解,如上代碼所示,flavorDimensions只有一個值app,這么編譯完后查看 Build Variants,如圖1:

會有四個組合,分別是main的debug和release環(huán)境以及app2的debug和release環(huán)境。那如果我們再增加一個新的維度
lib,會是什么樣子的呢?上代碼:
flavorDimensions "app","lib"
productFlavors {
main {
applicationId "com.zdu.client"
dimension "app"
}
app2 {
applicationId "com.zdu.test"
dimension "app"
}
app3 {
applicationId "com.zdu.test2"
dimension "lib"
}
}
sync后查看Build Variants,如圖2:

變成了mainApp3的debug和release環(huán)境以及app2App3的debug和release環(huán)境。
原因就是main和app2都是使用的app維度,所以他們倆是同維度的單位,而app3是lib維度屬性,所以app3要分別和main,app2兩個渠道進行組合,形成了一個新的維度單位。
PS:這個功能,大家理解了就行,目前我沒找到可以使用的場景。一般來說,只需要保持一個維度就好。
②創(chuàng)建新渠道app2的文件目錄
首先切換到Project目錄訪問,在app-src的目錄下,也就是說和main平級的目錄下,創(chuàng)建app2文件夾,然后在app2的目錄下創(chuàng)建跟main目錄下一模一樣的文件目錄結(jié)構(gòu),如圖:

基于此,準備工作就算做好,基本配置和基本結(jié)構(gòu)已經(jīng)搞定,接下來就是來了解如何去多渠道開發(fā)。
使用技巧
再看下上圖,app2和main目錄結(jié)構(gòu)是一樣的,那是不是意味著,app2和main是平級的?切換到app2分支的時候就會走app2的java代碼和res的資源呢?
先回答第一個問題:
app2和main是平級的?
app2和main并不是平級,相反的,app2是main的附屬,main是公共代碼資源庫,app2的所有缺失的java和res資源都會去main下找公共資源,所以我們切換到app2渠道下,可以直接運行app,除了applicationId不同之外,app不會有任何變化。
main是公共代碼資源庫,這句話的意思是說,無論有多少個渠道,main下的java和res都是最基本的存在,類似于所有其他的渠道都在引用main這個庫的意思。這和我們開發(fā)引用一個庫是類似的原理,只是完全反轉(zhuǎn)過來,我們開發(fā)一個庫,是app來引用這個庫,而多渠道下都在一個app下,其他渠道以類似引用的方式來使用main下的java和res。
切換到app2分支的時候就會走app2的java代碼和res的資源呢?
如果理解了第一個問題,那第二個問題也就比較好理解了。app2作為main的附屬,切換到app2分支后,會將app2下的java代碼和res合并到main下編譯運行。
隨之又會有一個新的問題,java代碼和res資源是如何合并的?
java代碼的合并比較簡單,舉個簡單的例子,如圖:java代碼和res資源是如何合并的?

我們在app2創(chuàng)建如圖的目錄結(jié)構(gòu),編譯運行后,相當于在main下也創(chuàng)建了一樣的目錄結(jié)構(gòu),將app2下的代碼復制一份到對應的目錄結(jié)構(gòu)下。如果在app2和main的相同目錄結(jié)構(gòu)都創(chuàng)建一樣的類會怎樣?如圖

那么這就要求我們渠道下的java目錄結(jié)構(gòu)和類名不能和main公共資源下的完全一致。
res資源的合并相對來說就是真正的合并了,但drawable,layout,和values下的合并還有所不同。
drawable合并
drawable的合并只需要命名一致,并對比main項目中圖片放置的位置放到tea項目的對應位置即可完成替換。

圖片替換要注意兩點:第一,目前和命名一致;第二:main下有幾套圖片,app2下就要有幾套圖片,可以多但不能少。
app2下新增一個main沒有的圖片,代碼中去引用了的話,切換到main渠道下會報錯找不到該資源文件,這個問題稍后講解。
layout合并
laout布局文件跟drawable圖片合并一樣,也是要求命名一致,但涉及到布局文件中的id的處理,要求比較嚴格,如果相同的功能只是布局位置,字體大小,色值等調(diào)整,那么id必須一致,因為同一個java文件引用不同渠道下的layout布局,如果id不同,切換渠道肯定報錯;如果app2中新增一個id,而又在java代碼中引用了,那么切換到main渠道下也會報錯,因為main渠道下的layout沒有這個id,這塊的處理稍后再說。
string,color合并
string和color等類似獨一份的資源文件合并又有所不同,簡單的說就是,相同命名的string和color會被替換,不同命名的會新增。如圖:


相同的app_name就會被替換成MyApp2的名稱。
不同命名的會新增,也會有l(wèi)ayout布局id類似的問題,如果main下string.xml沒有相同命名的資源,同時又在java代碼中引用了,一樣會出問題,這塊稍后一起講解。
java代碼的差異化處理
java代碼的差異化處理是重中之重,再怎么相似的倆app,總有些個別地方邏輯不同的地方。我這邊提供兩種處理差異化代碼的方式:
main下公共代碼庫差異化處理
兩個app共用一套代碼的前提下,在main下進行代碼區(qū)分,這種情況需要做渠道區(qū)分,BuildConfig類中已經(jīng)有渠道區(qū)分常量:BuildConfig.FLAVOR
那么在代碼中就可以判斷:
if ("main".equals(BuildConfig.FLAVOR)) {
// 處理main下邏輯
} else if ("app2".equals(BuildConfig.FLAVOR)) {
// 處理app2下邏輯
}
這里是建議大家寫一個工具類,不然每個差異化的地方都要這么判斷很蠢的。
public class FlavorUtils {
public static boolean isMain() {
return "main".equals(BuildConfig.FLAVOR);
}
public static boolean isApp2() {
return "app2".equals(BuildConfig.FLAVOR);
}
}
差異化不多的情況下,這種寫法是最方便的,也是最效率的,唯一的壞處就是在于要多判斷。
注:這種差異化處理是將main和app2分別當做一個獨立的渠道,但因為main還是公共代碼庫,所以切換到app2下進行編譯,會同時編譯app2和main下的java代碼,這種情況下main代碼中引用app2的類是沒有問題的。
但如果切換到main渠道下去編譯,你會發(fā)現(xiàn)編譯后提示找不到app2下類的錯誤,那是因為切換到main渠道下,只會編譯main下java代碼,不會編譯app2的java代碼,自然就找不到對應app2下的類了。解決方式也有:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs = ['src/main/java','src/app2/java']
}
app2 {
java.srcDirs = ['src/app2/java']
}
}
配置main下的java.srcDirs編譯目錄,切換到main渠道后同時編譯main/java和app2/java,就可以了。
分離公共代碼庫,每個app創(chuàng)建對應的渠道
在前文中,我們都是把main當做一個單獨的app渠道,app2作為第二個渠道,現(xiàn)在的方式就是,將main的渠道單獨分離出來,創(chuàng)建app1渠道。將app1和app2差異的類從main下剪切出來同時復制到對應的app1和app2下,單獨去開發(fā)對應的渠道代碼,互相不干擾。
這樣,main的功能性就只是公共代碼資源庫的職能,不能再作為一個單獨的渠道去編譯運行了。但同時,build也需要修改下:
sourceSets {
main {
jniLibs.srcDirs = ['libs']
java.srcDirs = ['src/main/java']
}
app1 {
java.srcDirs = ['src/app1/java']
}
app2 {
java.srcDirs = ['src/app2/java']
}
}
各自編譯各自的java代碼。
app1和app2下相同的類也不會報錯:

原因很簡單,因為編譯了app1渠道,沒有編譯app2渠道,自然不會出現(xiàn)類沖突的問題。
注:這種java代碼的差異化處理需要注意,main只能引用app1和app2下路徑和類名一致的java類,互相切換渠道才不會報錯,如果main只引用了app1中有的類,而app2下沒有這個類,那切換到app2渠道下肯定要報錯了。
gradle使用技巧
上面那些可以讓我們順利的寫代碼,但還不夠。比如環(huán)境配置,簽名配置,不同渠道下的各種三方key值,甚至不同環(huán)境都會有不同的key值等等,這些在正式開發(fā)中,肯定會遇到的。下面就給大家詳細的介紹下,遇到這些問題,該怎么去處理。
三方key值配置
三方key值一般都是寫在AndroidManifest中的,如:
<!--微信id-->
<meta-data
android:name="WEIXIN_ID"
android:value="******************" />
單渠道下,我們可以直接把id寫在AndroidManifest下,多渠道下,就需要改造一番:
<!--微信id-->
<meta-data
android:name="WEIXIN_ID"
android:value="${WX_KEY}" />
gradle中這樣配置:
productFlavors {
main {
applicationId "com.zdu.client"
dimension "app"
manifestPlaceholders = [
WX_KEY : "*************",
]
}
app2 {
applicationId "com.zdu.test"
dimension "app"
manifestPlaceholders = [
WX_KEY : "%%%%%%%%%%%%%%%%%",
]
}
}
這樣配置之后,就能分渠道加載不同的key值。
簽名配置
多渠道下,僅支持debug多簽名配置,不支持release的多簽名配置,換句話說,release下只能配置一個簽名。
首先,新增一個debug簽名:
signingConfigs {
release {
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
storeFile STORE_FILE
storePassword STORE_PASSWORD
}
debug {
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
storeFile STORE_FILE
storePassword STORE_PASSWORD
}
app2Debug {
keyAlias KEY_ALIAS
keyPassword KEY_PASSWORD
storeFile STORE_FILE
storePassword STORE_PASSWORD
}
}
然后配置簽名引用:
buildTypes {
debug {
// main簽名
productFlavors.main.signingConfig signingConfigs.debug
//app2簽名
productFlavors.app2.signingConfig signingConfigs.app2Debug
}
}
由于只能配置debug環(huán)境的簽名,不能配置release的簽名,就導致不能多渠道多簽名開發(fā),只能共同使用一個簽名。當然非要多簽名開發(fā)也是可以的,就是每次換渠道手動改gradle文件,無非就是比較麻煩罷了。
不過話又說回來了,同一個公司的產(chǎn)品使用同一個簽名文件是很常見的事件,能省去很多麻煩。
不同環(huán)境下的key值配置
這個技巧挺實用的,比如各種統(tǒng)計三方的key,往往都是測試環(huán)境和正式環(huán)境不同,這個時候就需要這種來配置了。
正常開發(fā),一般最少會有倆環(huán)境,咱們先模擬一番:
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
signingConfig signingConfigs.release
}
}
在這里面如果想跟設(shè)置簽名的那種直接配置各種參數(shù)是不行的,這里就不舉例子了,不信的話各位可以試試。
這里要使用另外一種方式:
android.applicationVariants.all { variant ->
println(variant.name)
if (variant.name == 'mainDebug') {
buildConfigField "Integer", "ENV", "2"
}
if (variant.name == 'mainRelease') {
buildConfigField "Integer", "ENV", "0"
}
if (variant.name == 'app2Debug') {
buildConfigField "Integer", "ENV", "2"
}
if (variant.name == 'app2Release') {
buildConfigField "Integer", "ENV", "0"
}
}

variant.name就是圖里面對應的名稱,環(huán)境不同,只需要在這個判斷里寫對應環(huán)境的值,然后在BuildConfig.ENV就能使用不同的值了。
結(jié)語
多渠道開發(fā)算是小眾開發(fā)方式,也正因為小眾,網(wǎng)上也沒有太多太詳細的資料。我寫這篇文章除了當做資料記憶之外,也希望能幫助到需要此功能的朋友們可以愉快的開發(fā)。