Shadow源码分析—如何启动插件Activity

一、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目录结构如下图:

image

pluginmanager.apk是插件管理apk,其本身也是一个插件,支持升级,此插件会优先被加载

config.json是插件相关配置信息,其中UUID作为当前版本插件的唯一标示,项目使用时也可以定义自己的规范

2、关于测试插件sample-plugin-app-debug.apk,会在编译期修改继承关系

以SplashActivity为例,可以看到源码中为:

image

反编译插件apk中

image

其继承关系发生改变,通过看代码可知ShadowActivity并不是一个真实的Activity,其父类实现及重写了各种Activity及Context的方法,这些方法可以根据自己业务的实际情况自定义

Shadow在编译期使用javassist修改插件Activity的继承关系 相关源码在/projects/sdk/core/gradle-plugin。这里只关注对Activity的处理,其他的大同小异,不熟悉gradle插件的可以自行google,gradle插件基本围绕Plugin、Task、Transform这三个类,可以使用java、groovy、kt语言,没有什么技术壁垒,只要熟悉Android编译流程,可以玩出很多有趣的操作。

image

自定义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、我们看一下宿主注册表

image

需要关注一下红框标注的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的确是目前系统兼容性最优的解决方案,虽然逻辑上有点绕。

本文写的有些仓促,如有错误,欢迎指正交流
欢迎转载,不过请注明出处
欢迎关注下方公众号

编外程序猿

你可能感兴趣的:(Shadow源码分析—如何启动插件Activity)