什么是热修复
**定义 **: 热修复(HotFix)是以补丁的方式动态修复紧急Bug,不再需要重新发布App,不需要用户重新下载覆盖安装的方式来实现代码的替换修改。这里就不多啰嗦了,可以自行搜索网上的介绍。
目前主流HotFix方案对比:
HotFix方案 | Tinker | QZone | AndFix | Robust |
---|---|---|---|---|
类替换 | yes | yes | no | no |
So替换 | yes | no | no | no |
资源替换 | yes | yes | no | no |
全平台支持 | yes | yes | no | yes |
即时生效 | no | no | yes | yes |
性能损耗 | 较小 | 较大 | 较小 | 较小 |
补丁包大小 | 较小 | 较大 | 一般 | 一般 |
开发透明 | yes | yes | no | no |
复杂度 | 较低 | 较低 | 复杂 | 复杂 |
Rom体积 | Dalvik较大 | 较小 | 较小 | 较小 |
成功率 | 较高(95%) | 较高 | 一般 | 最高(99.9%) |
注:
- Tinker的成功率数据,是从微信团队张绍文同学那儿打听得到的,该数据是微信APP自身的成功率,可信度高;
- Robust的成功率数据,来自美团Robust开源项目官方文档。
- QZone成功率和Tinker应该在同一水平(或稍低点)的样子。
- AndFix 是公司以前就接入的,内部测试成功率只有80%左右(仅供参
考),而且修复起来还有诸多限制。
Tinker的原理
Tinker的优势和特性
综合考虑来说,Tinker的补丁包以及功能全面性、稳定性是比较吸引人的,并且功能还能做到类替换 、资源替换以及So替换。这样一来它就不仅仅是热修复了,还能做到热更新。因此我们最后采用了Tinker (其实还是因为微信几亿设备也是用的Tinker这套方案,靠谱点)。
微信和阿里还提供了补丁后台托管,版本管理SDK ,不缺钱或者不想因为热修复对项目代码造成侵入性的话,也可以直接使用微信或阿里封装好的傻瓜式接入方案,微信 Tinker Patch 方案目前是补丁包日请求量1w以内免费;阿里云 Sophix 目前还在公测阶段,暂时不收费。
微信 Tinker Patch 官方地址:Tinker Patch
阿里 SopHix 官方地址:Sophix
接入Tinker步骤
1.添加工程gradle plugin依赖
在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11')
}
}
2.添加tinker库依赖及插件应用
在app的gradle文件app/build.gradle,我们需要添加tinker的库依赖以及apply tinker的gradle插件:
//apply tinker插件
apply plugin: 'com.tencent.tinker.patch'
...
...
dependencies {
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.7.11')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.7.11')
}
3.gradle配置Tinker的一些参数
这步可参考Tinker 开源项目 sample中的app/build.gradle。
4.自定义Application代理类
程序启动时会加载默认的Application类,这导致我们补丁包是无法对它做修改了。如何规避?在这里我们并没有使用类似InstantRun hook Application的方式,而是通过代码框架的方式来避免,这也是为了尽量少的去反射,提升框架的兼容性。
这里我们要实现的是完全将原来的Application类隔离起来,即其他任何类都不能再引用我们自己的Application。将代码都放到代理类ApplicationLike中来,我们需要做的其实是以下几个工作:
- 将我们项目原来的Application类以及它的Base类的所有代码拷贝到创建的ApplicationLike继承类中,例如SampleApplicationLike。你也可以直接将自己的Application改为继承ApplicationLike,然后做改动;
- Application的attachBaseContext方法实现要单独移动到onBaseContextAttached中;
- 对ApplicationLike中,引用application的地方改成getApplication();
- 对其他引用Application或者它的静态对象与方法的地方,改成引用ApplicationLike的静态对象与方法;
更详细的内容大家可以参考sample例子里SampleApplicationLike的做法。
GitHub地址: tinker/tinker-sample-android/app/build.gradle
对于为何放弃Instant Run 实现,而采用代理的方案,张绍文同学是这么解释的:
详情可参考微信Android团队技术分享博客,地址链接:WeMobileDev/article
5.Tinker SDK初始化以及调用
初始化
创建一个类继承自ApplicationLike ,并添加DefaultLifeCycle注解,指定需要自动生成的Application路径和名称,将AndroidManifest.xml里面的application名称设置为它 :
代理类SampleApplicationLike 代码:
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends ApplicationLike {
private static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
/**
* install multiDex before install tinker
* so we don't need to put the tinker lib classes in the main dex
*
* @param base
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
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.with(getApplication());//初始化热更新SDK
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}
写好之后Sync一下,它会在编译时自动生成SampleApplication。如果不想通过注解自动生成,我们也可以手动写这个Application放到项目里,但构造方法需要设置好代理类的path:
package tinker.sample.android.app;
import com.tencent.tinker.loader.app.TinkerApplication;
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(7, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
}
}
调用Tinker合并与清除补丁:
loadPatch :
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
loadLibrary :
// #method 1, hack classloader library path
TinkerLoadLibrary.installNavitveLibraryABI(getApplicationContext(), "armeabi");
System.loadLibrary("stlport_shared");
// #method 2, for lib/armeabi, just use TinkerInstaller.loadLibrary
// TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), "stlport_shared");
// #method 3, load tinker patch library directly
// TinkerInstaller.loadLibraryFromTinker(getApplicationContext(), "assets/x86", "stlport_shared");
cleanPatch:
Tinker.with(getApplicationContext()).cleanPatch();
6.补丁包生成与安装
6.1 打开右上侧Gradle,并双击assembleDebug,生成基准包。
6.2 安装基准包
app/build/bakApk 下,可以看到生成了基准包Apk以及R文件、mapping(mapping文件混淆下才会有),然后将该Apk安装到手机中。
平时开发测试时我们可通过AS 开发工具下方的Terminal 窗口 输入如下命令将APK Push到手机:
//APK已安装情况
adb install -r app/build/bakApk/app-debug-0620-14-12-54.apk
//APK未安装
adb install app/build/bakApk/app-debug-0620-14-12-54.apk
然后将app/build/bakApk 下生成的文件路径填入gradle 的ext 中:
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-debug-0620-14-12-54.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0620-14-12-54-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
6.3 生成补丁包
oldApk路径填好之后,开始修改Bug,bug改完之后,双击tinkerPatchDebug,这个gradle命令会对当前代码和oldApk进行差异对比,在app/build/output/tinkerPatch下生成补丁。
生成的补丁信息,我们需要的补丁包是patch_signed_7zip.apk:
6.4 补丁包下载安装
补丁包生成之后,我们则可把它放到服务器后台,客户端通过接口去下载补丁包了,测试中我们一样是通过adb 将文件push到手机sd卡根目录:
adb push ./aipai/build/outputs/tinkerPatch/offical/debug/patch_signed_7zip.apk /storage/sdcard0/
补丁包push到手机之后,我们在基准包代码中已经写了如下代码,此时返回基准包触发该代码,则可把补丁包合并到基准包实现热更新:
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
爬坑及小技巧:
1.TinkerId 设置问题。
git项目中会有TinkerId,如果是通过非Clone方式拉取的代码,则需要push一次同步到Git中才会有,如果为了测试方便,也可以直接在 gtadle.properties文件指定tinkerId,如:TINKER_ID = 1
2.Java1.8 兼容问题
在gradle中设置 JavaVersion 为1.8,导致Application代理失败造成一启动就崩溃问题,有两种办法:
- 去除gradle tinker-android-anno 依赖库,不通过DefaultLifeCycle注解自动生成Application的办法,采用直接手动创建Application,并在构造方法中(第二个参数),设置代理类。
- anno 注解不支持 jackOptions 因此需要通过添加 lambda插件来兼容Java1.8
//添加插件
apply plugin: 'me.tatarka.retrolambda'
3.补丁包push到sd卡:
adb push ./app/build/outputs/tinkerPatch/debug/patch_signed_7zip.apk /storage/sdcard0/
4.安装apk:
adb install app/build/bakApk/app-debug-0620-14-12-54.apk
或
adb install -r app/build/bakApk/app-debug-0620-14-12-54.apk
5.多渠道打包:
通过flavor 生成渠道包的情况下,会因为BuildInfo不同而导致Apk的Dex文件不同,从而导致每个渠道的补丁包都需要一对一,那么假如有几十个渠道,则同样需要几十个渠道的补丁包,这是非常不合理的。那么怎么办呢?
解决方案:
1.将渠道信息写在AndroidManifest.xml或文件中,例如channel.ini;
2.将渠道信息写在apk文件的zip comment中,这样一来,所有渠道包的Dex文件都是相同的,我们就可以通过assembleRelease 生成的基准包,来打补丁包。所有渠道都可以共用这个补丁包。至于这种渠道打包方式的工具,可以使用GitHub上开源的 packer-ng-plugin 或者可使用美团点评使用了V2 Scheme签名的 walle;
3.若不同渠道存在功能上的差异,建议将差异部分放于单独的dex或采用相同代码不同配置方式实现;
强烈建议采取第二种方式!!!
未完待续~
欢迎交流讨论,有问题也非常欢迎指出不足之处~