你必學(xué)廢!Android熱修復(fù)技術(shù)全解析,看完不會我吃五斤熱X!

前言

熱修復(fù)技術(shù)是當(dāng)下Android開發(fā)中比較高級和熱門的知識點(diǎn),是中級開發(fā)人員通向高級開發(fā)中必須掌握的技能。同時(shí)目前Android業(yè)內(nèi),熱修復(fù)技術(shù)也是百花齊放,各大廠都推出了自己的熱修復(fù)方案,使用的技術(shù)方案也各有所異,當(dāng)然各個(gè)方案也都存在各自的局限性。希望通過本文的梳理闡述,了解這些熱修復(fù)方案的對比及實(shí)現(xiàn)原理,掌握熱修復(fù)技術(shù)的本質(zhì),同時(shí)也能應(yīng)用實(shí)踐到實(shí)際項(xiàng)目中去,幫助大家學(xué)以致用(文末有學(xué)習(xí)筆記分享)。

什么是熱修復(fù)

簡單來講,為了修復(fù)線上問題而提出的修補(bǔ)方案,程序修補(bǔ)過程無需重新發(fā)版!

正常版本開發(fā)與熱修復(fù)開發(fā)流程對比

為什么要學(xué)習(xí)熱修復(fù)

在正常軟件開發(fā)流程中,線下開發(fā)->上線->發(fā)現(xiàn)bug->緊急修復(fù)上線。不過對于這種方式代價(jià)太大,而且永遠(yuǎn)避免不了面臨如下幾個(gè)問題:

  1. 開發(fā)上線的版本能保證不存在Bug么?
  2. 修復(fù)后的版本能保證用戶都及時(shí)更新么?
  3. 如何最大化減少線上Bug對業(yè)務(wù)的影響?

而相對比之下,熱修復(fù)的開發(fā)流程就顯得更加靈活,無需重新發(fā)版,實(shí)時(shí)高效熱修復(fù),無需下載新的應(yīng)用,代價(jià)小,最重要的是及時(shí)的修復(fù)了bug。而且隨著熱修復(fù)技術(shù)的發(fā)展,現(xiàn)在不僅可以修復(fù)代碼,同時(shí)還可以修復(fù)資源文件及SO庫。

怎么選擇合適的熱修復(fù)技術(shù)方案?

文章開篇就說了現(xiàn)在各大廠都推出了自己的熱修復(fù)方案,那么我們到底該如何去選擇一套適合自己的熱修復(fù)技術(shù)去學(xué)習(xí)呢?接下來我將從現(xiàn)在主流熱修復(fù)的方案對比來給予你答案。

國內(nèi)主流熱修復(fù)技術(shù)方案

1、阿里系

名稱 說明
AndFix 開源,實(shí)時(shí)生效
HotFix 阿里百川,未開源,免費(fèi)、實(shí)時(shí)生效
Sophix 未開源,商業(yè)收費(fèi),實(shí)時(shí)生效/冷啟動修復(fù)

HotFix是AndFix的優(yōu)化版本,Sophix是HotFix的優(yōu)化版本。目前阿里系主推是Sophix。

2、騰訊系

名稱 說明
Qzone超級補(bǔ)丁 QQ空間,未開源,冷啟動修復(fù)
QFix 手Q團(tuán)隊(duì),開源,冷啟動修復(fù)
Tinker 微信團(tuán)隊(duì),開源,冷啟動修復(fù)。提供分發(fā)管理,基礎(chǔ)版免費(fèi)

3、其他

名稱 說明
Robust 美團(tuán), 開源,實(shí)時(shí)修復(fù)
Nuwa 大眾點(diǎn)評,開源,冷啟動修復(fù)
Amigo 餓了么,開源,冷啟動修復(fù)

各熱修復(fù)方案對比

