Android 360加固+Walle多渠道自动化打包上传蒲公英

Gradle 相关总结
APT 和 AGPTransform 区别
Gradle+Transform+Asm自动化注入代码
Android 360加固+Walle多渠道自动化打包上传蒲公英

概述

我们的目标是全自动化,并且在每个团队成员的电脑上都能够实现一行命令执行,不需要做额外的配置。在测试app项目过程中,通常都是需要开发打测试包给到测试,但是无论是iOS还是Android的打包过程都是相当漫长的,频繁的回归测试需要频繁的打包,对于开发同学影响还是蛮大的。因此在这种情况下,开发通常都会搭建一个简单的自动化打包平台(Jenkins),自动化构建打包或者上传到蒲公英,firm等分发平台。作为测试也需要了解相关的知识,用以优化提高开发测试效率。

前期技术调研

技术方案 优点 缺点
Android原生方案 通过PrpductFlovers进行变体打包,变体与变体之间构建灵活 每个变体都是assebleXXXRelease重新打包加固,打包速度慢
美团walle 每个渠道是通过解apk之后,插入渠道信息,再重新签名 打包速度快 加固需要自己实现或者其他第三方方案
360加固保 同walle方案类似,打包速度快;可以加固多渠道打包一体化 渠道之间的差异需要获取meta-data硬编码
腾讯 VasDolly 每个渠道是通过解apk之后,插入渠道信息,再重新签名 打包速度快 加固需要自己实现或者其他第三方方案

下面是来自VasDolly实现原理的表格,目前市面上的多渠道打包工具主要有packer-ng-plugin和美团的Walle

多渠道打包工具对比 VasDolly packer-ng-plugin Walle
V1签名方案 支持 支持 不支持
V2签名方案 支持 不支持 支持
已有注释块的APK 支持 不支持 不支持
根据已有APK生成渠道包 支持 不支持 不支持(这个Walle可以通过命令行支持)
命令行工具 支持 支持 支持
强校验 支持 不支持 不支持
多线程加速打包 支持 不支持 不支持

而我们使用的360加固包+Walle结合使用,看到网上很多文章说Walle打多渠道包之后加固,导致渠道信息丢失,首先360加固保加固会破坏签名结构,而Walle是直接把去打信息放到安全区,所以会导致失败。

实现

找到360加固宝的zip以及文档:360加固保下载地址

因为我们需要把自动加固和多渠道打包做到自动化,所以我们需要使用360加固宝的命令行工具:命令行工具文档

通过文档,找到几个关键的命令行

登录:java -jar jiagu.jar –login <username> <password>
导入签名:java -jar jiagu.jar -importsign <keystore_path> <keystore_password> <alias<alias_password>
导入渠道列表文件:java -jar jiagu.jar -importmulpkg <mulpkg_path>
加固 多渠道打包:java -jar jiagu.jar -jiagu <inputAPKpath> <outputpath> -autosign -automulpkg

多渠道打包 美团Walle

使用Gradle的打多渠道包的方式,Walle并没有提供输入需要打多渠道包apk的路径,所以我们使用命令行工具

java -jar walle-cli-all.jar batch -f [渠道配置文件] [待打包的apk路径] [打包输出的路径]

walle-cli-all.jar文件下载地址:官方:walle-cli-all.jar, 其他开发提供的编译版本
官方的版本打完包会发现在系统9.0(P)下无法正常安装, 相关问题可以查看Issue, 当然你也可以自己拉取源码编译。

下面是我们配置的gradle代码:

apply from: '../gradle/andResGuard.gradle'

class RepackageExt {
    String pgy_uploadUrl
    String pgy__api_key
    int pgy_buildInstallType
    String pgy_buildPassword
    String jjiagu_arPath
    String jiagu_account
    String jiagu_password
    String channel_wallJarPath
    String channel_file
}

def repackageExt = getExtensions().create("repackageExt", RepackageExt)


