Search code examples
iosgrand-central-dispatchnsthreadnsrunloop

Order of operations in runloop on iOS


What is the order of operations on iOS?

I'm thinking sepcifically about timing of

  • setNeedsLayout and layoutSubviews
  • setNeedsDisplay and drawRect
  • touch recognition
  • [NSTimer scheduledTimerWithTimeInterval:0.000001 tar(...)]
  • dispatch_async(dispatch_get_main_queue(), ^{ /* code */}

As an example of an answer I would like to receive it could be in this format:

dispatch_async on main Happens before the next runcycle

drawRect Happens at the end of the runcycle


Solution

  • (Parts of this are copied from my answer to a similar question.)

    It turns out that the run loop is complicated, and a simple question like “Does drawRect: happen at the end of the runcycle?” doesn't have a simple answer.

    CFRunLoop is part of the open-source CoreFoundation package, so we can take a look at exactly what it entails. The run loop looks roughly like this:

    while (true) {
        Call kCFRunLoopBeforeTimers observer callbacks;
        Call kCFRunLoopBeforeSources observer callbacks;
        Perform blocks queued by CFRunLoopPerformBlock;
        Call the callback of each version 0 CFRunLoopSource that has been signaled;
        // Touch events are a version 0 source in iOS 8.0.
        // CFSocket is a version 0 source.
        if (any version 0 source callbacks were called) {
            Perform blocks newly queued by CFRunLoopPerformBlock;
        }
        if (I didn't drain the main queue on the last iteration
            AND the main queue has any blocks waiting)
        {
            remove all blocks from the main queue
            execute all the blocks just removed from the main queue
        } else {
            Call kCFRunLoopBeforeWaiting observer callbacks;
            // Core Animation uses a BeforeWaiting observer to perform layout and drawing.
            Wait for a CFRunLoopSource to be signalled
              OR for a timer to fire
              OR for a block to be added to the main queue;
            Call kCFRunLoopAfterWaiting observer callbacks;
            if (the event was a timer) {
                call CFRunLoopTimer callbacks for timers that should have fired by now
            } else if (event was a block arriving on the main queue) {
                remove all blocks from the main queue
                execute all the blocks just removed from the main queue
            } else {
                look up the version 1 CFRunLoopSource for the event
                if (I found a version 1 source) {
                    call the source's callback
                }
                // Interface orientation changes are a version 1 source in iOS 8.0.
            }
        }
        Perform blocks queued by CFRunLoopPerformBlock;
    }
    

    Core Animation registers a kCFRunLoopBeforeWaiting observer with an order of 2000000 (although that is not documented; you can figure it out by printing [NSRunLoop mainRunLoop].description). This observer commits the current CATransaction, which (if necessary) performs layout (updateConstraints and layoutSubviews) and then drawing (drawRect:).

    Note that the run loop can evaluate the true in while(true) twice before executing BeforeWaiting observers. If it dispatches timers or a version 1 source, and that puts block on the main queue, the run loop will go around twice before calling the BeforeWaiting observers (and it will dispatch version 0 sources both times).

    The system uses a mixture of version 0 sources and version 1 sources. In my testing, touch events are delivered using a version 0 source. (You can tell by putting a breakpoint in a touch handler; the stack trace contains __CFRunLoopDoSources0.) Events like entering/leaving foreground are dispatched through CFRunLoopPerformBlock, so I don't know what kind of source really provides them. Interface orientation changes are delivered through a version 1 source. CFSocket is documented to be a version 0 source. (It's likely that NSURLSession and NSURLConnection use CFSocket internally.)

    Note that the run loop is structured so only one of these branches happens on each iteration:

    1. Ready timers fire, or
    2. Blocks on dispatch_get_main_queue() run, or
    3. A single version 1 source is dispatched to its callback.

    After that, any number of version 0 sources can call their callbacks.

    So:

    1. Layout always happens before drawing, if both are pending when the Core Animation observer runs. The CA observer runs after timers, main queue blocks, or the external event callback have run.
    2. The main GCD queue has seniority over timers and version 1 sources, if the run loop didn't drain the main queue on the prior turn of the loop.
    3. Timers have seniority over the main queue and version 1 sources, should all three be ready.
    4. The main queue has seniority over version 1 sources, should both be ready.

    Also remember that you can request immediate layout at any time using layoutIfNeeded.