一个基于 SPI 的 Android 组件化框架
文章目录
一个基于 SPI 的 Android 组件化框架
详细的源码请移步 github:spi_component
首先,我们思考下什么是组件化,什么是插件化?什么时候我们用组件化,什么时候我们用插件化?
什么是组件化,什么是插件化。
所谓的组件化就是将整个项目拆分为几个组件,单个组件高内聚,组件间低耦合,这些组件拼到一起,编译成一个目标 app
。
所谓的插件化就是将整个项目拆分为几个模块,分别打包编译,在主 app
需要用到某个模块的时候
按需加载这些模块。
什么时候我们用组件化,什么时候我们用插件化
这是一个策略问题,用何种方式需要根据实际情况来。
刚刚说了,组件化是将项目拆分为几个组件,单个组件高内聚,组件间低耦合。所以复用会比较方便,如果公司产品多可以考虑这种方式。
另外,组件化框架一般会允许组件单独编译,单独调试,由于组件一般不会太大,所以编译调试单个组件的时间较整体编译会少很多。而且组件化方式编译的 app 是常规模式编译的,可以较好的适配新的 Android 版本,相较插件化更稳定。
插件化是将整个项目拆分为几个模块,和组件化类似,有相似的优势,但是一般插件化方案都涉及 Hook 技术,而这些技术随便 Android 版本迭代,可能会被封杀,存在一些适配的风险,可能会不稳定。
随着我们的业务发展,我们的主工程越来越复杂,不得不考虑组件化和插件化来简化我们的开发
工作,考虑到稳定性,我们决定改造我们的工程,后续切换到组件化开发。
组件化的一些思考
内部需求
在改造的时候,我们采集了一下内部对于组件化需求:
- 学习成本低,希望比较容易上手(接入简单)。
- 现有的代码改动少,调用方式最好不变。
- 组件之间互相调用(希望可以直接使用其他组件的资源,相对显式打开其他组件的页面等)方便、高效。
- 希望不要引入太多无关的代码。
- 希望新增组件的方式简单点。
开源方案待解决问题
参考一些开源的组件化方案,有很多借鉴的地方,但是感觉还是有些不是很符合我们的需求,比如:
- 组件间的资源无法较好的使用,Activity 的跳转还好,但是涉及到 Fragment 的调用的时候,比如组件 1 需要使用组件 2 里面的 Fragment 的时候就不是很方便了,基本无解。
- 有些组件的初始化需要放到 Application 里面,或者需要使用 Application 里面的一些方法,主流的组件化方法感觉对这块支持不好,主 App 维护一个 Application , 组件作为单独 App 的时候也需要维护一个 Application ,修改是相对繁琐。
- 组件的单独编译和作为 library 库编译方式不灵活,主要表现为,想单独编译的时候,需要修改脚本,切换为 library 库的时候也需要修改脚本。
架构介绍
模块介绍
鉴于以上的一些特点,我们希望可以在现有的基础上做一些优化,希望组件化开发更简单,更方便。
于是,我设想了这个基于 SPI
的组件化框架。框架的大致架构如下:
模块名称 | 作用 |
---|---|
core | 框架的核心,就是 app 的骨架,定义了各个组件对外暴露的部分。可通过 getXXX 方式暴露出去。而且比较灵活, Fragment 资源图片等都可以暴露。 |
app |
app 的承载容器,就是对外发布的时候,用这个项目来编译,包含所有的组件 |
compx | 某个组件,依赖 core ,实现 core 里面这个组件对外暴露的业务逻辑等。 |
single-alone/compx-alone | 组件单独调试编译入口,就是编译出来的 app 只有这一个组件实现了。 |
core
模块定义了各种组件承担的业务和对外暴露的一些资源等,主要是通过接口暴露。
同时提供各个模块的默认实现。
compx
模块是一个组件的实现,实现的是 core
模块里面定义的的组件,需要依赖 core
模块。
app
模块,就是我们对外发布的 app
,把所有的组件放到一起编译。 依赖 core
和所有的 comp
模块。
single-alone/compx-alone
某个组件的入口 app
,即依赖 core
和某个 compx
模块,
方便单独调试某个模块。
组件是如何工作的
-
一个组件需要在
core
模块里面定义好,我们暂用ICompXService
来表示,其实就是一个接口,然后这个接口可以定义这个组件想对外暴露的东西,比如可以获取某个值(getXXX),可以获取一个 Fragment (getXXXFragment) 等。 -
新建一个
compX
模块,依赖core
模块,然后实现刚刚在core
模块里面定义的ICompXService
接口,我们记为CompXServiceImpl
,这样我们就有了一个组件。 -
在
core
模块的AppTool
类里面定义一个ICompXService
,这样我们就有了一个组件,当AppTool
类被加载的时候,通过SPI
,我们就可以把compX
里面实现的CompXServiceImpl
和刚刚定义的ICompXService
绑定起来。(SPI
的原理大家可以参考
Java SPI机制原理和使用场景) -
app
模块同时依赖core
和compX
模块,然后通过core
模块的AppTool
类,我们可以就可以使用CompXServiceImpl
了。
组件的原理大概就是这样的。
疑难点是如何解决的
1 组件直接使用其他组件的 Fragment 等其他资源
上面介绍了, compX
通过 core
里面的 ICompXService
定义的,提供了对外的接口,如果有 Fragment
需要对外暴露,直接在 ICompXService
里面定义一个方法暴露就好了,然后实现的时候,暴露出需要暴露的 Fragment
就好了,其他组件通过 AppTool
类拿到 ICompXService
时候,就可以通过接口暴露的方法使用 Fragment
了。
2 组件的初始化需要放到 Application 里面
有时候,我们需要在 Application
初始化一些资源,一般情况下,我们的做法是,在主 app 里面的 Application
里面添加初始化的代码,然后在组件 app 的 Application
里面添加初始化的代码。
这样有一个问题就是如果需要改动代码,主 App 和组件的 App 里面都需要修改,同时,主 App 的 Application
代码是比较复杂的,因为主 App 里面包含所有的组件,在修改主 App 的 Application
的风险是比较大的。
为了解决这个问题,我们的做法是抽象出来一个类 XApplication
,这个类和 Application
类类似,把主要的方法,比如 onCreate
方法提取出来,通过 ICompXService
的 getApplication
方法,在初始化 AppTool
的时候,就可以拿到组件的 XApplication
,然后,在 App
定义的 Application
里面的回调方法里面同步调用组件的 XApplication
里面对于的方法。
通过这种方法,我们可以仅在组件里面处理自己的初始化逻辑就好了。
可能碰到的情况是组件的初始化有先有后,有的初始化依赖某个组件初始化完成了才可以初始化。这个问题暂时考虑会在 XApplication
里面通过一个优先级的参数来表示这个组件初始化的顺序,在 App 的 Application
方法回调的时候根据优先级来依次回调 XApplication 里面的方法。
3 便捷调试某个组件
前面提到了组件作为 library 库的时候和单独调试的时候,都需要修改编译脚本,不是很方便。所以我们给出的方法是,单独新建一个 app 模块,作为单独调试组件的入口,在需要单独调试的时候,我们直接运行这个入口 app 模块就好了。当我们需要对外发布的时候,我们编译主 App 就好了。
4 如何新增一个组件
新增一个组件的话,需要做如下的事情,
- 在
core
里面定义这个组件,也就是继承ICompService
接口,我们记为ICompXService
。 - 在
core
模块的AppTool
类里面注册这个ICompXService
。 - 新建一个
library
,依赖这个core
模块,然后实现ICompXService
。 - app 模块或者组件独立入口
app
模块依赖这个新建的library
。
经过上面四步,新增一个组件完成了,中间不需要特别修改编译脚本,只需要修改一些引用库之间的关系。
一些需要注意的事项
- core 模块会通过 SPI 来加载组件的实现,也就是组件 library 里面实现 ICompXService 接口
的类,所以组件 library 里面要按照 SPI 的要求新建入口文件。