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

GoileoLee發表於2018-02-07

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

寫在開頭

本文主要是跟著官方文件以自己的理解,捋一遍 Amigo 的流程。
在 GitHub 上 Amigo 的 Wiki 中,How it works 分為三個大的步驟:

  • 檢查補丁包
  • 釋放 Apk
    • 釋放 Dex 到指定目錄
    • 拷貝 So 檔案到 Amigo 的指定目錄
    • 優化 Dex 檔案
  • 替換修復
    • 替換 ClassLoader
    • 替換 Dex
    • 替換動態連結庫
    • 替換資原始檔
    • 替換原有 Application
    • Amigo 外掛

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

怎麼實現的

通過學習原始碼發現,替換使用者的 Application 是 Amigo 的第一步,因為它在編譯的時候就完成了替換工作。

AmigoPlugin.groovy

在 buildSrc/src/main/groovy/me.ele.amigo/AmigoPlugin.groovy 指令碼檔案中完成了替換原有 Application 的工作。

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()

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

相關文章