ext {
println(">>>>>>Repackage ext 配置阶段")
//蒲公英配置
uploadUrl = ""
_api_key = ""
buildInstallType = 2
buildPassword = ""

account = ""  //360加固账号
password = ""   //360加固密码
signPassword = ""//签名pass ,这里的变量会替换app中同名的变量的值
signAlias = ""//签名key
keyPath = ""

rootPath = "" //apk原始路径,实际上这里的加固和多渠道包也是放在同一个目录下
debugRootPath = "" //apk原始路径,实际上这里的加固和多渠道包也是放在同一个目录下
jarPath = ""   //360加固执行命令的jar包路径
wallJarPath = ""// Walle执行命令的jar包路径
channelFile = ""//多渠道配置文件

login = ""  //360登录命令
importKey = "" //360 导入签名信息命令
checkSignInfo = "" //360 检查签名信息
initJiaguService = "" //初始化360加固服务配置

println(">>>>>>Repackage ext 配置阶段结束")
}

/**
   * Gradle执行配置阶段之后,对我们的配置进行初始化
   */
project.afterEvaluate {
println(">>>>>配置阶段完成时候执行")
jarPath = repackageExt.jjiagu_arPath
wallJarPath = repackageExt.channel_wallJarPath
channelFile = repackageExt.channel_file

uploadUrl = repackageExt.pgy_uploadUrl
_api_key = repackageExt.pgy__api_key
buildInstallType = repackageExt.pgy_buildInstallType
buildPassword = repackageExt.pgy_buildPassword

account = repackageExt.jiagu_account
password = repackageExt.jiagu_password

initApkOutDir()
initSignInfo()
initCmd()


//正式服
project.tasks.getByName('resguardRelease') { Task task ->
    task.dependsOn(assembleCleanApkDirRelease)
}
project.tasks.getByName('assembleQihuJiaGuRelease') { Task task ->
    task.dependsOn(resguardRelease)
}
project.tasks.getByName('assembleWalleChannelsRelease') { Task task ->
    task.dependsOn(assembleQihuJiaGuRelease)
}
project.tasks.getByName('assembleUploadPGYRelease') { Task task ->
    task.dependsOn(assembleWalleChannelsRelease)
}

//测试服
project.tasks.getByName('assembleUploadPGYDebug') { Task task ->
    task.dependsOn(resguardDebug)
}
}

/**
   * 定义apk删除目录任务
 */
project.task("assembleCleanApkDirRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
    def apkRootPath = file(rootPath).getParentFile().path
    println "==================>开始执行删除apk输出目录任务${apkRootPath}"
    delete(apkRootPath)
    println "==================>删除apk输出目录任务完成${apkRootPath}"
}
}

/**
 * 加固
 *
 * rootPath: 原始apk输出的路径
 *
 */
project.task("assembleQihuJiaGuRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
    println "开始加固任务==================>${rootPath}"
    startJiaGu(file(rootPath))
    // 360加固我是用自动加固,所以后缀会有_sign
    def jiaGuApk = findApkFile(file(rootPath).getParentFile().path, "_sign")
    println "加固完成任务==================>${jiaGuApk.path}"
}
}

/**
   * 定义Gardle Task,walle多渠道打包
   */
project.task("assembleWalleChannelsRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
    // 360加固我是用自动加固,所以后缀会有_sign
    def needChannelApk = findApkFile(file(rootPath).getParentFile().path, "_sign")
    println "开始渠道打包任务==================>${needChannelApk.path}"
    if (needChannelApk != null) {
        // 官方的版本打完包会发现在系统9.0(P)下无法正常安装相关问题可以查看Issue(https://github.com/Meituan-Dianping/walle/issues/264)
        //"java -jar {walle-cli-all.jar文件路径} batch -f {渠道文件路径} {要加渠道的apk文件路径} {渠道包的输出路径}"
        def channelCmd = "java -jar ${wallJarPath} batch -f ${channelFile} ${needChannelApk.path} ${needChannelApk.getParentFile().path}"
        executeJiaGuCMD(channelCmd)
    }
    println "渠道打包任务完成==================>${needChannelApk.path}"
}
}

