Tinker是微信官方的Android热补丁解决方案,它支持动态修改代码(class文件)、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件,实现动态部署。
它主要包括以下几个部分:
1)gradle编译插件: tinker-patch-gradle-plugin (用于生成差分补丁的gradle插件)
2)核心sdk库: tinker-android-lib (核心sdk,主要的类位于com.tencent.tinker.lib.tinker.*, 比如Tinker(Tinker补丁清除,判断是否加载补丁)、TinkerInstaller(Tinker补丁安装)、TinkerApplication(负责重写ClassLoader的加载规则,先加载补丁dex中的类,从而达到覆盖旧class的目的))
Github
https://github.com/Tencent/tinker/wiki
https://github.com/Tencent/tinker
QQ空间超级补丁技术,基于虚拟机class loader动态加载,大致的过程就是:把BUG方法修复以后,放到一个单独的DEX里,应用启动后,将补丁dex插入到class loader的dexElements数组的最前面,让虚拟机优先去加载修复完后的方法。
微信针对QQ空间超级补丁技术的不足提出了一个提供DEX差量包,整体替换DEX的方案。主要的原理是与QQ空间超级补丁技术基本相同,区别在于不再将patch.dex增加到elements数组中,而是差量的方式给出patch.dex,然后将patch.dex与应用的classes.dex合并,然后整体替换掉旧的DEX,达到修复的目的。
HotFIx(Andfix技术+后台管理(补丁安全,版本管理和发布)) 是阿里百川推出的热修复服务,相对于QQ空间超级补丁技术和微信Tinker来说,定位于紧急bug修复的场景下,能够最及时的修复bug,下拉补丁立即生效无需等待。
AndFix作为native解决方案,运行时在Native修改Filed指针的方式,实现方法的替换(hook),达到即时生效无需重启
结合了Andfix和Tinker这两个开源框架的优势,支即时生效的热修复,同时也支持资源、类替换等需要APK重启的冷修复。
基于两大热修复技术的同时,也对接入方式做了改良,提供快速接入的方式,很傻瓜。接入方式请参考#Sophix接入
而且APK打包方式是无侵入式的(Tinker会使用gradle插件,根据注解等信息生成一些中间类)。依托于阿里云,可以轻松实现版本管理、补丁推送、灰度发布等功能。缺点是,功能逐步完善后,已经商业化了,需要money。不过东航2018应该会引入阿里的EMAC,届时热修复也是其中的一项功能。
类替换 |
Sophix |
Tinker |
Andfix |
So替换 |
支持 |
支持 |
不支持 |
类替换 |
支持 |
支持 |
不支持(只支持方法替换) |
资源替换 |
支持 |
支持 |
不支持 |
全平台支持 |
支持 |
支持 |
部分支持(由于厂商的自定义ROM,对少数机型暂不支持) |
及时生效 |
部分支持 |
不支持 |
支持 |
接入复杂度 |
傻瓜式 |
较复杂 |
比较简单 |
补丁包大小 |
较小 |
较小 |
较小 |
侵入式打包 |
无 |
依赖侵入式 |
无 |
后台发布支持 |
支持(阿里云) |
支持(tinkerpatch,今天挂掉了) |
支持(HotFix) |
价格 |
收费(有免费调用次数限制) |
免费开源(支持本地打补丁) |
免费开源 |
http://www.tinkerpatch.com/Docs/SDK
可见Tinker与阿里的Sophix相比,基本上没有任何优势。最大的优势是开源和免费。
由于技术实现原理与系统限制,Tinker有以下已知问题:
Demo
https://github.com/Tencent/tinker/tree/master/tinker-sample-android
https://github.com/Tencent/tinker/wiki/Tinker-接入指南
Gradle配置
gradle.properties
TINKER_VERSION=1.9.2
build.gradle
dependencies {
classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
}
dependencies {
//optional, help to generate the final application
provided('com.tencent.tinker:tinker-android-anno:1.9.1')
//tinker's main Android lib
compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}
...
...
apply plugin: 'com.tencent.tinker.patch'
++ 补丁生成的gradle脚本
重点关注下面的信息:
buildConfigField "String", "MESSAGE", "\"I am the base apk\""
buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
/*proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')*/
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-R.txt"
//only use for build all flavor, if not, just ignore this field
//tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
上面的R.txt很重要,主要用于资源文件。因为Android apk在打包的时候,res下的资源文件会对应生成一个到Resource的java文件中,存放着id和内存地址的映射。而我们上层通过getString(R.id.xxx), getDrawable(R.drawable.xxx)获取这些对象。
热更新之所以支持资源替换,实际上是修改了映射资源的java文件,并把其中的映射关系修改为指向,patch包中需要替换的资源。
def gitSha() {
try {
String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
if (gitRev == null) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
混淆配置
proguard-rules.pro
-dontwarn cn.jpush.android.**
代码接入
修改并重构Application,使用TinkerApplication
这里可以配置,我们支持的热更新方式可以选择,包括
TINKER_DEX_MASK
TINKER_ENABLE_ALL
TINKER_DEX_AND_LIBRARY
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//you must install multiDex whatever tinker is installed!
MultiDex.install(base);
SampleApplicationContext.application = getApplication();
SampleApplicationContext.context = getApplication();
TinkerManager.setTinkerApplicationLike(this);
TinkerManager.initFastCrashProtect();
//should set before tinker is installed
TinkerManager.setUpgradeRetryEnable(true);
//optional set logIml, or you can use default debug log
TinkerInstaller.setLogIml(new MyLogImp());
//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);
Tinker tinker = Tinker.with(getApplication());
}
推荐使用tinker-android-anno,通过注解自动生成中间类。我们只需要对MyApp的生命周期处理的交给TinkerApplication。
getApplication().registerActivityLifecycleCallbacks(callback);
加载补丁
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
清除补丁
Tinker.with(getApplicationContext()).cleanPatch();
补丁校验
SamplePatchListener
public int patchCheck(String path, String patchMd5) {
}
可以做一些MD5校验等补丁应用前的操作
补丁监听
SampleResultService
public void onPatchResult(final PatchResult result) {
}
监听补丁的应用情况,并做出响应动作。比如补丁应用成功后,提示用户重启。或者监控应用一旦切换到后台,自动的杀掉进程。
应用到海外APP
参考工程文件:UpdateManager.java
启动应用检测更新(checkUpdateOnUi)-> 有大版本更新优先进行APK更新 -> 通过主应用版本号从服务器获取补丁版本(临时方案,简化原定为一对一的原则) -> 判断是否需要删除已应用的补丁(旧的补丁可能会有不良影响,比如APK升级后,旧的补丁也会被加载) -> 否有补丁更新(本地应用的补丁,与服务器的补丁比大小) -> 检测补丁是否已经下载 -> 进度条弹窗下载补丁 -> 下载完成后,关闭弹窗,静默应用补丁 -> 补丁合成成功则提示重启进程(失败不作任何提示)
补丁会临时的下载到SD卡上,一旦应用成功原始的patch文件会被删除,它们会与原始apk的dex被合成为fix_dex文件,放到apk的文件目录中。每次启动应用都会去加载。
补充:Sophix接入
简单概括,大体上是
1. 登录移动热修复控制台:https://hotfix.console.aliyun.com/
创建应用,并在阿里云平台上上传并管理补丁
2. 本地应用程序清单,配置好API KEY
application节点下加入如下配置
3. 本地代码加载Sophix框架
public class MyApplication extends MultiDexApplication {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
String appVersion = "0.0.0";
try {
appVersion = this.getPackageManager()
.getPackageInfo(this.getPackageName(), 0)
.versionName;
} catch (Exception e) {
}
// appVersion如果为空,后面initialize会抛异常
LogMan.logDebug("appVersion: " + appVersion);
// initialize最好放在attachBaseContext最前面,初始化直接在Application类里面,切勿封装到其他类
SophixManager.getInstance().setContext(this)
.setAppVersion(appVersion)
.setAesKey(null)
.setEnableDebug(true)
.setPatchLoadStatusStub(new PatchLoadStatusListener() {
@Override
public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
LogMan.logDebug("PatchLoadStatusListener code: " + code);
// 补丁加载回调通知
if (code == PatchStatus.CODE_LOAD_SUCCESS) {
// 表明补丁加载成功
LogMan.logDebug("patch loading success");
} else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
// 表明新补丁生效需要重启. 开发者可提示用户或者强制重启;
// 建议: 用户可以监听进入后台事件, 然后调用killProcessSafely自杀,以此加快应用补丁,详见1.3.2.3
LogMan.logDebug("code patch loading success");
SophixManager.getInstance().killProcessSafely();
} else {
LogMan.logDebug("code patch loading failed");
// 其它错误信息, 查看PatchStatus类说明
}
}
}).initialize();
}
btn_load.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LogMan.logDebug("start to load patch from AliYun");
SophixManager.getInstance().queryAndLoadNewPatch();
}
});