轉載請註明出處:https://juejin.im/post/5a712b696fb9a01cb74eacd6
寫在開頭
本文主要是跟著官方文件以自己的理解,捋一遍 Amigo 的流程。
在 GitHub 上 Amigo 的 Wiki 中,How it works 分為三個大的步驟:
- 檢查補丁包
- 釋放 Apk
- 釋放 Dex 到指定目錄
- 拷貝 So 檔案到 Amigo 的指定目錄
- 優化 Dex 檔案
- 替換修復
- 替換 ClassLoader
- 替換 Dex
- 替換動態連結庫
- 替換資原始檔
- 替換原有 Application
- Amigo 外掛
官方文件講解的都是精華部分、核心部分。
而這裡我們按照 Amigo 一次成功修復的流程來學習它。
怎麼實現的
通過學習原始碼發現,替換使用者的 Application 是 Amigo 的第一步,因為它在編譯的時候就完成了替換工作。
在 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() 傳入了
- 自定義的補丁 dex 地址;
- dex 解壓縮後存放的目錄;
- C/C++ 依賴的本地庫檔案目錄;
- 上一級的類載入器;
小結:通過繼承 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