Android熱修復及原理總結(jié)和介紹

熱修復的產(chǎn)生原因

  • 剛發(fā)布的版本出現(xiàn)了bug准浴,需要修復bug事扭、測試并打包在各大應用市場重新發(fā)布上架。這樣會耗費大量的人力和物力乐横,代價比較大求橄。
  • 已經(jīng)修復了此前版本的bug,如果下一個版本是一個大版本葡公,那么兩個版本的發(fā)布時間間隔往往會較長吹泡,這樣如果要等到大版本發(fā)布才能修復此前的bug哥谷,則bug就會長期地影響用戶耕拷。
  • 版本升級率不高疫铜,并且需要很長時間來完成新版本的覆蓋柱嫌,此前版本的bug會一直影響還沒有升級到新版本的用戶。
  • 有一些重要的小功能,需要短時間內(nèi)完成版本覆蓋,比如節(jié)日活動。

以上情況下豹爹,就需要用到熱修復來解決問題裆悄。

熱修復框架的對比

  • 阿里系——AndFix、阿里百川臂聋、Sophix光稼、Dexposed
  • 騰訊系——微信Tinker、QQ空間的超級補丁孩等、手機QQ的Qfix
  • 知名公司——美團的Robust艾君、餓了么的Amigo、美麗說蘑菇街的Aceso
  • 其他——Nuwa肄方、RocooFix冰垄、AnoleFix

雖然市面上熱修復框架很多,但熱修復的核心技術(shù)主要是三種:資源修復权她、代碼修復和動態(tài)鏈接庫修復虹茶。其中每個核心技術(shù)又有各種不同的技術(shù)方案,各種技術(shù)方案又有不同的實現(xiàn)隅要,而且很多熱修復框架都還在持續(xù)的迭代和完善蝴罪。總的來說步清,熱修復框架的技術(shù)實現(xiàn)是繁多可變的要门,作為開發(fā)者,需要了解這些技術(shù)方案的基本原理廓啊。
熱修復框架的對比

資源修復

很多熱修復框架的資源修復實現(xiàn)都參考了Instant Run的資源修復的原理欢搜。

Instant Run

Instant Run是Android Studio 2.0以后新增的一種運行機制,能夠顯著減少開發(fā)者第二次以及以后的App構(gòu)建和部署時間谴轮。

在沒有Instant Run之前狂巢,我們編譯部署App的流程如下圖所示:
Instant Run出現(xiàn)之前
也就是說,每次修改代碼或者資源之后书聚,傳統(tǒng)的編譯部署都需要重新安裝Apk和重啟App唧领。這樣的機制,效率比較低雌续,有重復不必要的耗時斩个,Instant Run避免了這一情況。
Instant Run的運行機制

可以看出Instant Run的構(gòu)建部署都是基于更改的部分驯杜。Instant Run的部署有三種方式受啥,Instant Run會根據(jù)修改代碼和資源的情況來決定用那種部署方式,無論那種方式,都不需要重新安裝Apk滚局。

  • Hot Swap——它是效率最高的部署方式居暖,代碼的增量修改不需要重啟App,甚至不需要重啟當前Activity藤肢。典型場景:修改了一個現(xiàn)有方法中的代碼邏輯時采用Hot Swap太闺。
  • Warm Swap——App不需要重啟,但是當前的Activity需要重啟嘁圈。典型場景:修改或者刪除了一個現(xiàn)有的資源文件時會采用Warm Swap省骂。
  • Cold Swap——需要重啟App,但不需要重新安裝Apk最住。采用Cold Swap的情況很多钞澳,比如添加、刪除或者修改了一個類的字段或方法涨缚,添加了一個類等轧粟。
    更多Intant Run原理相關(guān)的介紹,有興趣的同學可以出門左轉(zhuǎn):深度理解Android InstantRun原理以及源碼分析

Instant Run資源修復

既然很多熱修復框架的的資源修復都是參考了Instant Run的資源修復原理脓魏,那我們了解Instant Run的資源修復原理就能夠觸類旁通了逃延。Instant Run資源修復的核心邏輯在MonkeyPatcher的monkeyPatchExistingResources方法里面

