畢業(yè)5年了還不知道熱修復(fù)鹏溯?

前言

熱修復(fù)到現(xiàn)在2022年已經(jīng)不是一個(gè)新名詞,但是作為Android開發(fā)核心技術(shù)棧的一部分太抓,我這里還得來一次冷飯熱炒空闲。

隨著移動(dòng)端業(yè)務(wù)復(fù)雜程度的增加,傳統(tǒng)的版本更新流程顯然無法滿足業(yè)務(wù)和開發(fā)者的需求走敌,
熱修復(fù)技術(shù)的推出在很大程度上改善了這一局面进副。國(guó)內(nèi)大部分成熟的主流 App都擁有自己的熱更新技術(shù),像手淘悔常、支付寶、微信给赞、QQ机打、餓了么、美團(tuán)等片迅。

可以說残邀,一個(gè)好的熱修復(fù)技術(shù),將為你的 App助力百倍柑蛇。對(duì)于每一個(gè)想在 Android 開發(fā)領(lǐng)域有所造詣的開發(fā)者芥挣,掌握熱修復(fù)技術(shù)更是必備的素質(zhì)

熱修復(fù)是 Android 大廠面試中高頻面試知識(shí)點(diǎn)耻台,也是我們必須要掌握的知識(shí)點(diǎn)空免。熱修復(fù)技術(shù),可以看作 Android平臺(tái)發(fā)展成熟至一定階段的必然產(chǎn)物盆耽。
Android熱修復(fù)了解嗎蹋砚?修復(fù)哪些東西?
常見熱修復(fù)框架對(duì)比以及各原理分析摄杂?

1.什么是熱修復(fù)

熱修復(fù)說白了就是不再使用傳統(tǒng)的應(yīng)用商店更新或者自更新方式坝咐,使用補(bǔ)丁包推送的方式在用戶無感知的情況下,修復(fù)應(yīng)用bug或者推送新的需求

傳統(tǒng)更新熱更新過程對(duì)比如下:

熱修復(fù)過程圖.jpg

熱修復(fù)優(yōu)缺點(diǎn):

  • 優(yōu)點(diǎn):
    • 1.只需要打補(bǔ)丁包析恢,不需要重新發(fā)版本墨坚。
    • 2.用戶無感知,不需要重新下載最新應(yīng)用
    • 3.修復(fù)成功率高
  • 缺點(diǎn)
    • 補(bǔ)丁包濫用映挂,容易導(dǎo)致應(yīng)用版本不可控泽篮,需要開發(fā)一套完整的補(bǔ)丁包更新機(jī)制,會(huì)增加一定的成本

2.熱修復(fù)方案

首先我們得知道熱修復(fù)修復(fù)哪些東西袖肥?

  • 1.代碼修復(fù)
  • 2.資源修復(fù)
  • 3.動(dòng)態(tài)庫修復(fù)

2.1:代碼修復(fù)方案

從技術(shù)角度來說咪辱,我們的目的是非常明確的:把錯(cuò)誤的代碼替換成正確的代碼。
注意這里的替換椎组,并不是直接擦寫dx文件油狂,而是提供一份新的正確代碼,讓應(yīng)用運(yùn)行時(shí)繞過錯(cuò)誤代碼,執(zhí)行新的正確代碼专筷。

image.png

想法簡(jiǎn)單直接弱贼,但實(shí)現(xiàn)起來并不容易。目前主要有三類技術(shù)方案:

2.1.1.類加載方案

之前分析類加載機(jī)制有說過:
加載流程先是遵循雙親委派原則磷蛹,如果委派原則沒有找到此前加載過此類吮旅,
則會(huì)調(diào)用CLassLoader的findClass方法,再去BaseDexClassLoader下面的dexElements數(shù)組中查找味咳,如果沒有找到庇勃,最終調(diào)用defineClassNative方法加載

代碼修復(fù)就是基于這點(diǎn):
將新的做了修復(fù)的dex文件,通過反射注入到BaseDexClassLoader的dexElements數(shù)組的第一個(gè)位置上dexElements[0]槽驶,下次重新啟動(dòng)應(yīng)用加載類的時(shí)候责嚷,會(huì)優(yōu)先加載做了修復(fù)的dex文件,這樣就達(dá)到了修復(fù)代碼的目的掂铐。原理很簡(jiǎn)單

代碼如下:

public class Hotfix {

    public static void patch(Context context, String patchDexFile, String patchClassName)
                    throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //獲取系統(tǒng)PathClassLoader的"dexElements"屬性值
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object origDexElements = getDexElements(pathClassLoader);

        //新建DexClassLoader并獲取“dexElements”屬性值
        String otpDir = context.getDir("dex", 0).getAbsolutePath();
        Log.i("hotfix", "otpdir=" + otpDir);
        DexClassLoader nDexClassLoader = new DexClassLoader(patchDexFile, otpDir, patchDexFile, context.getClassLoader());
        Object patchDexElements = getDexElements(nDexClassLoader);

        //將patchDexElements插入原origDexElements前面
        Object allDexElements = combineArray(origDexElements, patchDexElements);

        //將新的allDexElements重新設(shè)置回pathClassLoader
        setDexElements(pathClassLoader, allDexElements);

        //重新加載類
        pathClassLoader.loadClass(patchClassName);
}
private static Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //首先獲取ClassLoader的“pathList”實(shí)例
        Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
        pathListField.setAccessible(true);//設(shè)置為可訪問
        Object pathList = pathListField.get(classLoader);

        //然后獲取“pathList”實(shí)例的“dexElements”屬性
        Field dexElementField = pathList.getClass().getDeclaredField("dexElements");
        dexElementField.setAccessible(true);

        //讀取"dexElements"的值
        Object elements = dexElementField.get(pathList);
        return elements;
    }
    //合拼dexElements
    private static Object combineArray(Object obj, Object obj2) {
        Class componentType = obj2.getClass().getComponentType();
        //讀取obj長(zhǎng)度
        int length = Array.getLength(obj);
        //讀取obj2長(zhǎng)度
        int length2 = Array.getLength(obj2);
        Log.i("hotfix", "length=" + length + ",length2=" + length2);
        //創(chuàng)建一個(gè)新Array實(shí)例罕拂,長(zhǎng)度為ojb和obj2之和
        Object newInstance = Array.newInstance(componentType, length + length2);
        for (int i = 0; i < length + length2; i++) {
                //把obj2元素插入前面
                if (i < length2) {
                        Array.set(newInstance, i, Array.get(obj2, i));
                } else {
                        //把obj元素依次放在后面
                        Array.set(newInstance, i, Array.get(obj, i - length2));
                }
        }
        //返回新的Array實(shí)例
        return newInstance;
    }
    private static void setDexElements(ClassLoader classLoader, Object dexElements) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //首先獲取ClassLoader的“pathList”實(shí)例
        Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
        pathListField.setAccessible(true);//設(shè)置為可訪問
        Object pathList = pathListField.get(classLoader);

        //然后獲取“pathList”實(shí)例的“dexElements”屬性
        Field declaredField = pathList.getClass().getDeclaredField("dexElements");
        declaredField.setAccessible(true);

        //設(shè)置"dexElements"的值
        declaredField.set(pathList, dexElements);
    }
}

類加載過程如下:

image.png

微信Tinker,QQ 空間的超級(jí)補(bǔ)丁全陨、手 QQ 的QFix 爆班、餓了 么的 AmigoNuwa 等都是使用這個(gè)方式

缺點(diǎn):因?yàn)轭惣虞d后無法卸載,所以類加載方案必須重啟App辱姨,讓bug類重新加載后才能生效柿菩。

2.1.2:底層替換方案

底層替換方案不會(huì)再次加載新類,而是直接在 Native 層 修改原有類雨涛,
這里我們需要提到Art虛擬機(jī)中ArtMethod
每一個(gè)Java方法在Art虛擬機(jī)中都對(duì)應(yīng)著一個(gè)ArtMethod碗旅,ArtMethod記錄了這個(gè)Java方法的所有信息,包括所屬類镜悉、訪問權(quán)限祟辟、代碼執(zhí)行地址等

結(jié)構(gòu)如下:

// art/runtime/art_method.h
class ArtMethod FINAL {
...
 protected:
  GcRoot<mirror::Class> declaring_class_;
  GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;
  GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;
  uint32_t access_flags_;
  uint32_t dex_code_item_offset_;
  uint32_t dex_method_index_;
  uint32_t method_index_;

  struct PACKED(4) PtrSizedFields {
        void* entry_point_from_interpreter_;      // 1
        void* entry_point_from_jni_;
        void* entry_point_from_quick_compiled_code_;  //2
  } ptr_sized_fields_;
  ...
}

