带weak delegate的NSObject代理崩溃问题:原因及安全实现方案
问题描述
我正在调试一个实现了UIScrollViewDelegate代理的库,核心代码如下:
class ScrollViewDelegateProxy: NSObject, UIScrollViewDelegate { private weak var realDelegate: UIScrollViewDelegate? init(scrollView: UIScrollView) { super.init() self.realDelegate = scrollView.delegate scrollView.delegate = self } // MARK: - UIScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { // Do something... realDelegate?.scrollViewDidScroll?(scrollView) } // Other scroll delegate methods... // MARK: - Method Forwarding override func responds(to aSelector: Selector!) -> Bool { if let realDelegate { realDelegate.responds(to: aSelector) } else { super.responds(to: aSelector) } } override func forwardingTarget(for aSelector: Selector!) -> Any? { if let realDelegate, realDelegate.responds(to: aSelector) { realDelegate } else { super.forwardingTarget(for: aSelector) } } }
这个代理通过方法转发处理UIScrollViewDelegate以外的消息(比如滚动视图是UITableView时,其delegate是包含更多方法的UITableViewDelegate,需要把这些消息转发给真实代理)。
在UITableView场景中,偶尔会触发崩溃,错误信息:
'NSInvalidArgumentException', reason: '-[SomeLibrary.ScrollViewDelegateProxy tableView:viewForHeaderInSection:]: unrecognized selector sent to instance'
我推测崩溃原因:
- 当
realDelegate还存在时,responds(to:)对某个选择器返回true; - 之后
realDelegate被释放; - 后续调用该选择器时,
forwardingTarget(for:)找不到目标,且代理本身未实现该方法; - 最终触发未识别选择器异常。
我曾读过一篇关于iOS代理转发的文章,但不确定对应的Swift实现方式。想问:用weak属性配合方法转发的实现是否有安全隐患?安全的实现方案是什么?
解答
一、当前实现的安全隐患
你的推测完全正确,当前实现存在竞态条件隐患:
- 由于
realDelegate是weak属性,responds(to:)返回true后,realDelegate可能在方法调用前被释放; responds(to:)和forwardingTarget(for:)是两个独立的方法调用,中间没有原子性保障,系统可能先调用responds(to:)确认方法存在,再调用方法时realDelegate已经不存在;- 此时
forwardingTarget(for:)返回nil或父类对象,而代理本身没实现该方法,直接触发未识别选择器崩溃。
另外,当前responds(to:)的实现存在语法错误:分支中遗漏了return语句,导致逻辑完全失效,这也是潜在问题。
二、安全的实现方案
要解决这个问题,需要保证responds(to:)和方法调用的一致性,同时完善Objective-C运行时的方法转发全链路,必须覆盖四个关键方法:responds(to:)、forwardingTarget(for:)、methodSignature(for:)、forwardInvocation(_:)。
修正后的完整代码:
class ScrollViewDelegateProxy: NSObject, UIScrollViewDelegate { private weak var realDelegate: UIScrollViewDelegate? init(scrollView: UIScrollView) { super.init() self.realDelegate = scrollView.delegate scrollView.delegate = self } // MARK: - UIScrollViewDelegate func scrollViewDidScroll(_ scrollView: UIScrollView) { // Do something... realDelegate?.scrollViewDidScroll?(scrollView) } // Other scroll delegate methods... // MARK: - Runtime Method Forwarding override func responds(to aSelector: Selector!) -> Bool { // 先检查自身是否实现,再检查realDelegate是否存在且响应 if super.responds(to: aSelector) { return true } guard let delegate = realDelegate else { return false } return delegate.responds(to: aSelector) } override func forwardingTarget(for aSelector: Selector!) -> Any? { guard let delegate = realDelegate, delegate.responds(to: aSelector) else { return super.forwardingTarget(for: aSelector) } return delegate } // 当forwardingTarget返回nil时,提供方法签名兜底 override func methodSignature(for aSelector: Selector!) -> NSMethodSignature? { if let signature = super.methodSignature(for: aSelector) { return signature } guard let delegate = realDelegate else { return super.methodSignature(for: aSelector) } return delegate.methodSignature(for: aSelector) } // 最终消息转发入口,处理所有无法直接转发的情况 override func forwardInvocation(_ invocation: NSInvocation!) { if let delegate = realDelegate, delegate.responds(to: invocation.selector) { invocation.invoke(with: delegate) } else { super.forwardInvocation(invocation) } } }
关键改进点:
- 修复
responds(to:)的逻辑错误:补充return语句,先检查自身实现,再判断代理的响应状态; - 完善转发链路:添加
methodSignature(for:)和forwardInvocation(_:),当realDelegate已释放时,能通过这两个方法兜底,避免直接崩溃; - 状态一致性检查:每个转发相关方法都先判断
realDelegate是否存在,确保responds(to:)的返回结果和实际调用时的状态一致; - 消除竞态风险:即使
realDelegate在检查后被释放,forwardInvocation也能优雅处理(不执行或调用父类逻辑),不会触发未识别选择器异常。
额外建议:
- 如果代理需要处理
UITableView或UICollectionView的特定代理方法,可以让ScrollViewDelegateProxy遵循对应的协议,空实现可选方法,进一步规避未识别选择器问题; - 可以监听滚动视图的生命周期,在
realDelegate被释放时,主动将滚动视图的delegate重置为nil(需根据业务场景判断是否合适)。
内容的提问来源于stack exchange,提问作者Bechsh
相关产品推荐
相关产品推荐

