熱修復(fù)相關(guān)知識總結(jié)

一伯复、補丁 DEX 生成

  1. 修復(fù)問題后,修改 Java/Kotlin 代碼焦辅,編譯生成新的 .class 文件
javac -d ./build/classes FixClass.java  # 生成 FixClass.class
  1. 生成新舊 DEX 文件
# 舊代碼生成 old.dex
d8 old_classes/*.class --output old.dex

# 新代碼生成 new.dex
d8 new_classes/*.class --output new.dex
  1. 差異對比(Diff)對比新舊 DEX 文件葛账,提取差異部分,使用二進制差分算法(如 BSDiff)生成補丁文件(.patch)
bsdiff old.dex new.dex patch.dex
# Tinker 示例
java -jar tinker-patch-cli.jar -old old.apk -new new.apk -output patch/

4.在本地模擬補丁加載流程,驗證修復(fù)效果

// 測試補丁加載邏輯(本地調(diào)試)
File patchFile = new File("本地路徑/patch.dex");
HotfixManager.applyPatch(patchFile);

二佳头、代碼中需提前實現(xiàn)的邏輯

  1. 補丁下載與存儲
// 偽代碼:下載補丁
File patchDir = new File(getFilesDir(), "hotfix_patches");
File patchFile = new File(patchDir, "patch_new.dex");
DownloadManager.download("https://example.com/patch.dex", patchFile);
  1. 補丁校驗與安全性驗證
// 校驗簽名和完整性
if (!SecurityUtils.verifySignature(patchFile, publicKey)) {
    throw new SecurityException("補丁簽名無效");
}
if (!MD5Utils.check(patchFile, expectedMD5)) {
    throw new IOException("補丁文件損壞");
}

3.使用 DexClassLoader 加載補丁鹰贵,并通過反射插入到 ClassLoade

// 加載補丁 DEX
File optimizedDir = new File(getCacheDir(), "dex_opt");
DexClassLoader loader = new DexClassLoader(
    patchFile.getPath(),   // 原始 DEX 路徑(如 /data/data/app/hotfix_patches/patch.dex)
    optimizedDir.getPath(), // 優(yōu)化后的 ODEX 存儲目錄(如 /data/data/app/cache/dex_opt)
    null,
    getClassLoader()
);


// 反射注入補丁到 PathClassLoader
try {
    Field pathListField = ClassLoader.class.getDeclaredField("pathList");
    pathListField.setAccessible(true);
    Object pathList = pathListField.get(getClassLoader());
    Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
    dexElementsField.setAccessible(true);
    Object[] dexElements = (Object[]) dexElementsField.get(pathList);

    // 獲取補丁的 dexElements
    Object patchElement = getDexElements(dexClassLoader); // 反射獲取補丁的 Element 數(shù)組
    Object newElements = Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + 1);
    System.arraycopy(patchElement, 0, newElements, 0, 1); // 補丁插入到最前面
    System.arraycopy(dexElements, 0, newElements, 1, dexElements.length);
    dexElementsField.set(pathList, newElements);
} catch (Exception e) {
    e.printStackTrace();
}
  1. 資源與 Native 庫修復(fù)
// 合并插件資源
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, patchFile.getAbsolutePath());
Resources newResources = new Resources(assetManager, getResources().getDisplayMetrics(), getResources().getConfiguration());
// 替換全局 Resources 實例(需 Hook ContextImpl)

三、線上 App 運行時的流程

  1. 檢查補丁更新
  2. 下載補丁文件
  3. 加載并應(yīng)用補丁
  4. 重啟生效或即時生效
    全量替換方案(Tinker):需重啟 App 或特定 Activity康嘉。
    方法替換方案(AndFix):即時生效(通過替換 ArtMethod)

完整代碼

public class HotfixManager {
    public static void applyPatch(Context context, File patchFile) {
        // 1. 檢查補丁文件是否存在
        if (patchFile == null || !patchFile.exists()) {
            Log.e("Hotfix", "補丁文件不存在");
            return;
        }

        // 2. 準備優(yōu)化目錄(dex_opt)
        File optimizedDir = new File(context.getCodeCacheDir(), "dex_opt");
        if (!optimizedDir.exists()) {
            optimizedDir.mkdirs();
        }

        try {
            // 3. 創(chuàng)建 DexClassLoader 加載補丁
            DexClassLoader dexClassLoader = new DexClassLoader(
                    patchFile.getAbsolutePath(),
                    optimizedDir.getAbsolutePath(),
                    null,
                    context.getClassLoader()
            );

            // 4. 反射獲取應(yīng)用的 PathClassLoader 中的 dexElements
            ClassLoader appClassLoader = context.getClassLoader();
            Object appPathList = getPathList(appClassLoader);
            Object[] appDexElements = getDexElements(appPathList);

            // 5. 獲取補丁的 dexElements
            Object patchPathList = getPathList(dexClassLoader);
            Object[] patchDexElements = getDexElements(patchPathList);

            // 6. 合并補丁和原 dexElements(將補丁插入到數(shù)組最前)
            Object[] newDexElements = combineArray(patchDexElements, appDexElements);

            // 7. 將合并后的數(shù)組注入到應(yīng)用的 ClassLoader
            setDexElements(appPathList, newDexElements);

            Log.i("Hotfix", "補丁加載成功");
        } catch (Exception e) {
            Log.e("Hotfix", "補丁加載失敗", e);
        }
    }

    // 反射工具方法(需處理異常和兼容性)
    private static Object getPathList(ClassLoader classLoader) throws Exception {
        Field pathListField = ClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        return pathListField.get(classLoader);
    }

    private static Object[] getDexElements(Object pathList) throws Exception {
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        return (Object[]) dexElementsField.get(pathList);
    }

    private static void setDexElements(Object pathList, Object[] elements) throws Exception {
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        dexElementsField.set(pathList, elements);
    }

    private static Object[] combineArray(Object[] front, Object[] end) {
        Object[] result = Arrays.copyOf(front, front.length + end.length);
        System.arraycopy(end, 0, result, front.length, end.length);
        return result;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末碉输,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子亭珍,更是在濱河造成了極大的恐慌敷钾,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肄梨,死亡現(xiàn)場離奇詭異阻荒,居然都是意外死亡,警方通過查閱死者的電腦和手機众羡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進店門侨赡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事羊壹”突拢” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵油猫,是天一觀的道長稠茂。 經(jīng)常有香客問我,道長情妖,這世上最難降的妖魔是什么睬关? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮毡证,結(jié)果婚禮上电爹,老公的妹妹穿的比我還像新娘。我一直安慰自己情竹,他們只是感情好藐不,可當我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秦效,像睡著了一般雏蛮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上阱州,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天挑秉,我揣著相機與錄音,去河邊找鬼苔货。 笑死犀概,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的夜惭。 我是一名探鬼主播姻灶,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼诈茧!你這毒婦竟也來了产喉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤敢会,失蹤者是張志新(化名)和其女友劉穎曾沈,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸥昏,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡塞俱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了吏垮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片障涯。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡罐旗,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出唯蝶,到底是詐尸還是另有隱情尤莺,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布生棍,位于F島的核電站,受9級特大地震影響媳谁,放射性物質(zhì)發(fā)生泄漏涂滴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一晴音、第九天 我趴在偏房一處隱蔽的房頂上張望柔纵。 院中可真熱鬧,春花似錦锤躁、人聲如沸搁料。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽郭计。三九已至,卻和暖如春椒振,著一層夾襖步出監(jiān)牢的瞬間昭伸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工澎迎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留庐杨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓夹供,卻偏偏與公主長得像灵份,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哮洽,可洞房花燭夜當晚...
    茶點故事閱讀 45,507評論 2 359