熱修復知識預備

本篇學習基於《Android進階解密》第13章熱修復原理。接下來的會把熱修復、Hook、插件化的知識學習完。這章來學習熱修復的三種基本方案。

1.熱修復的出現

日常開發中,我們可能遇到下面的情況:

  • 線上版本出現了嚴重的bug,如果纔剛剛發版,或者下個版本已經規定了具體的計劃發佈,這個時候爲了解決bug,需要fix+測試+在各個應用市場重新發布,這會消耗大量的人力物力,代價比較大
  • 版本升級率不高,並且需要很長時間來完成版本覆蓋,此前版本的Bug就會一直影響不升級版本的用戶
  • 有一個小而重要的功能,需要短時間內完成版本覆蓋,比如節日活動

而熱修復框架就應運而生,它可以解決上述這些問題,對線上版本進行修復。

2.熱修復框架的種類和對比

熱修復框架的種類很多,按照公司團隊劃分主要有下面幾種:

類別 成員
阿里系 AndFix、Dexposed、阿里百川、Sophix
騰訊系 微信的Thinker、QQ空間的超級補丁、手機QQ的QFix
知名公司 美團Robust、餓了麼的Amigo、蘑菇街的Aceso
其他 RocooFix、Nuwa、AnoleFix…

雖然修復框架很多,但熱修復框架的核心技術主要有三類,分別是:

  1. 代碼修復
  2. 資源修復
  3. 動態鏈接庫修復

其中每個核心技術又有很多不同技術方案,另外這些熱修復框架仍然在不斷的更新迭代中,所以我們不是特別需要去了解所有框架的代碼,而是瞭解其本質的原理,和基本方案,這樣在實際使用其他熱修復框架時,我們需要搞清楚該框架能夠完成哪些功能, 結合我們的情況再去選擇框架就很方便了。

3.資源修復

很多熱修復框架都參考了Instant Run的資源修復原理。我們需要先了解Instant Run是怎麼實現資源修復的。

3.1 Instant Run概述

Instant Run是Android Studio2.0以後新增的一個運行機制,能夠明顯減少開發人員第二次及以後的構建和部署項目的時間,有熱重載內味了,在沒有使用 Instant Run前,我們編譯部署應用程序的流程如下圖所示:
在這裏插入圖片描述
從圖中可以看出傳統的編譯部署需要重新安裝和重啓App,這顯然會很耗時,Instant Run會避免這種情況:
在這裏插入圖片描述
從圖可以看出Instant Run的構建和部署都是基於更改的部分,Instant Run部署有三種方式,Instant Run會根據代碼的情況決定採用哪一種部署方式,無論哪種方式都不需要重新安裝App,這一點就提高了不少的效率。

  • Hot swap
    從名稱也可以看出Hot Swap是效率最高的部署方式,代碼的增量改變不需要重啓App,甚至不需要重啓當前的Activity。修改一個現有方法中的代碼時會採用Hot Swap
  • Warm Swap
    App不需要重啓,但是Activity需要重啓,修改或刪除一個現有的資源文件時會採用 Warm Swap
  • Cold Swap
    App需要重啓,但是不需要重新安裝,採用Cold Swap的情況很多,比如添加、刪除、修改一個字段和方法、添加一個類等。

3.2 Instant Run資源修復

我們需要了解Instant Run的資源修復原理,Instant Run並不是Android源碼,需要通過反編譯獲取。
Instant Run資源修復的核心邏輯在 MonkeyPatchermonkeyPatchExistingResources()


