android 外掛程式載入機制之二

來源:互聯網
上載者:User

標籤: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 外掛程式載入機制之二

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.