今天在使用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 / 方法未实现。
解决方法
- 可以在中间层添加对应的方法,以便让 CADisplayLink 的 selector 检查通过。
- 可以使用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)
}
}