2

This is a next chapter of a question I asked a while back. I have this simplified Timer, it is inspired by the Timer object in Matt Neuburg's book.

import Foundation
import XCPlayground

class Timer {
    private var queue = dispatch_queue_create("timer", nil)
    private var source: dispatch_source_t!
    var tick:()->() = {}
    var interval:NSTimeInterval = 1

    func start() {
        self.stop()
        self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue)
        dispatch_source_set_timer(source, DISPATCH_TIME_NOW, UInt64(self.interval) * 1000000000, 0)
        dispatch_source_set_event_handler(source, self.tick)
        dispatch_resume(self.source)
    }

    func stop() {
        if self.source != nil {
            dispatch_suspend(self.source)
        }
    }

}

var timer = Timer()
timer.interval = 5
timer.tick = { print("now \(NSDate())") }
timer.start()

timer.stop()
timer = Timer() // <<--BAD STUFF HAPPENS HERE

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

The problem is the point where I assign a new timer to the same variable. The basic use case is that I'm done with a timer, so I stop it, but then perhaps want to create a new one and launch it.

In a playground, that marked line gives the error:

Execution was interrupted, reason: EXC_BAD_INSTRUCTION {code=EXC_I386_INVOP, subcode=0x0).

If I do something similar in my iOS app, I get an exception trace that looks like:

0: _dispatch_xref_dispose
5: MyViewController.scanTimer.setter
....

It doesn't seem to matter if it's a struct or a class. Same thing happens. I wondered if I needed a deinit, but I haven't found an implementation of that that makes it go away.

Community
  • 1
  • 1
Travis Griggs
  • 21,522
  • 19
  • 91
  • 167

1 Answers1

5

What is actually causing your code to crash is when the system tries to dispose of the event source. You can see this by calling start() twice in a row using your class. During the second invocation you will see that the application crashes trying to create a new event source and assign it to the source field.

It is also true that if you don't call dispatch_suspend for the event source, that the crash never happens. You can see this in your example if you comment out the call to stop() before replacing the timer.

I cannot explain that behavior. I don't know why calling dispatch_suspend causes the crash when disposing of the event source. It looks to me like a bug that you should report to Apple.

Having said that, it is not clear in your code why you would call dispatch_suspend and not dispatch_source_cancel. When you call stop() on your timer you are done with the dispatch source. If you were to call start() again, you would get a brand new event source anyway. I recommend changing your stop() function to:

func stop() {
    if self.source != nil {
        dispatch_source_cancel(self.source)
        self.source = nil
    }
}

This has the added benefit of working around the crash.

If you will take the advice, I also recommend replacing your hard-coded constant with the dispatch library symbolic constant for the number of nanoseconds in a second:

dispatch_source_set_timer(source, DISPATCH_TIME_NOW,
    UInt64(self.interval) * NSEC_PER_SEC, 0)

I suggest this because the number of zeros is easy to get wrong and using the constant helps the reader understand what the code is actually doing.

Scott Thompson
  • 22,629
  • 4
  • 32
  • 34