public static void monkeyPatchExistingResources(Context context,
                                                String externalResourceFile, Collection activities) {
    if (externalResourceFile == null) {
        return;
    }
    try {
        //創(chuàng)建一個新的AssetManager對象
        AssetManager newAssetManager = (AssetManager) AssetManager.class
                .getConstructor(new Class[0]).newInstance(new Object[0]);
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
                "addAssetPath", new Class[] { String.class });
        mAddAssetPath.setAccessible(true);
        //通過反射調(diào)用mAddAssetPath方法,加載指定路徑的資源文件
        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]);
        if (activities != null) {
            for (Activity activity : activities) {
                            //遍歷所有的Avitity對象轧拄,獲取其Resources對象
                Resources resources = activity.getResources();
                try {
                    Field mAssets = Resources.class
                            .getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    //將resources對象的mAssets字段設置為新創(chuàng)建的newAssetManager
                    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);
    }
}

簡述其過程:創(chuàng)建一個新的AssetManager對象揽祥,通過反射調(diào)用它的addAssetPath方法,加載指定路徑下的資源檩电。然后遍歷Activity列表拄丰,得到每個Activity的Resources對象,在通過反射得到Resources對象的mAsset字段(AssetManager類型)俐末,將mAsset字段設置為新創(chuàng)建的AssetManager對象料按。其后的代碼邏輯,也采用類似的方式卓箫,根據(jù)SDK的版本不同载矿,用不同的方式,得到Resources的弱引用集合烹卒,再遍歷這個弱引用集合闷盔,將弱引用集合中的Resources的mAssets字段引用都替換成新創(chuàng)建的AssetManager對象。
可以看出旅急,Instant Run的資源修復可以簡單地總結(jié)為兩個步驟:

  1. 創(chuàng)建新的AssetManager對象逢勾,通過反射調(diào)用addAssetPath方法加載外部資源,這樣新創(chuàng)建的AssetManager就含有了新的外部資源藐吮。
  2. 將AssetManager類型的字段mAsset全部設置為新創(chuàng)建的AssetManager對象溺拱。

代碼修復

代碼修復主要有三種方案逃贝,分別是底層替換方法,類加載方案和Instant Run方案

類加載方案

類加載方案基于Dex分包方案迫摔,要了解Dex分包方案沐扳,需要先了解下65535限制和LinearAlloc限制

65535限制

隨著應用程序的功能越來越復雜,代碼量不斷增大句占,引入的庫越來越多沪摄,程序在編譯時會提示65535限制:

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

這個異常說明程序中引用的方法數(shù)量不能超過65536個,產(chǎn)生這一問題的原因是系統(tǒng)的65536限制辖众,這個限制歸因于DVM bytecode的限制卓起,DVM指令集的方法調(diào)用指令invoke-kind索引為16bits和敬,最多能引用65535個方法凹炸。

LinearAlloc限制

在安裝Apk時,如果遇到提示INSTALL_FAILED_DEXPORT昼弟,產(chǎn)生的原因就是LinearAlloc限制啤它,DVM的LinearAlloc是一個固定的緩存區(qū),當方法數(shù)超過了緩存區(qū)的大小時舱痘,就會報錯变骡。

為了解決65535限制和LinearAlloc限制,從而產(chǎn)生了Dex分包方案芭逝,Dex分包方案主要做的是在打包apk時塌碌,將程序代碼分成多個dex文件,App啟動時必須用到的類和這些類的直接引用類都放在主dex文件中旬盯,其他代碼放在次dex文件中台妆。當App啟動時,先加載主dex文件胖翰,等到App啟動后接剩,再按需動態(tài)加載次dex文件。從而避免了主dex文件的65535限制和LinearAlloc限制萨咳。
Dex分包方案主要有兩種:google官方方案和Dex自動拆包+動態(tài)加載方案懊缺。

在Android的ClassLoader加載過程中,有一個環(huán)節(jié)就是BaseDexClassLoader的findClass方法中培他,調(diào)用了DexPathList的findClass方法鹃两。

public Class<?> findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }


    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

在DexPathList的findClass方法中,遍歷DexElements數(shù)組舀凛,調(diào)用Element的findClass方法怔毛,Element是DexPathList的靜態(tài)內(nèi)部類

static class Element {
    /**
     * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
     * (only when dexFile is null).
     */
    private final File path;
    private final DexFile dexFile;
    private ClassPathURLStreamHandler urlHandler;
    private boolean initialized;
    ...
    public Class<?> findClass(String name, ClassLoader definingContext,
            List<Throwable> suppressed) {
        return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                : null;
    }
    ...
}

