主目录见:Android高级进阶知识(这是总目录索引)
框架地址:VirtualApk
在线源码查看:AndroidXRef
上一篇文章插件化框架VirtualApk之初始化我们已经讲了框架初始化的内容,主要就是hook Instrumentation
类和hook AMS
系统服务。今天这篇就是加载插件apk的内容,是插件化非常重要的一步,废话不多说,直接开写。。。
一.插件加载
在宿主工程初始化之后,我们就要加载插件工程了,还是从用法下手:
PluginManager pluginManager = PluginManager.getInstance(base);
File apk = new File(Environment.getExternalStorageDirectory(), "Test.apk");
if (apk.exists()) {
try {
pluginManager.loadPlugin(apk);
} catch (Exception e) {
e.printStackTrace();
}
}
在前面我们已经实例化过PluginManager
实例了,因为单例模式,所以直接会返回PluginManager
实例,所以我们跟进PluginManager#loadPlugin()
方法:
public void loadPlugin(File apk) throws Exception {
if (null == apk) {
throw new IllegalArgumentException("error : apk is null.");
}
if (!apk.exists()) {
throw new FileNotFoundException(apk.getAbsolutePath());
}
LoadedPlugin plugin = LoadedPlugin.create(this, this.mContext, apk);
if (null != plugin) {
this.mPlugins.put(plugin.getPackageName(), plugin);
// try to invoke plugin's application
plugin.invokeApplication();
} else {
throw new RuntimeException("Can't load plugin which is invalid: " + apk.getAbsolutePath());
}
}
这个方法主要做了两件事,一个是根据apk构建LoadedPlugin
类实例,然后就是调用插件的Application
,我们一个一个来看,首先我们看LoadedPlugin#create()
方法做了啥:
public static LoadedPlugin create(PluginManager pluginManager, Context host, File apk) throws Exception {
return new LoadedPlugin(pluginManager, host, apk);
}
这个方法并没有做什么复杂的操作,就是实例化了LoadedPlugin
类,参数分别是PluginManager
实例,宿主工程的上下文对象Context
,最后就是apk文件,接着我们看LoadedPlugin
类的构造函数写了啥:
LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws PackageParser.PackageParserException {
this.mPluginManager = pluginManager;
this.mHostContext = context;
this.mLocation = apk.getAbsolutePath();
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
this.mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
this.mPackageInfo.signatures = this.mPackage.mSignatures;
this.mPackageInfo.packageName = this.mPackage.packageName;
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
}
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
this.mPackageManager = new PluginPackageManager();
this.mPluginContext = new PluginContext(this);
this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
this.mResources = createResources(context, apk);
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
tryToCopyNativeLib(apk);
// Cache instrumentations
Map<ComponentName, InstrumentationInfo> instrumentations = new HashMap<ComponentName, InstrumentationInfo>();
for (PackageParser.Instrumentation instrumentation : this.mPackage.instrumentation) {
instrumentations.put(instrumentation.getComponentName(), instrumentation.info);
}
this.mInstrumentationInfos = Collections.unmodifiableMap(instrumentations);
this.mPackageInfo.instrumentation = instrumentations.values().toArray(new InstrumentationInfo[instrumentations.size()]);
// Cache activities
Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity activity : this.mPackage.activities) {
activityInfos.put(activity.getComponentName(), activity.info);
}
this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
// Cache services
Map<ComponentName, ServiceInfo> serviceInfos = new HashMap<ComponentName, ServiceInfo>();
for (PackageParser.Service service : this.mPackage.services) {
serviceInfos.put(service.getComponentName(), service.info);
}
this.mServiceInfos = Collections.unmodifiableMap(serviceInfos);
this.mPackageInfo.services = serviceInfos.values().toArray(new ServiceInfo[serviceInfos.size()]);
// Cache providers
Map<String, ProviderInfo> providers = new HashMap<String, ProviderInfo>();
Map<ComponentName, ProviderInfo> providerInfos = new HashMap<ComponentName, ProviderInfo>();
for (PackageParser.Provider provider : this.mPackage.providers) {
providers.put(provider.info.authority, provider.info);
providerInfos.put(provider.getComponentName(), provider.info);
}
this.mProviders = Collections.unmodifiableMap(providers);
this.mProviderInfos = Collections.unmodifiableMap(providerInfos);
this.mPackageInfo.providers = providerInfos.values().toArray(new ProviderInfo[providerInfos.size()]);
// Register broadcast receivers dynamically
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {
receivers.put(receiver.getComponentName(), receiver.info);
try {
BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
this.mHostContext.registerReceiver(br, aii);
}
} catch (Exception e) {
e.printStackTrace();
}
}
this.mReceiverInfos = Collections.unmodifiableMap(receivers);
this.mPackageInfo.receivers = receivers.values().toArray(new ActivityInfo[receivers.size()]);
}
我们看到这个方法比较长,但是都很重要,所以我们这里不省略,一部分一部分来进行说明,首先我们看下面这部分:
this.mPluginManager = pluginManager;
this.mHostContext = context;
this.mLocation = apk.getAbsolutePath();
this.mPackage = PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);
this.mPackage.applicationInfo.metaData = this.mPackage.mAppMetaData;
这部分首先是将pluginManager
和context
赋值给LoadedPlugin
类的相应属性,接着获取apk的绝对路径进行保存,然后调用PackageParserCompat#parsePackage()
方法解析apk包中的AndroidManifest.xml
文件(如果对PackageParser解析APK不是很清楚的话那么可以参考文章PackageParser解析APK(上),PackageParser解析APK(下))将解析的结果保存在PackageParser.Package
类中,最后保存application meta-data
。代码往下:
this.mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
this.mPackageInfo.signatures = this.mPackage.mSignatures;
this.mPackageInfo.packageName = this.mPackage.packageName;
if (pluginManager.getLoadedPlugin(mPackageInfo.packageName) != null) {
throw new RuntimeException("plugin has already been loaded : " + mPackageInfo.packageName);
}
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
这部分代码比较简单,主要是实例化一个PackageInfo
类,然后往里面属性放进相应的值,大家应该一看就知道,不做赘述,我们直接来看下一部分代码:
this.mPackageManager = new PluginPackageManager();
this.mPluginContext = new PluginContext(this);
this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
this.mResources = createResources(context, apk);
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
这里面有很多重要的方法,首先我们来说第一个实例化PluginPackageManager
类,这里主要要将插件中的PackageManager
替换成宿主程序的PackageManager
,这样的话,就可以用宿主的PackageManager
来全权接管获取插件中包apk的信息,因为解析插件apk用的是宿主的上下文对象。接着就是实例化PluginContext
类,这个类继承ContextWrapper
,主要作用也是将插件中的上下文对象替换成这个PluginContext
类实例,然后里面的方法进行重写,替换成我们自己构建的对象。然后就是获取so文件的路径。我们往下看这部分会看到createResources()
和createClassLoader()
方法,这两个方法特别重要,我们这边重点讲解,首先来看createResources()
方法。
二.资源的加载
@WorkerThread
private static Resources createResources(Context context, File apk) {
if (Constants.COMBINE_RESOURCES) {
//如果插件资源合并到宿主里面去的情况,插件可以访问宿主的资源
Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());
ResourcesManager.hookResources(context, resources);
return resources;
} else {
//插件使用独立的Resources,不与宿主有关系,无法访问到宿主的资源
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
}
加载资源这块,如果不清楚的话可以先看AssetManager加载资源过程,我们知道如果插件不调用宿主的资源的话,那么我们这里只要创建一个单独的Resources
实例给插件即可,但是如果要想插件能访问到宿主的资源的话,那么我们这里就得将资源添加到同一个AssetManager
中,我们跟进ResourcesManager#createResources()
方法:
public static synchronized Resources createResources(Context hostContext, String apk) {
Resources hostResources = hostContext.getResources();
Resources newResources = null;
AssetManager assetManager;
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
assetManager = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
} else {
assetManager = hostResources.getAssets();
}
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
}
if (isMiUi(hostResources)) {
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
} else if (isVivo(hostResources)) {
newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
} else if (isNubia(hostResources)) {
newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
} else if (isNotRawResources(hostResources)) {
newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
} else {
// is raw android resources
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
// lastly, sync all LoadedPlugin to newResources
for (LoadedPlugin plugin : pluginList) {
plugin.updateResources(newResources);
}
} catch (Exception e) {
e.printStackTrace();
}
return newResources;
}
这部分代码也会长一点,而且涉及到了兼容性方面的问题,我们也拆开来看,首先我们来看下面这段代码:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
assetManager = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
} else {
assetManager = hostResources.getAssets();
}
这里为啥要区别Android L之前的版本和之后的版本呢?这里主要是在Resources
对象最终是通过AssetManager
对象来获取资源的,不过会先通过资源id查找到资源文件名。resources.arsc
包含了资源的id索引,但是在Android L之前,资源resources.arsc
的解析在前面已经解析完了,这样的话,在addAssetPath
方法将插件资源加入到资源路径列表里后,但是在resources.arsc
中并没有插件资源的id索引,这样会导致Resources
查找不到资源。所以滴滴这个插件化框架想出一个方法,就是构造一个新的 AssetManager
,将宿主和加载过的插件的所有 apk 全都添加一遍,然后再调用hookResources
方法将新的 Resources
替换回原来的(这个地方其实还有其他做法,但是这个地方不展开了)。
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
}
这段代码就是将当前的插件已经加载过的插件的资源重新添加一遍。接着就是很蛋疼的代码了,这应该也是写这个框架当初疼了不少时间的问题:
if (isMiUi(hostResources)) {
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
} else if (isVivo(hostResources)) {
newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
} else if (isNubia(hostResources)) {
newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
} else if (isNotRawResources(hostResources)) {
newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
} else {
// is raw android resources
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
因为不同厂商都重写了Resources
类,所以这个地方做一个适配。最后将各个插件的Resources
类进行更新即可。这样我们的资源就加载完成了。
三.类加载
private static ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) {
File dexOutputDir = context.getDir(Constants.OPTIMIZE_DIR, Context.MODE_PRIVATE);
String dexOutputPath = dexOutputDir.getAbsolutePath();
DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
if (Constants.COMBINE_CLASSLOADER) {
try {
DexUtil.insertDex(loader);
} catch (Exception e) {
e.printStackTrace();
}
}
return loader;
}
如果对类加载不是非常清楚的,可以先看看ClassLoader及dex加载过程,这样我们就知道为什么用DexClassLoader
这个类加载器来加载类了,这里也是一样,如果要让插件工程能调用宿主工程的类的话,那么就要将插件的dex和宿主的dex进行合并放在dexElements
中。我们来看看DexUtil#insertDex()
方法:
public static void insertDex(DexClassLoader dexClassLoader) throws Exception {
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(baseDexElements, newDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectUtil.setField(pathList.getClass(), pathList, "dexElements", allDexElements);
insertNativeLibrary(dexClassLoader);
}
这个类的作用就是将宿主的 DexPathList
中的dexElements
属性值取出来,然后调用combineArray
将两个数组进行合并,然后设置到宿主的dexElements
中去。最后我们看到又调用了insertNativeLibrary()
方法,这个方法是做什么的呢?
private static synchronized void insertNativeLibrary(DexClassLoader dexClassLoader) throws Exception {
if (sHasInsertedNativeLibrary) {
return;
}
sHasInsertedNativeLibrary = true;
Object basePathList = getPathList(getPathClassLoader());
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
List<File> nativeLibraryDirectories = (List<File>) ReflectUtil.getField(basePathList.getClass(),
basePathList, "nativeLibraryDirectories");
nativeLibraryDirectories.add(Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE));
Object baseNativeLibraryPathElements = ReflectUtil.getField(basePathList.getClass(), basePathList, "nativeLibraryPathElements");
final int baseArrayLength = Array.getLength(baseNativeLibraryPathElements);
Object newPathList = getPathList(dexClassLoader);
Object newNativeLibraryPathElements = ReflectUtil.getField(newPathList.getClass(), newPathList, "nativeLibraryPathElements");
Class<?> elementClass = newNativeLibraryPathElements.getClass().getComponentType();
Object allNativeLibraryPathElements = Array.newInstance(elementClass, baseArrayLength + 1);
System.arraycopy(baseNativeLibraryPathElements, 0, allNativeLibraryPathElements, 0, baseArrayLength);
Field soPathField;
if (Build.VERSION.SDK_INT >= 26) {
soPathField = elementClass.getDeclaredField("path");
} else {
soPathField = elementClass.getDeclaredField("dir");
}
soPathField.setAccessible(true);
final int newArrayLength = Array.getLength(newNativeLibraryPathElements);
for (int i = 0; i < newArrayLength; i++) {
Object element = Array.get(newNativeLibraryPathElements, i);
String dir = ((File)soPathField.get(element)).getAbsolutePath();
if (dir.contains(Constants.NATIVE_DIR)) {
Array.set(allNativeLibraryPathElements, baseArrayLength, element);
break;
}
}
ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryPathElements", allNativeLibraryPathElements);
} else {
File[] nativeLibraryDirectories = (File[]) ReflectUtil.getFieldNoException(basePathList.getClass(),
basePathList, "nativeLibraryDirectories");
final int N = nativeLibraryDirectories.length;
File[] newNativeLibraryDirectories = new File[N + 1];
System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
newNativeLibraryDirectories[N] = Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);
}
}
这个方法也比较长,我们看到这边也有一个版本兼容的判断,我们先来看看Android L之前的情况:
File[] nativeLibraryDirectories = (File[]) ReflectUtil.getFieldNoException(basePathList.getClass(),
basePathList, "nativeLibraryDirectories");
final int N = nativeLibraryDirectories.length;
File[] newNativeLibraryDirectories = new File[N + 1];
System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
newNativeLibraryDirectories[N] = Systems.getContext().getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
ReflectUtil.setField(basePathList.getClass(), basePathList, "nativeLibraryDirectories", newNativeLibraryDirectories);
这段代码逻辑比较清晰,首先是获取到DexPathList
类中的nativeLibraryDirectories属性,这个属性主要是存储so文件的目录,因为有可能会有多个,所以是个数组,这个方法主要是将插件的so文件路径添加进nativeLibraryDirectories
,然后重新设置给DexPathList
中。同样的如果Android L之后,前面是一样的只不过有一段代码不同:
this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories, null, 140 suppressedExceptions);
DexPathList
类中增加了一个属性nativeLibraryPathElements
,具体的细节这里也就不做说明了,如果这块有不明白的可以给私信。程序紧接着会调用 tryToCopyNativeLib(apk)
方法,就是将apk中的so文件拷贝到指定的位置。
四.动态注册广播
我们知道LoadedPlugin
构造函数会紧接着缓存instrumentations,activities,services,providers,receivers。这部分代码比较简单,我就直接跳过了。我们直接来讲这段动态注册广播的代码:
Map<ComponentName, ActivityInfo> receivers = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity receiver : this.mPackage.receivers) {
receivers.put(receiver.getComponentName(), receiver.info);
try {
BroadcastReceiver br = BroadcastReceiver.class.cast(getClassLoader().loadClass(receiver.getComponentName().getClassName()).newInstance());
for (PackageParser.ActivityIntentInfo aii : receiver.intents) {
this.mHostContext.registerReceiver(br, aii);
}
} catch (Exception e) {
e.printStackTrace();
}
}
我们知道,四大组件中的广播有静态注册广播和动态注册广播之分,我们这里可以采用动态注册广播的方法,将插件的广播注册到宿主的上下文Context中,这样的话,就可以在插件中发送广播,然后宿主可以接收得到了。所以相对于其他组件,广播还是比较容易处理的。
五.创建插件Application
在PluginManager#loadPlugin()
方法的最后会调用LoadedPlugin#invokeApplication
方法来创建插件的Application,这部分代码稍微简单点,我们一起来浏览下:
public void invokeApplication() {
if (mApplication != null) {
return;
}
// make sure application's callback is run on ui thread.
RunUtil.runOnUiThread(new Runnable() {
@Override
public void run() {
mApplication = makeApplication(false, mPluginManager.getInstrumentation());
}
}, true);
}
这个地方在ui线程又调用了makeApplication()
方法:
private Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
if (null != this.mApplication) {
return this.mApplication;
}
String appClass = this.mPackage.applicationInfo.className;
if (forceDefaultAppClass || null == appClass) {
appClass = "android.app.Application";
}
try {
this.mApplication = instrumentation.newApplication(this.mClassLoader, appClass, this.getPluginContext());
instrumentation.callApplicationOnCreate(this.mApplication);
return this.mApplication;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
这个方法是调用初始化时候hook掉的instrumentation
来创建插件工程的实现了Application
类的子类,然后调用它的onCreate()
方法。到这里,加载插件的代码已经解析完毕了。
总结:加载插件的代码我们已经分析完成了,如果有分析不完整的地方,大家可以提意见,我会补充或者进行解答,或者有错误大家可以提出来。下几篇就会讲关于四大组件启动流程要做的一些处理了,希望大家一起进步。