怎么選擇合適的熱修復(fù)方案
怎么選?這個(gè)只能說一切看需求。如果公司綜合實(shí)力強(qiáng),完全考慮自研都沒問題,但需要綜合考慮成本及維護(hù)。下面給出2點(diǎn)建議,如下:

  1. 項(xiàng)目需求
  • 只需要簡單的方法級別Bug修復(fù)?
  • 需要資源及so庫的修復(fù)?
  • 對平臺兼容性要求及成功率要求?
  • 有需求對分發(fā)進(jìn)行控制,對監(jiān)控?cái)?shù)據(jù)進(jìn)行統(tǒng)計(jì),補(bǔ)丁包進(jìn)行管理?
  • 公司資源是否支持商業(yè)付費(fèi)?
  1. 學(xué)習(xí)及使用成本
  • 集成難度
  • 代碼侵入性
  • 調(diào)試維護(hù)
  1. 選擇大廠
  • 技術(shù)性能有保障
  • 有專人維護(hù)
  • 熱度高,開源社區(qū)活躍
  1. 如果考慮付費(fèi),推薦選擇阿里的Sophix,Sophix是綜合優(yōu)化的產(chǎn)物,功能完善、開發(fā)簡單透明、提供分發(fā)及監(jiān)控管理。如果不考慮付費(fèi),只需支持方法級別的Bug修復(fù),不支持資源及so,推薦使用Robust。如果考慮需要同時(shí)支持資源及so,推薦使用Tinker。最后如果公司綜合實(shí)力強(qiáng),可考慮自研,靈活性及可控制最強(qiáng)。

熱修復(fù)技術(shù)方案原理

技術(shù)分類

image

NativeHook 原理

原理及實(shí)現(xiàn)

NativeHook的原理是直接在native層進(jìn)行方法的結(jié)構(gòu)體信息對換,從而實(shí)現(xiàn)完美的方法新舊替換,從而實(shí)現(xiàn)熱修復(fù)功能。
下面以AndFix的一段jni代碼來進(jìn)行說明,如下:

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {

    // 通過Method對象得到底層Java函數(shù)對應(yīng)ArtMethod的真實(shí)地址
    art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

    art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->class_loader_ =
    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->class_loader_; //for plugin classloader
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->clinit_thread_id_ =
    reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->status_-1;
    //for reflection invoke
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
    //把舊函數(shù)的所有成員變量都替換為新函數(shù)的
    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
    smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
    smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
    smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
    smeth->dex_method_index_ = dmeth->dex_method_index_;
    smeth->method_index_ = dmeth->method_index_;

    smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
    dmeth->ptr_sized_fields_.entry_point_from_interpreter_;

    smeth->ptr_sized_fields_.entry_point_from_jni_ =
    dmeth->ptr_sized_fields_.entry_point_from_jni_;
    smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
    dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

    LOGD("replace_6_0: %d , %d",
         smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
         dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}

void setFieldFlag_6_0(JNIEnv* env, jobject field) {
    art::mirror::ArtField* artField =
            (art::mirror::ArtField*) env->FromReflectedField(field);
    artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
    LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);
}

每一個(gè)Java方法在art中都對應(yīng)一個(gè)ArtMethod,ArtMethod記錄了這個(gè)Java方法的所有信息,包括訪問權(quán)限及代碼執(zhí)行地址等。通過env->FromReflectedMethod得到方法對應(yīng)的ArtMethod的真正開始地址,然后強(qiáng)轉(zhuǎn)為ArtMethod指針,從而對其所有成員進(jìn)行修改。

這樣以后調(diào)用這個(gè)方法時(shí)就會直接走到新方法的實(shí)現(xiàn)中,達(dá)到熱修復(fù)的效果。

優(yōu)點(diǎn)

  • 即時(shí)生效
  • 沒有性能開銷,不需要任何編輯器的插樁或代碼改寫

缺點(diǎn)

  • 存在穩(wěn)定及兼容性問題。ArtMethod的結(jié)構(gòu)基本參考Google開源的代碼,各大廠商的ROM都可能有所改動,可能導(dǎo)致結(jié)構(gòu)不一致,修復(fù)失敗。
  • 無法增加變量及類,只能修復(fù)方法級別的Bug,無法做到新功能的發(fā)布

javaHook 原理

原理及實(shí)現(xiàn)

以美團(tuán)的Robust為例,Robust 的原理可以簡單描述為:

1、打基礎(chǔ)包時(shí)插樁,在每個(gè)方法前插入一段類型為 ChangeQuickRedirect 靜態(tài)變量的邏輯,插入過程對業(yè)務(wù)開發(fā)是完全透明

