【Android 修煉手冊】經常使用技術篇 -- Android 熱修復解析

這是【Android 修煉手冊】第 8 篇文章,若是尚未看過前面系列文章,歡迎點擊 這裏 查看~html

預備知識

  1. 瞭解 android 基本開發
  2. 瞭解 ClassLoader 相關知識

看完本文能夠達到什麼程度

  1. 瞭解熱修復常見的實現原理

閱讀前準備工做

  1. clone CommonTec 項目,其中 hotfix 和 patch 是熱修復代碼 示例代碼基於 AndFix,NuWa,Robust 進行了調整,抽取主要部分用來說解原理。

文章概覽

summary

1、熱修復和插件化

插件化和熱修復的原理,都是動態加載 dex/apk 中的類/資源,二者的目的不一樣。插件化目標在於加載 activity 等組件,達到動態下發組件的功能,熱修復目標在修復已有的問題。目標不一樣,也就致使其實現方式上的差異。因爲目標是動態加載組件,因此插件化重在解決組件的生命週期,以及資源的問題。而熱修復重在解決替換已有的有問題的類/方法/資源等。 關於插件化,能夠看前面分享的文章Android 插件化分析java

2、使用 gradle 簡化插件開發流程

若是看過Android 插件化分析裏的 gradle 簡化插件開發流程,這裏能夠略過~android

在學習和開發熱修復的時候,咱們須要動態去加載補丁 apk,因此開發過程當中通常須要有兩個 apk,一個是宿主 apk,一個是補丁 apk,對應的就須要有宿主項目和補丁項目。
CommonTec 這裏建立了 app 做爲宿主項目,plugin 爲插件項目。爲了方便,咱們直接把生成的插件 apk 放到宿主 apk 中的 assets 中,apk 啓動時直接放到內部存儲空間中方便加載。
這樣的項目結構,咱們調試問題時的流程就是下面這樣:
修改插件項目 -> 編譯生成插件 apk -> 拷貝插件 apk 到宿主 assets -> 修改宿主項目 -> 編譯生成宿主 apk -> 安裝宿主 apk -> 驗證問題
若是每次咱們修改一個很小的問題,都經歷這麼長的流程,那麼耐心很快就耗盡了。最好是能夠直接編譯宿主 apk 的時候自動打包插件 apk 並拷貝到宿主 assets 目錄下,這樣咱們無論修改什麼,都直接編譯宿主項目就行了。如何實現呢?還記得咱們以前講解過的 gradle 系列麼?如今就是學以至用的時候了。
首先在 plugin 項目的 build.gradle 添加下面的代碼:c++

project.afterEvaluate {
    project.tasks.each {
        if (it.name == "assembleDebug") {
            it.doLast {
                copy {
                    from new File(project.getBuildDir(), 'outputs/patch/debug/patch-debug.apk').absolutePath
                    into new File(project.getRootProject().getProjectDir(), 'hotfix/src/main/assets')
                    rename 'patch-debug.apk', 'patch.apk'
                }
            }
        }
    }
}
複製代碼

這段代碼是在 afterEvaluate 的時候,遍歷項目的 task,找到打包 task 也就是 assembleDebug,而後在打包以後,把生成的 apk 拷貝到宿主項目的 assets 目錄下,而且重命名爲 plugin.apk。git

而後在 app 項目的 build.gradle 添加下面的代碼:github

project.afterEvaluate {
    project.tasks.each {
        if (it.name == 'mergeDebugAssets') {
            it.dependsOn ':patch:assembleDebug'
        }
    }
}
複製代碼

找到宿主打包的 mergeDebugAssets 任務,依賴插件項目的打包,這樣每次編譯宿主項目的時候,會先編譯插件項目,而後拷貝插件 apk 到宿主 apk 的 assets 目錄下,之後每次修改,只要編譯宿主項目就能夠了。數組

3、ClassLoader

若是看過Android 插件化分析裏的 ClassLoader 分析,這裏能夠略過~緩存

ClassLoader 是熱修復和插件化中必需要掌握的,由於插件是未安裝的 apk,系統不會處理其中的類,因此須要咱們本身來處理。app

3.1 java 中的 ClassLoader

BootstrapClassLoader 負責加載 JVM 運行時的核心類,好比 JAVA_HOME/lib/rt.jar 等等框架

ExtensionClassLoader 負責加載 JVM 的擴展類,好比 JAVA_HOME/lib/ext 下面的 jar 包

