概述
热修复是一种动态修复程序解决问题的思想,其特点如下:
- 无需发版,实现高效Bug修复,用户无感知
- 无需下载新应用,只需下载补丁包,代价小
- 修复成功率高,降低紧急Bug的损失
原理介绍
大致有三种方案:底层Native替换方案、Java层类加载方案
Java层类加载方案
类加载方案基于Dex分包方案,Dex分包方案则是因为65536限制和LinearAlloc限制。
- 65536限制就是DVM指令集的方法调用指令invoke-kind索引为16bits,最多只能引用65535个方法,超过则会编译失败。
- LinearAlloc限制就是LinearAlloc是DVM中一个固定的缓存区,方法数超过混存取大小会在安装时提示INSTALL_FAILED_DEXOPT错误。
Dex分包方案就是打包时将应用代码分成多个Dex文件,启动必须用到的类放到主Dex中,其他代码放到次Dex中,应用启动先加载主Dex把应用启动起来,再动听加载次Dex,从而解决65536限制和LinearAlloc限制的问题。
说回类加载方案,当应用在加载一个类的时候 他会利用ClassLoader机制去寻找这个类,关键代码在DexPathList.findClass()函数中
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element代表了一个dex文件,其内部封装了DexFile用于加载dex文件,由于上面的分包方案一个应用有了多个dex,而dexElements则有序的存储了所有dex。这边findClass则会遍历dexElements在各个dex中按顺序查找所需类,找到后就返回并终止查找。类加载方案则是将要修改的.class文件打包成包含dex的patch.jar,App下载拿到后然后将这个dex放到dexElements列表的首位或者利用其它方法让patch.jar中的class排在前面位置,则要修改的.class则会先被找到,存在bug的旧.class根据ClassLoader的双亲委派模式就不会被加载
PS:由于类加载后不会卸载,class被findClass一次后不需要findClass第二次所以需要重新启动应用,重新findClass才会生效。
底层Native替换方案
利用Native反射替换要修复的类的方法的信息(执行入口、访问权限、所属类、代码执行地址等)
PS:即时生效,但由于基于Native层直接替换原有类,限制多,无法增减原有类的方法和字段
方案对比
特性 | AndFix(阿里) | Hotfix(阿里) | Sophix(阿里) | 超级补丁(QQ空间) | Tinker(微信) Amigo(饿了么) | Robust(美团) Aceso(美丽说蘑菇街) |
---|---|---|---|---|---|---|
即时生效 | 是 | 是 | 同时支持即时生效和冷启动修复 | 否 | 否 | 是 |
方法替换 | 是 | 是 | 是 | 是 | 是 | 是 |
类替换 | 否 | 否 | 是 | 是 | 是 | 否 |
资源替换 | 否 | 否 | 是 | 是 | 是 | 否 |
so替换 | 否 | 否 | 是 | 否 | 是 | 否 |
支持ART | 是 | 是 | 是 | 是 | 是 | 是 |
Tinker使用介绍
- 到 http://www.tinkerpatch.com 平台注册一个app并获取appKey
- 根据 http://www.tinkerpatch.com/Docs/SDK 文档做配置
- build.gradle.中
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
// TinkerPatch 插件
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.6"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
- app/build.gradle中,主要是签名和tinker的包
apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'
android {
signingConfigs {
release {
keyAlias 'key0'
keyPassword 'tinker'
storeFile file('../tinker.jks')
storePassword 'tinker'
}
}
compileSdkVersion 26
defaultConfig {
applicationId "com.android.commonlib.tinkertest"
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName "1.0.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
signingConfig signingConfigs.release
}
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation "com.android.support:multidex:1.0.2"
//若使用annotation需要单独引用,对于tinker的其他库都无需再引用
annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:1.9.6") { changing = true }
compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.6") { changing = true }
implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.6") { changing = true }
}
- Application
public class SampleApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initTinkerPatch();
}
private void initTinkerPatch() {
// 我们可以从这里获得Tinker加载过程的信息
ApplicationLike tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK
TinkerPatch.init(
tinkerApplicationLike
// new TinkerPatch.Builder(tinkerApplicationLike)
// .requestLoader(new OkHttp3Loader())
// .build()
)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3)
;
// 获取当前的补丁版本
// fetchPatchUpdateAndPollWithInterval 与 fetchPatchUpdate(false)
// 不同的是,会通过handler的方式去轮询
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
@Override
public void attachBaseContext(Context base) {
super.attachBaseContext(base);
//you must install multiDex whatever tinker is installed!
MultiDex.install(base);
}
}
- tinkerpatch.gradle
apply plugin: 'tinkerpatch-support'
/**
* TODO: 请按自己的需求修改为适应自己工程的参数
*/
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.1-0525-15-59-41"
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 = "a2fc5f63bf186415"
/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.1"
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}"
}
}
/**
1. 一般来说,我们无需对下面的参数做任何的修改
2. 对于各参数的详细介绍请参考:
3. 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
}
}
配置完成,测试下,先运行assembleRelease模拟打正式包,生成文件如下(即app-release.apk)安装在手机上。
每次编译都会生成一个基准包,这边此次生成的基准包放在app-1.0.1-0525-15-59-41目录中,修改tinkerpatch.gradle中baseInfo的值为【app-1.0.1-0525-15-59-41】这个目录。
然后随意修改下我们的代码,表示我们要打的补丁。之后再运行tinkerPatchRelease就会基于这个基准包生成补丁包了。即tinker_result/patch_signed_7zip.apk
- 发布到tinkerpatch平台,我们刚刚的appVersion是1.0.1,在tinkerpatch中建一个1.0.1的版本,然后下发补丁即可。
- 建议本地先测试下,可以将补丁包发送到本地目录下,然后调用以下代码即可自动打补丁,重启应用即可生效。如果有错误也可以从log中看出
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView textView = findViewById(R.id.textview);
textView.setText("...Hello World...");
textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}
});
}