🔌 iOS 上的插件化设计

前言

iOS 社区这两年越来越多底层向 (涉及到汇编 / llvm) 的东西, 关于 工程化 相关的讨论会少一点,底层东西并非不好,只是作为屠龙刀的角色,一般只在特定场景去发挥优化作用。

对于广大 iOS 开发者,工程和业务却是每天需要打交道的地方,拆分业务也属于必不可少的事项,插件化作为解耦手段之一,对于每个公司和团队,方案可能都略有不同。

业界关于此类的文章也较少,经过搜索只发现了一篇 《优酷 iOS 插件化页面架构方案》 ,现结合自己经验理解在此抛砖引玉。

效果演示

目前暂不提供完整方案代码,后续可考虑把简单 Demo 放出来。

文章应该把思路和关键实现说得比较清楚,要自己实现应该也不太难。

虽然不能 show code ,但有图有真相,可以先看看插件化的实现效果。

增加插件

直接编写好对应的 plugin.h/plugin.m ,就能无侵入地增加一个新插件和进行逻辑交互:

删除插件

将要插件的实现文件直接删掉,就能无耦合的拔除插件:

插件化是什么

要理解插件化,就要先理解 插件 的概念。

稍微区别于 “组件”,插件的粒度会更小一些,如果说每一个组件是工程中的某一个模块,那每一个插件就可以是业务模块中的子功能。

作为插件来说,它的特点一定是 可拔插 的。

具体的表现就是,插件的引入和删除都是对现有业务无侵入的,或者微小侵入的,不会对现有业务造成影响

插件的拆分上要满足 单一职责,通过各个不同的插件,来提供和完成不同的功能。

在命名上,可能有的叫 Widget,有的叫 Plugin 等等,这里暂把它称作 Plugin 。

插件化 就是通过不同的 插件 来组织业务模块:

image-20210424100619149

插件化的由来

关于为什么要做插件化,具体下来应该有 2 个问题:

首先是为什么要做 业务拆分 的问题 ,然后在此基础上,才是为什么业务拆分需要 插件化 的问题。

一个业务本来不需要拆分,那么也没有去做插件化的必要了,不用去做过度设计。

为什么要做业务拆分

关于为什么要做业务拆分,我目前的理解是:业务复杂不是问题,业务复杂造成难以维护的结果才是问题

对于一个复杂的业务,设计模式和架构的作用是去将它 有序 的组织起来,更好管理,脱离无序混乱 的组织状态,却不会减少项目本身的功能和交互。

想要解决问题,做到可维护性高,那么就要做到:

  • 减少依赖,互相间关系越简单越好
  • 分工明确,便于专注某一功能的开发和测试
  • 代码功能便于复用

根据我有限的开发经验来说,首先需要整理依赖,先垂直上下分层,公用功能聚合下沉,业务逻辑上移。然后是去除横向依赖,这个过程又会有一次功能的的聚合与拆分。而各种设计模式和架构就是去帮助我们做到这些事情。

为什么业务拆分要用到插件化

记得刚入行时,有不少与 UIViewController 瘦身 相关的讨论,比较有印象的是这篇 《被误解的 MVC 和被神化的 MVVM》

除了有使用 Controller 分类 / 独立 UITablieView 的 DataSource 等手段,常见的手段就是用各种 Manager 专门管理某一个功能逻辑。随着项目逐渐迭代和复杂,尽管用了拆分手段,代码同样会越来越难维护。

最大问题往往还是 耦合,因为依赖太多,其它地方引入也会粘连到不必须的代码,本无复杂的功能,却需要继承很多不是必须要使用的代码,让项目的维护变困难。

常见的一种情况是 多重继承 带来的依赖和层级问题:开发人员想要复用功能,一般就会采用继承,当继承层级变多,对父类的修改又会影响子类及相关类,功能将出现问题。如果不想影响过多,就会直接 复制粘贴 ,成为一个 CV 工程师 ( 不用问,问就是我也这么干过😂 )。

插件化要达到目的,就是在拆分模块业务的基础上,同时解决 耦合问题 ,利用组合插件的方式管理业务模块。

插件化的设计思路

下面开始进入插件化设计的实践部分。

插件化围绕某个业务对象进行

插件逻辑都是围绕某个具体业务做拆分。

进行插件化改造,会有一个业务对象当作是 Container (容器),是整个插件化业务的入口和中心。

如果是有组件化经验的同学,可以把 Container 当作在组件化中壳工程的角色。

插件化的工作目的,就是将业务进行拆分和组织,就如把集团拆分成事业群,各自独立管理自己的业务进行发展,同时也能协作。

在 iOS 日常工作中,Container 对象的类型基本都属于 UIViewController 或者 UIView,因为很多复杂的交互与逻辑都会写在它们当中(尤其是 UIViewController ),但是 并不代表 Container 就被限定在 UIViewController/UIView 二者的类型当中,其它的对象类型和业务也同样可以运用插件化的思想。

