Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 420
hfossli Avatar asked Oct 03 '14 12:10

hfossli


3 Answers

(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.

like image 99
rob mayoff Avatar answered Oct 21 '22 20:10

rob mayoff


One task after the other is added to the runloop from various sources; the runloop will execute the oldest task on the runloop and not start another task until the call for that task returns.

like image 30
gnasher729 Avatar answered Oct 21 '22 20:10

gnasher729


  1. Handling of user interaction
  2. UI Components call setNeedsLayout and setNeedsDisplay if they need an update
  3. Layouting is done using layoutSubviews (called indirectly by layoutSublayers)
  4. Painting is done using drawRect and drawInContext:
  5. dispatch_async call is performed
  6. Your timer with 0.000001 seconds delay could be executed before or after the dispatch_async. Difficult to say.

1 and 2 are actually mixed because it's mostly the user interaction causing changes in the UI by calling setNeedsLayout and setNeedsDisplay somewhere.

The order of 1, 2, 3 and 4 is well-defined. 5 should also always happen afterwards. NSTimer depends on various circumstances - you should not rely that it's called before or after the dispatch_async call but it most likely will be executed after the painting is done.

like image 38
fluidsonic Avatar answered Oct 21 '22 20:10

fluidsonic