Called endBackgroundTask but not working

When my app enter to background, I start a background task, and when Expiration happens, I end my background task. The code likes below:

backgroundTask = [[UIApplication sharedApplication]  beginBackgroundTaskWithExpirationHandler:^{
			dispatch_async(dispatch_get_main_queue(), ^{
				if (backgroundTask != UIBackgroundTaskInvalid) {
					[[UIApplication sharedApplication] endBackgroundTask:backgroundTask];
					backgroundTask = UIBackgroundTaskInvalid;
					[self cancel];
				}
			});
}];

When the breakpoint is triggered at the endBackgroundTask line, I also get the following log:

[BackgroundTask] Background task still not ended after expiration handlers were called: <UIBackgroundTaskInfo: 0x282d7ab40>: taskID = 36, taskName = Called by MyApp, from MyMethod, creationTime = 892832 (elapsed = 26). This app will likely be terminated by the system. Call UIApplication.endBackgroundTask(:) to avoid this.

The log don't appear every time, so why is that? Is there something wrong with my code?

Replies

Is there something wrong with my code?

Almost certainly. Problems like this are almost always caused by you accidentally ‘leaking’ a background task. The API makes that easy to do because the -beginBackgroundTaskWithExpirationHandler: method returns the task ID but takes an expiration handler that needs to access the task ID. This means the the task ID must be stored in some sort of global and that opens you up to a world of potential state management problems.

What I usually do is wrap this stuff into a class and then use that class as my abstraction layer. This gives a one-to-one mapping between objects and task IDs, which makes it easier for me to track their allocation. Pasted in below is a sketch of what that might look like (sorry it’s in Swift, that’s what I had lying around).

ps I have a post, UIApplication Background Task Notes, that has a whole bunch of other hints and tips regarding this API.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


/// Prevents the process from suspending by holding a `UIApplication` background
/// task assertion.
///
/// The assertion is released if:
///
/// * You explicitly release the assertion by calling ``release()``.
/// * There are no more strong references to the object and so it gets deinitialised.
/// * The system ‘calls in’ the assertion, in which case it calls the
///   ``systemDidReleaseAssertion`` closure, if set.
///
/// You should aim to explicitly release the assertion yourself, as soon as
/// you’ve completed the work that the assertion covers.

final class QRunInBackgroundAssertion {
    
    /// The name used when creating the assertion.

    let name: String
    
    /// Called when the system releases the assertion itself.
    ///
    /// This is called on the main thread.
    ///
    /// To help avoid retain cycles, the object sets this to `nil` whenever the
    /// assertion is released.

    var systemDidReleaseAssertion: (() -> Void)? {
        willSet { dispatchPrecondition(condition: .onQueue(.main)) }
    }

    private var taskID: UIBackgroundTaskIdentifier
    
    /// Creates an assertion with the given name.
    ///
    /// The name isn’t used by the system but it does show up in various logs so
    /// it’s important to choose one that’s meaningful to you.
    ///
    /// Must be called on the main thread.

    init(name: String) {
        dispatchPrecondition(condition: .onQueue(.main))
        self.name = name
        self.systemDidReleaseAssertion = nil
        // Have to initialise `taskID` first so that I can capture a fully
        // initialised `self` in the expiration handler.  If the expiration
        // handler ran /before/ I got a chance to set `self.taskID` to `t`,
        // things would end badly.  However, that can’t happen because I’m
        // running on the main thread — courtesy of the Dispatch precondition
        // above — and the expiration handler also runs on the main thread.
        self.taskID = .invalid
        let t = UIApplication.shared.beginBackgroundTask(withName: name) {
            self.taskDidExpire()
        }
        self.taskID = t
    }
    
    /// Release the assertion.
    ///
    /// It’s safe to call this redundantly, that is, call it twice in a row or
    /// call it on an assertion that’s expired.
    ///
    /// Must be called on the main thread.

    func release() {
        dispatchPrecondition(condition: .onQueue(.main))
        self.consumeValidTaskID() { }
    }
    
    deinit {
        // We don’t apply this assert because it’s hard to force the last object
        // reference to be released on the main thread.  However, it should be
        // safe to call through to `consumeValidTaskID(_:)` because no other
        // thread can be running inside this object (because that would have its
        // own retain on us).
        //
        // dispatchPrecondition(condition: .onQueue(.main))
        self.consumeValidTaskID() { }
    }
    
    private func consumeValidTaskID(_ body: () -> Void) {
        // Move this check to all clients except the deinitialiser.
        //
        // dispatchPrecondition(condition: .onQueue(.main))
        guard self.taskID != .invalid else { return }
        UIApplication.shared.endBackgroundTask(self.taskID)
        self.taskID = .invalid
        body()
        self.systemDidReleaseAssertion = nil
    }
    
    private func taskDidExpire() {
        dispatchPrecondition(condition: .onQueue(.main))
        self.consumeValidTaskID() {
            self.systemDidReleaseAssertion?()
        }
    }
}

Version History

  • 2023-08-11 Fixed a horrendous bug where QRunInBackgroundAssertion was failing to call endBackgroundTask(_:) at all. There’s no excuse for this; it was just a case of ‘forward actuator stick error’.

  • 2023-05-26 While working on the ProcessInfo version of the code (see below) I ended up tweaking this code a little, so I decided to update this post with that new version. I didn’t make any big conceptual changes, just a bunch of comments and clarifications.

  • 2023-05-05 First posted.

Oh, and this week I had need of this code in a watchOS app, where there’s no access to UIApplication, so I wrote a version of the code that uses ProcessInfo.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

