[TOC]
Hotfix方案分為三種:
1. 類加載方案(Tinker)
2. 底層替換方案(Sophix)
3. InstantRun方案(Robust)
三種典型的方案特點如下:
| 方案對比 | Sophix | Tinker | Robust |
| --- | --- | --- | --- |
| DEX修復 | 同時支持即時生效和冷啟動修復 | 冷啟動修復 | 即時成效 |
| 資源更新 | 差量包,不用合成 | 差量包,需要合成 | 差量包 |
| SO庫更新 | 插樁實現,開發透明 | 替換接口,開發不透明 | 不支持 |
| 性能損耗 | 低,僅冷啟動情況下有些損耗 | 高,有合成操作 | 低 |
| 四大組件 | 不能新增 | 不能新增 | 不能新增 |
| 生成補丁 | 直接選擇已經編好的新舊包在本地生成 | 編譯新包時設置基線包 | 編譯新包時設置基線包 |
| 補丁大小 | 小 | 小 | 小 |
| 接入成本 | 傻瓜式接入 | 復雜 | 一般,文件服務器需要自己實現 |
| Android版本 | 全部支持 | 全部支持 | 全部支持 |
| 安全機制 | 加密傳輸及簽名校驗 | 加密傳輸及簽名校驗 | 自己實現 |
| 服務端支持 | 支持服務端控制 | 支持服務端控制 | 自己實現 |
### 美團Robust
原理介紹可以看這里:[Android熱補丁之Robust原理解析(一)](http://w4lle.com/2017/03/31/robust-0/)
類似于 InstantRun 熱插拔的機制,在打基準包時為每類插入了一個`ChangeQuickRedirect`靜態變量,并為每個方法插入了一段代理邏輯。當打上補丁后,靜態變量不為空,這樣就會調用代理方法,以此實現了熱修復的功能。示例代碼如下:
~~~
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
if (u != null) {
if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
return;
}
}
super.onCreate(bundle);
...
}
~~~
其主要流程中涉及到的點如下:
1. 打基準包:根據配置使用Javassit或者ASM為每個類進行插樁。
2. 打補丁包:掃描Modify、Add注釋修飾的方法,為對應的類生成補丁。
3. 安裝補丁:使用DexClassLoader加載補丁,加載完畢后反射得到特定的補丁入口類,并創建其對象。補丁入口類里面定義了需要修改的基準類以及對應的熱修復ChangeQuickRedirect實現類。得到哪些修改的基準類后,再通過反射循環拿到每個基準類的class,將其中類型為 ChangeQuickRedirect 的靜態變量反射修改為 補丁包中生成的熱修復實現類。

