最近在項目中遇到需要實現 Apk 多渠道、定製化打包, 查找了一些資料,成功實現了上述功能,在此記錄以備不時之需,溫故而知新,可以爲師矣~
需求可以總結如下:

如何實現多個 Apk 安裝在同一設備
在之前的印象中,同一個應用在同一設備上只能安裝一個,除非手動修改 AndroidManifest.xml 文件中的包名( package
),但這麼做的後果就是新的應用真的是新的應用,舊版應用再也收不到更新。而現在你通過 Gradle,你可以輕鬆構建多個不同版本的應用,並且在同一設備上安裝使用。
這裏要用到 productFlavors ,productFlavors
可以用來自定義應用構建版本,我們可以用其 applicationId
屬性來實現多個 Apk 安裝在同一設備上。
build.gradle 中部分配置代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | android { compileSdkVersion 24 buildToolsVersion "24.0.1" //默認配置,所有 productFlavors 都會繼承 defaultConfig 中配置的屬性 defaultConfig { //默認的 applicationId,一般與 AndroidManifest.xml 文件 package屬性相同 applicationId "com.littlejie.multichannel" minSdkVersion 15 targetSdkVersion 24 versionCode 1 versionName "1.0" } // productFlavors 定義了一個應用的自定義構建版本 //一個單一的項目可以同時定義多個不同的 flavor 來改變應用的輸出。 // productFlavors 這個概念是爲了解決不同的版本之間的差異非常小的情況,通常用於區分同一個應用的不同渠道/客戶等,可包含少量業務功能差別。 // productFlavors 中的 flavor 不能跟 buildType 中的一樣,否則會報: "ProductFlavor names cannot collide with BuildType names" productFlavors { //默認版本,不設置 applicationId ,繼承 defaultConfig 中的配置 flavors_default { } //開發版本, applicationId 替換爲 com.littlejie.multichannel.dev flavors_dev { applicationId "com.littlejie.multichannel.dev" } //發佈版本, applicationId 替換爲 com.littlejie.multichannel.dev flavors_release { applicationId "com.littlejie.multichannel.release" } } } |
MainActivity.java:
1
2
3
4
5
6
7
8
9
10
11
12
|
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 中執行如下命令:
1 2 | //打 debug 包,gradle 命令會在後面 `gradle task`中詳細講述 gradle clean assembleDebug |
打包完成後,將 Apk 安裝到模擬器(adb install name.apk
),運行,log 如下:
flavors_default:
1
|
09-17
22:43:55.390
19747-19747/com.littlejie.multichannel
D/MainActivity:
package
name
=
com.littlejie.multichannel
|
flavors_dev:
1 | 09-17 22:11:30.860 2638-2638/com.littlejie.multichannel.dev D/MainActivity: package name = com.littlejie.multichannel.dev |
flavors_release:
1
|
09-17
22:44:55.610
20650-20650/com.littlejie.multichannel.release
D/MainActivity:
package
name
=
com.littlejie.multichannel.release
|
從這裏可以看出,不同 flavor 的 package name 被 applicationId 替換掉了,而且同一個模擬器上可以同時安裝以上三個應用。
下面我們再看看 AndroidManifest.xml 中發生了什麼變化。這裏需要用到 aapt 來查看 AndroidManifest.xml 的信息:
1 2 | //輸出 apk 的 AndroidManifest.xml 文件的信息 aapt dump xmltree ***.apk AndroidManifest.xml |
關於 aapt 使用的更多用法,可以閱讀這篇博文:使用 aapt 查看 apk 的各種信息
下面是 flavors_dev 版本的信息,可以看出 Java 源文件的包名並沒有發生改變,而 package 屬性的值被替換爲 applicationId了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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 打包的時,動態合併屬性,將 package 替換爲 applicationId 指定的值,但並不會替換 Java 文件的包名,包括生成的 R 文件(可以去對應 module 下的 build/generated 目錄下查看對應 flavor 的 R 文件)。
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 activity/service registrations, continues to be called the “package”.
補充:ApplicationId versus PackageName
替換 AndroidManifest.xml 中的屬性
這裏可以參考友盟統計 SDK 中使用的方案。該方案通過在 AndroidManifest.xml 文件中 application
標籤下指定 <mate-data>
設置佔位符來實現動態替換屬性值。
1 | <android:name="UMENG_CHANNEL" android:value="${UMENG_CHANNEL}" /> |
佔位符形如${name}
,在最終執行 AndroidManifest.xml 文件合併的時候,佔位符會被 build.gradle 中對應值取代。 build.gradle 的配置需要用到上節講到的 productFlavors 的 manifestPlaceholders
屬性, manifestPlaceholders
屬性直譯過來就是清單文件佔位符。
下面是 build.gradle
的節選代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
的寫法如下:
1 | manifestPlaceholders = [VALUE_NAME1 : "value" , VALUE_NAME2 : "value"] |
補充:關於 AndroidManifest 文件合併規則可以查看 官方文檔
替換資源文件
多渠道打包的時候可能會碰到這種情況:每個應用市場的啓動頁圖標、應用名稱可能會有點小出入,更有甚者,連佈局都不一樣。這時候我們該怎麼辦呢?
有一種解決辦法就是:在代碼裏進行判斷,根據渠道的不一樣,加載不同的圖片和佈局,這是一種解決辦法。但是當渠道有很多時,代碼就會變得很難維護,而且指定渠道用到的資源文件都會被打入所有 Apk 中。所以這個方法並不值得推薦。那麼,有什麼好的解決辦法呢?
辦法 Google 早就給我們想好了,而且相當簡單,那就是:在 main 的同級目錄下創建以渠道名命名的文件夾,然後創建資源文件(路徑要與 main 中的一致),然後打包的時候 gradle 就會自己替換或者合併資源。
例如, App 的默認 icon 路徑爲 main\res\mipmap-hdpi\ic_launcher.png
,那麼 flavors_dev的路徑就爲 flavors_dev\res\mipmap-hdpi\ic_launcher.png
,打包 flavors_dev 渠道的時候會自動替換圖片。
對於資源合併,如果在 main 下的 strings.xml 內容爲:
1
2
3
4
|
<resource>
<string
name="app_name">MultiChannel</string>
<string
name="string_merge">我是string,我暫時沒被合併</string>
</resource>
|
在 flavors_dev 下的 strings.xml 內容爲:
1 2 3 | <resource> <string name="string_merge">我是dev_string,我會把string合併</string> </resource> |
當打 flavors_dev 渠道包時,最終 strings.xml 會變成:
1
2
3
4
|
<resource>
<string
name="app_name">MultiChannel</string>
<string
name="string_merge">我是dev_string,我會把string合併</string>
</resource>
|
以上特性可以用來替換 Apk 的應用名稱和應用圖標,這比使用前面講到的佔位符方便很多。同理,替換圖片和合並顏色的原理也相似。
多渠道使用獨立簽名
多渠道打包的時候,可能每個渠道包的簽名都必須不一樣,真正做到定製化,那麼,怎麼實現每個渠道包使用指定的簽名呢?
平時我們打包的時候是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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 //} } |
而給每個渠道包指定簽名其實也差不多。
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 的配置節選:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//定義簽名屬性
signingConfigs
{
flavors_default
{
//如果簽名文件在項目的根目錄下,則可以這麼寫
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
並不能設置多個簽名
//debug
{
//
productFlavors.flavors_default.signingConfig signingConfigs.flavors_default
//
productFlavors.flavors_dev.signingConfig signingConfigs.flavors_dev
//}
}
|
下面我們來驗證下生成的包的簽名是否正確,查看簽名我們會用到如下兩個命令:
1 2 3 4 5 6 | //查看簽名文件的屬性 keytool -list -keystore 簽名文件 //查看 apk 的簽名,需要提前解壓 apk ,獲取 CERT.RSA(位於解壓目錄下 /META-INF 下) //以下命令行是在 apk 解壓目錄下執行 keytool -printcert -file META-INF/CERT.RSA |
更多 keytool 命令使用可以查看 官方文檔
首先,我們來看下 littlejie.jks 的信息:
1
2
3
4
5
6
7
8
9
10
|
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 信息
1 2 3 4 5 6 7 8 9 10 11 | lishengjiedeMacBook-Pro:apk littlejie$ keytool -printcert -file multichannel-flavors_default-release/META-INF/CERT.RSA 所有者: CN=littlejie 發佈者: 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 |
可以發現兩者的 SHA1 值是相等的。
同理,可以查看 littlejie_dev.jks 和 multichannel-flavors_dev-release.apk 的簽名信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
//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
發佈者:
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 無效,有興趣的同學可以看上述註釋掉的 debug 簽名部分配置。簡單來說,debug 簽名只能指定一個或者使用默認的 debug 簽名。
若哪位大神有解決方案,歡迎指出~
這裏再做幾點補充:
- 多渠道使用獨立簽名,打包時千萬不要使用 Android Studio 中 Build 菜單下的 Generate Signed APK,因爲當你使用這個打包的時候, Android Studio 會讓你指定使用的簽名文件, so 你就等着哭吧~樓主因爲這個折騰了半天。解決方法就是使用 gradle tasks。傳送門:Android Gradle Build Tasks
- 鑑於第一點中的傳送門需要FQ,所以在這裏簡單介紹一下 Android Gradle Build Tasks的使用。
- 打全部包:
gradle assemble
- 打全部 Debug 包:
gradle assembleDebug
,可以簡寫爲gradle aD
,前提是沒有相同縮寫的參數 - 打全部 Release 包:
gradle assembleRelease
,可以簡寫爲gradle aR
- 打指定 flavor 包:
gradle assemble(flavor)(Debug|Release)
- 打包完成後安裝(設備上沒有安裝該 apk ,否則會失敗,而且只能指定 flavor ,不然也會失敗):
gradle install(flavor)(Debug|Release)
- 打包前先 clean 一下(在測試的時候很必要,如果不 clean 的話,可能會導致某些小修改不會及時打入新包):
gradle clean assembleDebug
利用 Gradle 修改構建版本號
樓主表示對 Groovy 不是很熟,所以利用 Gradle 自動修改構建版本這個就先留着,我先去研究幾天~
總結
以上就是自己在使用 Gradle 實現 Android 多渠道打包時碰到的問題, Android 官方關於使用 Gradle 的文檔已經很詳細了,自己總結的只是一點皮毛,有時間要去自習研讀下。
工作一年多,愣是沒有寫博客做總結,好多東西都是用過就忘,下次要用再找,沒有成體系的 Android 知識結構,對工資不滿意,可就連想跳槽面試都沒底氣。這次寫這篇博客畫了思維導圖,自以爲邏輯清晰了,可是真正要把這些東西講述清楚,還真是一件麻煩的事~看來,自己還有很長的路要走~
這段時間自己也在思考,是轉行還是去考事業編制,還是繼續做 Android。轉行,除了編程自己好像別的什麼也不會,當然自己編程也做的不怎麼好。考事業編制,這個可以考慮,畢竟再很多人眼裏這是個旱澇保收的職業。繼續做 Android ,這個也不錯,除了每次都花大把時間用來改 UI,別的都還不錯(吐槽產品)。
話說,有沒有什麼工作,自由、上班時間少、工資高的?當然沒有,至少現階段的自己是接觸不到的,所以,騷年,還是努力吧!多讀書、多看報、多運動,少吃零食多睡覺~
恩,算是對工作一年多的總結也是吐槽~
讀萬卷書,行萬里路~