1. 程式人生 > >Android外掛化原理解析--外掛化載入機制

Android外掛化原理解析--外掛化載入機制

上文 Activity生命週期管理 中我們地完成了『啟動沒有在AndroidManifest.xml中顯式宣告的Activity』的任務;通過Hook AMS和攔截ActivityThread中H類對於元件排程我們成功地繞過了AndroidMAnifest.xml的限制。


但是我們啟動的『沒有在AndroidManifet.xml中顯式宣告』的Activity和宿主程式存在於同一個Apk中;通常情況下,外掛均以獨立的檔案存在甚至通過網路獲取,這時候外掛中的Activity能否成功啟動呢?


要啟動Activity元件肯定先要建立對應的Activity類的物件,從上文 Activity生命週期管理 知道,建立Activity類物件的過程如下:


java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
        cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
也就是說,系統通過ClassLoader載入了需要的Activity類並通過反射呼叫建構函式創建出了Activity物件。如果Activity元件存在於獨立於宿主程式的檔案之中,系統的ClassLoader怎麼知道去哪裡載入呢?因此,如果不做額外的處理,外掛中的Activity物件甚至都沒有辦法創建出來,談何啟動?


因此,要使存在於獨立檔案或者網路中的外掛被成功啟動,首先就需要解決這個外掛類載入的問題。
下文將圍繞此問題展開,完成『啟動沒有在AndroidManifest.xml中顯示宣告,並且存在於外部外掛中的Activity』的任務。


閱讀本文之前,可以先clone一份 understand-plugin-framework,參考此專案的classloader-hook 模組。另外,外掛框架原理解析系列文章見索引。


ClassLoader機制


或許有的童鞋還不太瞭解Java的ClassLoader機制,我這裡簡要介紹一下。


Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校檢、轉換解析和初始化的,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。
與那些在編譯時進行鏈連線工作的語言不同,在Java語言裡面,型別的載入、連線和初始化都是在程式執行期間完成的,這種策略雖然會令類載入時稍微增加一些效能開銷,但是會為Java應用程式提供高度的靈活性,Java裡天生可以同代拓展的語言特性就是依賴執行期動態載入和動態連結這個特點實現的。例如,如果編寫一個面相介面的應用程式,可以等到執行時在制定實際的實現類;使用者可以通過Java與定義的和自定義的類載入器,讓一個本地的應用程式可以在執行時從網路或其他地方載入一個二進位制流作為程式碼的一部分,這種組裝應用程式的方式目前已經廣泛應用於Java程式之中。從最基礎的Applet,JSP到複雜的OSGi技術,都使用了Java語言執行期類載入的特性。


Java的類載入是一個相對複雜的過程;它包括載入、驗證、準備、解析和初始化五個階段;對於開發者來說,可控性最強的是載入階段;載入階段主要完成三件事:


根據一個類的全限定名來獲取定義此類的二進位制位元組流
將這個位元組流所代表的靜態儲存結構轉化為JVM方法區中的執行時資料結構
在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。
『通過一個類的全限定名獲取描述此類的二進位制位元組流』這個過程被抽象出來,就是Java的類載入器模組,也即JDK中ClassLoader API。


Android Framework提供了DexClassLoader這個類,簡化了『通過一個類的全限定名獲取描述次類的二進位制位元組流』這個過程;我們只需要告訴DexClassLoader一個dex檔案或者apk檔案的路徑就能完成類的載入。因此本文的內容用一句話就可以概括:


將外掛的dex或者apk檔案告訴『合適的』DexClassLoader,藉助它完成外掛類的載入


關於CLassLoader機制更多的內容,請參閱『深入理解Java虛擬機器』這本書。


思路分析


Android系統使用了ClassLoader機制來進行Activity等元件的載入;apk被安裝之後,APK檔案的程式碼以及資源會被系統存放在固定的目錄(比如/data/app/package_name/base-1.apk )系統在進行類載入的時候,會自動去這一個或者幾個特定的路徑來尋找這個類;但是系統並不知道存在於外掛中的Activity元件的資訊(外掛可以是任意位置,甚至是網路,系統無法提前預知),因此正常情況下系統無法載入我們外掛中的類;因此也沒有辦法建立Activity的物件,更不用談啟動元件了。