2、加載補(bǔ)丁時(shí),從補(bǔ)丁包中讀取要替換的類及具體替換的方法實(shí)現(xiàn),新建ClassLoader加載補(bǔ)丁dex。當(dāng)changeQuickRedirect不為null時(shí),可能會執(zhí)行到accessDispatch從而替換掉之前老的邏輯,達(dá)到fix的目的

下面通過Robust的源碼來進(jìn)行分析。
首先看一下打基礎(chǔ)包是插入的代碼邏輯,如下:

public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
        //為每個(gè)方法自動插入修復(fù)邏輯代碼,如果ChangeQuickRedirect為空則不執(zhí)行
        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的核心修復(fù)源碼如下:

public class PatchExecutor extends Thread {
    @Override
    public void run() {
        ...
        applyPatchList(patches);
        ...
    }
    /**
     * 應(yīng)用補(bǔ)丁列表
     */
    protected void applyPatchList(List<Patch> patches) {
        ...
        for (Patch p : patches) {
            ...
            currentPatchResult = patch(context, p);
            ...
            }
    }
     /**
     * 核心修復(fù)源碼
     */
    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) {
             ...
        }
        ...
        //通過遍歷其中的類信息進(jìn)而反射修改其中 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;
    }
}

優(yōu)點(diǎn)

  • 高兼容性(Robust只是在正常的使用DexClassLoader)、高穩(wěn)定性,修復(fù)成功率高達(dá)99.9%
  • 補(bǔ)丁實(shí)時(shí)生效,不需要重新啟動
  • 支持方法級別的修復(fù),包括靜態(tài)方法
  • 支持增加方法和類
  • 支持ProGuard的混淆、內(nèi)聯(lián)、優(yōu)化等操作

缺點(diǎn)

  • 代碼是侵入式的,會在原有的類中加入相關(guān)代碼
  • so和資源的替換暫時(shí)不支持
  • 會增大apk的體積,平均一個(gè)函數(shù)會比原來增加17.47個(gè)字節(jié),10萬個(gè)函數(shù)會增加1.67M

java mulitdex 原理

原理及實(shí)現(xiàn)

Android內(nèi)部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三個(gè)類加載器實(shí)現(xiàn)從DEX文件中讀取類數(shù)據(jù),其中PathClassLoader和DexClassLoader都是繼承自BaseDexClassLoader實(shí)現(xiàn)。dex文件轉(zhuǎn)換成dexFile對象,存入Element[]數(shù)組,findclass順序遍歷Element數(shù)組獲取DexFile,然后執(zhí)行DexFile的findclass。源碼如下:

// 加載名字為name的class對象
public Class findClass(String name, List<Throwable> suppressed) {
    // 遍歷從dexPath查詢到的dex和資源Element
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        // 如果當(dāng)前的Element是dex文件元素
        if (dex != null) {
            // 使用DexFile.loadClassBinaryName加載類
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],將補(bǔ)丁的dex插入到數(shù)組的最前端。因?yàn)镃lassLoader的findClass是通過遍歷dexElements[]中的dex來尋找類的。所以會優(yōu)先查找到修復(fù)的類。從而達(dá)到修復(fù)的效果。

下面使用Nuwa的關(guān)鍵實(shí)現(xiàn)源碼進(jìn)行說明如下:

public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        //新建一個(gè)ClassLoader加載補(bǔ)丁Dex
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
        //反射獲取舊DexElements數(shù)組
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
        //反射獲取補(bǔ)丁DexElements數(shù)組
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        //合并,將新數(shù)組的Element插入到最前面
        Object allDexElements = combineArray(newDexElements, baseDexElements);
        Object pathList = getPathList(getPathClassLoader());
        //更新舊ClassLoader中的Element數(shù)組
        ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
    }

    private static PathClassLoader getPathClassLoader() {
        PathClassLoader pathClassLoader = (PathClassLoader) DexUtils.class.getClassLoader();
        return pathClassLoader;
    }

    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return ReflectionUtils.getField(paramObject, paramObject.getClass(), "dexElements");
    }

    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        return ReflectionUtils.getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int allLength = firstArrayLength + Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(firstArray, k));
            } else {
                Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
            }
        }
        return result;
    }

