Android 插件化原理解析——插件加载机制
上文?Activity生命周期管理?中我們地完成了『啟動沒有在AndroidManifest.xml中顯式聲明的Activity』的任務;通過Hook?AMS和攔截ActivityThread中H類對于組件調度我們成功地繞過了AndroidMAnifest.xml的限制。
但是我們啟動的『沒有在AndroidManifet.xml中顯式聲明』的Activity和宿主程序存在于同一個Apk中;通常情況下,插件均以獨立的文件存在甚至通過網絡獲取,這時候插件中的Activity能否成功啟動呢?
要啟動Activity組件肯定先要創建對應的Activity類的對象,從上文?Activity生命周期管理?知道,創建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); |
也就是說,系統通過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的類加載是一個相對復雜的過程;它包括加載、驗證、準備、解析和初始化五個階段;對于開發者來說,可控性最強的是加載階段;加載階段主要完成三件事:
『通過一個類的全限定名獲取描述此類的二進制字節流』這個過程被抽象出來,就是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
看到這里我就問你怕不怕!!!這也是我們之前提到的私有或者隱藏的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 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加載插件類』的任務;因此第二種方案也宣告完成!我們簡要總結一下這種方式的原理:
小結
本文中我們采用兩種方案成功完成了『啟動沒有在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!所以目前我這個實現基本沒什么暖用;當然我這里只是就『代碼加載』進行舉例;至于資源,那牽扯到另外一個問題——插件系統的資源管理機制這個在后續文章的合適機會我會單獨講解。
原文出處: http://weishu.me/2016/04/05/understand-plugin-framework-classloader/
總結
以上是生活随笔為你收集整理的Android 插件化原理解析——插件加载机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android 插件化原理解析——Act
- 下一篇: Android插件化原理解析——广播的管