代码之家  ›  专栏  ›  技术社区  ›  Chris Marshall

试图找出一个神秘的强引用来自哪里

  •  -1
  • Chris Marshall  · 技术社区  · 6 年前

    好啊。我一直在读objc机组人员的优秀的“先进的雨燕”书。这本书非常棒(但读起来很难)。这让我重新评估了过去4年我在斯威夫特的工作方式。我不会链接到这里,因为我不希望这个问题被标记为垃圾邮件。

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

    其中一个工具是基本的GCD计时器。

    在我将要附加的操场中,有一个基本的计时器类,以及一个测试类和两个测试实现。

    在其中一个,我注册了一个代表,而另一个,我不注册。这会影响是否在主测试实例上调用deinit。

    看起来gcd计时器挂起了对委托的强引用,即使我显式地删除了该引用。

    正如您将看到的,即使计时器完全取消分配(调用了其deinit),它仍保留对其委托对象的强引用(因此从不调用主deinit)。

    第257行很有趣。如果您将其注释掉,那么计时器将继续启动,即使它已被取消引用。我可以理解这一点,因为我假设GCD计时器保留对其事件处理程序的强引用。我可以通过使用内联闭包而不是引用实例方法来避免这种情况。其实没什么关系,因为显式调用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 回复  |  直到 5 年前
        1
  •  0
  •   E_net4 Tunn    5 年前

    问题是,如果没有实际创建一个项目,就无法预测。

    游乐场Swift引擎处理参考计数和范围似乎与应用程序引擎有所不同。它能把事情拖得更久。我可以通过将整个东西包装在另一个被取消引用的范围中,使它正确地工作。

    不幸的是,“神秘点”的答案不适用于这里,因为这是针对高级计时器的方法。这是一个较低级别的GCD定时任务。