大数跨境
0
0

知乎 iOS 插件系统实践

知乎 iOS 插件系统实践 知乎技术专栏
2025-08-08
0
导读:知乎技术专栏第 16 篇

知乎技术专栏第 16 篇


1.  框架简介

ZHPluginSDK 是一个功能强大的通用化插件系统,其基于 Swift 进行开发,采用了特殊的插件注册和消息派发机制,使其可以在系统内高效派发任意数据结构。借助于伴生设计的视图管理系统,可以快捷高效的组合构建大型业务的复杂 UI。

ZHPluginSDK 还提供了高低多个维度的插件容器,开发者可以基于业务拓扑结构随意创建多个独立插件或容器,再借由插件系统将其动态聚合为一个复杂业务。

1.1  功能特性

  • 插件基于类型注册,对外隐藏插件实例对象,防止外部引用,强制耦合

  • 插件消息可携带任何类型的数据

  • 插件消息在传递过程中不会擦除数据类型,监听方无需进行类型判断和转换,可直接使用

  • 除了异步消息,ZHPluginSDK 还支持同步消息机制

  • 基于运行时实现了消息的隐式注册和精准派发

  • 利用 propertyWrapper 构造语法糖,实现插件消息的优雅监听

  • 消息拦截转发器

    • 消息拦截(丢弃)

    • 消息变形(收一发一)

    • 多消息派发(收一发多)

  • 基于插件的树状拓扑结构,提供类似 SwiftUI 的环境变量机制

  • UI 插件的视图管理机制

  • 自定义插件优先级

  • 独立插件可以利用 MessageRealy 轻量化地为子组件提供消息监听和发送能力

  • 使用 ContainerComposer 为大型业务提供多插件容器的聚合机制

1.2  架构概览


1.2.1  简单系统

简单业务场景下,单一容器配合多个插件实现业务开发

  • 各个业务模块注册为 Plugin

  • RootViewController构建 PluginContainer 管理多个业务插件,处理消息交互

  • 创建并注册一个 PluginViewContainer 负责管理插件的视图插槽请求,处理全局布局逻辑

  • 可选创建 MessageInterceptor 负责监听消息,用于记录全局日志或处理导航栈等需要 VC 处理的逻辑

1.2.2  复杂系统

在复杂业务场景下,例如直播间、文稿、电子书等业务,可能同时存在多个业务复杂的模块,如果在一个容器中全部平铺为 plugin,不利于业务代码的组织和隔离,且在处理插件热插拔时需要手动管理关联插件。借由多个 PluginContainer 和 ContainerComposer 可以实现树状的业务拓扑结构,并实现多个独立模块的灵活聚合。


2.  架构设计

2.1  ContainerComposer 多容器组合依赖

2.1.1  设计动机

目前的设计架构下,一个业务场景应该只使用一个  PluginContainer 控制多个  Plugin 来实现业务功能的插件化。如果在复杂业务中,存在大量的子组件,而这些子组件之间也存在模块划分,隶属于多个层级,那单一插件容器在管理上就会出现一些弊端:

  • 单一容器中的插件在概念上都属于平级,无法很好描述业务模块的细粒度划分和上下层级

  • 一个容器仅有一个 ViewContainer 负责插件视图布局,插件较多的情况下,视图调度代码会很繁杂


2.1.2  多容器组合机制示意图

  • 多容器组合

  • Plugin 发送消息流转

  • PluginContainer 发送消息流转

PluginContainerComposer 发送消息流转


2.1.3  机制使用及实现


使用示例:

// MARK: - TestMessagestruct TestMessage: PluginMessage {}func test() {    let composerA = PluginContainerComposer()
    let containerB = PluginContainer()    let composerB = PluginContainerComposer()
    composerA.compose(containerB)    composerA.compose(composerB)
    composerA.send(message: TestMessage())}

PluginContainerComposer 和 PluginContainer 均继承于 ComposibleContainer, 因此以上两种 Container 可以以组合形式进行嵌套聚合。可以将 Composer 看做 Folder,Container 看做 File。

