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)

}

}

相关推荐
molunnnn7 小时前
第四章 Agent的几种经典范式
开发语言·python
洛_尘7 小时前
JAVA EE初阶 2: 多线程-初阶
java·开发语言
@卞8 小时前
C语言常见概念
c语言·开发语言
wjs20248 小时前
Eclipse 关闭项目详解
开发语言
沐知全栈开发8 小时前
《隐藏(Hide)》
开发语言
lkbhua莱克瓦249 小时前
Java基础——方法
java·开发语言·笔记·github·学习方法
catchadmin9 小时前
PHP 依赖管理器 Composer 2.9 发布
开发语言·php·composer
范纹杉想快点毕业9 小时前
《嵌入式开发硬核指南:91问一次讲透底层到架构》
java·开发语言·数据库·单片机·嵌入式硬件·mongodb