在 ArtMethod結(jié)構(gòu)體中侣肄,最重要的就是 注釋1和注釋2標(biāo)注的內(nèi)容旧困,從名字可以看出來,他們就是方法的執(zhí)行入口稼锅。
我們知道吼具,Java代碼在Android中會(huì)被編譯為 Dex Code

Art虛擬機(jī)中可以采用解釋模式或者 AOT機(jī)器碼模式執(zhí)行 Dex Code

  • 解釋模式:
    就是去除Dex Code矩距,逐條解釋執(zhí)行拗盒。
    如果方法的調(diào)用者是以解釋模式運(yùn)行的,在調(diào)用這個(gè)方法時(shí)锥债,就會(huì)獲取這個(gè)方法的 entry_point_from_interpreter_陡蝇,然后跳轉(zhuǎn)執(zhí)行痊臭。
  • AOT模式:
    就會(huì)預(yù)先編譯好 Dex Code對(duì)應(yīng)的機(jī)器碼,然后在運(yùn)行期直接執(zhí)行機(jī)器碼登夫,不需要逐條解釋執(zhí)行Dex Code广匙。
    如果方法的調(diào)用者是以AOT機(jī)器碼方式執(zhí)行的,在調(diào)用這個(gè)方法時(shí)恼策,就是跳轉(zhuǎn)到 entry_point_from_quick_compiled_code_中執(zhí)行鸦致。

那是不是只需要替換這個(gè)幾個(gè) entry_point_* 入口地址就能夠?qū)崿F(xiàn)方法替換了呢?
并沒有那么簡(jiǎn)單涣楷,因?yàn)椴徽撌墙忉屇J竭€是AOT模式分唾,在運(yùn)行期間還會(huì)需要調(diào)用ArtMethod中的其他成員字段

AndFix采用的是改變指針指向

// AndFix/jni/art/art_method_replace_6_0.cpp
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
    art::mirror::ArtMethod* smeth =
                    (art::mirror::ArtMethod*) env->FromReflectedMethod(src);  // 1

    art::mirror::ArtMethod* dmeth =
                    (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);  // 2
    ...
    // 3
    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_);
}

缺點(diǎn):存在一些兼容性問題,由于ArtMethod結(jié)構(gòu)體是Android開源的一部分狮斗,所以每個(gè)手機(jī)廠商都可能會(huì)去更改這部分的內(nèi)容鳍寂,這就可能導(dǎo)致ArtMethod替換方案在某些機(jī)型上面出現(xiàn)未知錯(cuò)誤。

Sophix為了規(guī)避上面的AndFix的風(fēng)險(xiǎn)情龄,采用直接替換整個(gè)結(jié)構(gòu)體。這樣不管手機(jī)廠商如何更改系統(tǒng)捍壤,我們都可以正確定位到方法地址

2.4.3:install run方案

Instant Run 方案的核心思想是——插樁骤视,在編譯時(shí)通過插樁在每一個(gè)方法中插入代碼,修改代碼邏輯鹃觉,在需要時(shí)繞過錯(cuò)誤方法专酗,調(diào)用patch類的正確方法。

首先盗扇,在編譯時(shí)Instant Run為每個(gè)類插入IncrementalChange變量

IncrementalChange  $change;

為每一個(gè)方法添加類似如下代碼:

public void onCreate(Bundle savedInstanceState) {
    IncrementalChange var2 = $change;
    //$change不為null祷肯,表示該類有修改,需要重定向
    if(var2 != null) {
        //通過access$dispatch方法跳轉(zhuǎn)到patch類的正確方法
        var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});
    } else {
        super.onCreate(savedInstanceState);
        this.setContentView(2130968601);
        this.tv = (TextView)this.findViewById(2131492944);
    }
}

如上代碼疗隶,當(dāng)一個(gè)類被修改后佑笋,Instant Run會(huì)為這個(gè)類新建一個(gè)類,命名為xxx&override斑鼻,且實(shí)現(xiàn)IncrementalChange接口蒋纬,并且賦值給原類的$change變量。

public class MainActivity$override implements IncrementalChange {
}

