美團Robust熱修復框架原理解析

1、熱修復框架現狀

目前熱修復框架主要有QQ空間補丁、HotFix、Tinker、Robust等。熱修復框架按照原理大體能夠分爲三類:java

  1. 基於 multidex機制 干預 ClassLoader 加載dex
  2. native 替換方法結構體
  3. instant-run 插樁方案

QQ空間補丁和Tinker都是使用的方案一; 阿里的AndFix使用的是方案二; 美團的Robust使用的是方案三。算法

1. QQ空間補丁原理

把補丁類生成 patch.dex,在app啓動時,使用反射獲取當前應用的ClassLoader,也就是 BaseDexClassLoader,反射獲取其中的pathList,類型爲DexPathList, 反射獲取其中的Element[] dexElements, 記爲elements1;而後使用當前應用的ClassLoader做爲父ClassLoader,構造出 patch.dexDexClassLoader,通用經過反射能夠獲取到對應的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

2. Tinker 原理

對於Tinker,修復前和修復後的apk分別定義爲apk1和apk2,tinker自研了一套dex文件差分合並算法,在生成補丁包時,生成一個差分包 patch.dex,後端下發patch.dex到客戶端時,tinker會開一個線程把舊apk的class.dex和patch.dex合併,生成新的class.dex並存放在本地目錄上,從新啓動時,會使用本地新生成的class.dex對應的elements替換原有的elements數組。函數

3. AndFix 原理

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

2、美團 Robust 熱修復方案原理

下面,進入今天的主題,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過程,因此這種方案兼容性最好。

3、Robust 實現細節

Robust 的實現方案很簡單,若是隻是這麼簡單瞭解一下,有不少細節問題,咱們不去接觸就不會意識到。 Robust 的實現能夠分紅三個部分:插樁、生成補丁包、加載補丁包。下面先從插樁開始。

1. 插樁

Robust 預先定義了一個配置文件 robust.xml,在這個配置文件能夠指定是否開啓插樁、哪些包下須要插樁、哪些包下不須要插樁,在編譯 Release 包時,RobustTransform 這個插件會自動遍歷全部的類,並根據配置文件中指定的規則,對類進行如下操做:

  1. 類中增長一個靜態變量 ChangeQuickRedirect changeQuickRedirect
  2. 在方法前插入一段代碼,若是是須要修補的方法就執行補丁包中的方法,若是不是則執行原有邏輯。

經常使用的字節碼操縱框架有:

  • ASM
  • AspectJ
  • BCEL
  • Byte Buddy
  • CGLIB
  • Cojen
  • Javassist
  • Serp

美團 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

至此插樁環節就結束了。

2. 生成補丁包

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 的類:

  1. 第一步 根據bug類 clone 出 Patch 類, 而後再刪除不須要打補丁的類。(爲何使用刪除方法而不是新增方法? 刪除更簡單)
  2. 第二步 爲 Patch 建立一個構造方法,用來接收bug類的實例對象。
  3. 遍歷 Patch 類中的全部方法,使用 ExprEditor + 反射 修改表達式。
  4. 刪除 Patch 類中全部的變量和父類。

這裏舉個例子,爲何這裏的處理這麼麻煩。

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;
    }
}

以一個類的一個方法修復生成補丁爲例,補丁包中包含三個文件:

  • TestPatch
  • TestPatchController
  • PatchesInfoImpl

生成的補丁包是jar格式的,咱們須要使用 jar2dex 將 jar 包轉換成 dex包。

3. 加載補丁包

當線上app反生bug後,能夠通知客戶端拉取對應的補丁包,下載補丁包完成後,會開一個線程執行如下操做:

  1. 使用 DexClassLoader 加載外部 dex 文件,也就是咱們生成的補丁包。
  2. 反射獲取 PatchesInfoImpl 中補丁包映射關係,如PatchedClassInfo("com.meituan.sample.Test", "com.meituan.robust.patch.TestPatchControl")。
  3. 反射獲取 Test 類插樁生成 changeQuickRedirect 對象,實例化 TestPatchControl,並賦值給 changeQuickRedirect

至此,bug就修復了,無需重啓實時生效。

4. 一些問題

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.

4、總結

首先要承認國內不一樣熱修復方案的開發者和組織作出的工做,作好熱修復解決方案不是一件簡單的事。 其次,從別人解決熱修復方案實施過程遇到問題上來看,這些開發者遇到問題後,追根溯源,會去找致使這個問題的本質緣由,而後才思考解決方案,這一點很值得咱們學習。

今年年初我花一個月的時間收錄整理了一套知識體系,若是有想法深刻的系統化的去學習的,能夠點擊傳送門,我會把我收錄整理的資料都送給你們,幫助你們更快的進階。

相關文章
相關標籤/搜索