Element內(nèi)部封裝了DexFile,它用于加載dex文件腾降,因此每一個dex文件對應著一個Element對象拣度,多個Element對象組成了dexElements數(shù)組。當要查找某個類時,會先遍歷dexElements數(shù)組抗果,調(diào)用Element的findClass方法來查找筋帖。如果Element(dex文件)中找到該類就返回,否則就接著在下一個Element(dex文件)中去查找冤馏。根據(jù)這樣的流程日麸,我們先將有bug的類進行修改,再打包成包含dex文件的補丁包(如Patch.jar)逮光,將Patch.dex文件對應的Element對象放在dexElement數(shù)組的第一個元素代箭,這樣在DexPathList.findClass方法的執(zhí)行時,會先從Patch.dex中找到已修復bug的目標類涕刚,根據(jù)ClassLoader的雙親委派模式嗡综,排在后面的dex文件中存在bug的類就不會加載了,從而實現(xiàn)了替換之前存在bug的類杜漠。這就是類加載方案的原理极景。
類加載方案

類加載方案需要重啟App讓ClassLoader重新加載新的類,為什么要重啟App呢驾茴?這是因為類加載之后無法被卸載盼樟,之前存在bug的類一直在ClassLoader的緩存中,要想重新加載新的類替換存在bug的類锈至,就需要重啟App晨缴,讓ClassLoader重新加載,因此采用類加載方案的熱修復框架是不能即時生效的峡捡。雖然很多熱修復框架都采用了類加載方案击碗,但具體的實現(xiàn)細節(jié)和步驟還是有一些區(qū)別的。比如QQ空間的超級補丁和Nuwa棋返,是按照上面說的將補丁包對應的Element元素放在dexElements數(shù)組的第一個元素使其優(yōu)先加載延都。微信Tinker將新舊apk做了更改區(qū)分,得到了patch.dex睛竣,再將patch.dex與原來apk中的class.dex合并晰房,生成新的class.dex,然后運行時射沟,通過反射將class.dex放在dexElement數(shù)組的第一個元素殊者。餓了么的Amigo則是將補丁包中的每一個dex文件對應的Element取出來,組成新的Element數(shù)組验夯,在運行時通過反射用新的Element數(shù)組替換原來的dexElement數(shù)組猖吴。
采用類加載方案的主要以騰訊系為主,包括微信的Tinker挥转、QQ空間的超級補丁海蔽、手機QQ的Qfix共屈、餓了么的Amigo和Nuwa等

底層替換方案

底層替換方案不會再次加載新類,而是直接在Native層修改原有的類党窜,由于在原有類上進行修改限制比較多拗引,并且不能增減原有類的方法和字段。如果我們增加了方法數(shù)幌衣,那么方法索引數(shù)也會增加矾削,這樣訪問方法時會無法通過找到正確的方法,同樣的豁护,字段也是類似的情況哼凯。底層替換方案和反射的原理有些關(guān)聯(lián),通過Method.invoke()方法來調(diào)用一個方法楚里,其中invoke方法是一個native方法断部,對應jni層的Method_invoke函數(shù),Method_invoke函數(shù)內(nèi)部又會調(diào)用invokeMethod函數(shù)腻豌,并將java方法對應的javaMethod對象作為參數(shù)傳進去家坎,javaMethod在ART虛擬機中對應著一個ArtMethod指針嘱能,ArtMethod結(jié)構(gòu)體中包含了java方法中的所有信息吝梅,包括方法入口、訪問權(quán)限惹骂、所屬的類和代碼執(zhí)行地址等苏携。

class ArtMethod {
 …………
 protect:
  HeapReference<Class> declaring_class_;
  HeapReference<ObjectArray<ArtMethod>> dex_cache_resolved_methods_;
  HeapReference<ObjectArray<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_;
    void* entry_point_from_jni_;
    void* entry_point_from_quick_compiled_code_;
 
#if defined(ART_USE_PORTABLE_COMPILER)
    void* entry_point_from_portable_compiled_code_;
#endif
  } ptr_sized_fields_;
  static GcRoot<Class> java_lang_reflect_ArtMethod_;
 
}
……

