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

前言

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

隨著移動端業(yè)務(wù)復(fù)雜程度的增加捻浦,傳統(tǒng)的版本更新流程顯然無法滿足業(yè)務(wù)和開發(fā)者的需求晤揣, 熱修復(fù)技術(shù)的推出在很大程度上改善了這一局面。國內(nèi)大部分成熟的主流 App都擁有自己的熱更新技術(shù)朱灿,像手淘昧识、支付寶、微信盗扒、QQ滞诺、餓了么、美團等环疼。

可以說习霹,一個好的熱修復(fù)技術(shù),將為你的 App助力百倍炫隶。對于每一個想在 Android 開發(fā)領(lǐng)域有所造詣的開發(fā)者淋叶,掌握熱修復(fù)技術(shù)更是必備的素質(zhì)

熱修復(fù)是 Android 大廠面試中高頻面試知識點伪阶,也是我們必須要掌握的知識點煞檩。熱修復(fù)技術(shù),可以看作 Android平臺發(fā)展成熟至一定階段的必然產(chǎn)物栅贴。 Android熱修復(fù)了解嗎斟湃?修復(fù)哪些東西? 常見熱修復(fù)框架對比以及各原理分析檐薯?

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

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

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

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

  • 優(yōu)點:
    • 1.只需要打補丁包坛缕,不需要重新發(fā)版本墓猎。
    • 2.用戶無感知,不需要重新下載最新應(yīng)用
    • 3.修復(fù)成功率高
  • 缺點
    • 補丁包濫用赚楚,容易導(dǎo)致應(yīng)用版本不可控毙沾,需要開發(fā)一套完整的補丁包更新機制,會增加一定的成本

2.熱修復(fù)方案

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

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

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

從技術(shù)角度來說左胞,我們的目的是非常明確的:把錯誤的代碼替換成正確的代碼。 注意這里的替換举户,并不是直接擦寫dx文件烤宙,而是提供一份新的正確代碼,讓應(yīng)用運行時繞過錯誤代碼敛摘,執(zhí)行新的正確代碼门烂。

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

2.1.1.類加載方案

之前分析類加載機制有說過: 加載流程先是遵循雙親委派原則屯远,如果委派原則沒有找到此前加載過此類蔓姚, 則會調(diào)用CLassLoader的findClass方法,再去BaseDexClassLoader下面的dexElements數(shù)組中查找慨丐,如果沒有找到坡脐,最終調(diào)用defineClassNative方法加載

代碼修復(fù)就是基于這點: 將新的做了修復(fù)的dex文件,通過反射注入到BaseDexClassLoader的dexElements數(shù)組的第一個位置上dexElements[0]房揭,下次重新啟動應(yīng)用加載類的時候备闲,會優(yōu)先加載做了修復(fù)的dex文件,這樣就達到了修復(fù)代碼的目的捅暴。原理很簡單

代碼如下:

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”實例
        Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
        pathListField.setAccessible(true);//設(shè)置為可訪問
        Object pathList = pathListField.get(classLoader);

        //然后獲取“pathList”實例的“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長度
        int length = Array.getLength(obj);
        //讀取obj2長度
        int length2 = Array.getLength(obj2);
        Log.i("hotfix", "length=" + length + ",length2=" + length2);
        //創(chuàng)建一個新Array實例恬砂,長度為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實例
        return newInstance;
    }
    private static void setDexElements(ClassLoader classLoader, Object dexElements) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //首先獲取ClassLoader的“pathList”實例
        Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
        pathListField.setAccessible(true);//設(shè)置為可訪問
        Object pathList = pathListField.get(classLoader);

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

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

類加載過程如下:

微信Tinker,QQ 空間的超級補丁蓬痒、手 QQ 的QFix 泻骤、餓了 么的 AmigoNuwa 等都是使用這個方式

缺點:因為類加載后無法卸載,所以類加載方案必須重啟App梧奢,讓bug類重新加載后才能生效狱掂。

2.1.2:底層替換方案

底層替換方案不會再次加載新類,而是直接在 Native 層 修改原有類亲轨, 這里我們需要提到Art虛擬機中ArtMethod: 每一個Java方法在Art虛擬機中都對應(yīng)著一個 ArtMethod趋惨,ArtMethod記錄了這個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中會被編譯為 Dex Code渐裸。

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

  • 解釋模式: 就是去除Dex Code巫湘,逐條解釋執(zhí)行。 如果方法的調(diào)用者是以解釋模式運行的昏鹃,在調(diào)用這個方法時尚氛,就會獲取這個方法的 entry_point_from_interpreter_,然后跳轉(zhuǎn)執(zhí)行洞渤。
  • AOT模式: 就會預(yù)先編譯好 Dex Code對應(yīng)的機器碼阅嘶,然后在運行期直接執(zhí)行機器碼,不需要逐條解釋執(zhí)行Dex Code。 如果方法的調(diào)用者是以AOT機器碼方式執(zhí)行的讯柔,在調(diào)用這個方法時抡蛙,就是跳轉(zhuǎn)到 entry_point_from_quick_compiled_code_中執(zhí)行。

那是不是只需要替換這個幾個 entry_point_* 入口地址就能夠?qū)崿F(xiàn)方法替換了呢魂迄? 并沒有那么簡單粗截,因為不論是解釋模式還是AOT模式,在運行期間還會需要調(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_);
}

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

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

2.4.3:install run方案

Instant Run 方案的核心思想是——插樁选泻,在編譯時通過插樁在每一個方法中插入代碼,修改代碼邏輯美莫,在需要時繞過錯誤方法页眯,調(diào)用patch類的正確方法。

