熱修復和外掛化

android大哥發表於2018-07-31

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包,多包)

  1. 一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個dex檔案排列成一個有序的陣列dexElements,當找類的時候,會按順序遍歷dex檔案,然後從當前遍歷的dex檔案中找類,如果找類則返回,如果找不到從下一個dex檔案繼續查詢

  2. 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 任玉剛想法

  1. 找到需要Hook方法的系統類

  2. 利用代理模式來代理系統類的執行攔截我們需要攔截的方法
    也就是代理acitivty 代理activity中去反射apk中activity的所有生命週期的方法,然後將activity的生命週期和代理activity的生命週期進行同步

  3. 使用反射的方法把這個系統類替換成你的代理類

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)

  1. 找到需要Hook方法的系統類
  2. DexClassLoader
  3. 提前佔坑(欺上瞞下) VirtualAPK庫中的清單檔案中有很多activity,1個service 一個廣播
  4. 廣播是 動態轉靜態

2.3 開始簡單擼程式碼地代表一下

  1. 寫一個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);
     	}
     }
    複製程式碼
  2. 寫一個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方法寫,改改我們自己的要改的程式碼

  1. 在application中 attachBaseContext 方法中調起hook ,attachBaseContext比onCreate更早

     try {
     		Hooker.hookInstrumentation();
     	} catch (Exception e) {
     		e.printStackTrace();
     	}
    複製程式碼

相關文章