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

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

Walle 和 微信資源混淆沖突
問題描述:
- Gradle Plugin 2.2 以上打包默認使用 V1和V2簽名打包兼容版本;
- 使用 AndResGuard 工具對上述安裝包進行資源混淆,因為該工具中僅集成了 jarsigner 沒有集成 apksigner,所以最終重簽的包只是 V1 簽名包;
- Walle 打多渠道包,首先要校驗簽名包中是否進行過 V2簽名,沒有則直接編譯失敗。
問題定位:
Walle 打多渠道包僅支持含有V2簽名的包,但 AndResGuard 沒有集成新的簽名工具 apksigner,只能得到 V1 簽名的混淆包,如此兩者不能同時執(zhí)行。
問題解決:
1. 得到資源混淆后的包后,使用美團第一代打包方式向 META-INF 中寫入空文件的方式打多渠道包,這種方式得到的包是 V1 簽名;
2. 對混淆后的V1包 使用 apksigner 重新簽名,重簽主要要先對齊后簽名。
為了練一下 Python 腳本,使用第一種方式,改為第一代多渠道打包方式進行打包。具體打包腳本是配合 assembleRelease Task 執(zhí)行的,這里會用到 groovy 腳本和 python 腳本。
groovy 腳本時配合assembleRelease任務(wù)在該任務(wù)執(zhí)行完成后開啟另一個 task 對得到的 release 包進行資源混淆;python 腳本主要是用來進行多渠道打包,對資源混淆后的 apk 解壓縮后向其 META-INF 目錄下寫入一個以渠道名命名的空文件。
andResGuard.gradle 完整腳本代碼如下:
// 美團多渠道混淆方案V1簽名實現(xiàn):
// 原理:向apk文件的 META-INF 目錄下寫一個空文件,可以不用重新簽名應(yīng)用;
// 通過為不同的渠道應(yīng)用添加不同的空文件,可以標識一個渠道;
// 好處:因為不用重新簽名,所以節(jié)省了解壓重簽名的時間;另外不同于原始的多渠道打包方案,build每次只打一個渠道包,構(gòu)建時間很長
// 壞處:對于使用V2簽名得到的apk進行修改,v2簽名就會失效,此時安裝到 7.0手機,會直接提示:檢測使用V2簽名,但是沒有這樣的簽名;6.0手機安裝時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í)行的都是使用通過命令行執(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 日志就會看到先會得到初始的 release 包,然后調(diào)用 resourceProguardTask("release") 執(zhí)行對原始包進行資源混淆,這里也可以看到混淆時的日志,執(zhí)行完畢后,腳本會執(zhí)行 buildChannelApkTask("release") 對混淆后的包開始打多渠道包,也即是按照第一代V1簽名方式,向 META_INF 目錄下寫入空文件方式。
Tips
- 這里的方案只是暫時的,V2簽名是趨勢,這種放棄 V2 簽名的方式并不建議,可以等到微信資源混淆提供集成了新一代簽名工具的 打包工具,或者自己直接對混淆后的文件再使用 新一代簽名工具重簽名和打包得到 V2 再使用 walle 打多渠道包。