本文导语:
本文的核心内容介绍:
(1)对比当前市场上的热修复方案,对Tinker热修复方案进行了简单的介绍。
(2)详细讲解了微信Tinker的完整接入过程,文末提供了一个自己写的非常轻量的Demo,可以帮助开发者迅速实现自己项目中热修复的接入,将热修复技术运用到真实的项目中,而不仅仅是Demo测试。
(3)加入了walle的多渠道打包方案,能迅速打出很多个渠道包。详细的介绍了真实项目上线时APK及补丁包的版本维护,如何通过单个补丁包,修复多个渠道,进行热修复的实现方案。
(4)文章末尾总结了接入过程中可能遇到的坑,及相应的解决方案,能帮助你无障碍的接入Tinker。以及简单的分享了一些关于热修复技术方面需要储备的一些技术知识。
希望读完本文的朋友,能对热修复及相关的技术和概念,有比较深的理解。
一、Tinker热修复方案原理简介
1、腾讯官方介绍:
Tinker is a hot-fix solution library for Android, it supports dex, library and resources update without reinstalling apk.
Tinker 是一个开源项目(Github链接),它是微信官方的 Android 热补丁解决方案,它支持动态下发代码、So 库以及资源,让应用能够在不需要重新安装的情况下实现更新。
image.png
2、Tinker原理理解:
Tinker将old.apk(也就是下面要讲到的基准包,上线发布时的APK)和new.apk,进行对比,得到patch.dex,然后应用程序通过在代码中加入初始化tinker的代码,可以实现在程序运行的时候加载patch.dex(补丁文件),然后patch.dex与本机APK的classex.dex合并,生成新的classes.dex。运行时通过反射将合并后的dex文件放置在加载的dexElements数组的前面。运行时替代的原理,其实和Qzone的方案差不多,都是去反射修改dexElements。
3、为什么要使用热修复?
(1) 看看传统的App升级更新流程:
image.png
如上图,随着移动端业务复杂程度的增加,传统的APP更新流程显然无法满足业务和开发者的需求,无论是对于用户还是开发维护人员,过程过于繁琐,不够灵活。
主要存在以下几个弊端:
- 对于开发者而言,重新发布版本代价太大。
- 用户下载安装成本太高,可能失去耐心而直接卸载。
- bug修复不及时,用户体验差。
(2) 再看看热修复的开发流程,明显更加灵活。
- image.png
热修复的几大优势: - 无需重新发版,实时高效,开发的维护成本降低。
- 补丁静默安装,用户无感知就能实现Bug的修复,体验好。
- 修复成功率高,把损失降到最低。
4、为何选择使用腾讯的Tinker修复方案?
当前市面的热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的超级补丁方案。但它们都存在无法解决的问题。其中AndFix可能接入是最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的;QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题;美团提出的思想方案主要是基于Instant Run的原理,兼容性比较好,但目前尚未开源。
使用Tinker的原因:
Tinker热补丁方案不仅支持类、So 以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做 bugfix,甚至可以替代功能的发布。Tinker 已运行在微信的数亿 Android 设备上,那么为什么你不使用 Tinker 呢?
(偷偷告诉你:其实现在最好的热修复方案,是阿里2017年6月份发布的新一代非侵入式Android热修复方案——Sophix,不过本人是在去年上半年就开始使用热修复技术了,所以那会市面上的热修复技术,相较而言,Tinker是最优的选择,而且也经过了本人实际项目中的使用,所以我觉得大家在项目中如果还没有使用过热修复,那Tinker是很不错的选择,毕竟Tinker 已运行在微信的数亿 Android 设备上。对于阿里的Sophix,有兴趣的研究的朋友们,推荐大家可以去研读《Android热修复技术原理》)
二、Tinker接入步骤详解
①工程的根目录的build.gradle中配置:
// TinkerPatch 插件
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.6"
compile "com.android.support:multidex:1.0.1"
②app的build.gradle中添加TinkerPatch的SDK依赖:
compile "com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.6"
③为了简单方便,我们将 TinkerPatch 相关的配置都放于tinkerpatch.gradle中, 我们需要在app的build.gradle中将其引入:
apply from: 'tinkerpatch.gradle'
④tinkerpatch.gradle将其放在跟build.gradle同一级目录即可,tinkerpatch.gradle中的完整配置如下。
project-structure.png
apply plugin: 'tinkerpatch-support'
/**
* TODO: 请按自己的需求修改为适应自己工程的参数
*/
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.0-0527-01-08-12"
def variantName = "release"
/**
* 对于插件各参数的详细解析请参考
* http://tinkerpatch.com/Docs/SDK
*/
tinkerpatchSupport {
/** 可以在debug的时候关闭 tinkerPatch **/
/** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,
这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加
你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名, com.xxx前缀的包名不用修改
**/
tinkerEnable = true
reflectApplication = true
/**
* 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
* 如果只在某个渠道使用了加固,可使用多flavors配置
**/
protectedApp = false
/**
* 实验功能
* 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
**/
supportComponent = true
autoBackupApkPath = "${bakPath}"
appKey = "在tinkpatch管理后台创建你的应用,会有一个唯一的appkey值,填入此处即可"
/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.0"
def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
/**
* 若有编译多flavors需求, 可以参照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample
* 注意: 除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息(相关工具:walle 或者 packer-ng)
**/
}
/**
* 用于用户在代码中判断tinkerPatch是否被使能
*/
android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
}
}
/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
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"
// path = "/usr/local/bin/7za"
}
buildConfig {
keepDexApply = false
}
}
⑤打出基准包。
image.png
⑥安装基准包的效果
baseapk.png
⑥模拟热修复,修改代码,打出补丁包:注意,baseApk的信息一定要和生成的基准包的路径名称匹配,否则无法成功打出补丁包。
image.png
⑦在TinkerPatch Platform,创建你的应用,发布补丁:
add_patch_version.png
realease_patch.png
watch.png
⑧点击拉取补丁,然后退出App,重新启动,查看日志,成功拉取补丁的日志及App运行效果如下图所示:
logcat.png
image.png
⑨查看后台补丁监控信息,可以看到补丁下发的情况:
image.png
到此,接入Tinker就完成了。实际项目中,咱们的应用肯定是要在各大应用市场上线的,那么肯定要打多个渠道包,按照常规,我们是采用productFlavors实现的。假设项目要打10个渠道包,那么得针对每个渠道包,分开打10个补丁包,这显然是不合理的。针对这种需求,Tinker官方给我们提供了多渠道打包的方案,如下图:
image.png
所以我今天要介绍的就是官方推荐的一种方案:使用walle实现多渠道打包。
三、Tinker结合Walle多渠道打包的使用详解
Walle(瓦力):是美团开源的Android Signature V2 Scheme 签名下的新一代渠道包打包神器,跟gradle打包不一样,walle是在APK Signature Block区块添加自定义的渠道信息来生成渠道包,从而提高了渠道包生成效率,可以作为单机工具来使用,也可以部署在HTTP服务器上来实时处理渠道包Apk的升级网络请求。 ---Walle的介绍
1.在项目根目录的bulid,gradle文件中添加Walle插件的依赖:
classpath 'com.meituan.android.walle:plugin:1.1.6'
2.在当前App的 build.gradle 文件中apply这个插件,并添加上用于读取渠道号的AAR
apply plugin: 'walle'
dependencies {
compile 'com.meituan.android.walle:library:1.1.6'
}
//配置插件
walle {
// 指定渠道包的输出路径
apkOutputFolder = new File("${project.buildDir}/outputs/channels");
// 定制渠道包的APK的文件名称
apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk';
// 渠道配置文件
channelFile = new File("${project.getProjectDir()}/channel.txt")
}
3.配置项具体解释:
- apkOutputFolder:指定渠道包的输出路径, 默认值为
new File("${project.buildDir}/outputs/apk")
- apkFileNameFormat:定制渠道包的APK的文件名称, 默认值为
'${appName}-${buildType}-${channel}.apk'
可使用以下变量:
projectName - 项目名字
appName - App模块名字
packageName - applicationId (App包名packageName)
buildType - buildType (release/debug等)
channel - channel名称 (对应渠道打包中的渠道名字)
versionName - versionName (显示用的版本号)
versionCode - versionCode (内部版本号)
buildTime - buildTime (编译构建日期时间)
fileSHA1 - fileSHA1 (最终APK文件的SHA1哈希值)
flavorName - 编译构建 productFlavors 名
4.在App目录下新建channel.txt配置所需要的渠道。具体内容格式详见:渠道配置文件示例,支持使用#号添加注释。
5.生成渠道包——在Android Studio的控制台—Teminal输入:
①生成所有渠道的渠道: gradlew clean assembleReleaseChannels
②生成某一个渠道:gradlew clean assembleReleaseChannels -PchannelList=baidu
③生成指定的多个渠道包 ./gradlew clean assembleReleaseChannels -PchannelList=baidu,xiaomi
Build成功后的效果如下图:
walle_build_apk_sucessful.png
生成的多渠道包的目录如下图:
image.png
6.针对多渠道打出适用所有渠道包的补丁包:使用tinkerPatchRelease打出补丁包,具体如下图所示。
image.png
适用于所有渠道的补丁包的位置如下:
image.png
7.获取渠道信息
String channel = WalleChannelReader.getChannel(this.getApplicationContext());
四、接入Tinker热修复和多渠道打包后,项目线上APK的发布及补丁包发布的维护。
(1)多渠道APK的发布:
每次上线时,只需要执行上面生成渠道包的命令,打出多个渠道的APK即可,将各个渠道分发到各个应用市场即可。根据项目需求,可以通过获取渠道信息,进行渠道统计。切记每次发布新版本时,一定要备份好bacApk目录的文件,发布补丁的时候需要。因为一旦丢失,就失去了基准包的信息了,就无法打出相应基准包的补丁包了。
(2)补丁包的发布:
当线上APK出现bug需要修复时,在tinkerPatch.gradle中配置好你线上发布的基准包的信息(之前备份的基准包),使用tinkerPatchRelease打出补丁包,在TinkerPatch管理后台下发补丁。具体如上步骤6.
配置基准包信息:
image.png
五、接入时可能遇到的问题:
(1)接入Tinker时,打包的时候出现以下错误com.tencent.tinker.loader.TinkerRuntimeException: Tinker Exception:applicationLike must not be null.:是因为你的 tinkerPatch.gradle中配置 reflectApplication = false,但是你又没有相应的改造你的Application类。本文介绍的是不改造我们的 Application 类接入Tinker,所以 配置应该为:reflectApplication = ture。
image.png
(2)多渠道打包时,出现下面错误
- What went wrong:
Execution failed for task ':app:compileReleaseJavaWithJavac'.
Could not find tools.jar. Please check that D:\develop\JRE-New contains a valid JDK installation.
这个错误是因为在安装JDK时,会安装两次,一次安装JDK,一次安装jre,因为第一次JDK的安装就已经安装了一个jre,而安装时的提示会再次安装一个jre。所以在第二次安装jre时,先暂停,你需要将第一次安装JDK的目录下的一个/jre文件夹删掉,然后在安装另一个jre,这样就可以了。再重新执行walle打包的命令,就能成功打出多渠道包了。
image.png
(3)Demo打开运行时,如果提示下面问题, Rebuild一下工程或者将implementation 'com.android.support:appcompat-v7:26.1.0'改成 implementation 'com.android.support:appcompat-v7:27.1.1'即可:
Error:Execution failed for task ':app:preDebugAndroidTestBuild'.
Conflict with dependency 'com.android.support:support-annotations' in project ':app'. Resolved versions for app (26.1.0) and test app (27.1.1) differ. See https://d.android.com/r/tools/test-apk-dependency-conflicts.html for details.
(4)执行多渠道打包命令(如gradlew clean assembleReleaseChannels)时,若提示如下BUILD FAILED的信息,Rebuild一下工程再执行打包命令即可正常打包。
- What went wrong:
Execution failed for task ':app:clean'.
Unable to delete file: C:\Users\18673\Desktop\TinkerPatchDemo-master\app\build\intermediates\manifests\full\debug\AndroidManifest.xml
知识储备:
1、关于Dex
Dex是Android平台上可执行文件的类型,是可以在Dalvik虚拟机上直接运行的文件格式。Java源代码经过ADT的复杂编译后转换成Dex文件,这是一个逐步优化的过程。Dex文件的指令码就是Dalvik虚拟机专有的一套指令集,相比标准java的.class文件,它体积小,运行效率高。
2、Android JVM的运行过程
3、关于虚拟机Dalvik
- 每一个Android应用都运行在一个Dalvik虚拟机实例里,而每一个虚拟机实例都是一个独立的进程空间。虚拟机的线程机制,内存分配和管理,Mutex等等都是依赖底层操作系统而实现的。
- 所有Android应用的线程都对应一个Linux线程,虚拟机因而可以更多的依赖操作系统的线程调度和管理机制。
- 不同的应用在不同的进程空间里运行,加之对不同来源的应用都使用不同的Linux用户来运行,可以最大程度的保护应用的安全和独立运行。
4、关于ART模式
ART模式英文全称为:Android runtime,谷歌Android 4.4系统新增的一种应用运行模式,与传统的Dalvik模式不同,ART模式可以实现更为流畅的安卓系统体验。
Android系统是以Linux系统为底层构建的。谷歌为了降低应用的开发难度在Linux底层之上构筑了一个名为“Dalvik”的虚拟机。
因为Dalvik虚拟机的存在,Android系统的开发者只需使用谷歌提供的SDK(软件开发工具包)即可较为轻松的按照一套“规则”创建APP,不用顾忌硬件、驱动等问题,在每次执行应用的时候Dalvik虚拟机都会将程序的语言由高级语言编译为机器语言,这样当前设备才能够运行这一应用。
ART模式与Dalvik模式最大的不同在于,在启用ART模式后,系统在安装应用的时候会进行一次预编译,在安装应用程序时会先将代码转换为机器语言存储在本地,这样在运行程序时就不会每次都进行一次编译了,执行效率也大大提升。
本文完整Demo GitHub下载地址请戳:TinkerPatchDemo
本文参考:
Tinker源码TinkerPatch 接入及平台使用文档Android 热修复 Tinker接入及源码浅析—hongyangMultiDex与热修复实现原理Tinker加入Walle多渠道打包