解決這個問題有兩個思路,要麼全盤接管這個類載入的過程;要麼告知系統我們使用的外掛存在於哪裡,讓系統幫忙載入;這兩種方式或多或少都需要干預這個類載入的過程。老規矩,知己知彼,百戰不殆。我們首先分析一下,系統是如果完成這個類載入過程的。


我們再次搬出Activity的建立過程的程式碼:


java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
這裡可以很明顯地看到,系統通過待啟動的Activity的類名className,然後使用ClassLoader物件cl把這個類載入進虛擬機器,最後使用反射建立了這個Activity類的例項物件。要想幹預這個ClassLoader(告知它我們的路徑或者替換他),我們首先得看看這玩意到底是個什麼來頭。(從哪裡建立的)


cl這個ClasssLoader物件通過r.packageInfo物件的getClassLoader()方法得到,r.packageInfo是一個LoadedApk類的物件;那麼,LoadedApk到底是個什麼東西??


我們查閱LoadedApk類的文件,只有一句話,不過說的很明白:


Local state maintained about a currently loaded .apk.


LoadedApk物件是APK檔案在記憶體中的表示。 Apk檔案的相關資訊,諸如Apk檔案的程式碼和資源,甚至程式碼裡面的Activity,Service等元件的資訊我們都可以通過此物件獲取。


OK, 我們知道這個LoadedApk是何方神聖了;接下來我們要搞清楚的是:這個 r.packageInfo 到底是從哪裡獲取的?


我們順著 performLaunchActivity上溯,輾轉handleLaunchActivity回到了 H 類的LAUNCH_ACTIVITY訊息,找到了r.packageInfo的來源:


final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
        r.activityInfo.applicationInfo, r.compatInfo);

handleLaunchActivity(r, null);

 getPackageInfoNoCheck方法很簡單,直接呼叫了getPackageInfo方法:

public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
        CompatibilityInfo compatInfo) {
    return getPackageInfo(ai, compatInfo, null, false, true, false);
}
在這個getPackageInfo方法裡面我們發現了端倪:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
        // 獲取userid資訊
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
    // 嘗試獲取快取資訊
        WeakReference<LoadedApk> ref;
        if (differentUser) {
            // Caching not supported across users
            ref = null;
        } else if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
                // 快取沒有命中,直接new
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

        // 省略。。更新快取
        return packageInfo;
    }
}
這個方法很重要,我們必須弄清楚每一步;

首先,它判斷了呼叫方和或許App資訊的一方是不是同一個userId;如果是同一個user,那麼可以共享快取資料(要麼快取的程式碼資料,要麼快取的資源資料)
接下來嘗試獲取快取資料;如果沒有命中快取資料,才通過LoadedApk的建構函式建立了LoadedApk物件;建立成功之後,如果是同一個uid還放入了快取。


提到快取資料,看過Hook機制之Binder Hook的童鞋可能就知道了,我們之前成功藉助ServiceManager的本地代理使用快取的機制Hook了各種Binder;因此這裡完全可以如法炮製——我們拿到這一份快取資料,修改裡面的ClassLoader;自己控制類載入的過程,這樣載入外掛中的Activity類的問題就解決了。這就引出了我們載入外掛類的第一種方案:


激進方案:Hook掉ClassLoader,自己操刀


從上述分析中我們得知,在獲取LoadedApk的過程中使用了一份快取資料;這個快取資料是一個Map,從包名到LoadedApk的一個對映。正常情況下,我們的外掛肯定不會存在於這個物件裡面;但是如果我們手動把我們外掛的資訊新增到裡面呢?系統在查詢快取的過程中,會直接命中快取!進而使用我們新增進去的LoadedApk的ClassLoader來載入這個特定的Activity類!這樣我們就能接管我們自己外掛類的載入過程了!


這個快取物件mPackages存在於ActivityThread類中;老方法,我們首先獲取這個物件:


// 先獲取到當前的ActivityThread物件
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);


// 獲取到 mPackages 這個靜態成員變數, 這裡快取了dex包的資訊
Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
Map mPackages = (Map) mPackagesField.get(currentActivityThread);
拿到這個Map之後接下來怎麼辦呢?我們需要填充這個map,把外掛的資訊塞進這個map裡面,以便系統在查詢的時候能命中快取。但是這個填充這個Map我們出了需要包名之外,還需要一個LoadedApk物件;如何建立一個LoadedApk物件呢?


