什么是熱修復
**定義 **: 熱修復(HotFix)是以補丁的方式動態(tài)修復緊急Bug薛夜,不再需要重新發(fā)布App,不需要用戶重新下載覆蓋安裝的方式來實現(xiàn)代碼的替換修改版述。這里就不多啰嗦了梯澜,可以自行搜索網(wǎng)上的介紹。
目前主流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 |
性能損耗 | 較小 | 較大 | 較小 | 較小 |
補丁包大小 | 較小 | 較大 | 一般 | 一般 |
開發(fā)透明 | yes | yes | no | no |
復雜度 | 較低 | 較低 | 復雜 | 復雜 |
Rom體積 | Dalvik較大 | 較小 | 較小 | 較小 |
成功率 | 較高(95%) | 較高 | 一般 | 最高(99.9%) |
</br>
注:
- Tinker的成功率數(shù)據(jù)渴析,是從微信團隊張紹文同學那兒打聽得到的晚伙,該數(shù)據(jù)是微信APP自身的成功率,可信度高俭茧;
- Robust的成功率數(shù)據(jù)咆疗,來自美團Robust開源項目官方文檔。
- QZone成功率和Tinker應該在同一水平(或稍低點)的樣子母债。
- AndFix 是公司以前就接入的午磁,內(nèi)部測試成功率只有80%左右(僅供參
考),而且修復起來還有諸多限制毡们。
Tinker的原理
Tinker的優(yōu)勢和特性
綜合考慮來說迅皇,Tinker的補丁包以及功能全面性、穩(wěn)定性是比較吸引人的漏隐,并且功能還能做到類替換 喧半、資源替換以及So替換。這樣一來它就不僅僅是熱修復了青责,還能做到熱更新挺据。因此我們最后采用了Tinker (其實還是因為微信幾億設備也是用的Tinker這套方案,靠譜點)脖隶。
微信和阿里還提供了補丁后臺托管扁耐,版本管理SDK ,不缺錢或者不想因為熱修復對項目代碼造成侵入性的話产阱,也可以直接使用微信或阿里封裝好的傻瓜式接入方案婉称,微信 Tinker Patch 方案目前是補丁包日請求量1w以內(nèi)免費;阿里云 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的一些參數(shù)
這步可參考Tinker 開源項目 sample中的app/build.gradle。
4.自定義Application代理類
程序啟動時會加載默認的Application類藻烤,這導致我們補丁包是無法對它做修改了绷雏。如何規(guī)避头滔?在這里我們并沒有使用類似InstantRun hook Application的方式,而是通過代碼框架的方式來避免坤检,這也是為了盡量少的去反射,提升框架的兼容性膘婶。
這里我們要實現(xiàn)的是完全將原來的Application類隔離起來悬襟,即其他任何類都不能再引用我們自己的Application逝段。將代碼都放到代理類ApplicationLike中來奶躯,我們需要做的其實是以下幾個工作:
- 將我們項目原來的Application類以及它的Base類的所有代碼拷貝到創(chuàng)建的ApplicationLike繼承類中,例如SampleApplicationLike儡蔓。你也可以直接將自己的Application改為繼承ApplicationLike,然后做改動;
- Application的attachBaseContext方法實現(xiàn)要單獨移動到onBaseContextAttached中获询;
- 對ApplicationLike中,引用application的地方改成getApplication();
- 對其他引用Application或者它的靜態(tài)對象與方法的地方,改成引用ApplicationLike的靜態(tài)對象與方法较解;
更詳細的內(nèi)容大家可以參考sample例子里SampleApplicationLike的做法。
GitHub地址: tinker/tinker-sample-android/app/build.gradle
對于為何放棄Instant Run 實現(xiàn)奸焙,而采用代理的方案,張紹文同學是這么解釋的:
詳情可參考微信Android團隊技術分享博客,地址鏈接:WeMobileDev/article
5.Tinker SDK初始化以及調(diào)用
初始化
創(chuàng)建一個類繼承自ApplicationLike ,并添加DefaultLifeCycle注解,指定需要自動生成的Application路徑和名稱郭卫,將AndroidManifest.xml里面的application名稱設置為它 :
<application
android:name=".app.SampleApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
代理類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放到項目里,但構(gòu)造方法需要設置好代理類的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);
}
}
調(diào)用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 打開右上側(cè)Gradle,并雙擊assembleDebug,生成基準包。
6.2 安裝基準包
app/build/bakApk 下,可以看到生成了基準包Apk以及R文件、mapping(mapping文件混淆下才會有)勾栗,然后將該Apk安裝到手機中。
平時開發(fā)測試時我們可通過AS 開發(fā)工具下方的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到手機之后,我們在基準包代碼中已經(jīng)寫了如下代碼少孝,此時返回基準包觸發(fā)該代碼继低,則可把補丁包合并到基準包實現(xiàn)熱更新:
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的辦法,采用直接手動創(chuàng)建Application抑钟,并在構(gòu)造方法中(第二個參數(shù))涯曲,設置代理類。
- 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或采用相同代碼不同配置方式實現(xiàn);
強烈建議采取第二種方式!!!
未完待續(xù)~
歡迎交流討論祥绞,有問題也非常歡迎指出不足之處~