AppClassLoader 負責加載 classpath 裏的 jar 包和目錄

3.2 android 中的 ClassLoader

在這裏,咱們統稱 dex 文件,包含 dex 的 apk 文件以及 jar 文件爲 dex 文件 PathClassLoader 用來加載系統類和應用程序類,用來加載 dex 文件,可是 dex2oat 生成的 odex 文件只能放在系統的默認目錄。

DexClassLoader 用來加載 dex 文件,能夠從存儲空間加載 dex 文件,能夠指定 odex 文件的存放目錄。

咱們在插件化中通常使用的是 DexClassLoader。

3.3 雙親委派機制

每個 ClassLoader 中都有一個 parent 對象,表明的是父類加載器,在加載一個類的時候,會先使用父類加載器去加載,若是在父類加載器中沒有找到,本身再進行加載,若是 parent 爲空,那麼就用系統類加載器來加載。經過這樣的機制能夠保證系統類都是由系統類加載器加載的。 下面是 ClassLoader 的 loadClass 方法的具體實現。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 先從父類加載器中進行加載
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 沒有找到,再本身加載
                    c = findClass(name);
                }
            }
            return c;
    }
複製代碼

3.4 如何加載插件中的類

要加載插件中的類,咱們首先要建立一個 DexClassLoader,先看下 DexClassLoader 的構造函數須要那些參數。

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        // ...
    }
}
複製代碼

構造函數須要四個參數:
dexPath 是須要加載的 dex / apk / jar 文件路徑
optimizedDirectory 是 dex 優化後存放的位置,在 ART 上,會執行 oat 對 dex 進行優化,生成機器碼,這裏就是存放優化後的 odex 文件的位置
librarySearchPath 是 native 依賴的位置
parent 就是父類加載器,默認會先從 parent 加載對應的類

建立出 DexClassLaoder 實例之後,只要調用其 loadClass(className) 方法就能夠加載插件中的類了。具體的實如今下面:

// 從 assets 中拿出插件 apk 放到內部存儲空間
    private fun extractPlugin() {
        var inputStream = assets.open("plugin.apk")
        File(filesDir.absolutePath, "plugin.apk").writeBytes(inputStream.readBytes())
    }

    private fun init() {
        extractPlugin()
        pluginPath = File(filesDir.absolutePath, "plugin.apk").absolutePath
        nativeLibDir = File(filesDir, "pluginlib").absolutePath
        dexOutPath = File(filesDir, "dexout").absolutePath
        // 生成 DexClassLoader 用來加載插件類
        pluginClassLoader = DexClassLoader(pluginPath, dexOutPath, nativeLibDir, this::class.java.classLoader) } 複製代碼

4、熱修復須要解決的難點

熱修復不一樣於插件化,不須要考慮各類組件的生命週期,惟一須要考慮的就是如何能將問題的方法/類/資源/so 替換爲補丁中的新方法/類/資源/so。
其中最重要的是方法和類的替換,因此有很多熱修復框架只作了方法和類的替換,而沒有對資源和 so 進行處理。

5、主流的熱修復框架對比

這裏選取幾個比較主流的熱修復框架進行對比

Qzone/Nuwa AndFix Robust Tinker Sophix
dex 修復 y y y y y
so 修復 n n n y y
資源修復 n n n y y
全平臺支持 y n y y y
即時生效 n y y n 同時支持
補丁包大小

上面是熱修復框架的一些對比,若是按照實現 dex 修復的原理來劃分的話,大概能分紅下面幾種:

native hook
Andfix
dex 插樁
Qzone
Nuwa
InstantRun Robust
Aceso
全量替換 dex
Tinker
混合方案
Sophix

下面對這幾種熱修復的方案進行詳細分析。

6、dex 熱修復方案

6.1 native hook 替換 ArtMethod 內容

6.1.1 原理

在解釋 native hook 原理以前,先介紹一下虛擬機的一些簡單實現。java 中的類,方法,變量,對應到虛擬機裏的實現是 ClassArtMethodArtField。以 Android N 爲例,簡單看一下這幾個類的一些結構。