優點:
* 由于使用多ClassLoader方案(補丁中無新增Activity,所以不算激進類型的動態加載,無需hook system),**兼容性和穩定性更好**,不存在preverify的問題
* 由于采用 InstantRun 的熱更新機制,所以可以即時生效,**不需要重啟**
* 支持Android2.3-10版本
* 對性能影響較小,不需要合成patch
* 支持方法級別的修復,支持靜態方法
* 支持新增方法和類
* 支持ProGuard的混淆、內聯、編譯器優化后引起的問題(橋方法、lambda、內部類等)等操作
當然,有優點就會有缺點:
* 暫時不支持新增字段,但可以通過新增類解決
* 暫時不支持修復構造方法,已經在內測
* **暫時不支持資源和 so 修復**,不過這個問題不大,因為獨立于 dex 補丁,已經有很成熟的方案了,就看怎么打到補丁包中以及 diff 方案。
* 對于返回值是 this 的方法支持不太好
* 沒有安全校驗,需要開發者在加載補丁之前自己做驗證
* 可能會出現深度方法內聯導致的不可預知的錯誤(幾率很小可以忽略)
### 2\. Sophix
相關文檔
[《深入探索Android熱修復技術原理》](https://yq.aliyun.com/articles/115122?spm=a2c4g.11186623.0.0.3afd1f05wgVPnD)
[業界首個非侵入式熱修復方案Sophix重磅推出,顛覆移動端傳統更新流程!](https://developer.aliyun.com/article/103527)
[Android熱修復升級探索——追尋極致的代碼熱替換](https://developer.aliyun.com/article/74598)
[Android熱修復升級探索——資源更新之新思路](https://developer.aliyun.com/article/96378)
[Android熱修復升級探索——Dalvik下冷啟動修復的新探索](https://developer.aliyun.com/article/107396)
#### 2.1 代碼修復
Sophix的代碼修復采用了底層替換方案,時效性最好,加載輕快,立即見效。
Java中的Method在ART虛擬機中對應一個ArtMethod指針,ArtMethod結構體中包含了Java方法的所有信息,包括執行入口、訪問權限、所屬類和代碼執行地址等。
替換ArtMethod結構體中的字段或者替換整個ArtMethod結構體,這就是**底層替換方案**。
AndFix采用的是替換ArtMethod結構體中的字段,這樣會有兼容性問題,因為廠商可能會修改ArtMethod結構體,導致方法替換失敗。
Sophix采用的是替換整個ArtMethod結構體,這樣不會存在兼容問題。
此外,Sophix也有類加載方案。這時需要冷啟動才會生效。
#### 2.2 資源修復
目前市面上的很多資源熱修復方案基本上都是參考了Instant Run的實現。實際上,Instant Run的推出正是推動這次熱修復浪潮的主因,各家熱修復方案,在代碼、資源等方面的實現,很大程度上地參考了Instant Run的代碼,而資源修復方案正是被拿來用到最多的地方。
簡要說來,Instant Run中的資源熱修復分為兩步:
1. 構造一個新的AssetManager,并通過反射調用addAssetPath,把這個完整的新資源包加入到AssetManager中。這樣就得到了一個含有所有新資源的AssetManager。
2. 找到所有之前引用到原有AssetManager的地方,通過反射,把引用處替換為AssetManager。
對于補丁中的id一樣的資源,直接addAssetPath最后是沒有效果的。所以這里**構造了一個package id為0x66的資源包,這個包里只包含改變了的資源項,然后直接在原有AssetManager中addAssetPath這個包就可以了**。由于補丁包的package id為0x66,不與目前已經加載的0x7f沖突,因此直接加入到已有的AssetManager中就可以直接使用了。
**L版本上這樣就夠了,但是在Android KK和以下版本,addAssetPath是不會加載資源的,必須重新構造一個新的AssetManager并加入patch,再換掉原來的。**所以這里可以先析構在重新初始化,native層會重新執行一遍流程。
#### 2.3 so修復
so庫的修復本質上是對native方法的修復和替換。采用的是類似類修復反射注入方式。把補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面,就能夠達到加載so庫的時候是補丁so庫,而不是原來so庫的目錄,從而達到修復的目的。
不過Sohpix也對so的及時生效方面做了一定的探索:
* **針對動態注冊的so,可以做到及時生效**。不對Dalvik虛擬機上有一個bug導致第二次load的so不生效。因此需要對補丁so進行改名。
* **針對靜態注冊的so,需要先對影響到的類進行解注冊操作,但能難知道哪些類受到了影響**。其次,就算解決了前者,在加載補丁so之后,由于so信息保存在虛擬機中的Hashtable(采用開放定址法)中,會有補丁so可能出現在原始so之前的情況,因此加載補丁so也不會生效。
因此,Sophix最后采用了冷啟動生效的方式,方案還是有兩種:
1. 接口調用替換方案
SDK提供統一的so加載方法,內部會先嘗試加載補丁中的so,不行再調用`System.loadLibrary`方法。此方案缺點是對業務方有侵入性。
2. 反射注入方案
將補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面,就能夠達到加載so庫的時候是補丁so庫
### 3.Tinker
#### 3.1 代碼修復
利用類加載機制,將合成好的補丁dex插入到DexPathList中elements數組的最前面,這樣查找類的時候就會先加載補丁中的類。
但是這里注意一下,這里的補丁都采用了diff算法來達到patch文件輕量的目錄,針對dex自研了DexDiff算法,其作用域是指令級別的。差分dex在客戶端會經過合成來變成一個全量的dex。
#### 3.2 資源修復
InstantRun的原理:還是通過AssetManager#addAssetPath來添加補丁中的resources文件,**但是這樣不會直接生效**。因此會重新創建一個AssetManager并更新所有的引用處。
注意這里對資源的修改,會比較生成的資源補丁文件的大小,超過配置的值則會采用bsdiff算法,否則不會采用。
#### 3.3 so修復[?](https://blog.yorek.xyz/android/3rd-library/hotfix/#33-so "Permanent link")
Tinker對so的修復沒有直接把so插入到nativeLibraryDirectories數組的最前面,原因是“我們并沒有為你區分abi(部分手機判斷不準確)”,所以無法獲取到具體的與架構相關的so。
Tinker提供了`TinkerApplicationHelper#loadArmV7aLibrary`、`TinkerApplicationHelper#loadArmLibrary`方法加載library。里面的邏輯是嘗試先從patch中找到對應的library,然后調用`System.load(String filename)`方法加載。如果找不到,則還是調用系統的`System.loadLibrary(libName)`。
## QFix
### 基本原理
DexClassLoader當找類的時候,會按順序遍歷dex文件,然后從當前遍歷的dex文件中找類,如果找類則返回,如果找不到繼續從下一個dex文件查找。理論上,如果在不同的dex中有相同的類存在,那么會優先選擇排在前面的dex文件的類,Qzone方案的靈感就是從上述的DexPathList類中的for循環體而來。

在此基礎上,Qzone 團隊構想了熱補丁的方案,把有問題的類打包到一個dex(patch.dex)中去,然后把這個dex插入到Elements的最前面,如下圖:

如果懂拆分dex的原理的話,大家應該很快就會實現該方案。如果沒有拆分dex的項目的話,可以參考一下谷歌的multidex方案實現,然后在插入數組的時候,把補丁包插入到最前面去。
當patch.dex中包含Main.class時就會優先加載,在后續的DEX中遇到Main.class的話就會直接返回而不去加載,這樣就達到了修復的目的。### Sophix
### unexpected DEX
如果**兩個相關聯的類在不同的dex**中就會報錯,但是拆分dex沒有報錯這是為什么,原來這個校驗的前提是:如果引用者(也就是ModuleManager)這個類被打上了CLASS\_ISPREVERIFIED標志,那么就會進行dex的校驗

# 參考資料
[QFix探索之路——手Q熱補丁輕量級方案](https://mp.weixin.qq.com/s/ce22vVN5zyqY0vAkNt1DDw)
- Android
- 四大組件
- Activity
- Fragment
- Service
- 序列化
- Handler
- Hander介紹
- MessageQueue詳細
- 啟動流程
- 系統啟動流程
- 應用啟動流程
- Activity啟動流程
- View
- view繪制
- view事件傳遞
- choreographer
- LayoutInflater
- UI渲染概念
- Binder
- Binder原理
- Binder最大數據
- Binder小結
- Android組件
- ListView原理
- RecyclerView原理
- SharePreferences
- AsyncTask
- Sqlite
- SQLCipher加密
- 遷移與修復
- Sqlite內核
- Sqlite優化v2
- sqlite索引
- sqlite之wal
- sqlite之鎖機制
- 網絡
- 基礎
- TCP
- HTTP
- HTTP1.1
- HTTP2.0
- HTTPS
- HTTP3.0
- HTTP進化圖
- HTTP小結
- 實踐
- 網絡優化
- Json
- ProtoBuffer
- 斷點續傳
- 性能
- 卡頓
- 卡頓監控
- ANR
- ANR監控
- 內存
- 內存問題與優化
- 圖片內存優化
- 線下內存監控
- 線上內存監控
- 啟動優化
- 死鎖監控
- 崩潰監控
- 包體積優化
- UI渲染優化
- UI常規優化
- I/O監控
- 電量監控
- 第三方框架
- 網絡框架
- Volley
- Okhttp
- 網絡框架n問
- OkHttp原理N問
- 設計模式
- EventBus
- Rxjava
- 圖片
- ImageWoker
- Gilde的優化
- APT
- 依賴注入
- APT
- ARouter
- ButterKnife
- MMKV
- Jetpack
- 協程
- MVI
- Startup
- DataBinder
- 黑科技
- hook
- 運行期Java-hook技術
- 編譯期hook
- ASM
- Transform增量編譯
- 運行期Native-hook技術
- 熱修復
- 插件化
- AAB
- Shadow
- 虛擬機
- 其他
- UI自動化
- JavaParser
- Android Line
- 編譯
- 疑難雜癥
- Android11滑動異常
- 方案
- 工業化
- 模塊化
- 隱私合規
- 動態化
- 項目管理
- 業務啟動優化
- 業務架構設計
- 性能優化case
- 性能優化-排查思路
- 性能優化-現有方案
- 登錄
- 搜索
- C++
- NDK入門
- 跨平臺
- H5
- Flutter
- Flutter 性能優化
- 數據跨平臺