Tinker + Bugly + Jenkins 爬坑之路

前陣子 Android 端的線上崩潰比較多,熱修復被提上日程。實現方案是 Tinker,Jenkins 打包,最後補丁包上傳到 Bugly 進行分發。主要在 Jenkins 打包這一塊爬了很多坑,現記錄下來,供你們參考。android

1. Tinker + Bugly熱修復實現

首先是本地實現,按照官方文檔,只要一步一步按照文檔來,這個步驟仍是比較容易的,這裏就再也不贅述了,不懂的能夠先參考官方文檔: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

2. 結合 Jenkins 所遇到的坑

先說明一下我司使用 Jenkins 打包 apk 的背景知識。Jenkins 打包 apk 使用的是 Ant 插件,打包腳本因爲公司項目的緣由,不方便展現出來,你們若是有疑問的話,能夠在評論裏說明,本人會私下裏幫助你們解決。app

下面爬坑 /(ㄒoㄒ)/~~運維

坑1 ☞ 打補丁包時,基準包哪裏找?

因爲公司 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"

坑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
相關文章
相關標籤/搜索