public static void monkeyPatchExistingResources(@Nullable Context context,
                                                    @Nullable String externalResourceFile,
                                                    @Nullable Collection<Activity> activities) {
 
    if (externalResourceFile == null) {
        return;
    }
 
    try {
        // 1 創建一新的 AssetManager
        AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
        // 2
        Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        mAddAssetPath.setAccessible(true);
        //3 通過反射調用 addAssetPath方法加載外部的資源(SD卡)
        if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }
 
        Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
        mEnsureStringBlocks.setAccessible(true);
        mEnsureStringBlocks.invoke(newAssetManager);
 
        if (activities != null) {
            for (Activity activity : activities) {
                // 4
                Resources resources = activity.getResources();
 
                try {
                    //5 反射得到Resources的AssetManager類型的 mAssets字段
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    //6 將mAssets字段的引用替換爲新創建的 AssetManager
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                   ...
                }
                    ... ...
                pruneResourceCaches(resources);
            }
        }
 
        /**
        *  根據SDK版本的不同,用不同的方式得到Resources的弱引用集合 
        */
        Collection<WeakReference<Resources>> references;
        if (SDK_INT >= KITKAT) {
            Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
            Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
            mGetInstance.setAccessible(true);
            Object resourcesManager = mGetInstance.invoke(null);
            try {
                Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
                fMActiveResources.setAccessible(true);
                @SuppressWarnings("unchecked")
                ArrayMap<?, WeakReference<Resources>> arrayMap =
                        (ArrayMap<?, WeakReference<Resources>>) fMActiveResources.get(resourcesManager);
                references = arrayMap.values();
            } catch (NoSuchFieldException ignore) {
                Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                mResourceReferences.setAccessible(true);
                references = (Collection<WeakReference<Resources>>) mResourceReferences.get(resourcesManager);
            }
        } else {
            Class<?> activityThread = Class.forName("android.app.ActivityThread");
            Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
            fMActiveResources.setAccessible(true);
            Object thread = getActivityThread(context, activityThread);
            @SuppressWarnings("unchecked")
            HashMap<?, WeakReference<Resources>> map =
                    (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
            references = map.values();
        }
         // 遍歷並得到弱引用集合中的Resources,將Resources的mAssets字段引用替換成新的AssetManager
        for (WeakReference<Resources> wr : references) {
            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());
            }
        }
    } ...

註釋1:創建一個新的 AssetManager
註釋2:、註釋3:通過反射調用 addAssetPath()加載外部(SD卡)的資源
註釋4:遍歷Activity列表,得到每個Activity的Resources
註釋5:通過反射得到Resources的 AssetManager類型的 mAssets字段
註釋6:改寫 mAssets字段的引用爲新的 AssetManager
註釋7:採用同樣的方式,將 Resources.Theme的 mAssets字段的引用替換爲新創建的 AssetManager
緊接着根據SDK版本的不同,用不同方式得到 Resources的弱引用集合,再遍歷這個弱引用集合,將弱引用集合中的 Resources的 mAssets字段引用替換成新創建的 AssetManager。

可以看出Instant Run中的資源熱修復可以簡單的總結爲兩點

  1. 創建新的AssetManager,通過反射調用 addAssetPath()加載外部的資源,這樣新創建的 AssetManager就含有了外部的資源
  2. 將AssetManager類型的mAssets字段引用全部替換爲新創建的 AssetManager

4.代碼修復

代碼修復主要有3個方案,分別是底層替換方案、類加載方案和Instant Run方案。

4.1 類加載方案

類加載方案基於 Dex分包方案。爲了理解Dex分包,我們將從65536限制LinearAlloc限制說起。

1.65536限制
隨着應用功能越來越複雜,代碼量不斷增大,引入的庫也越來越多,可能會在編譯時提示如下異常:

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

這說明應用中引用的方法數超過了最大數65536個,產生這一問題的原因就是系統的65536限制,65536限制的主要原因是 DVM Bytecode的限制,DVM指令集的方法調用指令invoke-kind 索引爲16bits,所以最多能引用65535個方法

2.LinearAlloc限制
在安裝應用時可能會提示 INSTALL_FAILED_DEXOPT,產生的原因就是 LinearAlloc限制,DVM中的LinearAlloc是一個固定的緩存區,當方法數超出了緩存區的大小時會報錯。

爲了解決上面兩種限制,從而產生了 Dex分包方案:
打包時將代碼分成多個Dex,將應用啓動時必須用到的類和這些類的直接引用類放到主Dex中,其他代碼放到次Dex中。
當應用啓動時先加載主Dex,等到應用啓動後再動態地加載次Dex,從而緩解了主Dex的65536限制和 LinearAlloc限制。

Dex分包方案主要有兩種,分別是:

  • Google官方方案
  • Dex自動拆包和動態加載方案

這裏就不再講解分包方案,接着來學習類加載方案,在之前學習了ClassLoader加載過程,其中一個環節就是 DexPathList.findClass():

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

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

