为 UIControl 实现线程安全的 Block 事件扩展:原理与实践
一、背景与痛点
在 iOS 开发中,UIControl
的 Target-Action 机制是处理用户交互事件的标准方式。然而当面临以下场景时,传统方案会暴露明显缺陷:
- 线程安全问题:在后台线程修改事件监听可能导致 UI 操作不同步
- 代码冗余:需为每个事件单独创建 selector 方法
- 维护成本:难以批量移除特定类型的事件监听
- 内存风险:容易因循环引用导致内存泄漏
代码如下:
import UIKit
// MARK: - 线程安全关联对象键
private struct AssociatedKeys {
@MainActor
static var blockKey: UInt8 = 0
}
// MARK: - Block 事件包装类
final class ControlBlockTarget: NSObject {
var block: ((UIControl) -> Void)?
var events: UIControl.Event
init(block: ((UIControl) -> Void)?, events: UIControl.Event) {
self.block = block
self.events = events
super.init()
}
@objc func invoke(sender: UIControl) {
block?(sender)
}
}
// MARK: - 线程安全扩展
extension UIControl {
// MARK: - Public Methods
/// 添加 Block 事件(线程安全)
public func sb_addBlock(for events: UIControl.Event, block: @escaping (UIControl) -> Void) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
let target = ControlBlockTarget(block: block, events: events)
self.addTarget(target, action: #selector(ControlBlockTarget.invoke(sender:)), for: events)
self.blockTargets.append(target)
}
}
/// 设置唯一 Block(覆盖原有同类型事件)
public func sb_setBlock(for events: UIControl.Event, block: @escaping (UIControl) -> Void) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
// 原子化移除指定事件的所有处理
self.sb_removeAllBlocks(for: events)
// 添加新处理
let target = ControlBlockTarget(block: block, events: events)
self.addTarget(target, action: #selector(ControlBlockTarget.invoke(sender:)), for: events)
self.blockTargets.append(target)
}
}
/// 移除指定事件的所有 Block
public func sb_removeAllBlocks(for events: UIControl.Event) {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
var remainingTargets: [ControlBlockTarget] = []
for target in self.blockTargets {
if target.events == events {
// 完全匹配时移除
self.removeTarget(target, action: nil, for: target.events)
} else if target.events.contains(events) {
// 处理事件交集的情况
let newEvents = target.events.subtracting(events)
if !newEvents.isEmpty {
self.removeTarget(target, action: nil, for: target.events)
target.events = newEvents
self.addTarget(target, action: #selector(ControlBlockTarget.invoke(sender:)), for: newEvents)
remainingTargets.append(target)
} else {
self.removeTarget(target, action: nil, for: target.events)
}
} else {
remainingTargets.append(target)
}
}
self.blockTargets = remainingTargets
}
}
/// 原子化移除所有 Block
public func sb_removeAllBlocks() {
sb_removeAllBlocks(for: .allEvents)
}
// MARK: - Private Properties
private var blockTargets: [ControlBlockTarget] {
get {
objc_getAssociatedObject(self, &AssociatedKeys.blockKey) as? [ControlBlockTarget] ?? []
}
set {
objc_setAssociatedObject(
self,
&AssociatedKeys.blockKey,
newValue,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
}
}
}
使用示例
let button = UIButton()
button.sb_addBlock(for: .touchUpInside) { [weak self] _ in
self?.handleLogin()
}
// 替换现有事件
button.sb_setBlock(for: .touchDragExit) { _ in
print("拖动取消操作")
}
// 批量移除事件
view.sb_removeAllBlocks(for: .allTouchEvents)
注意事项
- 不可嵌套调用:避免在 Block 内执行
sb_removeAllBlocks
- 优先级控制:与系统
addTarget
方法混用时注意执行顺序 - 性能监控:在列表滚动等高频场景建议使用节流包装器
- 调试技巧:通过
po blockTargets
在 LLDB 查看当前事件绑定状态
本文是原创文章,采用 CC BY-NC-ND 4.0 协议,完整转载请注明来自 风屋
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果