You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带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属性配合方法转发的实现是否有安全隐患?安全的实现方案是什么?

解答

一、当前实现的安全隐患

你的推测完全正确,当前实现存在竞态条件隐患:

  1. 由于realDelegate是weak属性,responds(to:)返回true后,realDelegate可能在方法调用前被释放;
  2. responds(to:)和forwardingTarget(for:)是两个独立的方法调用,中间没有原子性保障,系统可能先调用responds(to:)确认方法存在,再调用方法时realDelegate已经不存在;
  3. 此时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)
        }
    }
}

关键改进点:

  1. 修复responds(to:)的逻辑错误:补充return语句,先检查自身实现,再判断代理的响应状态;
  2. 完善转发链路:添加methodSignature(for:)和forwardInvocation(_:),当realDelegate已释放时,能通过这两个方法兜底,避免直接崩溃;
  3. 状态一致性检查:每个转发相关方法都先判断realDelegate是否存在,确保responds(to:)的返回结果和实际调用时的状态一致;
  4. 消除竞态风险:即使realDelegate在检查后被释放,forwardInvocation也能优雅处理(不执行或调用父类逻辑),不会触发未识别选择器异常。

额外建议:

  • 如果代理需要处理UITableView或UICollectionView的特定代理方法,可以让ScrollViewDelegateProxy遵循对应的协议,空实现可选方法,进一步规避未识别选择器问题;
  • 可以监听滚动视图的生命周期,在realDelegate被释放时,主动将滚动视图的delegate重置为nil(需根据业务场景判断是否合适)。

内容的提问来源于stack exchange,提问作者Bechsh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 22:17:05