我們當然可以直接反射呼叫它的建構函式直接創建出需要的物件,但是萬一哪裡有疏漏,構造引數填錯了怎麼辦?又或者Android的不同版本使用了不同的引數,導致我們創建出來的物件與系統創建出的物件不一致,無法work怎麼辦?


因此我們需要使用與系統完全相同的方式建立LoadedApk物件;從上文分析得知,系統建立LoadedApk物件是通過getPackageInfo來完成的,因此我們可以呼叫這個函式來建立LoadedApk物件;但是這個函式是private的,我們無法使用。


有的童鞋可能會有疑問了,private不是也能反射到嗎?我們確實能夠呼叫這個函式,但是private表明這個函式是內部實現,或許那一天Google高興,把這個函式改個名字我們就直接GG了;但是public函式不同,public被匯出的函式你無法保證是否有別人呼叫它,因此大部分情況下不會修改;我們最好呼叫public函式來保證儘可能少的遇到相容性問題。(當然,如果實在木有路可以考慮呼叫私有方法,自己處理相容性問題,這個我們以後也會遇到)

間接呼叫getPackageInfo這個私有函式的public函式有同名的getPackageInfo系列和getPackageInfoNoCheck;簡單檢視原始碼發現,getPackageInfo除了獲取包的資訊,還檢查了包的一些元件;為了繞過這些驗證,我們選擇使用getPackageInfoNoCheck獲取LoadedApk資訊。


構建外掛LoadedApk物件


我們這一步的目的很明確,通過getPackageInfoNoCheck函式創建出我們需要的LoadedApk物件,以供接下來使用。


這個函式的簽名如下:


public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
            CompatibilityInfo compatInfo) {
因此,為了呼叫這個函式,我們需要構造兩個引數。其一是ApplicationInfo,其二是CompatibilityInfo;第二個引數顧名思義,代表這個App的相容性資訊,比如targetSDK版本等等,這裡我們只需要提取出app的資訊,因此直接使用預設的相容性即可;在CompatibilityInfo類裡面有一個公有欄位DEFAULT_COMPATIBILITY_INFO代表預設相容性資訊;因此,我們的首要目標是獲取這個ApplicationInfo資訊。


構建外掛ApplicationInfo資訊


我們首先看看ApplicationInfo代表什麼,這個類的文件說的很清楚:


Information you can retrieve about a particular application. This corresponds to information collected from the AndroidManifest.xml’s <application> tag.


也就是說,這個類就是AndroidManifest.xml裡面的 這個標籤下面的資訊;這個AndroidManifest.xml無疑是一個標準的xml檔案,因此我們完全可以自己使用parse來解析這個資訊。


那麼,系統是如何獲取這個資訊的呢?其實Framework就有一個這樣的parser,也即PackageParser;理論上,我們也可以借用系統的parser來解析AndroidMAnifest.xml從而得到ApplicationInfo的資訊。但遺憾的是,這個類的相容性很差;Google幾乎在每一個Android版本都對這個類動刀子,如果堅持使用系統的解析方式,必須寫一系列相容行程式碼!!DroidPlugin就選擇了這種方式,相關類如下:


看到這裡我就問你怕不怕!!!這也是我們之前提到的私有或者隱藏的API可以使用,但必須處理好相容性問題;如果Android 7.0釋出,這裡估計得新增一個新的類PackageParseApi24。


我這裡使用API 23作為演示,版本不同的可能無法執行請自行查閱 DroidPlugin 不同版本如何處理。


OK回到正題,我們決定使用PackageParser類來提取ApplicationInfo資訊。下圖是API 23上,PackageParser的部分類結構圖:


 看起來有我們需要的方法 generateApplication;確實如此,依靠這個方法我們可以成功地拿到ApplicationInfo。
由於PackageParser是@hide的,因此我們需要通過反射進行呼叫。我們根據這個generateApplicationInfo方法的簽名:

public static ApplicationInfo generateApplicationInfo(Package p, int flags,
   PackageUserState state)
可以寫出呼叫generateApplicationInfo的反射程式碼:

Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
// 首先拿到我們得終極目標: generateApplicationInfo方法
// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// public static ApplicationInfo generateApplicationInfo(Package p, int flags,
//    PackageUserState state) {
// 其他Android版本不保證也是如此.
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
        packageParser$PackageClass,
        int.class,
                packageUserStateClass);
要成功呼叫這個方法,還需要三個引數;因此接下來我們需要一步一步構建呼叫此函式的引數資訊。