class Class: public Object {
public:
    // ...
    // classloader 指針
	uint32_t class_loader_;
    // 數組的類型表示
	uint32_t component_type_;
    // 解析 dex 生成的緩存
	uint32_t dex_cache_;
    // interface table,保存了實現的接口方法
	uint32_t iftable_;
    // 類描述符,例如:java.lang.Class
	uint32_t name_;
    // 父類
	uint32_t super_class_;
    // virtual method table,虛方法表,指令 invoke-virtual 會用到,保存着父類方法以及子類複寫或者覆蓋的方法,是 java 多態的基礎
	uint32_t vtable_;
    // public private 
	uint32_t access_flags_;
    // 成員變量
	uint64_t ifields_;
    // 保存了全部方法,包括 static,final,virtual 方法
	uint64_t methods_;
    // 靜態變量
	uint64_t sfields_;
    // class 當前的狀態,加載,解析,初始化等等
	Status status_;
	static uint32_t java_lang_Class_;
};

class ArtField {
public:
	uint32_t declaring_class_;
	uint32_t access_flags_;
	uint32_t field_dex_idx_;
	uint32_t offset_;
};

class ArtMethod {
public:
	uint32_t declaring_class_;
	uint32_t access_flags_;
    // 方法字節碼的偏移
	uint32_t dex_code_item_offset_;
    // 方法在 dex 中的 index
	uint32_t dex_method_index_;
    // 在 vtable 或者 iftable 中的 index
	uint16_t method_index_;
    // 方法的調用入口
    struct PACKED(4) PtrSizedFields {
        ArtMethod** dex_cache_resolved_methods_;
        GcRoot<mirror::Class>* dex_cache_resolved_types_;
        void* entry_point_from_jni_;
        void* entry_point_from_quick_compiled_code_;
    } ptr_sized_fields_;
};
複製代碼

上面列出了三個結構的一部分變量,其實從這些變量能夠比較清楚的看到,Class 中的 iftable_,vtable_,methods_ 裏面保存了全部的類方法,sfields_,ifields_ 保存了全部的成員變量。而在 ArtMethod 中,ptr_sized_fields_ 變量指向了方法的調用入口,也就是執行字節碼的地方。在虛擬機內部,調用一個方法的時候,能夠簡單的理解爲會找到 ptr_sized_fields_ 指向的位置,跳轉過去執行對應的方法字節碼或者機器碼。簡圖以下:

class_method

這裏也順便說一下上面三個結構的內容是何時填充的,就是在 ClassLoader 加載類的時候。簡圖以下:

classloader

其實到這裏,咱們就簡單理解了虛擬機的內部實現,也就很容易想到 native hook 的原理了。既然每次調用方法的時候,都是經過 ArtMethod 找到方法,而後跳轉到其對應的字節碼/機器碼位置去執行,那麼咱們只要更改了跳轉的目標位置,那麼天然方法的實現也就被改變了。簡圖以下:

native_hook

因此 native hook 的本質就是把舊方法的 ArtMethod 內容替換成新方法的 ArtMethod 內容。 具體的實現代碼在這裏(只實現了 Android N 上的修復),下面看一些重點代碼。

6.1.2 實現代碼
  1. 首先要找到替換的舊方法和新方法,這一步在 java 中進行,直接經過反射獲取便可
// 建立補丁的 ClassLoader
pluginClassLoader = DexClassLoader(pluginPath, dexOutPath.absolutePath, nativeLibDir.absolutePath, this::class.java.classLoader) // 經過補丁 ClassLoader 加載新方法 val toMethod = pluginClassLoader.loadClass("com.zy.hotfix.native_hook.PatchNativeHookUtils").getMethod("getMsg")
// 反射獲取到須要修改的舊方法
val fromMethod = nativeHookUtils.javaClass.getMethod("getMsg")
複製代碼
  1. 以後調用 native 方法替換 ArtMethod 內容
nativeHookUtils.patch(fromMethod, toMethod)
複製代碼
Java_com_zy_hotfix_native_1hook_NativeHookUtils_patch(JNIEnv* env, jobject clazz, jobject src, jobject dest) {
    // 獲取到 java 方法對應的 ArtMethod
    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_)->clinit_thread_id_ =
            reinterpret_cast<art::mirror::Class*>(smeth->declaring_class_)->clinit_thread_id_;
    reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->status_ =
            static_cast<art::mirror::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;

    // 替換方法中的內容
    smeth->declaring_class_ = dmeth->declaring_class_;
    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->hotness_count_ = dmeth->hotness_count_;
    // 替換方法的入口
    smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
            dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
    smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
            dmeth->ptr_sized_fields_.dex_cache_resolved_types_;
    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_;
}
複製代碼

