试图找出神秘的强引用来自何处

时间:2019-01-07 11:56:40

标签: ios swift grand-central-dispatch

好。我一直在阅读objc团队撰写的出色的“ Advanced Swift”书。非常棒(但读起来很难)。我不得不重新评估过去四年来我在Swift中做事的方式。我不会在这里链接它,因为我不想将此问题标记为垃圾邮件。

我正在做的一件事是建立一个具有良好通用用途的工具箱(或重写已经拥有的工具)。

这些工具之一是基本的GCD计时器。

在我要介绍的操场上,有一个基本的计时器类,还有一个测试类和几个测试实现。

在其中一个中,我注册了一个代表,而在另一个中,我没有注册。这就决定了是否在主测试实例上调用deinit。

即使我明确删除了该引用,GCD计时器也似乎挂在了对该委托的强引用上。

如您所见,即使完全释放了计时器(调用了它的deinit),它也保留了对其委托对象的强引用(因此,永远不会调用主deinit)。

第257行很有趣。如果您将其注释掉,则即使已取消引用计时器,计时器也会继续触发。我可以理解,因为我假设GCD计时器保留了对其eventHandler的强烈引用。我也许可以通过使用内联闭包而不是引用实例方法来避免这种情况。没什么大不了,因为显式调用invalidate()完全可以。

但是,这的确使我想知道还有哪些其他强引用。更改为内联闭包不能解决主要问题。也就是说,即使它是孤立的,主上下文仍保留上下文。

我想知道是否有人可以向我解释如何保留主(iAmADelegate)实例。昨天我花了一整天试图找出答案。

更新看起来这在实际的应用程序上下文中不会发生。 Here is a very basic applet that demonstrates the same tests in the context of an iOS app

记录下来,这是我所得到的控制台。如果您在操场上跑步,应该会得到相同的结果:

** Test With Delegate
main init
main creating a new timer
timer init
timer changing the delegate from nil to Optional(__lldb_expr_21.EventClass)
timer resume
timer create GCD object
main callback count: 0
main callback count: 1
main callback count: 2
main callback count: 3
main callback count: 4
main deleting the timer
timer invalidate
main callback count: 5
timer changing the delegate from Optional(__lldb_expr_21.EventClass) to nil
timer deinit
** Done

** Test Without Delegate
main init
main creating a new timer
timer init
timer resume
timer create GCD object
main deleting the timer
timer invalidate
timer deinit
** Done
main deinit

这是操场:

import Foundation

/* ################################################################## */
/**
 This is the basic callback protocol for the general-purpose GCD timer class. It has one simple required method.
 */
public protocol BasicGCDTimerDelegate: class {
    /* ############################################################## */
    /**
     Called periodically, as the GCDTimer repeats (or fires once).

     - parameter inTimer: The BasicGCDTimer instance that is invoking the callback.
     */
    func timerCallback(_ inTimer: BasicGCDTimer)
}

/* ################################################################## */
/**
 This is a general-purpose GCD timer class.

 It requires that an owning instance register a delegate to receive callbacks.
 */
public class BasicGCDTimer {
    /* ############################################################## */
    // MARK: - Private Enums
    /* ############################################################## */
    /// This is used to hold state flags for internal use.
    private enum _State {
        /// The timer is currently invalid.
        case _invalid
        /// The timer is currently paused.
        case _suspended
        /// The timer is firing.
        case _running
    }

    /* ############################################################## */
    // MARK: - Private Instance Properties
    /* ############################################################## */
    /// This holds our current run state.
    private var _state: _State = ._invalid
    /// This holds a Boolean that is true, if we are to only fire once (default is false, which means we repeat).
    private var _onlyFireOnce: Bool = false
    /// This contains the actual dispatch timer object instance.
    private var _timerVar: DispatchSourceTimer!
    /// This is the contained delegate instance
    private weak var _delegate: BasicGCDTimerDelegate?

