這里主要說(shuō)一下同時(shí)使用 微信資源混淆以及同時(shí)使用 walle 打多渠道包 遇到的坑,以及如何解決這個(gè)問(wèn)題的。如果只需要知道如何通過(guò)向 META-INF 目錄下寫(xiě)入代表空文件方式打多渠道包的方式可以直接看最后的腳本代碼部分。
之前在項(xiàng)目中進(jìn)行 apk 瘦身時(shí),接觸到使用 微信資源混淆可以有效較小 apk 的大小,于是項(xiàng)目中在打包時(shí)使用了微信資源混淆結(jié)合 7zip 對(duì) apk 進(jìn)行極度壓縮,得出的 apk 文件確實(shí)變小了很多。
后來(lái)在發(fā)版時(shí)需要打渠道包,用到的一直是 walle 打包。美團(tuán)第二代多渠道打包方式,適應(yīng)了 Android 7.0 新簽名特性,所以僅針對(duì)能進(jìn)行 V2 簽名的 apk 文件才能打多渠道包。
為什么說(shuō)微信資源混淆和walle打多渠道包會(huì)遇到一些坑呢?這里會(huì)涉及到Android 打包簽名的一些問(wèn)題,先簡(jiǎn)單提一下微信資源混淆流程、walle 打多渠道包流程以及Android簽名機(jī)制。
微信資源混淆AndResGuard
思路:主要修改 resource.arsc,將長(zhǎng)路徑改為短路徑,然后重新打包 apk。
作用:
- 混淆資源 ID 長(zhǎng)度(res/drawable/icon -> r/s/a.png),使apktool反編譯更難;
- 減少 apk 大小,可以減少 resources.arsc 和 package 大??;
- 支持 7zip 重新打包,開(kāi)啟7z深度壓縮能很大程度減少安裝包體積。
參考:
https://github.com/shwenzhang/AndResGuard http://dev.qq.com/topic/59152309ca95d00d727ba756
在項(xiàng)目中如何使用 AndResGuard 呢?github 上提供了兩種方式:
- 使用 AndResGuard plugin,在 gradle 文件中配置資源混淆參數(shù);執(zhí)行命令:./gradlew resguardRelease ,最終得到的包是進(jìn)行資源混淆后的包。
- 使用提供的 resourceproguard.jar 在命令行下打包,也即是對(duì) assembleRelease 得到的apk文件在命令行下執(zhí)行:java -jar resourceproguard.jar input.apk 等命令得到混淆后的包。
Tips:github 文檔寫(xiě)得比較詳細(xì); issues 中有很多問(wèn)題值得一看。
美團(tuán)第二代多渠道打包Walle
walle主要針對(duì)新的簽名方案而出的一種新多渠道打包方式。
首先對(duì)比下兩種簽名方案:

