360加固 + 美团Walle多渠道自动化打包
使用美团walle进行多渠道打包可以节省大量的时间, 但是使用中发现一个问题:
之前是使用walle打多渠道包, 然后将apk上传360进行加固; 后来发现加固之后的APP没有了渠道信息, 并且发现360加固完成后的自动重签名只有V1签名
因此需要我们自己手动重签名, 需要调整实现流程: 加固——重签名——多渠道打包
我们希望实现gradle脚本实现自动化加固、签名和多渠道打包
目前可以使用瓦力多渠道打包的Python脚本实现自动化打包, 但是使用的python2.7
版本
另一种思路是使用gradle脚本实现:
在此感谢原创作者:天子卿
附作者文章链接:https://juejin.im/post/5c825ac6f265da2db9129bd0
通过作者文章完成此demo, 如有侵权,请联系删除
首先是集成Walle
这里只简单写下Gradle插件使用方式, 如有其他需求请查看主页Meituan-Dianping/walle
配置build.gradle
在位于项目的根目录
build.gradle
文件中添加Walle Gradle插件的依赖, 如下:buildscript { dependencies { classpath 'com.meituan.android.walle:plugin:1.1.6' } }
并在当前App的
build.gradle
文件中apply这个插件,并添加上用于读取渠道号的AARapply plugin: 'walle' dependencies { compile 'com.meituan.android.walle:library:1.1.6' }配置插件
walle { // 指定渠道包的输出路径 apkOutputFolder = new File("${project.buildDir}/outputs/channels"); // 定制渠道包的APK的文件名称 apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk'; // 渠道配置文件 channelFile = new File("${project.getProjectDir()}/channel") }如何获取渠道信息
在需要渠道等信息时可以通过下面代码进行获取
String channel = WalleChannelReader.getChannel(this.getApplicationContext());
实现原理
1. 加固
加固过程: 浏览了360加固官网,整个加固过程其实很简单,主要有以下的三个步骤:
-
输入360加固平台的帐号、密码
-
将签名文件上传到加固平台
-
上传需要加固的apk文件进行加固
关键加固命令行代码如下:
/** * 360加固 * @param apk 加固的原始apk File * @param outputPath 输出目录 */ def reinforceApk(File apk,outputPath) { println "--- 360 reinforceApk start! ---" println "reinforce apk:" + apk if(apk == null || !apk.exists()) { throw new FileNotFoundException('apk is not exists and cannot reinforce') println "---360 reinforceApk throw exception and forced stop!---" } exec { commandLine "{命令执行符号}", "-c", "java -jar ${REINFORCE_JAR} -login ${REINFORCE_NAME} ${REINFORCE_PASSWORD}" commandLine "{命令执行符号}", "-c", "java -jar ${REINFORCE_JAR} -showsign" commandLine "{命令执行符号}", "-c", "java -jar ${REINFORCE_JAR} -jiagu ${apk} ${outputPath}" } println "--- 360 reinforce end! ---" }
说明: 系统环境不同,
{命令执行符号}
也会不同(Linux系统:sh ;Mac系统:bash ;windows系统:powershell);
2. 重签名
加固完成后,对加固apk进行重签名;
使用AndroidSDK中的build-tools目录下, 使用压缩对齐工具和签名工具完成重签名, 步骤如下:
-
对齐,对Apk文件进行存档对齐优化,确保所有的未压缩数据都从文件的开始位置以指定的对齐方式排列
-
签名,选择Signature V2
commandLine "{命令执行符号}","-c", "{zipalign工具的文件路径} -v -p 4 {已加固的apk文件路径} {对齐后输出的apk文件路径}" commandLine "{命令执行符号}", "-c", "{apksigner工具的文件路径} sign --ks {签名文件的位置} --ks-key-alias {alias别名} --ks-pass pass:{签名文件存储的密码} --key-pass pass:{alias密码} --out {签名后输出的apk文件} {对齐后输出的apk文件路径}"
3. 多渠道打包
签名完成后, 使用walle进行多渠道打包
平时使用walle多渠道打包,只需要在app/build.gradle下配置插件,指定渠道包的输出路径和渠道配置文件即 可,最后在Android studio的Terminal中输入./gradlew assembleReleaseChannels,任务执行完成后在指定的 输出路径下生成多个对应的渠道包。具体的流程和细节可参考官方介绍。
这种多渠道打包方式是全自动化构建,很难去干涉到构建流程,不符合我们的需求
-
在app/build.gradle配置插件时,在官方介绍中并没有找到指定源APK输入路径的方式,估计打包插件默认使用的是app/build/outputs/apk/release下的apk文件,这样就没办法对不同文件路径下的已加固apk包进行多渠道打包。
-
打包任务设置在assembleRelease之后执行,这个执行依赖封装在插件内部,外部很难修改打包任务依赖于加固任务,在加固任务之后执行。
除了上面的多渠道打包方式之后,walle还提供了另外一种多渠道打包方式,用命令行执行walle提供的walle-cli-all.jar执行打包操作,只需要一条打包命令即可完成打包。
commandLine "sh", "-c", "java -jar {walle-cli-all.jar文件路径} batch -f {渠道文件路径} {要加渠道的apk文件路径} {渠道包的输出路径}"
walle-cli-all.jar文件下载地址:官方:walle-cli-all.jar, 其他开发提供的编译版本
这里为什么会有两个版本呢! 因为发现了一个比较坑的地方官方的版本打完包会发现在系统9.0(P)下无法正常安装, 相关问题可以查看Issue, 当然你也可以自己拉取源码编译
整体流程
首先,将加固和打包操作封装成自动化操作,利用gradle脚本构建加固任务。
为了代码解耦,我们不在app/build.gradle里面实现加固任务,而是重新建一个gradle文件來实现具体的加固和多渠道打包过程,在app/build.gradle只需要通过apply from: '×××.gradle'
引用这个gradle文件即可,
当需要修改加固的一些代码逻辑时,只需要在这个gradle文件里面修改。
引入工具包。根据自己的系统环境,在加固助手网页选择对应的加固助手工具,下载后将里面的jiagu文件夹拷贝到自己项目的根目录下;
在walle-cli-jar下载链接下载jar包到自己项目中。
确定加固任务的时机。加固任务时机应该在release包生成之后,那么加固任务应该依赖于assembleRelease这个任务,并且设置在这个任务之后执行。
接下来就是我们的基本流程了
1. 找到release包, 一般在`app/build/outputs/apk/release/`路径下
2. 执行加固命令,将release包路径设置到命令中,并指定加固apk文件的输出路径
3. 找到已加固的apk文件,对已加固apk文件进行对齐、重签名。(360已加固的apk文件会在原有的release文件名后面加上"_jiagu")
4. 找到重新签名的apk文件,执行多渠道打包命令。(重签名后的文件名是在原有文件名后面加上"_sign")
task assembleReinforceRelease() {
group '360reinforce'
dependsOn("assembleRelease")
doLast {
cleanFilesPath(CHANNEL_APKS_PATH) //清空上一次生成的渠道包
def releaseApkFile = findApkFile(DEFAULT_APK_PATH, "-release") //遍历文件,寻找release包
println "--release--1-" + releaseApkFile
if (releaseApkFile != null) {
reinforceApk(releaseApkFile, SOURCE_APK_PATH) //执行加固
def reinforceApk = findApkFile(SOURCE_APK_PATH, "_jiagu") //寻找已加固的apk包
println "--jiagu--2-" + reinforceApk
if(reinforceApk != null) {
zipAlignApk(reinforceApk) // zip对齐
def zipAlignApk = findApkFile(SOURCE_APK_PATH, "_zip")
if (zipAlignApk != null) {
signApkV2(zipAlignApk) //使用V2重签名
def signatureApk = findApkFile(SOURCE_APK_PATH, "_sign")
println "--sign--3-" + signatureApk
if(signatureApk != null) {
buildChannelApks(signatureApk) //执行多渠道打包
renameChannelApkFiles(CHANNEL_APKS_PATH) //重命名渠道包
}
}
}
}
}
}
代码优化:
- 流程中涉及360加固平台帐号密码等敏感信息,可以将这部分信息放到签名信息所在的文件(eg:keystore.properties)中统一管理,然后将这些信息加载到gradle文件中;
- 各种输入输出的文件路径定义为常量,便于修改和管理;
加固方法:
/**
* 360加固
* @param apk 加固的原始apk File
* @param outputPath 输出目录
*/
def reinforceApk(File apk, outputPath) {
println "--- 360 reinforceApk start! ---"
println "reinforce apk:" + apk
if (apk == null || !apk.exists()) {
println "---360 reinforceApk throw exception and forced stop!---"
throw new FileNotFoundException('apk is not exists and cannot reinforce')
}
def file = new File(outputPath)
if (!file.exists()) {
file.mkdir()
}
exec {
commandLine getCommand(), "-c", "java -jar ${REINFORCE_JAR} -login ${REINFORCE_NAME} ${REINFORCE_PASSWORD}"
commandLine getCommand(), "-c", "java -jar ${REINFORCE_JAR} -showsign"
commandLine getCommand(), "-c", "java -jar ${REINFORCE_JAR} -jiagu ${apk} ${outputPath}"
}
println "--- 360 reinforce end! ---"
}
压缩对齐方法:
/**
* 加固后的apk 对齐压缩
* @param apk 已加固apk
* @return 返回对齐压缩后的apk
*/
def zipAlignApk(File apk) {
if (apk == null || !apk.exists()) {
println "---zipalign reinforceApk throw exception and forced stop!---"
throw new FileNotFoundException('apk is not exists and cannot reinforce')
}
def BUILD_TOOL_PATH = getAndroidSdkPath()
def APK_NAME = getApkName() + "_jiagu_zip.apk"
def file = new File("${SOURCE_APK_PATH}/${APK_NAME}")
if (file.exists()) {
file.delete()
}
exec {
commandLine getCommand(), "-c", "${BUILD_TOOL_PATH}zipalign -v -p 4 ${apk} ${SOURCE_APK_PATH}/${APK_NAME}"
}
}
签名方法:
/**
* 对apk签名
* @param zipApk 压缩对齐后的apk
* @return 签名后的apk
*/
def signApkV2(File zipApk) {
if (zipApk == null || !zipApk.exists()) {
println "---sign zipApk throw exception and forced stop!---"
throw new FileNotFoundException('apk is not exists and cannot reinforce')
}
def BUILD_TOOL_PATH = getAndroidSdkPath()
def APK_NAME = "app-release_" + getApkVersionName() + "_jiagu_zip_sign.apk"
def file = new File("${SOURCE_APK_PATH}/${APK_NAME}")
if (file.exists()) {
file.delete()
}
exec {
commandLine getCommand(), "-c", "${BUILD_TOOL_PATH}apksigner sign --ks ${KEY_PATH} --ks-key-alias ${ALIAS} --ks-pass pass:${KEY_PASSWORD} --key-pass pass:${ALIAS_PASSWORD} --out ${SOURCE_APK_PATH}/${APK_NAME} ${zipApk}"
}
}
添加渠道信息:
/**
* 对签名后的apk添加渠道信息
* @param apk 已签名apk
* @return 添加渠道信息后的apk
*/
def buildChannelApks(File apk) {
if (apk == null || !apk.exists()) {
println "---Channel build Apk throw exception and forced stop!---"
throw new FileNotFoundException('apk is not exists and cannot reinforce')
}
def file = new File(CHANNEL_APKS_PATH)
if (!file.exists()) {
file.mkdir()
}
def APK_NAME = getApkName() + "_jiagu_zip_sign.apk"
exec {
//java -jar walle-cli-all.jar batch -f /Users/Meituan/walle/app/channel /Users/Meituan/walle/app/build/outputs/apk/app.apk
commandLine getCommand(), "-c", "java -jar ${WALLE_JAR} batch -f ${WALLE_CHANNELS_CONFIG} ${SOURCE_APK_PATH}/${APK_NAME} ${CHANNEL_APKS_PATH}"
}
}
重命名apk:
/**
* 重命名apk
* @param path 渠道apk目录路径
* @return
*/
def renameChannelApkFiles(path) {
// def APK_NAME = "app-release_" + getApkVersionName() + "_jiagu_zip_sign.apk"
def regex = getApkName() + "_jiagu_zip_sign"
def dir = new File(path+"/")
dir.listFiles().each {file ->
if (file.name =~ /${regex}.*\.apk/) {
String newName = file.name
newName = newName.replaceAll(~/_jiagu/, "")
newName = newName.replaceAll(~/_zip/, "")
newName = newName.replaceAll(~/_sign/, "")
file.renameTo(new File(file.getParent(), newName))
}
}
}
涉及到的各种常量,各种密钥名、路径都要根据自己的实际情况修改:
/*加载keystore.properties信息到该gradle文件中*/
def keystorePropertiesFile = rootProject.file("keystore.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
ext {
/*加固*/
REINFORCE_JAR = "${project.rootDir}/jiagu/jiagu.jar"
REINFORCE_NAME = keystoreProperties['360_NAME'] //360加固账号
REINFORCE_PASSWORD = keystoreProperties['360_PASSWORD'] //360加固密码
KEY_PATH = keystoreProperties['storeFile'] //密钥路径
KEY_PASSWORD = keystoreProperties['storePassword'] //密钥密码
ALIAS = keystoreProperties['keyAlias'] //密钥别名
ALIAS_PASSWORD = keystoreProperties['keyPassword'] //别名密码
SOURCE_APK_PATH = "${project.buildDir}/bakApk" //源apk文件路径
DEFAULT_APK_PATH = "${project.buildDir}/outputs/apk/release" //默认release文件路径
/*多渠道打包*/
WALLE_JAR = "${project.rootDir}/walle-cli-all.jar"
WALLE_CHANNELS_CONFIG = "${project.rootDir}/app/channel" //渠道配置文件
CHANNEL_APKS_PATH = "${project.buildDir}/outputs/channels" //渠道Apk输出路径
}
再次感谢原创作者:天子卿
附作者文章链接:https://juejin.im/post/5c825ac6f265da2db9129bd0
在作者的启发下完成此demo, 如果对你有帮助请不要吝啬star