在ArtMethod結(jié)構(gòu)體中,比較重要的字段是dex_cache_resolved_methods_和entry_point_from_quick_compiled_code_对粪,他們是一個java方法的執(zhí)行入口右冻,當我們調(diào)用某個方法時,就會取得該方法的執(zhí)行入口著拭,通過執(zhí)行入口就可以跳過去執(zhí)行該方法纱扭,替換ArtMethod結(jié)構(gòu)體中的字段或者替換整個ArtMethod結(jié)構(gòu)體,就是底層替換方案的原理儡遮。AndFix采用的是替換ArtMethod結(jié)構(gòu)體中的字段乳蛾,這樣會有兼容性問題,因為廠商可能會修改ArtMethod的結(jié)構(gòu)體鄙币,導致替換失敗肃叶。Sophix采用的是替換整個ArtMethod結(jié)構(gòu)體,這樣就不會有兼容性問題十嘿。底層替換方案直接替換了方法因惭,可以立即生效,不需要重啟App绩衷。采用底層替換方案的主要以阿里系為主蹦魔,AndFix激率、Dexposed、阿里百川勿决、Sophix柱搜。

Instant Run方案

除了資源修復,代碼修復同樣可以借鑒Instant Run的原理剥险。Instant Run在第一次構(gòu)建apk時聪蘸,使用ASM在每個方法中注入一段代碼。ASM是一個java字節(jié)碼操控框架表制,它能夠動態(tài)生成類或者增強現(xiàn)有類的功能健爬。ASM可以直接生成class文件,也可以在類被加載到虛擬機之前么介,動態(tài)改變類的行為娜遵。

IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
      localIncrementalChange.access$dispatch(
              "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                      paramBundle });
      return;
  }

這里以某個Activity的onCreate方法為例,當我們執(zhí)行Instant Run時壤短,如果方法沒有變化设拟,$change則為null,不做任何處理久脯,方法正常執(zhí)行纳胧。如果方法有變化,就會生成替換類帘撰,這個類實現(xiàn)了IncrementalChange接口跑慕,同時也會生成一個AppPatchLoaderImpl類,這個類的getPatchClasses方法會返回被修改的類的列表摧找,根據(jù)這個列表核行,將上述代碼中的$change設置為生成的替換類,因此滿足了上述代碼中的判斷條件蹬耘,會執(zhí)行替換類的access$dispatch方法芝雪,在access$dispatch方法方法中會根據(jù)參數(shù)"onCreate.(Landroid/os/Bundle;)V",執(zhí)行替換類的onCreate方法综苔,從而實現(xiàn)了對onCreate方法的修改惩系,借鑒Instant Run代碼修復原理的熱修復框架有Robust和Aceso。

動態(tài)鏈接庫(.so)修復

熱修復框架的動態(tài)鏈接庫(.so)修復休里,主要是更新so文件蛆挫,換句話說就是重新加載so文件。因此so修復的基本原理就是重新加載so文件妙黍。加載so文件主要用到了System類中的load方法和loadLibrary方法

@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}

@CallerSensitive
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}

其中l(wèi)oad方法傳入的參數(shù)是so文件在磁盤上的完整路徑悴侵,用于加載指定路徑下的so文件。loadLibrary方法的參數(shù)是so庫的名稱拭嫁,用于加載apk安裝后可免,從apk包中復制到/data/data/packagename/lib下的so文件抓于,目前的so修復都是基于這兩個方法。
System類的load方法和loadLibrary方法內(nèi)部浇借,各自調(diào)用了Runtime類load0方法和loadLibrary0方法

synchronized void load0(Class<?> fromClass, String filename) {
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError(
                "Expecting an absolute path of the library: " + filename);
        }
        if (filename == null) {
            throw new NullPointerException("filename == null");
        }
        String error = doLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

synchronized void loadLibrary0(ClassLoader loader, String libname) {
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError(
    "Directory separator should not appear in library name: " + libname);
        }
        String libraryName = libname;
        if (loader != null) {
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                // It's not necessarily true that the ClassLoader used
                // System.mapLibraryName, but the default setup does, and it's
                // misleading to say we didn't find "libMyLibrary.so" when we
                // actually searched for "liblibMyLibrary.so.so".
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            String error = doLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }

        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
        String lastError = null;
        for (String directory : getLibPaths()) {
            String candidate = directory + filename;
            candidates.add(candidate);

            if (IoUtils.canOpenReadOnly(candidate)) {
                String error = doLoad(candidate, loader);
                if (error == null) {
                    return; // We successfully loaded the library. Job done.
                }
                lastError = error;
            }
        }

        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