功能描述:
- 對(duì)V2簽名包中的 ID-value 進(jìn)行擴(kuò)展,提供自定義 ID-value(渠道信息),保存在 apk包中;
- apk 安裝過(guò)程進(jìn)行的簽名校驗(yàn),會(huì)忽略 V2Block 中的其他 ID-value,如此就能正常安裝;
- 在運(yùn)行階段,通過(guò)讀取 ZIP 等結(jié)構(gòu)的信息找到自定義的 ID-value,便可以獲取渠道信息。
每到一個(gè)渠道包,只需要向 APK 中添加一個(gè) ID-value 即可,速度很快。
參考:
https://tech.meituan.com/android-apk-v2-signature-scheme.html
針對(duì)V2簽名方式第一代多渠道打包方式存在的問(wèn)題如下:
新的簽名方案 簽名信息會(huì)保存在區(qū)塊2(APK Signing Block)中,其他三塊區(qū)域是受保護(hù)的,在簽名后任何對(duì)這三塊的修改都無(wú)法越過(guò)新應(yīng)用簽名方案的檢查;
第一代渠道包生成方案是在 META-INF 目錄下添加空文件,用空文件作為渠道的唯一標(biāo)識(shí)。在新的簽名方案下,META-INF 目錄是在保護(hù)區(qū)內(nèi),添加空文件對(duì)區(qū)塊1, 3, 4均有影響,也即是對(duì) V2 包進(jìn)行了修改,V2簽名無(wú)效,安裝會(huì)出錯(cuò)。
Android 7.0 新簽名機(jī)制
APK V1 簽名
- 簽名工具
兩種:jarsigner 或 signapk,Android Studio 默認(rèn) signapk。
區(qū)別:主要是證書(shū)和密鑰存儲(chǔ)格式不同,前者通過(guò) Java KeyStore (.jks / .keystore 文件)格式,后者分別用 .pem 和 .pk8 格式存儲(chǔ)證書(shū)和密鑰。
注:無(wú)論哪種簽名工具,最終在 META-INF 目錄下生成三個(gè)文件:MANIFEST.MF, CERT.SF, CERT.RSA (若 jarsigner 簽名,則 .sf 和 .rsa 文件名會(huì)根據(jù) alias 來(lái)定)。
APK V2 簽名:
簽名工具
apksigner, Android 7.0之后出現(xiàn)的簽名工具,可以使用 apksigner.jar 工具對(duì)對(duì)齊后的包進(jìn)行 V2簽名。
(僅在高于 25 版本的 SDK\build-tools\中才能找到 apksigner.jar)
注:經(jīng)過(guò) apksigner 簽名的apk同時(shí)支持 v1 和 v2簽名。V2 簽名作用
一種對(duì)全文件進(jìn)行簽名的方案 ,能提供更快的應(yīng)用安裝時(shí)間,對(duì)未授權(quán) APK 文件的更改提供更多保護(hù)。
默認(rèn)情況下,Gradle Plugin 2.2.0 會(huì)使用 V2 簽名和傳統(tǒng) V1簽名來(lái)簽署應(yīng)用。
新應(yīng)用簽名機(jī)制方案驗(yàn)證流程如下圖所示:

Walle 和 微信資源混淆沖突
問(wèn)題描述:
- Gradle Plugin 2.2 以上打包默認(rèn)使用 V1和V2簽名打包兼容版本;
- 使用 AndResGuard 工具對(duì)上述安裝包進(jìn)行資源混淆,因?yàn)樵摴ぞ咧袃H集成了 jarsigner 沒(méi)有集成 apksigner,所以最終重簽的包只是 V1 簽名包;
- Walle 打多渠道包,首先要校驗(yàn)簽名包中是否進(jìn)行過(guò) V2簽名,沒(méi)有則直接編譯失敗。
問(wèn)題定位:
Walle 打多渠道包僅支持含有V2簽名的包,但 AndResGuard 沒(méi)有集成新的簽名工具 apksigner,只能得到 V1 簽名的混淆包,如此兩者不能同時(shí)執(zhí)行。
問(wèn)題解決:
1. 得到資源混淆后的包后,使用美團(tuán)第一代打包方式向 META-INF 中寫(xiě)入空文件的方式打多渠道包,這種方式得到的包是 V1 簽名;
2. 對(duì)混淆后的V1包 使用 apksigner 重新簽名,重簽主要要先對(duì)齊后簽名。
為了練一下 Python 腳本,使用第一種方式,改為第一代多渠道打包方式進(jìn)行打包。具體打包腳本是配合 assembleRelease Task 執(zhí)行的,這里會(huì)用到 groovy 腳本和 python 腳本。
groovy 腳本時(shí)配合assembleRelease任務(wù)在該任務(wù)執(zhí)行完成后開(kāi)啟另一個(gè) task 對(duì)得到的 release 包進(jìn)行資源混淆;python 腳本主要是用來(lái)進(jìn)行多渠道打包,對(duì)資源混淆后的 apk 解壓縮后向其 META-INF 目錄下寫(xiě)入一個(gè)以渠道名命名的空文件。
andResGuard.gradle 完整腳本代碼如下:
// 美團(tuán)多渠道混淆方案V1簽名實(shí)現(xiàn):
// 原理:向apk文件的 META-INF 目錄下寫(xiě)一個(gè)空文件,可以不用重新簽名應(yīng)用;
// 通過(guò)為不同的渠道應(yīng)用添加不同的空文件,可以標(biāo)識(shí)一個(gè)渠道;
// 好處:因?yàn)椴挥弥匦潞灻?,所以?jié)省了解壓重簽名的時(shí)間;另外不同于原始的多渠道打包方案,build每次只打一個(gè)渠道包,構(gòu)建時(shí)間很長(zhǎng)
// 壞處:對(duì)于使用V2簽名得到的apk進(jìn)行修改,v2簽名就會(huì)失效,此時(shí)安裝到 7.0手機(jī),會(huì)直接提示:檢測(cè)使用V2簽名,但是沒(méi)有這樣的簽名;6.0手機(jī)安裝時(shí)OK的。
// 解決方式:改成 V1 簽名。
def rootDir = "${project.projectDir}"
def channelFile = "${rootDir}/doc/channel"
def outputDir = "${project.projectDir}/pkg/channel_release_apks"
def emptyFile = "${rootDir}/doc/temp"
def buildChannelApkTask = {
def variantName ->
println '---begin to build channels---'
// 閉包調(diào)用
def applicationId = android.defaultConfig.applicationId
def versionCode = android.defaultConfig.versionCode
def inputApk = "${project.projectDir}/build/outputs/apk/app-${variantName}.apk"
project.exec {
workingDir "${project.projectDir}"
commandLine "python", "${rootDir}/pkg/build_channels_apk.py", "$channelFile", "$inputApk", "$outputDir", "$emptyFile", "$applicationId", "$versionCode"
}
}
afterEvaluate {
tasks.withType(Task).each { task ->
task.doLast {
// 執(zhí)行 assembleRelease 打多渠道資源混淆包
if (task.name.equals("assembleRelease")) {
// 這兩句執(zhí)行的都是使用通過(guò)命令行執(zhí)行腳本代碼
resourceProguardTask("release")
buildChannelApkTask("release")
}
}
}
}
// test task
task test1 {
doLast {
buildChannelApkTask("release")
}
}
buildChannel.python 完整腳本代碼如下:
import os
import zipfile
import shutil
import sys
# only for V1 signing apk -> resource proguard apk -> get channel resource proguard apk
channelFile = sys.argv[1]
inputApk = sys.argv[2]
outputDir = sys.argv[3]
emptyFile = sys.argv[4]
applicationId = sys.argv[5]
versionCode = sys.argv[6]
def deleteDir(sourceDir):
for file in os.listdir(sourceDir):
targetFile = os.path.join(sourceDir, file)
if os.path.isfile(targetFile):
os.remove(targetFile)
def buildChannelApk(apkFile, channelFile, outputDir, emptyFile, applicationId, versionCode):
print('---begin to write empty file into META-INF---')
f = open(emptyFile, 'w')
f.close()
print('---write empty file into META-INF end---')
if (os.path.exists(outputDir)):
deleteDir(outputDir)
else:
os.mkdir(outputDir)
# read channel info from channel file
print('---begin to add channel info---')
with open(channelFile, 'r') as f:
channels = f.readlines()
for c in channels:
destApkFile = '%s/%s-%s-release-%s.apk' % (outputDir, applicationId, c.strip(), versionCode)
shutil.copy(apkFile, destApkFile) # copy the original apk to destApkFile
zipped = zipfile.ZipFile(destApkFile, 'a')
empty_channel_file = "META-INF/ycchannel_{channel}".format(channel=c.strip())
zipped.write(emptyFile, empty_channel_file)
zipped.close()
print('---add channel info end---')
buildChannelApk(inputApk, channelFile, outputDir, emptyFile, applicationId, versionCode)
在命令行直接執(zhí)行"gradle assembleRelease" ,查看 build 日志就會(huì)看到先會(huì)得到初始的 release 包,然后調(diào)用 resourceProguardTask("release") 執(zhí)行對(duì)原始包進(jìn)行資源混淆,這里也可以看到混淆時(shí)的日志,執(zhí)行完畢后,腳本會(huì)執(zhí)行 buildChannelApkTask("release") 對(duì)混淆后的包開(kāi)始打多渠道包,也即是按照第一代V1簽名方式,向 META_INF 目錄下寫(xiě)入空文件方式。
Tips
- 這里的方案只是暫時(shí)的,V2簽名是趨勢(shì),這種放棄 V2 簽名的方式并不建議,可以等到微信資源混淆提供集成了新一代簽名工具的 打包工具,或者自己直接對(duì)混淆后的文件再使用 新一代簽名工具重簽名和打包得到 V2 再使用 walle 打多渠道包。