首先宣告,《Android外掛化開發指南》這本書所介紹的Android底層是基於Android6.0(API level 23)的,而本書介紹的各種外掛化解決方案,以及配套的70多個例子,在Android7.0(API level 24)手機上測試都是能正常工作的。
如果讀者您的手機是Android 26、27,甚至28(也就是Android P),那麼會有30個外掛化的例子不能正常工作,這是因為Android系統底層的原始碼改動導致的。
本篇文章,專門介紹Android O的改動對外掛化產生的影響,以及相應的外掛化解決方案。
(一)從ActivityManagerNative的重構談起
首先是ActivityManagerNative這個類的gDefault欄位,這個欄位在API 25以及之前的版本,定義如下:
public abstract class ActivityManagerNative extends Binder implements IActivityManager { private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() { protected IActivityManager create() { IBinder b = ServiceManager.getService("activity"); if (false) { Log.v("ActivityManager", "default service binder = " + b); } IActivityManager am = asInterface(b); if (false) { Log.v("ActivityManager", "default service = " + am); } return am; } }; }
所以,我們可以通過反射獲取ActivityManagerNative的gDefault欄位,執行它的create方法,得到IActivityManager介面型別的物件。
看到這個介面型別,我們眼前一亮,可以通過Proxy.newProxyInstance方法,hook掉這個IActivityManager物件,攔截它的startActivity方法,把要啟動的、沒有在Manifest中宣告的Activity,替換成佔坑StubActivity,程式碼如下所示:
public static void hookAMN() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException { //獲取AMN的gDefault單例gDefault,gDefault是final靜態的 Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault"); // gDefault是一個 android.util.Singleton<T>物件; 我們取出這個單例裡面的mInstance欄位 Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", gDefault, "mInstance"); // 建立一個這個物件的代理物件MockClass1, 然後替換這個欄位, 讓我們的代理物件幫忙幹活 Class<?> classB2Interface = Class.forName("android.app.IActivityManager"); Object proxy = Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class<?>[] { classB2Interface }, new MockClass1(mInstance)); //把gDefault的mInstance欄位,修改為proxy Class class1 = gDefault.getClass(); RefInvoke.setFieldObject("android.util.Singleton", gDefault, "mInstance", proxy); }
我們在書中的第5章詳細講解過上述這些程式碼。但不幸的是,這些程式碼在Android O(API level 26)以上的系統版本中就不能執行了,在執行到這句話的時候,gDefault的值為空:
Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");
這是因為Google在Android O中,把ActivityManagerNative中的這個gDefault欄位刪除了,轉移到了ActivityManager類中,但此時,這個欄位改名為IActivityManagerSingleton,所以在Android P中,要把這句話改為:
Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManager", "IActivityManagerSingleton");
但這又不相容於Android O以下的版本了,所以寫一個if-else條件語句,根據Android系統的版本,來做不同的處理,如下所示:
Object gDefault = null; if (android.os.Build.VERSION.SDK_INT <= 25) { //獲取AMN的gDefault單例gDefault,gDefault是靜態的 gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault"); } else { //獲取ActivityManager的單例IActivityManagerSingleton,他其實就是之前的gDefault gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManager", "IActivityManagerSingleton"); }
(二)Element和DexFile的興衰史
接下來我們把目光轉移到外掛類的載入。我們在書中介紹了3種載入方式:
1. 為每一個外掛建立一個ClassLoader,用外掛ClassLoader去載入外掛中的類。
2. 把所有外掛中的dex,都合併到宿主App的dex陣列中。
3. 把宿主App所使用的ClassLoader,替換成我們自己建立的ClassLoader,在這個新的ClassLoader中,有一個容器變數,承載所有外掛的ClassLoader,用來載入外掛中的類。
這其中,第2種方式的實現是最簡單的,也就是合併所有外掛的dex到一個陣列中,具體程式碼實現如下所示:
public final class BaseDexClassLoaderHookHelper { public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile) throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException { // 獲取 BaseDexClassLoader : pathList Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList"); // 獲取 PathList: Element[] dexElements Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements"); // Element 型別 Class<?> elementClass = dexElements.getClass().getComponentType(); // 建立一個陣列, 用來替換原始的陣列 Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1); // 構造外掛Element(File file, boolean isDirectory, File zip, DexFile dexFile) 這個建構函式 Class[] p1 = {File.class, boolean.class, File.class, DexFile.class}; Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)}; Object o = RefInvoke.createObject(elementClass, p1, v1); Object[] toAddElementArray = new Object[] { o }; // 把原始的elements複製進去 System.arraycopy(dexElements, 0, newElements, 0, dexElements.length); // 外掛的那個element複製進去 System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length); // 替換 RefInvoke.setFieldObject(pathListObj, "dexElements", newElements); } }
這個思路沒問題。注意其中的這麼幾句話:
Class[] p1 = {File.class, boolean.class, File.class, DexFile.class}; Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)}; Object o = RefInvoke.createObject(elementClass, p1, v1); Object[] toAddElementArray = new Object[] { o };
這幾句話中,通過反射執行了Element的帶有4個引數的建構函式,但不幸的是,在Android O以及之後的版本,這個帶有4個引數的建構函式就被廢棄了。
此外,在這個建構函式中使用到的DexFile這個類,也被廢棄了,對此Google給出的解釋是,只有Android系統可以使用DexFile,App層面不能使用它。
於是,我們不得不另闢蹊徑,通過執行DexPathList類的makeDexElements方法,來生成外掛中的dex:
List<File> legalFiles = new ArrayList<>(); legalFiles.add(apkFile); List<IOException> suppressedExceptions = new ArrayList<IOException>(); Class[] p1 = {List.class, File.class, List.class, ClassLoader.class}; Object[] v1 = {legalFiles, optDexFile, suppressedExceptions, cl}; Object[] toAddElementArray = (Object[]) RefInvoke.invokeStaticMethod("dalvik.system.DexPathList", "makeDexElements", p1, v1);
這段程式碼,在Android O之前的版本也是適用的。所以,我們找到了比DexFile更好用的makeDexElements方法,進行Hook。