Amigo 學習(二)類和資源是怎麼熱更的?

轉載請註明出處:https://juejin.im/post/5a712b696fb9a01cb74eacd6java

寫在開頭

本文主要是跟着官方文檔以本身的理解,捋一遍 Amigo 的流程。
在 GitHub 上 Amigo 的 Wiki 中,How it works 分爲三個大的步驟:node

  • 檢查補丁包
  • 釋放 Apk
    • 釋放 Dex 到指定目錄
    • 拷貝 So 文件到 Amigo 的指定目錄
    • 優化 Dex 文件
  • 替換修復
    • 替換 ClassLoader
    • 替換 Dex
    • 替換動態連接庫
    • 替換資源文件
    • 替換原有 Application
    • Amigo 插件

官方文檔講解的都是精華部分、核心部分。
而這裏咱們按照 Amigo 一次成功修復的流程來學習它。android

怎麼實現的

經過學習源碼發現,替換用戶的 Application 是 Amigo 的第一步,由於它在編譯的時候就完成了替換工做。bash

AmigoPlugin.groovy

在 buildSrc/src/main/groovy/me.ele.amigo/AmigoPlugin.groovy 腳本文件中完成了替換原有 Application 的工做。app

1. 編譯時替換 Application

me.ele.amigo.AmigoPlugin.groovy框架

manifestFile = output.processManifest.manifestOutputFile
//fake original application as an activity, so it will be in main dex
Node node = (new XmlParser()).parse(manifestFile)
Node appNode = null
for (Node n : node.children()) {
    if (n.name().equals("application")) {
    appNode = n;
    break
    }
}
QName nameAttr = new QName("http://schemas.android.com/apk/res/android", 'name', 'android');
applicationName = appNode.attribute(nameAttr)
if (applicationName == null || applicationName.isEmpty()) {
	applicationName = "android.app.Application"
}
// 將原來的 Application 替換成 Amigo
appNode.attributes().put(nameAttr, "me.ele.amigo.Amigo")
// new 一個 Node,將原來的 Application 設置爲 Activity,以保證其必定會在主 dex 中。
Node hackAppNode = new Node(appNode, "activity")
hackAppNode.attributes().put("android:name", applicationName)
manifestFile.bytes = XmlUtil.serialize(node).getBytes("UTF-8")
複製代碼

而Amigo 框架最核心的代碼都在 Amigo.java 中,咱們接下來看看 Amigo.java 中都作了哪些事情。佈局

2. 核心類 Amigo.java

核心方法 attachBaseContext() --> attachApplication()post

