標籤:catch work 來源 this 一點 生命週期 api field apk
------本文轉載自 Android外掛程式化原理解析——外掛程式載入機制
這一系列的文章實在是寫的好!
5 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資訊。
5.1 構建外掛程式LoadedApk對象
我們這一步的目的很明確,通過getPackageInfoNoCheck函數建立出我們需要的LoadedApk對象,以供接下來使用。
這個函數的簽名如下:
public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo) {
因此,為了調用這個函數,我們需要構造兩個參數。其一是ApplicationInfo,其二是CompatibilityInfo;
第二個參數顧名思義,代表這個App的相容性資訊,比如targetSDK版本等等,這裡我們只需要提取出app的資訊,
因此直接使用預設的相容性即可;在CompatibilityInfo類裡面有一個公有欄位
DEFAULT_COMPATIBILITY_INFO代表預設相容性資訊;因此,我們的首要目標是擷取這個ApplicationInfo資訊。
5.2 構建外掛程式ApplicationInfo資訊
我們首先看看ApplicationInfo代表什麼,這個類的文檔說的很清楚:
Information you can retrieve about aparticular application. This corresponds to information collected from theAndroidManifest.xml’s <application> tag.
也就是說,這個類就是AndroidManifest.xml裡面的這個標籤下面的資訊;
這個AndroidManifest.xml無疑是一個標準的xml檔案,因此我們完全可以自己使用parse來解析這個資訊。
那麼,系統是如何擷取這個資訊的呢?其實Framework就有一個這樣的parser,也即PackageParser;
理論上,我們也可以借用系統的parser來解析AndroidMAnifest.xml從而得到ApplicationInfo的資訊。
但遺憾的是,這個類的相容性很差;Google幾乎在每一個Android版本都對這個類動刀子,
如果堅持使用系統的解析方式,必須寫一系列相容行代碼!!DroidPlugin就選擇了這種方式。
我們決定使用PackageParser類來提取ApplicationInfo資訊, 看起來有我們需要的方法 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);
要成功調用這個方法,還需要三個參數;因此接下來我們需要一步一步構建調用此函數的參數資訊。
5.3 構建PackageParser.Package
generateApplicationInfo方法需要的第一個參數是PackageParser.Package;
從名字上看這個類代表某個apk包的資訊,我們看看文檔怎麼解釋:
Representation of a full package parsed fromAPK files on disk. A package consists of a single base APK, and zero or moresplit 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;
5.4 構建PackageUserState
第三個參數是PackageUserState,代表不同使用者中包的資訊。由於Android是一個多任務多使用者系統,
因此不同的使用者同一個包可能有不同的狀態;這裡我們只需要擷取包的資訊,因此直接使用預設的即可;
至此,generateApplicaionInfo的參數我們已經全部構造完成,直接調用此方法即可得到我們需要的applicationInfo對象;
在返回之前我們需要做一點小小的修改:使用系統系統的這個方法解析得到的ApplicationInfo對象
中並沒有apk檔案本身的資訊,所以我們把解析的apk檔案的路徑設定一下(ClassLoa der依賴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;
5.5 替換ClassLoader5.5.1 擷取LoadedApk資訊
方才為了擷取ApplicationInfo我們費了好大一番精力;回顧一下我們的初衷:
我們最終的目的是調用getPackageInfoNoCheck得到LoadedApk的資訊,
並替換其中的mClassLoader然後把把添加到ActivityThread的mPackages緩衝中;
從而達到我們使用自己的ClassLoader載入外掛程式中的類的目的。
現在我們已經拿到了getPackageInfoNoCheck這個方法中至關重要的第一個參數applicationInfo;
上文提到第二個參數CompatibilityInfo代表裝置相容性資訊,直接使用預設的值即可;
因此,兩個參數都已經構造出來,我們可以調用getPackageInfoNoCheck擷取LoadedApk:
// android.content.res.CompatibilityInfoClass<?> 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執行個體能被成功第建立;由於整個流程較為複雜,我們簡單梳理一下:
1,在ActivityThread接收到IApplication的scheduleLaunchActivity遠程調用之後,將訊息轉寄給H
2,H類在handleMessage的時候,調用了getPackageInfoNoCheck方法來擷取待啟動的組件資訊。
在這個方法中會優先尋找mPackages中的緩衝資訊,而我們已經手動把外掛程式資訊添加進去;
因此能夠成功命中緩衝,擷取到獨立存在的外掛程式資訊。
3,H類然後調用handleLaunchActivity最終轉寄到performLaunchActivity方法;
這個方法使用從getPackageInfoNoCheck中拿到LoadedApk中的mClassLoader來載入Activity類,
進而使用反射建立Activity執行個體;接著建立Application,Context等完成Activity組件的啟動。
看起來好像已經天衣無縫萬事大吉了;但是運行一下會出現一個異常。
錯誤提示說是無法執行個體化 Application,而Application的建立也是在performLaunchActivity中進行的,這裡有些蹊蹺,我們仔細查看一下。
5.5.2 繞過系統檢查
通過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層來回牽扯;這其中的來龍去脈要完全把握清楚還請讀者親自翻閱源碼。
上文給出的方案中,我們全盤接管了外掛程式中類的載入過程,這是一種相對暴力的解決方案。
6.小結
本文中我們採用兩種方案成功完成了『啟動沒有在AndroidManifest.xml中顯示聲明,並且存在於外部外掛程式中的Activity』的任務。
『激進方案』中我們自訂了外掛程式的ClassLoader,並且繞開了Framework的檢測;
利用ActivityThread對於LoadedApk的緩衝機制,我們把攜帶這個自訂的ClassLoader的外掛程式資訊添加進mPackages中,進而完成了類的載入過程。
『保守方案』中我們深入探究了系統使用ClassLoaderfindClass的過程,
發現應用程式使用的非系統類別都是通過同一個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 外掛程式載入機制之二