從上述代碼可以看出Runtime類的load0方法和loadLibrary0方法的執(zhí)行捉撮,最終都會調(diào)用到doLoad方法。只是需要注意到妇垢,在loadLibrary0方法中巾遭,loader不為null時,doLoad方法的第一個參數(shù)filename闯估,是通過loader.findLibrary方法獲取的灼舍。findLibrary方法的實現(xiàn)在BaseDexClassLoader中。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }
}

在BaseDexClassLoader的findLibrary方法內(nèi)部調(diào)用了DexPathList的findLibrary方法涨薪。

public String findLibrary(String libraryName) {
    String fileName = System.mapLibraryName(libraryName);
    for (Element element : nativeLibraryPathElements) {
        String path = element.findNativeLibrary(fileName);
        if (path != null) {
            return path;
        }
    }
    return null;
}
final class DexPathList {
    static class Element {

        public String findNativeLibrary(String name) {
            maybeInit();
            if (isDirectory) {
                String path = new File(dir, name).getPath();
                if (IoUtils.canOpenReadOnly(path)) {
                    return path;
                }
            } else if (zipFile != null) {
                String entryName = new File(dir, name).getPath();
                if (isZipEntryExistsAndStored(zipFile, entryName)) {
                  return zip.getPath() + zipSeparator + entryName;
                }
            }
            return null;
        }
    }
}

mapLibraryName方法的功能是將xxx動態(tài)庫的名字轉(zhuǎn)換為libxxx.so骑素,比如前面?zhèn)鬟f過來的nickname為sdk_jni, 經(jīng)過該方法處理后返回的名字為libsdk_jni.so.
nativeLibraryPathElements表示所有的Native動態(tài)庫, 包括app目錄的native庫和系統(tǒng)目錄的native庫。其中的每一個Element對應著一個so庫文件刚夺。DexPathList的findLibrary方法就是用來通過so庫的名稱献丑,從nativeLibraryPathElements中遍歷每一個Element對象,findNativeLibrary方法去查找侠姑,如果存在就返回其路徑创橄。
so修復的其中一種方案就是就是將so的補丁文件對應的Element插入到nativeLibraryPathElements的第一個元素,這樣就能使補丁文件的路徑先被找到并返回结借。
接著看Runtime類的doLoad方法

private String doLoad(String name, ClassLoader loader) {
    String ldLibraryPath = null;
    String dexPath = null;
    if (loader == null) {
        ldLibraryPath = System.getProperty("java.library.path");
    } else if (loader instanceof BaseDexClassLoader) {
        BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
        ldLibraryPath = dexClassLoader.getLdLibraryPath();
    }
    synchronized (this) {
        return nativeLoad(name, loader, ldLibraryPath);
    }
}

