这款插件框架它的第一步实现思想就是去自定义加载插件apk的classloader去替换系统加载组件的classloader,从而实现加载插件组件。在application的源码中我们可以看到:
/* package */ final void attach(Context context) {
attachBaseContext(context);
mLoadedApk =
ContextImpl.getImpl(context).mPackageInfo;
}
class ContextImpl extends Context{
final LoadedApk mPackageInfo;
}
public final class LoadedApk {
Resources mResources;
private ClassLoader mClassLoader;
}
可以看出application在attachBaseContext(context);之后从context.mpackageinfo中拿packageinfo 而这个字段在Contextlmpl中其内部的mClassLoader正是加载组件的ClassLoader
同时这里还包含了加载的资源。
那么只要在attachBaseContext(context);中反射将该字段的mClassLoader替换掉就可以了。看ZeusPlugin框架的源码 他在PluginManager中的
mPackageInfo=PluginUtil.getField(application.getBaseContext(), "mPackageInfo");
public static boolean loadPlugin(String pluginId, int version) {
ZeusClassLoader classLoader = new ZeusClassLoader(cl);
classLoader.addAPKPath(pluginId, pluginApkPath, PluginUtil.getLibFileInside(pluginId));
PluginUtil.setField(mPackageInfo, "mClassLoader", classLoader);
}
可以看到在加载插件的方法里将自定义的classLoader反射替换mclassLoader字段,我们查看下自定义的classLoader,ZeusClassLoader是一个框架的classLoader管理类内部有一个
private ZeusPluginClassLoader[] mClassLoader = null;
用来存储ClassLoader 再看它的loadClass方法
Class> clazz = null;
try {
//先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apk
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException ignored) {
}
if (clazz != null) {
return clazz;
}
//挨个的到插件里进行查找
if (mClassLoader != null) {
for (ZeusPluginClassLoader classLoader : mClassLoader) {
if (classLoader == null) continue;
try {
//这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能
clazz = classLoader.loadClassByself(className);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException ignored) {
}
}
}
throw new ClassNotFoundException(className + " in loader " + this);
}
可以看到先用父类也就是系统classLoader进行加载,如果无法加载,那么说明加载的class不是当前的而是插件的就用ZeusPluginClassLoader去加载,而ZeusPluginClassLoader这个真正的ClassLoader之中定义的插件id 路径 复制路径等字段 以实现了ClassLoader的管理和加载。
现在有了加载组件的ClassLoader还需要来拦截系统的Intent并返回我们需要加载的activity类,
通过研究源码可以发现startActivity方法的调用过程中都用了ContextImpl中Mainthread中的
mInstrumentation调用的Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
和public Activity newActivity(Class> clazz, Context context,
IBinder token, Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
Object lastNonConfigurationInstance)
两个方法 因此只要和前面用同样的手法 自定义Instrumentation代理并将其反射进系统字段就可以了 看ZeusPlugin的实现:
public class ZeusInstrumentation extends Instrumentation{
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if(className.equals("com.zeus.ZeusActivityForStandard") && intent != null){
Bundle bundle = intent.getExtras();
if(bundle != null){
String realActivity = bundle.getString(PluginConfig.PLUGIN_REAL_ACTIVITY);
if(!TextUtils.isEmpty(realActivity)){
return super.newActivity(cl, realActivity, intent);
}
}
}
return super.newActivity(cl, className, intent);
}
}
在最后返回activity的时候如果className为框架约定好的类名则说明这个是要加载的类那么只要将之前加载的插件类名拿出来newActivity就可以了
接着在attachBaseContext(context);中Object mMainThread = PluginUtil.getField(mBaseContext, “mMainThread”);
PluginUtil.setField(mMainThread, “mInstrumentation”, new ZeusInstrumentation());
先获得MainThread对象然后将ZeusInstrumentation替换进去就可以了
之后我们还需要获取插件中的Resource,可以看新建一个Resource需要三个参数
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
分别是AssetManager用来加载资源,原资源的config和metrice。后俩个直接获得宿主resource
然后获取就可以了,而AssetManager新建出来后需要反射其中的addAssetPath(String path)方法去添加加载路径,添加完路径将Resources创建出后就需要反射替换进系统里以获得插件资源
PluginResources newResources = new PluginResources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration());
PluginUtil.setField(mBaseContext, "mResources", newResources);
将新建出来的Resources替换进了宿主contextImpl的mResources中以替换资源,并把PluginUtil.setField(mBaseContext, “mTheme”, null);置为空以防theme错误
最后在manifest中注册约定好的替代activity 以骗过ams的各种检测在new Acitivity的时候再返回真实的插件activity就可以了。