在尝试 DroidPlugin 插件化的过程中,会遇到一些挺细节的问题,最终大部分情况还是通过阅读框架代码或者源码解决,个人觉得这个也是一个比较好的学习过程,所以将一些细节问题纪录一下。
关于插件Applaciton的问题(资源加载引发的思考)
这个问题,主要和同事讨论,如果避免资源文件冲突的问题,因为部分插件化框架中,通过编译的时的设置,改变资源文件在R文件中的ID值,然后通过反射的方式借助 AssetsManager 来进行资源文件的加载。
于是在 DroidPlugin 源码中全局搜索了 AssetsManager,Resources 这些关键字,发现其实并没有,多少调用点,也并没有多少和资源加载相关的逻辑。结合 DroidPlugin 框架中多进程运行的原理,猜想插件app是运行在自己的application,调用资源时,使用的是自己R文件中ID,所以不会和宿主冲突。之后仔细看了下框架源码,也验证了这个想法。
之前的原理篇中描述了,其实启动Activity关键的两步流程,其中有一步是 handleLaunchActivity 将真实的跳转 Activity 信息替换回 占坑的Activity 过程中,有一步十分关键的流程。
PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);
从这个命名上也可以看出,这是个关于插件进程的管理。这个方法中的主要代码如下。
boolean found = false;
synchronized (sPluginLoadedApkCache) {
Object object = ActivityThreadCompat.currentActivityThread();
if (object != null) {
Object mPackagesObj = FieldUtils.readField(object, "mPackages");
Object containsKeyObj = MethodUtils.invokeMethod(mPackagesObj, "containsKey", pluginInfo.packageName);
if (containsKeyObj instanceof Boolean && !(Boolean) containsKeyObj) {
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB) {
loadedApk = MethodUtils.invokeMethod(object, "getPackageInfoNoCheck", pluginInfo.applicationInfo, CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());
} else {
loadedApk = MethodUtils.invokeMethod(object, "getPackageInfoNoCheck", pluginInfo.applicationInfo);
}
sPluginLoadedApkCache.put(pluginInfo.packageName, loadedApk);
/*添加ClassLoader LoadedApk.mClassLoader*/
String optimizedDirectory = PluginDirHelper.getPluginDalvikCacheDir(hostContext, pluginInfo.packageName);
String libraryPath = PluginDirHelper.getPluginNativeLibraryDir(hostContext, pluginInfo.packageName);
String apk = pluginInfo.applicationInfo.publicSourceDir;
if (TextUtils.isEmpty(apk)) {
pluginInfo.applicationInfo.publicSourceDir = PluginDirHelper.getPluginApkFile(hostContext, pluginInfo.packageName);
apk = pluginInfo.applicationInfo.publicSourceDir;
}
if (apk != null) {
ClassLoader classloader = null;
try {
classloader = new PluginClassLoader(apk, optimizedDirectory, libraryPath, hostContext.getClassLoader().getParent());
} catch (Exception e) {
}
if(classloader==null){
PluginDirHelper.cleanOptimizedDirectory(optimizedDirectory);
classloader = new PluginClassLoader(apk, optimizedDirectory, libraryPath, hostContext.getClassLoader().getParent());
}
synchronized (loadedApk) {
FieldUtils.writeDeclaredField(loadedApk, "mClassLoader", classloader);
}
sPluginClassLoaderCache.put(pluginInfo.packageName, classloader);
Thread.currentThread().setContextClassLoader(classloader);
found = true;
}
ProcessCompat.setArgV0(pluginInfo.processName);
}
}
}
if (found) {
PluginProcessManager.preMakeApplication(hostContext, pluginInfo);
}
首先通过反射,拿到需要加载的插件 apk 信息
loadedApk = MethodUtils.invokeMethod(object, "getPackageInfoNoCheck", pluginInfo.applicationInfo, CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());
之后将它存到一个cache中,以备后续的流程使用,
sPluginLoadedApkCache.put(pluginInfo.packageName, loadedApk);
之后从插件信息中取出插件信息,难道classloader信息,也写入到缓存的loadedApk中
FieldUtils.writeDeclaredField(loadedApk, "mClassLoader", classloader);
再之后会调用 preMakeApplication 方法,启动插件的Applicaiton
主要有如下关键的两步,分别是通过包名拿到之前缓存的 loadedApk 信息,然后 通过反射,创建应用
final Object loadedApk = sPluginLoadedApkCache.get(pluginInfo.packageName);
MethodUtils.invokeMethod(loadedApk, "makeApplication", false, ActivityThreadCompat.getInstrumentation());
至此在启动插件 Activity 之前,就已经将插件所在 Applicaiton 启动起来了(当前,前面的流程,有判空操作,不会重复启动),后续会和 宿主 运行在同一个任务中,所以调用资源文件时,也不会有冲突。