Попытка выяснить, откуда исходит таинственная сильная ссылка - PullRequest
0 голосов
/ 07 января 2019

OK. Я читал отличную книгу «Advanced Swift» от команды objc. Это довольно круто (но трудно читать). Это заставило меня переоценить то, как я работаю в Swift за последние 4 года. Я не буду связывать это здесь, потому что я не хочу, чтобы этот вопрос был помечен как спам.

Одна вещь, которую я делаю, - это создание набора инструментов хороших утилит общего назначения (или переписывание тех, которые у меня уже есть).

Одним из этих инструментов является базовый таймер GCD.

На игровой площадке, которую я добавлю, есть базовый класс таймера, а также класс тестирования и пара тестовых реализаций.

В одном из них я регистрирую делегата, а в другом - нет. Это влияет на то, вызывается ли deinit в основном тестовом экземпляре.

Похоже, что таймер GCD зависает от сильной ссылки на делегата, даже когда я явно удаляю эту ссылку.

Как вы увидите, даже когда таймер полностью освобожден (вызывается его deinit), он сохраняет сильную ссылку на свой объект делегата (поэтому основной deinit никогда не вызывается).

Строка 257 интересная. Если вы это прокомментируете, то таймер продолжает срабатывать, даже если он был разыменован. Я могу понять это, поскольку я предполагаю, что таймер GCD сохраняет строгую ссылку на свой eventHandler. Я мог бы избежать этого с помощью встроенного замыкания вместо ссылки на метод экземпляра. Это не имеет большого значения, так как явный вызов invalidate () вполне подходит.

Однако это заставляет меня задуматься о том, какие другие сильные ссылки хранятся. Переход к закрытому действию не решает основную проблему; а именно, что основной контекст сохраняет контекст, даже если он кажется осиротевшим.

Мне интересно, может ли кто-нибудь объяснить мне, почему основной (iAmADelegate) экземпляр сохраняется. Я провел весь день вчера, пытаясь понять это.

ОБНОВЛЕНИЕ Похоже, что это не происходит в реальном контексте приложения. Вот очень простой апплет, демонстрирующий те же тесты в контексте приложения для iOS .

Для справки, вот консоль для того, что я получаю. Если вы управляете детской площадкой, вы должны получить то же самое:

** 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 голосов
/ 11 января 2019

Проблема заключается в том, что нельзя было предсказать, не создавая проект.

Кажется, что движок Playground Swift работает со счетчиком ссылок и областью действия не так, как движок приложения. Это дольше висит на вещах. Я мог бы, вероятно, заставить его вести себя правильно, обернув все это в другую область видимости, которая разыменовывается.

К сожалению, ответ «Mystery Spot» здесь не применим, так как это относится к методу таймера более высокого уровня. Это низкоуровневая заданная по времени задача GCD.

...