這篇博客說說插件的加載機(jī)制南蓬,建議閱讀Android插件化系列第(二)篇---動態(tài)加載技術(shù)之a(chǎn)pk換膚了解類的加載機(jī)制歇父。
一坏怪、相關(guān)概念
1.1、為什么需要動態(tài)加載
這個問題积糯,前面已經(jīng)介紹過掂墓,如下
Android系統(tǒng)使用了ClassLoader機(jī)制來進(jìn)行Activity等組件的加載;apk被安裝之后看成,APK文件的代碼以及資源會被系統(tǒng)存放在固定的目錄(比如/data/app/package_name/1.apk)系統(tǒng)在進(jìn)行類加載的時候君编,會自動去這一個或者幾個特定的路徑來尋找這個類;但是系統(tǒng)并不知道存在于插件中的Activity組件的信息川慌,插件可以是任意位置吃嘿,甚至是網(wǎng)絡(luò),系統(tǒng)無法提前預(yù)知梦重,因此正常情況下系統(tǒng)無法加載我們插件中的類兑燥;因此也沒有辦法創(chuàng)建Activity的對象,更不用談啟動組件了琴拧。這個時候就需要使用動態(tài)加載技術(shù)了艾蓝。
1.2于置、類的加載機(jī)制
對于android中的classloader是按照以下的流程话速,loadClass方法在加載一個類的實(shí)例的時候乳讥,會先查詢當(dāng)前ClassLoader實(shí)例是否加載過此類汹忠,有就返回赋焕;如果沒有僧界。查詢Parent是否已經(jīng)加載過此類涨共,如果已經(jīng)加載過扒吁,就直接返回Parent加載的類火鼻;如果繼承路線上的ClassLoader都沒有加載,才由Child執(zhí)行類的加載工作雕崩;這樣做的好處:首先是共享功能魁索,一些Framework層級的類一旦被頂層的ClassLoader加載過就緩存在內(nèi)存里面,以后任何地方用到都不需要重新加載盼铁。除此之外還有隔離功能粗蔚,不同繼承路線上的ClassLoader加載的類肯定不是同一個類,這樣的限制避免了用戶自己的代碼冒充核心類庫的類訪問核心類庫包可見成員的情況饶火。這也好理解支鸡,一些系統(tǒng)層級的類會在系統(tǒng)初始化的時候被加載冬念,比如java.lang.String,如果在一個應(yīng)用里面能夠簡單地用自定義的String類把這個系統(tǒng)的String類給替換掉牧挣,那將會有嚴(yán)重的安全問題
結(jié)論:
DexClassLoader可以加載jar/apk/dex急前,可以從SD卡中加載未安裝的apk;
PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk瀑构;
現(xiàn)在介紹兩種插件在宿主中加載的兩種方案裆针。
二、動態(tài)加載方案
2.1寺晌、合并dexElements數(shù)組
這里的合并是指世吨,將PathClassLoader和DexClassLoader中的dexElements進(jìn)行合并,這種思路從何而來呢呻征?通常在Android中我們用上述兩個ClassLoader加載類耘婚,他們的父類是BaseDexClassLoader。在父類的構(gòu)造函數(shù)中創(chuàng)建了一個DexPathList對象陆赋,從名字看上去沐祷,估計這個類表示的是把很多個Dex文件的路徑放到一個List集合中。
BaseDexClassLoader.java
來看DexPathList的代碼
DexPathList.java
類在build之后就會變成一個dex文件攒岛,而這個文件的路徑就存放在dexElements赖临。所以自然就會想到,我們把宿主和插件的dex都放到這里面灾锯,這樣系統(tǒng)就會幫我們加載了兢榨。
/**
* 創(chuàng)建DexClassLoader,不能用DexClassLoader,因?yàn)镈exClassLoader只能加載安裝過的
*/
public DexClassLoader createDexClassLoader(Activity pActivity) {
String cachePath = pActivity.getCacheDir().getAbsolutePath();
String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/chajian_demo.apk";
return new DexClassLoader(apkPath, cachePath, cachePath, getClassLoader());
}
*/
public static void injectClassLoader(DexClassLoader loader,Context context){
//獲取宿主的ClassLoader
PathClassLoader pathLoader = (PathClassLoader)context.getClassLoader();
try {
//獲取宿主pathList
Object hostPathList = getPathList(pathLoader);
//獲取插件pathList
Object pluginPathList = getPathList(loader);
//獲取宿主ClassLoader中的dex數(shù)組
Object hostDexElements = getDexElements(hostPathList);
//獲取插件CassLoader中的dex數(shù)組
Object pluginDexElements = getDexElements(pluginPathList);
//獲取合并后的pathList
Object sumDexElements = combineArray(hostDexElements, pluginDexElements);
//將合并的pathList設(shè)置到本應(yīng)用的ClassLoader
setField(hostPathList, suZhuPathList.getClass(), "dexElements", sumDexElements);
} catch (Exception e) {
e.printStackTrace();
}
}
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return getField(paramObject, paramObject.getClass(), "dexElements");
}
private static Object getField(Object obj, Class<?> cl, String field)
throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
//反射需要獲取的字段
Field localField = cl.getDeclaredField(field);
localField.setAccessible(true);
return localField.get(obj);
}
上面的代碼演示了怎么合并系統(tǒng)默認(rèn)加載器PathClassLoader和動態(tài)加載器DexClassLoader中的dexElements數(shù)組,這種方案還是比較簡單的顺饮,現(xiàn)在看麻煩一些的吵聪。
2.1、替換LoadedApk中的mClassLoader
LoadedApk是什么兼雄? LoadedApk對象是APK文件在內(nèi)存中的表示吟逝。 Apk文件的相關(guān)信息,諸如Apk文件的代碼和資源君旦,甚至代碼里面的Activity,Service等組件的信息我們都可以通過此對象獲取嘲碱。
為什么想到要替換LoadedApk中的mClassLoader金砍,這個答案也是看源碼的,在Activity的啟動過程中麦锯,會獲取LoadedApk對象恕稠。
public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
int flags, int userId) {
final boolean differentUser = (UserHandle.myUserId() != userId);
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
ref = null;
} else if ((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) {
throw new SecurityException(
"Requesting code from " + packageName
+ " to be run in process "
+ mBoundApplication.processName
+ "/" + mBoundApplication.appInfo.uid);
}
return packageInfo;
}
}
首先判斷了是不是同一個userId,如果是同一個user扶欣,嘗試獲取緩存數(shù)據(jù)鹅巍;如果沒有命中緩存數(shù)據(jù)千扶,才通過LoadedApk的構(gòu)造函數(shù)創(chuàng)建了LoadedApk對象;因此當(dāng)我們拿到這一份緩存數(shù)據(jù)骆捧,修改里面的ClassLoader澎羞,自己控制類加載的過程,這樣加載插件中的Activity類的問題就解決了敛苇。
public class HookLoadedApk {
public static Map<String, Object> sLoadedApk = new HashMap<String, Object>();
public static void hookLoadedApkInActivityThread(File apkFile) throws Exception {
// 1妆绞、獲取到當(dāng)前的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
//2、 獲取 mPackages 靜態(tài)成員變量, 這里緩存了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、創(chuàng)建DexClassLoader
ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
//6来涨、替換掉loadedApk
mClassLoaderField.set(loadedApk, classLoader);
// 由于是弱引用, 為了防止被GC图焰,我們必須在某個地方存一份
sLoadedApk.put(applicationInfo.packageName, loadedApk);
WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference);
}
/**
* 反射generateApplicationInfo方法,得到ApplicationInfo對象
*
* generateApplicationInfo方法簽名:
* public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state, int userId)
*
* 這個方法需要Package參數(shù)和PackageUserState參數(shù)
*
*
*/
public static ApplicationInfo generateApplicationInfo(File apkFile) throws Exception{
// 獲取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);
// 創(chuàng)建出一個PackageParser對象供使用
Object packageParser = packageParserClass.newInstance();
// 調(diào)用 PackageParser.parsePackage 解析apk的信息
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
// 得到第一個參數(shù) :PackageParser.Package 對象
Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);
//得到第三個參數(shù):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;
return applicationInfo;
}
}
這種方法參考了weishu蹦掐,比較復(fù)雜技羔,因?yàn)锳ctivityThread對于LoadedApk有緩存機(jī)制,我們才有機(jī)可乘笤闯,把自定義的ClassLoader的插件信息添加進(jìn)mPackages中堕阔,從而完成了插件的加載。關(guān)于這兩種方案颗味,不能說哪一種更好超陆,雖然第一種方案易理解,代碼少浦马,但是有一個問題时呀,一旦插件之間甚至插件與宿主之間使用的類庫有沖突,就會崩潰晶默,DroidPlugin采用的就是第二種方案谨娜,Small采用的是第一種方案,合并dexElements數(shù)組磺陡。第二種方案也有缺點(diǎn)趴梢,除了Hook過程復(fù)雜外,每一個版本的apk解析都有差別币他,使用的PackageParser的兼容性就比較差坞靶,根據(jù)不同版本來分別Hook。詳細(xì)的可以參考weishu蝴悉,解釋的比我更清楚彰阴。
Please accept mybest wishes for your happiness and success !
參考博客
http://weishu.me/2016/04/05/understand-plugin-framework-classloader/