Android插件化系列第(四)篇---插件加载机制两种方案
这篇博客说说插件的加载机制,建议阅读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";
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数组
Object hostDexElements = getDexElements(hostPathList);
//获取插件CassLoader中的dex数组
Object pluginDexElements = getDexElements(pluginPathList);
//获取合并后的pathList
Object sumDexElements = combineArray(hostDexElements, pluginDexElements);
//将合并的pathList设置到本应用的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);
}
上面的代码演示了怎么合并系统默认加载器PathClassLoader和动态加载器DexClassLoader中的dexElements数组,这种方案还是比较简单的,现在看麻烦一些的。
2.1、替换LoadedApk中的mClassLoader
Paste_Image.pngLoadedApk是什么? LoadedApk对象是APK文件在内存中的表示。 Apk文件的相关信息,诸如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,尝试获取缓存数据;如果没有命中缓存数据,才通过LoadedApk的构造函数创建了LoadedApk对象;因此当我们拿到这一份缓存数据,修改里面的ClassLoader,自己控制类加载的过程,这样加载插件中的Activity类的问题就解决了。
public class HookLoadedApk {
public static Map<String, Object> sLoadedApk = new HashMap<String, Object>();
public static void hookLoadedApkInActivityThread(File apkFile) throws Exception {
// 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、创建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参数和PackageUserState参数
*
*
*/
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);
// 创建出一个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;
return applicationInfo;
}
}
这种方法参考了weishu,比较复杂,因为ActivityThread对于LoadedApk有缓存机制,我们才有机可乘,把自定义的ClassLoader的插件信息添加进mPackages中,从而完成了插件的加载。关于这两种方案,不能说哪一种更好,虽然第一种方案易理解,代码少,但是有一个问题,一旦插件之间甚至插件与宿主之间使用的类库有冲突,就会崩溃,DroidPlugin采用的就是第二种方案,Small采用的是第一种方案,合并dexElements数组。第二种方案也有缺点,除了Hook过程复杂外,每一个版本的apk解析都有差别,使用的PackageParser的兼容性就比较差,根据不同版本来分别Hook。详细的可以参考weishu,解释的比我更清楚。
Please accept mybest wishes for your happiness and success !
参考博客
http://weishu.me/2016/04/05/understand-plugin-framework-classloader/