Runtime類的doLoad方法最后會調(diào)用到調(diào)用到native層的nativeLoad函數(shù)筐摘,而nativeLoad函數(shù)又會調(diào)用到LoadNativeLibrary函數(shù)卒茬,so文件加載的主要邏輯就是在LoadNativeLibrary函數(shù)中船老。

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env, const std::string& path, jobject class_loader,
                                  std::string* error_msg) {
  error_msg->clear();

  SharedLibrary* library;
  Thread* self = Thread::Current();
  {
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path); //檢查該動態(tài)庫是否已加載
  }
  if (library != nullptr) {
    if (env->IsSameObject(library->GetClassLoader(), class_loader) == JNI_FALSE) {
      //不能加載同一個采用多個不同的ClassLoader
      return false;
    }
    ...
    return true;
  }

  const char* path_str = path.empty() ? nullptr : path.c_str();
  //通過dlopen打開動態(tài)共享庫.該庫不會立刻被卸載直到引用技術(shù)為空.
  void* handle = dlopen(path_str, RTLD_NOW);
  bool needs_native_bridge = false;
  if (handle == nullptr) {
    if (android::NativeBridgeIsSupported(path_str)) {
      handle = android::NativeBridgeLoadLibrary(path_str, RTLD_NOW);
      needs_native_bridge = true;
    }
  }

  if (handle == nullptr) {
    *error_msg = dlerror(); //打開失敗
    VLOG(jni) << "dlopen(\"" << path << "\", RTLD_NOW) failed: " << *error_msg;
    return false;
  }


  bool created_library = false;
  {
    std::unique_ptr<SharedLibrary> new_library(
        new SharedLibrary(env, self, path, handle, class_loader));
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);
    if (library == nullptr) {
      library = new_library.release();
      //創(chuàng)建共享庫,并添加到列表
      libraries_->Put(path, library);
      created_library = true;
    }
  }
  ...

  bool was_successful = false;
  void* sym;
  //查詢JNI_OnLoad符號所對應的方法
  if (needs_native_bridge) {
    library->SetNeedsNativeBridge();
    sym = library->FindSymbolWithNativeBridge("JNI_OnLoad", nullptr);
  } else {
    sym = dlsym(handle, "JNI_OnLoad");
  }

  if (sym == nullptr) {
    was_successful = true;
  } else {
    //需要先覆蓋當前ClassLoader.
    ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
    self->SetClassLoaderOverride(class_loader);

    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
    // 真正調(diào)用JNI_OnLoad()方法的過程
    int version = (*jni_on_load)(this, nullptr);

    if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
      fault_manager.EnsureArtActionInFrontOfSignalChain();
    }
    //執(zhí)行完成后, 需要恢復到原來的ClassLoader
    self->SetClassLoaderOverride(old_class_loader.get());
    ...
  }

  library->SetResult(was_successful);
  return was_successful;
}

簡述一下,LoadNativeLibrary函數(shù)主要做了三件事:

  1. 判斷so文件是否已經(jīng)被加載過圃酵,前后兩次加載的ClassLoader是否為同一個柳畔,避免重復加載。
  2. 打開so文件并得到句柄郭赐,如果句柄獲取失敗薪韩,則返回false。創(chuàng)建新的SharedLibrary捌锭,如果傳入的path對應的Library為空俘陷,就將新創(chuàng)建的SharedLibrary復制給Library,并將Library存儲到libraries中观谦。
  3. 查找JNI_OnLoad指針拉盾,根據(jù)不同情況設置was_successful的值,最終返回was_successful豁状。

熱修復框架的so修復主要有兩個方案:

  • 將so補丁插入到NativeLibraryElement數(shù)組的前部捉偏,讓so補丁的路徑先被返回和加載倒得。
  • 調(diào)用System類的load方法來接管so的加載入口。

本文參考:
《Android進階解密》第十三章
源碼簡析之ArtMethod結(jié)構(gòu)與涉及技術(shù)介紹
loadLibrary動態(tài)庫加載過程分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夭禽,一起剝皮案震驚了整個濱河市霞掺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌讹躯,老刑警劉巖菩彬,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異潮梯,居然都是意外死亡挤巡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門酷麦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矿卑,“玉大人,你說我怎么就攤上這事沃饶∧竿ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵糊肤,是天一觀的道長琴昆。 經(jīng)常有香客問我,道長馆揉,這世上最難降的妖魔是什么业舍? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮升酣,結(jié)果婚禮上舷暮,老公的妹妹穿的比我還像新娘。我一直安慰自己噩茄,他們只是感情好下面,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绩聘,像睡著了一般沥割。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上凿菩,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天机杜,我揣著相機與錄音,去河邊找鬼衅谷。 笑死椒拗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的会喝。 我是一名探鬼主播陡叠,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼玩郊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了枉阵?” 一聲冷哼從身側(cè)響起译红,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兴溜,沒想到半個月后侦厚,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡拙徽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年刨沦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片膘怕。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡想诅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出岛心,到底是詐尸還是另有隱情来破,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布忘古,位于F島的核電站徘禁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏髓堪。R本人自食惡果不足惜送朱,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望干旁。 院中可真熱鬧驶沼,春花似錦、人聲如沸疤孕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽祭阀。三九已至,卻和暖如春鲜戒,著一層夾襖步出監(jiān)牢的瞬間专控,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工遏餐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伦腐,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓失都,卻偏偏與公主長得像柏蘑,于是被迫代替她去往敵國和親幸冻。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345