這篇博客說說插件的載入機制,建議閱讀Android插件化系列第(二)篇—動態載入技術之apk換膚了解類的載入機制。
一、相關概念
1.1、為什麼需要動態載入
這個問題,前面已經介紹過,如下
Android系統使用了ClassLoader機制來進行Activity等組件的載入;apk被安裝之後,APK文件的代碼以及資源會被系統存放在固定的目錄(比如/data/app/package_name/1.apk)系統在進行類載入的時候,會自動去這一個或者幾個特定的路徑來尋找這個類;但是系統並不知道存在於插件中的Activity組件的信息,插件可以是任意位置,甚至是網路,系統無法提前預知,因此正常情況下系統無法載入我們插件中的類;因此也沒有辦法創建Activity的對象,更不用談啟動組件了。這個時候就需要使用動態載入技術了。
1.2、類的載入機制
對於android中的classloader是按照以下的流程,loadClass方法在載入一個類的實例的時候,會先查詢當前ClassLoader實例是否載入過此類,有就返回;如果沒有。查詢Parent是否已經載入過此類,如果已經載入過,就直接返回Parent載入的類;如果繼承路線上的ClassLoader都沒有載入,才由Child執行類的載入工作;這樣做的好處:首先是共享功能,一些Framework層級的類一旦被頂層的ClassLoader載入過就緩存在內存裡面,以後任何地方用到都不需要重新載入。除此之外還有隔離功能,不同繼承路線上的ClassLoader載入的類肯定不是同一個類,這樣的限制避免了用戶自己的代碼冒充核心類庫的類訪問核心類庫包可見成員的情況。這也好理解,一些系統層級的類會在系統初始化的時候被載入,比如java.lang.String,如果在一個應用裡面能夠簡單地用自定義的String類把這個系統的String類給替換掉,那將會有嚴重的安全問題
結論:
DexClassLoader可以載入jar/apk/dex,可以從SD卡中載入未安裝的apk;
PathClassLoader只能載入系統中已經安裝過的apk;
現在介紹兩種插件在宿主中載入的兩種方案。
二、動態載入方案
2.1、合併dexElements數組
這裡的合併是指,將PathClassLoader和DexClassLoader中的dexElements進行合併,這種思路從何而來呢?通常在Android中我們用上述兩個ClassLoader載入類,他們的父類是BaseDexClassLoader。在父類的構造函數中創建了一個DexPathList對象,從名字看上去,估計這個類表示的是把很多個Dex文件的路徑放到一個List集合中。
BaseDexClassLoader.java來看DexPathList的代碼
DexPathList.java類在build之後就會變成一個dex文件,而這個文件的路徑就存放在dexElements。所以自然就會想到,我們把宿主和插件的dex都放到這裡面,這樣系統就會幫我們載入了。
/** * 創建DexClassLoader,不能用DexClassLoader,因為DexClassLoader只能載入安裝過的 */public DexClassLoader createDexClassLoader(Activity pActivity) { String cachePath = pActivity.getCacheDir.getAbsolutePath; String apkPath = Environment.getExternalStorageDirectory.getAbsolutePath + "/chajian_demo.apk"; returnnew DexClassLoader(apkPath, cachePath, cachePath, getClassLoader); }
*/ publicstaticvoid injectClassLoader(DexClassLoader loader,Context context){ //獲取宿主的ClassLoader PathClassLoader pathLoader = (PathClassLoader)context.getClassLoader; try { //獲取宿主pathListObject hostPathList = getPathList(pathLoader); //獲取插件pathListObject pluginPathList = getPathList(loader); //獲取宿主ClassLoader中的dex數組Object hostDexElements = getDexElements(hostPathList); //獲取插件CassLoader中的dex數組Object pluginDexElements = getDexElements(pluginPathList); //獲取合併后的pathListObject sumDexElements = combineArray(hostDexElements, pluginDexElements); //將合併的pathList設置到本應用的ClassLoader setField(hostPathList, suZhuPathList.getClass, "dexElements", sumDexElements); } catch (Exception e) { e.printStackTrace; } }
privatestatic Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); }
privatestatic Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { return getField(paramObject, paramObject.getClass, "dexElements"); }
privatestatic Object getField(Object obj, Class cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { //反射需要獲取的欄位 Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); }
上面的代碼演示了怎麼合併系統默認載入器PathClassLoader和動態載入器DexClassLoader中的dexElements數組,這種方案還是比較簡單的,現在看麻煩一些的。
2.1、替換LoadedApk中的mClassLoader
LoadedApk是什麼? LoadedApk對象是APK文件在內存中的表示。 Apk文件的相關信息,諸如Apk文件的代碼和資源,甚至代碼裡面的Activity,Service等組件的信息我們都可以通過此對象獲取。
為什麼想到要替換LoadedApk中的mClassLoader,這個答案也是看源碼的,在Activity的啟動過程中,會獲取LoadedApk對象。
publicfinal LoadedApk
getPackageInfo(String packageName, CompatibilityInfo compatInfo,
intflags,
intuserId) { final boolean differentUser = (UserHandle.myUserId != userId); synchronized (mResourcesManager) { WeakReference
ref;
if(differentUser) {
ref=
null; }
elseif((flags & Context.CONTEXT_INCLUDE_CODE) !=
0) {
ref= mPackages.
get(packageName); }
else{
ref= mResourcePackages.
get(packageName); } LoadedApk packageInfo =
ref!=
null?
ref.
get:
null;
if(packageInfo !=
null&& (packageInfo.mResources ==
null|| packageInfo.mResources.getAssets.isUpToDate)) {
if(packageInfo.isSecurityViolation && (flags&Context.CONTEXT_IGNORE_SECURITY) ==
0) {
thrownewSecurityException(
"Requesting code from "+ packageName +
" to be run in process "+ mBoundApplication.processName +
"/"+ mBoundApplication.appInfo.uid); }
returnpackageInfo; } }
首先判斷了是不是同一個userId,如果是同一個user,嘗試獲取緩存數據;如果沒有命中緩存數據,才通過LoadedApk的構造函數創建了LoadedApk對象;因此當我們拿到這一份緩存數據,修改裡面的ClassLoader,自己控制類載入的過程,這樣載入插件中的Activity類的問題就解決了。
sLoadedApk =
newHashMap;
publicstaticvoidhookLoadedApkInActivityThread(File apkFile)
throwsException {
// 1、獲取到當前的ActivityThread對象Class activityThreadClass = Class.forName(
"android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(
"currentActivityThread"); currentActivityThreadMethod.setAccessible(
true); Object currentActivityThread = currentActivityThreadMethod.invoke(
null);
//2、 獲取 mPackages 靜態成員變數, 這裡緩存了dex包的信息Field mPackagesField = activityThreadClass.getDeclaredField(
"mPackages"); mPackagesField.setAccessible(
true); Map mPackages = (Map) mPackagesField.get(currentActivityThread);
// 方法簽名:public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo)//3、獲取getPackageInfoNoCheck方法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);
//4、獲取applicationInfo信息ApplicationInfo applicationInfo = generateApplicationInfo(apkFile); Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo); String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath; String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath;
//5、創建DexClassLoaderClassLoader classLoader =
newDexClassLoader(apkFile.getPath, odexPath, libDir, ClassLoader.getSystemClassLoader); Field mClassLoaderField = loadedApk.getClass.getDeclaredField(
"mClassLoader"); mClassLoaderField.setAccessible(
true);
//6、替換掉loadedApkmClassLoaderField.set(loadedApk, classLoader);
// 由於是弱引用, 為了防止被GC,我們必須在某個地方存一份sLoadedApk.put(applicationInfo.packageName, loadedApk); WeakReference weakReference =
newWeakReference(loadedApk); mPackages.put(applicationInfo.packageName, weakReference); }
/** * 反射generateApplicationInfo方法,得到ApplicationInfo對象 * * generateApplicationInfo方法簽名: * public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state, int userId) * * 這個方法需要Package參數和PackageUserState參數 * * */publicstaticApplicationInfo
generateApplicationInfo(File apkFile)
throwsException{
// 獲取PackageParser類Class packageParserClass = Class.forName(
"android.content.pm.PackageParser");
// 獲取PackageParser$Package類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對象供使用Object packageParser = packageParserClass.newInstance;
// 調用 PackageParser.parsePackage 解析apk的信息Method parsePackageMethod = packageParserClass.getDeclaredMethod(
"parsePackage", File.class,
int.class);
// 得到第一個參數 :PackageParser.Package 對象Object packageObj = parsePackageMethod.invoke(packageParser, apkFile,
0);
//得到第三個參數:PackageUserState對象Object defaultPackageUserState = packageUserStateClass.newInstance;
// 反射generateApplicationInfo得到ApplicationInfo對象ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser, packageObj,
0, defaultPackageUserState); String apkPath = apkFile.getPath; applicationInfo.sourceDir = apkPath; applicationInfo.publicSourceDir = apkPath;
returnapplicationInfo; } }
這種方法參考了weishu,比較複雜,因為ActivityThread對於LoadedApk有緩存機制,我們才有機可乘,把自定義的ClassLoader的插件信息添加進mPackages中,從而完成了插件的載入。關於這兩種方案,不能說哪一種更好,雖然第一種方案易理解,代碼少,但是有一個問題,一旦插件之間甚至插件與宿主之間使用的類庫有衝突,就會崩潰,DroidPlugin採用的就是第二種方案,Small採用的是第一種方案,合併dexElements數組。第二種方案也有缺點,除了Hook過程複雜外,每一個版本的apk解析都有差別,使用的PackageParser的兼容性就比較差,根據不同版本來分別Hook。詳細的可以參考weishu,解釋的比我更清楚。
Please accept mybest wishes for your happiness and success !
參考博客
http://www.wjdiankong.cn/android%E4%B8%AD%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E7%AF%87%E4%B9%8B-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BDactivity%E5%85%8D%E5%AE%89%E8%A3%85%E8%BF%90%E8%A1%8C%E7%A8%8B%E5%BA%8F/