Swift + CADisplayLink 弱引用代理(Proxy 模式) 里的陷阱

今天在使用CADisplayLink做刷新动画时为防止循环引用使添加了中间层来打破循环强引用,在使用过程中打算中间层采用了消息转发处理页面刷新,在VC中使用可以正常使用,但是在cell中嵌套collection View后使用此方式遇到了方法找不到的问题。最初的代码如下:

Swift 复制代码
class TextScrollingProxy: NSObject {
    weak var target: AnyObject?
    init(target: AnyObject) {
        self.target = target
        super.init()
    }
override func responds(to aSelector: Selector!) -> Bool {
//方法调用
        return target?.responds(to: aSelector) ?? false
    }
override func forwardingTarget(for aSelector: Selector!) -> Any? {
//消息转发
        return target
    }
}
     public func startAutoScroll() {
        stopAutoScroll()
         let proxy = CHWeakProxy(target: self)
        displayLinkProxy = proxy
displayLink = CADisplayLink(target: proxy, selector: #selector(scrollCollectionView))
        displayLink?.preferredFramesPerSecond = 30
        displayLink?.add(to: .main, forMode: .common)

    }
    
    public func stopAutoScroll() {
        displayLink?.remove(from: .main, forMode: .common)
        displayLink?.invalidate()
        displayLink = nil
    }

    @objc
    private func scrollCollectionView() {

        let collectionView = collectionView
        let totalWidth = collectionView.contentSize.width
        let currentOffsetX = collectionView.contentOffset.x
        let screenWidth = collectionView.frame.width
        let newOffsetX = currentOffsetX + 0.3 // 控制速度

        if newOffsetX >= totalWidth - screenWidth {
            collectionView.setContentOffset(.zero, animated: false)
        } else {
            collectionView.setContentOffset(CGPoint(x: newOffsetX, y: 0), animated: false)
        }
    }

class TextScrollingProxy: NSObject {

weak var target: AnyObject?

init(target: AnyObject) {

self.target = target

super.init()

}

override func responds(to aSelector: Selector!) -> Bool {

//方法调用

return target?.responds(to: aSelector) ?? false

}

override func forwardingTarget(for aSelector: Selector!) -> Any? {

//消息转发

return target

}

}

public func startAutoScroll() {

stopAutoScroll()

let proxy = CHWeakProxy(target: self)

displayLinkProxy = proxy

displayLink = CADisplayLink(target: proxy, selector: #selector(scrollCollectionView))

displayLink?.preferredFramesPerSecond = 30

displayLink?.add(to: .main, forMode: .common)

}

public func stopAutoScroll() {

displayLink?.remove(from: .main, forMode: .common)

displayLink?.invalidate()

displayLink = nil

}

@objc

private func scrollCollectionView() {

let collectionView = collectionView

let totalWidth = collectionView.contentSize.width

let currentOffsetX = collectionView.contentOffset.x

let screenWidth = collectionView.frame.width

let newOffsetX = currentOffsetX + 0.3 // 控制速度

if newOffsetX >= totalWidth - screenWidth {

collectionView.setContentOffset(.zero, animated: false)

} else {

collectionView.setContentOffset(CGPoint(x: newOffsetX, y: 0), animated: false)

}

}

在使用中报错TextScrollingProxy的scrollCollectionView方法未实现导致崩溃。从代码层面来看TextScrollingProxy 写法本身没错,转发逻辑是对的。

初步怀疑:

1、 CADisplayLink 的初始化阶段有个坑:

它在创建时就会立即检查 target 是否响应指定 selector,

如果 target(即你的 proxy)自己不实现该方法,

即使你在 forwardingTarget 里做了转发,也会直接崩溃。

也就是这句触发的:

displayLink = CADisplayLink(target: proxy, selector: #selector(scrollCollectionView))

系统内部会调用:

if (![target respondsToSelector:selector]) {

// crash!

}

如果在VC中可以正常使用上述问题也就不存在

2、 Cell 会被 复用和释放;

CADisplayLink 的 target 持有了 proxy,然后 proxy 转发到 已经被回收或正在复用的 cell;selector 调用时找不到方法 → unrecognized selector。

cell 有复用机制,应该不会被释放

继续大胆猜测

proxy.target 是 weak 引用;(取消weak可以正常使用,但违背了初衷)

即使 Cell 没被释放,如果 Cell 在 UICollectionView 的 复用队列里,某些时刻它可能暂时不在屏幕上;

CADisplayLink 每帧调用 selector 时,运行时会先看 proxy.target 是否非 nil;

如果被 dequeued 还没调用 prepareForReuse / willDisplay/layout,proxy.target 有可能是 nil → selector 无法找到 → unrecognized selector。

这就是 VC 正常、Cell 崩溃的原因:Cell 的状态可能在 DisplayLink tick 时不稳定。

这里也希望知道的宝子们帮忙指教......

根本原因

TextScrollingProxy 是中间转发层,它自己没有实现 scrollCollectionView。

虽然你覆写了 responds(to:) 去问 target,但此时 target 是 weak,

Swift runtime 检查时认为 proxy 本身没有实现 → 报

❌ unrecognized selector / 方法未实现。

解决方法

  1. 可以在中间层添加对应的方法,以便让 CADisplayLink 的 selector 检查通过。
  2. 可以使用block来解决(推荐)

在block中使用若引用更为便捷

Swift 复制代码
class TextScrollingProxy: NSObject {
//保存回调
    private let callback : () -> Void
    init(_ callback: @escaping () -> Void) {
        self.callback = callback
    }
@objc func tick(){
//执行回调
        callback()
    }
}
public func startAutoScroll() {
        stopAutoScroll()
//初始化中间层
        let proxy = TextScrollingProxy { [weak self] in
            self?.scrollCollectionView()
        }
        displayLinkProxy = proxy
//#selector(TextScrollingProxy.tick))中将要确定执行中间层的什么方法
 displayLink = CADisplayLink(target: proxy, selector: #selector(TextScrollingProxy.tick))
        displayLink?.preferredFramesPerSecond = 30
        displayLink?.add(to: .main, forMode: .common)
    }
    
    public func stopAutoScroll() {
        displayLink?.remove(from: .main, forMode: .common)
        displayLink?.invalidate()
        displayLink = nil
    }

    @objc
    private func scrollCollectionView() {

        let collectionView = collectionView
        let totalWidth = collectionView.contentSize.width
        let currentOffsetX = collectionView.contentOffset.x
        let screenWidth = collectionView.frame.width
        let newOffsetX = currentOffsetX + 0.3 // 控制速度

        if newOffsetX >= totalWidth - screenWidth {
            collectionView.setContentOffset(.zero, animated: false)
        } else {
            collectionView.setContentOffset(CGPoint(x: newOffsetX, y: 0), animated: false)
        }
    }

class TextScrollingProxy: NSObject {

//保存回调

private let callback : () -> Void

init(_ callback: @escaping () -> Void) {

self.callback = callback

}

@objc func tick(){

//执行回调

callback()

}

}

public func startAutoScroll() {

stopAutoScroll()

//初始化中间层

let proxy = TextScrollingProxy { [weak self] in

self?.scrollCollectionView()

}

displayLinkProxy = proxy

//#selector(TextScrollingProxy.tick))中将要确定执行中间层的什么方法

displayLink = CADisplayLink(target: proxy, selector: #selector(TextScrollingProxy.tick))

displayLink?.preferredFramesPerSecond = 30

displayLink?.add(to: .main, forMode: .common)

}

public func stopAutoScroll() {

displayLink?.remove(from: .main, forMode: .common)

displayLink?.invalidate()

displayLink = nil

}

@objc

private func scrollCollectionView() {

let collectionView = collectionView

let totalWidth = collectionView.contentSize.width

let currentOffsetX = collectionView.contentOffset.x

let screenWidth = collectionView.frame.width

let newOffsetX = currentOffsetX + 0.3 // 控制速度

if newOffsetX >= totalWidth - screenWidth {

collectionView.setContentOffset(.zero, animated: false)

} else {

collectionView.setContentOffset(CGPoint(x: newOffsetX, y: 0), animated: false)

}

}

相关推荐
测试员周周2 小时前
【Appium 系列】第16节-WebView-H5上下文切换 — 混合应用的自动化难点
运维·开发语言·人工智能·功能测试·appium·自动化·测试用例
杜子不疼.5 小时前
【C++ AI 大模型接入 SDK】 - DeepSeek 模型接入(上)
开发语言·c++·chatgpt
加号35 小时前
【C#】 串口通信技术深度解析及实现
开发语言·c#
sycmancia6 小时前
Qt——编辑交互功能的实现
开发语言·qt
石山代码6 小时前
C++ 内存分区 堆区
java·开发语言·c++
无风听海6 小时前
C# 隐式转换深度解析
java·开发语言·c#
一只大袋鼠7 小时前
Git 进阶(二):分支管理、暂存栈、远程仓库与多人协作
java·开发语言·git
LuminousCPP8 小时前
数据结构 - 线性表第四篇:C 语言通讯录优化升级全记录(踩坑 + 思考)
c语言·开发语言·数据结构·经验分享·笔记·学习
web3.08889998 小时前
1688 图搜接口(item_search_img / 拍立淘) 接入方法
开发语言·python
один but you9 小时前
从可变参数到 emplace:现代 C++ 性能优化的核心组合
java·开发语言