Element內部封裝了DexFile,DexFile用於加載dex文件,因此每個dex文件對應一個Element。
多個Element組成了有序的Element數組 dexElements。當要查找類時,會在註釋1處遍歷dexElements,註釋2處調用 Element.findClass(),最終會調用Native方法查找,如果找到了就返回該類,如果找不到就接着在下一個Element中進行查找。

根據上面的流程,我們將有Bug的類 Key.class進行修改,再將 Key.class打包成含dex的補丁包Patch.jar,放在Element數組 dexElements的第一個元素,這樣首先會找到 Patch.dex中的Key.class去替換之前存在Bug的 Key.class,排在數組後面的 dex文件中存在Bug的Key.class根據ClassLoader雙親委託模式就不會被加載,這就是類加載的方案,如下圖所示:
在這裏插入圖片描述
類加載方案需要重啓App後讓ClassLoader重新加載新的類,爲什麼要重啓呢?這是因爲類是無法被卸載的。要向重新加載新的類就需要重啓App,因此採用類加載方案的熱修復框架時不能即時生效的

很多熱修復框架都採用了類加載的方案,但他們在細節上面都有不同。

4.2 底層替換方案

與類加載方案不同的是,底層替換方案不會再次加載新類,而是直接在Native層修改原有類,由於在原有類進行修改限制會比較多,且不能增減原有的方法和字段,如果我們增加了方法數,那麼方法索引數也會增加,這樣訪問方法時會無法通過索引找到正確的方法,同樣的字段也是類似的情況。
底層替換方案和反射的原理有些關聯,就拿方法替換來說,方法反射我們可以調用 java.lang.Class.getDeclaredMethod,假設我們要反射Key的show方法,會調用如下所示的代碼:

Key.class.getDeclaredMethod("show").invoke(Ket.class.newInstance());

Android8.0的invoke()方法:

// Method.java
    @FastNative
    public native Object invoke(Object obj, Object... args)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

它是一個native方法,對應JNI層的代碼爲:

// java_lang_reflect_Method.cc

static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
                             jobject javaArgs) {
  ScopedFastNativeObjectAccess soa(env);
  return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
}

調用了 InvokeMethod()

// relection.cc

jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
                     jobject javaReceiver, jobject javaArgs, size_t num_frames) {
  ...
  ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
  const bool accessible = executable->IsAccessible();
  ArtMethod* m = executable->GetArtMethod(); // 1
  ...
}

註釋1:獲取傳入了 javaMethod在ART虛擬機中對應一個 ArtMethod指針,ArtMethod結構體中包含了Java方法的所有信息,包括執行入口、訪問權限、所屬類和代碼執行地址等,ArtMethod結構如下所示:

// art_method.h
class ArtMethod FINAL {
...
 protected:
  GcRoot<mirror::Class> declaring_class_;
  std::atomic<std::uint32_t> access_flags_;
  uint32_t dex_code_item_offset_;
  uint32_t dex_method_index_;
  uint16_t method_index_;
  uint16_t hotness_count_;
  struct PtrSizedFields {
    ArtMethod** dex_cache_resolved_methods_;  //1
    void* data_;    
    void* entry_point_from_quick_compiled_code_; // 2
  } ptr_sized_fields_;
}

在ArtMethod中結構中比較重要的字段是上述代碼中註釋1和註釋2,他們表示的是方法的執行入口,當我們調用某一個方法時,就會取得這個方法(就比如前面的“show”)的執行入口。通過執行入口就可以跳過去執行show方法。
替換ArtMethod結構體中的字段或者替換掉整個ArtMethod結構體,這就是底層替換方案
AndFix採用的是替換ArtMethod結構體中的字段,這樣會有兼容問題,因爲廠商可能會修改ArtMethod結構體,導致方法替換失敗。Sophix採用的是替換整個ArtMethod結構體,這樣就不會存在兼容問題。底層替換方案直接替換了方法,可以理解生效而不需重啓。採用底層替換方案主要使 阿里係爲主,包括AndFix、Dexposed、阿里百川、Sophix。

4.3 Instant Run方案

除了資源的修復,代碼修復同樣可以借鑑Instant Run的原理,可以說Instant Run的出現推動了修復框架的發展。
Instant Run在第一次構建APK時,使用ASM在每一個方法中注入了類似如下的代碼:

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