    /* ############################################################## */
    /**
     This dynamically initialized calculated property will return (or create and return) a basic GCD timer that (probably) repeats.

     It uses the current queue.
     */
    private var _timer: DispatchSourceTimer! {
        if nil == _timerVar {   // If we don't already have a timer, we create one. Otherwise, we simply return the already-instantiated object.
            print("timer create GCD object")
            _timerVar = DispatchSource.makeTimerSource()                                    // We make a generic, default timer source. No frou-frou.
            let leeway = DispatchTimeInterval.milliseconds(leewayInMilliseconds)            // If they have provided a leeway, we apply it here. We assume milliseconds.
            _timerVar.setEventHandler(handler: _eventHandler)                               // We reference our own internal event handler.
            _timerVar.schedule(deadline: .now() + timeIntervalInSeconds,                    // The number of seconds each iteration of the timer will take.
                               repeating: (_onlyFireOnce ? 0 : timeIntervalInSeconds),      // If we are repeating (default), we add our duration as the repeating time. Otherwise (only fire once), we set 0.
                               leeway: leeway)                                              // Add any leeway we specified.
        }

        return _timerVar
    }

    /* ############################################################## */
    // MARK: - Private Instance Methods
    /* ############################################################## */
    /**
     This is our internal event handler that is called directly from the timer.
     */
    private func _eventHandler() {
        delegate?.timerCallback(self)   // Assuming that we have a delegate, we call its handler method.

        if _onlyFireOnce {  // If we are set to only fire once, we nuke from orbit.
            invalidate()
        }
    }

    /* ############################################################## */
    // MARK: - Public Instance Properties
    /* ############################################################## */
    /// This is the time between fires, in seconds.
    public var timeIntervalInSeconds: TimeInterval = 0
    /// This is how much "leeway" we give the timer, in milliseconds.
    public var leewayInMilliseconds: Int = 0

    /* ############################################################## */
    // MARK: - Public Calculated Properties
    /* ############################################################## */
    /**
     - returns: true, if the timer is invalid. READ ONLY
     */
    public var isInvalid: Bool {
        return ._invalid == _state
    }

    /* ############################################################## */
    /**
     - returns: true, if the timer is currently running. READ ONLY
     */
    public var isRunning: Bool {
        return ._running == _state
    }

    /* ############################################################## */
    /**
     - returns: true, if the timer will only fire one time (will return false after that one fire). READ ONLY
     */
    public var isOnlyFiringOnce: Bool {
        return _onlyFireOnce
    }

    /* ############################################################## */
    /**
     - returns: the delegate object. READ/WRITE
     */
    public var delegate: BasicGCDTimerDelegate? {
        get {
            return _delegate
        }

        set {
            if _delegate !== newValue {
                print("timer changing the delegate from \(String(describing: delegate)) to \(String(describing: newValue))")
                _delegate = newValue
            }
        }
    }

    /* ############################################################## */
    // MARK: - Deinitializer
    /* ############################################################## */
    /**
     We have to carefully dismantle this, as we can end up with crashes if we don't clean up properly.
     */
    deinit {
        print("timer deinit")
        self.invalidate()
    }

    /* ############################################################## */
    // MARK: - Public Methods
    /* ############################################################## */
    /**
     Default constructor

     - parameter timeIntervalInSeconds: The time (in seconds) between fires.
     - parameter leewayInMilliseconds: Any leeway. This is optional, and default is zero (0).
     - parameter delegate: Our delegate, for callbacks. Optional. Default is nil.
     - parameter onlyFireOnce: If true, then this will only fire one time, as opposed to repeat. Optional. Default is false.
     */
    public init(timeIntervalInSeconds inTimeIntervalInSeconds: TimeInterval,
                leewayInMilliseconds inLeewayInMilliseconds: Int = 0,
                delegate inDelegate: BasicGCDTimerDelegate? = nil,
                onlyFireOnce inOnlyFireOnce: Bool = false) {
        print("timer init")
        self.timeIntervalInSeconds = inTimeIntervalInSeconds
        self.leewayInMilliseconds = inLeewayInMilliseconds
        self.delegate = inDelegate
        self._onlyFireOnce = inOnlyFireOnce
    }

