How do I avoid memory leaks when calling async functions in a Timer.scheduledTimer's repeating code?

94 Views Asked by At

I am using an SDK which initiates a download, and am trying to build resiliency into my app if the app is closed before the download is complete. If the app is closed while the download still is happening, when the app is relaunched, the SDK returns an error. Example stub of SDK function.

self.sdkFunction() { error in 
   guard error == nil else {
      print("some error")
   }
   print("download complete")
}

I leverage some dispatch groups to create barriers because there are multiple downloads that all must succeed, but I do not think that is particularly relevant here so I have omitted it. If the downloads do not succeed, a function containing code like this is called:

DispatchQueue.main.async {
 self.timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { _ in
   print("Timer fired", Date())
   self.sdkFunction() { error in 
      guard error == nil else {
         print("some error")
      }
      print("download complete", Date())
      self.timer.invalidate()
   }
 }
}

What happens here is that every 5 seconds, I see the timer is fired. No error are printed. If I leave the app open, so the download can complete, eventually I will see "download complete" printed, but I see it printed multiple times all at once, one for each time the timer fired.

My assumption here is that the reason this is happening is the closure is being kept alive while the SDK's closure works in the background. Eventually, once the download is done, all of them are unblocked and continue to print. This seems to me like a memory leak because new memory space is allocated for each time the closure is called and not relinquished until after the download completes, if it does.


The behavior I observe is it seems sdkFunction only checks if the download has completed when it is called. Its closure will not be executed asynchronously again unless sdkFunction is called again. However, if called again and the download is complete, every prior invocation executes its closure. I cannot modify the code in the SDK, unfortunately.

Am I incorrectly capturing references here in a way that keep the previous closure invocations alive but in a sleeping state? Ideally I'd like each closure be terminated and have its memory reclaimed if the timer's interval has elapsed so there are not multiple asynchronous invocations running at once.

1

There are 1 best solutions below

0
Paulw11 On

From your description, sdkFunction is "broken". It should either:

  • Invoke the supplied completion handler immediately, indicating whether the operation is complete or not, or;
  • Not invoke the supplied completion handler until the operation either completes or fails, but it should do this without you needing to call sdkFunction again.

Yes, the repeated invocation of the function will consume memory, but there is nothing you can do about this, since it is sdkFunction that is holding the reference to the closure and it will hold this reference until it invokes the closure and (presumably) releases its reference to it

A memory leak is where memory is allocated and never released.

If sdkFunction is not releasing the reference to the closure, then a [weak self] will help:

self.sdkFunction() { [weak self], error in 
      guard error == nil else {
         print("some error")
      }
      print("download complete", Date())
      self.timer.invalidate()
   }

The other approach you could take, if the completion handler to sdkFunction is optional, is to detect if you already have a pending handler. Something like

var handlerQueued=false

func completion(error:Error?) {
      guard error == nil else {
         print("some error")
      }
      print("download complete", Date())
      self.timer.invalidate()
      self.timer = nil
      self.handlerQueued = false
}

DispatchQueue.main.async {
 self.timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { _ in
   print("Timer fired", Date())
   self.sdkFunction(completion: self.handlerQueued: nil ? completion)
   self.handlerQueued = true
 }
}