研究这个Gradle自动化脚本初衷是为了实现自动化打包、加固和增加多渠道,从而一键完成应用发布上架应用市场前的所有操作,以达到解放双手和节约时间成本的效果。后期有考虑配合curl指令将打包好的apk自动上传到服务器或者托管平台,亦或可结合Jenkins自动化构建、打包、上传等,从而实现整个流程的自动化目的。
通常我们App上架到应用市场基本上都经历过以下流程,先本地打一个release包,然后通过在线加固或者下载加固工具进行加固,由于加固会先剔除签名信息,所以加固后要进行再次签名,然后生成多渠道包,这样基本上整个流程就结束了,画了个思维导图如下:
我的简单理解就是给原有的apk进行加密和套壳,产生一个新的apk,然后运行的时候会进行解密相关的动作,所以加固后的app一般会影响启动时间,网上也有很多加固平台的对比,主要涉及启动时间、包体积大小、兼容性、安全性等等。本次研究只是讨论如何实现自动化加固与多渠道打包思想,360加固并非最好选择,加固主要是为了防止应用在上线后被反编译、调试、破解、二次打包和内存截取等多种威胁。
本次Gradle自动化实践的步骤主要是基于360加固+腾讯的VasDolly多渠道打包。
/** * 自动下载360加固保,也可以自己下载然后放到根目录 */ def download360jiagu() { // 下载360压缩包 File zipFile = file(packers["zipPath"]) if (!zipFile.exists()) { if (!zipFile.parentFile.exists()) { zipFile.parentFile.mkdirs() println("packers===create parentFile jiagu ${zipFile.parentFile.absolutePath}") } // 加固保的下载地址 def downloadUrl = isWindows() ? packers["jiagubao_windows"] : packers["jiagubao_mac"] // mac自带curl命令 windows需要下载curl安装 def cmd = "curl -o ${packers["zipPath"]} ${downloadUrl}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) } File unzipFile = file(packers["unzipPath"]) if (!unzipFile.exists()) { //解压 Zip 文件 ant.unzip(src: packers["zipPath"], dest: packers["unzipPath"], encoding: "GBK") println 'packers===unzip 360jiagu' //将解压后的文件开启读写权限,防止执行 Jar 文件没有权限执行,windows若没有权限需要自己手动改 if (!isWindows()) { def cmd = "chmod -R 777 ${packers["unzipPath"]}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) } } } 复制代码
gradle其实为我们提供了一系列相关的任务,如下图
我们执行加固前是需要拿到一个release包的,所以我们可以利用assembleRelease
在加固前先执行
assembleRelease
这个Task。
task packersNewRelease { group 'packers' //可以利用task的依赖关系先执行打包 dependsOn 'assembleRelease' } 复制代码
所谓自动执行加固,无非就是几行命令,360加固保提供了一套 命令行进行加固
特别提醒,此处360配置可选项的增强服务有bug,已经跟官方沟通,他们需要在下个版本修复,当前存在bug的版本3.2.2.3(2020-03-16),命令行目前无法只选择盗版监测 复制代码
/** * 对于release apk 进行360加固 */ def packers360(File releaseApk) { println 'packers===beginning 360 jiagu' def packersFile = file(app["packersPath"]) if (!packersFile.exists()) { packersFile.mkdir() } exec { // 登录360加固保 executable = 'java' args = ['-jar', packers["jarPath"], '-login', packers["account"], packers["password"]] println 'packers===import 360 login' } exec { // 导入签名信息 executable = 'java' args = ['-jar', packers["jarPath"], '-importsign', signing["storeFile"], signing["storePassword"], signing["keyAlias"], signing["keyPassword"]] println 'packers===import 360 sign' } exec { // 查看360加固签名信息 executable = 'java' args = ['-jar', packers["jarPath"], '-showsign'] println 'packers===show 360 sign' } exec { // 初始化加固服务配置,后面可不带参数 executable = 'java' args = ['-jar', packers["jarPath"], '-config'] println 'packers===init 360 services' } exec { // 执行加固,然后自动签名,若不采取自动签名,需要自己通过build-tools命令自己签名 executable = 'java' args = ['-jar', packers["jarPath"], '-jiagu', releaseApk.absolutePath, app["packersPath"], '-autosign'] println 'packers===excute 360 jiagu' } println 'packers===360 jiagu finished' println "packers===360 jiagu path ${app["packersPath"]}" } 复制代码
关于自动签名,其实360在加固的时候提供了自动签名的配置选项,如果你不想将签名文件上传给360,在加固后可以自己选择手动签名,因为这涉及到安全性的问题,此版本我采取的是360自动签名,如果大家想自己手动签名,下面我给出一套方案,主要是利用 zipalign
和 apksigner
命令 他们都是位于SDK文件中的build-tools目录中,我们执行自动化签名需要gradle配置好路径。
zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk 复制代码
apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk 复制代码
apksigner verify my-app-release.apk 复制代码
关于多渠道打包,我们之前项目一直使用的是腾讯的VasDolly,故我们此次是采取VasDolly命令,但是需要先下载 VasDolly.jar ,至于放在什么位置没有要求,只需要gradle配置好路径即可,我直接是放在项目根目录。
/** * 腾讯channel重新构建渠道包 */ def reBuildChannel() { File channelFile = file("${app["channelPath"]}/new") if (!channelFile.exists()) { channelFile.mkdirs() } def cmd = "java -jar ${app["vasDollyPath"]} put -c ${"../channel.txt"} ${outputpackersApk()} ${channelFile.absolutePath}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) println 'packers===excute VasDolly reBuildChannel' } 复制代码
我们都知道,签名需要签名文件,密码、别名等等文件,360加固需要配置账号与密码,这些都属于敏感信息,google官方不建议直接放在gradle中,它是以纯文本记录在gradle中的,建议存储在properties文件中。
// 把敏感信息存放到自定义的properties文件中 def propertiesFile = rootProject.file("release.properties") def properties = new Properties() properties.load(new FileInputStream(propertiesFile)) ext { // 签名配置 signing = [keyAlias : properties['RELEASE_KEY_ALIAS'], keyPassword : properties['RELEASE_KEY_PASSWORD'], storeFile : properties['RELEASE_KEYSTORE_PATH'], storePassword: properties['RELEASE_STORE_PASSWORD'] ] // app相关的配置 app = [ //默认release apk的文件路径,因为加固是基于release包的 releasePath : "${project.buildDir}/outputs/apk/release", //对release apk 加固后产生的加固apk地址 packersPath : "${project.buildDir}/outputs/packers", //加固后进行腾讯多渠道打包的地址 channelPath : "${project.buildDir}/outputs/channels", //腾讯VasDolly多渠道打包jar包地址 vasDollyPath: "../VasDolly.jar" ] // 360加固配置 packers = [account : properties['ACCOUNT360'], //账号 password : properties['PASSWORD360'], //密码 zipPath : "${project.rootDir}/jiagu/360jiagu.zip", //加固压缩包路径 unzipPath : "${project.rootDir}/jiagu/360jiagubao/", //加固解压路径 jarPath : "${project.rootDir}/jiagu/360jiagubao/jiagu/jiagu.jar", //执行命令的jar包路径 channelConfigPath: "${project.rootDir}/jiagu/Channel.txt", //加固多渠道 jiagubao_mac : "https://down.360safe.com/360Jiagu/360jiagubao_mac.zip", //加固mac下载地址 jiagubao_windows : "https://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip" //加固widnows下载地址 ] 复制代码
apply from: "${project.rootDir}/packers.gradle" 复制代码
def dest = "A" 复制代码
使用ext扩展块,一次扩展多个属性 ext { account = "XXXX" password = "XXXXX" } 复制代码
单引号不支持插值 def name = '张三' 双引号支持插值 def name = "我是${'张三'}" 三个单引号支持换行 def name = """ 张三 李四 """ 复制代码
// 这两种写法等价 println('A') println 'A' 复制代码
repositories { println "A" } repositories() { println "A" } repositories({println "A" }) 复制代码
task B { // TaskB依赖TaskA,故会先执行TaskA dependsOn A //其次执行packersRelease doLast { println "B" } } 复制代码
//taskB必须总是在 taskA 之后运行, 无论 taskA 和 taskB 是否将要运行 taskB.mustRunAfter(taskA) //没有msut那么严格 taskB.shouldRunAfter (taskA) 复制代码
// 使用一个相对路径 File configFile = file('src/config.xml') // 使用一个绝对路径 configFile = file(configFile.absolutePath) // 使用一个项目路径的文件对象 configFile = file(new File('src/config.xml'))` 复制代码
// 对文件集合进行迭代 collection.each {File file -> println file.name } 复制代码
copy { from 源文件地址 into 目标目录地址 rename(“原文件名”, "新文件名字") } 复制代码
这个功能准备在下篇文章更新,我们可以通过curl命令上传到自己的服务器,如果你在测试阶段可以上传到蒲公英或者fir.im托管平台,目前他们都提供了相关的操作方式,这样基本上整个自动化的目的就完成了,当然你也可以选择Jenknis自动化构建、打包及上传。
方式一:fir-CLI 命令行工具上传 $ fir p path/to/application -T YOUR_FIR_TOKEN 方式二:API 上传 通过curl命令调用相关的api 1.获取凭证 curl -X "POST" "http://api.bq04.com/apps" / -H "Content-Type: application/json" / -d "{/"type/":/"android/", /"bundle_id/":/"xx.x/", /"api_token/":/"aa/"}" 2.上传apk curl -F "key=xxxxxx" / -F "token=xxxxx" / -F "file=@aa.apk" / -F "x:name=aaaa" / -F "x:version=a.b.c" / -F "x:build=1" / -F "x:release_type=Adhoc" / #type=ios 使用 -F "x:changelog=first" / https://up.qbox.me 复制代码
curl -F "file=@/tmp/example.ipa" -F "uKey=" -F "_api_key=" https://upload.pgyer.com/apiv1/app/upload 复制代码
我们的需求是需要打两批包,用于老后台与新后台,老后台的包必须加上app-前缀,所以有三个任务 packersNewRelease
执行正常的加固打包用于新后台, packersOldRelease
用于打包加前缀app-名称用于老后台, packersRelease
这个任务用于一键同时打包成老后台与新后台。
同时可以在gradle控制台查看打包任务的输出日志,如下:
为了能够让大家尝试自动化gradle脚本带来的便利之处,下面我贡献上自己的整个gradle源码,需要的可以拿走去研究,如存在问题也希望多多交流。
/** * @author hule * @date 2020/04/15 13:42 * description:360自动加固+Vaslloy多渠道打包 */ // 把敏感信息存放到自定义的properties文件中 def propertiesFile = rootProject.file("release.properties") def properties = new Properties() properties.load(new FileInputStream(propertiesFile)) ext { // 签名配置 signing = [keyAlias : properties['RELEASE_KEY_ALIAS'], keyPassword : properties['RELEASE_KEY_PASSWORD'], storeFile : properties['RELEASE_KEYSTORE_PATH'], storePassword: properties['RELEASE_STORE_PASSWORD'] ] // app相关的配置 app = [ //默认release apk的文件路径,因为加固是基于release包的 releasePath : "${project.buildDir}/outputs/apk/release", //对release apk 加固后产生的加固apk地址 packersPath : "${project.buildDir}/outputs/packers", //加固后进行腾讯多渠道打包的地址 channelPath : "${project.buildDir}/outputs/channels", //腾讯VasDolly多渠道打包jar包地址 vasDollyPath: "../VasDolly.jar" ] // 360加固配置 packers = [account : properties['ACCOUNT360'], //账号 password : properties['PASSWORD360'], //密码 zipPath : "${project.rootDir}/jiagu/360jiagu.zip", //加固压缩包路径 unzipPath : "${project.rootDir}/jiagu/360jiagubao/", //加固解压路径 jarPath : "${project.rootDir}/jiagu/360jiagubao/jiagu/jiagu.jar", //执行命令的jar包路径 channelConfigPath: "${project.rootDir}/jiagu/Channel.txt", //加固多渠道 jiagubao_mac : "https://down.360safe.com/360Jiagu/360jiagubao_mac.zip", //加固mac下载地址 jiagubao_windows : "https://down.360safe.com/360Jiagu/360jiagubao_windows_64.zip" //加固widnows下载地址 ] } /** * 360加固,适用于新后台打包 */ task packersNewRelease { group 'packers' dependsOn 'assembleRelease' doLast { //删除加固后的渠道包 deleteFile() // 下载360加固文件 download360jiagu() // 寻找打包文件release apk def releaseFile = findReleaseApk() if (releaseFile != null) { //执行加固签名 packers360(releaseFile) //对加固后的apk重新用腾讯channel构建渠道包 reBuildChannel() } else { println 'packers===can/'t find release apk and can/'t excute 360 jiagu' } } } /** * 适用于老后台,老后台需要在渠道apk的名称增加前缀 app- */ task packersOldRelease { group 'packers' doLast { File channelFile = file("${app["channelPath"]}/new") if (!channelFile.exists() || !channelFile.listFiles()) { println 'packers==== please excute pakcersNewRelease first!' } else { File oldChannelFile = file("${app["channelPath"]}/old") if (!oldChannelFile.exists()) { oldChannelFile.mkdirs() } // 对文件集合进行迭代 channelFile.listFiles().each { File file -> copy { from file.absolutePath into oldChannelFile.absolutePath rename(file.name, "app-${file.name}") } } println 'packers===packersOldRelease sucess' } } } /** * 加固后,打新版本的渠道包时,同时生成老版本的渠道包 */ task packersRelease { group 'packers' dependsOn packersNewRelease dependsOn packersOldRelease packersOldRelease.mustRunAfter(packersNewRelease) doLast { println "packers===packersRelease finished" } } /** * 对于release apk 进行360加固 */ def packers360(File releaseApk) { println 'packers===beginning 360 jiagu' def packersFile = file(app["packersPath"]) if (!packersFile.exists()) { packersFile.mkdir() } exec { // 登录360加固保 executable = 'java' args = ['-jar', packers["jarPath"], '-login', packers["account"], packers["password"]] println 'packers===import 360 login' } exec { // 导入签名信息 executable = 'java' args = ['-jar', packers["jarPath"], '-importsign', signing["storeFile"], signing["storePassword"], signing["keyAlias"], signing["keyPassword"]] println 'packers===import 360 sign' } exec { // 查看360加固签名信息 executable = 'java' args = ['-jar', packers["jarPath"], '-showsign'] println 'packers===show 360 sign' } exec { // 初始化加固服务配置,后面可不带参数 executable = 'java' args = ['-jar', packers["jarPath"], '-config'] println 'packers===init 360 services' } exec { // 执行加固 executable = 'java' args = ['-jar', packers["jarPath"], '-jiagu', releaseApk.absolutePath, app["packersPath"], '-autosign'] println 'packers===excute 360 jiagu' } println 'packers===360 jiagu finished' println "packers===360 jiagu path ${app["packersPath"]}" } /** * 自动下载360加固保,也可以自己下载然后放到根目录 */ def download360jiagu() { // 下载360压缩包 File zipFile = file(packers["zipPath"]) if (!zipFile.exists()) { if (!zipFile.parentFile.exists()) { zipFile.parentFile.mkdirs() println("packers===create parentFile jiagu ${zipFile.parentFile.absolutePath}") } // 加固保的下载地址 def downloadUrl = isWindows() ? packers["jiagubao_windows"] : packers["jiagubao_mac"] // mac自带curl命令 windows需要下载curl安装 def cmd = "curl -o ${packers["zipPath"]} ${downloadUrl}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) } File unzipFile = file(packers["unzipPath"]) if (!unzipFile.exists()) { //解压 Zip 文件 ant.unzip(src: packers["zipPath"], dest: packers["unzipPath"], encoding: "GBK") println 'packers===unzip 360jiagu' //将解压后的文件开启读写权限,防止执行 Jar 文件没有权限执行,windows需要自己手动改 if (!isWindows()) { def cmd = "chmod -R 777 ${packers["unzipPath"]}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) } } } /** * 腾讯channel重新构建渠道包 */ def reBuildChannel() { File channelFile = file("${app["channelPath"]}/new") if (!channelFile.exists()) { channelFile.mkdirs() } def cmd = "java -jar ${app["vasDollyPath"]} put -c ${"../channel.txt"} ${outputpackersApk()} ${channelFile.absolutePath}" println cmd cmd.execute().waitForProcessOutput(System.out, System.err) println 'packers===excute VasDolly reBuildChannel' } /** * 是否是windows系统 * @return */ static Boolean isWindows() { return System.properties['os.name'].contains('Windows') } /** * 寻找本地的release apk * @return true */ def deleteFile() { delete app["channelPath"] delete app["packersPath"] println 'packers===delete all file' } /** * 首先打一个release包,然后找到当前的文件进行加固 * @return releaseApk */ def findReleaseApk() { def apkDir = file(app["releasePath"]) File releaseApk = apkDir.listFiles().find { it.isFile() && it.name.endsWith(".apk") } println "packers===find release apk ${releaseApk.name}" return releaseApk } /** * 加固输出并且重新命名 * @return packersApk */ def outputpackersApk() { File oldApkDir = file(app["packersPath"]) File oldApk = oldApkDir.listFiles().find { it.isFile() && it.name.contains("jiagu") } println "packers===output pacckers sourceApk ${oldApk.name}" copy { from app["packersPath"] + File.separator + oldApk.name into app["packersPath"] rename(oldApk.name, "release.apk") println 'packers===output pacckers renameApk release.apk' } File newApk = oldApkDir.listFiles().find { it.isFile() && it.name.equals("release.apk") } println "packers===output packers renameApk${newApk.absolutePath}" return newApk.absolutePath } 复制代码
本篇文章分享是基于360加固与腾讯VasDolly多渠道打包的自动化实践,提供的只是一种方式,不限于这两个平台,其他平台无非也就是更换一下加固与多渠道打包的命令,喜欢这篇gradle自动化加固与多渠道打包就随手点个赞吧,你的点赞是我写作的动力!