[TOC]
打包流程
前言
我們每一個(gè)產(chǎn)品中一般都是由一位同事來(lái)負(fù)責(zé)打包工作的,其他同學(xué)一般是不需要關(guān)心具體的流程的。然而掌握打包的知識(shí)對(duì)我們每一個(gè)人都是必要的,以備不時(shí)之需。
一般打包有兩種方式:
- 通過(guò)開(kāi)發(fā)工具提供的
build入口

- 使用命令行打包
gradle tasksName
不論哪種方式,背后執(zhí)行的都是一整套Google提供的構(gòu)建系統(tǒng)。下面我們就來(lái)一起看一下這一具體流程。
構(gòu)建流程

上面這個(gè)圖是Android官方提供的打包簡(jiǎn)略流程圖。清晰地展示了一個(gè)Android Project經(jīng)過(guò)編譯和打包后生成apk文件,然后再經(jīng)過(guò)簽名,就可以安裝到設(shè)備上。
具體步驟如下:
- 編譯器將您的源代碼轉(zhuǎn)換成DEX(Dalvik Executable) 文件,將資源文件轉(zhuǎn)換成已編譯資源。
- APK打包器將DEX文件和已編譯資源合并成單個(gè)APK。不過(guò),必須先將APK簽名,才能將應(yīng)用安裝并部署到Android設(shè)備上。
- APK打包器使用密鑰簽署APK:
a. 如果構(gòu)建的APK是debug版本,那么將使用調(diào)試密鑰簽名,Android會(huì)默認(rèn)提供一個(gè)debug的密鑰。
b. 如果構(gòu)建的是release版本,會(huì)使用發(fā)布版本的密鑰簽名。要生成自己的簽名文件發(fā)布應(yīng)用可以參考Android的官方文檔。 - 在生成最終的APK文件之前還會(huì)使用
zipalign工具來(lái)優(yōu)化文件。
生成的APK文件本質(zhì)還是一個(gè)zip文件,只不過(guò)被Google強(qiáng)行修改了一下后綴名稱(chēng)而已。所以我們將APK的后綴修改成.zip就可以查看其包含的內(nèi)容了。如圖所示:

- AndroidManifest.xml
- assets文件夾:這里面的資源是不經(jīng)過(guò)編譯原樣打包進(jìn)來(lái)的
- classes.dex:這個(gè)是Java的字節(jié)碼文件,Android會(huì)將所有的
class文件全部放到這一個(gè)文件里。 - META-INFO:關(guān)于簽名的信息存放,應(yīng)用安裝驗(yàn)證簽名的時(shí)候會(huì)驗(yàn)證該文件里面的信息
-res:資源文件,是被編譯過(guò)的。raw和圖片是保持原樣的,但是其他的文件會(huì)被編譯成二進(jìn)制文件。 - resources.arsc:保存資源文件的索引,由aapt生成
詳細(xì)流程

1. 使用aapt進(jìn)行資源預(yù)編譯。
打包的第一步就是將資源進(jìn)行預(yù)編譯,資源文件包括res文件夾下面的所有文件。在編譯的這一步,使用的是aapt工具進(jìn)行的,編譯后將會(huì)生成我們常見(jiàn)的R文件。R文件中是資源對(duì)應(yīng)的ID。每一個(gè)ID都是根據(jù)特殊規(guī)則來(lái)生成的,由4個(gè)字節(jié)組成,用十六進(jìn)制表示:0x PPTTEEEE。其中PP(Package ID)代表的是資源命名空間,TT( Type ID)是資源類(lèi)型標(biāo)識(shí),資源類(lèi)型有animator、anim、color、drawable、layout、menu、raw、string和xml等等若干種。EEEE(Entry ID)則代表的是每一個(gè)資源所出現(xiàn)的順序,呈現(xiàn)自增長(zhǎng)態(tài)。

