七、测试
因为布局简单且不是重点,这里就给出一张Demo 的运行图片,剩下的就靠想像了。
![](https://img.kancloud.cn/1a/fc/1afcbc80abed29230f8344e68966ea49_347x573.jpg)
1、编译基础包
没有基础包,那要补丁有什么用?所以,第一步就是打包一个apk。
在Terminal 中使用命令行./gradlew assembleDebug。不会命令行无所谓,Android Studio 为我们提供了图形化操作,根据下图操作即可:
![](https://img.kancloud.cn/aa/3f/aa3f95f19bb25e89cc45a8b3513cda47_786x671.jpg)
如果你是要release 签名的打包,则双击assembleRelease,不过还要配置签名文件,这个后面再说。
编译完成后,可以在build 目录下会自动创建一个bakApk 文件夹,里面就有打包好的apk 文件,因为之后的所有生成的补丁包都以这个apk 会标准,所以这就是那个基础包文件(相当于应用市场上的app)。
![](https://img.kancloud.cn/d1/f5/d1f573d7ac31aabba0cdce617f51b227_351x256.jpg)
如果这个apk 文件是release 签名且是要放到应用市场上的,那么你必须将apk 与R.txt(如果有使用混淆的话,还会有一个mapping.txt)这几个文件保存好,切记。
现在就把这个tinker-local-debug-1206-11-48-42.apk 安装到手机上(相当于是用户手机上的app)。
![](https://img.kancloud.cn/58/0c/580c19a62b42fe0f2761f59739c38c50_342x567.jpg)
1.点击"say someting"按钮吐司"Hello"。
2.点击"get string from .so"按钮吐司"hello LQR"。
3.点击"show info"按钮显示"patch is not loaded",说明当前没有加载补丁。
2、修复java 代码
下面是"say someting"按钮点击时,调用的方法,使用Toast 显示Hello 字符串:
```
public void say(View view) {
Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_SHORT).show();
}
```
1)修复代码
现在我想让它吐司Hello World,所以代码修改为:
```
public void say(View view) {
Toast.makeText(getApplicationContext(), "Hello World", Toast.LENGTH_SHORT).show();
}
```
2)制作补丁包
先将基础包(前面那个tinker-local-debug-1206-11-48-42.apk 文件)重命名为old-app.apk,然后双击tinkerPatchDebug,操作如下图所示:
![](https://img.kancloud.cn/0d/76/0d76a01db43903f75cfc6eee5a009cab_717x486.jpg)
编译完成后,build/outputs/apk/tinkerPatch 会产生3 个补丁包,我们要的就是patch_signed_7zip.apk。
![](https://img.kancloud.cn/8f/ba/8fba6ebf917c9d97284fc6ccdcefbc6f_598x467.jpg)
对于build/outputs/apk/tinkerPatch 目录下文件及文件夹的详细说明,请参考:[Tinker 官方Wiki:输出文件详解](https://github.com/Tencent/tinker/wiki/Tinker-接入指南)。
3)下发补丁包
将patch_signed_7zip.apk 放到手机的SD 卡目录下:不一定是SD 卡目录,位置由我们开发者决定,Demo 中调用
TinkerInstaller.onReceiveUpgradePatch(context, 补丁包的本地路径)方法时,第二个参数指定了是SD 卡,故如此操作。
![](https://img.kancloud.cn/cf/cb/cfcba2f77d698687e6e272eac157c3ff_426x382.jpg)
4)打补丁
可以看到,在install patch 之前,点击"say someting"按钮时,还是吐司"Hello"。点击"install patch"按钮后,会提示"patch success,please restart process",说明Tinker 已经打上补丁了。这时点击"show info",可以看到"patch is not loaded",说明当前补丁还没有生效。最后,点击"kill myself"按钮,杀死当前 app(进程)。
![](https://img.kancloud.cn/9a/ad/9aad5a931a29c307e085a21ff7677b75_342x567.jpg)
在重新打开app,再点击"say someting"按钮,吐司"Hello World"。再点击"show info",可以看到"patch is loaded",说明app 重启后补丁生效了。
![](https://img.kancloud.cn/58/8b/588bde2be58ab6b6c92b16f87860b535_342x567.jpg)
小结: Tinker 热修复无法让补丁实时生效,在重启进程后,补丁才会生效。Tinker 会在app 重启后自动应用补丁。
3、修复so 库
在一开始制作基础包时,工程中就已经加入了一些so 文件,存放在src/main/jniLibs 目录下,因为Android Studio 默认的库目录是libs(与src 同级),所以这里需要在app 的build.gradle 文件中进行配置,指定so 库所在文件夹。
![](https://img.kancloud.cn/12/b1/12b13e5e131d300e73e43ca512ba55a2_910x332.jpg)
下面是"get string from .so"按钮点击时调用的方法:
```
public void string_from_so(View view) {
String string = JniUtil.hello();
Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show();
}
```
这个JniUtil 的代码如下:
```
public class JniUtil {
public static String LIB_NAME = "LQRJni";
public JniUtil() {}
static {
System.loadLibrary(LIB_NAME);
}
public static native String hello();
}
```
加载so 库有2 点需要注意:
1.System.loadLibrary(libname)加载固定目录下的库文件,而System.load(filename)加载指定目录下的库文件。
2.System.loadLibrary(libname)的参数libname 指的是库的模块名,不是so 文件的名字,如libLQRJni.so 文件的模块名实际上是LQRJni。
> so 文件的制作代码包含在Demo 中,有兴趣的朋友可以尝试自己制作。
1)替换so 文件
回归正题,现在so 库中得到的文字是"Hello LQR",现在变一下,我需要得到的文字是"Hello CSDN_LQR",将新的so 文件替换掉旧的so 文件即可。
![](https://img.kancloud.cn/d6/b1/d6b1a818d5b7ace6e23201af5c30ba4a_879x187.jpg)
2)检查Tinker 的lib 匹配规则
在app 的build.gradle 文件中,我们前面在第三部分《Tinker 的配置及任务》的第2 节《配置Tinker 与任务》中,有如下一段配置:
```
lib {
pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]
}
```
这就是Tinker 的lib 匹配规则,在生成补丁的过程中,它会去把符合这个规则的库文件拿出来与基础包中的库文件进行匹配,从而将有差异的库文件放入到补丁包中。而Tinker 官方Demo 的配置中是没有"src/main/jniLibs//.so"这一段的,这将导致Tinker 在产生补丁包时不会去检查src/main/jniLibs 目录下的文件变化,进而补丁包中不会包含修复好的so 文件,这很重要,切记。
3)生成补丁与下发补丁包
生成补丁与下发补丁包的过程与之前的操作一致,这里不再重覆,不过我们来看看tinkerPatch 跟之前有什么区别吧:
![](https://img.kancloud.cn/68/3d/683d4967488d5a18bc807fca9bd5191b_677x436.jpg)
最后记得将patch_signed_7zip.apk 放到手机的SD 卡目录下。
4)卸载补丁
补丁是可以打多个的,用补丁的版本号做区分,在卸载的时候,可以根据补丁的版本号来卸载,也可以把之前所有的补丁卸载掉,实际开发中,看项目需求来解决用哪种方式来卸载补丁,这里我选择清理之前所有的补丁,下面是"uninstall patch"按钮的点击事件:
```
public void uninstall_patch(View view) {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
Tinker.with(getApplicationContext()).cleanPatch();
}
```
卸载补丁之前,要先杀死当前App 的其他进程。卸载补丁之后,App 是不安全的(因为此时Tinker 已经初始化完成),最好,需要重启一下App。
5)打补丁
现在我们再来打一次补丁,操作如下图所示:
![](https://img.kancloud.cn/de/67/de67b2cd6eef959d2381ade7852e3fd3_342x567.jpg)
可以看到,在install patch 之前,点击"get string from .so"按钮时,还是吐司"Hello LQR"。点击"install patch"按钮后,会提示"patch success,please restart process",说明Tinker 已经打上补丁了。这时点击"show info",可以看到"patch is not loaded",说明当前补丁还没有生效。最后,点击"kill myself"按钮,杀死当前app(进程)。现在重启app,理想状态下,当我们再次点击"get string from .so"按钮时,会吐司"Hello CSDN_LQR"。
然而,吐司还是"Hello LQR",并没有变化,而且点击"show info"按钮后,可以看到"patch is loaded",说明补丁已经加载了,这是为啥?
再来看看下面的操作:
重启app 后,先点击"load library(hack)"按钮,再点击"get string from .so"按钮,出现了,吐司变成了"Hello CSDN_LQR"。
上图是Tinker 官网Wiki 的文档部分截图,从红线部分可以知道,因为部分手机判断abi 并不准确(可能因为Android 碎片化比较严重吧),Tinker 没有区分abi,自然也不会在app 启动时,自动加载对应的so 库,这需要开发者自己判
断。
下面是"load library(hack)"按钮点击调用的方法:
```
public void load_library_hack(View view) {
String CPU_ABI = android.os.Build.CPU_ABI;
// 将tinker library 中的CPU_ABI 架构的so 注册到系统的library path 中。
TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
}
```
这是Tinker 提供的使用Hack 方式加载补丁中的so 库,只是一个方法调用而已,并没有什么特别。对于非Hack 方式加载补丁的方式,我本人是没有测试成功的,很奇怪,搞不明白问题的原因,官方的文档也写得不清不楚的,有知道本Demo加载不成功的原因的朋友请不吝赐教一下哈,thx。
> 小结: Tinker 虽然会在app 重启后自动加载补丁,但不会自动加载补丁中的so 文件,开发者需自己判定好abi 来加载so 文件。
4、修复资源文件
这部分跟前面的重合度极高,故不做演示了,你可以在补丁包中对本demo 中的头像进行替换试试,与修复java 文件的操作基本一致,这部分需要提醒的是,app 的build.gradle 文件中Tinker 配置有如下这一段:
```
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
...
}
```
不难理解,这就是Tinker 对资源文件的匹配规则,日常开发够用,如果你的项目中把资源文件放到了这里没有的目录下,需要修改这部分的配置。
- 第一章 热修复设计
- 第一节、AOT/JIT & dexopt 与dex2oat
- 一、AOT/JIT
- 二、dexopt 与dex2oat
- 第二节、热修复设计之CLASS_ISPREVERIFIED 问题
- 一、前言
- 二、建立测试Demo
- 三、制作补丁
- 四、加载补丁
- 五、CLASS_ISPREVERIFIED
- 第三节、热修复设计之热修复原理
- 一、Android 热修复
- 二、Android 虚拟机和编译加载顺序
- 三、混合模式的理解
- 四、源码类到机器执行的文件过程
- 五、补丁包
- 六、类补丁生效原理
- 七、Davlik 虚拟机的限制
- 八、Davlik Class resolved by unexpected DEX: 限制和处理方式
- 九、类加载器的双亲委派加载机制
- 第四节、Tinker 的集成与使用(自动补丁包生成)
- 一、简述
- 二、Tinker 组件依赖
- 三、Tinker 的配置及任务
- 四、Tinker 封装与拓展
- 五、编写Application 的代理类
- 六、常用API
- 七、测试
- 八、细节
- 第二章 插件化设计
- 第一节、Class 文件与Dex 文件的结构解读
- 一、Class 文件
- 二、Dex 文件
- 三、Class 文件和Dex 文件对比
- 第二节、Android 资源加载机制详解
- 第三节、四大组件调用原理
- 第四节、so 文件加载机制
- 第五节、Android 系统服务实现原理
- 第三章 组件化框架设计
- 第一节、阿里巴巴开源路由框——ARouter 原理分析
- 第二节、APT 编译时期自动生成代码&动态类加载
- 第三节、Java SPI 机制
- 第四节、AOP&IOC
- 第五节、手写组件化架构
- 第四章 图片加载框架
- 第一节 图片加载框架选型
- 第二节 Glide 原理分析
- 第三节 手写图片加载框架实战
- 第五章 网络访问框架设计
- 第一节 网络通信必备基础
- 第二节 OkHttp 源码解读
- 第三节 Retrofit2 源码解析
- 第六章 RXJava响应式编程框架设计
- 第一节 RXJava之链式调用
- 第二节 RXJava之扩展的观察者模式
- 第三节 RXJava之事件变换设计
- 第四节 Scheduler 线程控制
- 第七章 IOC架构设计
- 第一节 依赖注入与控制反转
- 第二节 ButterKnife 原理上篇、中篇、下篇
- 第三节 IOC架构设计之Dagger2架构设计
- 第八章 Android架构组件 JetPack
- 第一节 LiveData的工作原理
- 第二节 Navigation 如何解决tabLayout 问题
- 第三节 ViewModel 如何感知View 生命周期及内核原理
- 第四节 Room 架构方式方法
- 第五节 dataBinding 为什么能够支持MVVM
- 第六节 WorkManager 内核揭秘
- 第七节 Lifecycles 生命周期