// MARK: - ComposibleContainerprotocol ComposibleContainerAnyObject {    var containerAssociatedKey: ContainerAssociatedKey? { get set }    func receive(containerMessageany PluginMessagecontainerIdString)}extension ComposibleContainer {    var id: String { objectIdentifier(self) }}
// MARK: - PluginContainerComposer/// A special plugin container that can compose multiple `ComposibleContainer`s to relay messages among them.public final class PluginContainerComposerComposibleContainer {    private var composedContainers: [ComposibleContainer= []
    var containerAssociatedKey: ContainerAssociatedKey?
        func compose(containers: [ComposibleContainer]) {        for container in containers {            if id == container.id {                assertionFailure("The container composer cannnot compose itself.")                return            }
            let deallocHook = DeallocHook { [weak selfweak container] in                guard let selflet container else { return }
                self.composedContainers.removeAll(where: { container.id == $0.id })            }            container.containerAssociatedKey = .init(associatedContainer: self, deallocHook: deallocHook)            composedContainers.append(container)        }    }
    func receive(containerMessagePluginMessagecontainerIdString) {        for container in composedContainers {            if container.id == containerId { continue }            container.receive(containerMessage: containerMessage, containerId: id)        }        containerAssociatedKey?.associatedContainer?.receive(containerMessage: containerMessage, containerId: id)    }}extension PluginContainerComposer {    public func send(messageany PluginMessage) {        receive(containerMessage: message, containerId: id)    }
    public func compose(_ containersPluginContainer...) {        compose(containers: containers)    }
    public func compose(_ multiContainersPluginContainerComposer...) {        compose(containers: multiContainers)    }}


2.2  MessageRelay 消息中继

2.2.1  设计动机

现有的消息监听机制是利用 MessageObserver 实现的,其限定了只有插件本身才能接收到插件消息,在设计上是为了简化监听流程和保证精准派发,但是有些插件本身不够细化,可能存在较大的业务功能,其内部的层级的组件无法监听插件消息响应事件,只能通过 Plugin 进行消息透传,一定程度上增加了代码复杂度。

2.2.2  MessageRelay 机制示意图

2.2.3  机制使用及实现

MessageRelay 是一个 Protocol, 任何 Object 都可以遵守此协议成为一中继点,Plugin 可以使用收到的 MessageSender 将任意一个内部遵守 MessageRelay 的实例作为一个中继点注册到 PluginContainer 中去。

/// MessageRelay Protocol/// Every conforming types can receive plugin messages with the `MessageObserver`, just like a plugin.public protocol MessageRelayAnyObject {    func messageRelayConnected(messageSenderany MessageSender)}
/// MessageSender Protocolpublic protocol MessageSender {    /// A convenience observer for bridgin to RxSwift/ReactorKit seamlessly.    /// Every `PluginMessage` signal bound to this `publisher` will trigger the `send(message:)` method.    var publisher: AnyObserver<any PluginMessage> { get }
    /// Sends a plugin message.    func send(messageany PluginMessage)
    /// Relays messages to a message relay.    /// - Note: The `messageRealy` won't be retained.    @discardableResult    func relayMessages(to messageRelayany MessageRelay) -> Result<VoidError>}
  • MessageRelay 不具备 Plugin 同样的消息优先级。

  • 消息中继对象不会被 PluginContainer 强引用,中继对象在释放后会利用 DeallocHook 机制立刻从 MessageCenter 中的注册表中移除。

  • 同一个中继对象无法在同一个容器内被添加多次,后续重复添加会被忽略

var deallocHookKey = "ZHPluginSDK_deallocHookKey"// MARK: MessageSenderextension InnerMessageSenderMessageSender {    var publisher: AnyObserver<any PluginMessage> { messageObserver.asObserver() }
    func send(messagePluginMessage) {        messageCenter?.send(message: message, fromContainer: false)    }
    @discardableResult    func relayMessages(to messageRelayMessageRelay) -> Result<VoidError> {        guard let messageCenter else { return .failure("Never") }
        lock.lock()        defer { lock.unlock() }
        let relayIdentifier = messageRelay.id        if messageCenter.messageRelays[relayIdentifier] != nil {            return .failure("Has already been relayed")        }
        // Removes the message relay from the cache map when it releases.        let deallocCallback = DeallocHook(handler: { [weak selfin            self?.lock.lock()            self?.messageCenter?.messageRelays.removeValue(forKey: relayIdentifier)            self?.lock.unlock()        })        objc_setAssociatedObject(            messageRelay,            &deallocHookKey,            deallocCallback,            .OBJC_ASSOCIATION_RETAIN_NONATOMIC        )
        messageCenter.messageRelays[relayIdentifier] = WeakBox(messageRelay, policy: .weak)
        messageRelay.messageRelayConnected(messageSender: self)
        return .success(())    }}

使用示例

