一、Shadow是什么
Shadow是腾讯开源的一个插件框架,与传统支持四大组件的插件不同,Shadow通过宿主注册的壳子Activity(以Activity为例),将Activity的方法回调,委托给插件Activity执行。
Shadow的插件可以独立运行,此时插件Activity是真实的Activity;但是作为插件使用时,会在编译期通过AOP技术修改插件Activity的继承关系,具体实现可参照下面的源码。
插件运行在独立的进程,下面统称为插件进程。宿主和插件通过binder实现进程间通信。
Github:https://github.com/Tencent/Shadow
二、为了便于理解,先提一下以下几点
Shadow的项目结构还是比较清晰的,不过有几个点还是有必要提前说明,便于更快的理解其实现逻辑
1、Shadow的测试代码都在/projects/sample/source目录下,sample-host是宿主代码,可以直接运行,生成的sample-host-debug.apk目录结构如下图:
pluginmanager.apk是插件管理apk,其本身也是一个插件,支持升级,此插件会优先被加载
config.json是插件相关配置信息,其中UUID作为当前版本插件的唯一标示,项目使用时也可以定义自己的规范
2、关于测试插件sample-plugin-app-debug.apk,会在编译期修改继承关系
以SplashActivity为例,可以看到源码中为:
反编译插件apk中
其继承关系发生改变,通过看代码可知ShadowActivity并不是一个真实的Activity,其父类实现及重写了各种Activity及Context的方法,这些方法可以根据自己业务的实际情况自定义
Shadow在编译期使用javassist修改插件Activity的继承关系 相关源码在/projects/sdk/core/gradle-plugin。这里只关注对Activity的处理,其他的大同小异,不熟悉gradle插件的可以自行google,gradle插件基本围绕Plugin、Task、Transform这三个类,可以使用java、groovy、kt语言,没有什么技术壁垒,只要熟悉Android编译流程,可以玩出很多有趣的操作。
自定义Transform通过重写transform(invocation: TransformInvocation)方法,TransformInvocation可以拦截class的输入输出,替换class的实现在ReplaceClassName的replaceClassName(ctClass: CtClass, oldName: String, newName: String)方法,代码很简单,不再详述。
关于编译期修改class最常用的有三种方式 javassist、asm、aspectj。三者特点不同,应用场景也不一样,在此做简单介绍:
javassist语言更接近java语言,其api非常容易理解,基本看个demo就可以上手,需要注意CtClass的释放;asm学习难度较大,其api比较晦涩,更接近字节码的写法,但其编译速度大概是javassist的3倍(网友测评,有兴趣可以自己试一下),Android studio提供了很多高效的插件,类似ASM Bytecode Outline(谁用谁知道);说到aspectj就不得不提JakeWharton大神,他提供了一种思路,让aspectj在Android编译器修改字节码成为可能,具体可以看一下https://github.com/JakeWharton/hugo 这是一个有7.2k star的demo,不过aspectj本身有其局限性,aspectj通过正则匹配的方式修改字节码,难免会有误操作,而且只能对方法、构造方法、类、变量进行处理。
所以如果只是一些简单的字节码修改,可以考虑javassist或者aspectj,如果对编译效率有要求,不妨试试asm,这也是google官方推荐的处理方式。
3、我们看一下宿主注册表
需要关注一下红框标注的PluginProcessPPS,他是插件进程的service,在后面的源码中会反复提到与其相关的一些逻辑,其主要代码实现在父类PluginProcessService
最下面三个Activity,就是所谓的壳子了,也是在插件进程
三、Application里做了些什么(后续会贴出大量相关代码,具体逻辑已在代码里添加详细注释)
主要是onCreate方法
@Override
public void onCreate() {
super.onCreate();
sApp = this;
//严格检查
detectNonSdkApiUsageOnAndroidP();
//暂时不用关注,回过头来看一看没坏处
LoggerFactory.setILoggerFactory(new AndroidLogLoggerFactory());
//在全动态架构中,Activity组件没有打包在宿主而是位于被动态加载的runtime,
//为了防止插件crash后,系统自动恢复crash前的Activity组件,此时由于没有加载runtime而发生classNotFound异常,导致二次crash
//因此这里恢复加载上一次的runtime
DynamicRuntime.recoveryRuntime(this);
//将asset下的插件管理apk以及插件apk复制到../data/data/...目录
PluginHelper.getInstance().init(this);
HostUiLayerProvider.init(this);
}
DynamicRuntime.recoveryRuntime(this)的注释是官方提供的,这处代码的内部实现可以说是Shadow唯一一处反射处理,个人觉得没必要较真说与官方宣称的0反射相悖,对此官方也给出了相关解释:https://juejin.im/post/5d1b466f6fb9a07ed524b995
对于Container的动态化是可选的,什么意思呢?就是如果你没有腾讯团队的苦恼,完全可以把runtime作为一个library让宿主依赖。
四、启动插件Activity前期准备工作
String partKey = (String) partKeySpinner.getSelectedItem();
Intent intent = new Intent(MainActivity.this, PluginLoadActivity.class);
intent.putExtra(Constant.KEY_PLUGIN_PART_KEY, partKey);
switch (partKey) {
case Constant.PART_KEY_PLUGIN_MAIN_APP:
intent.putExtra(Constant.KEY_ACTIVITY_CLASSNAME, "com.tencent.shadow.sample.plugin.app.lib.gallery.splash.SplashActivity");
break;
}
startActivity(intent);
运行宿主工程,点击那个硕大的按钮就开启的启动插件页面的流程,这里要注意一下partKey的值,可以理解为目标插件的唯一标示,当前值是sample-plugin-app,这个值在后面的逻辑中还会用到
PluginLoadActivity可以理解为加载插件的过度页面,实现逻辑很简单,下面只是把startPlugin方法的执行步骤做了简单注释
public void startPlugin() {
PluginHelper.getInstance().singlePool.execute(new Runnable() {
@Override
public void run() {
//方法名虽然叫loadPluginManager,实际上并没有真正安装manager插件,只是将插件路径包装成FixedPathPmUpdater,作为构造函数的参数,创建一个DynamicPluginManager保存在Application中
HostApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);
Bundle bundle = new Bundle();
//插件zip的路径
bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, PluginHelper.getInstance().pluginZipFile.getAbsolutePath());
//当前值是:sample-plugin-app
bundle.putString(Constant.KEY_PLUGIN_PART_KEY, getIntent().getStringExtra(Constant.KEY_PLUGIN_PART_KEY));
//要启动的插件中的Activity路径
bundle.putString(Constant.KEY_ACTIVITY_CLASSNAME, getIntent().getStringExtra(Constant.KEY_ACTIVITY_CLASSNAME));
//EnterCallback主要是用于处理插件加载过程中的过度状态
HostApplication.getApp().getPluginManager()
.enter(PluginLoadActivity.this, Constant.FROM_ID_START_ACTIVITY, bundle, new EnterCallback() {
@Override
public void onShowLoadingView(final View view) {
mHandler.post(new Runnable() {
@Override
public void run() {
mViewGroup.addView(view);
}
});
}
@Override
public void onCloseLoadingView() {
finish();
}
@Override
public void onEnterComplete() {
}
});
}
});
}
所以下面会执行到DynamicPluginManager的enter方法:
@Override
public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {
if (mLogger.isInfoEnabled()) {
mLogger.info("enter fromId:" + fromId + " callback:" + callback);
}
//动态管理插件的更新逻辑
updateManagerImpl(context);
//mManagerImpl的类型是SamplePluginManager
mManagerImpl.enter(context, fromId, bundle, callback);
mUpdater.update();
}
这个类有几个成员变量:
mUpdater :就是startPlugin方法中第一行注释提到的FixedPathPmUpdater类型的对象,存储了本地pluginmanager.apk的相关信息
mManagerImpl : 这是一个SamplePluginManager类型的对象,这个类实际上实在pluginmanager.apk里,所以可以看到源码是通过反射获取的
思考一下,SamplePluginManager和上面提到的DynamicPluginManager各自的职责是什么?
个人认为有一个上下级的关系,Shadow的插件可以分为管理型插件(也就是pluginmanager.apk)和业务型插件,DynamicPluginManager封装了管理型插件的更新及进入业务插件的入口,内部实现并不复杂,并且持有SamplePluginManager;SamplePluginManager实现了对业务插件的安装、卸载及跳转等逻辑,进程间通信的逻辑在其夫类处理。
下面的主线任务是围绕SamplePluginManager对启动插件Activity的具体处理,支线代码暂时掠过
SamplePluginManager的继承关系
SamplePluginManager -->FastPluginManager -->PluginManagerThatUseDynamicLoader -->BasePluginManager -->PluginManagerImpl -->PluginManager
接着上面的方法调用,会进入SamplePluginManager的enter(final Context context, long fromId, Bundle bundle, final EnterCallback callback) 进而执行onStartActivity(context, bundle, callback)方法
private void onStartActivity(final Context context, Bundle bundle, final EnterCallback callback) {
final String pluginZipPath = bundle.getString(Constant.KEY_PLUGIN_ZIP_PATH);
final String partKey = bundle.getString(Constant.KEY_PLUGIN_PART_KEY);
final String className = bundle.getString(Constant.KEY_ACTIVITY_CLASSNAME);
if (className == null) {
throw new NullPointerException("className == null");
}
final Bundle extras = bundle.getBundle(Constant.KEY_EXTRAS);
//callback是从PluginLoadActivity传过来的,控制加载loading的生命周期
if (callback != null) {
final View view = LayoutInflater.from(mCurrentContext).inflate(R.layout.activity_load_plugin, null);
callback.onShowLoadingView(view);
}
executorService.execute(new Runnable() {
@Override
public void run() {
try {
//插件安装的逻辑 其内部会将zip包下的apk复制到指定目录 包括插件本身的目录,odex优化的目录,so的目录
//此过程会解析config.json 拿到当前内置插件的版本信息即唯一标示
//同时会将当前插件的相关属性更新的数据库
InstalledPlugin installedPlugin = installPlugin(pluginZipPath, null, true);
Intent pluginIntent = new Intent();
pluginIntent.setClassName(
context.getPackageName(),
className
);
if (extras != null) {
pluginIntent.replaceExtras(extras);
}
startPluginActivity(context, installedPlugin, partKey, pluginIntent);
} catch (Exception e) {
throw new RuntimeException(e);
}
if (callback != null) {
callback.onCloseLoadingView();
}
}
});
}
startPluginActivity的实现在其父类FastPluginManager
public Intent convertActivityIntent(InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) throws RemoteException, TimeoutException, FailedException {
//这个partKey的真实值是"sample-plugin-app"
loadPlugin(installedPlugin.UUID, partKey);
Map map = mPluginLoader.getLoadedPlugin();
Boolean isCall = (Boolean) map.get(partKey);
if (isCall == null || !isCall) {
mPluginLoader.callApplicationOnCreate(partKey);
}
return mPluginLoader.convertActivityIntent(pluginIntent);
}
/**
* 其中有一堆跨进程通信的逻辑 具体已经在其内部的方法实现中加了注释
* 如果对跨进程通信不太熟悉,建议先提前了解一下
* @param uuid
* @param partKey
* @throws RemoteException
* @throws TimeoutException
* @throws FailedException
*/
private void loadPluginLoaderAndRuntime(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
if (mPpsController == null) {
//partKey是启动插件的时候在PluginLoadActivity中赋值
//getPluginProcessServiceName 获取插件进程服务的名字
//bindPluginProcessService启动插件进程服务 由此可见,shadow宿主和插件的信息传递是进程间通信的过程
bindPluginProcessService(getPluginProcessServiceName(partKey));
//等待链接超时时间
waitServiceConnected(10, TimeUnit.SECONDS);
}
loadRunTime(uuid);
loadPluginLoader(uuid);
}
private void loadPlugin(String uuid, String partKey) throws RemoteException, TimeoutException, FailedException {
loadPluginLoaderAndRuntime(uuid, partKey);
//到这基本上前期准备工作已经完成
Map map = mPluginLoader.getLoadedPlugin();
//指向了要启动的Activity所在的插件
if (!map.containsKey(partKey)) {
//如果当前已安装的插件中不包括此插件 需要进行安装
mPluginLoader.loadPlugin(partKey);
}
}
UUID:上面已经提到,是通过解析config.json拿到的
mPpsController:其成员变量mRemote是一个IBinder对象,这个值也就是我们在启动插件进程的服务PluginProcessService拿到的真实Binder的代理对象,实际上他(mRemote)指向的是插件进程的PpsBinder对象
首先启动插件进程的PluginProcessService
加载运行时插件sample-runtime-debug.apk
加载sample-loader-debug.apk
加载业务插件sample-plugin-app-debug.apk
最后将目标activity转换为代理activity
具体执行逻辑可看下面的代码注释
/**
* 启动PluginProcessService
*
* @param serviceName 注册在宿主中的插件进程管理service完整名字
*/
public final void bindPluginProcessService(final String serviceName) {
...
//CountDownLatch是一个同步工具,协调多个线程之间的同步
//可以看下这篇文章 https://www.cnblogs.com/Lee_xy_z/p/10470181.html
final CountDownLatch startBindingLatch = new CountDownLatch(1);
final boolean[] asyncResult = new boolean[1];
//从onStartActivity方法可知,当前线程并不是UI线程
mUiHandler.post(new Runnable() {
@Override
public void run() {
Intent intent = new Intent();
//serviceName的值是com.tencent.shadow.sample.host.PluginProcessPPS
intent.setComponent(new ComponentName(mHostContext, serviceName));
boolean binding = mHostContext.bindService(intent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {//service对应的是PluginProcessService中的mPpsControllerBinder
mServiceConnecting.set(false);
mPpsController = PluginProcessService.wrapBinder(service);
try {
//跨进程执行PluginProcessService的setUuidManager方法
//UuidManagerBinder内部封装了三个方法,可以让插件进程拿到loader、runtime及指定其他业务插件的相关信息
mPpsController.setUuidManager(new UuidManagerBinder(PluginManagerThatUseDynamicLoader.this));
} catch (DeadObjectException e) {
...
} catch (RemoteException e) {
...
}
try {
//第一次拿到的是一个null
IBinder iBinder = mPpsController.getPluginLoader();
if (iBinder != null) {
mPluginLoader = new BinderPluginLoader(iBinder);
}
} catch (RemoteException ignored) {
}
mConnectCountDownLatch.get().countDown(){
}
@Override
public void onServiceDisconnected(ComponentName name) {
...
}
}, BIND_AUTO_CREATE);
asyncResult[0] = binding;
startBindingLatch.countDown();
}
});
try {
//当前线程会最多等待10s,startBindingLatch的线程计数为0之前,当前线程会处在中断状态
startBindingLatch.await(10, TimeUnit.SECONDS);
if (!asyncResult[0]) {
throw new IllegalArgumentException("无法绑定PPS:" + serviceName);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
加载runtime和loader插件的方法很相似,具体实现都在PluginProcessService,有一点需要注意的是,loadPluginLoader方法,会创建一个PluginLoaderBinder对象,其中封装了一些关键方法,具体可以看一下源码,类似上面提到的 mPluginLoader.callApplicationOnCreate(partKey)
相关代码:
void loadPluginLoader(String uuid) throws FailedException {
...
省略部分代码
...
//pluginLoader类型:PluginLoaderBinder
//pluginLoader持有DynamicPluginLoader的对象 封装了一系列插件运行的方法
PluginLoaderImpl pluginLoader = new LoaderImplLoader().load(installedApk, uuid, getApplicationContext());
pluginLoader.setUuidManager(mUuidManager);
mPluginLoader = pluginLoader;
...
}
loadPlugin方法更不用多说,自然是加载指定的业务插件
好了,现在前期的准备工作已经完成,后续的逻辑就是如何把目标Activity替换成壳子Activity,并且委托给代理对象实现插件Activity生命周期及各种系统方法的调用了。
五、启动插件Activity
接着上面的代码分析,准备工作完成后会执行mPluginLoader.callApplicationOnCreate(partKey);
mPluginLoader是哪来的?这是一个BinderPluginLoader类型的对象,其中的变量mRemote指向的实体是插件进程创建的pluginLoader对象,可以看上面离这最近的代码。
关于插件的ContentProvider和BroadcastReceiver的相关初始化也在callApplicationOnCreate方法中
mPluginLoader.convertActivityIntent(pluginIntent)最终会跨进程调用到ComponentManager的toContainerIntent方法:
/**
* 构造pluginIntent对应的ContainerIntent
* 调用前必须先调用isPluginComponent判断Intent确实一个插件内的组件
*/
private fun Intent.toContainerIntent(bundleForPluginLoader: Bundle): Intent {
val className = component.className!!
val packageName = packageNameMap[className]!!
//对应真实的ComponentName
component = ComponentName(packageName, className)
//壳子ComponentName
//componentMap会在loadPlugin时建立插件activity和壳子activity的映射关系
val containerComponent = componentMap[component]!!
val businessName = pluginInfoMap[component]!!.businessName
val partKey = pluginInfoMap[component]!!.partKey
val pluginExtras: Bundle? = extras
replaceExtras(null as Bundle?)
val containerIntent = Intent(this)
containerIntent.component = containerComponent
bundleForPluginLoader.putString(CM_CLASS_NAME_KEY, className)
bundleForPluginLoader.putString(CM_PACKAGE_NAME_KEY, packageName)
containerIntent.putExtra(CM_EXTRAS_BUNDLE_KEY, pluginExtras)
containerIntent.putExtra(CM_BUSINESS_NAME_KEY, businessName)
containerIntent.putExtra(CM_PART_KEY, partKey)
containerIntent.putExtra(CM_LOADER_BUNDLE_KEY, bundleForPluginLoader)
containerIntent.putExtra(LOADER_VERSION_KEY, BuildConfig.VERSION_NAME)
containerIntent.putExtra(PROCESS_ID_KEY, DelegateProviderHolder.sCustomPid)
return containerIntent
}
最终会start壳子即:com.tencent.shadow.sample.plugin.runtime.PluginDefaultProxyActivity,这个类本身没有任何实现,主要逻辑在其父类PluginContainerActivity,这个类重写了activity的一堆方法,真正使用的时候可以根据自己的业务情况进行增减
看一下PluginContainerActivity的构造函数:
public PluginContainerActivity() {
HostActivityDelegate delegate;
DelegateProvider delegateProvider = DelegateProviderHolder.getDelegateProvider();
if (delegateProvider != null) {
delegate = delegateProvider.getHostActivityDelegate(this.getClass());
delegate.setDelegator(this);
} else {
Log.e(TAG, "PluginContainerActivity: DelegateProviderHolder没有初始化");
delegate = null;
}
hostActivityDelegate = delegate;
}
delegate就是那位关键先生了,宿主就是委托delegate这个代理对象去调用插件里的对应方法
所以delegate或者其父类必然也定义了对应的方法,并且具备与插件activity关联的能力
到这要回忆一下文章开始所说的,插件activity在编译期已经修改了其继承关系,他的父类已经被改为ShadowActivity
调用delegateProvider.getHostActivityDelegate(this.getClass())会创建一个ShadowActivityDelegate对象,也就是上面代码中的delegate
delegateProvider又是什么时候创建并写入DelegateProviderHolder中的呢?
回忆一下PluginProcessService中的loadPluginLoader方法,这是加载loader插件的方法,这个方法会创建一个DynamicPluginLoader对象,DynamicPluginLoader的初始化方法中创建了一个SamplePluginLoader类型的mPluginLoader对象,他就是delegateProvider
internal class DynamicPluginLoader(hostContext: Context, uuid: String) {
companion object {
private const val CORE_LOADER_FACTORY_IMPL_NAME =
"com.tencent.shadow.dynamic.loader.impl.CoreLoaderFactoryImpl"
}
...
init {
try {
val coreLoaderFactory = mDynamicLoaderClassLoader.getInterface(
CoreLoaderFactory::class.java,
CORE_LOADER_FACTORY_IMPL_NAME
)
mPluginLoader = coreLoaderFactory.build(hostContext)
DelegateProviderHolder.setDelegateProvider(mPluginLoader)
ContentProviderDelegateProviderHolder.setContentProviderDelegateProvider(mPluginLoader)
mPluginLoader.onCreate()
} catch (e: Exception) {
throw RuntimeException("当前的classLoader找不到PluginLoader的实现", e)
}
mContext = hostContext;
mUuid = uuid;
}
}
SamplePluginLoader干什么用的不用多说,上面已经介绍过
onCreate是这些代理方法中关键的一步,我们直接看ShadowActivityDelegate中的方法,PluginContainerActivity的onCreate只是执行了hostActivityDelegate.onCreate(savedInstanceState)
/**
* com.tencent.shadow.core.loader.delegates.ShadowActivityDelegate
*/
override fun onCreate(savedInstanceState: Bundle?) {
...
掠过一些参数传递、主题及Configuration的设置
...
try {
//创建插件activity的对象
val aClass = mPluginClassLoader.loadClass(pluginActivityClassName)
val pluginActivity = PluginActivity::class.java.cast(aClass.newInstance())
initPluginActivity(pluginActivity)
mPluginActivity = pluginActivity
...
pluginActivity.onCreate(pluginSavedInstanceState)
...
} catch (e: Exception) {
throw RuntimeException(e)
}
}
override fun onResume() {
mPluginActivity.onResume()
}
上面说了,这时候插件Activity是个假货,他就是一个我们自定义的Object对象,他只是一个打工仔,但是活儿怎么干,还得靠老板指挥,老板又比较忙,所以只能安插一个监工,也就是ShadowActivityDelegate的mHostActivityDelegator,mHostActivityDelegator就是当前的壳子activity,在PluginContainerActivity的构造方法中调用delegate.setDelegator(this)赋值
看一下initPluginActivity(pluginActivity)方法
private fun initPluginActivity(pluginActivity: PluginActivity) {
pluginActivity.setHostActivityDelegator(mHostActivityDelegator)
pluginActivity.setPluginResources(mPluginResources)
pluginActivity.setHostContextAsBase(mHostActivityDelegator.hostActivity as Context)
pluginActivity.setPluginClassLoader(mPluginClassLoader)
pluginActivity.setPluginComponentLauncher(mComponentManager)
pluginActivity.setPluginApplication(mPluginApplication)
pluginActivity.setShadowApplication(mPluginApplication)
pluginActivity.applicationInfo = mPluginApplication.applicationInfo
pluginActivity.setBusinessName(mBusinessName)
pluginActivity.setPluginPartKey(mPartKey)
pluginActivity.remoteViewCreatorProvider = mRemoteViewCreatorProvider
}
到这插件activity就跑起来了
六、后记
关于Shadow的跨进程设计可以看下官方的文章:https://juejin.im/post/5d1968545188255543342406
本文只关注插件Activity的启动流程,细节方面并未细说,主要目的还是简述一下Shadow的实现思路。毫无疑问,Shadow的确是目前系统兼容性最优的解决方案,虽然逻辑上有点绕。
本文写的有些仓促,如有错误,欢迎指正交流
欢迎转载,不过请注明出处
欢迎关注下方公众号