構建PackageParser.Package

generateApplicationInfo方法需要的第一個引數是PackageParser.Package;從名字上看這個類代表某個apk包的資訊,我們看看文件怎麼解釋:

Representation of a full package parsed from APK files on disk. A package consists of a single base APK, and zero or more split APKs.

果然,這個類代表從PackageParser中解析得到的某個apk包的資訊,是磁碟上apk檔案在記憶體中的資料結構表示;因此,要獲取這個類,肯定需要解析整個apk檔案。PackageParser中解析apk的核心方法是parsePackage,這個方法返回的就是一個Package型別的例項,因此我們呼叫這個方法即可;使用反射程式碼如下:

// 首先, 我們得創建出一個Package物件出來供這個方法呼叫
// 而這個需要得物件可以通過 android.content.pm.PackageParser#parsePackage 這個方法返回得 Package物件得欄位獲取得到
// 創建出一個PackageParser物件供使用
Object packageParser = packageParserClass.newInstance();
// 呼叫 PackageParser.parsePackage 解析apk的資訊
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

// 實際上是一個 android.content.pm.PackageParser.Package 物件
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);
這樣,我們就得到了generateApplicationInfo的第一個引數;第二個引數是解析包使用的flag,我們直接選擇解析全部資訊,也就是0;


構建PackageUserState


第三個引數是PackageUserState,代表不同使用者中包的資訊。由於Android是一個多工多使用者系統,因此不同的使用者同一個包可能有不同的狀態;這裡我們只需要獲取包的資訊,因此直接使用預設的即可;


至此,generateApplicaionInfo的引數我們已經全部構造完成,直接呼叫此方法即可得到我們需要的applicationInfo物件;在返回之前我們需要做一點小小的修改:使用系統系統的這個方法解析得到的ApplicationInfo物件中並沒有apk檔案本身的資訊,所以我們把解析的apk檔案的路徑設定一下(ClassLoader依賴dex檔案以及apk的路徑):


// 第三個引數 mDefaultPackageUserState 我們直接使用預設建構函式構造一個出來即可
Object defaultPackageUserState = packageUserStateClass.newInstance();


// 萬事具備!!!!!!!!!!!!!!
ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
        packageObj, 0, defaultPackageUserState);
String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath;
替換ClassLoader


獲取LoadedApk資訊


方才為了獲取ApplicationInfo我們費了好大一番精力;回顧一下我們的初衷:


我們最終的目的是呼叫getPackageInfoNoCheck得到LoadedApk的資訊,並替換其中的mClassLoader然後把把新增到ActivityThread的mPackages快取中;從而達到我們使用自己的ClassLoader載入外掛中的類的目的。


現在我們已經拿到了getPackageInfoNoCheck這個方法中至關重要的第一個引數applicationInfo;上文提到第二個引數CompatibilityInfo代表裝置相容性資訊,直接使用預設的值即可;因此,兩個引數都已經構造出來,我們可以呼叫getPackageInfoNoCheck獲取LoadedApk:


// android.content.res.CompatibilityInfo
Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);


Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
defaultCompatibilityInfoField.setAccessible(true);


Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);


Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);
我們成功地構造出了LoadedAPK, 接下來我們需要替換其中的ClassLoader,然後把它新增進ActivityThread的mPackages中:


String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(loadedApk, classLoader);


// 由於是弱引用, 因此我們必須在某個地方存一份, 不然容易被GC; 那麼就前功盡棄了.
sLoadedApk.put(applicationInfo.packageName, loadedApk);


WeakReference weakReference = new WeakReference(loadedApk);

mPackages.put(applicationInfo.packageName, weakReference

我們的這個CustomClassLoader非常簡單,直接繼承了DexClassLoader,什麼都沒有做;當然這裡可以直接使用DexClassLoader,這裡重新建立一個類是為了更有區分度;以後也可以通過修改這個類實現對於類載入的控制:


public class CustomClassLoader extends DexClassLoader {


    public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }
}
到這裡,我們已經成功地把把外掛的資訊放入ActivityThread中,這樣我們外掛中的類能夠成功地被載入;因此外掛中的Activity例項能被成功第建立;由於整個流程較為複雜,我們簡單梳理一下:


在ActivityThread接收到IApplication的 scheduleLaunchActivity遠端呼叫之後,將訊息轉發給H
H類在handleMessage的時候,呼叫了getPackageInfoNoCheck方法來獲取待啟動的元件資訊。在這個方法中會優先查詢mPackages中的快取資訊,而我們已經手動把外掛資訊新增進去;因此能夠成功命中快取,獲取到獨立存在的外掛資訊。
H類然後呼叫handleLaunchActivity最終轉發到performLaunchActivity方法;這個方法使用從getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader來載入Activity類,進而使用反射建立Activity例項;接著建立Application,Context等完成Activity元件的啟動。
看起來好像已經天衣無縫萬事大吉了;但是執行一下會出現一個異常,如下:


04-05 02:49:53.742  11759-11759/com.weishu.upf.hook_classloader E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.weishu.upf.hook_classloader, PID: 11759
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.weishu.upf.ams_pms_hook.app/com.weishu.upf.ams_pms_hook.app.MainActivity}: java.lang.RuntimeException: Unable to instantiate application android.app.Application: java.lang.IllegalStateException: Unable to get package info for com.weishu.upf.ams_pms_hook.app; is package not installed?
錯誤提示說是無法例項化 Application,而Application的建立也是在performLaunchActivity中進行的,這裡有些蹊蹺,我們仔細檢視一下。


繞過系統檢查


通過ActivityThread的performLaunchActivity方法可以得知,Application通過LoadedApk的makeApplication方法建立,我們檢視這個方法,在原始碼中發現了上文異常丟擲的位置:


try {
    java.lang.ClassLoader cl = getClassLoader();
    if (!mPackageName.equals("android")) {
        initializeJavaContextClassLoader();
    }
    ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
    app = mActivityThread.mInstrumentation.newApplication(
            cl, appClass, appContext);
    appContext.setOuterContext(app);
} catch (Exception e) {
    if (!mActivityThread.mInstrumentation.onException(app, e)) {
        throw new RuntimeException(
            "Unable to instantiate application " + appClass
            + ": " + e.toString(), e);
    }
}
木有辦法,我們只有一行一行地檢視到底是哪裡丟擲這個異常的了;所幸程式碼不多。(所以說,縮小異常範圍是一件多麼重要的事情!!!)


第一句 getClassLoader() 沒什麼可疑的,雖然方法很長,但是它木有丟擲任何異常(當然,它呼叫的程式碼可能丟擲異常,萬一找不到只能進一步深搜了;所以我覺得這裡應該使用受檢異常)。


然後我們看第二句,如果包名不是android開頭,那麼呼叫了一個叫做initializeJavaContextClassLoader的方法;我們查閱這個方法:


private void initializeJavaContextClassLoader() {
    IPackageManager pm = ActivityThread.getPackageManager();
    android.content.pm.PackageInfo pi;
    try {
        pi = pm.getPackageInfo(mPackageName, 0, UserHandle.myUserId());
    } catch (RemoteException e) {
        throw new IllegalStateException("Unable to get package info for "
                + mPackageName + "; is system dying?", e);
    }
    if (pi == null) {
        throw new IllegalStateException("Unable to get package info for "
                + mPackageName + "; is package not installed?");
    }


    boolean sharedUserIdSet = (pi.sharedUserId != null);
    boolean processNameNotDefault =
        (pi.applicationInfo != null &&
         !mPackageName.equals(pi.applicationInfo.processName));
    boolean sharable = (sharedUserIdSet || processNameNotDefault);
    ClassLoader contextClassLoader =
        (sharable)
        ? new WarningContextClassLoader()
        : mClassLoader;
    Thread.currentThread().setContextClassLoader(contextClassLoader);

}

 這裡,我們找出了這個異常的來源:原來這裡呼叫了getPackageInfo方法獲取包的資訊;而我們的外掛並沒有安裝在系統上,因此係統肯定認為外掛沒有安裝,這個方法肯定返回null。所以,我們還要欺騙一下PMS,讓系統覺得外掛已經安裝在系統上了;至於如何欺騙 PMS,Hook機制之AMS&PMS 有詳細解釋,這裡直接給出程式碼,不贅述了:

private static void hookPackageManager() throws Exception {

    // 這一步是因為 initializeJavaContextClassLoader 這個方法內部無意中檢查了這個包是否在系統安裝
    // 如果沒有安裝, 直接丟擲異常, 這裡需要臨時Hook掉 PMS, 繞過這個檢查.

    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

    // 獲取ActivityThread裡面原始的 sPackageManager
    Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
    sPackageManagerField.setAccessible(true);
    Object sPackageManager = sPackageManagerField.get(currentActivityThread);

    // 準備好代理物件, 用來替換原始的物件
    Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
    Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
            new Class<?>[] { iPackageManagerInterface },
            new IPackageManagerHookHandler(sPackageManager));

    // 1. 替換掉ActivityThread裡面的 sPackageManager 欄位
    sPackageManagerField.set(currentActivityThread, proxy);
}
OK到這裡,我們已經能夠成功地載入簡單的獨立的存在於外部檔案系統中的apk了。至此 關於 DroidPlugin 對於Activity生命週期的管理已經完全講解完畢了;這是一種極其複雜的Activity管理方案,我們僅僅寫一個用來理解的demo就Hook了相當多的東西,在Framework層來回牽扯;這其中的來龍去脈要完全把握清楚還請讀者親自翻閱原始碼。另外,我在此 對DroidPlugin 作者獻上我的膝蓋~這其中的玄妙讓人歎為觀止!

上文給出的方案中,我們全盤接管了外掛中類的載入過程,這是一種相對暴力的解決方案;能不能更溫柔一點呢?通俗來說,我們可以選擇改革,而不是革命——告訴系統ClassLoader一些必要資訊,讓它幫忙完成外掛類的載入。

保守方案:委託系統,讓系統幫忙載入

我們再次搬出ActivityThread中載入Activity類的程式碼:

java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
        cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
我們知道 這個r.packageInfo中的r是通過getPackageInfoNoCheck獲取到的;在『激進方案』中我們把外掛apk手動新增進快取,採用自己載入辦法解決;如果我們不干預這個過程,導致無法命中mPackages中的快取,會發生什麼?

查閱 getPackageInfo方法如下:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
        WeakReference<LoadedApk> ref;
        if (differentUser) {
            // Caching not supported across users
            ref = null;
        } else if (includeCode) {
            ref = mPackages.get(aInfo.packageName);
        } else {
            ref = mResourcePackages.get(aInfo.packageName);
        }

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

            // 略
    }
 可以看到,沒有命中快取的情況下,系統直接new了一個LoadedApk;注意這個建構函式的第二個引數aInfo,這是一個ApplicationInfo型別的物件。在『激進方案』中我們為了獲取獨立外掛的ApplicationInfo花了不少心思;那麼如果不做任何處理這裡傳入的這個aInfo引數是什麼?

追本溯源不難發現,這個aInfo是從我們的替身StubActivity中獲取的!而StubActivity存在於宿主程式中,所以,這個aInfo物件代表的實際上就是宿主程式的Application資訊!

我們知道,接下來會使用new出來的這個LoadedApk的getClassLoader()方法獲取到ClassLoader來對外掛的類進行載入;而獲取到的這個ClassLoader是宿主程式使用的ClassLoader,因此現在還無法載入外掛的類;那麼,我們能不能讓宿主的ClasLoader獲得載入外掛類的能力呢?;如果我們告訴宿主使用的ClassLoader外掛使用的類在哪裡,就能幫助他完成載入!

宿主的ClassLoader在哪裡,是唯一的嗎?

上面說到,我們可以通過告訴宿主程式的ClassLoader外掛使用的類,讓宿主的ClasLoader完成對於外掛類的載入;那麼問題來了,我們如何獲取到宿主的ClassLoader?宿主程式使用的ClasLoader預設情況下是全域性唯一的嗎?

答案是肯定的。

因為在FrameWork中宿主程式也是使用LoadedApk表示的,如同Activity啟動是載入Activity類一樣,宿主中的類也都是通過LoadedApk的getClassLoader()方法得到的ClassLoader載入的;由類載入機制的『雙親委派』特性,只要有一個應用程式類由某一個ClassLoader載入,那麼它引用到的別的類除非父載入器能載入,否則都是由這同一個載入器載入的(不遵循雙親委派模型的除外)。

表示宿主的LoadedApk在Application類中有一個成員變數mLoadedApk,而這個變數是從ContextImpl中獲取的;ContextImpl重寫了getClassLoader方法,因此我們在Context環境中直接getClassLoader()獲取到的就是宿主程式唯一的ClassLoader。

LoadedApk的ClassLoader到底是什麼?