import Foundation// MARK: - TestMessagestruct TestMessagePluginMessage {}// MARK: - PluginAclass PluginAPlugin {    @MessageObserver    var testMessage: TestMessage?
    let component = ComponentB()
    required init?(paramsAny?) {}
    func pluginInitialized(        messageSenderMessageSender,        nameString?,        slotViewAccessor@escaping (String?) -> PluginSlotView?    ) {        messageSender.relayMessages(to: component)    }
    func queryView(with viewIdString) -> UIView? { nil }}// MARK: - ComponentBclass ComponentBMessageRelay {    @MessageObserver    var testMessage: TestMessage?
    let component = ComponentC()
    func messageRelayConnected(messageSenderMessageSender) {        messageSender.relayMessages(to: component)    }}// MARK: - ComponentCclass ComponentCMessageRelay {    @MessageObserver    var testMessage: TestMessage?
    func messageRelayConnected(messageSenderMessageSender) {}}func test() {    let pluginContainer = PluginContainer()    pluginContainer.addPlugin(PluginA.self)}


2.3  同步消息机制

2.3.1  设计动机

但是在特殊场景下,一些同学会有强诉求想要发送的消息可以收到同步响应。

  • 一个组件想要查询系统中的某个状态,此状态变更频繁,不宜广播

  • 发送消息调用某一功能,需要基于响应做逻辑判定 (跳转路由,查询登录状态)

此功能在设计之处,并非没有考虑,但其实现的方法和效果有多种弊端,并且此种特殊场景也是可以利用现有的机制进行一些兼容,虽然不甚优雅,但也算可用,推荐的兼容方式有以下两种:

利用 Message 的数据灵活性,在 Message 中添加 callback 回调 m

优点:

  • 实现简单,使用方便

缺点:

  • 多个消息监听方都会返回数据

    消息监听方可以多次重复调用 callback

    消息监听方在监听到消息以后,可以不做响应

将同步消息转换为一个EnvironmentValue ,将调用封装为一个可选的 block 属性

优点:

  • 调用方主动调用,与普通的同步函数相似,只会有一个返回值

缺点:

  • 环境变量可以被任意容器重载,调用方无法感知是哪个插件实现

    同样因为方法会被任意重载,所以真实的监听方无法避免其实现被其他模块误修改


2.3.2  功能指标

  • 同构发送:与异步消息机制同构,任何插件和容器均可发送和监听异步消息

  • 强类型自定义响应值:自定义消息指定响应值类型

  • 多回复机制:同步消息也可以被多处监听,调用者可以以数组的形式同步获取多个响应结果

  • 指定回复机制:发送方可灵活控制发送策略

    • 基于插件类型和名字,指定特定插件响应

    • 同容器响应,仅对发送方所处 PluginContainer 中的其他监听者派发消息

    • 单次回复(最近节点,最远节点),在最近节点策略下,消息派发过程中,一旦有插件响应即终止后续派发,性能最高

  • 响应来源:绑定同步消息的返回值由一个响应体封装,携带响应者的相关标识信息(监听者类型/名字)


2.3.3  API 设计/使用

创建一个同步消息

创建一个同步消息很简单,只需要遵守 SyncPluginMessage 协议即可,你需要为你的同步消息指定一个响应类型。

public protocol SyncPluginMessage {    associatedtype Response}

💁 发送同步消息就如同调用一个同步函数,消息体携带的数据就是入参,而指定的 Response 就是返回值类型。

你想要发送一个路由跳转的消息到某个路由管理插件,并得知此路由是否可被处理以进行兜底处理或错误提示。

struct RouteToMessageSyncPluginMessage {    typealias Response = Bool    let destinationUr: String    }

向直播间缓存系统,查询某个用户的信息

struct LiveUser {    let idString    let nameString    let avatorString}struct QueryLiveUserInfoSyncPluginMessage {    typealias Response = LiveUser
    let userIdString}

💁 可以看到,同步消息不同于异步消息,它是没有消息类型的(事件消息和状态消息),因为同步消息是实时的,所以自然都是事件消息类型。

发送同步消息

Plugin,PluginContainer 和 PluginContainerComposer 都可以发送同步消息。

通用的消息发送方法定义如下:

func send<MessageSyncPluginMessage>(messageMessagestrategySyncMessageDispatchStrategy) -> [Message.Response]

继续使用我们上面的例子,发送一个路由消息