此時(shí)坚弱,在運(yùn)行時(shí)原類中每個(gè)方法的var2 != null蜀备,通過accessdispatch(參數(shù)是方法名和原參數(shù))定位到patch類MainActivitydispatch(參數(shù)是方法名和原參數(shù))定位到patch類MainActivityoverride中修改后的方法。

Instant Run是google在AS2.0時(shí)用來實(shí)現(xiàn)“熱部署”的荒叶,同時(shí)也為“熱修復(fù)”提供了一個(gè)絕佳的思路碾阁。美團(tuán)的Robust就是基于此

2.2:資源修復(fù)方案

這里我們來看看install run的原理即可些楣,市面上的常見修復(fù)方案大部分都是基于此方法脂凶。

public static void monkeyPatchExistingResources(Context context,
            String externalResourceFile, Collection<Activity> activities) {
    if (externalResourceFile == null) {
            return;
    }
    try {
// 創(chuàng)建一個(gè)新的AssetManager
        AssetManager newAssetManager = (AssetManager) AssetManager.class
                        .getConstructor(new Class[0]).newInstance(new Object[0]); // ... 1
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
                        "addAssetPath", new Class[] { String.class }); // ... 2
        mAddAssetPath.setAccessible(true);
// 通過反射調(diào)用addAssetPath方法加載外部的資源(SD卡資源)
        if (((Integer) mAddAssetPath.invoke(newAssetManager,
                        new Object[] { externalResourceFile })).intValue() == 0) { // ... 3
                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]);
        if (activities != null) {
            for (Activity activity : activities) {
                Resources resources = activity.getResources(); // ... 4
                try { 
// 反射得到Resources的AssetManager類型的mAssets字段
                    Field mAssets = Resources.class
                                    .getDeclaredField("mAssets"); // ... 5
                    mAssets.setAccessible(true);
// 將mAssets字段的引用替換為新創(chuàng)建的newAssetManager
                    mAssets.set(resources, newAssetManager); // ... 6
                } catch (Throwable ignore) {
                    ...
                }

// 得到Activity的Resources.Theme
                Resources.Theme theme = activity.getTheme();
                try {
                    try {
// 反射得到Resources.Theme的mAssets字段
                        Field ma = Resources.Theme.class
                                        .getDeclaredField("mAssets");
                        ma.setAccessible(true);
// 將Resources.Theme的mAssets字段的引用替換為新創(chuàng)建的newAssetManager
                        ma.set(theme, newAssetManager); // ... 7
                    } catch (NoSuchFieldException ignore) {
                            ...
                    }
                        ...
                } catch (Throwable e) {
                    Log.e("InstantRun",
                                    "Failed to update existing theme for activity "
                                                    + activity, e);
                }
                pruneResourceCaches(resources);
        }
        }
/**
*  根據(jù)SDK版本的不同宪睹,用不同的方式得到Resources 的弱引用集合
*/ 
        Collection<WeakReference<Resources>> 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<?, WeakReference<Resources>> 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<?, WeakReference<Resources>> map = (HashMap) fMActiveResources
                            .get(thread);

            references = map.values();
        }
//遍歷并得到弱引用集合中的 Resources ,將 Resources mAssets 字段引用替換成新的 AssetManager
            for (WeakReference<Resources> 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) {
                        ...
                    }
                    resources.updateConfiguration(resources.getConfiguration(),
                                    resources.getDisplayMetrics());
                }
            }
    } catch (Throwable e) {
            throw new IllegalStateException(e);
    }
}
  • 注釋1處創(chuàng)建一個(gè)新的 AssetManager 艰猬,
  • 注釋2注釋3 處通過反射調(diào)用 addAssetPath 方法加載外部( SD 卡)的資源横堡。
  • 注釋4 處遍歷 Activity 列表,得到每個(gè) Activity 的 Resources 冠桃,
  • 注釋5 處通過反射得到 Resources 的 AssetManager 類型的 rnAssets 字段 命贴,
  • 注釋6處改寫 mAssets 字段的引用為新的 AssetManager 。

采用同樣的方式食听,

  • 注釋7處將 Resources. Theme 的 m Assets 字段 的引用替換為新創(chuàng)建的 AssetManager 胸蛛。
  • 緊接著 根據(jù) SDK 版本的不同,用不同的方式得到 Resources 的弱引用集合樱报,
  • 再遍歷這個(gè)弱引用集合葬项, 將弱引用集合中的 Resources 的 mAssets 字段引用都替換成新創(chuàng)建的 AssetManager 。

