外掛化-外掛APK的解析
本文主要來看一下在外掛化技術中,實現宿主執行時使用外掛 apk
類、資源等原理。(宿主即我們的主apk。外掛apk即可以被載入的外掛模組)。本文所談的實現引用自: VirtualApk : ofollow,noindex">https://github.com/didi/VirtualAPK
另外歡迎關注我的Android進階計劃: https://github.com/SusionSuc/AdvancedAndroid , 好,開始:
外掛apk中resource訪問
外掛化技術應該實現: 對於外掛中的資源在外掛中仍然可以使用 R.xxx.xx
的方式來使用。但是要知道外掛是在apk安裝後加載的。我們日常訪問資源都是使用 context.getResources()
。很明顯,這個 Resources
中是不會包含外掛中的資源的。那麼如何解決這個問題呢?
我們先來回顧一下Android中的資源分類, Android中的資源分為兩大類 : 可直接訪問的資源、無法直接訪問的原生資源。
- 直接訪問資源 : 這些資源可以使用 R.xx.xx 進行訪問, 都儲存在res目錄下, 在編譯的時候, 會自動生成R.java 資源索引檔案。
- 原生資源 : 這些資源存放在assets下, 不能使用R類進行訪問, 只能通過 AssetManager 以二進位制流形式讀取資源。
先來回顧一下 AssetManager
和 Resources
:
Resources
在Android中我們可以通過這個類來訪問我們應用程式的資源。我們知道 Android
在構建過程中會為每個資源生產一個符號(就是我們編碼過程中的各種資源id)檔案 R.java
。 Resources
提供了許多方法,允許我們通過id來訪問資源。比如:
int getColor(int id, Resourcess.Theme theme)返回與特定資源ID關聯的主題顏色整數。
AssetManager
AssetManager
是比 Resources
更低一級的實現。 Resources
可以使用 AssetManager
來構造的。 Resources
提供給開發者一個十分方便的訪問應用程式資源的方式。不過對於原生資源(assets目錄下)就沒有辦法訪問了。 AssetManager
允許我們可以直接訪問這些資原始檔。
比如對於應用程式 assets
目錄下的檔案,我們就可以通過 AssetManager
來訪問:
InputStream open(String fileName, int accessMode)
所以我們是不是把外掛中的資源放到 AssetManager
,然後新構造一個Resouce就OK了呢?
外掛 apk
的Resources我們可以通過 AssetManager.addAssetPath(apkPath)
來加入到 AssetManager
中。我們來看一下這個方法:
/** * Add an additional set of assets to the asset manager.This can be * either a directory or ZIP file.Not for use by applications.Returns * the cookie of the added asset, or 0 on failure. * {@hide} */ public final int addAssetPath(String path) { returnaddAssetPathInternal(path, false); //最終會呼叫到native的方法。 }
但是隻是新增到 AssetManager
中是不行的,這是因為我們日常開發訪問的是 Resources
的API,那麼如何讓 Resources
含有外掛的資源列表呢?我們可以使用 AssetManager
來新構造一個 Resources
:
/** * Create a new Resources object on top of an existing set of assets in an AssetManager. */ @Deprecated public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) { ..... mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments()); }
即通過 new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration())
。我們就可以把上面add到 AssetManager
的外掛資源和原有的 apk
資源整合成一個 Resources
。
那麼接下來,我們只要讓外掛在獲得資源時,是從上面這個整合過的 Resources
中獲取就可以完成在外掛中直接訪問外掛資源了。上面解析的三步實現虛擬碼如下:
Resources hostResources = hostContext.getResources(); //hostContext是宿主的context,這個資源是在編譯時就確定好的。 AssetManager assetManager = hostResources.getAssets(); //拿到宿主的 AssetManager assetManager.addAssetPath(apkPath)// 這一步需要通過反射來完成 Resources newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); //整合宿主資源和外掛資源為一個資源 ...... 替換外掛中訪問的Resources為新整合的Resources : 由於在外掛中也是通過`context.getResources()`來獲取Resources。因此我們只需要hook外掛對`Resources`的獲取,返回我們整合過的 newResources即可。
通過上面的步驟,就可以實現外掛中使用外掛的資源,並且由於新的資源是整合過的,其實也可以實現在宿主中使用外掛的資源。
不過使用這個方式是存在一些問題的:
AssetManager.addAssetPath(apkPath) new Resources() Resource
外掛APK中類的載入
如果在宿主中需要訪問外掛的一個類 AActivity.class
。如果你在直接訪問肯定會拋類找不到異常的,這是因為這個類根本就不能被 Classloader
載入,它找不到。那麼如何讓外掛中的類可以被 Classloader
的載入呢? 這涉及到類的動態載入的問題。
我們還是先來回顧一下 Classloader
的相關知識。在Android中存在兩種類載入器 : DexClassLoader
和 PathClassLoader
,他們倆的不同之處是:
- DexClassLoader可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk
- PathClassLoader只能載入系統中已經安裝過的apk
如果你對Android中的類載入過程還不是很瞭解,推薦看一下這篇文章 : https://www.jianshu.com/p/a620e368389a
所以我們可以通過 DexClassLoader
來載入外掛apk中的類:
DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
可以載入外掛apk的類的 DexClassLoader
已經構造完成了, 那麼有什麼用呢?要知道當app執行時,遇到一個未載入的外掛類時,由於類載入的 雙親委派模型
,並不會到我們建立的這個 DexClassLoader
中去尋找未載入的類:

簡單的類載入邏輯.png
如上圖,即對於未載入的類,在尋找可以載入這個類的 Classloader
時,根本不會在我們新建的這個 DexClassLoader
中尋找。那怎麼辦呢?有兩種思路:
第一 : hook類載入過程,如果沒有找到要載入的類,就手動呼叫新建的 DexClassLoader
來嘗試載入這個類
第二 : 當我們在使用 context.getClassLoader()
方法是你會發現,你拿到的是 PathClassLoader
。
public class PathClassLoader extends BaseDexClassLoader
即 PathClassLoader
是 BaseDexClassLoader
的子類。我們來看一下 BaseDexClassLoader
是如何載入一個未載入的類的:
//BaseDexClassLoader.findClass() protected Class<?> findClass(String name) throws ClassNotFoundException { ...... Class c = pathList.findClass(name, suppressedExceptions); ...... } //pathList的型別 pathList = new DexPathList(this, dexPath, librarySearchPath, null); //DexPathList.java final class DexPathList { private Element[] dexElements; }
即是在 pathList
中去尋找類。我們知道 DexClassLoader
也繼承自 BaseDexClassLoader
。肯定也存在 pathList
。所以如果我們把 DexClassLoader
的 pathList
加在 PathClassLoader
的 pathList
中。那麼app在執行時不就相當於會從我們構造的 DexClassLoader
中尋找類了?
因此第二種方法就是把我們自己建立的 DexClassLoader
的 pathList
整合到可被搜尋的 Classloader
的 pathList
上,下面是主要程式碼實現思路:
baseClassLoader = context.getClassLoader(); Object baseDexElements = getDexElements(getPathList(baseClassLoader));//獲取pathList Object newDexElements = getDexElements(getPathList(dexClassLoader)); Object allDexElements = combineArray(baseDexElements, newDexElements); //結合兩個pathList,生成一個新的 DexElements 陣列 Object pathList = getPathList(baseClassLoader); Reflector.with(pathList).field("dexElements").set(allDexElements); //把BaseClassLoader的pathList的 DexElements 替換為結合過的新的 DexElements
經過上面的操作,App執行時就可以載入外掛中的類了。
外掛APK四大元件相關資訊的解析
上面關於外掛的資源和類載入的問題都大致分析了一下。不過還有一個十分重要的問題我們需要來看一下,那就是外掛中的四大元件,如何被宿主使用呢? 我們知道Android對於四大元件的處理是有特殊邏輯的,比如 Activity
必須在 AndroidManifest
檔案中註冊,並且自系統層面還有一系列校驗機制。
不過本小節我們先不看如何實現宿主使用外掛的四大元件的細節。我們先來看一下,如何把外掛apk的四大相關資訊給解析出來。這是實現宿主使用外掛的四大元件的基礎。那麼怎麼解析呢?
其實Android提供了 PackageParser
,這個類主要用於對Android apk檔案的解析。它會把一個 apk
解析成 PackageParser.Package
物件:
PackageParser.Package parsedPackage = PackageParser().parsePackage(context, apkFile, PackageParser.PARSE_MUST_BE_APK);
PackageParser.Package
我們來看一下解析出來的 PackageParser.Package
都有什麼:
public final static class Package { ...... public final ArrayList<Permission> permissions = new ArrayList<Permission>(0); public final ArrayList<PermissionGroup> permissionGroups = new ArrayList<PermissionGroup>(0); public final ArrayList<Activity> activities = new ArrayList<Activity>(0); public final ArrayList<Activity> receivers = new ArrayList<Activity>(0); public final ArrayList<Provider> providers = new ArrayList<Provider>(0); public final ArrayList<Service> services = new ArrayList<Service>(0); ...... }
一個 PackageParser.Package
中除了上面列舉的四大元件相關資訊,還有一些簽名啦等等(也可以猜測這個類解析出的資訊主要來源自 AndroidManifest.xml
)。
但需要注意的是,這裡的 Activity
可不是我們理解的那個 Activity
,我們來看一下這個類的宣告:
public final static class Activity extends Component<ActivityIntentInfo> { public final ActivityInfo info; //在程式執行時用來表示一個activity的資訊 } public static class Component<II extends IntentInfo> { public final Package owner; public final ArrayList<II> intents; public final String className; public Bundle metaData; } public final static class ActivityIntentInfo extends IntentInfo { public final Activity activity; } public static class IntentInfo extends IntentFilter
通過大致瞭解上面4個類的繼承結構以及與我們所瞭解的Android相關知識相結合,這裡的 Component
是用來表示一個元件(常說的四大元件)。它持有著元件的類資訊、intent資訊等。
Provider
、 Service
的結構基本與 Activity
相同。
即,通過 PackageParser
我們可以解析出一個 apk
中的四大元件、許可權、包簽名等資訊。
上文中其實有很多Android原始碼,這裡推薦一個可以很方便、快速檢視Android原始碼的網站: http://androidxref.com/ 。具體怎麼快速檢視可以參考這篇文章 : https://blog.csdn.net/qq_34908107/article/details/78421212
最後,歡迎關注我的Android進階計劃 : https://github.com/SusionSuc/AdvancedAndroid 。提出批評與指導,一起進步。