简单的说就是用户不用重新下载一个新的apk安装,而是直接下载一个补丁包,通过补丁来替换一些出现bug的类,当然下载补丁的过程用户一般是感觉不到的,表面上看是直接修复了bug。
简单的来说,就是把最后修改的类打包成dex,插入到ClassLoader的dex数组的最前面,当ClassLoader找类时,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找到则返回,如果找不到从下一个dex文件继续查找。因此我们最后修改的类会最先被找到,代替了旧的类,这样达到“修复”的目的,有点“换零件”的意思。
详细的热修复原理,请参考 安卓App热补丁动态修复技术介绍
官方是这么说的:
当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-7.X的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。
以上也是我使用Tinker热补丁方案的原因,不过还是有不足的。
由于原理与系统限制,Tinker有以下已知问题:
Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;(所以,如果要新增功能的话,还是建议版本升级,毕竟这个只是热修复方案。)
由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;(我倒觉得没关系)
在Android N上,补丁对应用启动时间有轻微的影响;(不知道程度有多大,目前只是做了Demo)
不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”; (这个得做兼容)
对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。 (希望以后能支持)
接入Tinker目前给了两种方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。
gradle接入的方式应该算是主流的方式,所以tinker也直接给出了例子,单独将该tinker-sample-android以project方式引入即可。
做法比较简单,具体方法请参考 Android 热修复 Tinker接入及源码浅析 的 “2gradle接入”一章。
由于这种方式要把整个“tinker-sample-android”引入到项目中,所以我不太喜欢,因此我决定用下面这种方式接入。
估计用这种方式的不多,毕竟主流的gradle接入方式很方便,网上很多方法都说得不明不白,有的甚至步骤有问题,花了好几天,终于接入成功。在这里写下我踩的坑,希望能帮助有需要的人。
先给出Tinker的github地址:https://github.com/Tencent/tinker
(1)添加Tinker的依赖
// build.gradle
...
dependencies {
...
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.7.7')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.7.7')
...
}
...
(2)配置签名信息(最好配一下)
// build.gradle
...
android{
...
signingConfigs {
release {
try {
// keystore文件放在与该build.gradle的同一目录下
storeFile file("release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
...
}
...
(3)编写ApplicationLike子类,并添加注解,生成Application
// 添加DefaultLifeCycle注解之后,编译后,会产生名为SimpleTinkerInApplication的Application子类
// application = ".SimpleTinkerInApplication" 指定编译后生成Application子类的类名
@DefaultLifeCycle(application = ".SimpleTinkerInApplication",
flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class SimpleTinkerInApplicationLike extends ApplicationLike {
public SimpleTinkerInApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
}
@Override
public void onCreate() {
super.onCreate();
TinkerInstaller.install(this);
}
}
(4)在AndroidManifest文件设置application的名称
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.johan.demo">
<application
-- 因为SimpleTinkerInApplication是编译后才生成的,所以这里会报错,build一下工程就行了 -->
android:name=".SimpleTinkerInApplication"
...
>
application>
manifest>
(5)在AndroidManifest文件指定TINKER_ID
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
package="com.johan.demo">
...
...
"TINKER_ID"
android:value="tinker_id_56888" />
(6)在AndroidManifest文件声明权限,修复需要读取SD文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.johan.demo">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
manifest>
(7)在Activity添加修复的按钮
import com.tencent.tinker.lib.tinker.TinkerInstaller;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Android6.0 之后需要申请权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
}
}
/**
* 预留一个按钮
* 点击按钮,开始修复
**/
public void fixIt(View view) {
// 第二个参数指定更新包的路径,这里为SD卡根目录的patch_signed.apk文件
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
}
}
(8)添加Tinker的混淆
// proguard-rules.pro
...
-keepattributes *Annotation*
-dontwarn com.tencent.tinker.anno.AnnotationProcessor
-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *
-keep public class * extends android.app.Application {
*;
}
-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {
*;
}
-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {
*;
}
-keep public class com.tencent.tinker.loader.TinkerLoader {
*;
}
-keep public class * extends com.tencent.tinker.loader.TinkerLoader {
*;
}
-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {
*;
}
-keep public class com.tencent.tinker.loader.TinkerTestAndroidNClassLoader {
*;
}
#for command line version, we must keep all the loader class to avoid proguard mapping conflict
#your dex.loader pattern here
-keep public class com.tencent.tinker.loader.** {
*;
}
-keep class tinker.sample.android.app.SampleApplication {
*;
}
...
可以参考 tinker_proguard.pro
(9)这时生成一个APP,并签名。把签名后的APP(命名为old.apk,作为原始APP,假设有Bug)和mapping.txt(签名后,会在项目目录/app/build/outputs/mapping/release这个文件夹下)复制到另一个地方保存起来,接下来有用到
===================== 解决Bug的过程 =====================
(10)解决Bug之后,把之前保存的mapping.txt文件复制到与proguard-rules.pro同一文件夹下,并添加混淆
// proguard-rules.pro
...
-applymapping mapping.txt
(11)生成新的APP,并签名,这时签名APP(命名为new.apk)是已经修改好Bug的了
(12)制作补丁包
Tinker提供了补丁包生成的工具,源码见:tinker-patch-cli,打成一个jar就可以使用,并且提供了命令行相关的参数以及文件。
命令行如下:
java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output
需要注意的就是tinker_config.xml,里面包含tinker的配置,例如签名文件等。
这里我们直接使用Tinker提供的签名文件,所以不需要做修改(项目中一般会用自己的签名,所以一定要修改),不过里面有个Application的item修改为我们指定编译后指定生成的Application一致(包名+类名),见步骤(3),例子中改为:
// tinker_config.xml
<loader value="com.johan.demo.SimpleTinkerInApplication"/>
...
// 如果签名改了,一定要记得修改(这个一直没注意)
<issue id="sign">
<path value="xxx.keystore"/>
<storepass value="xxx"/>
<keypass value="xxx"/>
<alias value="xxx"/>
issue>
这里提供一个简单的工具包,地址:https://github.com/JohanMan/tinker-patch-tool
解压之后,把你的old.apk和new.apk覆盖工具包里的old.apk和new.apk,然后修改tinker_config.xml,然后使用命令
java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output
会在工具包的output文件夹生成patch_signed.apk文件:
这就是更新的包了。
(13)把制作好的补丁包复制指定的地方,见步骤(7),例子中指定的地方是SD卡根目录下
(14)启动原始APP(我们安装的old.apk,有Bug),点击修复按钮,程序就会加载我们的补丁包进行修复
以上是我以命令行方式接入Tinker踩过的坑,自己多尝试一下,应该能成功的!!!
默认命令行式的接入,当热修复完毕之后,也就是执行
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
参考 Tinker 自定义扩展 :
AbstractResultService类是:patch补丁合成进程将合成结果返回给主进程的类。我们为你提供了默认实现DefaultTinkerResultService.java。
一般来说, 你可以继承DefaultTinkerResultService实现自己的回调,例如SampleResultService.java。当然,你也需要在AndroidManifest上添加你的Service。
android:name=".service.SampleResultService"
android:exported="false"
/>
默认我们在DefaultTinkerResultService会杀掉:patch进程,假设当前是补丁升级并且成功了,我们会杀掉当前进程,让补丁包更快的生效。若是修复类型的补丁包并且失败了,我们会卸载补丁包。
我们一般都不会想,修复补丁之后,立刻让app退出,应该等到合适的时机,才去重启app让补丁包生效。
我们先看一下Tinker为我们提供的DefaultTinkerResultService怎么处理合成结果:
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
// 检测是否需要杀死进程才能让补丁包生效
if (checkIfNeedKill(result)) {
// 杀死app进程
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
根据以上代码,其实我们只要删除杀死app进程哪一行代码就行了,所以我们继承DefaultTinkerResultService,重写onPatchResult方法:
public class SimpleResultService extends DefaultTinkerResultService {
private static final String TAG = "SimpleResultService";
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
if (checkIfNeedKill(result)) {
// 注释掉,不杀死app进程
// android.os.Process.killProcess(android.os.Process.myPid());
TinkerLog.i(TAG, "need kill and restart the app");
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
}
当然还要在AndroidManifest注册SimpleResultService:
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
package="com.johan.demo">
"android.permission.WRITE_EXTERNAL_STORAGE" />
...
".SimpleResultService" android:exported="false" />
...
虽然我们定义了SimpleResultService,但是我们怎么让Tinker用这个自定义的合成结果处理类呢?答案在TinkerInstaller。
还记得 步骤(3)编写ApplicationLike子类(SimpleTinkerInApplicationLike)吗,我们在SimpleTinkerInApplicationLike的onCreate方法中调用了TinkerInstaller的install方法,其实install方法有2个,参数不一样
public static Tinker install(ApplicationLike applicationLike)
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter,
PatchReporter patchReporter, PatchListener listener,
Class extends AbstractResultService> resultServiceClass,
AbstractPatch upgradePatchProcessor)
我们可以使用第2个方法,让Tinker使用我们的SimpleResultService:
TinkerInstaller.install(this, new DefaultLoadReporter(getApplication()),
new DefaultPatchReporter(getApplication()),
new DefaultPatchListener(getApplication()),
SimpleResultService.class, new UpgradePatch());
更多关于自定义Tinker,请参考 Tinker 自定义扩展
Tinker库地址
Tinker – 微信Android热补丁方案
Tinker 自定义扩展
安卓App热补丁动态修复技术介绍
Android 热修复 Tinker接入及源码浅析