前陣子 Android 端的線上崩潰比較多,熱修復被提上日程。實現方案是 Tinker,Jenkins 打包,最後補丁包上傳到 Bugly 進行分發。主要在 Jenkins 打包這一塊爬了很多坑,現記錄下來,供你們參考。android
首先是本地實現,按照官方文檔,只要一步一步按照文檔來,這個步驟仍是比較容易的,這裏就再也不贅述了,不懂的能夠先參考官方文檔:Bugly Android熱更新使用指南、Bugly Android熱更新詳解。這裏貼一下接入流程:git
- 打基準包安裝並上報聯網(注:填寫惟一的 tinkerId)
- 對基準包的 bug 修復(能夠是 Java 代碼變動,資源的變動)
- 修改基準包路徑、修改補丁包 tinkerId、mapping 文件路徑(若是開啓了混淆須要配置)、resId 文件路徑
- 執行 buildTinkerPatchRelease 打 Release 版本補丁包
- 選擇 app/build/outputs/patch目錄 下的補丁包並上傳(注:不要選擇 tinkerPatch 目錄下的補丁包,否則上傳會有問題)
- 編輯下發補丁規則,點擊當即下發
- 殺死進程並重啓基準包,請求補丁策略( SDK 會自動下載補丁併合成)
- 再次重啓基準包,檢驗補丁應用結果
- 查看頁面,查看激活數據的變化
這裏說一下使用指南
中的第三步:初始化 SDK,我這裏使用的是 enableProxyApplication = false
的方式,本來想用 enableProxyApplication = true
的這種比較靈活的方式,可是程序編譯報錯,沒時間去深究報錯的緣由,加上直接繼承的方式接入也沒什麼代價,就沒管是爲何了,知道緣由的能夠順手告知下。 ┑( ̄Д  ̄)┍github
一通擼下來仍是比較容易的,完成代碼的接入後,先打個包(基準包),安裝到手機上運行一遍,使程序聯網上報到 Bugly。以後,再按照打基準包的基線版本,修改 tinker-support.gradle
文件中的 baseApkDir
參數,而後就能夠打補丁包了。bash
先說明一下我司使用 Jenkins 打包 apk 的背景知識。Jenkins 打包 apk 使用的是 Ant 插件,打包腳本因爲公司項目的緣由,不方便展現出來,你們若是有疑問的話,能夠在評論裏說明,本人會私下裏幫助你們解決。app
下面爬坑 /(ㄒoㄒ)/~~運維
因爲公司 Jenkins 的打包策略是,在構建以前,先執行 clean
命令,這也就意味着,像本地打包同樣在 app/build/bakApk/app-xxxx-xx-xx-xx
目錄下找到基準包已經是不可能。那怎麼辦,沒有基準包怎麼打增量包?苦思良久,愚笨的我最終想到,在項目工程路徑下建立一個文件夾,要打增量包時,將基準包拷貝到該文件夾,而後上傳 SVN。這時,旁邊同窗來了句:能夠找運維同窗,雙方約定一個目錄,打基準包時將基準包由腳本拷貝過去,打補丁包時從約定的目錄取就行((ಥ _ ಥ) 我咋就想不到...)。ide
而後屁顛屁顛的跑去找運維同窗,溝通後發現,Jenkins 每次打包都會在 Jenkins 目錄下的 /jobs/pipeline名稱/builds/構建編號/archive/app/build/outputs/apk/kungeek/release/
保存一份 apk 文件的副本。路徑中 構件編號
如圖所示:gradle
接下來,打補丁包時將 tinker-support.gradle
文件中的 baseApkDir
參數修改成 /jobs/pipeline名稱/builds/構建編號/archive/app/build/outputs/apk/kungeek/release/
便可。代碼以下:ui
/** * 此處填寫每次構建生成的基準包目錄,注意變量要自定義 */ def baseApkDir = "${rootProject.projectDir}/../../jobs/${pipeline名稱}/builds/${baseApkBuildNumber}/archive/app/build/outputs/apk/kungeek/release"
因爲構建基準包的同時生成的 mapping 文件(若是開啓了混淆須要配置)、resId 文件在構建補丁包時也須要用到,因此,在構建基準包時,須要將這兩個文件拷貝到 /jobs/pipeline名稱/builds/構建編號/archive/app/build/outputs/apk/kungeek/release/
目錄下,拷貝代碼以下:編碼
<!--複製 tinker 生成的文件(apk文件、mapping.txt、R.txt)--> <copy todir="../../../../jobs/pipeline名稱/builds/${env.BUILD_NUMBER}/archive/app/build/outputs/apk/kungeek/release/" flatten="true"> <fileset dir="${android.root}/app/build/bakApk/"> <include name="*/*" /> </fileset> </copy>
注1:代碼中相對路徑問題讀者有疑問的話,麻煩再評論去提問。注2:代碼中構建編碼使用到了 Jenkins 的環境變量,須要先在 Ant 的構建腳本文件的
project
的標籤下添加<property environment="env"/>
來導入。
這裏遇到的坑是:由於 Tinker 構建的 apk 文件是存放在 app-xxxx-xx-xx-xx 目錄下,因此須要使用通配符來輔助複製文件,運維同窗本來是想將通配符加到 fileset
中造成之後完整的路徑,通過一段痛苦的嘗試以及百度後發現,通配符只能在 include
標籤中使用。(ノへ ̄、)
踩過前面一個一個的坑,終於在 Jenkins 上打了基準包以後,/jobs/pipeline名稱/builds/構建編號/archive/app/build/outputs/apk/kungeek/release/
目錄下有了 基準包 apk 文件、mapping 文件、resId 文件。
接下來,我覺得,只須要配置好基準包的構件編號等相關配置參數,再構建補丁包就沒問題了。而後 Jenkins 在構建好補丁包 apk 文件後,展現成果時報出的 apk 文件未找到
給了我當頭一棒,依然失敗。挫敗感油然而生~~~
以後,經運維同窗確認,Jenkins 構建期間是有在 app/build/outputs/patch
目錄下生成 patch_signed_7zip.apk
文件的,可是構建完成以後,又沒了。而後我試着看了下構建過程當中執行的命令,長這樣的:
sh gradlew clean buildTinkerPatchRelease --stacktrace sh gradlew checklist
執行了 buildTinkerPatchRelease 後,還執行的 checklist 任務,難道是執行 checklist 時把 patch 給清空了,以後我嘗試把這個命令註釋掉,再次打補丁包時成功。果真是這個 checklist 惹的事啊,過後發現,打補丁包後,再次執行 gradle task,基本都會清空 patch 目錄,這是個坑,你們記得避免。
咱們知道,在 Android Studio 中,一個 project 能夠有多個 module,包括 application 類型的 module,通常狀況下,執行 gradlew assembleRelease
任務會將全部的 APP 都打包,這裏打基準包也沒問題,可是打補丁包時就不行了,只能成功一個。
這裏提供分開打包一個方案:在每一個 application 的 build.gradle 中配置 productFlavors,且每一個 application 的命名都得不同,這樣,針對不一樣的 APP 就會產生不一樣的構建 task,好比:在 A 的 build.gradle 中配置名爲 a_app,則回產生一個名爲 buildTinkerPatchA_appRelease 的 task,最終使用此 task 來打補丁包便可。
那麼問題來了,最終打包的形式是什麼呢?是這樣?
sh gradlew buildTinkerPatchA_appRelease buildTinkerPatchA_appRelease
仍是這樣?
sh gradlew buildTinkerPatchA_appRelease sh gradlew buildTinkerPatchA_appRelease
都不是,這兩種方式其實和不配置 productFlavors 的打包方式是同樣的,那麼如何打包呢?
答案是在 Ant 的打包腳本中,執行屢次打包,關鍵代碼以下:
<!--構建APP a--> <exec dir="." executable="bash" failonerror="false"> <arg value="generated_apk_hotfix.sh"/> <arg value="buildTinkerPatchApp_aRelease"/> </exec> <!--構建APP b--> <exec dir="." executable="bash" failonerror="false"> <arg value="generated_apk_hotfix.sh"/> <arg value="buildTinkerPatchApp_bRelease"/> </exec>
構建腳本 generated_apk_hotfix.sh 文件關鍵代碼以下:
#!/bin/sh command=$1; # 增量包需分開打包,不然會失敗 sh gradlew ${command} --stacktrace
上面說到的坑只有 4 點,但實際上也遇到過挺多小問題的,但那些就不用多說了,很容易解決。
最後,總結一下結合 Jenkins 構建補丁包的思路。
首先,約定好基線版本的基準包 apk 包、mapping 文件、R.txt 文件的存放路徑,打基準包時將這三個文件存入該目錄。若是跟本文同樣存放在 Jenkins 的 pipeline 構建目錄下的話,記得要調整 pipeline 的清理策略,不然等須要打補丁包的時候,發現基線版本 apk 包什麼的被清理掉就尷尬了,我這裏是考慮到重複利用空間,因此放入此目錄下。
其次,經過約定的路徑,找到基準包、mapping 文件、R.txt 文件,打補丁包。這裏須要肯定一個找到基準包的策略,好比,我這裏是經過構建編號來匹配存放基準包的路徑,而後經過固定命名格式(如:app_release_版本號.apk)來匹配基準包以及 mapping 文件和 R.txt 文件,如此下來,我只須要肯定基線版本的版本號和構建編號便可。
最後,貼一下我最終的 tinker-support.gradle 文件代碼內容,你們有須要的能夠參考:
apply plugin: 'com.tencent.bugly.tinker-support' def bakPath = file("${buildDir}/bakApk/") /** 基準包的 Jenkins 構建編號*/ def baseApkBuildNumber = project.property("baseApkBuildNumber") /** 基準包的版本號*/ def baseApkVersion = project.property("baseApkVersion") /** * 此處填寫每次構建生成的基準包目錄 */ def baseApkDir = "${rootProject.projectDir}/../../jobs/Android_Trunk/builds/${baseApkBuildNumber}/archive/app/build/outputs/apk/release" /** 基準包的 apk 文件名*/ def baseApkFileName = "app-v${baseApkVersion}" /** * 對於插件各參數的詳細解析請參考 */ tinkerSupport { // 開啓tinker-support插件,默認值true enable = true // tinkerEnable功能開關 tinkerEnable = true // 指定歸檔目錄,默認值當前module的子目錄tinker autoBackupApkDir = "${bakPath}" autoGenerateTinkerId = true // 打基準包時生成 R.txt、mapping.txt 文件名的前綴 // rootProject.ext.android_version 指打包時的版本號 targetFileNamePrefix = "app-v${rootProject.ext.android_version}" // 是否啓用覆蓋tinkerPatch配置功能,默認值false // 開啓後tinkerPatch配置不生效,即無需添加tinkerPatch overrideTinkerPatchConfiguration = true // 編譯補丁包時,必需指定基線版本的apk,默認值爲空 // 若是爲空,則表示不是進行補丁包的編譯 // @{link tinkerPatch.oldApk } baseApk = "${baseApkDir}/${baseApkFileName}.apk" // 對應tinker插件applyMapping baseApkProguardMapping = "${baseApkDir}/${baseApkFileName}-mapping.txt" // 對應tinker插件applyResourceMapping baseApkResourceMapping = "${baseApkDir}/${baseApkFileName}-R.txt" tinkerId = "base-1.0.1" // buildAllFlavorsDir = "${bakPath}/${baseApkDir}" // 是否開啓加固模式,默認爲false // isProtectedApp = true // 是否開啓反射Application模式 enableProxyApplication = false supportHotplugComponent = true } /** * 通常來講,咱們無需對下面的參數作任何的修改 * 對於各參數的詳細介紹請參考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */ tinkerPatch { //oldApk ="${bakPath}/${appName}/app-release.apk" // tinkerEnable功能開關 tinkerEnable = true ignoreWarning = false useSign = true dex { dexMode = "jar" pattern = ["classes*.dex"] loader = [] } lib { pattern = ["lib/*/*.so"] } res { pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] ignoreChange = [] largeModSize = 100 } packageConfig { } sevenZip { zipArtifact = "com.tencent.mm:SevenZip:1.1.10" } buildConfig { keepDexApply = false } }
而後是維護在 gradle.properties 文件中的兩個變量:
# 打增量包時基準包的 Jenkins 構建編號 baseApkBuildNumber = 1 # 打增量包時基準包的版本號 baseApkVersion = 1.0.0.197094