I noticed the other day my iOS app is leaking memory like a sieve (I wrote some automated tests that did long run thrashing and then I noticed the app just grows in size until it dies).
I had long suspected, that I was going to get more disciplined about closure variables. I come from a Smalltalk background where Closures Just Worked(tm). So I've spent the day trying to figure out where my app is leaking memory. The Xcode tools so far haven't been much help. When I do the Debug Memory Graph
thing, it tells me I have lots of Arrays of various UIElements, that seem to correspond to outlet collections, but doesn't really make it clear why it's a leak.
But I thought surely I have some leaks due to closures. A while ago I had my own Ticker (a periodic timer) object:
class Ticker {
// MARK: - Properties Stored
var interval:Duration = 1.seconds
private var queue = DispatchQueue.main
private var source: DispatchSourceTimer!
var tick:()->() = {}
// MARK: - accessing
func start() {
self.stop()
self.source = DispatchSource.makeTimerSource(queue: self.queue)
let nsgap = DispatchTimeInterval.microseconds(Int(self.interval.microseconds.rounded().magnitude))
self.source.schedule(deadline: DispatchTime.now() + nsgap, repeating: nsgap, leeway: DispatchTimeInterval.seconds(0))
self.source.setEventHandler(handler: self.tick)
self.source.resume()
}
func stop() {
if self.source != nil {
self.source.cancel()
self.source = nil
}
}
}
It should go away probably and be replaced by just direct use of GCD or the native Timer object. Then I have a UIController subclass that first has one of these as a variables:
class MyController: UIViewController {
var scanTimer:Ticker = Ticker()
...
And later has a method that looks like this:
@IBAction func startScanning() {
self.stopScanning()
...
self.scanTimer = Ticker()
self.scanTimer.interval = 500.milliseconds
var tickCount = 0
self.scanTimer.tick = {
tickCount += 1
if tickCount > 20 {
self.stopScanning()
}
if self.bleBecameActive {
self.bleBecameActive = false
self.startBLEScan()
}
}
self.scanTimer.start()
}
The way I understand things... my controller has a hard reference to the timer. The timer has a hard reference to a closure (the tick
variable). And the closure has a reference to self
. Shouldn't that create a cycle?
But the Debug Memory Graph doesn't show this as an issue at all. Why not? Is it a) not really an issue or b) the tool isn't doing what I think it is?