Gradle實(shí)現(xiàn)Android多渠道定制化打包
最近在項(xiàng)目中遇到需要實(shí)現(xiàn) Apk 多渠道、定制化打包, Google 、百度查找了一些資料,成功實(shí)現(xiàn)了上述功能,在此記錄以備不時之需,溫故而知新,可以為師矣~
需求可以總結(jié)如下:
如何實(shí)現(xiàn)多個 Apk 安裝在同一設(shè)備
在之前的印象中,同一個應(yīng)用在同一設(shè)備上只能安裝一個,除非手動修改 AndroidManifest.xml 文件中的包名( package ),但這么做的后果就是新的應(yīng)用真的是新的應(yīng)用,舊版應(yīng)用再也收不到更新。而現(xiàn)在你通過 Gradle,你可以輕松構(gòu)建多個不同版本的應(yīng)用,并且在同一設(shè)備上安裝使用。
這里要用到 productFlavors ,productFlavors 可以用來自定義應(yīng)用構(gòu)建版本,我們可以用其 applicationId 屬性來實(shí)現(xiàn)多個 Apk 安裝在同一設(shè)備上。
build.gradle 中部分配置代碼如下:
- android {
- compileSdkVersion 24
- buildToolsVersion "24.0.1"
- //默認(rèn)配置,所有 productFlavors 都會繼承 defaultConfig 中配置的屬性
- defaultConfig {
- //默認(rèn)的 applicationId,一般與 AndroidManifest.xml 文件 package屬性相同
- applicationId "com.littlejie.multichannel"
- minSdkVersion 15
- targetSdkVersion 24
- versionCode 1
- versionName "1.0"
- }
- // productFlavors 定義了一個應(yīng)用的自定義構(gòu)建版本
- //一個單一的項(xiàng)目可以同時定義多個不同的 flavor 來改變應(yīng)用的輸出。
- // productFlavors 這個概念是為了解決不同的版本之間的差異非常小的情況,通常用于區(qū)分同一個應(yīng)用的不同渠道/客戶等,可包含少量業(yè)務(wù)功能差別。
- // productFlavors 中的 flavor 不能跟 buildType 中的一樣,否則會報(bào): "ProductFlavor names cannot collide with BuildType names"
- productFlavors {
- //默認(rèn)版本,不設(shè)置 applicationId ,繼承 defaultConfig 中的配置
- flavors_default {
- }
- //開發(fā)版本, applicationId 替換為 com.littlejie.multichannel.dev
- flavors_dev {
- applicationId "com.littlejie.multichannel.dev"
- }
- //發(fā)布版本, applicationId 替換為 com.littlejie.multichannel.release
- flavors_release {
- applicationId "com.littlejie.multichannel.release"
- }
- }
- }
MainActivity.java:
- public class MainActivity extends Activity {
- private static final String TAG = MainActivity.class.getSimpleName();
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- Log.d(TAG, "package name = " + this.getPackageName());
- }
- }
在 Android Studio 中執(zhí)行如下命令:
- //打 debug 包,gradle 命令會在后面 `gradle task`中詳細(xì)講述
- gradle clean assembleDebug
打包完成后,將 Apk 安裝到模擬器(adb install name.apk),運(yùn)行,log 如下:
flavors_default:
- 09-17 22:43:55.390 19747-19747/com.littlejie.multichannel D/MainActivity: package name = com.littlejie.multichannel
flavors_dev:
- 09-17 22:11:30.860 2638-2638/com.littlejie.multichannel.dev D/MainActivity: package name = com.littlejie.multichannel.dev
flavors_release:
- 09-17 22:44:55.610 20650-20650/com.littlejie.multichannel.release D/MainActivity: package name = com.littlejie.multichannel.release
從這里可以看出,不同 flavor 的 package name 被 applicationId 替換掉了,而且同一個模擬器上可以同時安裝以上三個應(yīng)用。
下面我們再看看 AndroidManifest.xml 中發(fā)生了什么變化。這里需要用到 aapt 來查看 AndroidManifest.xml 的信息:
- //輸出 apk 的 AndroidManifest.xml 文件的信息aapt dump xmltree ***.apk AndroidManifest.xml
下面是 flavors_dev 版本的信息,可以看出 Java 源文件的包名并沒有發(fā)生改變,而 package 屬性的值被替換為 applicationId了。
如果在申請第三方 SDK 接入,則對應(yīng)的包名應(yīng)該填 applicationId ,而不是 AndroidManifest.xml 中的默認(rèn)值
- lishengjiedeMacBook-Pro:apk littlejie$ aapt dump xmltree multichannel-flavors_dev-debug.apk AndroidManifest.xml
- N: android=http://schemas.android.com/apk/res/android
- E: manifest (line=2)
- A: android:versionCode(0x0101021b)=(type 0x10)0x1
- A: android:versionName(0x0101021c)="1.0" (Raw: "1.0")
- //此處 package 的值已替換成 applicationId 的值
- A: package="com.littlejie.multichannel.dev" (Raw: "com.littlejie.multichannel.dev")
- A: platformBuildVersionCode=(type 0x10)0x18 (Raw: "24")
- A: platformBuildVersionName=(type 0x4)0x40e00000 (Raw: "7.0")
- E: uses-sdk (line=7)
- A: android:minSdkVersion(0x0101020c)=(type 0x10)0xf
- A: android:targetSdkVersion(0x01010270)=(type 0x10)0x18
- E: application (line=11)
- A: android:theme(0x01010000)=@0x7f08008e
- A: android:label(0x01010001)=@0x7f060020
- A: android:icon(0x01010002)=@0x7f030000
- A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff
- A: android:allowBackup(0x01010280)=(type 0x12)0xffffffff
- A: android:supportsRtl(0x010103af)=(type 0x12)0xffffffff
- // Activity 的包名還是原來 AndroidManifest.xml 中申明的
- E: activity (line=17)
- A: android:name(0x01010003)="com.littlejie.multichannel.MainActivity" (Raw: "com.littlejie.multichannel.MainActivity")
- E: intent-filter (line=18)
- E: action (line=19)
- A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
- E: category (line=21)
- A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
applicationId 的原理可以理解為在 gradle 打包的時,動態(tài)合并屬性,將 package 替換為 applicationId 指定的值,但并不會替換 Java 文件的包名,包括生成的 R 文件(可以去對應(yīng) module 下的 build/generated 目錄下查看對應(yīng) flavor 的 R 文件)。
另外,由于最終生成的包中 AndroidManifest.xml 文件中的 package 屬性被 applicationId 替換掉,故對于某些第三方 SDK ,如:微信、高德地圖等需要驗(yàn)證包名的,就會碰到相當(dāng)?shù)疤鄣氖?,每個包都需要重新去生成 APPID 和 APPKEY,如果渠道很多,那么像微信就會出現(xiàn)問題微信賬號申請的應(yīng)用數(shù)就會超出微信的限制。
Android 官方文檔原文如下:
Therefore, we have decoupled the two usages of package name:
The final package that is used in your built .apk's manifest, and is the package your app is known as on your device and in the Google Play store, is the "application id".
The package that is used in your source code to refer to your R class, and to resolve any relative activityrvice registrations, continues to be called the "package".
補(bǔ)充:ApplicationId versus PackageName
替換 AndroidManifest.xml 中的屬性
這里可以參考友盟統(tǒng)計(jì) SDK 中使用的方案。該方案通過在 AndroidManifest.xml 文件中 application 標(biāo)簽下指定 <mate-data> 設(shè)置占位符來實(shí)現(xiàn)動態(tài)替換屬性值。
- <meta-data android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL}" />
占位符形如${name},在最終執(zhí)行 AndroidManifest.xml 文件合并的時候,占位符會被
build.gradle 中對應(yīng)值取代。 build.gradle 的配置需要用到上節(jié)講到的 productFlavors 的 manifestPlaceholders 屬性, manifestPlaceholders 屬性直譯過來就是清單文件占位符。
下面是 build.gradle 的節(jié)選代碼:
- productFlavors {
- //將 AndroidManifest.xml 文件中的 ${UMENG_CHANNEL} 替換為 default
- flavors_default {
- manifestPlaceholders = [UMENG_CHANNEL: "defalut"]
- }
- flavors_dev {
- applicationId "com.littlejie.multichannel.dev"
- manifestPlaceholders = [UMENG_CHANNEL: "dev"]
- }
- flavors_release {
- applicationId "com.littlejie.multichannel.release"
- manifestPlaceholders = [UMENG_CHANNEL: "release"]
- }
- }
如果你要替換多個屬性,則只需要將 manifestPlaceholders 的寫法如下:
- manifestPlaceholders = [VALUE_NAME1 : "value" , VALUE_NAME2 : "value"]
補(bǔ)充:關(guān)于 AndroidManifest 文件合并規(guī)則可以查看 官方文檔.
替換資源文件
多渠道打包的時候可能會碰到這種情況:每個應(yīng)用市場的啟動頁圖標(biāo)、應(yīng)用名稱可能會有點(diǎn)小出入,更有甚者,連布局都不一樣。這時候我們該怎么辦呢?
有一種解決辦法就是:在代碼里進(jìn)行判斷,根據(jù)渠道的不一樣,加載不同的圖片和布局,這是一種解決辦法。但是當(dāng)渠道有很多時,代碼就會變得很難維護(hù),而且指定渠道用到的資源文件都會被打入所有 Apk 中。所以這個方法并不值得推薦。那么,有什么好的解決辦法呢?
辦法 Google 早就給我們想好了,而且相當(dāng)簡單,那就是:在 main 的同級目錄下創(chuàng)建以渠道名命名的文件夾,然后創(chuàng)建資源文件(路徑要與 main 中的一致),然后打包的時候 gradle 就會自己替換或者合并資源。
例如, App 的默認(rèn) icon 路徑為 main\res\mipmap-hdpi\ic_launcher.png ,那么 flavors_dev的路徑就為 flavors_dev\res\mipmap-hdpi\ic_launcher.png ,打包 flavors_dev 渠道的時候會自動替換圖片。
對于資源合并,如果在 main 下的 strings.xml 內(nèi)容為:
- <resources>
- <string name="app_name">MultiChannel</string>
- <string name="string_merge">我是string,我暫時沒被合并</string>
- </resources>
在 flavors_dev 下的 strings.xml 內(nèi)容為:
- <resources>
- <string name="string_merge">我是dev_string,我會把string合并</string>
- </resources>
當(dāng)打 flavors_dev 渠道包時,最終 strings.xml 會變成:
- <resources>
- <string name="app_name">MultiChannel</string>
- <string name="string_merge">我是dev_string,我會把string合并</string>
- </resources>
以上特性可以用來替換 Apk 的應(yīng)用名稱和應(yīng)用圖標(biāo),這比使用前面講到的占位符方便很多。同理,替換圖片和合并顏色的原理也相似。
多渠道使用獨(dú)立簽名
多渠道打包的時候,可能每個渠道包的簽名都必須不一樣,真正做到定制化,那么,怎么實(shí)現(xiàn)每個渠道包使用指定的簽名呢?
平時我們打包的時候是這樣的:
- signingConfigs {
- release {
- storeFile file("簽名文件路徑")
- storePassword "storePassword"
- keyAlias "keyAlias"
- keyPassword "keyPassword"
- }
- }
- buildTypes {
- release {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- shrinkResources true
- //指定打 release 包時使用的簽名文件
- signingConfig signingConfigs.release
- }
- //如果 debug 包需要測試諸如微信、地圖等第三方 sdk ,則可以指定 debug 包使用 release 包的簽名
- //debug {
- // signingConfig signingConfigs.release
- //}
- }
而給每個渠道包指定簽名其實(shí)也差不多。
Google 官方原話:
This enables either having all release packages share the same SigningConfig, by setting android.buildTypes.release.signingConfig, or have each release package use their own SigningConfig by setting each android.productFlavors.*.signingConfig objects separately.
大意就是,在 buildType 下指定簽名的具體屬性,形如 android.productFlavors.*.signingConfig signingConfigs.* ,前一個 * 指代在 productFlavors 中定義的 flavor ,后一個 * 指代在 signingConfigs 定義的屬性。值得注意的是,signingConfigs 必須定義在 buildType 之前。
以下是 build.gradle 的配置節(jié)選:
- //定義簽名屬性
- signingConfigs {
- flavors_default {
- //如果簽名文件在項(xiàng)目的根目錄下,則可以這么寫
- storeFile file("../littlejie.jks")
- storePassword "******"
- keyAlias "******"
- keyPassword "*****"
- }
- flavors_dev {
- storeFile file("../littlejie_dev.jks")
- storePassword "*****"
- keyAlias "*****"
- keyPassword "*****"
- }
- }
- buildTypes {
- release {
- minifyEnabled true
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
- shrinkResources true
- //多個 flavor ,指定 flavor 使用指定 簽名
- productFlavors.flavors_default.signingConfig signingConfigs.flavors_default
- productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev
- }
- //如果 debug 包需要測試諸如微信、地圖等第三方 sdk ,則可以指定 debug 包使用 release 包的簽名
- //debug 并不能設(shè)置多個簽名
- //debug {
- // productFlavors.flavors_default.signingConfig signingConfigs.flavors_default
- // productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev
- //}
- }
下面我們來驗(yàn)證下生成的包的簽名是否正確,查看簽名我們會用到如下兩個命令:
- //查看簽名文件的屬性
- keytool -list -keystore 簽名文件
- //查看 apk 的簽名,需要提前解壓 apk ,獲取 CERT.RSA(位于解壓目錄下 /META-INF 下)
- //以下命令行是在 apk 解壓目錄下執(zhí)行
- keytool -printcert -file META-INF/CERT.RSA
更多 keytool 命令使用可以查看 官方文檔
首先,我們來看下 littlejie.jks 的信息:
- lishengjiedeMacBook-Pro:AndroidDemo littlejie$ keytool -list -keystore littlejie.jks
- 輸入密鑰庫口令:
- 密鑰庫類型: JKS
- 密鑰庫提供方: SUN
- 您的密鑰庫包含 1 個條目
- littlejie, 2016-9-18, PrivateKeyEntry,
- 證書指紋 (SHA1): A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84
解壓 multichannel-flavors_default-release.apk ,查看 CERT.RSA 信息
- lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_default-release/META-INF/CERT.RSA
- 所有者: CN=littlejie
- 發(fā)布者: CN=littlejie
- 序列號: 71693e05
- 有效期開始日期: Sun Sep 18 17:20:34 CST 2016, 截止日期: Thu Sep 12 17:20:34 CST 2041
- 證書指紋:
- MD5: AC:12:83:51:44:FC:82:68:8B:23:7B:E9:12:24:AE:52
- SHA1: A2:B1:BF:BF:F1:F3:26:F4:FD:0C:94:95:B5:32:90:69:24:F7:99:84
- SHA256: AD:04:19:5F:92:00:0D:FA:7C:E5:8A:12:57:72:4C:1E:0E:2E:FC:0D:92:28:05:D0:CC:42:FC:93:95:44:88:88
- 簽名算法名稱: SHA256withRSA
- 版本: 3
可以發(fā)現(xiàn)兩者的 SHA1 值是相等的。
同理,可以查看 littlejie_dev.jks 和 multichannel-flavors_dev-release.apk 的簽名信息
- //littlejie_dev.jks 的簽名信息
- lishengjiedeMacBook-Pro:AndroidDemo littlejie$ keytool -list -keystore littlejie_dev.jks
- 輸入密鑰庫口令:
- 密鑰庫類型: JKS
- 密鑰庫提供方: SUN
- 您的密鑰庫包含 1 個條目
- littlejie, 2016-9-18, PrivateKeyEntry,
- 證書指紋 (SHA1): B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2
- //multichannel-flavors_dev-release.apk 的簽名信息
- lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_dev-release/META-INF/CERT.RSA
- 所有者: CN=littlejie
- 發(fā)布者: CN=littlejie
- 序列號: 48346e15
- 有效期開始日期: Sun Sep 18 17:21:23 CST 2016, 截止日期: Thu Sep 12 17:21:23 CST 2041
- 證書指紋:
- MD5: 15:E9:E1:67:AB:33:8B:04:A4:C3:D0:05:8F:A6:35:37
- SHA1: B4:25:67:A5:9F:8C:1F:12:BD:85:6B:2D:FE:71:62:57:8A:CC:AE:E2
- SHA256: 96:A5:14:EC:28:25:32:0D:3E:D0:DB:D0:84:06:E7:9C:17:D7:91:83:A4:51:93:AB:34:3E:D9:FD:C5:FA:A1:8E
- 簽名算法名稱: SHA256withRSA
- 版本: 3
但是這里有個問題,就是這種給某個 flavor 指定簽名的方法對 debug 無效,有興趣的同學(xué)可以看上述注釋掉的 debug 簽名部分配置。簡單來說,debug 簽名只能指定一個或者使用默認(rèn)的 debug 簽名。
若哪位大神有解決方案,歡迎指出~
這里再做幾點(diǎn)補(bǔ)充:
1.多渠道使用獨(dú)立簽名,打包時千萬不要使用 Android Studio 中 Build 菜單下的 Generate Signed APK,因?yàn)楫?dāng)你使用這個打包的時候, Android Studio 會讓你指定使用的簽名文件, so 你就等著哭吧~樓主因?yàn)檫@個折騰了半天。解決方法就是使用 gradle tasks。傳送門:Android Gradle Build Tasks
2.鑒于第一點(diǎn)中的傳送門需要翻墻,所以在這里簡單介紹一下 Android Gradle Build Tasks 的使用。
- 打全部包: gradle assemble
- 打全部 Debug 包: gradle assembleDebug ,可以簡寫為 gradle aD ,前提是沒有相同縮寫的參數(shù)
- 打全部 Release 包: gradle assembleRelease,可以簡寫為 gradle aR
- 打指定 flavor 包: gradle assemble(flavor)(Debug|Release)
- 打包完成后安裝(設(shè)備上沒有安裝該 apk ,否則會失敗,而且只能指定 flavor ,不然也會失敗): gradle install(flavor)(Debug|Release)
- 打包前先 clean 一下(在測試的時候很必要,如果不 clean 的話,可能會導(dǎo)致某些小修改不會及時打入新包): gradle clean assembleDebug
利用 Gradle 修改構(gòu)建版本號
樓主表示對 Groovy 不是很熟,所以利用 Gradle 自動修改構(gòu)建版本這個就先留著,我先去研究幾天~
補(bǔ)充
有童鞋在評論中說:使用 productFlavors 打包效率太低,的確是這樣, gradle 好用是好用,就是打包效率低。如果只是單純生成渠道包,建議使用美團(tuán)多渠道打包方案,另外 360 加固也是一種不錯的選擇,效率都比使用 gradle 來的高。但如果需要替換 Apk 中的圖片、字符串、應(yīng)用的 applicationId 、給指定渠道的包使用指定的簽名,那么只能乖乖使用 gradle 打包了,慢你也得忍著~
之前剛開始調(diào)研的時候,發(fā)現(xiàn) Github 上有個 ApkCustomizationTool 項(xiàng)目,它是通過對 Apk 解包,替換圖片、字符串,然后重新簽名,不過這畢竟是事后諸葛亮,控制在打包的源頭總是畢竟好的,有興趣的同學(xué)可以去研究下。
不知大家有沒有這種感受,每次發(fā)版上傳渠道的時候想死有沒有?o(╯□╰)o
總結(jié)
以上就是自己在使用 Gradle 實(shí)現(xiàn) Android 多渠道打包時碰到的問題, Android 官方關(guān)于使用 Gradle 的文檔已經(jīng)很詳細(xì)了,自己總結(jié)的只是一點(diǎn)皮毛,有時間要去自習(xí)研讀下。