優(yōu)點(diǎn)

  • 不需要考慮對dalvik虛擬機(jī)和art虛擬機(jī)做適配
  • 代碼是非侵入式的,對apk體積影響不大

缺點(diǎn)

  • 需要下次啟動才修復(fù)
  • 性能損耗大,為了避免類被加上CLASS_ISPREVERIFIED,使用插樁,單獨(dú)放一個(gè)幫助類在獨(dú)立的dex中讓其他類調(diào)用。

dex替換

原理及實(shí)現(xiàn)

為了避免dex插樁帶來的性能損耗,dex替換采取另外的方式。原理是提供dex差量包,整體替換dex的方案。差量的方式給出patch.dex,然后將patch.dex與應(yīng)用的classes.dex合并成一個(gè)完整的dex,完整dex加載得到dexFile對象作為參數(shù)構(gòu)建一個(gè)Element對象然后整體替換掉舊的dex-Elements數(shù)組。

這也是微信Tinker采用的方案,并且Tinker自研了DexDiff/DexMerge算法。Tinker還支持資源和So包的更新,So補(bǔ)丁包使用BsDiff來生成,資源補(bǔ)丁包直接使用文件md5對比來生成,針對資源比較大的(默認(rèn)大于100KB屬于大文件)會使用BsDiff來對文件生成差量補(bǔ)丁。

下面我們關(guān)鍵看看Tinker的實(shí)現(xiàn)源碼,當(dāng)然具體的實(shí)現(xiàn)算法很復(fù)雜,我們只看關(guān)鍵的實(shí)現(xiàn),最后的修復(fù)在UpgradePatch中的tryPatch方法,如下:

  @Override
    public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
        //省略一堆校驗(yàn)
        ... ....

        //下面是關(guān)鍵的diff算法及合并實(shí)現(xiàn),實(shí)現(xiàn)相對復(fù)雜,感興趣可以再仔細(xì)閱讀源碼
        //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
        if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
            return false;
        }

        if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
            return false;
        }

        if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
            return false;
        }

        // check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted
        if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
            return false;
        }

        if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {
            TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
            manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
            return false;
        }

        TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");
        return true;
    }

優(yōu)點(diǎn)

  • 兼容性高
  • 補(bǔ)丁小
  • 開發(fā)透明,代碼非侵入式

缺點(diǎn)

  • 冷啟動修復(fù),下次啟動修復(fù)
  • Dex合并內(nèi)存消耗在vm head上,容易OOM,最后導(dǎo)致合并失敗

資源修復(fù)原理

Instant Run

1、構(gòu)建一個(gè)新的AssetManager,并通過反射調(diào)用addAssertPath,把這個(gè)完整的新資源包加入到AssetManager中。這樣就得到一個(gè)含有所有新資源的AssetManager

