目前熱修復框架主要有QQ空間補丁、HotFix、Tinker、Robust等。熱修復框架按照原理大體能夠分爲三類:java
QQ空間補丁和Tinker都是使用的方案一; 阿里的AndFix使用的是方案二; 美團的Robust使用的是方案三。算法
把補丁類生成 patch.dex
,在app啓動時,使用反射獲取當前應用的ClassLoader
,也就是 BaseDexClassLoader
,反射獲取其中的pathList
,類型爲DexPathList
, 反射獲取其中的Element[] dexElements
, 記爲elements1
;而後使用當前應用的ClassLoader
做爲父ClassLoader
,構造出 patch.dex
的 DexClassLoader
,通用經過反射能夠獲取到對應的Element[] dexElements
,記爲elements2
。將elements2
拼在elements1
前面,而後再去調用加載類的方法loadClass
。後端
隱藏的技術難點 CLASS_ISPREVERIFIED 問題數組
apk在安裝時會進行dex文件進行驗證和優化操做。這個操做能讓app運行時直接加載odex文件,可以減小對內存佔用,加快啓動速度,若是沒有odex操做,須要從apk包中提取dex再運行。app
在驗證過程,若是某個類的調用關係都在同一個dex文件中,那麼這個類會被打上
CLASS_ISPREVERIFIED
標記,表示這個類已經預先驗證過了。可是再使用的過程當中會反過來校驗下,若是這個類被打上了CLASS_ISPREVERIFIED
可是存在調用關係的類不在同一個dex文件中的話,會直接拋出異常。框架爲了解決這個問題,QQ空間給出的解決方案就是,準備一個 AntilazyLoad 類,這個類會單獨打包成一個 hack.dex,而後在全部的類的構造方法中增長這樣的代碼:編輯器
if (ClassVerifier.PREVENT_VERIFY) { System.out.println(AntilazyLoad.class); }這樣在 odex 過程當中,每一個類都會出現 AntilazyLoad 在另外一個dex文件中的問題,因此odex的驗證過程也就不會繼續下去,這樣作犧牲了dvm對dex的優化效果了。ide
對於Tinker,修復前和修復後的apk分別定義爲apk1和apk2,tinker自研了一套dex文件差分合並算法,在生成補丁包時,生成一個差分包 patch.dex,後端下發patch.dex到客戶端時,tinker會開一個線程把舊apk的class.dex和patch.dex合併,生成新的class.dex並存放在本地目錄上,從新啓動時,會使用本地新生成的class.dex對應的elements替換原有的elements數組。函數
AndFix的修復原理是替換方法的結構體。在native層獲取修復前類和修復後類的指針,而後將舊方法的屬性指針指向新方法。因爲不一樣系統版本下的方法結構體不一樣,並且davilk與art虛擬機處理方式也不同,因此須要針對不一樣系統針對性的替換方法結構體。學習
// AndFix 代碼目錄結構 jni ├─ Android.mk ├─ Application.mk ├─ andfix.cpp ├─ art │ ├─ art.h │ ├─ art_4_4.h │ ├─ art_5_0.h │ ├─ art_5_1.h │ ├─ art_6_0.h │ ├─ art_7_0.h │ ├─ art_method_replace.cpp │ ├─ art_method_replace_4_4.cpp │ ├─ art_method_replace_5_0.cpp │ ├─ art_method_replace_5_1.cpp │ ├─ art_method_replace_6_0.cpp │ └─ art_method_replace_7_0.cpp ├─ common.h └─ dalvik ├─ dalvik.h └─ dalvik_method_replace.cpp
下面,進入今天的主題,Robust熱修復方案。首先,介紹一下 Robust 的實現原理。
以 State 類爲例
public long getIndex() { return 100L; }
插樁後的 State 類
public static ChangeQuickRedirect changeQuickRedirect; public long getIndex() { if(changeQuickRedirect != null) { //PatchProxy中封裝了獲取當前className和methodName的邏輯,並在其內部最終調用了changeQuickRedirect的對應函數 if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) { return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue(); } } return 100L; }
咱們生成一個 StatePatch 類, 創一個實例並反射賦值給 State 的 changeQuickRedirect 變量。
public class StatePatch implements ChangeQuickRedirect { @Override public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) { String[] signature = methodSignature.split(":"); // 混淆後的 getIndex 方法 對應 a if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a return 106; } return null; } @Override public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) { String[] signature = methodSignature.split(":"); if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a return true; } return false; } }
當咱們執行出問題的代碼 getState 時,會轉而執行 StatePatch 中邏輯。這就 Robust 的核心原理,因爲沒有干擾系統加載dex過程,因此這種方案兼容性最好。
Robust 的實現方案很簡單,若是隻是這麼簡單瞭解一下,有不少細節問題,咱們不去接觸就不會意識到。 Robust 的實現能夠分紅三個部分:插樁、生成補丁包、加載補丁包。下面先從插樁開始。
Robust 預先定義了一個配置文件 robust.xml
,在這個配置文件能夠指定是否開啓插樁、哪些包下須要插樁、哪些包下不須要插樁,在編譯 Release 包時,RobustTransform 這個插件會自動遍歷全部的類,並根據配置文件中指定的規則,對類進行如下操做:
ChangeQuickRedirect changeQuickRedirect
經常使用的字節碼操縱框架有:
美團 Robust 分別使用了ASM、Javassist兩個框架實現了插樁修改字節碼的操做。我的感受 javaassist 更加容易理解一些,下面的代碼分析都以 javaassist 操做字節碼爲例進行闡述。
for (CtBehavior ctBehavior : ctClass.getDeclaredBehaviors()) { // 第一步: 增長 靜態變量 changeQuickRedirect if (!addIncrementalChange) { //insert the field addIncrementalChange = true; // 建立一個靜態變量並添加到 ctClass 中 ClassPool classPool = ctBehavior.getDeclaringClass().getClassPool(); CtClass type = classPool.getOrNull(Constants.INTERFACE_NAME); // com.meituan.robust.ChangeQuickRedirect CtField ctField = new CtField(type, Constants.INSERT_FIELD_NAME, ctClass); // changeQuickRedirect ctField.setModifiers(AccessFlag.PUBLIC | AccessFlag.STATIC); ctClass.addField(ctField); } // 判斷這個方法須要修復 if (!isQualifiedMethod(ctBehavior)) { continue; } // 第二步: 方法前插入一段代碼 ... }
對於方法前插入一段代碼,
// Robust 給每一個方法取了一個惟一id methodMap.put(ctBehavior.getLongName(), insertMethodCount.incrementAndGet()); try { if (ctBehavior.getMethodInfo().isMethod()) { CtMethod ctMethod = (CtMethod) ctBehavior; boolean isStatic = (ctMethod.getModifiers() & AccessFlag.STATIC) != 0; CtClass returnType = ctMethod.getReturnType(); String returnTypeString = returnType.getName(); // 這個body 就是要塞到方法前面的一段邏輯 String body = "Object argThis = null;"; // 在 javaassist 中 $0 表示 當前實例對象,等於this if (!isStatic) { body += "argThis = $0;"; } String parametersClassType = getParametersClassType(ctMethod); // 在 javaassist 中 $args 表達式表明 方法參數的數組,能夠看到 isSupport 方法傳了這些參數:方法全部參數,當前對象實例,changeQuickRedirect,是不是靜態方法,當前方法id,方法全部參數的類型,方法返回類型 body += " if (com.meituan.robust.PatchProxy.isSupport($args, argThis, " + Constants.INSERT_FIELD_NAME + ", " + isStatic + ", " + methodMap.get(ctBehavior.getLongName()) + "," + parametersClassType + "," + returnTypeString + ".class)) {"; // getReturnStatement 負責返回執行補丁包中方法的代碼 body += getReturnStatement(returnTypeString, isStatic, methodMap.get(ctBehavior.getLongName()), parametersClassType, returnTypeString + ".class"); body += " }"; // 最後,把咱們寫出來的body插入到方法執行前邏輯 ctBehavior.insertBefore(body); } } catch (Throwable t) { //here we ignore the error t.printStackTrace(); System.out.println("ctClass: " + ctClass.getName() + " error: " + t.getMessage()); }
再來看看 getReturnStatement
方法,
private String getReturnStatement(String type, boolean isStatic, int methodNumber, String parametersClassType, String returnTypeString) { switch (type) { case Constants.CONSTRUCTOR: return " com.meituan.robust.PatchProxy.accessDispatchVoid( $args, argThis, changeQuickRedirect, " + isStatic + ", " + methodNumber + "," + parametersClassType + "," + returnTypeString + "); "; case Constants.LANG_VOID: return " com.meituan.robust.PatchProxy.accessDispatchVoid( $args, argThis, changeQuickRedirect, " + isStatic + ", " + methodNumber + "," + parametersClassType + "," + returnTypeString + "); return null;"; // 省略了其餘返回類型處理 } }
PatchProxy.accessDispatchVoid
最終調用了 changeQuickRedirect.accessDispatch
。
至此插樁環節就結束了。
Robust 定義了一個 Modify 註解,
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR}) @Retention(RetentionPolicy.CLASS) @Documented public @interface Modify { String value() default ""; }
對於要修復的方法,直接在方法聲明時增長 Modify
註解
@Modify public String getTextInfo() { getArray(); //return "error occur " ; return "error fixed"; }
在編譯期間,Robust逐一遍歷全部類,若是這個類有方法須要修復,Robust 會生一個 xxPatch 的類:
這裏舉個例子,爲何這裏的處理這麼麻煩。
public class Test { private int num = 0; public void increase() { num += 1; } public void decrease() { // 這裏減錯了 num -= 2; } public static void main(String[] args) { Test t1 = new Test(); // 執行完 num=1 t1.increase(); // 執行完 num=2 t1.increase(); // 執行完 num=0, decrease 方法出現了bug,咱們本意是減1,結果減2了 t1.decrease(); } }
因此當咱們下發補丁時,對num進行減1的操做也是針對t1對象的num操做。這就是爲何咱們須要建立一個構造方案接受bug類實例對象。再來講下,咱們如何在 TestPatch 類中把全部對 TestPatch 變量和方法等調用遷移到 Test 上。這就須要使用到 ExprEditor (表達式編輯器)。
// 這個 method 就是 TestPatch 修復後的那個方法 method.instrument( new ExprEditor() { // 處理變量訪問 public void edit(FieldAccess f) throws CannotCompileException { if (Config.newlyAddedClassNameList.contains(f.getClassName())) { return; } Map memberMappingInfo = getClassMappingInfo(f.getField().declaringClass.name); try { // 若是是 讀取變量,那麼把 f 使用replace方法,替換成括號裏的返回的表達式 if (f.isReader()) { f.replace(ReflectUtils.getFieldString(f.getField(), memberMappingInfo, temPatchClass.getName(), modifiedClass.getName())); } // 若是是 寫數據到變量 else if (f.isWriter()) { f.replace(ReflectUtils.setFieldString(f.getField(), memberMappingInfo, temPatchClass.getName(), modifiedClass.getName())); } } catch (NotFoundException e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } } } )
ReflectUtils.getFieldString 方法調用的結果是生成一串相似這樣的字符串:
\$_=(\$r) com.meituan.robust.utils.EnhancedRobustUtils.getFieldValue(fieldName, instance, clazz)
這樣在 TestPatch 中對變量 num 的調用,在編譯期間都會轉爲經過反射對 原始bug類對象 t1 的 num 變量調用。
ExprEditor 除了變量訪問 FieldAccess, 還有這些狀況須要特殊處理。
public void edit(NewExpr e) throws CannotCompileException { } public void edit(MethodCall m) throws CannotCompileException { } public void edit(FieldAccess f) throws CannotCompileException { } public void edit(Cast c) throws CannotCompileException { }
須要處理的狀況太多了,以至於Robust的做者都忍不住吐槽: shit !!too many situations need take into consideration
生成完 Patch 類以後,Robust 會從模板類的基礎上生成一個這個類專屬的 ChangeQuickRedirect 類, 模板類代碼以下:
public class PatchTemplate implements ChangeQuickRedirect { public static final String MATCH_ALL_PARAMETER = "(\\w*\\.)*\\w*"; public PatchTemplate() { } private static final Map<Object, Object> keyToValueRelation = new WeakHashMap<>(); @Override public Object accessDispatch(String methodName, Object[] paramArrayOfObject) { return null; } @Override public boolean isSupport(String methodName, Object[] paramArrayOfObject) { return true; } }
以Test類爲例,生成 ChangeQuickRedirect 類名爲 TestPatchController, 在編譯期間會在 isSupport 方法前加入過濾邏輯,
// 根據方法的id判斷是不是補丁方法執行 public boolean isSupport(String methodName, Object[] paramArrayOfObject) { return "23:".contains(methodName.split(":")[3]); }
以上兩個類生成後,會生成一個維護 bug類 --> ChangeQuickRedirect 類的映射關係
public class PatchesInfoImpl implements PatchesInfo { public List getPatchedClassesInfo() { ArrayList arrayList = new ArrayList(); arrayList.add(new PatchedClassInfo("com.meituan.sample.Test", "com.meituan.robust.patch.TestPatchControl")); EnhancedRobustUtils.isThrowable = false; return arrayList; } }
以一個類的一個方法修復生成補丁爲例,補丁包中包含三個文件:
生成的補丁包是jar格式的,咱們須要使用 jar2dex 將 jar 包轉換成 dex包。
當線上app反生bug後,能夠通知客戶端拉取對應的補丁包,下載補丁包完成後,會開一個線程執行如下操做:
至此,bug就修復了,無需重啓實時生效。
a. Robust 致使Proguard 方法內聯失效
Proguard是一款代碼優化、混淆利器,Proguard 會對程序進行優化,若是某個方法很短或者只被調用了一次,那麼Proguard會把這個方法內部邏輯內聯到調用處。 Robust的解決方案是找到內聯方法,不對內聯的方法插樁。
b. lambada 表達式修復
對於 lambada 表達式沒法直接添加註解,Robust 提供了一個 RobustModify 類,modify 方法是空方法,再編譯期間使用 ExprEditor 檢測是否調用了 RobustModify 類,若是調用了,就認爲這個方法須要修復。
new Thread( () -> { RobustModify.modify(); System.out.print("Hello"); System.out.println(" Hoolee"); } ).start();
c. Robust 生成方法id是經過編譯期間遍歷全部類和方法,遞增id實現的
一個方法,能夠經過類名 + 方法名 + 參數類型惟一肯定。我本身的方案是把這三個數據組裝成 類名@方法名#參數類型md5
,支持 lambada 表達式(com.orzangleli.demo.Test#lambda$execute$0@2ab6d5a5d73bad3848b7be22332e27ea
)。我本身基於 Robust 的核心原理,仿寫了一個熱修復框架 Anivia.
首先要承認國內不一樣熱修復方案的開發者和組織作出的工做,作好熱修復解決方案不是一件簡單的事。 其次,從別人解決熱修復方案實施過程遇到問題上來看,這些開發者遇到問題後,追根溯源,會去找致使這個問題的本質緣由,而後才思考解決方案,這一點很值得咱們學習。
今年年初我花一個月的時間收錄整理了一套知識體系,若是有想法深刻的系統化的去學習的,能夠點擊傳送門,我會把我收錄整理的資料都送給你們,幫助你們更快的進階。