如何在 Swift 中使用目标和 CADisplayLink 实例之间的弱引用设置 CADisplayLink

How to set CADisplayLink in Swift with weak reference between target and CADisplayLink instance

在Objective-C中,我们可以用代理模式初始化CADisplayLink来打破强引用:

WeakProxy *weakProxy = [WeakProxy weakProxyForObject:self];
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];

然后,将dealloc中的displayLink作废即可:

- (void)dealloc
{
    [_displayLink invalidate];
}

但是,NSProxy 似乎无法在 Swift 中继承:https://bugs.swift.org/browse/SR-1715

我试着这样写:

weak var weakSelf = self    
displayLink = CADisplayLink(target: weakSelf!, selector: #selector(displayDidRefresh(dpLink:)))

没用。

我想知道是否有任何方法可以像 Objective-C 那样实现。

更好的方法可能是使 link 中的显示无效 viewWill/DidDisappear,另见

获取有用信息。

如果这不是一个选项: 使代理对象继承自 NSObject 而不是 NSProxy。例如 Objective-C 解决方案 此处给出

  • CADisplayLink at iOS 6.0 not retaining target

并且可以很容易地转换为 Swift 3:

class JAWeakProxy: NSObject {
    weak var target: NSObjectProtocol?

    init(target: NSObjectProtocol) {
        self.target = target
        super.init()
    }

    override func responds(to aSelector: Selector!) -> Bool {
        return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
    }

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

然后可以用作

displayLink = CADisplayLink(target: JAWeakProxy(target: self),
                            selector: #selector(didRefresh(dpLink:)))

你的方法

weak var weakSelf = self    
displayLink = CADisplayLink(target: weakSelf!, selector: #selector(displayDidRefresh(dpLink:)))

不起作用,因为它在 CADisplayLink 时展开 weakSelf 已初始化并传递对 self 的强引用作为目标。

这个代理 class 应该可以正常工作。不要忘记在 dealloc 之前失效。

import UIKit

class CADisplayLinkProxy {

    var displaylink: CADisplayLink?
    var handle: (() -> Void)?

    init(handle: (() -> Void)?) {
        self.handle = handle
        displaylink = CADisplayLink(target: self, selector: #selector(updateHandle))
        displaylink?.add(to: RunLoop.current, forMode: .common)
    }

    @objc func updateHandle() {
        handle?()
    }

    func invalidate() {
        displaylink?.remove(from: RunLoop.current, forMode: .common)
        displaylink?.invalidate()
        displaylink = nil
    }
}

用法:

class ViewController: UIViewController {

    var displaylinkProxy: CADisplayLinkProxy?

    override func viewDidLoad() {
        super.viewDidLoad()
        displaylinkProxy = CADisplayLinkProxy(handle: { [weak self] in
            self?.updateAnything()
        })
    }

    @objc func updateAnything() {
        print(Date())
    }
}

另一个解决方案,这个解决方案从其外部 API 隐藏代理/objc 运行时。 只要 DisplayLink 被变量引用,它就会一直存在。 一旦变量超出范围或设置为 nil,CADisplayLink 就会失效,因此也可以取消初始化目标。

import Foundation
import UIKit

/// DisplayLink provides a block based interface for CADisplayLink.
/// The CADisplayLink is invalidated upon DisplayLink deinit.
///
/// Usage:
/// ```
/// let displayLink = DisplayLink { caDisplayLink in print("Next frame scheduled \(caDisplayLink.targetTimestamp)") }
/// ```
///
/// Note: Keep a reference to the DisplayLink.
final class DisplayLink {
    let displayLink: CADisplayLink

    init(runloop: RunLoop? = .main, prepareNextFrame: @escaping (CADisplayLink) -> ()) {
        displayLink = CADisplayLink(
            target: DisplayLinkTarget(prepareNextFrame),
            selector: #selector(DisplayLinkTarget.prepareNextFrame))

        if let runloop = runloop {
            displayLink.add(to: runloop, forMode: .default)
        }
    }

    deinit {
        displayLink.invalidate()
    }
}

private class DisplayLinkTarget {
    let callback: (CADisplayLink) -> ()

    init(_ callback: @escaping (CADisplayLink) -> ()) {
        self.callback = callback
    }

    @objc func prepareNextFrame(displaylink: CADisplayLink) {
        callback(displaylink)
    }
}