Gradle 相關(guān)總結(jié)
APT 和 AGPTransform 區(qū)別
Gradle+Transform+Asm自動化注入代碼
Android 360加固+Walle多渠道自動化打包上傳蒲公英
概述
我們的目標(biāo)是全自動化,并且在每個團(tuán)隊成員的電腦上都能夠?qū)崿F(xiàn)一行命令執(zhí)行,不需要做額外的配置。在測試app項目過程中,通常都是需要開發(fā)打測試包給到測試,但是無論是iOS還是Android的打包過程都是相當(dāng)漫長的,頻繁的回歸測試需要頻繁的打包,對于開發(fā)同學(xué)影響還是蠻大的。因此在這種情況下,開發(fā)通常都會搭建一個簡單的自動化打包平臺(Jenkins),自動化構(gòu)建打包或者上傳到蒲公英,firm等分發(fā)平臺。作為測試也需要了解相關(guān)的知識,用以優(yōu)化提高開發(fā)測試效率。
前期技術(shù)調(diào)研
| 技術(shù)方案 | 優(yōu)點 | 缺點 |
|---|---|---|
| Android原生方案 | 通過PrpductFlovers進(jìn)行變體打包,變體與變體之間構(gòu)建靈活 | 每個變體都是assebleXXXRelease重新打包加固,打包速度慢 |
| 美團(tuán)walle | 每個渠道是通過解apk之后,插入渠道信息,再重新簽名 打包速度快 | 加固需要自己實現(xiàn)或者其他第三方方案 |
| 360加固保 | 同walle方案類似,打包速度快;可以加固多渠道打包一體化 | 渠道之間的差異需要獲取meta-data硬編碼 |
| 騰訊 VasDolly | 每個渠道是通過解apk之后,插入渠道信息,再重新簽名 打包速度快 | 加固需要自己實現(xiàn)或者其他第三方方案 |
下面是來自VasDolly實現(xiàn)原理的表格,目前市面上的多渠道打包工具主要有packer-ng-plugin和美團(tuán)的Walle。
| 多渠道打包工具對比 | VasDolly | packer-ng-plugin | Walle |
|---|---|---|---|
| V1簽名方案 | 支持 | 支持 | 不支持 |
| V2簽名方案 | 支持 | 不支持 | 支持 |
| 已有注釋塊的APK | 支持 | 不支持 | 不支持 |
| 根據(jù)已有APK生成渠道包 | 支持 | 不支持 | 不支持(這個Walle可以通過命令行支持) |
| 命令行工具 | 支持 | 支持 | 支持 |
| 強(qiáng)校驗 | 支持 | 不支持 | 不支持 |
| 多線程加速打包 | 支持 | 不支持 | 不支持 |
而我們使用的360加固包+Walle結(jié)合使用,看到網(wǎng)上很多文章說Walle打多渠道包之后加固,導(dǎo)致渠道信息丟失,首先360加固保加固會破壞簽名結(jié)構(gòu),而Walle是直接把去打信息放到安全區(qū),所以會導(dǎo)致失敗。
實現(xiàn)
找到360加固寶的zip以及文檔:360加固保下載地址
因為我們需要把自動加固和多渠道打包做到自動化,所以我們需要使用360加固寶的命令行工具:命令行工具文檔
通過文檔,找到幾個關(guān)鍵的命令行
登錄:java -jar jiagu.jar –login <username> <password>
導(dǎo)入簽名:java -jar jiagu.jar -importsign <keystore_path> <keystore_password> <alias<alias_password>
導(dǎo)入渠道列表文件:java -jar jiagu.jar -importmulpkg <mulpkg_path>
加固 多渠道打包:java -jar jiagu.jar -jiagu <inputAPKpath> <outputpath> -autosign -automulpkg
多渠道打包 美團(tuán)Walle
使用Gradle的打多渠道包的方式,Walle并沒有提供輸入需要打多渠道包apk的路徑,所以我們使用命令行工具
java -jar walle-cli-all.jar batch -f [渠道配置文件] [待打包的apk路徑] [打包輸出的路徑]
walle-cli-all.jar文件下載地址:官方:walle-cli-all.jar, 其他開發(fā)提供的編譯版本
官方的版本打完包會發(fā)現(xiàn)在系統(tǒng)9.0(P)下無法正常安裝, 相關(guān)問題可以查看Issue, 當(dāng)然你也可以自己拉取源碼編譯。
下面是我們配置的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 = ""http://簽名pass ,這里的變量會替換app中同名的變量的值
signAlias = ""http://簽名key
keyPath = ""
rootPath = "" //apk原始路徑,實際上這里的加固和多渠道包也是放在同一個目錄下
debugRootPath = "" //apk原始路徑,實際上這里的加固和多渠道包也是放在同一個目錄下
jarPath = "" //360加固執(zhí)行命令的jar包路徑
wallJarPath = ""http:// Walle執(zhí)行命令的jar包路徑
channelFile = ""http://多渠道配置文件
login = "" //360登錄命令
importKey = "" //360 導(dǎo)入簽名信息命令
checkSignInfo = "" //360 檢查簽名信息
initJiaguService = "" //初始化360加固服務(wù)配置
println(">>>>>>Repackage ext 配置階段結(jié)束")
}
/**
* Gradle執(zhí)行配置階段之后,對我們的配置進(jìn)行初始化
*/
project.afterEvaluate {
println(">>>>>配置階段完成時候執(zhí)行")
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刪除目錄任務(wù)
*/
project.task("assembleCleanApkDirRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
def apkRootPath = file(rootPath).getParentFile().path
println "==================>開始執(zhí)行刪除apk輸出目錄任務(wù)${apkRootPath}"
delete(apkRootPath)
println "==================>刪除apk輸出目錄任務(wù)完成${apkRootPath}"
}
}
/**
* 加固
*
* rootPath: 原始apk輸出的路徑
*
*/
project.task("assembleQihuJiaGuRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
println "開始加固任務(wù)==================>${rootPath}"
startJiaGu(file(rootPath))
// 360加固我是用自動加固,所以后綴會有_sign
def jiaGuApk = findApkFile(file(rootPath).getParentFile().path, "_sign")
println "加固完成任務(wù)==================>${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 "開始渠道打包任務(wù)==================>${needChannelApk.path}"
if (needChannelApk != null) {
// 官方的版本打完包會發(fā)現(xiàn)在系統(tǒng)9.0(P)下無法正常安裝相關(guān)問題可以查看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 "渠道打包任務(wù)完成==================>${needChannelApk.path}"
}
}
/**
* 上傳蒲公英
*
* 這里僅僅只選擇dev渠道上傳蒲公英
*
*/
project.task("assembleUploadPGYRelease") { Task task ->
task.setGroup("publishApks")
task.doLast {
def needUploadApk = findApkFile(file(rootPath).getParentFile().path, "_dev")
println "開始上傳蒲公英任務(wù)==================>${needUploadApk.path}"
if (needUploadApk != null) {
uploadPGY(needUploadApk.path)
}
println "上傳蒲公英任務(wù)完成==================>${needUploadApk.path}"
}
}
/**
* 上傳測試包蒲公英
*/
project.task("assembleUploadPGYDebug") { Task task ->
task.setGroup("publishApks")
task.doLast {
println "開始上傳蒲公英任務(wù)==================>${debugRootPath}"
uploadPGY(debugRootPath)
println "上傳蒲公英任務(wù)完成==================>${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)
// 導(dǎo)入簽名信息
executeJiaGuCMD(importKey)
// 查看360加固簽名信息
executeJiaGuCMD(checkSignInfo)
// 初始化加固服務(wù)配置,后面可不帶參數(shù)
executeJiaGuCMD(initJiaguService)
// 執(zhí)行加固,然后自動簽名,若不采取自動簽名,需要自己通過build-tools命令自己簽名
def startJiaGu = "java -jar ${jarPath} -jiagu ${needJiaGuApkFile.path} ${needJiaGuApkFile.getParentFile().path} -autosign"
executeJiaGuCMD(startJiaGu)
}
/**
* 根據(jù)后綴查找匹配的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/ }
}
/**
* 執(zhí)行命令
* @param cmd 命令
*/
private void executeJiaGuCMD(String cmd) {
println cmd
def process = cmd.execute()
println process.text
process.waitFor() // 用以等待外部進(jìn)程調(diào)用結(jié)束
println process.exitValue()
}
//上傳蒲公英托管 curl --help查看命令 -F指定HTTP分段POST數(shù)據(jù)(H)
private def uploadPGY(String filePath) {
def stdout = new ByteArrayOutputStream()
exec {
//設(shè)置要使用的可執(zhí)行文件的名稱。
executable = 'curl'
args = ['-F', "file=@${filePath}", '-F', "_api_key=${_api_key}", '-F', "buildInstallType=${buildInstallType}", '-F', "buildPassword=${buildPassword}", "${uploadUrl}"]
//設(shè)置輸出流以使用執(zhí)行命令的進(jìn)程的標(biāo)準(zhǔn)輸出。該過程完成后,流將關(guān)閉,修改命令輸出的地方,默認(rèn)為控制臺
standardOutput = stdout
}
String output = stdout.toString()
def parsedJson = new JsonSlurper().parseText(output)
println output
println "應(yīng)用二維碼地址:" + parsedJson.data.buildQRCodeURL
println "版本號:" + parsedJson.data.buildVersion
}
gradle中主要流程是:
1、經(jīng)過微信的resguardRelease打包,輸出結(jié)果;
2、將resguardRelease輸出的apk文件使用360加固保進(jìn)行加固;
3、對加固完成的apk文件,使用Walle打多渠道包;
4、最后一步就是上傳蒲公英托管平臺;
熟悉gradle應(yīng)該知道,gradle的執(zhí)行過程。
/**
* Gradle執(zhí)行配置階段之后,對我們的配置進(jìn)行初始化
*/
project.afterEvaluate {
initApkOutDir()
initSignInfo()
initCmd()
}
對于我們的打包gradle,首先我們會在配置階段做一些初始化操作,如:project.afterEvaluate閉包就是在配置完成時h,比如:獲取打包apk的路徑和apk簽名文件信息等等,代碼如下:會調(diào)用這個閉包,接下來就是獲取相關(guān)的數(shù)據(jù)的代碼。
/**
* 初始化打包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數(shù)據(jù)(H)
private def uploadPGY(String filePath) {
def stdout = new ByteArrayOutputStream()
exec {
//設(shè)置要使用的可執(zhí)行文件的名稱。
executable = 'curl'
args = ['-F', "file=@${filePath}", '-F', "_api_key=${_api_key}", '-F', "buildInstallType=${buildInstallType}", '-F', "buildPassword=${buildPassword}", "${uploadUrl}"]
//設(shè)置輸出流以使用執(zhí)行命令的進(jìn)程的標(biāo)準(zhǔn)輸出。該過程完成后,流將關(guān)閉,修改命令輸出的地方,默認(rèn)為控制臺
standardOutput = stdout
}
String output = stdout.toString()
def parsedJson = new JsonSlurper().parseText(output)
println output
println "應(yīng)用二維碼地址:" + parsedJson.data.buildQRCodeURL
println "版本號:" + parsedJson.data.buildVersion
}
當(dāng)然除了這種直接在Gradle文件中處理的方式,我覺得可以將這些庫整合成插件會比較方便使用,在我們項目中已經(jīng)把AndResGuard+ 360+Walle+蒲公英整合到一個插件中了,到此我們的自動化加固打包已經(jīng)結(jié)束了。