let results: [Bool] = sender.send(messageRouteToMessage(destinationUrl"zhihu://education/live_room/123", channe: String, strategy: .first(1))

不同于一般的同步方法调用,因为响应同步消息的监听者可能很多也可能一个没有,所以返回值是一个数组

💁 SyncMessageDispatchStrategy 是同步消息的派发策略,目前只有 first(Int) 和 all 两个类型。绝大部分情况下,同步消息的订阅者不会很多,且发送者也只想获取一个回复。使用 first(1) 可以满足大部分情况,并大幅提高派发效率,获取一个返回值后,就不会进行后续查询了。

针对这个特殊情况,对 Plugin 扩通了一个同步消息的发送函数,可以默认使用 first(1) 策略并返回一个Response?

 public func send<MessageSyncPluginMessage>(messageMessage) -> Message.Response? {        send(message: message, strategy: .first(1)).first    }

同步消息监听-Plugin

class RouteManagerPluginPlugin {
    @SyncMessageObserver<RouteManagerPluginRouteToMessage>    private var routeToMessage = .init { base, message in        return base.route(to: message.destinationUrl)    }   @SyncMessageObserver<RouteManagerPluginRouteToMessage>(channel: "")    private var routeToMessage2 = .init { base, message in        return base.route(to: message.destinationUrl)    }    @MessageObserver    private var requestToHomepage: $RequestToHomepage?    private func route(to urlString) -> Bool {        ...    }}

通过 SyncMessageObserver 监听同步消息时,需要指监听者的类型和监听的消息类型,消息类型用于派发系统检索,而监听者类型则用户转换base类型。

这里提供 base 是因为非 lazy 属性是无法在定义阶段获取 self 的,而 propertyWrapper 是不支持 lazy 属性的,所以需要提供base来绕过这个问题,同时也避免了 [weak self] 产生的可选绑定,毕竟消息中指定的返回值是一定要 return 的。

切记不要写作监听者的类型,因为编译器限制,在属性的声明阶段是无法使用 Self 的,所以必须要使用者手动填写。如果后续引入 Macro 的话,可以基于语法书查询到具体的 class 类型,就能自动生成相关代码了,但目前还不支持。

实际上 routeToMessage 本身也是一个纯函数,其接受两个参数 observer, message,并返回指定类型返回值。在 Plugin 内部直接调用 routeToMessage(self, JumpToMessage(destinationUrl: "***")) 可以复用其函数功能,且不对消息派发产生任何影响。

同步消息监听-Container/Composer

我始终推荐将功能封装为 Plugin 使用,如果必须要掌控实例,可以实现为 MessageRelay。但是为了保持API一致性和特殊用途,MessageInterceptor 也是支持同步消息的。

.listen 的使用还是一样简单,因为不影响消息的派发逻辑。

containerA.register(messageInterceptor: .listen(receiveSyncMessage: { syncMessage in     switch syncMessage {     case let message as SyncMessageY:         print(message)     default:         break     } }))

但是如果要拦截并修改返回值就需要使用专用的 SyncMessageInterceptor

containerA.register(messageInterceptor: SyncMessageInterceptor<SyncMessageY>(receiveSyncMessage: { message, transformer, strategy in    // 拦截响应不继续派发    // return [];    // 拦截响应不继续派发,并注入返回值    // return ["value from containerA"]    // 不影响派发流程,只是记录日志, listen 的逻辑    // Log.debug("receive a sync message.")    // return transformer.transform(to: message, strategy: strategy)    // 变更派发流程    let results = transformer.transform(toMessageB(), strategy: .first(1))    return results + transformer.transform(to: message, strategy: strategy)}))

如最后一个例子,因为消息派发策略是基于计数实现的,所以如果在注入返回值的时候,没有进行计数的更新,会导致调用方最终得到的结果和发送策略不一致,不过通常情况下这并不会产生严重后果,但是需要牢记:响应值的个数并不一定等于派发策略的数值。

2.3.4 问题说明

核心逻辑验证性代码

使用 AnySyncMessageObserver<Message> 作为中间类型是为了消除在查询阶段对 Observer 的语法检测限制。

使用参数泛型协议必须在 iOS16 及其以上才能使用,为了更好的兼容性,我被迫使用抽象类进行实现。

protocol SyncMessage<Response> {    associatedtype Response}struct JumpToHomePageSyncMessage {    typealias Response = Bool    }struct JumpToWalletSyncMessage {    typealias Response = Void}protocol Plugin {}protocol AnySyncMessageObserver<Message> {    associatedtype MessageSyncMessage    func invoke(_ observerAny_ messageMessage) -> Message.Response}@propertyWrapperclass SyncMessageObserver<ObserverMessageSyncMessage>: AnySyncMessageObserver {    var wrappedValue: (ObserverMessage) -> Message.Response
    func invoke(_ observerAny_ messageMessage) -> Message.Response {        wrappedValue(observer as! Observer, message)    }
    init(wrappedValue@escaping (ObserverMessage) -> Message.Response) {        self.wrappedValue = wrappedValue    }}class PluginAPlugin {    private var state = true
//  #SyncMessageObserver<JumpToHomePage> { base, message in//      base.state//  }
    @SyncMessageObserver<PluginAJumpToHomePage>    private var jumpToHomePage = { base, message in        base.state    }
    private var jumpToWallet = SyncMessageObserver<PluginAJumpToWallet>{ base, messge in
    }}class MessageSender {        func send<MSyncMessage>(_ messageM) -> [M.Response] {        let objects: [Plugin= [PluginA()]        var results: [M.Response= []        for item in objects {            for child in Mirror(reflecting: item).children {                                if let method = child.value as? any AnySyncMessageObserver<M> {                                       if let result = method.invoke(item, message) as? M.Response {                        results.append(result)                    }                }            }        }                return results    }}let result = MessageSender().send(JumpToHomePage())print(result)

同步消息的拦截器

在异步消息处理中,我们的插件容器可以注册消息拦截器,拦截器可以用来监听所有在容器中流转的消息,进行消息拦截,消息监听,消息变形和消息批派发。实现此机制的一个核心点在于,异步消息是发送后不用管,它不需要等待任何回调,也就无需关心下游的流转。

在同步消息中,因为要获取同步的返回值,且返回值是携带类型信息的。所以针对容器内所有同步消息的拦截,基于显式的类型转换,是无法进一步将类型信息传递给泛型派发系统的。

结合同步消息的特点和使用场景,我设计了一个新的拦截处理机制,现在的同步消息可以实现:

  • 消息监听:仅对同步消息进行监听,不影响消息流的派发和响应。

  • 响应值篡改: 消息拦截器处于容器层级的,可以直接拦截指定消息,给予返回值,并停止后续派发。或者给于返回值后,让其继续流转,实现效果为插入响应值。


3.  架构实践

3.1  框架依赖

  • 适用于 Swift :依托于 Swift 的泛型和强类型系统构建类型注册和监听机制

  • 接入 RxSwift:ZHPluginSDK 采用 RxSwift 作为消息监听的方式,方便消息聚合及与 UI 绑定的无缝对接


3.2  案例实践

我们通过一个直播间场景的实例来了解一下整体的的插件注册和消息发送及监听流程,此处也会涉及到组件 UI 相关的管理方式。假设现在有一个直播间控制器 LiveRoomViewController 其内部有两个子控制器分别是负责显示聊天内容的 ChatViewController 以及承载发送消息和其他功能按钮的 InputBarViewController。这个示例算是较为复杂的场景,因为插件都是 UIViewController,实际使用中如果是普通的 Object 插件或是 View 插件都会有所简化。

1)首先创建一个插件,任何类型遵守 Plugin 协议的类型均可以作为一个插件。

Plugin 的定义如下:

// MARK: - Plugin/// Plugin Protocolpublic protocol Plugin {    /// Initializes a plugin with initialization parameters.    init?(paramsAny?)
    /// The plugin has initialized and ready for sending/receiving messages or setting up UI.    func pluginInitialized(        messageSenderany MessageSender,        nameString?,        slotViewAccessor@escaping (String?) -> PluginSlotView?    )
    /// Queries a specific view in the plugin system by the `viewId`.    func queryView(with viewIdString) -> UIView?
    /// Will be released.    func pluginDeinit()}extension Plugin {    public func pluginDeinit() {}}

我们以 InputBarViewController 为例子,使其遵守 Plugin 协议需要实现三个方法:

插件化在容器内被创建时会调用 init?(params:) 进行初始化,一旦初始化完成被注入到插件系统当中时,会调用 pluginInitialized 进行通知并返回 messageSender 让插件可以进行消息发送,以及 slotViewAccessor 让插件获取自己的插槽视图进行布局。

因为都是通过消息进行异步通讯,所以通常可以把插件的初始化处理延后到 pluginInitialized 进行处理,而 messageSender 需要被保存起来以便后续发送消息。

这里还有一个 slotViewAccessor 是帮助 UI 插件进行 View 布局的,暂时不用关注参数 name 以及 slotViewAccessor 中的需要传递的 string。返回值 PluginSlotView 中有两个参数,分别是 slotView: UIView 和 slotViewController: UIViewController?,这两个数据就是业务方通过插件视图容器提供给插件的布局父视图,通常插件只需将自己平铺在上面即可,如果自己是 ViewController 则需要将自己注册为子控制器。

import UIKitclass InputBarViewControllerUIViewController {    private var messageSender: MessageSender?
    required init?(paramsAny?) {        super.init(nibName: nil, bundle: nil)    }
    required init?(coderNSCoder) {        fatalError("init(coder:) has not been implemented")    }}extension InputBarViewControllerPlugin {    func pluginInitialized(        messageSenderMessageSender,        nameString?,        slotViewAccessor@escaping (String?) -> PluginSlotView?    ) {        self.messageSender = messageSender
        guard            let viewAccessor = slotViewAccessor(nil),            let parentVC = viewAccessor.slotViewController        else { return }
        willMove(toParent: parentVC)        parentVC.addChild(self)        viewAccessor.slotView.addSubview(view)        view.snp.makeConstraints {            $0.edges.equalToSuperview()        }    }
    func queryView(with viewIdString) -> UIView? {        nil    }}

ChatViewController 的初始化方式类似,就不再赘述。

2)接下来是创建插件消息,任何类型 (class, struct, enum) 遵守 PluginMessage 协议均可以成为一个插件消息。

PluginMessage 定义如下:

// MARK: - PluginMessage/// PluginMessage Protocolpublic protocol PluginMessage {    /// The `type` decides the message's dispatching policy. default to `event`.    static var type: PluginMessageType { get }}// MARK: - PluginMessageTypepublic enum PluginMessageType {    /// Event message.    case event
    /// State Message    ///    /// Messages of same types will be cached by the message centor,    /// subsequently added plugins will recevie these cached messages again for asyncing state.    case state}extension PluginMessage {    public static var type: PluginMessageType { .event }}

插件消息只要求一个 type 属性用来标识消息的类型,默认情况下所有的消息都是事件消息。消息类型的选择和设计意图可查看设计答疑。

我们这里以发出一个文本消息为目的,创建一个插件消息。消息的定义非常的简单,你可以按照需求进行消息体的构建,因为没有类型和内容的限制,你可以传递任意数据类型,而无需进行转码操作。

/// 消息: 发送一个文本聊天消息struct SendTextMessagePluginMessage {    let textString}

3)创建 pluginContainer 并注册插件

我们在主控制器 LiveRoomViewController 中创建一个 PluginContainer 并将上述创建的两个插件类型注册进插件容器中。因为是包含的插件中有视图,如果UI 布局不是非常的复杂,可以将 LivewRoomViewController 设置为视图管理容器并遵守 PluginViewContainer 代理,这样的在插件注册完毕后,会通过 slotView(pluginType:lpluginName:viewId:) 方法想视图管理容器寻求布局所需要的父视图,这样所有的插件布局细节都归拢于一处,布局代码之间的耦合是无法消除的,但是将其从插件中剥离出来放置到业务方的一处代码中,可以赋予业务方更多的布局灵活性。

class LiveRoomViewControllerUIViewController {    // 1. 创建 pluginContainer    let pluginContainer = PluginContainer()
    // 直播间负责管理视图的布局, 这里可以先为 UI 插件创建视图容器,后续方便做布局和动画    private let inputBarArea = UIView()    private let chatViewArea = UIView()
    override func viewDidLoad() {        super.viewDidLoad()

        view.addSubview(inputBarArea)        view.addSubview(chatViewArea)        inputBarArea.snp.makeConstraints {            $0.leading.bottom.trailing.equalToSuperview()            $0.height.equalTo(44)        }        chatViewArea.snp.makeConstraints {            $0.leading.trailing.equalToSuperview()            $0.bottom.equalTo(inputBarArea.snp.top)            $0.height.equalTo(500)        }
        // 2. 设置插件视图容器,并插入两个插件        pluginContainer.config(viewContainer: self, retainPolicy: .weak)        pluginContainer.addPlugin(ChatViewController.self)        pluginContainer.addPlugin(InputBarViewController.self)    }}// 3. 让 LiveRoomViewController 作为视图管理容器根据插件类型返回对应的插槽视图extension LiveRoomViewControllerPluginViewContainer {    func slotView(pluginTypePlugin.TypepluginNameString?, viewIdString?) -> PluginSlotView? {        switch pluginType {        case is ChatViewController.Type:            return AnyPluginSlotView(slotView: chatViewArea, slotViewController: self)        case is InputBarViewController.Type:            return AnyPluginSlotView(slotView: inputBarArea, slotViewController: self)        default:            return nil        }    }}

4)发送消息&监听消息

我们这里假设有 InputBarViewController 发送一个文本消息,而 ChatViewController 需要监听此消息并处理。

发送一个消息非常的简单,利用 messageSender 发送任何 PluginMessage 对象即像外发送了一个插件消息。

class InputBarViewControllerUIViewController {    private var messageSenderMessageSender?    ...    private func send(text: String) {        // 1. 利用 messageSender 发送消息        messageSender?.send(messageSendTextMessage(text: text))    }    ...}

监听消息也一样简洁:通过 MessageObserver 标记一个 PluginMessage 类型的属性,就完成对其的注册,后续通过 $receiveTextMessage (RxSwift Observable ) 可以直接进行监听和聚合。

class ChatViewControllerUIViewController {        // 1. 注册消息监听    @MessageObserver    private var receiveTextMessage: SendTextMessage?
    private let disposeBag = DisposeBag()    ...            override func viewDidLoad() {        super.viewDidLoad()
        // 2. 通过 $receiveTextMessages 就可以进行消息监听,        // 而且监听时收到的 message 是非可选值        $receiveTextMessage.bind(onNext: { message in            print("receive text message: \(message.text)")        }).disposed(by: disposeBag)    }    ...}


3.3  进阶玩法

插件通过参数进行同步初始化

在上面的例子中我们并没有试用到  init?(params: Any?) 来对插件进行设置,通常一个插件并不需要在 init 时初始化自己的服务,大部分服务都可以延后至 initialized 的时候进行,但是如果有特殊的需求必须在创建时就要获取某些数据,那就需要对 init?(params: Any?) 中的 params 进行解析,如果解析出来数据不满足条件或是解析失败可以直接 return nil,告知容器无法初始化 plugin。

这个 params 这是由 plugin container 提供的,更确切一些来说是有注册到 plugin container 中的 PluginParamProvider 提供的。

创建一个类型遵守 PluginParamProvider 协议,并通过下述方法就可以为插件注入一个初始化参数的提供者。

class PluginContainer {       /// Registers a `PluginParamProvider` for providing initialization data for plugins.    /// - Note: Must be invoked before registering plugins.    public func register(        pluginParamProviderPluginParamProvider,        retainPolicyRetainPolicy    )}

而其中的 PluginParamProvider 协议只约定了一个方法,这里会告知你准备进行初始化的插件是什么类型,他的名字是什么,然后此 provider 需要返回对应的数据以帮助 plugin 完成初始化。

PluginParamProvider 可以注册多个,它们会按照注入的先后顺序响应参数请求,如果返回的值非 nil 就不会再询问下一个。

// MARK: - PluginParamProviderpublic protocol PluginParamProvider {    func initializationParam(pluginTypePlugin.TypenameString?) -> Any?}


3.3.2  需要添加多个同一类型的插件

在添加插件的时候,除了插件类型意外,还有多个参数可以使用,其中的 name 在你插入多个同类型插件时可以用来当作 id 标识,在其他的各种方法中,你也能看见不少 name 相关的字段,这些都是和你创建插件时传入的 name 相同以此来协助你对多个同类型插件实例进行区别处理。而 replaceExistingPlugin 则用来处理当注入同类型同名的插件时,是否进行插件替换或直接忽略。

    /// Registers a plugin.    /// - Parameters:    ///   - pluginType: Meta type of the plugin.    ///   - name: Name of the plugin. Same type plugins can be registered with different names.    ///   - replaceExistingPlugin: Replaces the old plugin when registering a new plugin whth the same type.    ///   - messageObservingPriority: The higher priority will receive messages earlier.    /// - Returns: `true` means registered successfully.    @discardableResult    public func addPlugin(        _ pluginTypePlugin.Type,        nameString= nil,        replaceExistingPluginBool = false,        messageObservingPriorityMessageObservingPriority = .normal    ) -> Bool


3.3.3  插件优先级

在上面的 addPlugin方法中还有一个参数 messageObservingPriority 则是用来控制插件接收消息的先后顺序。插件的接收顺序和插件的注入先后是没有关系的,我也不希望业务方依赖此顺序,因为每个消息到来,都是立刻同步派发,所以先后间隔很短,业务方也不应该对消息的时效敏感。

此处仅仅为补充设计,一般很少用到。MessageObservingPriority 的定义如下:

// MARK: - MessageObservingPrioritypublic struct MessageObservingPriorityExpressibleByIntegerLiteralComparable {    public let rawValue: Int
    public init(integerLiteral valueInt) {        self.rawValue = value    }
    public static func priority(_ valueInt) -> Self {        self.init(integerLiteral: value)    }
    public static func < (lhsMessageObservingPriorityrhsMessageObservingPriority) -> Bool {        lhs.rawValue < rhs.rawValue    }}extension MessageObservingPriority {    public static let low: Self = 100    public static let normal: Self = 1000    public static let high: Self = 10000}


3.3.4  消息拦截器

因为所有的插件都只能通过消息系统进行信息传递和交互,那业务方就可以通过消息拦截器进行切面控制,例如埋点、成功率监控和本地日志。

而利用其全面的消息转发能力可以进行一些操作 hook 和 AB test,更具象的使用可以根据实际情况随机应变。

通过下述方法进行拦截器注册,

  /// Registers a message interceptor, the lastest inserted interceptor will intercept messages firstly.    public func register(        messageInterceptorany MessageInterceptor,        retainPolicyRetainPolicy = .retain    )

可以看到 MessageInterceptor 中有方法 intercept(message:transformer:),message 就是拦截到信息,然后就可以通过 transformer 进行消息转发,如果不转发就视为丢弃,如果不做处理就调用 transformer.transform(to: messag)进行透传,或是转换为其他的消息类型,当然也可以调用多次。

而没有被丢弃的消息会按照拦截器的注册顺序依次传递

/// Message Interceptorpublic protocol MessageInterceptor {    var uniqueId: any Hashable { get }    /// Intercepts messages and then discards or converts them.    /// - Parameters:    ///   - message: The recevied message.    ///   - transformer: Uses the method `transform(to:)` of the transformer to dispath new converted messages.    /// - Note: The recevied message will be discarded if you don't call the `transform(to:)` method once.    func intercept(messageany PluginMessagetransformerMessageTransformer)}// MARK: - MessageTransformer/// Message Transformer/Dispatcherpublic struct MessageTransformer {    ...    /// Dispatches a new plugin message.    public func transform(to messageany PluginMessage)    ...}

为了方便纯粹的监听需求,提供了一个内建的拦截器以供大家使用

pluginContainer.register(messageInterceptor: .listen({ message in
}))


3.3.5  使用环境变量

环境变量是除了插件初始化参数以外的另一种数据注入方式,不同于 plugin 的初始化参数需要每次手动传入(或是借由 PluginParamProvider 自动注入)。

PluginContainer 和 PluginContainerComposer 可以通过函数 setEnvironment 将任意类型的数据设置为环境变量。

  • 环境变量在整个传递链路中的标识key是其类型,插件系统会基于类型生成一个唯一标识。

  • 在整个插件系统的树状拓扑结构中,各个容器都是一个节点,而环境变量的设置仅影响其下游各节点的环境变量值。

  • 同一个传递链路中,如果在中间节点变更了环境变量,则下游节点会收到变更,而上游节点并不受影响。

extension PluginContainer {    public func setEnvironment<E>(_ environmentObjectEkey: (any Hashable)? = nilpolicyRetainPolicy = .retain) {        environments.updateValue(.init(environmentObject, policy: policy), forKey: key?.hashValue ?? typeIdentifier(E.self).hashValue)    }}

使用方法:

class LiveRoomInfo {    let roomId: String    var imState: LiveIMState = .disconnected}class ChatPluginPlugin {    @PluginEnvironment    var liveRoomInfo: LiveRoomInfo?    /// ....
    private func send(messageString) {        guard let liveRoomInfo else { return }
        if liveRoomInfo.imState != .connected {            Toast.show("IM 服务链接中断")            return        }    }}// ....class RootViewControllerUIViewController {    let rootContainer = PluginContainer()    let liveRoomInfo = LiveRoomInfo()    func viewDidLoad() {                rootContainer.setEnvironment(liveRoomInfo)        rootContainer.addPlugin(ChatPlugin.self)    }}







· 推荐阅读 ·


知乎技术沙龙干货分享 | 大模型推理框架 ZhiLight 实践


知乎技术沙龙干货分享 | 云原生技术如何重塑企业 IT 架构


知乎核心业务 MongoDB 集群丝滑上云迁移实践之路


基于大模型的海量标签多分类方法


知乎超大规模TiDB集群管控实践


知乎开源 32B 模型,创意写作能力显著提升


从踩坑到提效:知乎社区生态 B 端 Cursor 实战经验


知乎增长活动页渲染优化实战






图片
图片
                  知乎技术专栏
分享知乎技术日志,探索社区技术创新。



【声明】内容源于网络
0
0
知乎技术专栏
分享知乎技术日志,探索社区技术创新。
内容 17
粉丝 0
知乎技术专栏 分享知乎技术日志,探索社区技术创新。
总阅读34
粉丝0
内容17