前阵子 Android 端的线上崩溃比较多,热修复被提上日程。实现方案是 Tinker,Jenkins 打包,最后补丁包上传到 Bugly 进行分发。主要在 Jenkins 打包这一块爬了不少坑,现记录下来,供大家参考。

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=$;

# 增量包需分开打包,否则会失败
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

Tinker + Bugly + Jenkins 爬坑之路的更多相关文章

  1. Tinker爬坑之路

    目的 热修复去年年底出的时候,变成了今年最火的技术之一.依旧记得去年面试的时候统一的MVP,然而今年却变成了RN,热修复.这不得不导致我们需要随时掌握最新的技术.不然可能随时会被淘汰.记得刚进公司,技 ...

  2. 多线程爬坑之路-Thread和Runable源码解析之基本方法的运用实例

    前面的文章:多线程爬坑之路-学习多线程需要来了解哪些东西?(concurrent并发包的数据结构和线程池,Locks锁,Atomic原子类) 多线程爬坑之路-Thread和Runable源码解析 前面 ...

  3. Vue 爬坑之路(六)—— 使用 Vuex + axios 发送请求

    Vue 原本有一个官方推荐的 ajax 插件 vue-resource,但是自从 Vue 更新到 2.0 之后,官方就不再更新 vue-resource 目前主流的 Vue 项目,都选择 axios ...

  4. Vue 爬坑之路(九)—— 用正确的姿势封装组件

    迄今为止做的最大的 Vue 项目终于提交测试,天天加班的日子终于告一段落... 在开发过程中,结合 Vue 组件化的特性,开发通用组件是很基础且重要的工作 通用组件必须具备高性能.低耦合的特性 为了满 ...

  5. Vue 爬坑之路(一)—— 使用 vue-cli 搭建项目

    vue-cli 是一个官方发布 vue.js 项目脚手架,使用 vue-cli 可以快速创建 vue 项目,GitHub地址是:https://github.com/vuejs/vue-cli vue ...

  6. Vue 爬坑之路(十二)—— vue-cli 3.x 搭建项目

    Vue Cli 3 官方文档:https://cli.vuejs.org/zh/guide/ 一.安装 @vue/cli 更新到 3.x 之后,vue-cli 的包名从 vue-cli 改成了 @vu ...

  7. Android爬坑之路

    做了那么久前端,现在终于可以回到我的老本行, 今天我用了一天的时间配置里Android开发环境,mac和windows双平台,eclipse和IDEA双平台,别问为什么,我就喜欢,中间大坑不断,再加上 ...

  8. 安卓易学,爬坑不易——腾讯老司机的RecyclerView局部刷新爬坑之路

    针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验.目前功能还在免费开放中. 点击地址:http://wetest ...

  9. 安卓易学,爬坑不易—腾讯老司机的RecyclerView局部刷新爬坑之路

    前言 安卓开发者都知道,RecyclerView比ListView要灵活的多,但不可否认的里面的坑也同样埋了不少人.下面让我们看看腾讯开发工程师用实例讲解自己踩坑时的解决方案和心路历程. 话说有图有真 ...

随机推荐

  1. Secondary NameNode究竟是做什么的

    Secondary NameNode:它究竟有什么作用? 在hadoop中,有一些命名不好的模块,Secondary NameNode是其中之一.从它的名字上看,它给人的感觉就像是NameNode的备 ...

  2. Python离线安装依赖包

    1.制作requirement.txt pip freeze > requirement.txt 2.下载离线Pytho安装包 pip download -r requirement.txt - ...

  3. 深度学习(二)BP求解过程和梯度下降

    一.原理 重点:明白偏导数含义,是该函数在该点的切线,就是变化率,一定要理解变化率. 1)什么是梯度 梯度本意是一个向量(矢量),当某一函数在某点处沿着该方向的方向导数取得该点处的最大值,即函数在该点 ...

  4. PHP之string

    string addcslashes() Quote string with slashes in a C style 以 C 语言风格使用反斜线转义字符串中的字符 addslashes() Quot ...

  5. JVM启动报错: Could not reserve enough space for object heap error

    首先了解一下参数的含义: 参数 含义 -Xms2G -Xmx2G 代表jvm可用的heap内存最小和最大 -XX:PermSize -XX:MaxPermSize 代表jvm的metadata内存的大 ...

  6. ZOJ 2971 Give Me the Number

    Give Me the Number Numbers in English are written down in the following way (only numbers less than  ...

  7. Nginx教程(6) 动静分离架构

    一.原理 Nginx 动静分离简单来说就是把动态跟静态请求分开,不能理解成只是单纯的把动态页面和静态页面物理分离.严格意义上说应该是动态请求跟静态请求分开,可以理解成使用Nginx 处理静态页面,To ...

  8. vue之生命周期的一点总结

    vue的生命周期的过程提供了我们执行自定义逻辑的机会,好好理解它的生命周期,对我们很有帮助. 一.vue实例的生命周期(vue2.0) 二.生命周期描述:(参考截图) 三.例子 window.vm = ...

  9. java并发编程(4)性能与可伸缩性

    性能与可伸缩性 一.Amdahl定律 1.问题和资源的关系 在某些问题中,资源越多解决速度越快:而有些问题则相反: 注意:每个程序中必然有串行的部分,而合理的分析出串行和并行的部分对程序的影响极大:串 ...

  10. php 在函数前面加个@的作用

    @是错误控制运算符,用屏蔽错误提示比如:@mysql_connect() 不会出现Warning, 而原来mysql_connect 会在页面上访提示Warning.主要是高版本的php不在支持mys ...