插件化机制的整体结构

先说明整体的结构关系,来看看是如何利用插件把代码组织起来的:

  • 箭头从 A ->B,表示 A 被 B 引入依赖。

  • 实线条,表示有物理文件的引入依赖。

  • 虚线条,表示没有物理文件引入,但会使用到。

  • 圆角矩形,代表实例对象

  • 椭圆形,代表协议

    结构关系如下图:

这里对有 5 个地方进行说明:

  • ContainnerProtocol ,作为容器对外提供的接口,它没有依赖其它地方,被 Container 和 ContainnerProtocol 所依赖。
  • Container ,作为插件化的业务对象,Container 对象要实现 ContainnerProtocol 。
  • PluginProtocol ,定义插件通用的方法,比如生命周期和注册事件等方法。
  • PluginManager,作为插件管理器来对插件进行加载和管理,将具体的插件与 Container 连接起来。
  • 各种 业务 Plugin,也就是写业务代码的地方。如果把虚线去掉,就会发现 业务 Plugin 是没有被其它地方依赖 的,业务方对于业务插件的迭代修改/扩展/删除都是非常简单的。

插件化问题的难点 - 交叉依赖

设计真正走到实施阶段,动作往往会变形。

前面有提到,我们的设想是通过 组装 的方式,直接将不同的插件合成一个模块,插件互相之间应该要没有依赖。

日常中多数的依赖问题都是依赖实例,根据 面向接口编程 的原则,在 OC 中常常利用协议来进行调用,达到 去除实体依赖 目的。

然而使用抽象协议代替实例后,仍会存在问题:

用协议代替实例时,没有依赖实体对象,替换 的确会更加容易,但业务进行删除协议时,使用到协议的地方都要去找出并进行删改,也会带来不少成本。当我们要移除某一个插件,在关联方中删除它的工作量也是客观存在的。

将实例改成利用协议来做逻辑,本质上还是要依赖于某个抽象,最后也无法避免下面的情况:

造成此类情况的原因很多,横向依赖不知不觉就产生了,或是因为没有一定的规约,或是因为业务开发执行问题。

而作为一个好用的插件来说,必须要做到 热拔插 的能力:

要用的时候插电⚡️为我们提供功能,不用的时候直接拔走❌,增加/删除一个插件都不会对现行业务产生影响。

因此对于插件化来说,首要做的就是 2 件事:

  • 插 - 无侵入增加业务插件
  • 拔 - 无痛的删除业务插件(去除业务插件间交互的横向依赖)

简单的说,我们要将上面搅成一团的混乱线缆拆开,使其变成有序的状态:

image-20210618223349433

插件注册

对于无侵入增加业务插件,主要在插件注册生成时做文章。

插件注册通过 3 步顺序来进行介绍:

  1. PluginManager 如何与 Container 建立关联
  2. PluginManager 如何对 Plugin 进行注册加载
  3. 如何增加一个新的业务 Plugin
PluginManager 如何与 Container/Plugin 建立关联

站在 Container / PluginManager /业务Plugin 3 个层面上看,插件化整体的启动与注册流程如下:

整个流程里,PluginManager 和 业务 Plugin 都对 Container 对象做了持有,其实持有 Container 对象也不是必须的,只是日常往往会用到 Container 的实例或者方法,例如拆解 UIViewController 业务,业务 Plugin 会用到如 UIViewController.view 进行布局等。如果用不到,不持有也是可以的,实际当中可以灵活运用。

业务 Plugin 如何做到无侵入的注册加载

前面说到了,增加 Plugin 最好要做到对业务无侵入。

PluginManager 在进行 Plugin 实例化时,没有直接引入业务 Plugin 相关的头文件,主要用到了 一个关键的手段:

利用 Plist 文件来生成 Plugin 。

在 PluginManager 注册加载业务 Plugin 的流程如下:

业务 Plugin 生成的主要逻辑:

如何增加新的业务插件

要增加一个新的业务 Plugin 很简单,只需要 2 个步骤:

  1. 实现业务 Plugin ( XXPlugin.h / XXPlugin.m )
  2. Plist 新增一个 Plugin 对应的字典
实现业务 Plugin

在上面 PluginManager 的生成 plguin 代码中,声明的 plugin 是一个实现了 IHZGameBasePlugin 协议的对象,新增的业务 plugin 都需要去实现 PluginProtocol

另外生成 plugin 利用到了 BasePlugin 中统一的类方法去创建实例对象,新增的业务 plugin 都需要继承 BasePlugin

因此,实现业务 Plugin 要注意 2 点:

  • 新 Plugin 要实现统一遵守的 PluginProtocol
  • 新 Plugin 需要继承 BasePlugin

一个新增业务 Plugin 的简单实现的示例:

Plist 文件里新增 Plugin 字典

添加完 Plugin 的 .h.m 文件,再去 Plist 文件的数组中,根据插件信息增加一个字典对象:

示例中使用的是 Plist 文件,主要考虑在 Xcode 里可以有比较好的格式显示,更直观。

实际上也可以使用 json 或者另外方式,适合自己的方式才是最好的。

至此也就解决了插件化的第一个问题:无侵入增加业务插件

插件间的通讯 - 解决插件间横向依赖

上面有说到使用协议代替实例,却仍旧做不到 无痛删除 ,究其原因还是 业务上发生了耦合 ,当调用代码被散落在各处,去除时也需要一个一个地找到。

依赖抽象也是依赖,如果依赖的协议功能被删除,那么手动去删除关联代码也是不可避免的工作。

现在来看,利用 事件机制 能比较好地做到解耦工作,使用事件机制有 2 个明显的好处:

  • 解决耦合,插件之间也不用互相关心。例如插件 A 要触发插件 B 的逻辑,可以通过事件机制发出一个事件 EventX,而不是直接在 A 中调用 B 。
  • 单独迭代,每个插件的业务方可以直接修改自己的逻辑。这样团队的协作也更有组织性,减少维护和沟通成本。

事件机制的实现 - 观察者 or 订阅发布 ?

关于实现事件机制,很容易想到使用 观察者 的设计模式,它有 3 个关键点:

  • 观察对象与观察者具有 一对多 的关系。
  • 当观察对象发生改变,观察者(订阅者)就可以接收到改变。
  • 观察者(订阅者)如何处理逻辑,观察对象无需关心,它们之间是松耦合的。

然而在开发复杂项目业务时,往往还会使用 发布-订阅模式 的机制来做实现。

相对于观察者模式的实现,发布订阅模式多出一个 中介者 ,有些类似中介者模式的思想,所有的订阅者和发布者,统一通过中介者进行订阅事件和派发事件:

发布订阅模式观察者模式 的主要区别是:

  • 从角色来说,发布订阅模式多出了一个中间者作为调度中心,来专门管理事件。
  • 从耦合的角度看,观察者模式中的观察者/被观察者的关系是 松耦合,发布订阅模式中的发布者/订阅者是完全 无耦合 的。
  • 从关注点出发,观察者模式是 2 者间直接交互,更关心 数据源 ,发布订阅模式则更关心 事件 消息。

综合当前业务场景,在插件化设计中,采用 发布订阅模式 是更合适的。

Note:

关于观察者模式和发布订阅模式的具体差异,我个人认为是观察者模式在 处理复杂情况 的一种解决方式,加入一个中间层而已,不用过度纠结。

不是有这么一句话么:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。

事件机制具体实现

利用 发布订阅模式来实现事件机制,主要工作是完成一个带有 发布&订阅 功能的中间者,先把它叫做 EventDispatcher。

EventDispatcher 需要对外提供的功能:

  • 订阅事件
  • 发布事件
  • 取消事件的订阅

发布事件进行通知订阅对象时,一定需要知道 3 个关键的东西:

  • 事件
  • 订阅对象
  • 响应方法

发布前就需要将 3 个关键要素存储起来,在进行订阅的动作时,也至少要保证传入这 3 个参数。

目前采用的是 NSMapTable 来做存储,它的一个特性就是可以弱引用对象作为 key/value,键值对的一方对象释放后就会被自动删除,对事件机制实现的存储结构如下:

进行 事件订阅 的逻辑处理流程如下:

进行 事件发布 的逻辑处理流程如下:

如何通过事件交互

插件之间通过事件来进行交互,主要有 4 个步骤进行:

  • 声明事件

  • 订阅事件

  • 派发事件

  • 处理事件

通过事件机制,某一个插件本身不用去关心和依赖其它插件,插件发生变化直接把事件或者状态抛出去,由关心事件的订阅者自己去自行响应处理。

例如同样是一个涉及更新的动作,平常的方式可能是:

1
2
3
4
5
//C.m
if (needUpdate){
[A update];
[B update];
}

通过事件的方式则是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 //C.m
if (needUpdate){
[self dispatchEvent:kAllPluginNeedUpdate];
}

// A.m
- (void)handlePluginUpdate
{
[self update];
//doSomething
}

// B.m
- (void)handlePluginUpdate
{
[self update];
//doSomething
}

至此,插件间的横向依赖问题也解得到了解决,可以做到无痛的删除插件,而不影响业务。

总结

插件化设计最重要事情是解除耦合,而无侵入引入插件和无痛删除插件,都属于检验耦合程度的手段。

相对于性能优化,架构的升级总是相对滞后,无法通过数据去直接观察,也无法直观体现成果。想了一想,我觉得有一句话可以来形容好架构:

善战者,无赫赫之功。

没有完美的设计,只有合适的设计,希望大家可以多多提意见,互相学习。

参考

事件处理机制

《观察者模式 vs 发布订阅模式》

《优酷 iOS 插件化页面架构方案》