[老實李] Android 中的熱修復方案總結

熱修復的三個部分

熱修復分為三個部分,分別是Java代碼部分熱修復Native代碼部分熱修復,還有資源熱修復

  1. 資源部分熱更新直接反射更改所有保存的AssetManager和Resources對象就行(可能需要重啟應用)

  2. Native代碼部分也很簡單,系統找到一個so文件的路徑是根據ClassLoader找的,修改ClassLoader里保存的路徑就行(可能需要重啟應用)

  3. Java部分的話目前主流有兩種方式,一種是Java派,一種是Native派。

  • java派:通過修改ClassLoader來讓系統優先加載補丁包里的類
    代表作有騰訊的tinker,谷歌官方的Instant Run,包括multidex也是采用的這種方案
    優點是穩定性較好,缺點是可能需要重啟應用
  • native派:通過內存操作實現,比如方法替換等
    代表作是阿里的SopHix,如果算上hook框架的話,還有dexposed,epic等等
    優點是即時生效無需重啟,缺點是穩定性不好:
    如果采用方法替換方式實現,假如這個方法被內聯/Sharpening優化了,那么就失效了;inline hook則無法修改超短方法。
    熱修復后使用反射調用對應方法時可能發生IllegalArgumentException。

類加載方案

  1. 什么是 dex?
    Android系統仿照java搞了一個虛擬機,不過它不叫JVM,它叫Dalvik/ART VM他們還是有很大區別的(這是不是我們的重點)。我們只需要知道,Dalvik/ART VM 虛擬機加載類和資源也是要用到ClassLoader,不過Jvm通過ClassLoader加載的class字節碼,而Dalvik/ART VM通過ClassLoader加載則是dex。
  2. ClassLoader的加載過程
    ClassLoader的加載過程,其中一個環節就是調用DexPathList的findClass的方法,如下所示:
    libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

Element內部封裝了DexFile,DexFile用于加載dex文件,因此每個dex文件對應一個Element。
多個Element組成了有序的Element數組dexElements。當要查找類時,會先遍歷dex文件數組,然后調用Element的findClass方法,其方法內部會調用DexFile的loadClassBinaryName方法查找類。如果在Element中(dex文件)找到了該類就返回,如果沒有找到就接著在下一個Element中進行查找。
根據上面的查找流程,我們將有bug的類Key.class進行修改,再將Key.class打包成包含dex的補丁包Patch.jar,放在Element數組dexElements的第一個元素,這樣會首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class,排在數組后面的dex文件中的存在bug的Key.class根據ClassLoader的雙親委托模式就不會被加載,這就是類加載方案.

微信Tinker將新舊apk做了diff,得到patch.dex,然后將patch.dex與手機中apk的classes.dex做合并,生成新的classes.dex,然后在運行時通過反射將classes.dex放在Element數組的第一個元素。

JavaHook 方案

美團 Robus 為代表

  1. 打基礎包時插樁,在每個方法前插入一段類型為 ChangeQuickRedirect 靜態變量的邏輯
  2. 加載補丁時,從補丁包中讀取要替換的類及具體替換的方法實現,新建 ClassLoader 加載補丁dex。
    下面通過Robust的源碼來進行分析。 首先看一下打基礎包是插入的代碼邏輯,如下:
 public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
        //為每個方法自動插入修復邏輯代碼,如果ChangeQuickRedirect為空則不執行
        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);
        ...
    }

Robust的核心修復源碼如下:

public class PatchExecutor extends Thread {
    @Override
    public void run() {
        ...
        applyPatchList(patches);
        ...
    }
    /**
     * 應用補丁列表
     */
    protected void applyPatchList(List<Patch> patches) {
        ...
        for (Patch p : patches) {
            ...
            currentPatchResult = patch(context, p);
            ...
            }
    }
     /**
     * 核心修復源碼
     */
    protected boolean patch(Context context, Patch patch) {
        ...
        //新建ClassLoader
        DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
                null, PatchExecutor.class.getClassLoader());
        patch.delete(patch.getTempPath());
        ...
        try {
            patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
            patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
            } catch (Throwable t) {
             ...
        }
        ...
        //通過遍歷其中的類信息進而反射修改其中 ChangeQuickRedirect 對象的值
        for (PatchedClassInfo patchedClassInfo : patchedClasses) {
            ...
            try {
                oldClass = classLoader.loadClass(patchedClassName.trim());
                Field[] fields = oldClass.getDeclaredFields();
                for (Field field : fields) {
                    if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
                        changeQuickRedirectField = field;
                        break;
                    }
                }
                ...
                try {
                    patchClass = classLoader.loadClass(patchClassName);
                    Object patchObject = patchClass.newInstance();
                    changeQuickRedirectField.setAccessible(true);
                    changeQuickRedirectField.set(null, patchObject);
                    } catch (Throwable t) {
                    ...
                }
            } catch (Throwable t) {
                 ...
            }
        }
        return true;
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。