經過上述方法的替換,再次調用舊方法,就會跳轉到新方法的入口,天然也就執行新方法的邏輯了。

6.1.3 優缺點

優勢
補丁能夠實時生效
缺點

  1. 兼容性差,因爲 Android 系統每一個版本的實現都有差異,因此須要作不少的兼容。(這也就是爲何上面提供的 demo 代碼只能運行在 Android N 上,由於沒有對其餘版本作兼容)
  2. 開發須要掌握 jni 相關知識

6.2 dex 插樁

6.2.1 原理

dex 插樁的實現,是 Qzone 團隊提出來的,Nuwa 框架採用這種實現而且開源。
系統默認使用的是 PathClassLoader,繼承自 BaseDexClassLoader,在 BaseDexClassLoader 裏,有一個 DexPathList 變量,在 DexPathList 的實現裏,有一個 Element[] dexElements 變量,這裏面保存了全部的 dex。在加載 Class 的時候,就遍歷 dexElements 成員,依次查找 Class,找到之後就返回。

insert_dex

下面是重點代碼。

public class PathClassLoader extends BaseDexClassLoader {
}

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
}

final class DexPathList {
    // 保存了 dex 的列表
    private Element[] dexElements;

    public Class findClass(String name, List<Throwable> suppressed) {
        // 遍歷 dexElements
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                // 從 DexFile 中查找 Class
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        // ...
        return null;
    }
}
複製代碼

從上面 ClassLoader 的實現咱們能夠知道,查找 Class 的關鍵就是遍歷 dexElements,那麼天然就想到了把補丁 dex 插入到 dexElements 最前面,這樣遍歷 dexElements 就會優先從補丁 dex 中查找 Class 了。

insert_dex2

具體的實如今這裏,下面放一些重點代碼。

6.2.2 實現代碼
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        // 建立補丁 dex 的 classloader,目的是使用其中的補丁 dexElements
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
        // 獲取到舊的 classloader 的 pathlist.dexElements 變量
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
        // 獲取到補丁 classloader 的 pathlist.dexElements 變量
        Object newDexElements = getDexElements(getPathList(dexClassLoader));
        // 將補丁 的 dexElements 插入到舊的 classloader.pathlist.dexElements 前面
        Object allDexElements = combineArray(newDexElements, baseDexElements);
    }

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

    private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return Reflect.on(paramObject).get("dexElements");
    }

    private static Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        return Reflect.on(baseDexClassLoader).get("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;
    }
複製代碼
6.2.3 優缺點

優勢

  1. 實現簡單
  2. 不須要太多的適配

缺點

  1. 須要從新啓動補丁才能生效。由於在插樁以前加載的類是不會再從新加載的,因此須要從新啓動,讓已經加載過的 Class 從新加載才能應用到補丁
  2. class verify 問題。關於這個問題能夠看Qzone 的解釋,這裏就不詳細展開了
  3. Art 虛擬機上因爲 oat 致使的地址偏移問題,可能會須要在補丁包中打入補丁無關的類,致使補丁包體積增大

6.3 dex 替換

dex 替換的方案,主要是 tinker 在使用,這裏生成的補丁包不僅是須要修改的類,而是包含了整個 app 全部的類,在替換時原理和 dex 插樁相似,也是替換掉 dexElements 中的內容便可,這裏就不詳細說了。

6.4 InstantRun

6.4.1 原理

InstantRun 是 AndroidStudio 2.0 新增的功能,方便快速的增量編譯應用並部署,美團參照其原理實現了 Robust 熱修復框架。 其中的原理是,給每一個 Class 中新增一個 changeQuickRedirect 的靜態變量,並在每一個方法執行以前,對這個變量進行了判斷,若是這個變量被賦值了,就調用補丁類中的方法,若是沒有被賦值,仍是調用舊方法。 原理比較簡單,下面看看實現。具體實如今這裏

instant_run

6.4.2 實現代碼
public class InstantRunUtils {
    // 上文中說的 changeQuickRedirect 變量,改了一下名字
    public static PatchRedirect patchRedirect;

    // 須要補丁的方法
    public int getValue() {
        // 判斷 patchRedirect 是否爲空
        if (patchRedirect != null) {
            // 不爲空,說明方法須要打補丁,因爲一個類中有不少方法,因此這裏須要判斷此方法是否須要補丁
            if (patchRedirect.needPatch("getValue")) {
                // 須要補丁,就調用補丁中的方法
                return (String) patchRedirect.invokePatchMethod("getValue");
            }
        }
        return 100;
    }

