Swift 面向协议编程的 RMP 模式
一、RMP 模式概述
RMP(Runtime Mixin Protocol)模式是 Swift 中一种强大的面向协议编程架构,通过三个核心要素解决传统面向对象编程的痛点,特别适合构建复杂的、需要高度复用的功能模块。通过合理的协议设计和运行时属性绑定,可以创建出既灵活又可维护的代码结构:
-
Protocol:解决多继承问题,通过协议声明能力
-
Runtime:解决协议无法储存对象变量问题,通过关联对象实现状态存储
-
Mixin:解决多协议冲突问题,通过协议组合实现能力复用
二、设计问题
面向对象继承的本质是单一树形结构 ,它在描述"是什么(is-a)"时很自然,
但在描述"能做什么(can-do)"时会遇到两个典型困境:
困境一:功能横切多个类
礼物动画、PK、连麦是独立功能,但视频房、语音房、多人房都需要它们。
用继承的话,要么写三遍,要么塞进公共基类------基类越来越臃肿。
困境二:功能组合爆炸
如果用继承来组合,"支持PK的视频房"和"支持PK的语音房"是两条继承链,
再加"支持连麦",变成四条------每增加一个维度,继承树翻倍。
解法:RMP 模式
把"能做什么"从类里剥离出来,定义为独立的能力单元(MixinProtocol)。类只需声明"我拥有这个能力",
能力的属性和逻辑全部自包含在 MixinProtocol 内,彼此不知道对方存在。
三、Swift 的实现基础
3.1 Protocol 层
Swift 的协议扩展(Protocol Extension) 天然支持为协议提供默认实现------这是实现
RMP 模式的基础。通过协议,我们可以声明能力接口,并通过扩展提供默认实现。
3.2 Runtime 层
协议扩展有一个限制:不能定义存储属性 。
这个限制通过 ObjC Runtime 关联对象(Associated Objects) 绕过:
Runtime 允许在运行时把任意值"绑定"到任意 NSObject 实例上,
等效于给协议扩展加了存储属性。
3.3 Mixin 层
通过协议组合,我们可以实现能力的自由组合,解决多协议冲突问题,
每个能力单元(MixinProtocol)专注于单一职责,通过 Mixin 协调多个能力的生命周期。
三者结合,就形成了完整的 RMP 模式:Protocol 声明能力,Runtime 存储状态,Mixin 组合功能。
四、属性绑定方式
在 RMP 模式中,我们需要在协议扩展中实现属性存储。以下是几种常用的属性绑定方式:
方式一:懒加载对象属性
适合首次访问时才创建、之后复用的对象(视图、子控制器等)。
swift
// 协议扩展中声明懒加载属性
var scoreView: PKScoreView {
lazyVarObject { PKScoreView() }
}
// 实现原理示例
func lazyVarObject<T: Any>(_ block: () -> T, _ fun: String = #function, _ file: String = #file, _ line: Int = #line) -> T {
let uniqueKey = "\(file)\(line)"
let keyString = "\(uniqueKey).\(fun)"
let key = UnsafeRawPointer(bitPattern: keyString.hashValue)!
guard let obj = objc_getAssociatedObject(self, key) as? T else {
let obj = block()
objc_setAssociatedObject(self, key, obj, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return obj
}
return obj
}
方式二:绑定属性
setter\ggetter 自动处理包装和解包:
swift
// 自动处理值类型的包装和解包
var count: Int {
get { associatedValue(defaultValue: 0) }
set { setAssociatedValue(newValue) }
}
var view: UIView? {
get { associatedValue(defaultValue: nil) }
set { setAssociatedValue(newValue) }
}
var isActive: Bool {
get { associatedValue(defaultValue: false) }
set { setAssociatedValue(newValue) }
}
// 实现原理示例
func associatedValue<T>(defaultValue: T, _ fun: String = #function, _ file: String = #file, _ line: Int = #line) -> T {
let uniqueKey = "\(file)"
let keyString = "\(uniqueKey).\(fun)"
let key = UnsafeRawPointer(bitPattern: keyString.hashValue)!
if let value = objc_getAssociatedObject(self, key) as? T {
return value
}
return defaultValue
}
func setAssociatedValue<T>(_ value: T, _ fun: String = #function, _ file: String = #file, _ line: Int = #line) {
let uniqueKey = "\(file)"
let keyString = "\(uniqueKey).\(fun)"
let key = UnsafeRawPointer(bitPattern: keyString.hashValue)!
objc_setAssociatedObject(self, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
五、RMP 模式的标准分层结构
RMP 模式的协议通常分三层,层级越高越具体:
┌─────────────────────────────────────────────────┐
│ Layer 0 工具层(Runtime 基础) │
│ BaseAssociatedProtocol │
│ 提供 lazyVarObject 等工具方法,所有 RMP 能力的基础 │
└───────────────────┬─────────────────────────────┘
│ 继承
┌───────────────────▼─────────────────────────────┐
│ Layer 1 能力层(Protocol 声明) │
│ PKProtocol / LinkMicProtocol / GiftProtocol / ...│
│ 每个能力单元 = 一个文件 = 一组内聚的属性+逻辑 │
└───────────────────┬─────────────────────────────┘
│ 组合
┌───────────────────▼─────────────────────────────┐
│ Layer 2 组合层(Mixin 协调) │
│ LiveRoomMixin : PKProtocol, LinkMicProtocol │
│ 简化 Host Class 的 conformance 声明 │
└───────────────────┬─────────────────────────────┘
│ 遵循
┌───────────────────▼─────────────────────────────┐
│ Layer 3 宿主层 │
│ class VideoRoomVC : BaseVC, LiveRoomMixin │
│ class VoiceRoomVC : BaseVC, LiveRoomMixin │
│ 宿主类体内不写任何 RMP 逻辑,只声明 conformance │
└─────────────────────────────────────────────────┘
六、RMP 模式的新实践:MVCM 聚合协议模式
在实践中,RMP 模式有了新的发展,根据功能复杂度采用不同的组织方式:
6.1 简单功能:独立能力单元
对于简单、单一职责的功能,直接创建一个独立的 RMP 能力单元:
适用场景:功能逻辑简单,不需要复杂的 UI、数据和操作分离。
示例 :BeautyProtocol.swift
-
只负责美颜功能的核心逻辑
-
包含美颜面板的管理、美颜参数的设置等
-
宿主只需遵循此协议即可获得美颜能力
6.2 复杂功能:MVCM 聚合协议模式
对于复杂功能,采用 MVC 模式划分多个协议,再通过 Mixin 组合,形成 MVCM(Model-View-Controller-Mixin)结构:
核心思想:将复杂功能按照 MVC 职责划分为三个协议,每个协议负责一个维度,最后通过 Mixin 协议组合在一起,由 Mixin 层统合 MVC 三层的交互。
协议划分:
| 协议类型 | 职责 | 命名规范 | 示例 |
|---|---|---|---|
| Model 协议 | 数据管理、状态存储 | {功能名}MProtocol.swift |
VideoReadyMProtocol.swift |
| View 协议 | UI 组件、布局、渲染 | {功能名}VProtocol.swift |
VideoReadyVProtocol.swift |
| Controller 协议 | 简单方法声明、回调管理 | {功能名}CProtocol.swift |
VideoReadyCProtocol.swift |
| Mixin 协议 | 统合 MVC 三层、处理跨层交互 | {功能名}Mixin.swift |
VideoReadyMixin.swift |
职责详解:
-
Model 层:
-
负责数据的存储和管理
-
定义数据结构和状态属性
-
提供数据操作方法
-
不直接与视图层交互
-
-
View 层:
-
负责 UI 组件的创建和布局
-
提供 UI 更新方法
-
通过参数接收数据,不直接访问数据层
-
不处理业务逻辑
-
-
Controller 层:
-
定义简单的方法接口
-
管理回调闭包
-
不包含复杂的业务逻辑
-
不直接操作视图
-
-
Mixin 层:
-
统合 MVC 三层,处理跨层交互
-
实现复杂的业务逻辑
-
协调数据层和视图层的通信
-
提供完整的功能实现
-
文件结构:
├── VideoReadyMProtocol.swift ← 数据层
├── VideoReadyVProtocol.swift ← 视图层
├── VideoReadyCProtocol.swift ← 控制层
└── VideoReadyMixin.swift ← 混合层(统合 MVC 三层)
优点:
- 职责清晰:每个协议专注于自己的职责,符合单一职责原则
- 代码组织:复杂功能的代码被合理拆分,提高可读性和可维护性
- 易于测试:各层可以独立测试,提高测试覆盖率
- 灵活性:可以根据需要单独使用某一层的能力
- 逻辑集中:复杂的业务逻辑集中在 Mixin 层,便于维护
使用方式 :
宿主类只需遵循 Mixin 协议,即可获得完整的功能:
swift
class VideoReadyController: BaseViewController, VideoReadyMixin {
// 无需实现任何逻辑,只需声明遵循协议
}
核心实现特点:
-
控制层轻量化:只保留简单的方法声明和回调管理,复杂逻辑移至 Mixin 层
-
混合层统合:Mixin 层负责协调 MVC 三层的交互,处理复杂业务逻辑
-
跨层通信:通过 Mixin 层实现数据层和视图层的通信,避免直接耦合
-
功能完整:宿主类只需遵循 Mixin 协议,即可获得完整的功能
这种 MVCM 结构特别适合复杂的业务模块,能够在保持 RMP 模式优势的同时,提供更清晰的代码结构和职责划分。
七、RMP 能力单元内访问宿主对象
RMP 能力单元的方法有时需要访问宿主 Controller 的属性(如 view、navigationController 等)。
由于能力单元不知道自己会被谁遵循,用交叉类型条件转换来安全地取得完整上下文:
swift
// Self 同时满足"宿主基类"和"当前能力单元"两个约束
func pk_start() {
guard let self = self as? BaseController & PKProtocol else { return }
// 现在可以同时访问 BaseController 的属性和 PKProtocol 的属性
self.view.addSubview(scoreView)
self.someMethod()
}
guard let self = self as? HostClass & ThisProtocol 是 RMP 能力单元内访问宿主的标准写法 ,
不要用 as! 强转,不要假设 self 的具体类型。
八、新建一个 RMP 能力单元的完整模板
8.1 简单能力单元模板
swift
import Foundation
import UIKit
// MARK: - 协议声明(定义对外暴露的接口)
/// 美颜功能 RMP 能力单元
public protocol BeautyProtocol: BaseAssociatedProtocol {
func beauty_setup()
func beauty_showPanel()
func beauty_hidePanel()
func beauty_updateOptions(_ options: [String: Any])
}
// MARK: - 默认实现(所有逻辑内聚于此)
public extension BeautyProtocol {
// ---- 属性(Runtime 绑定)----
// 懒加载属性
private var beautyControlView: BeautyControlView {
lazyVarObject { BeautyControlView() }
}
// 值类型属性
private var beautyOptions: [String: Any] {
get { associatedValue(defaultValue: [:]) }
set { setAssociatedValue(newValue) }
}
private var isBeautyEnabled: Bool {
get { associatedValue(defaultValue: true) }
set { setAssociatedValue(newValue) }
}
// ---- 方法 ----
func beauty_setup() {
guard let self = self as? UIViewController & BeautyProtocol else { return }
// 添加美颜控制面板
self.view.addSubview(beautyControlView)
beautyControlView.isHidden = true
// 初始化美颜参数
beautyOptions = [
"smoothness": 0.5,
"whiteness": 0.5,
"redness": 0.5
]
}
func beauty_showPanel() {
beautyControlView.isHidden = false
// 显示美颜面板逻辑
}
func beauty_hidePanel() {
beautyControlView.isHidden = true
// 隐藏美颜面板逻辑
}
func beauty_updateOptions(_ options: [String: Any]) {
beautyOptions = options
// 应用美颜参数逻辑
}
}
8.2 MVCM 结构模板
Model 协议
swift
/// 视频准备模块数据协议
public protocol VideoReadyMProtocol: BaseAssociatedProtocol {
// 数据属性
var topic: String { get set }
var coverImage: UIImage? { get set }
var coverImageURL: String { get set }
var isNotificationEnabled: Bool { get set }
// 方法
func resetState()
}
public extension VideoReadyMProtocol {
var topic: String {
get { associatedValue(defaultValue: "") }
set { setAssociatedValue(newValue) }
}
var coverImage: UIImage? {
get { associatedValue(defaultValue: nil) }
set { setAssociatedValue(newValue) }
}
var coverImageURL: String {
get { associatedValue(defaultValue: "") }
set { setAssociatedValue(newValue) }
}
var isNotificationEnabled: Bool {
get { associatedValue(defaultValue: true) }
set { setAssociatedValue(newValue) }
}
func resetState() {
topic = ""
coverImage = nil
coverImageURL = ""
isNotificationEnabled = true
}
}
View 协议
swift
/// 视频准备模块视图协议
public protocol VideoReadyVProtocol: AnyObject {
// UI 组件
var previewView: UIView { get }
var topicTextField: UITextField { get }
var coverImageView: UIImageView { get }
var startButton: UIButton { get }
var notificationButton: UIButton { get }
// 方法
func setupUI()
func updateTopic(_ topic: String)
func updateCoverImage(_ image: UIImage)
func updateNotificationButton(_ enabled: Bool)
}
public extension VideoReadyVProtocol where Self: UIView {
var previewView: UIView {
lazyVarObject {
let view = UIView()
view.backgroundColor = .black
return view
}
}
var topicTextField: UITextField {
lazyVarObject {
let textField = UITextField()
textField.placeholder = "输入直播标题"
textField.textColor = .white
return textField
}
}
var coverImageView: UIImageView {
lazyVarObject {
let imageView = UIImageView()
imageView.image = UIImage(named: "default_cover")
imageView.contentMode = .scaleAspectFill
return imageView
}
}
var startButton: UIButton {
lazyVarObject {
let button = UIButton(type: .custom)
button.setTitle("开始直播", for: .normal)
button.setTitleColor(.white, for: .normal)
return button
}
}
var notificationButton: UIButton {
lazyVarObject {
let button = UIButton(type: .custom)
button.setTitle("通知粉丝", for: .normal)
button.setTitleColor(.white, for: .normal)
return button
}
}
func setupUI() {
// 布局 UI 组件
addSubview(previewView)
addSubview(topicTextField)
addSubview(coverImageView)
addSubview(startButton)
addSubview(notificationButton)
// 设置布局约束
// ...
}
func updateTopic(_ topic: String) {
topicTextField.text = topic
}
func updateCoverImage(_ image: UIImage) {
coverImageView.image = image
}
func updateNotificationButton(_ enabled: Bool) {
let image = enabled ? UIImage(named: "notification_on") : UIImage(named: "notification_off")
notificationButton.setImage(image, for: .normal)
}
}
Controller 协议
swift
/// 视频准备模块控制协议
public protocol VideoReadyCProtocol: AnyObject {
// 回调闭包
var onBack: (() -> Void)? { get set }
var onStart: (([String: Any]) -> Void)? { get set }
var onSelectCover: (() -> Void)? { get set }
// 方法
func backAction()
func startAction()
func coverTapped()
func notificationTapped()
}
public extension VideoReadyCProtocol where Self: UIView {
var onBack: (() -> Void)? {
get { associatedValue(defaultValue: nil) }
set { setAssociatedValue(newValue) }
}
var onStart: (([String: Any]) -> Void)? {
get { associatedValue(defaultValue: nil) }
set { setAssociatedValue(newValue) }
}
var onSelectCover: (() -> Void)? {
get { associatedValue(defaultValue: nil) }
set { setAssociatedValue(newValue) }
}
func backAction() {
onBack?()
}
func startAction() {
// 具体实现移至 Mixin 层
}
func coverTapped() {
onSelectCover?()
}
func notificationTapped() {
// 具体实现移至 Mixin 层
}
}
Mixin 协议
swift
/// 视频准备模块混合协议
public protocol VideoReadyMixin: VideoReadyMProtocol, VideoReadyVProtocol, VideoReadyCProtocol {}
public extension VideoReadyMixin {
func setupVideoReady() {
// 初始化数据
resetState()
// 设置 UI
setupUI()
// 绑定事件
bindEvents()
// 启动预览
startPreview()
}
func bindEvents() {
// 封面点击
let coverTap = UITapGestureRecognizer { [weak self] _ in
self?.coverTapped()
}
coverImageView.isUserInteractionEnabled = true
coverImageView.addGestureRecognizer(coverTap)
// 开始按钮点击
startButton.addTarget(self, action: #selector(startAction), for: .touchUpInside)
// 通知按钮点击
notificationButton.addTarget(self, action: #selector(notificationTapped), for: .touchUpInside)
}
func startPreview() {
// 启动相机预览逻辑
}
// 重写控制层方法
func startAction() {
// 验证输入
guard !topic.isEmpty else {
showAlert("请输入直播标题")
return
}
// 构建参数
var params: [String: Any] = [
"topic": topic,
"coverImageURL": coverImageURL,
"notificationEnabled": isNotificationEnabled
]
// 调用回调
onStart?(params)
}
func notificationTapped() {
// 切换通知状态
isNotificationEnabled = !isNotificationEnabled
// 更新 UI
updateNotificationButton(isNotificationEnabled)
}
private func showAlert(_ message: String) {
// 显示提示
}
}
九、生命周期多路接入问题
问题根源
Swift 协议扩展对同一方法只有一个实现会生效。当多个 RMP 能力单元都想响应 viewDidLoad,
就会产生静默冲突------编译通过,但只有一个能力单元的逻辑被执行:
swift
extension PKProtocol { func viewDidLoad() { /* 只有这个生效 */ } }
extension LinkMicProtocol { func viewDidLoad() { /* 被静默忽略 */ } }
根本原因 :viewDidLoad 是单入口,而我们有 N 个 RMP 能力单元需要接入它。
问题的本质是谁来做多路分发。
解法一:各 RMP 能力单元用独立命名钩子,由 Mixin 链式编排
适用于不依赖 RxSwift 的场景,或需要精确控制调用顺序的模块。
每个 RMP 能力单元不实现 viewDidLoad,改为实现自己命名的钩子,
Mixin 协议提供统一实现,负责按序调用所有钩子。
swift
// ---- 每个 RMP 能力单元只定义自己命名的钩子,无冲突 ----
extension PKProtocol {
func pk_viewDidLoad() { /* PK 初始化 */ }
func pk_viewWillAppear(_ animated: Bool) { /* PK 刷新 */ }
}
extension LinkMicProtocol {
func lm_viewDidLoad() { /* 连麦初始化 */ }
}
// ---- Mixin:统一编排调用顺序 ----
protocol LiveRoomMixin: PKProtocol, LinkMicProtocol {
func setup()
func viewWillAppear(_ animated: Bool)
}
extension LiveRoomMixin {
func setup() {
pk_viewDidLoad() // PK 初始化
lm_viewDidLoad() // 连麦初始化
}
func viewWillAppear(_ animated: Bool) {
pk_viewWillAppear(animated) // PK 刷新
}
}
// ---- 宿主:调用 Mixin 方法 ----
class VideoRoomController: BaseViewController, LiveRoomMixin {
override func viewDidLoad() {
super.viewDidLoad()
setup() // 调用 Mixin 方法
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.viewWillAppear(animated) // 调用 Mixin 方法
}
}
解法二:使用事件发布-订阅模式
适用于使用 RxSwift 或其他响应式框架的项目。
swift
// ---- 基础控制器 ----
class BaseViewController: UIViewController {
let rx_viewDidLoad = PublishRelay<Void>()
let rx_viewWillAppear = PublishRelay<Bool>()
override func viewDidLoad() {
super.viewDidLoad()
rx_viewDidLoad.accept(())
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
rx_viewWillAppear.accept(animated)
}
}
// ---- RMP 能力单元 ----
extension PKProtocol {
func pk_bindEvents() {
guard let self = self as? BaseViewController & PKProtocol else { return }
// 监听生命周期事件
self.rx_viewDidLoad
.subscribe(onNext: { [weak self] in
self?.pk_setup()
})
.disposed(by: self.disposeBag)
self.rx_viewWillAppear
.subscribe(onNext: { [weak self] _ in
self?.pk_refresh()
})
.disposed(by: self.disposeBag)
}
}
// ---- Mixin:统一绑定 ----
extension LiveRoomMixin {
func bindMixins() {
pk_bindEvents()
lm_bindEvents()
}
}
// ---- 宿主 ----
class VideoRoomController: BaseViewController, LiveRoomMixin {
override func viewDidLoad() {
super.viewDidLoad()
bindMixins() // 绑定所有 RMP 能力单元
}
}
十、何时用 RMP 能力单元,何时用继承
| 场景 | 推荐方式 |
|---|---|
| 功能需要跨多个无关类复用 | RMP 能力单元 |
| 多个功能需要自由组合 | RMP 能力单元 |
| 向现有类追加功能,不想修改它 | RMP 能力单元 |
| 严格的 is-a 关系(Dog is-a Animal) | 继承 |
需要访问 super、重写生命周期 |
继承 |
| 功能仅一个类使用,不存在复用 | 直接写在类里 |
不要滥用 RMP 能力单元 :如果一个功能只有一个宿主,直接写在类里更清晰。
RMP 能力单元的价值在于复用和隔离,强行拆分只会增加间接层。
十一、命名规范
协议文件命名
{功能名}Protocol.swift(建议使用功能名作为前缀,Protocol 作为后缀)
方法 / 属性前缀
每个 RMP 能力单元的公开成员加功能名前缀,防止不同能力单元之间命名冲突:
| RMP 能力单元 | 前缀 | 示例 |
|---|---|---|
| PK 功能 | pk_ |
pk_start(), pk_scoreView |
| 连麦 | lm_ |
lm_onMic(), lm_isOnMic |
| 礼物动效 | gift_ |
gift_play(), gift_queue |
| 美颜 | beauty_ |
beauty_setup(), beauty_options |
| 视频房 | video_ |
video_initUI(), video_previewView |
十二、常见错误
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 把逻辑写进宿主 Controller | RMP 能力单元失去意义,基类重新膨胀 | 逻辑全写在 extension XxxProtocol 里 |
| 在宿主里 override 方法后调用 RMP 能力单元 | 形成重复调用层 | RMP 能力单元自己在 setup 里完成初始化 |
| 多个 RMP 能力单元共享关联对象 key | 属性互相覆盖,产生诡异 bug | 为每个属性生成唯一 key |
用 as! 强转宿主类型 |
运行时崩溃 | 用 guard let self = self as? Host & XxxProtocol |
| RMP 能力单元之间直接互相调用 | 能力单元耦合,破坏独立性 | 通过宿主或组合协议传递依赖 |
| 对只有一个宿主的功能也拆 RMP 能力单元 | 过度设计,增加无谓间接层 | 直接写在类里 |
十三、RMP 模式的优势
- 代码复用:能力单元可以被多个类复用,避免重复代码
- 解耦:能力单元之间相互独立,降低耦合度
- 灵活性:可以自由组合不同的能力单元
- 可测试性:每个能力单元可以独立测试
- 清晰的职责划分:每个能力单元专注于单一职责
- 易于维护:功能模块化,便于理解和修改
RMP 模式是 Swift 面向协议编程的强大实践,特别适合构建复杂的、需要高度复用的功能模块。通过合理的协议设计和运行时属性绑定,可以创建出既灵活又可维护的代码结构。