首先厢呵,在編譯時Instant Run為每個類插入IncrementalChange變量

IncrementalChange  $change;

為每一個方法添加類似如下代碼:

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)一個類被修改后碌奉,Instant Run會為這個類新建一個類,命名為xxx&override寒砖,且實現(xiàn)IncrementalChange接口赐劣,并且賦值給原類的$change變量。

public class MainActivity$override implements IncrementalChange {
}

此時哩都,在運行時原類中每個方法的var2 != null魁兼,通過accessdispatch(參數(shù)是方法名和原參數(shù))定位到patch類MainActivitydispatch(參數(shù)是方法名和原參數(shù))定位到patch類MainActivityoverride中修改后的方法。

Instant Run是google在AS2.0時用來實現(xiàn)“熱部署”的漠嵌,同時也為“熱修復(fù)”提供了一個絕佳的思路咐汞。美團的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)建一個新的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)建一個新的 AssetManager 植阴,
  • 注釋2注釋3 處通過反射調(diào)用 addAssetPath 方法加載外部( SD 卡)的資源蟹瘾。
  • 注釋4 處遍歷 Activity 列表,得到每個 Activity 的 Resources 掠手,
  • 注釋5 處通過反射得到 Resources 的 AssetManager 類型的 rnAssets 字段 热芹,
  • 注釋6處改寫 mAssets 字段的引用為新的 AssetManager 。

采用同樣的方式惨撇,

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

資源修復(fù)原理

  • 1.創(chuàng)建新的AssetManager剖淀,通過反射調(diào)用addAssetPath方法纯蛾,加載外部資源,這樣新創(chuàng)建的AssetManager就含有了外部資源
  • 2.將AssetManager類型的mAsset字段全部用新創(chuàng)建的AssetManager對象替換纵隔。這樣下次加載資源文件的時候就可以找到包含外部資源文件的AssetManager翻诉。

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

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

sdk提供接口替換System默認加載so庫接口

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

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

加載策略如下:

如果存在則加載補丁 so庫而不會去加載安裝apk安裝目錄下的so庫 如果不存在補丁so捌刮,那么調(diào)用System.loadLibrary去加載安裝apk目錄下的 so庫碰煌。

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

雖然這種方案實現(xiàn)簡單俄认,同時不需要對不同 sdk版本區(qū)分處理个少,但是有一定的局限性沒法修復(fù)三方包的so庫同時需要強制侵入接入方接口調(diào)用,接著我們來看下反射注入方案眯杏。

2夜焦、反射注入方案

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

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

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

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

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

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

對于 so庫的修復(fù)方案目前更多采取的是接口調(diào)用替換方式,需要強制侵入用戶 接口調(diào)用乌询。 目前我們的so文件修復(fù)方案采取的是反射注入的方案榜贴,重啟生效。具有更好的普遍性妹田。 如果有so文件修復(fù)實時生效的需求唬党,也是可以做到的,只是有些限制情況鬼佣。

常見熱修復(fù)框架驶拱?

特性 Dexposed AndFix Tinker/Amigo QQ Zone Robust/Aceso Sophix
技術(shù)原理 native底層替換 native底層替換 類加載 類加載 Instant Run 混合
所屬 阿里 阿里 微信/餓了么 QQ空間 美團/蘑菇街 阿里
即時生效 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底層方案晶衷,騰訊系多采用類加載機制蓝纲。其中,Sophix是商業(yè)化方案晌纫;Tinker/Amigo支持特性較多税迷,同時也更復(fù)雜,如果需要修復(fù)資源和so锹漱,可以選擇翁狐;如果僅需要方法替換,且需要即時生效凌蔬,Robust是不錯的選擇露懒。

總結(jié):

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

所以抠忘,對于功能迭代和常規(guī)bug修復(fù),版本迭代更新依然是主流外永。一般的代碼修復(fù)崎脉,使用Robust可以解決,如果還需要修復(fù)資源或so庫伯顶,可以考慮Tinker囚灼。

參考文章

作者:高級攻城獅
鏈接:https://juejin.cn/post/7142481619604111390

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末骆膝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子灶体,更是在濱河造成了極大的恐慌阅签,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝎抽,死亡現(xiàn)場離奇詭異政钟,居然都是意外死亡,警方通過查閱死者的電腦和手機樟结,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門养交,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狭吼,你說我怎么就攤上這事层坠。” “怎么了刁笙?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵破花,是天一觀的道長。 經(jīng)常有香客問我疲吸,道長座每,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任摘悴,我火速辦了婚禮峭梳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蹂喻。我一直安慰自己葱椭,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布口四。 她就那樣靜靜地躺著孵运,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蔓彩。 梳的紋絲不亂的頭發(fā)上治笨,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音赤嚼,去河邊找鬼旷赖。 笑死,一個胖子當(dāng)著我的面吹牛更卒,可吹牛的內(nèi)容都是我干的等孵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逞壁,長吁一口氣:“原來是場噩夢啊……” “哼流济!你這毒婦竟也來了锐锣?” 一聲冷哼從身側(cè)響起腌闯,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤绳瘟,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后姿骏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體糖声,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年分瘦,在試婚紗的時候發(fā)現(xiàn)自己被綠了蘸泻。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡嘲玫,死狀恐怖悦施,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情去团,我是刑警寧澤抡诞,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站土陪,受9級特大地震影響昼汗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鬼雀,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一顷窒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧源哩,春花似錦鞋吉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至崩侠,卻和暖如春漆魔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背却音。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工改抡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人系瓢。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓阿纤,卻偏偏與公主長得像,于是被迫代替她去往敵國和親夷陋。 傳聞我的和親對象是個殘疾皇子欠拾,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

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