    // 注入補丁
    public static void inject(ClassLoader classLoader) {
        try {
            // 獲取到補丁中的補丁信息
            Class patchInfoClass = classLoader.loadClass("com.zy.hotfix.instant_run.PatchInfo");
            patchInfoClass.getMethod("init").invoke(null);
            // patchMap 中存着 className -> PatchRedirect,即須要補丁的類描述符和對應的 PatchRedirect
            Map<String, Object> patchMap = (Map<String, Object>) patchInfoClass.getField("patchMap").get(null);
            for (String key: patchMap.keySet()) {
                PatchRedirect redirect = (PatchRedirect) patchMap.get(key);
                Class clazz = Class.forName(key);
                // 替換 class 中的 PatchRedirect
                clazz.getField("patchRedirect").set(null, redirect);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

而後咱們看看補丁中的 PatchRefirect 是怎麼實現的

public class InstantRunUtilsRedirect extends PatchRedirect {
    @Override
    public Object invokePatchMethod(String methodName, Object... params) {
        // 根據方法描述符調用對應的方法
        if (methodName.equals("getValue")) {
            return getValue();
        }
        return null;
    }

    @Override
    public boolean needPatch(String methodName) {
        // 判斷方法是否須要補丁
        if ("getValue".equals(methodName)) {
            return true;
        }
        return false;
    }

    // 補丁方法,返回正確的值
    public int getValue() {
        return 200;
    }
}
複製代碼
6.4.3 優缺點

優勢

  1. 使用 java 實現,開發方便
  2. 兼容性好
  3. 補丁實時生效

缺點

  1. 代碼是侵入比較高,須要在原有代碼中新增邏輯,並且須要對方法進行插樁,將這裏邏輯自動化處理
  2. 增大包體積

7、資源熱修復方案

關於資源的修復方案,沒有像代碼修復同樣方法繁多,基本上集中在對 AssetManager 的修改上。

7.1 替換 AssetManager

這個是 InstantRun 採用的方案,就是構造一個新的 AssetManager,反射調用其 addAssetPath 函數,把新的補丁資源包添加到 AssetManager 中,從而獲得含有完整補丁資源的 AssetManager,而後找到全部引用 AssetManager 的地方,經過反射將其替換爲新的 AssetManager。

7.2 添加修改的資源到 AssetManager 中,並從新初始化

這個是 Sophix 採用的方案,原理是構造一個 package id 爲 0x66 的資源包,只含有改變的資源,將其直接添加到原有的 AssetManager 中,這樣不會與原來的 package id 0x7f 衝突。而後將原來的 AssetManager 從新進行初始化便可,就不須要進行繁瑣的反射替換操做了。

8、so 熱修復方案

8.1 對加載過程進行封裝,替換 System.loadLibrary

在加載 so 庫的時候,系統提供了兩個接口

System.loadLibrary(String libName):用來加載已經安裝的 apk 中的 so
System.load(String pathName):能夠加載自定義路徑下的 so
複製代碼

經過上面兩個方法,咱們能夠想到,若是有補丁 so 下發,咱們就調用 System.load 去加載,若是沒有補丁 so 沒有下發,那麼仍是調用 System.loadLibrary 去加載系統目錄下的 so,原理比較簡單,可是咱們須要再上面進行一層封裝,並對調用 System.loadLibrary 的地方都進行替換。

8.2 反射注入補丁 so 路徑

還記得上面 dex 插樁的原理麼?在 DexPathList 中有 dexElements 變量,表明着全部 dex 文件,其實 DexPathList 中還有另外一個變量就是 Element[] nativeLibraryPathElements,表明的是 so 的路徑,在加載 so 的時候也會遍歷 nativeLibraryPathElements 進行加載,代碼以下:

public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        // 遍歷 nativeLibraryPathElements 
        for (Element element : nativeLibraryPathElements) {
            String path = element.findNativeLibrary(fileName);

            if (path != null) {
                return path;
            }
        }
        return null;
    }
複製代碼

看到這裏咱們就知道如何去作了吧,就像 dex 插樁同樣的方法,將 so 的路徑插入到 nativeLibraryPathElements 以前便可。

9、總結

summary

參考資料

www.cnblogs.com/popfisher/p…
tech.meituan.com/2016/09/14/…
深刻探索Android熱修復技術原理

關於我

about
相關文章
相關標籤/搜索