/**
   * 上传蒲公英
   *
   * 这里仅仅只选择dev渠道上传蒲公英
   *
   */
project.task("assembleUploadPGYRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
    def needUploadApk = findApkFile(file(rootPath).getParentFile().path, "_dev")
    println "开始上传蒲公英任务==================>${needUploadApk.path}"
    if (needUploadApk != null) {
        uploadPGY(needUploadApk.path)
    }
    println "上传蒲公英任务完成==================>${needUploadApk.path}"
}
}


/**
   * 上传测试包蒲公英
   */
project.task("assembleUploadPGYDebug") { Task task ->
task.setGroup("publishApks")
task.doLast {
    println "开始上传蒲公英任务==================>${debugRootPath}"
    uploadPGY(debugRootPath)
    println "上传蒲公英任务完成==================>${debugRootPath}"
}
}


/**
    * 初始化打包APK的输出路径
 */
private void initApkOutDir() {
def android = project.getExtensions().getByType(AppExtension)
android.applicationVariants.all { variant ->
    def apkRootPath = variant.packageApplicationProvider.get().outputDirectory
    variant.packageApplicationProvider.get().outputScope.apkDatas.forEach { apkData ->
        if (variant.buildType.name == "release") {
            rootPath = "${apkRootPath}\\${apkData.outputFileName}"
            println ">>>>release${rootPath}"
        }
        if (variant.buildType.name == "debug") {
            debugRootPath = "${apkRootPath}\\${apkData.outputFileName}"
            println ">>>>debug${debugRootPath}"
        }
    }
}
def versionName = android.getDefaultConfig().versionName
println("rootPath>>>>>${rootPath} , ${debugRootPath}   ${versionName}")
}

/**
   * 初始化签名信息
 */
private void initSignInfo() {
def android = project.getExtensions().getByType(AppExtension)
android.getSigningConfigs().each { SigningConfig signingConfig ->
    if (signingConfig.name == "release") {
        signPassword = signingConfig.keyPassword
        signAlias = signingConfig.keyAlias
        keyPath = signingConfig.storeFile
        println("signingConfig>>>${signingConfig}")
    }
}
  }

/**
   * 初始化加固打包命令
   */
private void initCmd() {
login = "java -jar ${jarPath} -login ${account} ${password}"
importKey = "java -jar ${jarPath} -importsign ${keyPath} ${signPassword} ${signAlias} ${signPassword}"
checkSignInfo = "java -jar ${jarPath} -showsign"
initJiaguService = "java -jar ${jarPath} -config"
}


private void startJiaGu(File needJiaGuApkFile) {
// 登录360加固保
executeJiaGuCMD(login)
// 导入签名信息
executeJiaGuCMD(importKey)
// 查看360加固签名信息
executeJiaGuCMD(checkSignInfo)
// 初始化加固服务配置,后面可不带参数
executeJiaGuCMD(initJiaguService)
// 执行加固,然后自动签名,若不采取自动签名,需要自己通过build-tools命令自己签名
def startJiaGu = "java -jar ${jarPath} -jiagu ${needJiaGuApkFile.path} ${needJiaGuApkFile.getParentFile().path} -autosign"
executeJiaGuCMD(startJiaGu)

}

  /**
   * 根据后缀查找匹配的apk文件
   * @param path
   * @param suffix
   * @return
 */
private static File findApkFile(path, suffix) {
def dir = new File(path)
return dir.listFiles().find { it.isFile() && it =~ /.*${suffix}\.apk/ }
  }

  /**
     * 执行命令
     * @param cmd 命令
     */
private void executeJiaGuCMD(String cmd) {
println cmd
def process = cmd.execute()
println process.text
process.waitFor()  // 用以等待外部进程调用结束
println process.exitValue()
  }

