Shadow插件化系列简单详解
Posted 初一十五啊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Shadow插件化系列简单详解相关的知识,希望对你有一定的参考价值。
前言
组件化模块已经暂时完结,今天更新一下Shadow插件化系列
今天有点事,事件有点赶,可能有点点乱,讲究看,后续:
- shadow优势
- shadow源码分析
- 框架介绍
关注公众号:初一十五a
解锁 《Android十三大板块文档》,让学习更贴近未来实战。已形成PDF版
内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+Flutter项目实战+Flutter电子书
12.高级Android组件化强化实战
13.十二模块之补充部分:其他Android十一大知识体系
整理不易,关注一下吧。开始进入正题,ღ( ´・ᴗ・` ) 🤔
背景
由于QQ的包体积越来越大,给运营推广带来很大的压力。我们在进行无用资源清除、图片格式转换、资源压缩等等一系列组合拳之后,包体积有了一定的缩减,
但是也到达了一个瓶颈。经过一系列调研之后,决定采用插件化技术,将部分模块使用插件的方式进行下发,从而有效的去缩减包体积大小。
一丶什么是插件化
1.1 模块化和插件化
通常模块化是指一个应用有多个业务模块,每个模块有独立的module,各module之间通过路由或者接口的形式进行通信,但是最终打包的时候,所有module都会被打包到同一个apk里面。
插件化和模块化类似的地方就是,每个模块也是独立的module,不同的是插件所属的模块不会打包进宿主apk,插件模块会打包成独立的apk,然后通过网络下发的方式,让宿主动态去加载插件apk。
1.2 插件化的好处
宿主插件互相解藕,
减少宿主APK的包体积;
插件可以动态下发,升级插件功能或者修复问题非常方便快捷,不依赖发版。
1.3 插件化的知识基础
1.3.1 类加载机制
所有的插件化框架,都需要去使用Classloader去加载插件里面的类文件,所以这里需要大家了解一下Classloader的类加载机制,
相对于 java 的 ClassLoader,双亲委派是同样适用的,只不过类加载器有些出入,下面看一下android中常用的几个 ClassLoader:
public static PliginManager getPluginManager(File apk)
final FixedPathPmUpdater fixedPathUpdater = new FixedpathpmUpdater(apk);
File tempPm = fixedPathPmUpdater.getLatest();
if (tempPm ! = null)
return new DynamicPluginManager(fixedPathPmUpdater);
return null;
在Android中,我们正常安装到手机里面的APK所包含的类都是使用 PathClassLoader 进行加载的,而 PathClassLoader 所持有的 parent 则是 BootClassLoader。
插件化技术通常都会使用 DexClassLoader 去加载存储卡中的 APK,而在构造一个 DexClassLoader 的适合,需要指定一个 parent,通常都是指定为 PathClassLoader,这样 DexClassLoader、PathClassLoader、BootClassLoader形成了一个从子到父的链,通常也是满足双亲委派机制的(有些场景会继承 DexClassLoader 去修改类加载流程,有可能会破坏双亲委派的链,这里就不做讨论了)。
1.3.2 四大组件的占坑逻辑
Android的四大组件通常都需要在清单文件声明(动态广播除外),所以这里就有个插件化所需要解决的核心问题:插件里的四大组件没有在清单文件声明,那么怎么去启动他们呢? 各大插件化框架基本上都是使用狸猫换太子的方式来实现的,也就是所谓的占坑。
占坑可以理解为:我们先在宿主的清单文件中声明好提供给插件使用的四大组件坑位,当我们尝试去启动插件Activity 的时候,先使用坑位Activity替换要启动的插件Activity,骗过 Manifest 的校验,然后在加载 Activity 的时候,再去加载真正要启动的插件Activity,从而实现了狸猫换太子的方案。
各大厂商的占坑方案:
三丶为什么要用Shadow
我在做技术调研的时候,主要考虑插件化技术的以下几个方面:
-
兼容性
-
稳定性
-
社区活跃度
所以对目前市面上最大的几个插件化框架做了对比VirtualAPK、Replugin、Shadow
3.1.VirtualAPK
VirtualAPK 是滴滴开源的插件化框架,它的稳定性在滴滴已经得到验证,下面我们来看一下它的缺点。
-
至少2年以上没有代码提交记录了,处于不维护状态。
-
目前只适配到了Android 9.0
3.2.Replugin
360出品的插件化框架,老牌插件化框架,社区也比较活跃。
-
没有适配AndroidX,虽然我对它做过AndroidX的适配,但是没有上线验证过,所以整体稳定性有待考量。
-
维护较慢,也是很久没有维护了。
-
目前也是只兼容到了Android9.0版本的系统。
Replugin 目前只兼容到Android9.0。它通过Hook宿主的ClassLoader去实现偷梁换柱的插件加载逻辑,入侵度一般。占坑太重,生成过多坑位,很多是用不上的。最严重的问题就是没有适配AndroidX,虽然我对它进行了AndroidX的适配改造,但是稳定性还没有得到验证。
3.3.Shadow
腾讯出品的插件化框架,在手机QQ得到验证,稳定性值得肯定,w号称 0 Hook(有待商榷),社区还是比较活跃的。
介绍
Shadow是一个腾讯自主研发的Android插件框架,经过线上亿级用户量检验。 Shadow不仅开源分享了插件技术的关键代码,还完整的分享了上线部署所需要的所有设计。
与市面上其他插件框架相比,Shadow主要具有以下特点:
- 复用独立安装App的源码:插件App的源码原本就是可以正常安装运行的。
- 零反射无Hack实现插件技术:从理论上就已经确定无需对任何系统做兼容开发,更无任何隐藏API调用,和Google限制非公开SDK接口访问的策略完全不冲突。
- 全动态插件框架:一次性实现完美的插件框架很难,但Shadow将这些实现全部动态化起来,使插件框架的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版本插件框架所限制。
- 宿主增量极小:得益于全动态实现,真正合入宿主程序的代码量极小(15KB,160方法数左右)。
- Kotlin实现:core.loader,core.transform核心代码完全用Kotlin实现,代码简洁易维护。
2.维护非常及时,反馈也非常快
-
缺点
由于设计的过于灵活,上手成本增加不少;发布一个插件,至少需要一个PluginManager和一个插件zip,受网络等外部环境影响概率增大;没有发布到maven上,所以暂时只能依靠源码或者自己发布maven。SDK功能不够全面,需要我们进行二次开发。
3.4.总结
综合对上面三个插件化框架的调研,由于VirtualApk和Replugin最高只适配到了Android9.0系统,并且很久没有维护了,所以这里不做考虑。
而Shadow作为手Q开源的插件化框架,号称采用了零反射无Hack实现插件技术,所以对于Android版本的兼容性(尤其是高版本Android系统)应该优于VirtualApk和Replugin,并且社区较活跃,Shadow的开发人员反馈问题也非常及时。
所以我们暂定 Shadow 作为我们插件化技术方案,接下来就是对 Shadow的使用方式和原理进行剖析。
四丶Shadow简介
图列举了Shadow对比其他插件化框架对宿主代码的增量:
Shadow官方号称 零反射无Hack实现插件技术、全动态插件框架,基本上将能够动态下发的全部动态下发了,从而实现宿主代码增量极少,并且可以非常灵活的控制插件的下发。但是Shadow真的是对宿主没有任何反射Hack么?这里暂时卖个关子。
五丶Shadow原理剖析
5.1 一个Shadow插件组成部分
- PluginManager 插件管理
生成PluginPackageManager
val buildPackageManager = executorService.submit(Callable
//获取插件信息
val packageInfo = getPackageInfo.get()
//解析成我们自己的PluginInfo。
val pluginInfo = ParsePluginApkBloc.parse(packageInfo, loadParameters, hostAppContext)
//构建PluginPackManager
PluginPackageManager(commonPluginPackageManager, pluginInfo)
)
整个过程很简单,直接看PluginPackageManager。
class PluginPackageManager(val commonPluginPackageManager: CommonPluginPackageManager,
val pluginInfo: PluginInfo) : PackageManager()
override fun getApplicationInfo(packageName: String?, flags: Int): ApplicationInfo
val applicationInfo = ApplicationInfo()
applicationInfo.metaData = pluginInfo.metaData
applicationInfo.className = pluginInfo.applicationClassName
return applicationInfo
override fun getPackageInfo(packageName: String?, flags: Int): PackageInfo?
if (pluginInfo.packageName == packageName)
val info = PackageInfo()
info.versionCode = pluginInfo.versionCode
info.versionName = pluginInfo.versionName
info.signatures = pluginInfo.signatures
return info;
return null;
override fun getActivityInfo(component: ComponentName, flags: Int): ActivityInfo
val find = pluginInfo.mActivities.find
it.className == component.className
if (find == null)
return commonPluginPackageManager.getActivityInfo(component, flags)
else
return find.activityInfo
override fun getProviderInfo(component: ComponentName?, flags: Int): ProviderInfo
ImplementLater()
override fun getReceiverInfo(component: ComponentName?, flags: Int): ActivityInfo
ImplementLater()
插件管理模块,该模块打包成独立apk下发。宿主通过接口声明+反射和 PluginManager 形成映射关系。宿主所有的插件调用,都需要通过 PluginManager。
- Plugin-loader
插件的 loader 模块,该模块会作为独立的apk被打包进插件的zip包中。PluginManager 通过和 loader 的通信,实现对插件的安装、调用等操作。
- Plugin-runtime
插件的 runtime 模块,该模块会作为独立的 apk 被打包进插件的zip包中。该模块包含了代理Acitvity、代理ContentProvider等类。
- Plugin-app
插件的业务模块,该模块会作为独立的 apk 被打包进插件的zip包中。就是咱们所有的插件业务代码。
5.2 SDK结构
5.3 源码剖析
5.3.1 Shadow运行流程图
下图是启动插件Activity的大概流程:
通过上面流程图,我们了解了Shadow每个模块的执行顺序:
-
Host 通过 PluginManager 去调用插件;
-
PluginManager 又通过 IBinder 和插件服务通信,加载 plugin-runtime、 plugin-loader;
-
PluginManager 通过获取操作 plugin-loader 的 IBinder,从而实现了对插件的加载、调用等操作。
了解了 Shadow 模块的执行顺序之后,接下来我们结合源码从每个模块的具体安装、加载、调用的细节来分析一下。
5.3.2 PluginManager
PluginManager 的类图
- PluginManager 的加载流程
PluginManager 的加载,在使用的时候,会创建一个 DynamicPluginManager 对象(这个 DynamicPluginManager 是写在宿主里面的)
public static PliginManager getPluginManager(File apk)
final FixedPathPmUpdater fixedPathUpdater = new FixedpathpmUpdater(apk);
File tempPm = fixedPathPmUpdater.getLatest();
if (tempPm ! = null)
return new DynamicPluginManager(fixedPathPmUpdater);
return null;
当我们调用 DynamicPluginManager 的 enter 方法进行插件操作的时候,会调用 updateManagerImpl() 方法,尝试去加载真正的Manager实现类:
@Override
public void enter(Context context,string uuid,long fromld,Bundle bundle,EnterCallback callback)
if (mManagerlmpl = = null)
updateManagerlmpl(context);
...
mManagerlmpl.enter(context,uuid,fromld,bundle,callback);
mUpdater.update();
private void updateManagerlmpl(Context context)
...
ManagerlmpLoader implLoader = new ManagerlmpLoader(context,latestManagerlmplApk);
PluginManagerlmpl newlmpl = implLoader.load();
...
mManagerlmpl = newlmpl;
...
ManagerImplLoader 的构造方法会创建 PluginManager.apk 所释放的路径:
然后再看看 ManagerImplLoader 的 load() 方法:
这个工厂类是通过ApkClassLoader反射加载的 ManagerFactoryImpl,ManagerFactoryImpl 是需要我们把包名和类名丝毫不动的声明在PluginManager.apk中,再看一下 ManagerFactoryImpl 的 buildManager()
调用
最终,加载出来的是我们自己在 PluginManager.apk 中创建的 SamplePluginManager,这就是我们真正调用的 PluginManager。
5.3.3 plugin-runtime
从 4.3.1 的插件运行流程图,我们可以看到 plugin-runtime 的加载是:PluginManager 通过 IBinder 去跨进程通知插件 Service 加载的,这个插件 Service,就是咱们在宿主中定义的继承 PluginProcessService 的 Service。
所以我们需要看一下 PluginProcessService 里面是如何加载 plugin-runtime 模块的
在调用DynamicRuntime.loadRuntime()
加载Runtime:
public class DynamicRuntime
public static boolean loadRuntime(InstalledApk installedRuntimeApk)
ClassLoader contextClassLoader = DynamicRuntime.class.getClassLoader();
RuntimeClassLoader runtimeClassLoader = getRuntimeClassLoader();
if (runtimeClassLoader != null)
String apkPath = runtimeClassLoader.apkPath;
if (TextUtils.equals(apkPath, installedRuntimeApk.apkFilePath))
//已经加载相同版本的runtime了,不需要加载
return false;
else
//版本不一样,说明要更新runtime,先恢复正常的classLoader结构
recoveryClassLoader();
try
//正常处理,将runtime 挂到pathclassLoader之上
hackParentToRuntime(installedRuntimeApk, contextClassLoader);
catch (Exception e)
throw new RuntimeException(e);
return true;
private static void hackParentToRuntime(InstalledApk installedRuntimeApk, ClassLoader contextClassLoader) throws Exception
//RuntimeClassLoader加载Runtime插件。
RuntimeClassLoader runtimeClassLoader = new RuntimeClassLoader(installedRuntimeApk.apkFilePath, installedRuntimeApk.oDexPath,
installedRuntimeApk.libraryPath, contextClassLoader.getParent());
//这是全框架中唯一一处反射系统API的地方。但这是Java层提供的API,相对来说风险较小。
Field field = getParentField();
if (field == null)
throw new RuntimeException("在ClassLoader.class中没找到类型为ClassLoader的parent域");
field.setAccessible(true);
field.set(contextClassLoader, runtimeClassLoader);
总结
从上面的流程可以看出,加载 plugin-runtime 模块,其实就是为了破坏宿主的 ClassLoader parent 引用链,将 RuntimeClassLoader 插入到宿主ClassLoader和其Parent的中间位置,最终修改后的父子关系如下:
–BootClassLoader
----RuntimeClassLoader
------PathClassLoader
通过这种方式,通过类加载的双亲委派的机制,宿主的 PathClassLoader 加载不到的类(如代理Activity),就回去parent查找,RuntimeClassLoader 则可以加载这个类。
5.3.4 plugin-loader
和 plugin-runtime 模块类似,plugin-loader 模块也是 PluginManager 通过 IBinder 去 PluginProcessService 加载的。
我们这里看一下 PluginProcessService 加载 plugin-loader 的逻辑:
void loadPluginLoader(string uuid) throws FailedException
...
//1.获取已安装的 plugin-loader
installedApk installedApk
installedApk = mUuidManager.getPluginLoader(uuid);
...
//2.加载 plugin-loader的映射接口 pluginLoaderlmpl
pluginLoaderlmpl pluginLoader = new LoaderimplLoaderlmplLoader().load(installerApk,uuid,getApplicationcontext();
pluginLoader.setUuidManager(mUuidManager);
//3.pluginprocessService 成员变量缓存 pluinloader
mPluginLoader = pluginLoader;
//获取操作 plugin- loader 的IBinder
IBinder getpluginLoader()
ruturn mPluginLoader;
我们先看一下 PluginLoaderImpl 这个类,它其实就是一个继承了 IBinder 的接口。在 运行流程图,我们可以看到 PluginManager 是通过 IBinder 方式去 PluginProcessService 获取到了 PluginLoader 的 IBinder,这样 PluginManager 就可以通过 PluginLoader 的 IBinder 直接对 plugin-loader 进行插件操作了,而 PluginLoader 的 IBinder 就是这个 PluginLoaderImpl 的实现,最后我们在看看 LoaderImplLoader 的 load() 方法是如何调用的。
今天有点事,事件有点赶,可能有点点乱,讲究看,后续:
- shadow优势
- shadow源码分析
- 框架介绍
关注公众号:初一十五a
解锁 《Android十三大板块文档》,让学习更贴近未来实战。已形成PDF版
内容如下:
1.2022最新Android11位大厂面试专题,128道附答案
2.音视频大合集,从初中高到面试应有尽有
3.Android车载应用大合集,从零开始一起学
4.性能优化大合集,告别优化烦恼
5.Framework大合集,从里到外分析的明明白白
6.Flutter大合集,进阶Flutter高级工程师
7.compose大合集,拥抱新技术
8.Jetpack大合集,全家桶一次吃个够
9.架构大合集,轻松应对工作需求
10.Android基础篇大合集,根基稳固高楼平地起
11.Flutter番外篇:Flutter面试+Flutter项目实战+Flutter电子书
12.高级Android组件化强化实战
13.十二模块之补充部分:其他Android十一大知识体系
整理不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔
以上是关于Shadow插件化系列简单详解的主要内容,如果未能解决你的问题,请参考以下文章
新方向 直面Shadow,零反射无Hook如何实现Android插件化?
Android 插件化Hook 插件化框架 ( 通过反射获取 “宿主“ 应用中的 Element[] dexElements )
Android 插件化Hook 插件化框架 ( 合并 “插件包“ 与 “宿主“ 中的 Element[] dexElements | 设置合并后的 Element[] 数组 )