/// Prevents the process from suspending by holding a `ProcessInfo` expiry
/// activity assertion.
///
/// The assertion is released if:
///
/// * You explicitly release the assertion by calling ``release()``.
/// * There are no more strong references to the object and so it gets
///   deinitialised.
/// * The system ‘calls in’ the assertion, in which case it calls the
///   ``systemDidReleaseAssertion`` closure, if set.
///
/// You should aim to explicitly release the assertion yourself, as soon as
/// you’ve completed the work that the assertion covers.
///
/// This uses `performExpiringActivity(withReason:using:)`, which… well… how to
/// say this kindly… has some very odd design characteristics.  The API kinda
/// makes sense if you’re doing CPU bound work but its design does not work well
/// if you’re doing something I/O bound, like networking (r. 109839489). And
/// that’s the primary use case for this class.  The end result is that you have
/// to waste a thread that’s just sitting inside the expiry closure doing
/// nothing. Moreover, this is a Dispatch worker thread, so there’s a limit to
/// how many times you can do this.  So, you have to be _really_ careful not to
/// allocate too many instances of this class.
///
/// I could fix this by having all the instances share a single assertion but…
/// well… let’s just say this code is already complicated.

final class QRunInBackgroundAssertionEx {

    /// The name used when creating the assertion.

    let name: String
    
    /// Called when the system releases the assertion itself.
    ///
    /// This is called on the main thread.
    /// 
    /// To help avoid retain cycles, the object sets this to `nil` whenever the
    /// assertion is released.

    var systemDidReleaseAssertion: (() -> Void)? {
        willSet { dispatchPrecondition(condition: .onQueue(.main)) }
    }

    // I would’ve liked to use `OSAllocatedUnfairLock` but it requires iOS 16.
    // In its absence, be aware that `stateLock` protects… you guess it!…
    // `state.

    private let stateLock: NSLock = NSLock()
    private enum State {
        case starting
        case started(DispatchSemaphore)
        case released
    }
    private var state: State

    /// Creates an assertion with the given name.
    ///
    /// The name isn’t used by the system but it does show up in various logs so
    /// it’s important to choose one that’s meaningful to you.
    ///
    /// Must be called on the main thread.

    init(name: String) {
        dispatchPrecondition(condition: .onQueue(.main))
        self.name = name
        self.systemDidReleaseAssertion = nil
        self.state = .starting

        // See “Concurrency Notes” below.
        
        ProcessInfo.processInfo.performExpiringActivity(withReason: name) { didExpire in
            let semaphore = self.stateLock.withLock { () -> DispatchSemaphore? in
                switch (self.state, didExpire) {
                case (.starting, true):
                    // Failed to start; we can’t represent this in our API so we
                    // just flipped to the `.released` state and we’re done.
                    self.state = .released
                    return nil
                case (.starting, false):
                    // Started successfully.  Let’s block (outside the lock, of
                    // course) waiting on the semaphore.
                    let semaphore = DispatchSemaphore(value: 0)
                    self.state = .started(semaphore)
                    return semaphore
                case (.started(let semaphore), true):
                    // We have started and now we’re expiring.  Signal our
                    // semaphore to unblock the thread that’s waiting on it.
                    semaphore.signal()
                    self.state = .released
                    // Run the ‘did release’ callback.  This is async, so we can
                    // kick it off with the lock held.
                    DispatchQueue.main.async { self.runSystemDidReleaseAssertion() }
                    return nil
                case (.started(_), false):
                    // This shouldn’t be possible.
                    fatalError()
                case (.released, _):
                    // Our client called `release()` before we managed to start.
                    // That’s weird, but easy to handle.
                    return nil
                }
            }
            if let semaphore {
                semaphore.wait()
            }
        }
    }
    
    /// Release the assertion.
    ///
    /// It’s safe to call this redundantly, that is, call it twice in a row or
    /// call it on an assertion that’s expired.
    ///
    /// Must be called on the main thread.

    func release() {
        dispatchPrecondition(condition: .onQueue(.main))
        self.releaseOnAnyThread()
        // Set to `nil` to reduce the chances of a retain loop.
        self.systemDidReleaseAssertion = nil
    }
    
    private func releaseOnAnyThread() {
        self.stateLock.withLock {
            switch self.state {
            case .starting:
                // The transition from `.starting` to `.started` happens
                // asynchonously, so it’s possible that you could release the
                // assertion before that’s completed. This sets the state to
                // `.released` so that the concurrent code doing the transition
                // just gives up.
                self.state = .released
            case .started(let semaphore):
                // Unblock the thread waiting in our closure.
                semaphore.signal()
                self.state = .released
            case .released:
                // Releasing redundantly is a no-op.
                break
            }
        }
    }
    
    private func runSystemDidReleaseAssertion() {
        dispatchPrecondition(condition: .onQueue(.main))
        self.systemDidReleaseAssertion?()
        // Set to `nil` to reduce the chances of a retain loop.
        self.systemDidReleaseAssertion = nil
    }
    
    deinit {
        // We don’t apply this assert because it’s hard to force the last object
        // reference to be released on the main thread.  Fortunately,
        // `releaseOnAnyThread()` is thread safe.
        //
        // dispatchPrecondition(condition: .onQueue(.main))

        self.releaseOnAnyThread()
        
        // We don’t nil out `systemDidReleaseAssertion` here because that
        // property is confined to the main thread and we can’t be sure we’re
        // running on the main thread.  However, that’s not a problem because
        // the rationale for nil’ing this out is to minimise retain loop and, if
        // we got to this deinitialiser, that’s not a problem.

        self.systemDidReleaseAssertion = nil
    }
}