轉載請註明出處:https://juejin.im/post/5a712b696fb9a01cb74eacd6java
本文主要是跟着官方文檔以本身的理解,捋一遍 Amigo 的流程。
在 GitHub 上 Amigo 的 Wiki 中,How it works 分爲三個大的步驟:node
官方文檔講解的都是精華部分、核心部分。
而這裏咱們按照 Amigo 一次成功修復的流程來學習它。android
經過學習源碼發現,替換用戶的 Application 是 Amigo 的第一步,由於它在編譯的時候就完成了替換工做。bash
在 buildSrc/src/main/groovy/me.ele.amigo/AmigoPlugin.groovy 腳本文件中完成了替換原有 Application 的工做。app
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 中都作了哪些事情。佈局
核心方法 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()。
private void setAPKClassLoader(ClassLoader classLoader) throws Exception {
writeField(getLoadedApk(), "mClassLoader", classLoader);
}
複製代碼
這個方法裏面只有一行代碼
writeField() 是對反射的字段進行寫操做的封裝,第一個參數爲須要反射的類的對象,第二個參數爲須要反射的字段名,第三個參數爲寫入的值,即所賦的值。
繼續看 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 類的對象。
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() 傳入了
小結:經過繼承 DexClassLoader 自定義的 ClassLoader,替換當前 ActivityThread 中的 Apk 包信息裏的類加載器,以實現加載補丁包的目的。
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