概述
熱修復是一種動態修復程序解決問題的思想,其特點如下:
- 無需發版,實現高效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");
}
});
}