前言:
前陣子剛好做完多渠道分包需求,剛好記錄下沾凄。之前網(wǎng)上也看了很多資料梗醇,自己也收藏了很多,主要是方便下次自己再看撒蟀。但是遇到很不幸的事情:
這個(gè)是美團(tuán)多渠道分包方案的技術(shù)連接叙谨,當(dāng)再次進(jìn)去的時(shí)候就變成這樣子。沒辦法保屯,好文章就應(yīng)該轉(zhuǎn)載記錄手负,萬一以后不見了呢?
其實(shí)姑尺,看過美團(tuán)的分包方案的文章的小伙伴竟终,也應(yīng)該明白是經(jīng)過多次迭代和技術(shù)優(yōu)化的。
本來在這篇文章講得很詳細(xì)的切蟋,但是進(jìn)不去了统捶,進(jìn)不去了!不過沒關(guān)系柄粹,那個(gè)方案在安卓7.0已經(jīng)面臨要被淘汰的風(fēng)險(xiǎn)了瘾境。咱們也不多講。想看使用的話可以參考這位道友的文章:
美團(tuán)多渠道分包方案原理
PS:該原理版權(quán)歸美團(tuán)所有镰惦,本文只做記錄,方便保存而已犬绒。
在Android 7.0(Nougat)推出了新的應(yīng)用簽名方案APK Signature Scheme v2后旺入,之前快速生成渠道包的方式(美團(tuán)Android自動(dòng)化之旅—生成渠道包)已經(jīng)行不通了,在此應(yīng)用簽名方案下如何快速生成渠道包呢凯力?
本文會(huì)對(duì)新的應(yīng)用簽名方案APK Signature Scheme v2以及新一代渠道生成工具進(jìn)行詳細(xì)深入的介紹茵瘾。
新的應(yīng)用簽名方案APK Signature Scheme v2
Android 7.0(Nougat)引入一項(xiàng)新的應(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)簽名方案來簽署你的應(yīng)用雕旨。
下面以 新的應(yīng)用簽名方案
來指APK Signature Scheme v2扮匠。
目前該方案不是強(qiáng)制性的,在 build.gradle
添加 v2SigningEnabled false
凡涩,就能使用傳統(tǒng)簽名方案來簽署我們的應(yīng)用(見下面的代碼片段)棒搜。
android {
...
defaultConfig { ... }
signingConfigs {
release {
storeFile file("myreleasekey.keystore")
storePassword "password"
keyAlias "MyReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}
}
但新的應(yīng)用簽名方案有著良好的向后兼容性,能完全兼容低于Android 7.0(Nougat)的版本活箕。對(duì)比舊簽名方案力麸,它有更快的驗(yàn)證速度和更安全的保護(hù),因此新的應(yīng)用簽名方案可能會(huì)被采納成一個(gè)強(qiáng)制配置育韩,筆者認(rèn)為現(xiàn)在有必要對(duì)現(xiàn)有的渠道包生成方式進(jìn)行檢查克蚂、升級(jí)或改造來支持新的應(yīng)用簽名方案。
新的簽名方案對(duì)已有的渠道生成方案有什么影響呢筋讨?下圖是新的應(yīng)用簽名方案和舊的簽名方案的一個(gè)對(duì)比:
新的簽名方案會(huì)在ZIP文件格式的 Central Directory
區(qū)塊所在文件位置的前面添加一個(gè)APK Signing Block區(qū)塊埃叭,下面按照ZIP文件的格式來分析新應(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的修改都逃不過新的應(yīng)用簽名方案的檢查然想。
之前的渠道包生成方案是通過在META-INF目錄下添加空文件莺奔,用空文件的名稱來作為渠道的唯一標(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)過我們舊的生成渠道包方案處理后狠半,在安裝時(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?]
目前另外一種比較流行的渠道包快速生成方案(往APK中添加ZIP Comment)也因?yàn)樯鲜鲈颍瑹o法在新的應(yīng)用簽名方案下進(jìn)行正常工作颤难。
如果新的應(yīng)用簽名方案后續(xù)改成強(qiáng)制要求神年,那我們現(xiàn)有的生成渠道包的方式就會(huì)無法工作,那我們難道要退回到解放前行嗤,通過傳統(tǒng)的方式(例如:使用APKTool逆向工具已日、采用Flavor + BuildType等比較耗時(shí)的方案來進(jìn)行渠道包打包)來生成支持新應(yīng)用簽名方案的渠道包嗎?
如果只有少量渠道包的場(chǎng)景下栅屏,這種耗時(shí)時(shí)長還能夠勉強(qiáng)接受飘千。但是目前我們有將近900個(gè)渠道堂鲜,如果采用傳統(tǒng)方式打完所有的渠道包需要近3個(gè)小時(shí),這是不能接受的占婉。
那我們有沒有其他更好的渠道包生成方式泡嘴,既能支持新的應(yīng)用簽名方案,又能體驗(yàn)毫秒級(jí)的打包耗時(shí)呢逆济?我們來分析一下新方案中的區(qū)塊2——Block酌予。
可擴(kuò)展的APK Signature Scheme v2 Block
通過上面的描述,可以看出因?yàn)锳PK包的區(qū)塊1奖慌、3抛虫、4都是受保護(hù)的,任何修改在簽名后對(duì)它們的修改简僧,都會(huì)在安裝過程中被簽名校驗(yàn)檢測(cè)失敗建椰,而區(qū)塊2(APK Signing Block)是不受簽名校驗(yàn)規(guī)則保護(hù)的,那是否可以在這個(gè)不受簽名保護(hù)的區(qū)塊2(APK Signing Block)上做文章呢岛马?我們先來看看對(duì)區(qū)塊2格式的描述:
偏移 | 字節(jié)數(shù) | 描述 |
---|---|---|
@+0 | 8 | 這個(gè)Block的長度(本字段的長度不計(jì)算在內(nèi)) |
@+8 | n | 一組ID-value |
@-24 | 8 | 這個(gè)Block的長度(和第一個(gè)字段一樣值) |
@-16 | 16 | 魔數(shù) “APK Sig Block 42” |
區(qū)塊2中APK Signing Block是由這幾部分組成:2個(gè)用來標(biāo)示這個(gè)區(qū)塊長度的8字節(jié) + 這個(gè)區(qū)塊的魔數(shù)(APK Sig Block 42
)+ 這個(gè)區(qū)塊所承載的數(shù)據(jù)(ID-value)棉姐。
我們重點(diǎn)來看一下這個(gè)ID-value,它由一個(gè)8字節(jié)的長度標(biāo)示+4字節(jié)的ID+它的負(fù)載組成啦逆。V2的簽名信息是以ID(0x7109871a
)的ID-value來保存在這個(gè)區(qū)塊中伞矩,不知大家有沒有注意這是一組ID-value,也就是說它是可以有若干個(gè)這樣的ID-value來組成夏志,那我們是不是可以在這里做一些文章呢乃坤?
為了驗(yàn)證我們的想法,先來看看新的應(yīng)用簽名方案是怎么驗(yàn)證簽名信息的沟蔑,見下圖:
通過上圖可以看出新的應(yīng)用簽名方案的驗(yàn)證過程:
- 尋找APK Signing Block湿诊,如果能夠找到,則進(jìn)行驗(yàn)證瘦材,驗(yàn)證成功則繼續(xù)進(jìn)行安裝厅须,如果失敗了則終止安裝
- 如果未找到APK Signing Block,則執(zhí)行原來的簽名驗(yàn)證機(jī)制食棕,也是驗(yàn)證成功則繼續(xù)進(jìn)行安裝九杂,如果失敗了則終止安裝
那Android應(yīng)用在安裝時(shí)新的應(yīng)用簽名方案是怎么進(jìn)行校驗(yàn)的呢?筆者通過翻閱Android相關(guān)部分的源碼宣蠕,發(fā)現(xiàn)下面代碼段是用來處理上面所說的ID-value的:
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");
}
上述代碼中關(guān)鍵的一個(gè)位置是 if (id == APK_SIGNATURE_SCHEME_V2_BLOCK_ID) {return getByteBuffer(pairs, len - 4);}
,通過源代碼可以看出Android是通過查找ID為 APK_SIGNATURE_SCHEME_V2_BLOCK_ID = 0x7109871a
的ID-value甥捺,來獲取APK Signature Scheme v2 Block抢蚀,對(duì)這個(gè)區(qū)塊中其他的ID-value選擇了忽略。
在APK Signature Scheme v2中沒有看到對(duì)無法識(shí)別的ID镰禾,有相關(guān)處理的介紹皿曲。
當(dāng)看到這里時(shí)唱逢,我們可不可以設(shè)想一下,提供一個(gè)自定義的ID-value并寫入該區(qū)域屋休,從而為快速生成渠道包服務(wù)呢坞古?
怎么向ID-value中添加信息呢?通過閱讀ZIP的文件格式和APK Signing Block格式的描述劫樟,筆者通過編寫下面的代碼片段進(jìn)行驗(yàn)證痪枫,發(fā)現(xiàn)通過在已經(jīng)被新的應(yīng)用簽名方案簽名后的APK中添加自定義的ID-value,是不需要再次經(jīng)過簽名就能安裝的叠艳,下面是部分代碼片段奶陈。
public void writeApkSigningBlock(DataOutput dataOutput) {
long length = 24;
for (int index = 0; index < payloads.size(); ++index) {
ApkSigningPayload payload = payloads.get(index);
byte[] bytes = payload.getByteBuffer();
length += 12 + bytes.length;
}
ByteBuffer byteBuffer = ByteBuffer.allocate(Long.BYTES);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.putLong(length);
dataOutput.write(byteBuffer.array());
for (int index = 0; index < payloads.size(); ++index) {
ApkSigningPayload payload = payloads.get(index);
byte[] bytes = payload.getByteBuffer();
byteBuffer = ByteBuffer.allocate(Integer.BYTES);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.putInt(payload.getId());
dataOutput.write(byteBuffer.array());
dataOutput.write(bytes);
}
...
}
新一代渠道包生成工具
到這里為止一個(gè)新的渠道包生成方案逐步清晰了起來,下面是新一代渠道包生成工具的描述:
- 對(duì)新的應(yīng)用簽名方案生成的APK包中的ID-value進(jìn)行擴(kuò)展附较,提供自定義ID-value(渠道信息)吃粒,并保存在APK中
- 而APK在安裝過程中進(jìn)行的簽名校驗(yàn),是忽略我們添加的這個(gè)ID-value的拒课,這樣就能正常安裝了
- 在App運(yùn)行階段徐勃,可以通過ZIP的
EOCD(End of central directory)
、Central directory
等結(jié)構(gòu)中的信息(會(huì)涉及ZIP格式的相關(guān)知識(shí)早像,這里不做展開描述)找到我們自己添加的ID-value僻肖,從而實(shí)現(xiàn)獲取渠道信息的功能
新一代渠道包生成工具完全是基于ZIP文件格式和APK Signing Block存儲(chǔ)格式而構(gòu)建,基于文件的二進(jìn)制流進(jìn)行處理扎酷,有著良好的處理速度和兼容性檐涝,能夠滿足不同的語言編寫的要求,目前筆者采用的是Java+Groovy開發(fā)法挨, 該工具主要有四部分組成:
- 用于寫入ID-value信息的Java類庫
- Gradle構(gòu)建插件用來和Android的打包流程進(jìn)行結(jié)合
- 用于讀取ID-value信息的Java類庫
- 用于供
com.android.application
使用的讀取渠道信息的AAR
這樣谁榜,每打一個(gè)渠道包只需復(fù)制一個(gè)APK,然后在APK中添加一個(gè)ID-value即可凡纳,這種打包方式速度非城灾玻快,對(duì)一個(gè)30M大小的APK包只需要100多毫秒(包含文件復(fù)制時(shí)間)就能生成一個(gè)渠道包荐糜,而在運(yùn)行時(shí)獲取渠道信息只需要大約幾毫秒的時(shí)間巷怜。
這個(gè)項(xiàng)目我們?nèi)∶麨閃alle(瓦力),已經(jīng)開源暴氏,項(xiàng)目的Github地址是: https://github.com/Meituan-Dianping/walle (求Issue延塑、PR、Star)答渔。希望業(yè)內(nèi)有類似需求的團(tuán)隊(duì)能夠在APK Signature Scheme V2簽名下愉快地生成渠道包关带,同時(shí)也期待大家一起對(duì)該項(xiàng)目進(jìn)行完善和優(yōu)化。
總結(jié)
以上就是我們對(duì)新的應(yīng)用簽名方案進(jìn)行的分析沼撕,并根據(jù)它所帶來的文件存儲(chǔ)格式上的變化宋雏,找到了可以利用的ID-value芜飘,然后基于這個(gè)ID-value來構(gòu)建我們新一代渠道包生成工具。
新一代渠道包生成工具能夠滿足新應(yīng)用簽名方案對(duì)安全性的要求磨总,同時(shí)也能滿足對(duì)渠道包打包時(shí)間的要求嗦明,至此大家生成渠道包的方式需要升級(jí)了!
懶人工具
哈哈蚪燕,根據(jù)開源項(xiàng)目walle娶牌,在項(xiàng)目中實(shí)際應(yīng)用是通過命令行的方式來實(shí)現(xiàn)。在代碼中只是將walle的讀取channel的代碼集成到項(xiàng)目中了而已邻薯,寫入部分是通過命令行的方式寫入裙戏。既減少對(duì)三方庫的依賴,也方便不是開發(fā)人員同事使用厕诡。在下做了個(gè)小小的懶人工具累榜,方便自己偷懶。相關(guān)原理是批命令處理灵嫌,可自行百度查閱資料壹罚。
關(guān)于walle命令行的使用可以參考這里:
walle/walle-cli/README.md
懶人工具原理:批命令處理
1、獲取需分包的包體寿羞,同時(shí)截取文件名
rem 輸入需要分包的apk文件
echo 請(qǐng)將apk文件拖拽進(jìn)來:
@set /p apkFile=
echo\rem 截取apk文件名
for /f "delims=" %%i in ("dir /b %apkFile%") do (
set apkFileName=%%~ni
)
echo apk文件名稱:%apkFileName%
echo\
2猖凛、獲取簽名文件信息
rem 獲取簽名文件信息
@CALL %currentWorkPath%sign\readconfig keyAlias keyAliasValue
@CALL %currentWorkPath%sign\readconfig storeFile storeFileValue
@CALL %currentWorkPath%sign\readconfig storePassword storePasswordValue
@CALL %currentWorkPath%sign\readconfig keyPassword keyPasswordValue
echo\
3、執(zhí)行包體的簽名(使用V2簽名)
rem apk執(zhí)行簽名(使用V2簽名)
@call java -jar %currentWorkPath%lib\apksigner.jar sign --ks %currentWorkPath%sign%storeFileValue% --ks-key-alias %keyAliasValue% --ks-pass pass:%storePasswordValue% --key-pass pass:%keyPasswordValue% --out %currentWorkPath%apkoutput%apkFileName%_sign.apk %apkFile%
if ERRORLEVEL 0 (
echo 簽名成功,簽名包體路徑:
echo %currentWorkPath%apkoutput%apkFileName%_sign.apk
)
echo\
4绪穆、檢測(cè)當(dāng)前包體的簽名方式
rem 檢測(cè)apk包體的簽名方式
@call java -jar %currentWorkPath%lib\apksigner.jar verify -v %currentWorkPath%apkoutput%apkFileName%_sign.apk
echo\
5辨泳、美團(tuán)渠道分包命令
rem 美團(tuán)渠道分包
@call java -jar %currentWorkPath%lib\walle-cli-all.jar batch -f %currentWorkPath%channel %currentWorkPath%apkoutput%apkFileName%_sign.apk %currentWorkPath%apkoutput\apks
PS:有優(yōu)化的地方可自行優(yōu)化。
使用條件
1玖院、適用于windows系統(tǒng)
2菠红、需安裝和配置Java環(huán)境(自行百度安裝)
使用說明
1、在channel中配置需打包的渠道id
示例說明:
meituan # 美團(tuán)
samsungapps #三星
hiapk
anzhi
xiaomi # 小米
91com
2难菌、在sign目錄中配置簽名文件信息:
在config.ini文件配置應(yīng)用簽名,同時(shí)將簽名文件放到該目錄下
注意:config.ini配置簽名信息時(shí)试溯,第一行留一行空白
示例說明:
keyAlias=xxx
keyPassword=xxx
storeFile=xxx
storePassword=xxx
3、雙擊運(yùn)行start.bat后郊酒,拖入需簽名包體apk即可
效果圖:
項(xiàng)目地址:
BuildChannelApkTool
后記
1遇绞、關(guān)于jdk版本問題:
V2方式的簽名是android7.0后才出來的,所以只有版本>25的SDK\build-tools\中才能找到對(duì)應(yīng)的apksigner.jar燎窘。 但是實(shí)際應(yīng)用中發(fā)現(xiàn)摹闽,這個(gè)版本的jar要求jdk1.8的版本,安裝jdk1.7會(huì)報(bào)錯(cuò)褐健。
解決方式:java環(huán)境安裝為jdk1.8的就好了钩骇。你說它咋就好了呢。