好。我一直在阅读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.
}
}
答案 0 :(得分:0)
这个问题是没有实际创建项目就无法预测的。
似乎Playground Swift引擎处理引用计数和范围的方式与应用引擎不同。它挂在东西上的时间更长。通过将整个内容包装在另一个已取消引用的范围中,我可能可以使其正常工作。
不幸的是,“ Mystery Spot”答案在这里不适用,因为这适用于更高级别的Timer方法。这是一个较低级别的GCD定时任务。