1. Tinker + Bugly熱修復實現
首先是本地實現,按照官方文檔,只要一步一步按照文檔來,這個步驟還是比較容易的,這里就不再贅述了,不懂的可以先參考官方文檔:Bugly Android熱更新使用指南、Bugly Android熱更新詳解。這里貼一下接入流程:
打基准包安裝並上報聯網(注:填寫唯一的 tinkerId)
對基准包的 bug 修復(可以是 Java 代碼變更,資源的變更)
修改基准包路徑、修改補丁包 tinkerId、mapping 文件路徑(如果開啟了混淆需要配置)、resId 文件路徑
執行 buildTinkerPatchRelease 打 Release 版本補丁包
選擇 app/build/outputs/patch目錄 下的補丁包並上傳(注:不要選擇 tinkerPatch 目錄下的補丁包,不然上傳會有問題)
編輯下發補丁規則,點擊立即下發
殺死進程並重啟基准包,請求補丁策略( SDK 會自動下載補丁並合成)
再次重啟基准包,檢驗補丁應用結果
查看頁面,查看激活數據的變化
這里說一下使用指南
中的第三步:初始化 SDK,我這里使用的是 enableProxyApplication = false
的方式,原本想用 enableProxyApplication = true
的這種比較靈活的方式,但是程序編譯報錯,沒時間去深究報錯的原因,加上直接繼承的方式接入也沒什么代價,就沒管是為什么了,知道原因的可以順手告知下。 ┑( ̄Д  ̄)┍
一通擼下來還是比較容易的,完成代碼的接入后,先打個包(基准包),安裝到手機上運行一遍,使程序聯網上報到 Bugly。之后,再按照打基准包的基線版本,修改 tinker-support.gradle
文件中的 baseApkDir
參數,然后就可以打補丁包了。
2. 結合 Jenkins 所遇到的坑
先說明一下我司使用 Jenkins 打包 apk 的背景知識。Jenkins 打包 apk 使用的是 Ant 插件,打包腳本由於公司項目的原因,不方便展示出來,大家如果有疑問的話,可以在評論里說明,本人會私下里幫助大家解決。
下面爬坑 /(ㄒoㄒ)/~~
坑1 ☞ 打補丁包時,基准包哪里找?
由於公司 Jenkins 的打包策略是,在構建之前,先執行 clean
命令,這也就意味着,像本地打包一樣在 app/build/bakApk/app-xxxx-xx-xx-xx
目錄下找到基准包已是不可能。那怎么辦,沒有基准包怎么打增量包?苦思良久,愚笨的我最終想到,在項目工程路徑下創建一個文件夾,要打增量包時,將基准包拷貝到該文件夾,然后上傳 SVN。這時,旁邊同學來了句:可以找運維同學,雙方約定一個目錄,打基准包時將基准包由腳本拷貝過去,打補丁包時從約定的目錄取就行((ಥ _ ಥ) 我咋就想不到...)。
然后屁顛屁顛的跑去找運維同學,溝通后發現,Jenkins 每次打包都會在 Jenkins 目錄下的 /jobs/pipeline名稱/builds/構建編號/archive/app/build/outputs/apk/kungeek/release/
保存一份 apk 文件的副本。路徑中 構件編號
如圖所示:
接下來,打補丁包時將 tinker-support.gradle
文件中的 baseApkDir
參數修改為 /jobs/pipeline名稱/builds/構建編號/archive/app/build/outputs/apk/kungeek/release/
即可。代碼如下:
/** * 此處填寫每次構建生成的基准包目錄,注意變量要自定義 */ def baseApkDir = "${rootProject.projectDir}/../../jobs/${pipeline名稱}/builds/${baseApkBuildNumber}/archive/app/build/outputs/apk/kungeek/release"
坑2 ☞ Linux 下文件拷貝通配符問題
由於構建基准包的同時生成的 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
標簽中使用。(ノへ ̄、)
坑3 ☞ 構建補丁包完成后找不到生成的補丁包?
踩過前面一個一個的坑,終於在 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 目錄,這是個坑,大家記得避免。
坑4 ☞ 一個項目中多個 application 時,打補丁包不成功?
我們知道,在 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
3. 總結
上面說到的坑只有 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