    /* ############################################################## */
    /**
     If the timer is not currently running, we resume. If running, nothing happens.
     */
    public func resume() {
        if ._running != self._state {
            print("timer resume")
            self._state = ._running
            self._timer.resume()    // Remember that this could create a timer on the spot.
        }
    }

    /* ############################################################## */
    /**
     If the timer is currently running, we suspend. If not running, nothing happens.
     */
    public func pause() {
        if ._running == self._state {
            print("timer suspend")
            self._state = ._suspended
            self._timer.suspend()
        }
    }

    /* ############################################################## */
    /**
     This completely nukes the timer. It resets the entire object to default.
     */
    public func invalidate() {
        if ._invalid != _state, nil != _timerVar {
            print("timer invalidate")
            delegate = nil
            _timerVar.setEventHandler(handler: nil)

            _timerVar.cancel()
            if ._suspended == _state {  // If we were suspended, then we need to call resume one more time.
                print("timer one for the road")
                _timerVar.resume()
            }

            _onlyFireOnce = false
            timeIntervalInSeconds = 0
            leewayInMilliseconds = 0
            _state = ._invalid
            _timerVar = nil
        }
    }
}

// Testing class.
class EventClass: BasicGCDTimerDelegate {
    var instanceCount: Int = 0  // How many times we've been called.
    var timer: BasicGCDTimer?   // Our timer object.
    let iAmADelegate: Bool

    // Just prints the count.
    func timerCallback(_ inTimer: BasicGCDTimer) {
        print("main callback count: \(instanceCount)")
        instanceCount += 1
    }

    // Set the parameter to false to remove the delegate registration.
    init(registerAsADelegate inRegisterAsADelegate: Bool = true) {
        print("main init")
        iAmADelegate = inRegisterAsADelegate
        isRunning = true
    }

    // This won't get called if we register as a delegate.
    deinit {
        print("main deinit")
        timer = nil
        isRunning = false
    }

    // This will create and initialize a new timer, if we don't have one. If we turn it off, it will destroy the timer.
    var isRunning: Bool {
        get {
            return nil != timer
        }

        set {
            if !isRunning && newValue {
                print("main creating a new timer")
                timer = BasicGCDTimer(timeIntervalInSeconds: 1.0, leewayInMilliseconds: 200, delegate: iAmADelegate ? self : nil)
                timer?.resume()
            } else if isRunning && !newValue {
                print("main deleting the timer")

                // MARK: - MYSTERY SPOT
                timer?.invalidate()  // If you comment out this line, the timer will keep firing, even though we dereference it.
                // MARK: -

                timer = nil
            }
        }
    }
}

// We instantiate an instance of the test, register it as a delegate, then wait six seconds. We will see updates.
print("** Test With Delegate")   // We will not get a deinit after this one.
let iAmADelegate: EventClass = EventClass()

// We create a timer, then wait six seconds. After that, we stop/delete the timer, and create a new one, without a delegate.
DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
    iAmADelegate.isRunning = false
    print("** Done")   // We will not get a deinit after this one.
    print("\n** Test Without Delegate")
    // Do it again, but this time, don't register as a delegate (it will be quiet).
    let iAmNotADelegate: EventClass = EventClass(registerAsADelegate: false)

    DispatchQueue.main.asyncAfter(deadline: .now() + 6) {
        iAmNotADelegate.isRunning = false
        print("** Done")   // We will get a deinit after this one.
    }
}

1 个答案:

答案 0 :(得分:0)

这个问题是没有实际创建项目就无法预测的。

似乎Playground Swift引擎处理引用计数和范围的方式与应用引擎不同。它挂在东西上的时间更长。通过将整个内容包装在另一个已取消引用的范围中,我可能可以使其正常工作。

不幸的是,“ Mystery Spot”答案在这里不适用,因为这适用于更高级别的Timer方法。这是一个较低级别的GCD定时任务。