Android Signature V2 下的多渠道打包

相关背景

Android7.0 引入一项新的应用签名方案 APK Signnature Scheme v2(以下简称 v2 ),这种新机制使得 apk 的签名方式更加安全,默认情况下,Android Studio2.2Gradle2.2 会使用 v2 签名方案来签名 apk。如果使用 v2 这套签名方案就使得之前用的美团点评多渠道打包方案没用了,当然,美团点评在此基础上推出了新的解决方案 Walle。

需求

  • 客户端需要传一个 Channel 字段给服务器;
  • 在发包时需要使用 360 加固或者其他的加固方式给 apk 加固;
  • apk 命名需要自定义;
  • 批量打包;

解决方案

使用 v1 的签名方案

禁用掉 v2,使用 v1。打开模块级 build.gradle 文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
android{
signingConfigs{
release{
storeFile file("releasekey.keystore")
storePassword "password"
keyAlias "ReleaseKey"
keyPassword "password"
v2SigningEnabled false
}
}
}

使用 v2 的签名方案

之前的美团点评的多渠道打包方案无效,我们使用新的解决方案 Walle。如果不使用加固方案的话,直接按照 Walle 的 readme 操作即可,但是使用加固方案后,只能先加固,后签名,在通过 Walle 对这个签名后的 apk 进行多渠道打包,具体操作如下:

先在项目中引用 Walle

在位于项目的根目录 build.gradle 文件中添加 Walle Gradle 插件的依赖

1
2
3
4
5
buildscript{
dependencies{
classpath 'com.meituan.android.walle:plugin:1.1.5'
}
}

并在模块级 build.gradle 文件中 apply 这个插件,并添加上用于读取渠道号的 AAR

1
2
3
4
5
apply plugin: 'walle'

dependencies {
compile 'com.meituan.android.walle:library:1.1.5'
}

通过以下方式获取渠道信息,并上传给服务器

1
2
3
4
ChannelInfo channelInfo= WalleChannelReader.getChannelInfo(this.getApplicationContext());
if (channelInfo != null) {
String channel = channelInfo.getChannel();
}

至此,在项目中的配置就算完成了,如果想插入一些额外的信息或者要对 Walle 进行更多的配置,请参考 Walle 的 readme。配置完成后,通过 assembleRelease 命令生成使用v2签名方式的 apk。

python 工具打自定义名称的渠道包

拿这个 apk 去加固生成未签名的加固包。之后使用 python 工具去批量打自定义名称的渠道包。
channel.txt:渠道信息
config.py:配置签名信息以及 Android sdk 路径。

1
2
3
4
5
6
7
8
#Windows 下路径分割线请注意使用\\转义
keystorePath = ""
keyAlias = ""
keystorePassword = ""
keyPassword = ""

#Android SDK buidtools path , please use above 25.0+
sdkBuildToolPath = ""

MultiChannelBuildTool.py:基本思路就是遍历放置在同文件夹内的 apk 文件(将生成的未签名的加固包放置此处)。使用 v2 签名该 apk,然后将签名成功的 apk 通过 Walle 的命令行工具写入渠道等信息。

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/python  
# -*-coding:utf-8-*-
import zipfile
import shutil
import os
import sys
import platform
import config


# 获取脚本文件的当前路径
def curFileDir():
# 获取脚本路径
path = sys.path[0]
# 判断为脚本文件还是py2exe编译后的文件,
# 如果是脚本文件,则返回的是脚本的目录,
# 如果是编译后的文件,则返回的是编译后的文件路径
if os.path.isdir(path):
return path
elif os.path.isfile(path):
return os.path.dirname(path)


# 判断当前系统
def isWindows():
sysstr = platform.system()
if ("Windows" in sysstr):
return 1
else:
return 0


# 兼容不同系统的路径分隔符
def getBackslash():
if (isWindows() == 1):
return "\\"
else:
return "/"


# 获取当前目录中所有的apk源包
src_apks = []
# python3 : os.listdir()即可,这里使用兼容Python2的os.listdir('.')
for file in os.listdir('.'):
if os.path.isfile(file):
if ".apk" in file:
src_apks.append(file)

# 获取渠道列表
channel_file = 'channel.txt'
f = open(channel_file)
lines = f.readlines()
f.close()

# 当前脚本文件所在目录
parentPath = curFileDir() + getBackslash()
# config
libPath = parentPath + "lib" + getBackslash()
buildToolsPath = config.sdkBuildToolPath + getBackslash()
checkAndroidV2SignaturePath = libPath + "CheckAndroidV2Signature.jar"
walleChannelWritterPath = libPath + "walle-cli-all.jar"
keystorePath = config.keystorePath
keyAlias = config.keyAlias
keystorePassword = config.keystorePassword
keyPassword = config.keyPassword

for src_apk in src_apks:
# 原包路径
protectedSourceApkPath = parentPath + src_apk

zipalignedApkPath = protectedSourceApkPath[0: -4] + "_aligned.apk"
signedApkPath = zipalignedApkPath[0: -4] + "_signed.apk"

# 对齐
zipalignShell = buildToolsPath + "zipalign -v 4 " + protectedSourceApkPath + " " + zipalignedApkPath
os.system(zipalignShell)

# 签名
signShell = buildToolsPath + "apksigner sign --ks " + keystorePath + " --ks-key-alias " + keyAlias + " --ks-pass pass:" + keystorePassword + " --key-pass pass:" + keyPassword + " --out " + signedApkPath + " " + zipalignedApkPath
os.system(signShell)

# 检查V2签名是否正确
checkV2Shell = "java -jar " + checkAndroidV2SignaturePath + " " + signedApkPath
os.system(checkV2Shell)

# file name (with extension)
src_apk_file_name = os.path.basename(src_apk)
# 分割文件名与后缀
temp_list = os.path.splitext(src_apk_file_name)
# name without extension
src_apk_name = temp_list[0]
# 后缀名".apk "
src_apk_extension = temp_list[1]
# 创建生成目录,与文件名相关
output_dir = src_apk_name + 'android_' + "1-{channels}".format(channels=len(lines)) + '/'
# 目录不存在则创建
if not os.path.exists(output_dir):
os.mkdir(output_dir)

# 遍历渠道号并创建对应渠道号的apk文件
for line in lines:
# 获取当前渠道号,因为从渠道文件中获得带有\n,所有strip一下
target_channel = line.strip()
# 拼接对应渠道号的apk
target_apk = output_dir + src_apk_name + src_apk_extension
# 将签名后的apk拷贝建立新apk
shutil.copy(signedApkPath, target_apk)
writeChannelShell = "java -jar " + walleChannelWritterPath + " put -c " + target_channel + " " + target_apk
os.system(writeChannelShell)

参考

Walle: https://github.com/Meituan-Dianping/walle
Jay-Goo: https://github.com/Jay-Goo/ProtectedApkResignerForWalle

-------------本文结束 感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!