2. AIDL文件編譯
AIDL 全稱(chēng)Android Interface definition language。在設(shè)計(jì)進(jìn)程之間通信的時(shí)候需要用定義aidl文件,統(tǒng)一通信接口。Android打包時(shí)會(huì)將aidl文件轉(zhuǎn)化為一個(gè)同名的java接口。
3. Java文件編譯
這一步就是標(biāo)準(zhǔn)的Java編譯的過(guò)程,將.java編譯成.class文件。
4. 生成.dex文件
將所有的.class文件(包括第三方庫(kù)的.class文件)轉(zhuǎn)化成Dalvik文件,即.dex文件。
雖然Android使用Java語(yǔ)言編程。但是Android并不使用標(biāo)準(zhǔn)的Java虛擬機(jī)執(zhí)行java code。Android有自己的虛擬機(jī) Dalvik以及5.0以上的ART。 可以大致了解一下Dalvik虛擬機(jī)和傳統(tǒng)虛擬機(jī)的區(qū)別:
- Dalvik虛擬機(jī)占用內(nèi)存更少
- JVM是基于Stack的(Stack based),DVM是基于Register的(Register based). 更進(jìn)一步說(shuō),JVM將局部變量存在stack中,而DVM將變量保存在register中。因此標(biāo)準(zhǔn)java虛擬機(jī)需要更多的指令集,而DVM的指令集更少,另一方面DVM需要用寄存器編碼指令的source 和 destination ,因此Dalvik的一條指令更長(zhǎng)。
- 在JVM運(yùn)行時(shí),會(huì)根據(jù)需要去load每個(gè)class文件。而Dalvik中所有的class都在一個(gè)dex文件中。

這里有一個(gè)潛在的危險(xiǎn),由于dex設(shè)計(jì)上的原因,單個(gè)dex內(nèi)最多能引用的方法數(shù)上限為65536,這個(gè)數(shù)字對(duì)于當(dāng)今很多APP來(lái)說(shuō)都已經(jīng)不夠用了。不過(guò)Google也早早就放出了解決方案。這里就不詳細(xì)討論了,有興趣了解可以戳這里。
5. 打包APK
這一步就是講所有的資源文件,dex文件一起打包,生成APK文件。
6. 簽名
第五步生成的APK文件是不能直接安裝到Android手機(jī)上面的,必須經(jīng)過(guò)簽名后才行。
$ keytool -genkey -v -keystore my-release-key.keystore-alias alias_name -keyalg RSA -keysize 2048 -validity 10000
生成keytool生成一個(gè)keyStore,有效期是10000天。
$ jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1-keystore my-release-key.keystore my_application.apk alias_name
通過(guò)jarsinger來(lái)對(duì)APK簽名
7. 對(duì)齊
這是打包的最后一步。對(duì)apk包進(jìn)行對(duì)齊處理,工具為zipalign。對(duì)齊處理即使得所有資源文件距離文件起始偏移為4字節(jié)的整數(shù)倍,這樣通過(guò)內(nèi)存映射訪(fǎng)問(wèn)apk文件時(shí)處理速度更快。
zipalign -c -v 4 application.apk
以上就是Android打包的完成過(guò)程。
多渠道打包
配置gradle實(shí)現(xiàn)多渠道打包
每當(dāng)應(yīng)用發(fā)布一個(gè)新的版本的時(shí)候,我們會(huì)分發(fā)到每一個(gè)應(yīng)用市場(chǎng)中去,比如,360手機(jī)助手,小米應(yīng)用市場(chǎng),華為應(yīng)用市場(chǎng)等。為了能夠統(tǒng)計(jì)每個(gè)應(yīng)用市場(chǎng)的下載量,活躍量我們必須用一個(gè)標(biāo)記來(lái)區(qū)分這些不同市場(chǎng)分發(fā)下去的應(yīng)用,渠道號(hào)也就應(yīng)運(yùn)而生。隨著渠道的不斷增加,需要生成的渠道包也就越來(lái)越多。
在打包的過(guò)程中,我們一般都是使用gradle來(lái)進(jìn)行的。gradle為我們的打包提高了很多的便利,多渠道打包也可以輕松實(shí)現(xiàn)。
- 首先在
AndroidManifest.xml文件中定義一個(gè)meta-data
<meta-data
android:name="CHANNEL"
android:value="${CHANNEL_VALUE}" />
- 然后在gradle文件中設(shè)置一下productFlavors
android {
productFlavors {
xiaomi {
manifestPlaceholders = [CHANNEL_VALUE: "xiaomi"]
}
_360 {
manifestPlaceholders = [CHANNEL_VALUE: "_360"]
}
baidu {
manifestPlaceholders = [CHANNEL_VALUE: "baidu"]
}
wandoujia {
manifestPlaceholders = [CHANNEL_VALUE: "wandoujia"]
}
}
}
productFlavors是為了在同一個(gè)項(xiàng)目中創(chuàng)建應(yīng)用的不同版本。具體的配置信息可以看官方說(shuō)明。
- 執(zhí)行
gradle aS就可以將所有的渠道包輸出了。
gradle實(shí)現(xiàn)多渠道打包的缺點(diǎn)
雖然gradle配置多渠道打包很簡(jiǎn)單,也很方便,但是這種方式存在一個(gè)致命的缺陷,那就是費(fèi)時(shí)間。因?yàn)?code>AndroidManifest.xml文件被修改過(guò)了,所以所有的包都必須重新編譯簽名。一般來(lái)說(shuō)100個(gè)渠道包就要至少一個(gè)小時(shí)的時(shí)間,這一個(gè)小時(shí)5杯咖啡都不夠等的。更要命的是萬(wàn)一哪里需要微調(diào)一下代碼或者文案,那么不好意思,一切又得重頭來(lái)。這就很麻煩了,所以有沒(méi)有什么方法可以快速完成打包呢?我們繼續(xù)往下看。
多渠道快速打包
快速打包方案Version_1.0
如上所說(shuō),我們?nèi)サ叫畔⒅皇切薷牧艘幌耺anifest文件里面的一個(gè)meta-data的值而已,有沒(méi)有什么辦法可以不需要重新構(gòu)建代碼呢?答案是肯定的。我們可以使用apktool,反編譯我們的APK文件。
apktool d yourApkName build
經(jīng)過(guò)解碼后,我們會(huì)得到如下文件:

我們發(fā)現(xiàn)我們需要修改的manifest文件就在里面,所以通過(guò)命令可以修改下他的內(nèi)容,然后重新打包,就可以生成一個(gè)全新的渠道包了,省去了重新編譯構(gòu)建代碼的過(guò)程。使用一下Python腳本,將manifest文件里面channel信息進(jìn)行替換。
import re
def replace_channel(channel, manifest):
pattern = r'(<meta-data\s+android:name="channel"\s+android:value=")(\S+)("\s+/>)'
replacement = r"\g<1>{channel}\g<3>".format(channel=channel)
return re.sub(pattern, replacement, manifest)
然后通過(guò)apktool重新將文件夾打包生成APK。
apktool b build your_unsigned_apk
最后,使用jarsigner重新簽名apk:
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias
通過(guò)這上面的一系列過(guò)程,我們可以實(shí)現(xiàn)不重新編譯構(gòu)建項(xiàng)目就生成不同的渠道包。這會(huì)節(jié)省很多的時(shí)間。但是隨著渠道包增加,重新簽名也會(huì)占用很大一部分時(shí)間,那能不能不重新簽名呢?
分析簽名的算法后發(fā)現(xiàn),在打包過(guò)程后的META-INF文件夾下面添加空白文件是不會(huì)對(duì)簽名的結(jié)果產(chǎn)生影響的。

所以我們只要像META_INF文件夾里面寫(xiě)入空白的文件來(lái)標(biāo)識(shí)渠道號(hào)就可以了。
通過(guò)Python腳本像APK文件中寫(xiě)入渠道:
import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/mtchannel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)
執(zhí)行后會(huì)在META-INF文件夾下面生成一個(gè)空白文件:

然后我們?cè)陧?xiàng)目中去讀取這個(gè)空白文件:
public static String getChannel(Context context) {
ApplicationInfo appinfo = context.getApplicationInfo();
String sourceDir = appinfo.sourceDir;
String ret = "";
ZipFile zipfile = null;
try {
zipfile = new ZipFile(sourceDir);
Enumeration<?> entries = zipfile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
String entryName = entry.getName();
if (entryName.startsWith("mtchannel")) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
return ret.substring(split[0].length() + 1);
} else {
return "";
}
}
這樣每生成一個(gè)渠道包只需要復(fù)制一下APK,然后添加一個(gè)空態(tài)的文件到META-INF下面就可以了,這樣100個(gè)渠道包一分鐘之內(nèi)就可以搞定了。
具體的原因可以看下這里。
快速打包方案Version_2.0
上面的方案基本上已經(jīng)比較完美的解決我們打包的問(wèn)題了,然而好景不長(zhǎng),Google在Android 7.0中更新了應(yīng)用的簽名算法-APK Signature Scheme v2,它是一個(gè)對(duì)全文件進(jìn)行簽名的方案,能提供更快的應(yīng)用安裝時(shí)間、對(duì)未授權(quán)APK文件的更改提供更多保護(hù),在默認(rèn)情況下,Android Gradle 2.2.0插件會(huì)使用APK Signature Scheme v2和傳統(tǒng)簽名方案來(lái)簽署你的應(yīng)用。因?yàn)槭菍?duì)全文件進(jìn)行簽名的,所以之前的添加空白文件的方案就沒(méi)有作用了。
不過(guò)目前這個(gè)方案還不是強(qiáng)制性的,我們可以選擇在gradle配置文件中將其關(guān)閉:
android {
defaultConfig { ... }
signingConfigs {
release {
storeFile file("myreleasekey.keystore")
storePassword "password"
keyAlias "MyReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}
}
那么新的簽名方案對(duì)已有的渠道生成方案有什么影響呢?下圖是新的應(yīng)用簽名方案和舊的簽名方案的一個(gè)對(duì)比:

新的簽名方案會(huì)在ZIP文件格式的 Central Directory 區(qū)塊所在文件位置的前面添加一個(gè)APK Signing Block區(qū)塊,下面按照Z(yǔ)IP文件的格式來(lái)分析新應(yīng)用簽名方案簽名后的APK包。
整個(gè)APK(ZIP文件格式)會(huì)被分為以下四個(gè)區(qū)塊:
- Contents of ZIP entries(from offset 0 until the start of APK Signing Block)
- APK Signing Block
- ZIP Central Directory
- ZIP End of Central Directory
新應(yīng)用簽名方案的簽名信息會(huì)被保存在區(qū)塊2(APK Signing Block)中, 而區(qū)塊1(Contents of ZIP entries)、區(qū)塊3(ZIP Central Directory)、區(qū)塊4(ZIP End of Central Directory)是受保護(hù)的,在簽名后任何對(duì)區(qū)塊1、3、4的修改都逃不過(guò)新的應(yīng)用簽名方案的檢查。
之前的渠道包生成方案是通過(guò)在META-INF目錄下添加空文件,用空文件的名稱(chēng)來(lái)作為渠道的唯一標(biāo)識(shí),之前在META-INF下添加文件是不需要重新簽名應(yīng)用的,這樣會(huì)節(jié)省不少打包的時(shí)間,從而提高打渠道包的速度。但在新的應(yīng)用簽名方案下META-INF已經(jīng)被列入了保護(hù)區(qū)了,向META-INF添加空文件的方案會(huì)對(duì)區(qū)塊1、3、4都會(huì)有影響,新應(yīng)用簽名方案簽署的應(yīng)用經(jīng)過(guò)我們舊的生成渠道包方案處理后,在安裝時(shí)會(huì)報(bào)以下錯(cuò)誤:
Failure [INSTALL_PARSE_FAILED_NO_CERTIFICATES:
Failed to collect certificates from base.apk: META-INF/CERT.SF indicates base.apk is signed using APK Signature Scheme v2,
but no such signature was found. Signature stripped?]
區(qū)塊1,3,4是受保護(hù)的,任何的修改都會(huì)引起簽名的不一致,但是區(qū)塊2是不受保護(hù)的,所以能不能在區(qū)塊2上面找到解決辦法呢?首先看一下區(qū)塊2的文件結(jié)構(gòu):

區(qū)塊2中APK Signing Block是由這幾部分組成:2個(gè)用來(lái)標(biāo)示這個(gè)區(qū)塊長(zhǎng)度的8字節(jié) + 這個(gè)區(qū)塊的魔數(shù)(APK Sig Block 42)+ 這個(gè)區(qū)塊所承載的數(shù)據(jù)(ID-value)。
我們重點(diǎn)來(lái)看一下這個(gè)ID-value,它由一個(gè)8字節(jié)的長(zhǎng)度標(biāo)示+4字節(jié)的ID+它的負(fù)載組成。V2的簽名信息是以ID(0x7109871a)的ID-value來(lái)保存在這個(gè)區(qū)塊中,不知大家有沒(méi)有注意這是一組ID-value,也就是說(shuō)它是可以有若干個(gè)這樣的ID-value來(lái)組成,那我們是不是可以在這里做一些文章呢?
對(duì)于簽名的認(rèn)證過(guò)程是這樣的:
- 尋找APK Signing Block,如果能夠找到,則進(jìn)行驗(yàn)證,驗(yàn)證成功則繼續(xù)進(jìn)行安裝,如果失敗了則終止安裝
- 如果未找到APK Signing Block,則執(zhí)行原來(lái)的簽名驗(yàn)證機(jī)制,也是驗(yàn)證成功則繼續(xù)進(jìn)行安裝,如果失敗了則終止安裝

在校驗(yàn)的時(shí)候,檢驗(yàn)代碼如下:
public static ByteBuffer findApkSignatureSchemeV2Block(
ByteBuffer apkSigningBlock,
Result result) throws SignatureNotFoundException {
checkByteOrderLittleEndian(apkSigningBlock);
// FORMAT:
// OFFSET DATA TYPE DESCRIPTION
// * @+0 bytes uint64: size in bytes (excluding this field)
// * @+8 bytes pairs
// * @-24 bytes uint64: size in bytes (same as the one above)
// * @-16 bytes uint128: magic
ByteBuffer pairs = sliceFromTo(apkSigningBlock, 8, apkSigningBlock.capacity() - 24);
int entryCount = 0;
while (pairs.hasRemaining()) {
entryCount++;
if (pairs.remaining() < 8) {
throw new SignatureNotFoundException(
"Insufficient data to read size of APK Signing Block entry #" + entryCount);
}
long lenLong = pairs.getLong();
if ((lenLong < 4) || (lenLong > Integer.MAX_VALUE)) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount
+ " size out of range: " + lenLong);
}
int len = (int) lenLong;
int nextEntryPos = pairs.position() + len;
if (len > pairs.remaining()) {
throw new SignatureNotFoundException(
"APK Signing Block entry #" + entryCount + " size out of range: " + len
+ ", available: " + pairs.remaining());
}
int id = pairs.getInt();
if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {
return getByteBuffer(pairs, len - 4);
}
result.addWarning(Issue.APK_SIG_BLOCK_UNKNOWN_ENTRY_ID, id);
pairs.position(nextEntryPos);
}
throw new SignatureNotFoundException(
"No APK Signature Scheme v2 block in APK Signing Block");
}
我們可以發(fā)現(xiàn),述代碼中關(guān)鍵的一個(gè)位置是 if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {return getByteBuffer(pairs, len - 4);},通過(guò)源代碼可以看出Android是通過(guò)查找ID為 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a 的ID-value,來(lái)獲取APK Signature Scheme v2 Block,對(duì)這個(gè)區(qū)塊中其他的ID-value選擇了忽略。也就是說(shuō),在APK Signature Scheme v2中沒(méi)有看到對(duì)無(wú)法識(shí)別的ID,有相關(guān)處理的介紹。所以我們可以通過(guò)寫(xiě)入自定義的ID-Value來(lái)自定義渠道。
所以整理一下思路應(yīng)該是這樣的:
- 對(duì)新的應(yīng)用簽名方案生成的APK包中的ID-value進(jìn)行擴(kuò)展,提供自定義ID-value(渠道信息),并保存在APK中
- 在App運(yùn)行階段,可以通過(guò)ZIP的EOCD(End of central directory)、Central directory等結(jié)構(gòu)中的信息找到我們自己添加的ID-value,從而實(shí)現(xiàn)獲取渠道信息的功能
通過(guò)這個(gè)方案我們同樣可以實(shí)現(xiàn)不重新構(gòu)建不重新簽名就能生成渠道包。美團(tuán)提供了walle供我們使用,詳細(xì)使用方法點(diǎn)這里。