//上传蒲公英托管 curl --help查看命令 -F指定HTTP分段POST数据(H)
  private def uploadPGY(String filePath) {
def stdout = new ByteArrayOutputStream()
exec {
    //设置要使用的可执行文件的名称。
    executable = 'curl'
    args = ['-F', "file=@${filePath}", '-F', "_api_key=${_api_key}", '-F', "buildInstallType=${buildInstallType}", '-F', "buildPassword=${buildPassword}", "${uploadUrl}"]
    //设置输出流以使用执行命令的进程的标准输出。该过程完成后,流将关闭,修改命令输出的地方,默认为控制台
    standardOutput = stdout
}
String output = stdout.toString()
def parsedJson = new JsonSlurper().parseText(output)
println output
println "应用二维码地址:" + parsedJson.data.buildQRCodeURL
println "版本号:" + parsedJson.data.buildVersion
}

gradle中主要流程是:
1、经过微信的resguardRelease打包,输出结果;
2、将resguardRelease输出的apk文件使用360加固保进行加固;
3、对加固完成的apk文件,使用Walle打多渠道包;
4、最后一步就是上传蒲公英托管平台;

熟悉gradle应该知道,gradle的执行过程。

/**
 * Gradle执行配置阶段之后,对我们的配置进行初始化
 */
project.afterEvaluate {
    initApkOutDir()
    initSignInfo()
    initCmd()
}

对于我们的打包gradle,首先我们会在配置阶段做一些初始化操作,如:project.afterEvaluate闭包就是在配置完成时h,比如:获取打包apk的路径和apk签名文件信息等等,代码如下:会调用这个闭包,接下来就是获取相关的数据的代码。

/**
  * 初始化打包APK的输出路径
 */
void initApkOutDir() {
def android = project.getExtensions().getByType(AppExtension)
android.applicationVariants.all { variant ->
    def apkRootPath = variant.packageApplicationProvider.get().outputDirectory
    variant.packageApplicationProvider.get().outputScope.apkDatas.forEach { apkData ->
        rootPath = "${apkRootPath}\\${apkData.outputFileName}"
    }
}
println("rootPath>>>>>${rootPath}")
}

/**
 * 初始化签名信息
 */
void initSignInfo() {
def android = project.getExtensions().getByType(AppExtension)
android.getSigningConfigs().each { SigningConfig signingConfig ->
    if (signingConfig.name == "release") {
        signKeyPassword = signingConfig.keyPassword
        signkeyAlias = signingConfig.keyAlias
        keyPath = signingConfig.storeFile
        println("signingConfig>>>${signingConfig}")
    }
}

}

/**
   * 初始化加固打包命令
 */
void initCmd() {
login = "java -jar ${jarPath} -login ${account} ${password}"
importKey = "java -jar ${jarPath} -importsign ${keyPath} ${signKeyPassword} ${signkeyAlias} ${signKeyPassword}"
checkSignInfo = "java -jar ${jarPath} -showsign"
initJiaguService = "java -jar ${jarPath} -config"
}

最后就是我们上传蒲公英的代码:

//上传蒲公英托管 curl --help查看命令 -F指定HTTP分段POST数据(H)
private def uploadPGY(String filePath) {
def stdout = new ByteArrayOutputStream()
exec {
    //设置要使用的可执行文件的名称。
    executable = 'curl'
    args = ['-F', "file=@${filePath}", '-F', "_api_key=${_api_key}", '-F', "buildInstallType=${buildInstallType}", '-F', "buildPassword=${buildPassword}", "${uploadUrl}"]
    //设置输出流以使用执行命令的进程的标准输出。该过程完成后,流将关闭,修改命令输出的地方,默认为控制台
    standardOutput = stdout
}
String output = stdout.toString()
def parsedJson = new JsonSlurper().parseText(output)
println output
println "应用二维码地址:" + parsedJson.data.buildQRCodeURL
println "版本号:" + parsedJson.data.buildVersion
}

当然除了这种直接在Gradle文件中处理的方式,我觉得可以将这些库整合成插件会比较方便使用,在我们项目中已经把AndResGuard+ 360+Walle+蒲公英整合到一个插件中了,到此我们的自动化加固打包已经结束了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351