聲明:本文爲博主原創文章,轉載請註明出處:小嵩的博客
本系列傳送門:
微信Tinker 熱修復介紹及接入(一)
Tinker 原理深入理解(二)
Tinker 合併及加載補丁過程源碼分析 (三)
什麼是熱修復?
定義 : 熱修復(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%) | 較高(96%) | 一般 | 最高(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.7.11版本,最新版可參考 Tinker GitHub 項目主頁。
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名稱設置爲它 :
<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放到項目裏,但構造方法需要設置好代理類的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或採用相同代碼不同配置方式實現;
強烈建議採取第二種方式!!!
未完待續~
接下來這篇文章主要講解一下Tinker的實現原理:Tinker原理深入理解(二)
歡迎交流討論,有問題也非常歡迎指出不足之處~