Shadow对插件包管理的设计

在Shadow开源的代码中,首先分为core层和dynamic层。core层就完成了插件框架的所有功能,dynamic层又将插件框架动态化起来了。而后core层自己主要也分为两部分,一部分是loader相关的,一部分是manager相关的。其中loader就是解决插件框架核心功能的,好比将插件apk加载起来,将其中没有安装的Activity启动起来。而manager的功能就是管理插件包。这篇文章咱们就梳理一下manager在管理插件包相关的设计。Manager对于启动插件的过程管理,在另外的文章中再谈。android

InstalledApk

咱们先假设没有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个变量:apkFilePathoDexPathlibraryPathparcelExtras。其中前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

LoadParameters是插件的加载参数结构体。这是一个能够由动态实现的Manager和动态实现的Loader同时修改的类。这个结构体和宿主中的代码无关。目前LoadParameters中有4个参数:businessNamepartKeydependsOnhostWhiteListjson

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参数指的也是这个partKeyapp

dependsOn声明的是当前插件依赖哪些其余插件。指定依赖的插件,要填写插件的partKey。假设插件A依赖插件B,Shadow会将插件B的ClassLoader做为插件A的parent。这样插件A就能够访问插件B的类了。具备dependsOn声明的插件,它的ClassLoader是标准的双亲委派逻辑,不具有直接加载白名单中声明的宿主类的能力。代码体如今com.tencent.shadow.core.loader.blocs.LoadApkBloc#loadPlugin中构造PluginClassLoader时传入的specialClassLoadernull。所以,插件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

关于so文件

处于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的设计

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描述。

跟版本信息相关的字段有:versioncompact_versionUUIDUUID_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的描述都有两个基本信息:apkNamehash。这个设计是为了便于将来实现即便UUID不一样的config.json中也可能存在相同hash,没有发生变化的插件。那么根据hash相同,则能够决定跨UUID复用本地已经存在的插件了。

对于Plugin来讲,还存在partKeybusinessNamehostWhiteList,做用如以前所说的,能够在这里设置这些参数。

插件包生成Gradle插件

为了方便直接生成插件包,无需手工填写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中。欢迎你们贡献代码。

为何咱们的插件包是个zip包?

其实仔细分析一下前面所讲的全部设计,会发现咱们不该该将config.json和全部apk一块儿打包在一个zip包中。这样作不到跨UUID复用apk。这是由于咱们在开发Shadow时,新旧插件框架同时运行,并且没有人力修改插件的发布系统。插件的发布系统一直是发布zip包的。因此Shadow在前面全部涉及的基础上,封装了一层从zip安装插件包的实现。这一实现将来修改时应该不会影响底层设计。所以咱们将来也会修改这一设计。

相关文章
相关标签/搜索