2、找到所有值錢引用到原有AssetManager的地方,通過反射,把引用處替換為AssetManager

 public static void monkeyPatchExistingResources(Context context,
                                                    String externalResourceFile, Collection activities) {
        if (externalResourceFile == null) {
            return;
        }
        try {
            //反射一個(gè)新的   AssetManager
            AssetManager newAssetManager = (AssetManager) AssetManager.class
                    .getConstructor(new Class[0]).newInstance(new Object[0]);
           //反射 addAssetPath 添加新的資源包
            Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});
            mAddAssetPath.setAccessible(true);
            if (((Integer) mAddAssetPath.invoke(newAssetManager,
                    new Object[]{externalResourceFile})).intValue() == 0) {
                throw new IllegalStateException(
                        "Could not create new AssetManager");
            }
            Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
            mEnsureStringBlocks.setAccessible(true);
            mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
            //反射得到Activity中AssetManager的引用處,全部換成剛新構(gòu)建的AssetManager對象
            if (activities != null) {
                for (Activity activity : activities) {
                    Resources resources = activity.getResources();
                    try {
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    Resources.Theme theme = activity.getTheme();
                    try {
                        try {
                            Field ma = Resources.Theme.class.getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(theme, newAssetManager);
                        } catch (NoSuchFieldException ignore) {
                            Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
                            themeField.setAccessible(true);
                            Object impl = themeField.get(theme);
                            Field ma = impl.getClass().getDeclaredField("mAssets");
                            ma.setAccessible(true);
                            ma.set(impl, newAssetManager);
                        }
                        Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
                        mt.setAccessible(true);
                        mt.set(activity, null);
                        Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
                        mtm.setAccessible(true);
                        mtm.invoke(activity, new Object[0]);
                        Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
                        mCreateTheme.setAccessible(true);
                        Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
                        Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
                        mTheme.setAccessible(true);
                        mTheme.set(theme, internalTheme);
                    } catch (Throwable e) {
                        Log.e("InstantRun",
                                "Failed to update existing theme for activity "
                                        + activity, e);
                    }
                    pruneResourceCaches(resources);
                }
            }
            Collection references;
            if (Build.VERSION.SDK_INT >= 19) {
                Class resourcesManagerClass = Class.forName("android.app.ResourcesManager");
                Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
                mGetInstance.setAccessible(true);
                Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
                try {
                    Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                    fMActiveResources.setAccessible(true);
                    ArrayMap  arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);
                    references = arrayMap.values();
                } catch (NoSuchFieldException ignore) {
                    Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                    mResourceReferences.setAccessible(true);
                    references = (Collection) mResourceReferences.get(resourcesManager);
                }
            } else {
                Class activityThread = Class.forName("android.app.ActivityThread");
                Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                Object thread = getActivityThread(context, activityThread);
                HashMap  map = (HashMap) fMActiveResources.get(thread);
                references = map.values();
            }
            for (WeakReference wr : references) {
                Resources resources = (Resources) wr.get();
                if (resources != null) {
                    try {
                        Field mAssets = Resources.class.getDeclaredField("mAssets");
                        mAssets.setAccessible(true);
                        mAssets.set(resources, newAssetManager);
                    } catch (Throwable ignore) {
                        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                        mResourcesImpl.setAccessible(true);
                        Object resourceImpl = mResourcesImpl.get(resources);
                        Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                        implAssets.setAccessible(true);
                        implAssets.set(resourceImpl, newAssetManager);
                    }
                    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }

so修復(fù)原理

接口調(diào)用替換

sdk提供接口替換System默認(rèn)加載so庫的接口

SOPatchManger.loadLibrary(String libName)
替換
System.loadLibrary(String libName)

SOPatchManger.loadLibrary接口加載so庫的時(shí)候優(yōu)先嘗試去加載sdk指定目錄下補(bǔ)丁的so。若不存在,則再去加載安裝apk目錄下的so庫

優(yōu)點(diǎn):不需要對不同sdk版本進(jìn)行兼容,所以sdk版本都是System.loadLibrary這個(gè)接口

缺點(diǎn):需要侵入業(yè)務(wù)代碼,替換掉System默認(rèn)加載so庫的接口

反射注入

采取類似類修復(fù)反射注入方式,只要把補(bǔ)丁so庫的路徑插入到nativeLibraryDirectories數(shù)組的最前面,就能夠達(dá)到加載so庫的時(shí)候是補(bǔ)丁so庫而不是原來so庫的目錄,從而達(dá)到修復(fù)。

public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);

        for (NativeLibraryElement element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);

            if (path != null) {
                return path;
            }
        }

        return null;
    }

優(yōu)點(diǎn):不需侵入用戶接口調(diào)用

缺點(diǎn):需要做版本兼容控制,兼容性較差

使用熱修復(fù)技術(shù)有哪些需要注意的問題?

版本管理

使用熱修復(fù)技術(shù)后由于發(fā)布流程的變化,肯定也需求采用相應(yīng)的分支管理進(jìn)行控制。

通常移動開發(fā)的分支管理采用特性分支,如下:

分支 描述
master 主分支(只能merge,不能commit,設(shè)置權(quán)限),用于管理線上版本,及時(shí)設(shè)置對應(yīng)Tag
dev 開發(fā)分支,每個(gè)新版本的研發(fā)根據(jù)版本號基于主分支創(chuàng)建,測試通過驗(yàn)證后,上線合入master分支
function X 功能分支,按需求設(shè)定?;陂_發(fā)分支創(chuàng)建,完成功能開發(fā)后合入dev開發(fā)分支

接入熱修復(fù)后,推薦可參考如下分支策略:

分支 描述
master 主分支(只能merge,不能commit,設(shè)置權(quán)限),用于管理線上版本,及時(shí)設(shè)置對應(yīng)Tag(一般3位版本號)
hot_fix 熱修復(fù)分支?;趍aster分支創(chuàng)建,修復(fù)緊急問題后,測試推送后,將hot_fix再合并到master分支。再次為master分支打tag。(一般4位版本號)
dev 開發(fā)分支,每個(gè)新版本的研發(fā)根據(jù)版本號基于主分支創(chuàng)建,測試通過驗(yàn)證后,上線合入master分支
function X 功能分支,按需求設(shè)定?;陂_發(fā)分支創(chuàng)建,完成功能開發(fā)后合入dev開發(fā)分支

注意熱修復(fù)分支的測試及發(fā)布流程應(yīng)用正常版本流程一致,保證質(zhì)量。

分發(fā)監(jiān)控

目前主流的熱修復(fù)方案,像Tinker及Sophix都會提供補(bǔ)丁的分發(fā)及監(jiān)控。這也是我們選擇熱修復(fù)技術(shù)方案需要考慮的關(guān)鍵因素之一。畢竟為了保證線上版本的質(zhì)量,分發(fā)控制及實(shí)時(shí)監(jiān)測必不可少。

最后

想要深入了解熱修復(fù),需要了解類加載機(jī)制,Instant Run,multidex以及java底層實(shí)現(xiàn)細(xì)節(jié),JNI,AAPT和虛擬機(jī)的知識,需要龐大的知識貯備才能進(jìn)行深入理解,當(dāng)然Android Framwork的實(shí)現(xiàn)細(xì)節(jié)是非常重要的。熟悉熱修復(fù)的原理有助于我們提供自己的編程水平,提升自己解決問題的能力,最后熱修復(fù)不是簡單的客戶端SDK,它還包含了安全機(jī)制和服務(wù)端的控制邏輯,整條鏈路也不是短時(shí)間可以快速完成的。

所以為了方便朋友們更直觀快速的學(xué)習(xí)掌握Android熱修復(fù)技術(shù),我這里收集整理一套視頻+電子書的熱修復(fù)系列學(xué)習(xí)資料。視頻教程為愛奇藝高級工程師Lance老師主講,以Qzone熱修復(fù)實(shí)戰(zhàn)項(xiàng)目為例,深入淺出的全方位講解熱修復(fù)技術(shù)。電子書是來源于阿里的《深入探索Android熱修復(fù)技術(shù)原理》對熱修復(fù)技術(shù)有很深入解讀。

由于篇幅原因,這里只做一些截圖展示,需要完整資料的朋友,可以在點(diǎn)贊+評論后,點(diǎn)擊此處免費(fèi)獲??!

熱修復(fù)學(xué)習(xí)視頻

熱修復(fù)視頻內(nèi)容
深入探索Android熱修復(fù)技術(shù)原理電子書

熱修復(fù)技術(shù)原理電子書內(nèi)容

需要完整資料的朋友,可以在點(diǎn)贊+評論后,點(diǎn)擊此處免費(fèi)獲??!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 目前Android業(yè)內(nèi),熱修復(fù)技術(shù)百花齊放,各大廠都推出了自己的熱修復(fù)方案,使用的技術(shù)方案也各有所異,當(dāng)然各...
    黃俊彬閱讀 19,210評論 3 63
  • 上周五線上項(xiàng)目出現(xiàn)了緊急缺陷,無奈之下周六苦逼加班發(fā)補(bǔ)丁??,唯一值得欣慰的是由于出現(xiàn)缺陷的功能會在今天通過 ABT...
    Android之禪閱讀 1,316評論 0 0
  • 久違的晴天,家長會。 家長大會開好到教室時(shí),離放學(xué)已經(jīng)沒多少時(shí)間了。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,788評論 16 22
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    余生動聽閱讀 10,798評論 0 11
  • 可愛進(jìn)取,孤獨(dú)成精。努力飛翔,天堂翱翔。戰(zhàn)爭美好,孤獨(dú)進(jìn)取。膽大飛翔,成就輝煌。努力進(jìn)取,遙望,和諧家園??蓯塾巫?..
    趙原野閱讀 3,444評論 1 1

友情鏈接更多精彩內(nèi)容