Android 插件化原理解析

經過Hook AMS和攔截ActivityThread中H類對於組件調度能夠成功地繞過了AndroidMAnifest.xml的限制。java

可是啓動的『沒有在AndroidManifet.xml中顯式聲明』的Activity和宿主程序存在於同一個Apk中;一般狀況下,插件均以獨立的文件存在甚至經過網絡獲取,這時候插件中的Activity可否成功啓動呢?android

要啓動Activity組件確定先要建立對應的Activity類的對象,從上文 Activity生命週期管理 知道,建立Activity類對象的過程以下:git

1
2
3
4
5
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對象甚至都沒有辦法建立出來,談何啓動?github

所以,要使存在於獨立文件或者網絡中的插件被成功啓動,首先就須要解決這個插件類加載的問題。
下文將圍繞此問題展開,完成『啓動沒有在AndroidManifest.xml中顯示聲明,而且存在於外部插件中的Activity』的任務。api

閱讀本文以前,能夠先clone一份 understand-plugin-framework,參考此項目的classloader-hook 模塊。另外,插件框架原理解析系列文章見索引數組

ClassLoader機制

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

Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校檢、轉換解析和初始化的,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
與那些在編譯時進行鏈鏈接工做的語言不一樣,在Java語言裏面,類型的加載、鏈接和初始化都是在程序運行期間完成的,這種策略雖然會令類加載時稍微增長一些性能開銷,可是會爲Java應用程序提供高度的靈活性,Java裏天生能夠同代拓展的語言特性就是依賴運行期動態加載和動態連接這個特色實現的。例如,若是編寫一個面相接口的應用程序,能夠等到運行時在制定實際的實現類;用戶能夠經過Java與定義的和自定義的類加載器,讓一個本地的應用程序能夠在運行時從網絡或其餘地方加載一個二進制流做爲代碼的一部分,這種組裝應用程序的方式目前已經普遍應用於Java程序之中。從最基礎的Applet,JSP到複雜的OSGi技術,都使用了Java語言運行期類加載的特性。服務器

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

  1. 根據一個類的全限定名來獲取定義此類的二進制字節流
  2. 將這個字節流所表明的靜態存儲結構轉化爲JVM方法區中的運行時數據結構
  3. 在內存中生成一個表明這個類的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的建立過程的代碼:

1
2
3
4
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的來源:

1
2
3
4
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
        r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);

getPackageInfoNoCheck方法很簡單,直接調用了getPackageInfo方法:

1
2
3
4
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
        CompatibilityInfo compatInfo) {
    return getPackageInfo(ai, compatInfo, null, false, true, false);
}

在這個getPackageInfo方法裏面咱們發現了端倪:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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類中;老方法,咱們首先獲取這個對象:

1
2
3
4
5
6
7
8
9
10
// 先獲取到當前的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對象,以供接下來使用。

這個函數的簽名以下:

1
2
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就選擇了這種方式,相關類以下:

DroidPlugin的PackageParser

DroidPlugin的PackageParser

看到這裏我就問你怕不怕!!!這也是咱們以前提到的私有或者隱藏的API可使用,但必須處理好兼容性問題;若是Android 7.0發佈,這裏估計得添加一個新的類PackageParseApi24。

我這裏使用API 23做爲演示,版本不一樣的可能沒法運行請自行查閱 DroidPlugin 不一樣版本如何處理。

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

看起來有咱們須要的方法 generateApplication;確實如此,依靠這個方法咱們能夠成功地拿到ApplicationInfo。
因爲PackageParser是@hide的,所以咱們須要經過反射進行調用。咱們根據這個generateApplicationInfo方法的簽名:

1
2
public static ApplicationInfo generateApplicationInfo(Package p, int flags,
   PackageUserState state)

能夠寫出調用generateApplicationInfo的反射代碼:

1
2
3
4
5
6
7
8
9
10
11
12
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類型的實例,所以咱們調用這個方法便可;使用反射代碼以下:

1
2
3
4
5
6
7
8
9
// 首先, 咱們得建立出一個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的路徑):

1
2
3
4
5
6
7
8
9
// 第三個參數 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:

1
2
3
4
5
6
7
8
9
10
11
// 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中:

1
2
3
4
5
6
7
8
9
10
11
12
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,這裏從新建立一個類是爲了更有區分度;之後也能夠經過修改這個類實現對於類加載的控制:

1
2
3
4
5
6
public class CustomClassLoader extends DexClassLoader {

    public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }
}

到這裏,咱們已經成功地把把插件的信息放入ActivityThread中,這樣咱們插件中的類可以成功地被加載;所以插件中的Activity實例能被成功第建立;因爲整個流程較爲複雜,咱們簡單梳理一下:

  1. 在ActivityThread接收到IApplication的 scheduleLaunchActivity遠程調用以後,將消息轉發給H
  2. H類在handleMessage的時候,調用了getPackageInfoNoCheck方法來獲取待啓動的組件信息。在這個方法中會優先查找mPackages中的緩存信息,而咱們已經手動把插件信息添加進去;所以可以成功命中緩存,獲取到獨立存在的插件信息。
  3. H類而後調用handleLaunchActivity最終轉發到performLaunchActivity方法;這個方法使用從getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader來加載Activity類,進而使用反射建立Activity實例;接着建立Application,Context等完成Activity組件的啓動。

看起來好像已經完美無缺萬事大吉了;可是運行一下會出現一個異常,以下:

1
2
3
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方法建立,咱們查看這個方法,在源碼中發現了上文異常拋出的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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的方法;咱們查閱這個方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 有詳細解釋,這裏直接給出代碼,不贅述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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類的代碼:

1
2
3
4
5
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方法以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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究竟是個什麼東西??這個方法源碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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給出的源碼只能看出這麼多:

1
2
3
4
5
6
7
8
9
10
11
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方法以下:

1
2
3
4
5
6
7
8
9
10
11
12
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方法以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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在進行類加載,遍歷這個數組的時候,會自動遍歷到咱們添加進去的插件信息,從而完成插件類的加載!

接下來,咱們實現這個過程;咱們會用到一些較爲複雜的反射技術哦~不過代碼很是短:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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加載插件類』的任務;所以第二種方案也宣告完成!咱們簡要總結一下這種方式的原理:

  1. 默認狀況下performLacunchActivity會使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加載全部的類;咱們的思路是告訴宿主ClassLoader咱們在哪,讓其幫助完成類加載的過程。
  2. 宿主程序的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!因此目前我這個實現基本沒什麼暖用;固然我這裏只是就『代碼加載』進行舉例;至於資源,那牽扯到另一個問題——插件系統的資源管理機制這個在後續文章的合適機會我會單獨講解。

相關文章
相關標籤/搜索