1 . 熱修復
1.1 ClassLoader(雙親委派模機制)
某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,依次遞迴,如果父類載入器可以完成類載入任務,就成功返回;只有父類載入器無法完成此載入任務時,才自己去載入。保證了只載入一次
1.2 分類
- ClassLoader 頂層的 classloader
- BootClassLoader
由java程式碼實現而不是c++實現,是Android平臺上所有ClassLoader的最終parent,這個內部類是包內可見,所以我們沒法使用。 - BaseDexClassLoader
PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader,其主要邏輯都是在BaseDexClassLoader完成 - PathClassLoader
PathClassLoader是用來載入Android系統類和應用的類,並且不建議開發者使用 - DexClassLoader
支援載入APK、DEX和JAR,也可以從SD卡進行載入。 上面說dalvik不能直接識別jar,DexClassLoader卻可以載入jar檔案,這難道不矛盾嗎?其實在BaseDexClassLoader裡對".jar",".zip",".apk",".dex"字尾的檔案最後都會生成一個對應的dex檔案,所以最終處理的還是dex檔案,而URLClassLoader並沒有做類似的處理。 一般我們都是用這個DexClassLoader來作為動態載入的載入器
1.3 熱修復開始(分dex包,多包)
-
一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個dex檔案排列成一個有序的陣列dexElements,當找類的時候,會按順序遍歷dex檔案,然後從當前遍歷的dex檔案中找類,如果找類則返回,如果找不到從下一個dex檔案繼續查詢
-
BaseDexClassLoader 中有個 pathList 物件,pathList 中包含一個 DexFile 的集合 dexElements,而對於類載入呢, 就是遍歷這個集合,通過DexFile去尋找,(其實尋找類無非就是根據name全限定名來載入的),我們可以利用反射 看下BaseDexClassLoader原始碼
//所有的 private final DexPathList pathList; @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } #DexPathList public Class findClass(String name) { for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext); if (clazz != null) { return clazz; } } } return null; } 複製程式碼
1.4 寫程式碼(摘於Android熱補丁動態修復技術(二):實戰)
利用反射把我們們的dex或者是jar或者是apk裡面的 dexElements 插入到原有的之前,就是利用反射 把 外掛的dexElements+以前的dexElements 重新賦值給 pathList
private void inject(String path) {
try {
// 1 . 拿到現有的dexElements
// 通過反射獲取classes的dexElements
Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
// 拿到BaseDexClassLoader中的pathList屬性
Object pathList = getField(cl, "pathList", getClassLoader());
// 拿到BaseDexClassLoader中的pathList中dexElements集合 屬性
Object baseElements = getField(pathList.getClass(), "dexElements", pathList);
// 2 拿補丁包的 dexElements
// 獲取patch_dex的dexElements(需要先載入dex)
String dexopt = getDir("path").getAbsolutePath();
// 把path給 DexClassLoader 讓他生成 dexElements
DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
//拿到 補丁包中的 pathList 屬性
Object obj = getField(cl, "pathList", dexClassLoader);
//拿到 補丁包中的 dexElements 屬性
Object dexElements = getField(obj.getClass(), "dexElements", obj);
// 3. 合併
// 合併兩個Elements
Object combineElements = combineArray(dexElements, baseElements);
//4. 再重新複製給classLoader
// 將合併後的Element陣列重新賦值給app的classLoader
setField(pathList.getClass(), "dexElements", pathList, combineElements);
//======== 以下是測試是否成功注入 =================
Object object = getField(pathList.getClass(), "dexElements", pathList);
int length = Array.getLength(object);
//如果length == 2, 證明已經把我們們的放進去了
Log.e("BugFixApplication", "length = " + length);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
複製程式碼
1.5 呼叫
再 Application中的attachBaseContext中呼叫 attachBaseContext優先於onCreate public class MyApplication extends Application { private static MyApplication INSTANCE ;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
inject("外掛jar的目錄")
}
複製程式碼
1.6 其次還是不行
- 原因:
在apk安裝的時候,虛擬機器會將dex優化成odex後才拿去執行。在這個過程中會對所有class一個校驗。 校驗方式:假設A該類在它的static方法,private方法,建構函式,override方法中直接引用到B類。如果A類和B類在同一個dex中,那麼A類就會被打上CLASS_ISPREVERIFIED標記 被打上這個標記的類不能引用其他dex中的類,否則就會報圖中的錯誤 - 解決辦法(ASM和javaassist,在每一個構造方法中引入別的dex檔案中的類)
A類如果還引用了一個C類,而C類在其他dex中,那麼A類並不會被打上標記。換句話說,只要在static方法,構造方法,private方法,override方法中直接引用了其他dex中的類,那麼這個類就不會被打上CLASS_ISPREVERIFIED標記。
2 外掛化
2.1 在一個大的專案裡面,為了明確的分工,往往不同的團隊負責不同的外掛APP,這樣分工更加明確
2.2 技術各不同大致的意思
2.2.1 任玉剛想法
-
找到需要Hook方法的系統類
-
利用代理模式來代理系統類的執行攔截我們需要攔截的方法
也就是代理acitivty 代理activity中去反射apk中activity的所有生命週期的方法,然後將activity的生命週期和代理activity的生命週期進行同步 -
使用反射的方法把這個系統類替換成你的代理類
2.2.2 VirtualAPK
先看startActivity的原始碼,Instrumentation(ɪnstrəmenˈteɪʃn)的execStartActivity方法,然後再通過ActivityManagerProxy與AMS進行互動,之後ams通過binder技術ApplicationThread的scheduleLaunchActivity方法 ,其內部會呼叫mH類的sendMessage方法,傳遞的標識為H.LAUNCH_ACTIVITY,進入呼叫到ActivityThread的handleLaunchActivity方法->ActivityThread#handleLaunchActivity->mInstrumentation.newActivity(最後還是呼叫到我們們的Instrumentation中) 其實在newActivity中 就是 把傳過來的 intent中的acitivty ,或者說是class檔案, 從我們們外掛中 拿出來(DexClassLoader)
- 找到需要Hook方法的系統類
- DexClassLoader
- 提前佔坑(欺上瞞下) VirtualAPK庫中的清單檔案中有很多activity,1個service 一個廣播
- 廣播是 動態轉靜態
2.3 開始簡單擼程式碼地代表一下
-
寫一個hooker類 讓他去hook Instrumentation類 或者是 ActivityManagerProxy 下面hook的是 Instrumentation類
public class Hooker { private static final String TAG = "Hooker"; public static void hookInstrumentation() throws Exception { // 反射拿到ActivityThread類 Class<?> activityThread = Class.forName("android.app.ActivityThread"); //獲取ActivityThread 物件 有兩種方法 //拿的過程是首先拿到ActivityThread,由於ActivityThread可以通過靜態變數sCurrentActivityThread //或者靜態方法currentActivityThread()獲取,所以拿到其物件相當輕鬆 Method sCurrentActivityThread = activityThread.getDeclaredMethod("currentActivityThread"); // 下面這句話是 讓它可以通過反射拿到(有的屬性時private的話 是不允許拿到的) sCurrentActivityThread.setAccessible(true); //獲取ActivityThread 物件 Object activityThreadObject = sCurrentActivityThread.invoke(activityThread); //獲取 Instrumentation 物件 Field mInstrumentation = activityThread.getDeclaredField("mInstrumentation"); mInstrumentation.setAccessible(true); // 強轉成Instrumentation Instrumentation instrumentation = (Instrumentation) mInstrumentation.get(activityThreadObject); // 把自己的Instrumentation類給設定進去 HookInstrumentation hookInstrumentation = new HookInstrumentation(instrumentation); //將我們的 hookInstrumentation 設定進去 mInstrumentation.set(activityThreadObject, hookInstrumentation); } } 複製程式碼
-
寫一個HookInstrumentation 繼承 Instrumentation 這個只是簡單的把開啟com.nzy.myeventbus.MainActivity的activity 換成了com.nzy.myeventbus.ThreeActivity public class MyInstrumentation extends Instrumentation {
private MyInstrumentation base; public MyInstrumentation(Instrumentation base) { this.base = base; } @Override public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Log.e("TAG", "invoked MyInstrumentation#newActivity, " + "class name =" + className + ", intent = " + intent); if ("com.nzy.myeventbus.MainActivity".equals(className)) { className= "com.nzy.myeventbus.ThreeActivity"; } return super.newActivity(cl,className , intent); } } 複製程式碼
注意 : 這只是hook了newActivity方法 這個方法可以重寫,但是Instrumentation中像execStartActivity是隱藏的不能被重寫, 可以在MyInstrumentation 完全按照 Instrumentation 中 execStartActivity方法寫,改改我們自己的要改的程式碼
-
在application中 attachBaseContext 方法中調起hook ,attachBaseContext比onCreate更早
try { Hooker.hookInstrumentation(); } catch (Exception e) { e.printStackTrace(); } 複製程式碼