Shadow 腾讯插件化——深度解剖框架设计
简介
Shadow是最近腾讯开源的一款插件化框架。原理是使用宿主代理的方式实现组件的生命周期。目前的插件化框架,大部分都是使用hook系统的方式来做的。使用代理的基本上没有成体系的框架,只是一些小demo,Shadow框架的开源,在系统api 控制越来越严格的趋势下,算是一个新的方向。
框架对比
Shadow 主要具有以下特点:
- 复用独立安装 App 的源码:插件 App 的源码原本就是可以正常安装运行的。
- 零反射无 Hack 实现插件技术:从理论上就已经确定无需对任何系统做兼容开发,更无任何隐藏 API 调用,和 Google 限制非公开 SDK 接口访问的策略完全不冲突。
- 全动态插件框架:一次性实现完美的插件框架很难,但 Shadow 将这些实现全部动态化起来,使插件框架的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版本插件框架所限制。
- 宿主增量极小:得益于全动态实现,真正合入宿主程序的代码量极小(15KB,160 方法数左右)。
- Kotlin 实现:core.loader,core.transform 核心代码完全用 Kotlin 实现,代码简洁易维护。
编译与开发环境
环境准备
第一次 clone Shadow 的代码到本地后,建议先在命令行编译一次。
- 在编译前,必须设置 ANDROID_HOME 环境变量。
- 在编译时,必须使用 gradlew 脚本,以保证采用了项目配置的 Gradle 版本。
在命令行测试编译时可以执行这个任务:
./gradlew build
如果没有出错,再尝试用 Android Studio 打开工程。
- 必须使用 3.4 或更高版本的 Android Studio 打开工程。(业务插件开发时没有限制)
- 必须关闭 Android Studio 的 Instant Run 功能。
然后就可以在 IDE 中选择 sample-host 模块直接运行了。
Shadow 的所有代码都位于 projects 目录下的 3 个目录,分别是:
- sdk 包含 SDK 的所有代码
- test 包含 SDK 的自动化测试代码
- sample 包含演示代码
其中 sample 应该是大家体验 Shadow 的最佳环境。 详见 sample 目录中的 README 介绍。
注意事项:
- 1、Shadow是跨进程的,插件运行在插件进程,通过Binder机制通信,所以不了解Binder的,建议提前熟悉一下,否则看着会比较绕。
- 2、Shadow的宿主和业务插件之间还有一层中间层,中间层也是以插件的形式加载,同时可以升级,有较强的灵活性。
- 3、插件里写一个页面,比如继承自Activity,我们可以正常写,但是在编译期会修改继承关系,将其父类改为ShadowActivity,ShadowActivity实际上不是一个Activity,他持有HostActivity的代理对象,依赖此完成生命周期的回调。
这个操作是靠修改字节码实现的,自定义gradle脚本,通过javassist或者asm都可以实现,不再赘述。
以上是Shadow插件化的简单介绍;想要更多深入学习Android知识可以参考这份电子册《Android核心进阶技术手册》点击查看获取方式,免费且好用系列。
源码分析:
Shadow源码较多,我们只分析一下插件Activity是如何启动及运行的。
可以先看一下打包出来的apk的结构
我的理解pluginmanager.apk loader.apk runtime.apk是中间层
config.json 是发版信息,主要用于检查更新,其中的uuid即为当前版本的唯一标示
HostApplication的onCreate方法会有一些初始化的工作,主要是把asset目录下的插件复制到指定目录,还有runtime插件的状态恢复,非核心流程,不再详述。
我们直接看启动插件的逻辑,很容易就找到加载插件的缺省页PluginLoadActivity,只有一个startPlugin方法:
public void startPlugin() {PluginHelper.getInstance().singlePool.execute(new Runnable() {@Overridepublic void run() {// 方法名虽然叫loadPluginManager,实际上并没有真正安装manager插件,// 只是将插件路径包装成FixedPathPmUpdater,作为构造函数的参数,创建一个DynamicPluginManager保存在Application中HostApplication.getApp().loadPluginManager(PluginHelper.getInstance().pluginManagerFile);Bundle bundle = new Bundle();//插件的安装路径bundle.putString(Constant.KEY_PLUGIN_ZIP_PATH, PluginHelper.getInstance().pluginZipFile.getAbsolutePath());//当前值是:sample-plugin-appbundle.putString(Constant.KEY_PLUGIN_PART_KEY, getIntent().getStringExtra(Constant.KEY_PLUGIN_PART_KEY));//要启动的插件中的Activity路径 com.tencent.shadow.sample.plugin.app.lib.gallery.splash.SplashActivitybundle.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() {@Overridepublic void onShowLoadingView(final View view) {mHandler.post(new Runnable() {@Overridepublic void run() {mViewGroup.addView(view);}});}@Overridepublic void onCloseLoadingView() {finish();}@Overridepublic void onEnterComplete() {}});}});}
懒的长篇大论,相关逻辑已经写在注释里,会执行到DynamicPluginManager的enter方法:
public void enter(Context context, long fromId, Bundle bundle, EnterCallback callback) {if (mLogger.isInfoEnabled()) {mLogger.info("enter fromId:" + fromId + " callback:" + callback);}//动态管理插件的更新逻辑updateManagerImpl(context);//mManagerImpl的类型是SamplePluginManagermManagerImpl.enter(context, fromId, bundle, callback);mUpdater.update();}
mManagerImpl是一个接口,上面的代码其真实实例是SamplePluginManager,updateManagerImpl方法会安装pluginmanager.apk插件,同时通过反射创建一个SamplePluginManager实例,也就是上面的mManagerImpl,同时支持pluginmanager.apk插件的更新逻辑。
所以进入SamplePluginManager的enter->onStartActivity,代码逻辑比较简单,没什么可说的,需要注意一点是会启动一个线程,去加载zip包下的几个插件(runtime、loader、业务插件),而后会调用到其父类FastPluginManager的startPluginActivity方法:
public void startPluginActivity(Context context, InstalledPlugin installedPlugin, String partKey, Intent pluginIntent) throws RemoteException, TimeoutException, FailedException {Intent intent = convertActivityIntent(installedPlugin, partKey, pluginIntent);if (!(context instanceof Activity)) {intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);}//最终启动的是com.tencent.shadow.sample.plugin.runtime.PluginDefaultProxyActivity//PluginDefaultProxyActivity 在宿主manifest中有注册context.startActivity(intent);}
核心流程就在convertActivityIntent里,从命名就可以看出来,最终会把我们要启动的插件Activity,映射成一个在Manifest里注册的真实Activity,也就是注释中标注的PluginDefaultProxyActivity。
可以回看一下上文“思考”中的内容,即为Shadow第一次使用插件的主要流程,convertActivityIntent的代码如下:
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) {//其持有的是PluginLoaderBinder的引用//这里又是一次跨进程通信mPluginLoader.callApplicationOnCreate(partKey);}return mPluginLoader.convertActivityIntent(pluginIntent);}
loadPlugin:先安装中间层插件再安装业务插件,当然如果已安装,直接跳过
mPluginLoader:是一个比较关键的变量,具体他是什么初始化的,下面会具体分析
后续的代码执行逻辑可自行看源码,首先会执行loadPluginLoaderAndRuntime方法,这个方法里会初始化插件进程的服务,同时将插件进程的binder对象赋值给mPpsController:
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);}....../*** 启动PluginProcessService** @param serviceName 注册在宿主中的插件进程管理service完整名字*/public final void bindPluginProcessService(final String serviceName) {if (mServiceConnecting.get()) {if (mLogger.isInfoEnabled()) {mLogger.info("pps service connecting");}return;}if (mLogger.isInfoEnabled()) {mLogger.info("bindPluginProcessService " + serviceName);}mConnectCountDownLatch.set(new CountDownLatch(1));mServiceConnecting.set(true);//CountDownLatch是一个同步工具,协调多个线程之间的同步//可以看下这篇文章 https://www.cnblogs.com/Lee_xy_z/p/10470181.htmlfinal CountDownLatch startBindingLatch = new CountDownLatch(1);final boolean[] asyncResult = new boolean[1];//从onStartActivity方法可知,当前线程并不是UI线程mUiHandler.post(new Runnable() {@Overridepublic void run() {Intent intent = new Intent();//serviceName的值是com.tencent.shadow.sample.host.PluginProcessPPSintent.setComponent(new ComponentName(mHostContext, serviceName));boolean binding = mHostContext.bindService(intent, new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {//service对应的是PluginProcessService中的mPpsControllerBinderif (mLogger.isInfoEnabled()) {mLogger.info("onServiceConnected connectCountDownLatch:" + mConnectCountDownLatch);}mServiceConnecting.set(false);mPpsController = PluginProcessService.wrapBinder(service);try {//跨进程执行PluginProcessService的setUuidManager方法//UuidManagerBinder内部封装了三个方法,可以让插件进程拿到loader、runtime及指定其他业务插件的相关信息mPpsController.setUuidManager(new UuidManagerBinder(PluginManagerThatUseDynamicLoader.this));} catch (DeadObjectException e) {if (mLogger.isErrorEnabled()) {mLogger.error("onServiceConnected RemoteException:" + e);}} catch (RemoteException e) {if (e.getClass().getSimpleName().equals("TransactionTooLargeException")) {if (mLogger.isErrorEnabled()) {mLogger.error("onServiceConnected TransactionTooLargeException:" + e);}} else {throw new RuntimeException(e);}}try {//第一次拿到的是一个nullIBinder iBinder = mPpsController.getPluginLoader();if (iBinder != null) {mPluginLoader = new BinderPluginLoader(iBinder);}} catch (RemoteException ignored) {if (mLogger.isErrorEnabled()) {mLogger.error("onServiceConnected mPpsController getPluginLoader:", ignored);}}mConnectCountDownLatch.get().countDown();if (mLogger.isInfoEnabled()) {mLogger.info("onServiceConnected countDown:" + mConnectCountDownLatch);}}@Overridepublic void onServiceDisconnected(ComponentName name) {if (mLogger.isInfoEnabled()) {mLogger.info("onServiceDisconnected");}mServiceConnecting.set(false);mPpsController = null;mPluginLoader = null;}}, 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);}}
上文说过,整个流程是运行在子线程,所以启动服务要post到UI线程
后续执行的loadRunTime(uuid);loadPluginLoader(uuid);方法即为启动中间层插件的逻辑,大同小异,只分析loadPluginLoader的执行逻辑,因为要解释关键变量mPluginLoader是怎么来的。
public final void loadPluginLoader(String uuid) throws RemoteException, FailedException {if (mLogger.isInfoEnabled()) {mLogger.info("loadPluginLoader mPluginLoader:" + mPluginLoader);}if (mPluginLoader == null) {PpsStatus ppsStatus = mPpsController.getPpsStatus();if (!ppsStatus.loaderLoaded) {//动态加载sample-loader-debug.apk此插件 在插件进程创建了PluginLoaderBinder的实体mPpsController.loadPluginLoader(uuid);}//拿到PluginLoaderBinder的引用IBinder iBinder = mPpsController.getPluginLoader();mPluginLoader = new BinderPluginLoader(iBinder);}}
PpsStatus:只是一个状态bean,唯一作用就是保存插件的安装状态
mPpsController:怎么来的上文已经说过,所以他所调用的方法的具体实现,都是插件进程Service里,即PluginProcessService
mPpsController.loadPluginLoader方法,即为安装loader插件,具体不再分析,可以自行查看Shadow源码
PluginProcessService的loadPluginLoader方法调用,有个关键点要注意:
void loadPluginLoader(String uuid) throws FailedException {...try {...//pluginLoader类型:PluginLoaderBinder//pluginLoader持有DynamicPluginLoader的对象 封装了一系列插件运行的方法PluginLoaderImpl pluginLoader = new LoaderImplLoader().load(installedApk, uuid, getApplicationContext());pluginLoader.setUuidManager(mUuidManager);mPluginLoader = pluginLoader;} catch (RuntimeException e) {...} catch (Exception e) {...}}
上文中提到,第一次启动插件服务的时候mPluginLoader是null,他的初始化就是在这里,反射创建了一个PluginLoaderBinder对象,也就是mPluginLoader。但是真正干活的是其持有的DynamicPluginLoader对象。具体可以看一下com.tencent.shadow.dynamic.loader.impl.LoaderFactoryImpl类
不要忘了这是跨进程的,所以要这样封装,mPluginLoader也是一个binder对象。
再回到FastPluginManager的loadPlugin方法
中间层插件已处理完,那就到了业务插件,会调用mPluginLoader.getLoadedPlugin(),会返回已安装的插件信息,这个方法的具体实现,从上文分析可知,是在DynamicPluginLoader里。如果要加载的插件没有安装,会调用mPluginLoader.loadPlugin(partKey);安装指定插件。
后续的插件安装逻辑直接看源码吧,相信大家都能看懂,会调到ShadowPluginLoader的loadPlugin方法。
再回到convertActivityIntent方法
如果插件是第一次启动,那么会调用mPluginLoader.callApplicationOnCreate(partKey);
mPluginLoader是谁已经说了很多次,不再强调。这个方法会初始化插件的contentprovider以及broadcastreceiver
我们直接看mPluginLoader.convertActivityIntent(pluginIntent),一连串的方法调用连,最终会调用到ComponentManager类的方法:
/*** 调用前必须先调用isPluginComponent判断Intent确实一个插件内的组件*/private fun Intent.toActivityContainerIntent(): Intent {val bundleForPluginLoader = Bundle()val pluginComponentInfo = pluginComponentInfoMap[component]!!bundleForPluginLoader.putParcelable(CM_ACTIVITY_INFO_KEY, pluginComponentInfo)return toContainerIntent(bundleForPluginLoader)}
其实很好理解,这里就是将插件Activity映射到我们注册在宿主的Activity,同时将映射关系以及一些必要的数据传递。
在demo里最终映射的Activity是com.tencent.shadow.sample.plugin.runtime.PluginDefaultProxyActivity
这是一个真实的Activity,可以正常启动。其主要逻辑都在父类PluginContainerActivity中。
先看PluginContainerActivity的初始化方法:
HostActivityDelegate hostActivityDelegate;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;}
hostActivityDelegate:看命名就知道,这是宿主Activity的代理类,我猜应该是给插件Activity使用的,你们觉得呢?
我们来看一下hostActivityDelegate到底是什么:
override fun getHostActivityDelegate(aClass: Class<out HostActivityDelegator>): HostActivityDelegate {return ShadowActivityDelegate(this)}
回到PluginContainerActivity,以onCreate方法为例:
@Overridefinal protected void onCreate(Bundle savedInstanceState) {...if (hostActivityDelegate != null) {hostActivityDelegate.onCreate(savedInstanceState);} else {...}}
这里会调用hostActivityDelegate的onCreate,也就是ShadowActivityDelegate类的onCreate方法:
/*** com.tencent.shadow.core.loader.delegates.ShadowActivityDelegate*/override fun onCreate(savedInstanceState: Bundle?) {...try {val aClass = mPluginClassLoader.loadClass(pluginActivityClassName)val pluginActivity = PluginActivity::class.java.cast(aClass.newInstance())initPluginActivity(pluginActivity)mPluginActivity = pluginActivity...pluginActivity.onCreate(pluginSavedInstanceState)mPluginActivityCreated = true} catch (e: Exception) {throw RuntimeException(e)}}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.applicationInfopluginActivity.setBusinessName(mBusinessName)pluginActivity.setPluginPartKey(mPartKey)pluginActivity.remoteViewCreatorProvider = mRemoteViewCreatorProvider}
省略掉一些常规代码
val aClass = mPluginClassLoader.loadClass(pluginActivityClassName)
pluginActivityClassName:我们要启动的插件Activity的类路径即为SplashActivity
反射实例化保存在mPluginActivity,用于调用插件Activity的生命周期等系统方法
那么插件Activity要调用super方法,比如onCreate的super方法怎么办呢?
在initPluginActivity方法中会将mHostActivityDelegator 传递给插件activity使用:
pluginActivity.setHostActivityDelegator(mHostActivityDelegator)
本文最开始说过,插件Activity会在编译期修改其继承关系为ShadowActivity,ShadowActivity继承自PluginActivity:
public abstract class PluginActivity extends ShadowContext implements Window.Callback {HostActivityDelegator mHostActivityDelegator;public void onCreate(Bundle savedInstanceState) {mHostActivityDelegator.superOnCreate(savedInstanceState);}
}
宿主调用插件onCrate方法,插件会通过mHostActivityDelegator回调到宿主的super,即mHostActivityDelegator.superOnCreate(savedInstanceState);
public void superOnCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);}
到这整个流程就跑通了。
结尾
以我们多年的插件环境下业务开发经验,插件框架是不可能一步到位实现完美的。 因此,我们相信大部分业务在接入时都是需要一定的二次开发工作。 Shadow 现有的代码满足的是我们自己的业务现在的需求。得益于全动态的设计, 插件框架和插件本身都是动态发布的,插件包里既有插件代码也有插件框架代码, 所以可以根据新版本插件的需要同时开发插件框架。
例如,ShadowActivity 没有实现全所有 Activity 方法,你写的测试代码可能用到了, 就会出现 Method Not Found 错误,只需要在 ShadowActivity 中实现对应方法就可以了。 大部分方法的实现都只是需要简单的转调就能工作正常。
如果遇到不会实现的功能,可以提 Issue。最好附上测试代码。如有更多问题可以私信!
Shadow 腾讯插件化——深度解剖框架设计相关推荐
- Android插件化(使用Small框架)
github: https://github.com/cayden/MySmall Android插件化(使用Small框架) 框架源代码 1. Create Project File->New ...
- Android 手写实现插件化换肤框架 兼容Android10 Android11
目录 一.收集所有需要换肤的view及相关属性 二.统一为所有Activity设置工厂(兼容Android9以上) 三.加载皮肤包资源 四.处理支持库或者自定义view的换肤 五.处理状态栏换肤 六. ...
- 饮水思源--浅析深度学习框架设计中的关键技术
点击上方"深度学习大讲堂"可订阅哦! 编者按:如果把深度学习比作一座城,框架则是这座城中的水路系统,而基于拓扑图的计算恰似城中水的流动,这种流动赋予了这座城以生命.一个优雅的框架在 ...
- android插件化-获取apkplug框架已安装插件-03
上一篇文章成功的将apkplug框架嵌入了应用中而且启动 链接http://www.apkplug.com/blog/?post=10 这一篇文章实现怎样获取全部已安装插件 一 获取框架的System ...
- Winform开发框架之插件化应用框架实现
支持插件化应用的开发框架能给程序带来无穷的生命力,也是目前很多系统.程序追求的重要方向之一,插件化的模块,在遵循一定的接口标准的基础上,可以实现快速集成,也就是所谓的热插拔操作,可以无限对已经开发好系 ...
- Android组件化与插件化开发项目实战整理分享(含支付宝、360、美团、滴滴等大厂项目实战)
小公司不说,但是在大公司的项目发展到一定程度,就必须进行模块的拆分.模块化是一种指导理念,其核心思想就是分而治之.降低耦合.而在 Android 开发的实践,目前有两种途径来实现,一个是组件化,一个是 ...
- 360手机卫士插件化RePlugin今日开源,官方全面解读
作者:张炅轩,360手机卫士·客户端技术专家 写在前面 "RePlugin将在6月底开源,这将是我们献给安卓世界最好的礼物."当我们宣布这一消息时,心中的激动,无以言表.是的,三年 ...
- android插件化、组件化、热补丁傻傻分不清
时至今日,国内的android技术可谓是走在世界前沿,这或许还得感谢这堵"墙"的作用,正所谓哪里有压迫哪里就有反抗啊 从2015年中旬,android插件化的兴起,到2016年底, ...
- android 程序开发的插件化
本文为 博客园 黑暗伯爵 原创,转载请注明 http://hangxin1940.cnblogs.com 原文地址:android 程序开发的插件化 模块化方法 之一 框架已经放出: android ...
最新文章
- 全球智能制造发展现状及前景预测 工业机器人引领行业发展
- R语言数据可视化 ggplot2基础1 ggplot2 图形的分层语法 Layered Grammar 简介
- uniapph5授权成功后返回上一页_被成功验证过的的7条选品思路(收藏)
- luogu P4726 多项式指数函数(模板题FFT、多项式求逆、多项式对数函数)
- C++Gaussian-elimination高斯消元法的实现算法(附完整源码)
- go 语言系列(二)基本数据类型和操作符
- asp.net 添加成功弹出个div提示_IOS12免越狱一键修改微信提示音
- [编写高质量代码:改善java程序的151个建议]建议101 Class类
- 【CCCC】L2-027 名人堂与代金券 (25分),模拟水题
- Java中的内部类与匿名内部类
- 基于java网上购物系统论文,基于Java的网上购物系统的设计与实现_毕业设计(论文).doc...
- 【雷达信号处理基础】第1讲 -- 雷达系统概述
- 软件开发公司能开发哪些类型的app软件
- Simulink仿真---clark变换、反clark变换
- Python ADF 单位根检验 结果理解
- Steam帐号被盗怎么办
- 基于区块链的二维码门禁系统成品演示视频
- Kinect v2保存图像和深度图序列
- 苹果手机输入汉字显示拼音和汉字问题
- 孩子该不该学编程?学编程有用吗?