註釋1處是一個成員變量 localIncrementalChange ,它的值爲 $change$change實現了 IncrementalChange 這個抽象接口。
當我們點擊 InstantRun時,如果方法沒有變化則 $change爲null,如果方法有變化,就生成替換類,這裏我們假設MainActivity的onCreate方法做了修改,就會生成替換類 MainActivity$override,這個類實現了IncrementalChange 接口,同時也會生成一個 AppPatchesLoaderImpl類,這個類的 getPatchedClasses()會返回被修改的類的列表,根據列表會將MainActivity的 $chang 設置爲 MainActivity$override,因此滿足了註釋2的條件,就會執行 它的access$dispatch()了
這個方法會根據參數: "onCreate.(Landroid/os/Bundle;)V"執行 MainActivity.override的onCreate(),從而實現了 onCreate方法的修改。借鑑Instant Run的原理的熱修復框架有 Robust和Aceso

上面有個概念,什麼是ASM?
ASM是一個Java字節碼操控框架,它能夠動態生成類或增強現有類的功能,ASM可以直接產生class文件,也可以在類被加載到虛擬機之前動態改變類的行爲。

5.動態鏈接庫修復

Android平臺的動態鏈接庫主要是指 so庫,熱修復框架的 so的修復主要是更新 so,換句話所就是重新加載so庫,因此so庫的修復的基礎原理就是加載so庫。

5.1 System的load和loadLibrary方法

加載so庫主要用到了 System類的load和 loadLibrary(),如下所示

public final class System {
...
    @CallerSensitive
    public static void load(String filename) {
        Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);  // 1
    }

    @CallerSensitive
    public static void loadLibrary(String libname) {
        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname); // 2
    }
}

System的load()傳入的參數是so庫在磁盤的完整路徑,用於加載路徑的so。System的 loadLibrary()傳入的參數時so的名稱,用於加載App安裝後自動從apk包中複製到 /data/data/packagename/lib 下的so庫。
目前so庫的修復都是基於這兩個方法,這裏分別對這兩個方法進行講解。

1. System的load()
註釋1處的 Runtime.getRuntime()會得到當前Java應用程序需的運行環境 Runtime,Runtime.load()如下:

// Runtime.java
    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");
        }
        // 1
        String error = doLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }

註釋1之前都是對路徑名進行查錯,然後調用 doLoad(),並將該類的類加載器作爲參數傳了進去!

// Runtime.java
    private String doLoad(String name, ClassLoader loader) {
        String librarySearchPath = null;
        if (loader != null && loader instanceof BaseDexClassLoader) {
            BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
            librarySearchPath = dexClassLoader.getLdLibraryPath();
        }
        synchronized (this) {
            return nativeLoad(name, loader, librarySearchPath);
        }
    }

doLoad() 會調用native方法nativeLoad(),關於這個方法後面會講到。

2. System的loadLibrary()
接着來查看System的loadLibrary(),它會調用 Runtime.loadLibrary0()

// Runtime.java
    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) {
            // 1
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
                                               System.mapLibraryName(libraryName) + "\"");
            }
            // 2
            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;
        // 3
        for (String directory : getLibPaths()) {
            // 4
            String candidate = directory + filename;
            candidates.add(candidate);

            if (IoUtils.canOpenReadOnly(candidate)) {
                // 5
                String error = doLoad(candidate, loader);
                if (error == null) {
                    return; 
                }
                lastError = error;
            }
        }
        if (lastError != null) {
            throw new UnsatisfiedLinkError(lastError);
        }
        throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
    }

loadLibrary0() 分成兩個部分,一個是傳入的 ClassLoader不爲null的部分,另一個是ClassLoader爲null的部分。
(1)我們先來看看 傳入的ClassLoader爲null的情況:
註釋3:遍歷 getLibPaths()這個方法,這個方法會返回 java.library.path選項配置的路徑數組。
註釋4:拼接一條so庫的路徑,當然這個路徑是暴力拼的,爲了驗證其是否是正確的,就把它丟到 doLoad()中。直到找到它。