現在我們確保了『使用宿主ClassLoader幫助載入外掛類』可行性;那麼我們應該如何完成這個過程呢?

知己知彼,百戰不殆。

不論是宿主程式還是外掛程式都是通過LoadedApk的getClassLoader()方法返回的ClassLoader進行類載入的,返回的這個ClassLoader到底是個什麼東西??這個方法原始碼如下:

public ClassLoader getClassLoader() {
    synchronized (this) {
        if (mClassLoader != null) {
            return mClassLoader;
        }

        if (mIncludeCode && !mPackageName.equals("android")) {
            // 略...
            mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
                    mBaseClassLoader);

            StrictMode.setThreadPolicy(oldPolicy);
        } else {
            if (mBaseClassLoader == null) {
                mClassLoader = ClassLoader.getSystemClassLoader();
            } else {
                mClassLoader = mBaseClassLoader;
            }
        }
        return mClassLoader;
    }
}
可以看到,非android開頭的包和android開頭的包分別使用了兩種不同的ClassLoader,我們只關心第一種;因此繼續跟蹤ApplicationLoaders類:


public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{


    ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();


    synchronized (mLoaders) {
        if (parent == null) {
            parent = baseParent;
        }


        if (parent == baseParent) {
            ClassLoader loader = mLoaders.get(zip);
            if (loader != null) {
                return loader;
            }


            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
            PathClassLoader pathClassloader =
                new PathClassLoader(zip, libPath, parent);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);


            mLoaders.put(zip, pathClassloader);
            return pathClassloader;
        }


        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
        PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        return pathClassloader;
    }
}
可以看到,應用程式使用的ClassLoader都是PathClassLoader類的例項。那麼,這個PathClassLoader是什麼呢?從Android SDK給出的原始碼只能看出這麼多:


public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }


    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

}

SDK沒有匯出這個類的原始碼,我們去androidxref上面看;發現其實這個類真的就這麼多內容;我們繼續檢視它的父類BaseDexClassLoader;ClassLoader嘛,我們檢視findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:


protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;

}

可以看到,查詢Class的任務通過pathList完成;這個pathList是一個DexPathList類的物件,它的findClass方法如下:


public Class findClass(String name, List<Throwable> suppressed) {
   for (Element element : dexElements) {
       DexFile dex = element.dexFile;


       if (dex != null) {
           Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
           if (clazz != null) {
               return clazz;
           }
       }
   }
   if (dexElementsSuppressedExceptions != null) {
       suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
   }
   return null;
}
這個DexPathList內部有一個叫做dexElements的陣列,然後findClass的時候會遍歷這個陣列來查詢Class;如果我們把外掛的資訊塞進這個數組裡面,那麼不就能夠完成類的載入過程嗎?!!


給預設ClassLoader打補丁


通過上述分析,我們知道,可以把外掛的相關資訊放入BaseDexClassLoader的表示dex檔案的數組裡面,這樣宿主程式的ClassLoader在進行類載入,遍歷這個陣列的時候,會自動遍歷到我們新增進去的外掛資訊,從而完成外掛類的載入!


接下來,我們實現這個過程;我們會用到一些較為複雜的反射技術哦~不過程式碼非常短:


public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
        throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    // 獲取 BaseDexClassLoader : pathList
    Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
    pathListField.setAccessible(true);
    Object pathListObj = pathListField.get(cl);


    // 獲取 PathList: Element[] dexElements
    Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");
    dexElementArray.setAccessible(true);
    Object[] dexElements = (Object[]) dexElementArray.get(pathListObj);


    // Element 型別
    Class<?> elementClass = dexElements.getClass().getComponentType();


    // 建立一個數組, 用來替換原始的陣列
    Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);


    // 構造外掛Element(File file, boolean isDirectory, File zip, DexFile dexFile) 這個建構函式
    Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
    Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));


    Object[] toAddElementArray = new Object[] { o };
    // 把原始的elements複製進去
    System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
    // 外掛的那個element複製進去
    System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);


    // 替換
    dexElementArray.set(pathListObj, newElements);


}

短短的二十幾行程式碼,我們就完成了『委託宿主ClassLoader載入外掛類』的任務;因此第二種方案也宣告完成!我們簡要總結一下這種方式的原理:


