熱修復的產生原因
- 剛發布的版本出現了bug,需要修復bug、測試并打包在各大應用市場重新發布上架。這樣會耗費大量的人力和物力,代價比較大。
- 已經修復了此前版本的bug,如果下一個版本是一個大版本,那么兩個版本的發布時間間隔往往會較長,這樣如果要等到大版本發布才能修復此前的bug,則bug就會長期地影響用戶。
- 版本升級率不高,并且需要很長時間來完成新版本的覆蓋,此前版本的bug會一直影響還沒有升級到新版本的用戶。
- 有一些重要的小功能,需要短時間內完成版本覆蓋,比如節日活動。
以上情況下,就需要用到熱修復來解決問題。
熱修復框架的對比
- 阿里系——AndFix、阿里百川、Sophix、Dexposed
- 騰訊系——微信Tinker、QQ空間的超級補丁、手機QQ的Qfix
- 知名公司——美團的Robust、餓了么的Amigo、美麗說蘑菇街的Aceso
- 其他——Nuwa、RocooFix、AnoleFix
資源修復
很多熱修復框架的資源修復實現都參考了Instant Run的資源修復的原理。
Instant Run
Instant Run是Android Studio 2.0以后新增的一種運行機制,能夠顯著減少開發者第二次以及以后的App構建和部署時間。
可以看出Instant Run的構建部署都是基于更改的部分。Instant Run的部署有三種方式,Instant Run會根據修改代碼和資源的情況來決定用那種部署方式,無論那種方式,都不需要重新安裝Apk。
- Hot Swap——它是效率最高的部署方式,代碼的增量修改不需要重啟App,甚至不需要重啟當前Activity。典型場景:修改了一個現有方法中的代碼邏輯時采用Hot Swap。
- Warm Swap——App不需要重啟,但是當前的Activity需要重啟。典型場景:修改或者刪除了一個現有的資源文件時會采用Warm Swap。
- Cold Swap——需要重啟App,但不需要重新安裝Apk。采用Cold Swap的情況很多,比如添加、刪除或者修改了一個類的字段或方法,添加了一個類等。
更多Intant Run原理相關的介紹,有興趣的同學可以出門左轉:深度理解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 {
//創建一個新的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);
//通過反射調用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字段設置為新創建的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);
}
}
簡述其過程:創建一個新的AssetManager對象,通過反射調用它的addAssetPath方法,加載指定路徑下的資源。然后遍歷Activity列表,得到每個Activity的Resources對象,在通過反射得到Resources對象的mAsset字段(AssetManager類型),將mAsset字段設置為新創建的AssetManager對象。其后的代碼邏輯,也采用類似的方式,根據SDK的版本不同,用不同的方式,得到Resources的弱引用集合,再遍歷這個弱引用集合,將弱引用集合中的Resources的mAssets字段引用都替換成新創建的AssetManager對象。
可以看出,Instant Run的資源修復可以簡單地總結為兩個步驟:
- 創建新的AssetManager對象,通過反射調用addAssetPath方法加載外部資源,這樣新創建的AssetManager就含有了新的外部資源。
- 將AssetManager類型的字段mAsset全部設置為新創建的AssetManager對象。
代碼修復
代碼修復主要有三種方案,分別是底層替換方法,類加載方案和Instant Run方案
類加載方案
類加載方案基于Dex分包方案,要了解Dex分包方案,需要先了解下65535限制和LinearAlloc限制
65535限制
隨著應用程序的功能越來越復雜,代碼量不斷增大,引入的庫越來越多,程序在編譯時會提示65535限制:
com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
這個異常說明程序中引用的方法數量不能超過65536個,產生這一問題的原因是系統的65536限制,這個限制歸因于DVM bytecode的限制,DVM指令集的方法調用指令invoke-kind索引為16bits,最多能引用65535個方法。
LinearAlloc限制
在安裝Apk時,如果遇到提示INSTALL_FAILED_DEXPORT,產生的原因就是LinearAlloc限制,DVM的LinearAlloc是一個固定的緩存區,當方法數超過了緩存區的大小時,就會報錯。
為了解決65535限制和LinearAlloc限制,從而產生了Dex分包方案,Dex分包方案主要做的是在打包apk時,將程序代碼分成多個dex文件,App啟動時必須用到的類和這些類的直接引用類都放在主dex文件中,其他代碼放在次dex文件中。當App啟動時,先加載主dex文件,等到App啟動后,再按需動態加載次dex文件。從而避免了主dex文件的65535限制和LinearAlloc限制。
Dex分包方案主要有兩種:google官方方案和Dex自動拆包+動態加載方案。
在Android的ClassLoader加載過程中,有一個環節就是BaseDexClassLoader的findClass方法中,調用了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數組,調用Element的findClass方法,Element是DexPathList的靜態內部類
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內部封裝了DexFile,它用于加載dex文件,因此每一個dex文件對應著一個Element對象,多個Element對象組成了dexElements數組。當要查找某個類時,會先遍歷dexElements數組,調用Element的findClass方法來查找。如果Element(dex文件)中找到該類就返回,否則就接著在下一個Element(dex文件)中去查找。根據這樣的流程,我們先將有bug的類進行修改,再打包成包含dex文件的補丁包(如Patch.jar),將Patch.dex文件對應的Element對象放在dexElement數組的第一個元素,這樣在DexPathList.findClass方法的執行時,會先從Patch.dex中找到已修復bug的目標類,根據ClassLoader的雙親委派模式,排在后面的dex文件中存在bug的類就不會加載了,從而實現了替換之前存在bug的類。這就是類加載方案的原理。類加載方案需要重啟App讓ClassLoader重新加載新的類,為什么要重啟App呢?這是因為類加載之后無法被卸載,之前存在bug的類一直在ClassLoader的緩存中,要想重新加載新的類替換存在bug的類,就需要重啟App,讓ClassLoader重新加載,因此采用類加載方案的熱修復框架是不能即時生效的。雖然很多熱修復框架都采用了類加載方案,但具體的實現細節和步驟還是有一些區別的。比如QQ空間的超級補丁和Nuwa,是按照上面說的將補丁包對應的Element元素放在dexElements數組的第一個元素使其優先加載。微信Tinker將新舊apk做了更改區分,得到了patch.dex,再將patch.dex與原來apk中的class.dex合并,生成新的class.dex,然后運行時,通過反射將class.dex放在dexElement數組的第一個元素。餓了么的Amigo則是將補丁包中的每一個dex文件對應的Element取出來,組成新的Element數組,在運行時通過反射用新的Element數組替換原來的dexElement數組。
采用類加載方案的主要以騰訊系為主,包括微信的Tinker、QQ空間的超級補丁、手機QQ的Qfix、餓了么的Amigo和Nuwa等
底層替換方案
底層替換方案不會再次加載新類,而是直接在Native層修改原有的類,由于在原有類上進行修改限制比較多,并且不能增減原有類的方法和字段。如果我們增加了方法數,那么方法索引數也會增加,這樣訪問方法時會無法通過找到正確的方法,同樣的,字段也是類似的情況。底層替換方案和反射的原理有些關聯,通過Method.invoke()方法來調用一個方法,其中invoke方法是一個native方法,對應jni層的Method_invoke函數,Method_invoke函數內部又會調用invokeMethod函數,并將java方法對應的javaMethod對象作為參數傳進去,javaMethod在ART虛擬機中對應著一個ArtMethod指針,ArtMethod結構體中包含了java方法中的所有信息,包括方法入口、訪問權限、所屬的類和代碼執行地址等。
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結構體中,比較重要的字段是dex_cache_resolved_methods_和entry_point_from_quick_compiled_code_,他們是一個java方法的執行入口,當我們調用某個方法時,就會取得該方法的執行入口,通過執行入口就可以跳過去執行該方法,替換ArtMethod結構體中的字段或者替換整個ArtMethod結構體,就是底層替換方案的原理。AndFix采用的是替換ArtMethod結構體中的字段,這樣會有兼容性問題,因為廠商可能會修改ArtMethod的結構體,導致替換失敗。Sophix采用的是替換整個ArtMethod結構體,這樣就不會有兼容性問題。底層替換方案直接替換了方法,可以立即生效,不需要重啟App。采用底層替換方案的主要以阿里系為主,AndFix、Dexposed、阿里百川、Sophix。
Instant Run方案
除了資源修復,代碼修復同樣可以借鑒Instant Run的原理。Instant Run在第一次構建apk時,使用ASM在每個方法中注入一段代碼。ASM是一個java字節碼操控框架,它能夠動態生成類或者增強現有類的功能。ASM可以直接生成class文件,也可以在類被加載到虛擬機之前,動態改變類的行為。
IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
這里以某個Activity的onCreate方法為例,當我們執行Instant Run時,如果方法沒有變化,$change則為null,不做任何處理,方法正常執行。如果方法有變化,就會生成替換類,這個類實現了IncrementalChange接口,同時也會生成一個AppPatchLoaderImpl類,這個類的getPatchClasses方法會返回被修改的類的列表,根據這個列表,將上述代碼中的$change設置為生成的替換類,因此滿足了上述代碼中的判斷條件,會執行替換類的access$dispatch方法,在access$dispatch方法方法中會根據參數"onCreate.(Landroid/os/Bundle;)V",執行替換類的onCreate方法,從而實現了對onCreate方法的修改,借鑒Instant Run代碼修復原理的熱修復框架有Robust和Aceso。
動態鏈接庫(.so)修復
熱修復框架的動態鏈接庫(.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);
}
其中load方法傳入的參數是so文件在磁盤上的完整路徑,用于加載指定路徑下的so文件。loadLibrary方法的參數是so庫的名稱,用于加載apk安裝后,從apk包中復制到/data/data/packagename/lib下的so文件,目前的so修復都是基于這兩個方法。
System類的load方法和loadLibrary方法內部,各自調用了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方法的執行,最終都會調用到doLoad方法。只是需要注意到,在loadLibrary0方法中,loader不為null時,doLoad方法的第一個參數filename,是通過loader.findLibrary方法獲取的。findLibrary方法的實現在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方法內部調用了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動態庫的名字轉換為libxxx.so,比如前面傳遞過來的nickname為sdk_jni, 經過該方法處理后返回的名字為libsdk_jni.so.
nativeLibraryPathElements表示所有的Native動態庫, 包括app目錄的native庫和系統目錄的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方法最后會調用到調用到native層的nativeLoad函數,而nativeLoad函數又會調用到LoadNativeLibrary函數,so文件加載的主要邏輯就是在LoadNativeLibrary函數中。
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); //檢查該動態庫是否已加載
}
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打開動態共享庫.該庫不會立刻被卸載直到引用技術為空.
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();
//創建共享庫,并添加到列表
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);
// 真正調用JNI_OnLoad()方法的過程
int version = (*jni_on_load)(this, nullptr);
if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
fault_manager.EnsureArtActionInFrontOfSignalChain();
}
//執行完成后, 需要恢復到原來的ClassLoader
self->SetClassLoaderOverride(old_class_loader.get());
...
}
library->SetResult(was_successful);
return was_successful;
}
簡述一下,LoadNativeLibrary函數主要做了三件事:
- 判斷so文件是否已經被加載過,前后兩次加載的ClassLoader是否為同一個,避免重復加載。
- 打開so文件并得到句柄,如果句柄獲取失敗,則返回false。創建新的SharedLibrary,如果傳入的path對應的Library為空,就將新創建的SharedLibrary復制給Library,并將Library存儲到libraries中。
- 查找JNI_OnLoad指針,根據不同情況設置was_successful的值,最終返回was_successful。
熱修復框架的so修復主要有兩個方案:
- 將so補丁插入到NativeLibraryElement數組的前部,讓so補丁的路徑先被返回和加載。
- 調用System類的load方法來接管so的加載入口。
本文參考:
《Android進階解密》第十三章
源碼簡析之ArtMethod結構與涉及技術介紹
loadLibrary動態庫加載過程分析