(2)當ClassLoader不爲null時的情況:
註釋2:同樣的調用了 doLoad(),其中第一個參數時通過註釋1處的 ClassLoader.findLibrary()獲取到的路徑。
findLibrary() 在 ClassLoader的實現類 BaseDexClassLoader中實現:

// BaseDexClassLoader.java

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

調用了 DexPathList.findLibrary()

// DexPathList
    public String findLibrary(String libraryName) {
        String fileName = System.mapLibraryName(libraryName);
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            // 1
            String path = element.findNativeLibrary(fileName);
            if (path != null) {
                return path;
            }
        }
        return null;
    }

這個方法和 findClass()類似,在 NativeLibraryElement數組的每一個 NativeLibraryElement對應一個so庫,在註釋1處調用 NativeLibraryElement.findNativeLibrary()就可以返回so庫的路徑。
上面結合類加載方案,就可以得到so的修復的一種方案,就是將so補丁插入到 NativeLibraryElement數組的前面,讓so補丁的路徑先被返回,並調用Runtime的doLoad()進行加載,在doLoad中會調用 native方法 nativeLoad()

也就是說 load()loadLibrary()這兩個方法殊途同歸,最終都會調用native方法 nativeLoad(),那我們就深入到JNI去了解這個方法。

5.2 nativeLoad()分析

先來看看其JNI層中函數

// Runtime.c
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
                   jobject javaLoader, jstring javaLibrarySearchPath)
{
    return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}

在 Runtime_nativeLoad中調用了 JVM_NativeLoad()

// OpenjdkJvm.cc

JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
                                 jstring javaFilename,
                                 jobject javaLoader,
                                 jstring javaLibrarySearchPath) {
  // 將so的文件名稱轉換爲ScopedUtfChars類型
  ScopedUtfChars filename(env, javaFilename);
  if (filename.c_str() == NULL) {
    return NULL;
  }

  std::string error_msg;
  {
    // 獲取當前運行時的虛擬機
    art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
    // 虛擬機加載so庫
    bool success = vm->LoadNativeLibrary(env,
                                         filename.c_str(),
                                         javaLoader,
                                         javaLibrarySearchPath,
                                         &error_msg);
    if (success) {
      return nullptr;
    }
  }

  env->ExceptionClear();
  return env->NewStringUTF(error_msg.c_str());
}

上面的代碼是先獲取當前運行時的JVM指針,然後調用JVM的 LoadNativeLibrary()來加載so庫,也就是說 :
so庫是被JVM加載的,它的加載方法是 LoadNativeLibrary()
LoadNativeLibrary()的方法有點多,這裏分成3個part來講:

part.1 判斷是否加載過該so庫


bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                  const std::string& path,
                                  jobject class_loader,
                                  jstring library_path,
                                  std::string* error_msg) {
  error_msg->clear();
  SharedLibrary* library;
  Thread* self = Thread::Current();
  {
    MutexLock mu(self, *Locks::jni_libraries_lock_);
    // 1
    library = libraries_->Get(path);
  }
  ...
  // 2
  if (library != nullptr) {
    // 3
    if (library->GetClassLoaderAllocator() != class_loader_allocator) {
      StringAppendF(error_msg, "Shared library \"%s\" already opened by "
          "ClassLoader %p; can't open in ClassLoader %p",
          path.c_str(), library->GetClassLoader(), class_loader);
      LOG(WARNING) << error_msg;
      return false;
    }
    // 4          
    if (!library->CheckOnLoadResult()) {
      StringAppendF(error_msg, "JNI_OnLoad failed on a previous attempt "
          "to load \"%s\"", path.c_str());
      return false;
    }
    return true;
  }

註釋1:根據so名稱從 libraries_中獲取對應的 SharedLibrary類型指針 library,如果滿足註釋2處的條件就說明此前加載過該so。
在註釋3處如果此前加載用的ClassLoader和當前傳入的ClassLoader不相同的話,就會返回false
註釋4:判斷上次加載so的結果,如果有異常也會返回false,中斷so加載。如果滿足了註釋2、註釋3、註釋4的條件就會返回true,不再重複加載so。

part.2 獲取so庫對應的SharedLibrary指針

...

  Locks::mutator_lock_->AssertNotHeld(self);
  const char* path_str = path.empty() ? nullptr : path.c_str();
  bool needs_native_bridge = false;
  /**
  *  1 打開路徑 path_str的so庫,得到so句柄handle
  */
  void* handle = android::OpenNativeLibrary(env,
                                            runtime_->GetTargetSdkVersion(),
                                            path_str,
                                            class_loader,
                                            library_path,
                                            &needs_native_bridge,
                                            error_msg);

  VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_NOW) returned " << handle << "]";

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

  if (env->ExceptionCheck() == JNI_TRUE) {
    LOG(ERROR) << "Unexpected exception:";
    env->ExceptionDescribe();
    env->ExceptionClear();
  }
  bool created_library = false;
  {
    /**
    *  3 創建SharedLibrary
    */
    std::unique_ptr<SharedLibrary> new_library(
        new SharedLibrary(env,
                          self,
                          path,
                          handle,
                          needs_native_bridge,
                          class_loader,
                          class_loader_allocator));

    MutexLock mu(self, *Locks::jni_libraries_lock_);
    library = libraries_->Get(path);  // 4
    if (library == nullptr) {  // We won race to get libraries_lock.
      library = new_library.release();
      libraries_->Put(path, library);
      created_library = true;
    }
  }
  ...