預設情況下performLacunchActivity會使用替身StubActivity的ApplicationInfo也就是宿主程式的CLassLoader載入所有的類;我們的思路是告訴宿主ClassLoader我們在哪,讓其幫助完成類載入的過程。
宿主程式的ClassLoader最終繼承自BaseDexClassLoader,BaseDexClassLoader通過DexPathList進行類的查詢過程;而這個查詢通過遍歷一個dexElements的陣列完成;我們通過把外掛dex新增進這個陣列就讓宿主ClasLoader獲取了載入外掛類的能力。
小結


本文中我們採用兩種方案成功完成了『啟動沒有在AndroidManifest.xml中顯示宣告,並且存在於外部外掛中的Activity』的任務。


『激進方案』中我們自定義了外掛的ClassLoader,並且繞開了Framework的檢測;利用ActivityThread對於LoadedApk的快取機制,我們把攜帶這個自定義的ClassLoader的外掛資訊新增進mPackages中,進而完成了類的載入過程。


『保守方案』中我們深入探究了系統使用ClassLoader findClass的過程,發現應用程式使用的非系統類都是通過同一個PathClassLoader載入的;而這個類的最終父類BaseDexClassLoader通過DexPathList完成類的查詢過程;我們hack了這個查詢過程,從而完成了外掛類的載入。


這兩種方案孰優孰劣呢?


很顯然,『激進方案』比較麻煩,從程式碼量和分析過程就可以看出來,這種機制異常複雜;而且在解析apk的時候我們使用的PackageParser的相容性非常差,我們不得不手動處理每一個版本的apk解析api;另外,它Hook的地方也有點多:不僅需要Hook AMS和H,還需要Hook ActivityThread的mPackages和PackageManager!


『保守方案』則簡單得多(雖然原理也不簡單),不僅程式碼很少,而且Hook的地方也不多;有一點正本清源的意思,從最最上層Hook住了整個類的載入過程。


但是,我們不能簡單地說『保守方案』比『激進方案』好。從根本上說,這兩種方案的差異在哪呢?


『激進方案』是多ClassLoader構架,每一個外掛都有一個自己的ClassLoader,因此類的隔離性非常好——如果不同的外掛使用了同一個庫的不同版本,它們相安無事!『保守方案』是單ClassLoader方案,外掛和宿主程式的類全部都通過宿主的ClasLoader載入,雖然程式碼簡單,但是魯棒性很差;一旦外掛之間甚至外掛與宿主之間使用的類庫有衝突,那麼直接GG。


多ClassLoader還有一個優點:可以真正完成程式碼的熱載入!如果外掛需要升級,直接重新建立一個自定的ClassLoader載入新的外掛,然後替換掉原來的版本即可(Java中,不同ClassLoader載入的同一個類被認為是不同的類);單ClassLoader的話實現非常麻煩,有可能需要重啟程序。


在J2EE領域中廣泛使用ClasLoader的地方均採用多ClassLoader架構,比如Tomcat伺服器,Java模組化事實標準的OSGi技術;所以,我們有足夠的理由認為選擇多ClassLoader架構在大多數情況下是明智之舉。


目前開源的外掛方案中,DroidPlugin採用的『激進方案』,Small採用的『保守方案』那麼,有沒有兩種優點兼顧的方案呢??


答案自然是有的。


DroidPlugin和Small的共同點是兩者都是非侵入式的外掛框架;什麼是『非侵入式』呢?打個比方,你啟動一個外掛Activity,直接使用startActivity即可,就跟開發普通的apk一樣,開發外掛和普通的程式對於開發者來說沒有什麼區別。


如果我們一定程度上放棄這種『侵入性』,那麼我們就能實現一個兩者優點兼而有之的外掛框架!這裡我先賣個關子~

OK,本文的內容就到這裡了;關於『外掛機制對於Activity的處理方式』也就此完結。要說明的是,在本文的『保守方案』其實只處理了程式碼的載入過程,它並不能載入有資源的apk!所以目前我這個實現基本沒什麼暖用;當然我這裡只是就『程式碼載入』進行舉例;至於資源,那牽扯到另外一個問題——外掛系統的資源管理機制這個在後續文章的合適機會我會單獨講解。


接下來的文章,會講述Android四大元件的另外三個Service,BroadCastReceiver,ContentProvider的處理方式。喜歡就點個贊吧~持續更新,請關注github專案 understand-plugin-framework和我的 部落格! 這文章我前前後後準備了快兩個星期,如果你看到了這裡,還請支援一下 :)