Tinker源碼分析(五):加載so補丁流程

本系列 Tinker 源碼解析基于 Tinker v1.9.12

校驗so補丁流程

與加載資源補丁類似,加載so補丁也要先從校驗開始看起。

其實總體來說,Tinker 中加載 so 補丁文件的關鍵代碼就一句:

System.load(String filePath)

tryLoadPatchFilesInternal

final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);

if (isEnabledForNativeLib) {
    //tinker/patch.info/patch-641e634c/lib
    boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
    if (!libCheck) {
        //file not found, do not load patch
        Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
        return;
    }
}

checkComplete

從 assets/so_meta.txt 中讀取 so 補丁信息,每一條 so 補丁信息都會被封裝成一個 ShareBsDiffPatchInfo 對象,然后放入 libraryList 中。

String meta = securityCheck.getMetaContentMap().get(SO_MEAT_FILE);
//not found lib
if (meta == null) {
    return true;
}
ArrayList<ShareBsDiffPatchInfo> libraryList = new ArrayList<>();
ShareBsDiffPatchInfo.parseDiffPatchInfo(meta, libraryList);

if (libraryList.isEmpty()) {
    return true;
}

然后遍歷 libraryList ,去校驗里面的 ShareBsDiffPatchInfo 對象中 md5 和 name 值是否合法。合法的 ShareBsDiffPatchInfo 對象再放入 libs 中。

//tinker//patch-641e634c/lib
String libraryPath = directory + "/" + SO_PATH + "/";

HashMap<String, String> libs = new HashMap<>();

for (ShareBsDiffPatchInfo info : libraryList) {
    if (!ShareBsDiffPatchInfo.checkDiffPatchInfo(info)) {
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_PACKAGE_PATCH_CHECK, ShareConstants.ERROR_PACKAGE_CHECK_LIB_META_CORRUPTED);
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_PACKAGE_CHECK_FAIL);
        return false;
    }
    String middle = info.path + "/" + info.name;

    //unlike dex, keep the original structure
    libs.put(middle, info.md5);
}

接著會校驗 so 補丁文件夾是否存在

File libraryDir = new File(libraryPath);

if (!libraryDir.exists() || !libraryDir.isDirectory()) {
    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_LIB_DIRECTORY_NOT_EXIST);
    return false;
}

再校驗上面的從 so_meta.txt 中獲取到的 so 補丁文件路徑是否真的存在并且 so 文件是可讀的

//fast check whether there is any dex files missing
for (String relative : libs.keySet()) {
    File libFile = new File(libraryPath + relative);
    if (!SharePatchFileUtil.isLegalFile(libFile)) {
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_LIB_FILE_NOT_EXIST);
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISSING_LIB_PATH, libFile.getAbsolutePath());
        return false;
    }
}

都沒問題的話,就通過校驗

//if is ok, add to result intent
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_LIBS_PATH, libs);
return true;

加載so補丁流程

加載so補丁的入口:TinkerLoadLibrary.loadArmLibrary / TinkerLoadLibrary.loadArmV7Library ,區(qū)別就是前者用來加載 armeabi 平臺的,后者是用來加載 armeabi-v7a 平臺的。我們就來看 loadArmLibrary 方法吧。

loadArmLibrary

public static void loadArmLibrary(Context context, String libName) {
    // 校驗 libName 和 context 參數(shù)
    if (libName == null || libName.isEmpty() || context == null) {
        throw new TinkerRuntimeException("libName or context is null!");
    }

    // 這里需要保證已經(jīng)調(diào)用了 Tinker.install 方法 不然 Tinker 沒安裝的話會拋出異常
    Tinker tinker = Tinker.with(context);

    // 如果 tinker 支持 so 補丁,就加載外部的 so 文件
    if (tinker.isEnabledForNativeLib()) {
        if (TinkerLoadLibrary.loadLibraryFromTinker(context, "lib/armeabi", libName)) {
            return;
        }
    }
    // 如果 tinker 不支持 so 補丁,就調(diào)用系統(tǒng)的加載方法
    System.loadLibrary(libName);
}

loadLibraryFromTinker

loadLibraryFromTinker 一開始校驗了 libName 的名字是否是 lib 開頭、 .so 結(jié)尾的。

    final Tinker tinker = Tinker.with(context);

    libName = libName.startsWith("lib") ? libName : "lib" + libName;
    libName = libName.endsWith(".so") ? libName : libName + ".so";
    String relativeLibPath = relativePath + "/" + libName;

然后就是用 relativeLibPath 去和之前 so 校驗得到的 libs 去一一匹配。

如果匹配上了,就說明要加載的就是這個 so 文件,調(diào)用 System.load ,傳入文件路徑即可。