註釋1:根據so的路徑path_str來打開該so庫,並返回得到的so句柄
註釋2:如果獲取so句柄失敗,就會返回fasle,中斷so加載
註釋3:創建SharedLibrary,並將so句柄作爲參數傳入進去
註釋4:獲取傳入path對應的library,如果library爲空指針,就將新創建的SahredLibrary賦值給library,並將library存儲到 libraries_中。

part.3 找到JNI_OnLoad註冊so庫方法

...
  bool was_successful = false;
  // 1
  void* sym = library->FindSymbol("JNI_OnLoad", nullptr);
  // 2
  if (sym == nullptr) {
    VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
    was_successful = true;
  } else {
    ScopedLocalRef<jobject> old_class_loader(env, env->NewLocalRef(self->GetClassLoaderOverride()));
    self->SetClassLoaderOverride(class_loader);

    VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
    // 3
    int version = (*jni_on_load)(this, nullptr);

    if (runtime_->GetTargetSdkVersion() != 0 && runtime_->GetTargetSdkVersion() <= 21) {
      EnsureFrontOfChain(SIGSEGV);
    }

    self->SetClassLoaderOverride(old_class_loader.get());

    if (version == JNI_ERR) {
      StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
    } else if (JavaVMExt::IsBadJniVersion(version)) {
      StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
                    path.c_str(), version);
    } else {
      // 4
      was_successful = true;
    }
    VLOG(jni) << "[Returned " << (was_successful ? "successfully" : "failure")
              << " from JNI_OnLoad in \"" << path << "\"]";
  }
  library->SetResult(was_successful);
  return was_successful;
}

註釋1:查找JNI_OnLoad函數指針並賦值給空指針sym,我們在JNI學習中知道 JNI_OnLoad()用於native方法的動態註冊
註釋2:如果沒有找到JNI_OnLoad函數也算加載成功,將 was_successful 置爲true,這是因爲並不是所有的so都定義了JNI_OnLoad函數,因爲native方法出去了動態註冊,還有靜態註冊。
如果找到了 JNI_OnLoad(),就在註釋3處執行 JNI_OnLoad()並將結果複製給version,如果version爲JNI_ERR或者 BadJniVersion,說明沒有執行成功,was_successful 爲fasle。
如果執行成功,則置 was_successful爲true,表明已經將so庫中的方法註冊成功了。

part.4 LoadNativeLibrary總結
so庫加載可以總結以下每步工作:

  1. 判斷傳入so庫是否加載庫,兩次的ClassLoader是否是同一個,避免重複加載
  2. 獲取so句柄,創建新的SharedLibrary,如果原來的library指針爲空,就把這個新的SharedLibrary賦值給它,並存儲到libraries中
  3. 查找 so庫中的 JNI_OnLoad的函數指針並執行它,根據不同的情況設置 was_successful的值,最終返回該 was_successful。

講到這裏,可以總結一下so修復主要有兩個方案:

  1. 將so補丁插入到 NativeLibraryElement數組的前部,讓so補丁的路徑先被返回和加載
  2. 調用System的load方法來接管so的加載入口。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章