在Shadow开源的代码中,首先分为core层和dynamic层。core层就完成了插件框架的所有功能,dynamic层又将插件框架动态化起来了。而后core层自己主要也分为两部分,一部分是loader相关的,一部分是manager相关的。其中loader就是解决插件框架核心功能的,好比将插件apk加载起来,将其中没有安装的Activity启动起来。而manager的功能就是管理插件包。这篇文章咱们就梳理一下manager在管理插件包相关的设计。Manager对于启动插件的过程管理,在另外的文章中再谈。android
咱们先假设没有Manager实现,单纯用core.loader,看看启动插件时须要提供什么参数。保证这个场景可用是咱们设计SDK时的一个原则。所以有一个test-none-dynamic-host
的半成品sample,目前只是验证这个场景是能够编译成功、启动成功的。将来还须要增强这个测试用例。git
加载插件的入口方法是com.tencent.shadow.core.loader.ShadowPluginLoader#loadPlugin
,这个方法接收的参数类型是com.tencent.shadow.core.common.InstalledApk
。因此InstalledApk就是交给Loader的插件全部描述了。github
InstalledApk是咱们设计的始终不变的结构体,它处于打包在宿主的common模块。dynamic层加载loader实现、runtime实现时采用的也是这个结构体。咱们能够看到InstalledApk只有4个变量:apkFilePath
、oDexPath
、libraryPath
和parcelExtras
。其中前3个是DexClassLoader加载apk所需的参数,肯定了这3个参数就能够加载apk到DexClassLoader中了。第4个参数parcelExtras
则是一个扩展字段,咱们将另一个Parcelable序列化后存储到这个变量中,达到动态扩展参数的目的。这个另外的Parcelable就是com.tencent.shadow.core.load_parameters.LoadParameters
。这个LoadParameters能够任意修改,只须要manager和loader同时更新就能够了。算法
须要注意的是InstalledApk这个类的名字代表了这是一个已经安装了的插件。“免安装”的插件是免于安装到系统,但仍是须要安装到咱们的插件框架管理器(Manager)中的。Manager对于插件的安装,也是仿照系统安装正常app来设计的。主要工做就是将插件apk复制到Manager管理的特别目录中,就像系统安装apk是也将apk复制到data目录中同样。而后也像系统同样,将apk中的so解压复制出来。因此,构造InstalledApk传给loadPlugin以前,Manager要将插件apk置于宿主的data目录中,再将插件apk中所需ABI的so也解压到宿主的data目录中。必须将插件apk放在宿主的data目录中是由于咱们的DexClassLoader只有权限加载宿主data目录中的文件。数据库
LoadParameters是插件的加载参数结构体。这是一个能够由动态实现的Manager和动态实现的Loader同时修改的类。这个结构体和宿主中的代码无关。目前LoadParameters中有4个参数:businessName
、partKey
、dependsOn
、hostWhiteList
。json
businessName
是业务名。Shadow容许一个Loader同时加载多个插件。只要这些插件没有so库冲突,这些插件能够是彻底不相关的业务的。Shadow经过ClassLoader设计保证了多插件间Java类是隔离的,可是无法为Native的so库划出单独的内存空间。除了代码有可能冲突,对data目录的使用也是有可能冲突的。由于Shadow的原理是“插件是宿主代码的一部分”,因此全部插件均可以访问宿主的data目录。若是插件和宿主或者多插件之间有使用相同的data目录逻辑,好比MultiDex会向一个固定的SharePreference持久化数据,就会出现持久化数据的冲突。为了解决这个问题,引入的businessName
。当businessName
为空时,Shadow就认为这个插件跟宿主是同一个业务,这个插件直接使用宿主的data目录。当businessName
设置了值时,Shadow就会在宿主的data目录中以businessName
为参数新建一个子目录做为插件的data根目录使用。这样相同businessName
的插件就会使用同一个data目录了,不一样businessName
的插件的data目录就至关因而隔离的了。c#
partKey
是插件apk的别名。由于插件apk的文件名是有可能由于带了版本号或者什么参数而变化,因此有这样一个partKey
做为一个插件apk的不变的别名。partKey
能够用于在表示一个插件依赖另一个插件时使用。Shadow内部在实现区分多插件逻辑时也会用partKey
做为该apk的Key。在Loader等接口上的partKey
参数指的也是这个partKey
。app
dependsOn
声明的是当前插件依赖哪些其余插件。指定依赖的插件,要填写插件的partKey
。假设插件A依赖插件B,Shadow会将插件B的ClassLoader做为插件A的parent。这样插件A就能够访问插件B的类了。具备dependsOn
声明的插件,它的ClassLoader是标准的双亲委派逻辑,不具有直接加载白名单中声明的宿主类的能力。代码体如今com.tencent.shadow.core.loader.blocs.LoadApkBloc#loadPlugin
中构造PluginClassLoader时传入的specialClassLoader
为null
。所以,插件A若是依赖了插件B,在插件A的hostWhiteList
中声明的宿主类是无效的。要将hostWhiteList
声明在插件B中才有效。这个问题有人反馈过:github.com/Tencent/Sha… ,值得优化。插件A依赖插件B,还应该能使插件A中的资源依赖插件B中的资源,这应该表如今构造插件A的Resource对象时,将插件B做为插件A的android.content.pm.ApplicationInfo#sharedLibraryFiles
。关于跨插件依赖资源,代码尚未上传,须要整理一下。请关注com.tencent.shadow.core.loader.blocs.CreateResourceBloc#create
方法的实现变化。框架
hostWhiteList
就是为了容许插件访问宿主的类而设计的参数。hostWhiteList
中设置的是Java类的包名。没有设置dependsOn
的插件会将宿主的ClassLoader做为parent,可是插件的ClassLoader不是正常的双亲委派逻辑。插件ClassLoader同时还将宿主的ClassLoader的parent做为名为specialClassLoader
的变量持有。插件的ClassLoader加载类的主路径是先尝试本身加载,本身加载不到,再用specialClassLoader
加载。当要加载的类处于hostWhiteList
中则采用正常的双亲委派,用parent(也就是宿主的ClassLoader)加载。这样设计的目的就是为了让插件和宿主类隔离,又能够容许插件复用宿主的部分类。ide
处于apk中的so不能直接运行,要先解压到data目录中才能运行。这不是插件框架的限制,正常安装的App也是同样的过程。正常安装一个App,系统会在安装时根据当前手机的ABI自动肯定一个合适的ABI,而后从apk中解压出指定ABI的so到data目录。因此插件框架在安装插件apk时也须要决定采用哪一个ABI。Shadow目前的设计,没有自动化这一过程,须要在继承的PluginManager子类上Override com.tencent.shadow.core.manager.BasePluginManager#getAbi
方法,返回须要解压的ABI。
插件的ABI不能像正常app同样任意决定,由于如今大部分手机都是64位的了,而Android系统不容许在一个进程中混用64位的so和32位的so。因此在64位仍是32位这个选择上,插件要和宿主保持一致。一个特殊的状况,若是宿主没有任何so,安装在64位手机上时,系统会认为这是一个64位应用。进而致使插件不能加载32位的so。这个问题,除了让宿主先加载一个32位的so以外,我尚未找到合适的解决方法。
解压so的方法是com.tencent.shadow.core.manager.BasePluginManager#extractSo
。能够在实现中看到so目录的肯定是根据UUID
肯定的,跟partKey
无关。这是由于咱们没有技术手段能支持在同一个进程中将so隔离开。因此在同一个进程中加载的多个插件的so,相互之间没有隔离,都至关因而宿主加载的so。所以,插件A依赖插件B中的so,不须要特别声明。插件A同插件B有冲突so,又须要在同一个进程中工做,也须要so的设计方自行解决。
config.json是一个能够插件包的描述。Manager经过com.tencent.shadow.core.manager.installplugin.PluginConfig#parseFromJson
方法将json转换为com.tencent.shadow.core.manager.installplugin.PluginConfig
对象,而后再经过com.tencent.shadow.core.manager.installplugin.InstalledDao#insert
方法将插件包描述的全部信息写入到数据库中持久化存储。在这个过程当中,将apk文件的相对路径转换成绝对路径。
config.json中有两部份内容,一部分是插件包的版本信息,另外一部分是插件apk描述。
跟版本信息相关的字段有:version
、compact_version
、UUID
、UUID_NickName
。
version
表示的是该config.json文件采用的格式版本;compact_version
表示的是当前config.json文件跟哪些格式的旧版本是兼容的,能够被支持旧版本格式的Manager使用。
UUID
表示的是插件包内容的版本,只有相同UUID
的apk才能一同工做。apk有3中类型:Loader、Runtime、Plugin。因此在同一个config.json中描述的的全部apk都具备相同的UUID
,因此能一同工做。UUID
是要按照通常UUID生成算法生成的,以保证屡次发布的插件版本不会重复。UUID_NickName
则是通常业务使用的版本名,对插件更新逻辑没有实际做用。
须要注意的是,多个config.json是能够采用同一个UUID
的。由于咱们有时须要分段下载插件包,下载一部分先启动一部分。因此能够将插件apk分到多个config.json中,采用相同的UUID
。而且,Loader和Runtime只须要存在于其中一个插件包中就能够了。
Loader、Runtime和Plugin的描述都有两个基本信息:apkName
和hash
。这个设计是为了便于将来实现即便UUID
不一样的config.json中也可能存在相同hash
,没有发生变化的插件。那么根据hash
相同,则能够决定跨UUID
复用本地已经存在的插件了。
对于Plugin来讲,还存在partKey
、businessName
、hostWhiteList
,做用如以前所说的,能够在这里设置这些参数。
为了方便直接生成插件包,无需手工填写config.json,咱们实现了一个生成插件包的Gradle插件。也就是Sample中看到的shadow {packagePlugin {}}
DSL。这样能够经过Gradle脚本动态填写config.json的一些参数。
执行packageDebugPlugin
任务时会自动生成UUID。若是要复用以前UUID,能够在插件包生成的build目录放一个uuid.txt
文件,将UUID指定在里面。这部分代码见com.tencent.shadow.core.gradle.extensions.PackagePluginExtension#toJson
。须要注意的是,若是在源码依赖的sample中这样固定了UUID,会致使更新代码的sample-plugin不能正常更新安装,由于它的UUID 老是不变的。
生成的插件包zip的文件名,能够经过PluginSuffix
环境变量添加后缀。代码见com.tencent.shadow.core.gradle.CreatePackagePluginTaskKt#createPackagePluginTask
。
关于这个Gradle插件,目前的实现主要仍是知足咱们业务的基本需求。看起来设计上是不够通用,也不是很健壮的。有几个简单的单元测试在projects/sdk/core/gradle-plugin/src/test
中。欢迎你们贡献代码。
其实仔细分析一下前面所讲的全部设计,会发现咱们不该该将config.json和全部apk一块儿打包在一个zip包中。这样作不到跨UUID复用apk。这是由于咱们在开发Shadow时,新旧插件框架同时运行,并且没有人力修改插件的发布系统。插件的发布系统一直是发布zip包的。因此Shadow在前面全部涉及的基础上,封装了一层从zip安装插件包的实现。这一实现将来修改时应该不会影响底层设计。所以咱们将来也会修改这一设计。