資源修復(fù)原理

  • 1.創(chuàng)建新的AssetManager迹蛤,通過反射調(diào)用addAssetPath方法民珍,加載外部資源,這樣新創(chuàng)建的AssetManager就含有了外部資源
  • 2.將AssetManager類型的mAsset字段全部用新創(chuàng)建的AssetManager對(duì)象替換盗飒。這樣下次加載資源文件的時(shí)候就可以找到包含外部資源文件的AssetManager嚷量。

2.3:動(dòng)態(tài)鏈接庫so的修復(fù)

1.接口調(diào)用替換方案:

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

SOPatchManager.loadLibrary(String libName) -> System.loadLibrary(String libName)

SOPatchManager.loadLibrary接口加載 so庫的時(shí)候優(yōu)先嘗試去加載sdk 指定目錄下的補(bǔ)丁so

加載策略如下:

如果存在則加載補(bǔ)丁 so庫而不會(huì)去加載安裝apk安裝目錄下的so庫
如果不存在補(bǔ)丁so逆趣,那么調(diào)用System.loadLibrary去加載安裝apk目錄下的 so庫蝶溶。

image.png

我們可以很清楚的看到這個(gè)方案的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):不需要對(duì)不同 sdk 版本進(jìn)行兼容,因?yàn)樗械?sdk 版本都有 System.loadLibrary 這個(gè)接口宣渗。
缺點(diǎn):調(diào)用方需要替換掉 System 默認(rèn)加載 so 庫接口為 sdk提供的接口抖所, 如果是已經(jīng)編譯混淆好的三方庫的so 庫需要 patch,那么是很難做到接口的替換

雖然這種方案實(shí)現(xiàn)簡(jiǎn)單痕囱,同時(shí)不需要對(duì)不同 sdk版本區(qū)分處理田轧,但是有一定的局限性沒法修復(fù)三方包的so庫同時(shí)需要強(qiáng)制侵入接入方接口調(diào)用,接著我們來看下反射注入方案鞍恢。

2涯鲁、反射注入方案

前面介紹過 System. loadLibrary ( "native-lib"); 加載 so庫的原理,其實(shí)native-lib 這個(gè) so 庫最終傳給 native 方法執(zhí)行的參數(shù)是 so庫在磁盤中的完整路徑有序,比如:/data/app-lib/com.taobao.jni-2/libnative-lib.so, so庫會(huì)在 DexPathList.nativeLibraryDirectories/nativeLibraryPathElements 變量所表示的目錄下去遍歷搜索

sdk<23 DexPathList.findLibrary 實(shí)現(xiàn)如下

image.png

可以發(fā)現(xiàn)會(huì)遍歷 nativeLibraryDirectories數(shù)組抹腿,如果找到了 loUtils.canOpenReadOnly (path)返回為 true, 那么就直接返回該 path, loUtils.canOpenReadOnly (path)返回為 true 的前提肯定是需要 path 表示的 so文件存 在的。那么我們可以采取類似類修復(fù)反射注入方式旭寿,只要把我們的補(bǔ)丁so庫的路徑插入到nativeLibraryDirectories數(shù)組的最前面就能夠達(dá)到加載so庫的時(shí)候是補(bǔ)丁 庫而不是原來so庫的目錄警绩,從而達(dá)到修復(fù)的目的。

sdk>=23 DexPathList.findLibrary 實(shí)現(xiàn)如下

image.png

sdk23 以上 findLibrary 實(shí)現(xiàn)已經(jīng)發(fā)生了變化盅称,如上所示肩祥,那么我們只需要把補(bǔ)丁so庫的完整路徑作為參數(shù)構(gòu)建一個(gè)Element對(duì)象后室,然后再插入到nativeLibraryPathElements 數(shù)組的最前面就好了。

  • 優(yōu)點(diǎn):可以修復(fù)三方庫的so庫混狠。同時(shí)接入方不需要像方案1 —樣強(qiáng)制侵入用 戶接口調(diào)用
  • 缺點(diǎn):需要不斷的對(duì) sdk 進(jìn)行適配岸霹,如上 sdk23 為分界線,findLibrary接口實(shí)現(xiàn)已經(jīng)發(fā)生了變化将饺。