//TODO we should add cpu abi, and the real path later
// tinker 支持 so 補丁并且 tinker 完成加載補丁
if (tinker.isEnabledForNativeLib() && tinker.isTinkerLoaded()) {
    TinkerLoadResult loadResult = tinker.getTinkerLoadResultIfPresent();
    // 獲取上面校驗得到的 libs
    if (loadResult.libs != null) {
        for (String name : loadResult.libs.keySet()) {
            // 如果名字對上了,就說明要加載的就是這個外部的so補丁
            if (name.equals(relativeLibPath)) {
                String patchLibraryPath = loadResult.libraryDirectory + "/" + name;
                File library = new File(patchLibraryPath);
                // 確認 so 補丁文件存在
                if (library.exists()) {
                    //whether we check md5 when load
                    boolean verifyMd5 = tinker.isTinkerLoadVerify();
                    // 如果需要校驗md5值但是校驗失敗了,就回調(diào) onLoadFileMd5Mismatch 方法
                    if (verifyMd5 && !SharePatchFileUtil.verifyFileMd5(library, loadResult.libs.get(name))) {
                        tinker.getLoadReporter().onLoadFileMd5Mismatch(library, ShareConstants.TYPE_LIBRARY);
                    } else {
                        // 否則就調(diào)用 System.load 方法,傳入 so 補丁文件的路徑即可
                        System.load(patchLibraryPath);
                        TinkerLog.i(TAG, "loadLibraryFromTinker success:" + patchLibraryPath);
                        return true;
                    }
                }
            }
        }
    }
}

到這里,Tinker 中關于 so 補丁加載的流程就講完了。

番外

大家有沒有發(fā)現(xiàn),一個個單獨去調(diào)用 TinkerLoadLibrary.loadArmLibrary 會很麻煩,因為如果我的 so 補丁文件有很多個,就需要調(diào)用很多次。所以從 Tinker v1.7.7 之后,提供了一鍵反射的方案來加載 so 補丁文件。

具體方法 TinkerLoadLibrary.installNavitveLibraryABI

installNavitveLibraryABI

來看一下具體的代碼:

public static boolean installNavitveLibraryABI(Context context, String currentABI) {
    // 檢查 tinker 有沒有安裝
    Tinker tinker = Tinker.with(context);
    if (!tinker.isTinkerLoaded()) {
        TinkerLog.i(TAG, "tinker is not loaded, just return");
        return false;
    }
    // 檢查 tinker 加載的結(jié)果
    TinkerLoadResult loadResult = tinker.getTinkerLoadResultIfPresent();
    if (loadResult.libs == null) {
        TinkerLog.i(TAG, "tinker libs is null, just return");
        return false;
    }
    // 檢查當前 ABI 的 so 文件夾是否存在
    File soDir = new File(loadResult.libraryDirectory, "lib/" + currentABI);
    if (!soDir.exists()) {
        TinkerLog.e(TAG, "current libraryABI folder is not exist, path: %s", soDir.getPath());
        return false;
    }
    // 獲取 classloader
    ClassLoader classLoader = context.getClassLoader();
    if (classLoader == null) {
        TinkerLog.e(TAG, "classloader is null");
        return false;
    }
    TinkerLog.i(TAG, "before hack classloader:" + classLoader.toString());
    // 加載當前 ABI 的所有 so 補丁文件
    try {
        installNativeLibraryPath(classLoader, soDir);
        return true;
    } catch (Throwable throwable) {
        TinkerLog.e(TAG, "installNativeLibraryPath fail:" + throwable);
        return false;
    } finally {
        TinkerLog.i(TAG, "after hack classloader:" + classLoader.toString());
    }
}

在做了一堆的檢查之后,具體 so 文件加載是在 installNativeLibraryPath 方法中。

installNativeLibraryPath

installNativeLibraryPath 中做的事情主要有兩點:

  • 如果 classloader 中沒有注入 so 補丁文件夾的路徑的話,就執(zhí)行注入;
  • 如果 classloader 中已經(jīng)有 so 補丁文件夾的路徑了,就先刪除,再進行注入;

具體 hook 的代碼根據(jù) SDK 版本而定,這里就不展開講了。

private static void installNativeLibraryPath(ClassLoader classLoader, File folder)
    throws Throwable {
    if (folder == null || !folder.exists()) {
        TinkerLog.e(TAG, "installNativeLibraryPath, folder %s is illegal", folder);
        return;
    }
    // android o sdk_int 26
    // for android o preview sdk_int 25
    if ((Build.VERSION.SDK_INT == 25 && Build.VERSION.PREVIEW_SDK_INT != 0)
        || Build.VERSION.SDK_INT > 25) {
        try {
            V25.install(classLoader, folder);
        } catch (Throwable throwable) {
            // install fail, try to treat it as v23
            // some preview N version may go here
            TinkerLog.e(TAG, "installNativeLibraryPath, v25 fail, sdk: %d, error: %s, try to fallback to V23",
                    Build.VERSION.SDK_INT, throwable.getMessage());
            V23.install(classLoader, folder);
        }
    } else if (Build.VERSION.SDK_INT >= 23) {
        try {
            V23.install(classLoader, folder);
        } catch (Throwable throwable) {
            // install fail, try to treat it as v14
            TinkerLog.e(TAG, "installNativeLibraryPath, v23 fail, sdk: %d, error: %s, try to fallback to V14",
                Build.VERSION.SDK_INT, throwable.getMessage());

            V14.install(classLoader, folder);
        }
    } else if (Build.VERSION.SDK_INT >= 14) {
        V14.install(classLoader, folder);
    } else {
        V4.install(classLoader, folder);
    }
}

綜上所述,有了 TinkerLoadLibrary.installNavitveLibraryABI 之后,你就只需要傳入當前手機系統(tǒng)的 ABI ,就無需再對 so 的加載做任何的介入。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容