Android Gradle 自動化多渠道打包

前言

研究這個Gradle自動化腳本初衷是爲了實現自動化打包、加固和增加多渠道,從而一鍵完成應用發佈上架應用市場前的所有操作,以達到解放雙手和節約時間成本的效果。後期有考慮配合curl指令將打包好的apk自動上傳到服務器或者託管平臺,亦或可結合Jenkins自動化構建、打包、上傳等,從而實現整個流程的自動化目的,不過目前最火的應該是將GitLab Auto DevOps與Kubernetes集羣配合使用,來實現持續化集成與自動化部署,有興趣的可以自行去了解下。

作者:摺扇畫卿顏
鏈接:https://juejin.im/post/6844904138132684808

App打包發佈前準備

通常我們App上架到應用市場基本上都經歷過以下流程,先本地打一個release包,然後通過在線加固或者下載加固工具進行加固,由於加固會先剔除簽名信息,所以加固後要進行再次簽名,然後生成多渠道包,這樣基本上整個流程就結束了,畫了個思維導圖如下:

加固介紹

我的簡單理解就是給原有的apk進行加密和套殼,產生一個新的apk,然後運行的時候會進行解密相關的動作,所以加固後的app一般會影響啓動時間,網上也有很多加固平臺的對比,主要涉及啓動時間、包體積大小、兼容性、安全性等等。本次研究只是討論如何實現自動化加固與多渠道打包思想,360加固並非最好選擇,加固主要是爲了防止應用在上線後被反編譯、調試、破解、二次打包和內存截取等多種威脅。

下載360加固保

本次Gradle自動化實踐的步驟主要是基於360加固+騰訊的VasDolly多渠道打包。

  • 手動下載
    官方主要提供Windows、Mac、Linux三種版本,下載地址
  • 自動下載
    如果是Mac系統的話,在Gradle中直接使用curl命令即可,如果是Windwos需要下載安裝curl,curl主要是文件傳輸工具,可以通過命令行可支持文件下載及傳輸。
/**
 * 自動下載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)
        }
    }
}

打一個release包

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自動簽名,如果大家想自己手動簽名,下面我給出一套方案,主要是利用 zipalignapksigner命令 他們都是位於SDK文件中的build-tools目錄中,我們執行自動化簽名需要gradle配置好路徑。

  • 對齊未簽名的apk
zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.apk
  • 使用你的私鑰爲apk簽名
apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
  • 驗證apk是否已經被簽名
apksigner verify my-app-release.apk

基於加固Apk自動實現多渠道

關於多渠道打包,我們之前項目一直使用的是騰訊的VasDolly,故我們此次是採取VasDolly命令,但是需要先下載VasDolly.jar,至於放在什麼位置沒有要求,只需要gradle配置好路徑即可,我直接是放在項目根目錄。也可以使用360的多渠道加固,實際上整套都可以使用360加固提供的命令。

/**
 * 騰訊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下載地址
    ]

gradle相關基礎

  • gradle腳本插件的引用
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依賴
task B {
    // TaskB依賴TaskA,故會先執行TaskA
    dependsOn A
    //其次執行packersRelease
    doLast {
        println "B"
    }
}
  • task排序
//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.im託管平臺 入口
方式一: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 "[email protected]"            \
       -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腳本帶來的便利之處,下面我貢獻上自己的整個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自動化加固與多渠道打包就隨手點個贊吧,你的點贊是我寫作的動力!

文末

紙上說來終覺淺,牆裂建議時間比較充裕的小夥伴去B站觀看視頻講解:怎樣快速生成上百個渠道包;多渠道打包原理詳解

歡迎關注我的簡書,分享Android乾貨,交流Android技術。
對文章有何見解,或者有何技術問題,都可以在評論區一起留言討論,我會虔誠爲你解答。
也歡迎大家來我的B站找我玩,有各類Android架構師進階技術難點的視頻講解
B站直通車:https://space.bilibili.com/544650554

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章