對(duì)于 so庫的修復(fù)方案目前更多采取的是接口調(diào)用替換方式贡避,需要強(qiáng)制侵入用戶 接口調(diào)用。
目前我們的so文件修復(fù)方案采取的是反射注入的方案予弧,重啟生效刮吧。具有更好的普遍性丑搔。
如果有so文件修復(fù)實(shí)時(shí)生效的需求酗昼,也是可以做到的,只是有些限制情況颠通。

常見熱修復(fù)框架蚓庭?

特性 Dexposed AndFix Tinker/Amigo QQ Zone Robust/Aceso Sophix
技術(shù)原理 native底層替換 native底層替換 類加載 類加載 Instant Run 混合
所屬 阿里 阿里 微信/餓了么 QQ空間 美團(tuán)/蘑菇街 阿里
即時(shí)生效 YES YES NO NO YES 混合
方法替換 YES YES YES YES YES YES
類替換 NO NO YES YES YES YES
類結(jié)構(gòu)修改 NO NO YES NO NO YES
資源替換 NO NO YES YES NO YES
so替換 NO NO YES NO NO YES
支持gradle NO NO YES YES YES YES
支持ART NO YES YES YES YES YES

可以看出致讥,阿里系多采用native底層方案,騰訊系多采用類加載機(jī)制器赞。其中垢袱,Sophix是商業(yè)化方案;Tinker/Amigo支持特性較多拳魁,同時(shí)也更復(fù)雜,如果需要修復(fù)資源和so撮弧,可以選擇潘懊;如果僅需要方法替換,且需要即時(shí)生效贿衍,Robust是不錯(cuò)的選擇授舟。

總結(jié):

盡管熱修復(fù)(或熱更新)相對(duì)于迭代更新有諸多優(yōu)勢(shì),市面上也有很多開源方案可供選擇贸辈,但目前熱修復(fù)依然無法替代迭代更新模式释树。有如下原因:
熱修復(fù)框架多多少少會(huì)增加性能開銷,或增加APK大小
熱修復(fù)技術(shù)本身存在局限擎淤,比如有些方案無法替換so或資源文件
熱修復(fù)方案的兼容性奢啥,有些方案無法同時(shí)兼顧Dalvik和ART,有些深度定制系統(tǒng)也無法正常工作
監(jiān)管風(fēng)險(xiǎn)嘴拢,比如蘋果系統(tǒng)嚴(yán)格限制熱修復(fù)

所以桩盲,對(duì)于功能迭代和常規(guī)bug修復(fù),版本迭代更新依然是主流席吴。一般的代碼修復(fù)赌结,使用Robust可以解決捞蛋,如果還需要修復(fù)資源或so庫,可以考慮Tinker柬姚。

參考文章

掃描下方的微信二維碼拟杉,這里有一套完整的移動(dòng)端開發(fā)知識(shí)體系,助你進(jìn)階高級(jí)開發(fā)量承。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末搬设,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宴合,更是在濱河造成了極大的恐慌焕梅,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卦洽,死亡現(xiàn)場(chǎng)離奇詭異贞言,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)阀蒂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門该窗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蚤霞,你說我怎么就攤上這事酗失。” “怎么了昧绣?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵规肴,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我夜畴,道長(zhǎng)拖刃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任贪绘,我火速辦了婚禮兑牡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘税灌。我一直安慰自己均函,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布菱涤。 她就那樣靜靜地躺著苞也,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粘秆。 梳的紋絲不亂的頭發(fā)上墩朦,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音翻擒,去河邊找鬼氓涣。 笑死牛哺,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的劳吠。 我是一名探鬼主播引润,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼痒玩!你這毒婦竟也來了淳附?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蠢古,失蹤者是張志新(化名)和其女友劉穎奴曙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體草讶,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡洽糟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了堕战。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坤溃。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖嘱丢,靈堂內(nèi)的尸體忽然破棺而出薪介,到底是詐尸還是另有隱情,我是刑警寧澤越驻,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布汁政,位于F島的核電站,受9級(jí)特大地震影響缀旁,放射性物質(zhì)發(fā)生泄漏记劈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一诵棵、第九天 我趴在偏房一處隱蔽的房頂上張望抠蚣。 院中可真熱鬧祝旷,春花似錦履澳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吻谋,卻和暖如春忠蝗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漓拾。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工阁最, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留戒祠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓速种,卻偏偏與公主長(zhǎng)得像姜盈,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子配阵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容