public void attachApplication() {
    try {
        String workingChecksum = PatchInfoUtil.getWorkingChecksum(this);
        Log.e(TAG, "#attachApplication: working checksum = " + workingChecksum);
        if (TextUtils.isEmpty(workingChecksum)
                || !PatchApks.getInstance(this).exists(workingChecksum)) {
            Log.d(TAG, "#attachApplication: Patch apk doesn't exists");
            PatchCleaner.clearPatchIfInMainProcess(this);
            attachOriginalApplication();
            return;
        }
        if (PatchChecker.checkUpgrade(this)) {
            Log.d(TAG, "#attachApplication: Host app has upgrade");
            PatchCleaner.clearPatchIfInMainProcess(this);
            attachOriginalApplication();
            return;
        }
        // ensure load dex process always run host apk not patch apk
        if (ProcessUtils.isLoadDexProcess(this)) {
            Log.e(TAG, "#attachApplication: load dex process");
            attachOriginalApplication();
            return;
        }
        if (!ProcessUtils.isMainProcess(this) && isPatchApkFirstRun(workingChecksum)) {
            Log.e(TAG,
                    "#attachApplication: None main process and patch apk is not released yet");
            attachOriginalApplication();
            return;
        }
        
        // only release loaded apk in the main process
        attachPatchApk(workingChecksum);
    } catch (LoadPatchApkException e) {
        e.printStackTrace();
        loadPatchError = LoadPatchError.record(LoadPatchError.LOAD_ERR, e);
        //if patch apk fails to run, Amigo will clear working dir with app's next startup clear(this); try { attachOriginalApplication(); } catch (Throwable e2) { throw new RuntimeException(e2); } } catch (Throwable e) { throw new RuntimeException(e); } } 複製代碼

主要是作一些判斷,判斷校驗和是否爲空;判斷補丁包是否須要更新;判斷當前是否運行在主線程中;判斷補丁包是否第一次運行;
當條件都知足時,執行 attachPatchApk(),加載補丁包。
不然,執行 attachOriginalApplication(),將 Application 類替換回到之前的類。(此時的 Application 類是 Amigo)。學習

這裏的檢驗和 workingChecksum 是什麼?
利用 CRC32 生成的一串 long 型的數值。
CRC32 —— CRC32會把字符串,生成一個long長整形的惟一性ID(雖然科學證實不絕對惟一,可是仍是可用的)。優化

attachPatchApk() 是重點

private void attachPatchApk(String checksum) throws LoadPatchApkException {
    try {
        if (isPatchApkFirstRun(checksum) || !AmigoDirs.getInstance(this).isOptedDexExists(checksum)) {
            PatchInfoUtil.updateDexFileOptStatus(this, checksum, false);
            releasePatchApk(checksum);
        } else {
            PatchChecker.checkDexAndSo(this, checksum);
        }
        setAPKClassLoader(AmigoClassLoader.newInstance(this, checksum));
        setApkResource(checksum);
        revertBitFlag |= getClassLoader() instanceof AmigoClassLoader ? 1 : 0;
        attachPatchedApplication(checksum);
        PatchCleaner.clearOldPatches(this, checksum);
        shouldHookAmAndPm = true;
        Log.i(TAG, "#attachPatchApk: success");
    } catch (Exception e) {
        throw new LoadPatchApkException(e);
    }
}
複製代碼

判斷是否第一次運行補丁包;判斷 dex 文件夾是否建立。
知足條件就存入狀態,並釋放補丁包,加載佈局和主題文件。 不然,檢查補丁包中 dex 和 so 文件的校驗和。
接下來是設置補丁包的 ClassLoader 和 Resource 對象及attachPatchedApplication()。

3. 類加載器 AmigoClassloader

private void setAPKClassLoader(ClassLoader classLoader) throws Exception {
    writeField(getLoadedApk(), "mClassLoader", classLoader);
}
複製代碼

這個方法裏面只有一行代碼

writeField() 是對反射的字段進行寫操做的封裝,第一個參數爲須要反射的類的對象,第二個參數爲須要反射的字段名,第三個參數爲寫入的值,即所賦的值。

  • 那麼,這裏是反射替換了什麼類的 classLoader 對象呢?

繼續看 getLoadedApk().

private static Object getLoadedApk() throws Exception {
    @SuppressWarnings("unchecked")
    Map<String, WeakReference<Object>> mPackages =
            (Map<String, WeakReference<Object>>) readField(instance(), "mPackages", true);
    for (String s : mPackages.keySet()) {
        WeakReference wr = mPackages.get(s);
        if (wr != null && wr.get() != null) {
            return wr.get();
        }
    }
    return null;
}
複製代碼

而後反射對象是 instance()

sActivityThread = MethodUtils.invokeStaticMethod(clazz(), "currentActivityThread");  
複製代碼

再是 clazz()

sClass = Class.forName("android.app.ActivityThread");
複製代碼

好了~ 可見 instance() 中調用了 ActivityThread 類的 currentActivityThread()。
接着 getLoadedApk() 中反射獲取了 mPackages 屬性的值。咱們看一下 mpackages 是什麼類型

final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<String, WeakReference<LoadedApk>>();
複製代碼

回過頭來,再看 getLoadedApk()
返回的是一個 Object 對象,但其實這個對象本質是 LoadedApk 類型。

LoadedApk 是什麼?看官方的註釋

Local state maintained about a currently loaded .apk.

本地狀態保持關於當前加載的 .apk 。
就是當前加載的 apk 文件的信息管理類。從源碼中的命名 packageInfo 也能看出來。

那最後再回到 setAPKClassLoader(ClassLoader classLoader),能夠看到是傳入了一個 classLoader,經過反射賦值到 .apk 文件的信息管理類 LoadedApk 中的類加載器對象,也就是加載這個 .apk 文件的 ClassLoader 類的對象。

  • 那傳入的這個 classLoader 對象是怎麼來的?
public class AmigoClassLoader extends DexClassLoader {

    ...
    
    public AmigoClassLoader(String patchApkPath, String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
        try {
            patchApk = new File(patchApkPath);
            zipFile = new ZipFile(patchApkPath);
        } catch (IOException e) {
            e.printStackTrace();
            zipFile = null;
        }
    }
    
    public static AmigoClassLoader newInstance(Context context, String checksum) {
        return new AmigoClassLoader(PatchApks.getInstance(context).patchPath(checksum),
                getDexPath(context, checksum),
                AmigoDirs.getInstance(context).dexOptDir(checksum).getAbsolutePath(),
                getLibraryPath(context, checksum),
                AmigoClassLoader.class.getClassLoader().getParent());
    }
    
    ...
複製代碼

AmigoClassLoader 繼承了 DexClassLoader,調用了 super() 傳入了

  1. 自定義的補丁 dex 地址;
  2. dex 解壓縮後存放的目錄;
  3. C/C++ 依賴的本地庫文件目錄;
  4. 上一級的類加載器;

小結:經過繼承 DexClassLoader 自定義的 ClassLoader,替換當前 ActivityThread 中的 Apk 包信息裏的類加載器,以實現加載補丁包的目的。

4. 補丁資源加載 PatchResourceLoader

private void setApkResource(String checksum) throws Exception {
    PatchResourceLoader.loadPatchResources(this, checksum);
    Log.i(TAG, "hook Resources success");
}
複製代碼

處理補丁包資源加載的類 PatchResourceLoader

static void loadPatchResources(Context context, String checksum) throws Exception {
    AssetManager newAssetManager = AssetManager.class.newInstance();
    invokeMethod(newAssetManager, "addAssetPath", PatchApks.getInstance(context).patchPath(checksum));
    invokeMethod(newAssetManager, "ensureStringBlocks");
    replaceAssetManager(context, newAssetManager);
}
複製代碼

loadPatchResources() 中先是實例化了一個 AssetManager 對象,又調用了三個方法。
第一個方法,經過反射調用 addAssetPath 添加 /sdcard 上補丁包的新資源。
第二個方法,經過源碼發現,是確保 mStringBlocks 對象不爲 null。

/*package*/ final void ensureStringBlocks() {
    if (mStringBlocks == null) {
        synchronized (this) {
            if (mStringBlocks == null) {
                makeStringBlocks(sSystem.mStringBlocks);
            }
        }
    }
}
複製代碼

那爲何要反射這個方法?兼容 Android 4.4。在網上找到了這樣的註釋,這句話的核心是,「do it」,大體意思是,「寫上它就是了」...

// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
複製代碼

第三個方法,獲得 Resources 的弱引用集合,把他們的 AssetManager 成員替換成 newAssetManager。代碼較多,就不貼出來了,自行去看 PatchResourceLoader.java 文件吧。

寫在後頭

本想一篇文章寫完核心類Amigo分析、類加載、資源加載、so 文件加載、四大組件修復實現原理及回到項目的 Application。但寫完前三個就感受篇幅有點長了,後面的東西又不能用三言兩語可以說清楚。那就到此分篇吧,下一篇再接着寫。

若是文中有沒有講明白的地方,或者是錯誤之處,煩請指出,筆者必定當即更正。

推薦閱讀:Amigo學習(一)解決使用中遇到的問題
Amigo 學習(二)類和資源是怎麼加載的?

記錄在此,僅爲學習!
感謝您的閱讀!歡迎指正!
歡迎加入 Android 技術交流羣,羣號:155495090

相關文章
相關標籤/搜索