Run, RunLoop, Run!
Although rarely discussed among developers, it is one of the most important components of any app: Run Loops. Run Loops are like the beating heart of your app. It’s what makes your code actually Run.
The basic principle of Run Loop is actually quite simple. In iOS and OSX, CFRunLoop implements the core mechanism used by all high-level communication and distribution apis.
What exactly is a Run Loop?
Simply put, Run Loop is a communication mechanism for asynchronous or interthread communication. Think of it as a mailbox — waiting for messages and delivering them to recipients.
Run Loop does two things:
- Waiting for something to happen (such as a message)
- The message is sent to the corresponding recipient
In other platforms (Win32), this mechanism is called a “Message Pump.”
Run Loop is the key to differentiating interactive applications from command-line tools. The command-line tool accepts arguments, starts, executes specific commands, and finally exits. Interactive applications wait for user input, react accordingly, and then wait. In fact, this basic mechanism is also common in long-running programs. While (1) {select(); } is a good (albeit old) Run Loop example.
Run Loop’s job is to wait for something to happen. These things can be external events triggered by the user or the system (such as network requests), or internal application messages, such as interthread notifications, asynchronous code execution, timers, and so on. When an event (or message) is received, Run Loop finds a corresponding listener and passes the message to it.
Implementing a basic Run loop is easy. Here is a simple pseudocode version:
func postMessage(runloop, message){
runloop.queue.pushBack(message)
runloop.signal()
}
func run(runloop){
do {
runloop.wait()
message = runloop.queue.popFront()
dispatch(message)
} while (true)
}
Copy the code
With this simple mechanism, each thread runs () its own run loop and then uses postMessage() to exchange messages asynchronously with other threads. My colleague Cyril Mottier told me that the Implementation of the Android version is not much more complicated than that.
IOS and OS X
In Apple, this is implemented by CFRunLoop, a slightly more advanced variant (CFRunLoop. C has 3909 lines, looper.java 309 lines). With the exception of early initialization and your own thread generation, all code you write will be called by CFRunLoop at some point. (As far as I know, threads automatically created for GCD don’t require CFRunLoop, but there is definitely a messaging system that allows reuse.) The most important feature of CFRunLoop is CFRunLoopModes. CFRunLoop works with a “RunLoop Sources” system. Sources registered on the Run Loop have one or more modes, and the Run Loop itself runs in a given mode. When an event arrives at the source, it is only handed to the Run loop that has a pattern matching the source.
In addition, CFRunLoop is reentrant, either through its own code or internal framework code. Because there is only one CFRunLoop per thread, when a component wants to Run a RunLoop in a particular mode, it can do so by calling CFRunLoopRunInMode(). All Run Loop sources that are not registered for this mode will be stopped. Usually the component will eventually return to the previous schema.
CFRunLoop defines a pseudo-mode: kCFRunLoopCommonModes, which are actually a set of “normal” Run loops. The main RunLoop starts working in kCFRunLoopCommonModes. On the other hand, UIKit defines a special RunLoop mode called UITrackingRunLoopMode. It uses this mode “when control tracking occurs,” such as when touching. This is very important, because it keeps the TableView scrolling smoothly. When the main thread’s RunLoop is in UITrackingRunLoopMode, most background events, such as network callbacks, are not dispatched. That way, without the extra processing, scrolling won’t get stuck (and if it does now, it’s your fault).
Reveal CFRunLoop
If you’ve ever debugged iOS and OS X code with stack traces, chances are you’ve noticed an all-caps method CFRUNLOOP_IS_CALLING_OUT in the stack trace. This is what CFRunLoop likes to do when it calls program code. Here are six functions defined in cfrunloop. c:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
Copy the code
You guessed it, these functions are used for nothing more than trace debugging. CFRunLoop ensures that all application code is called through one of the above functions. Let’s look at them one by one.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
CFRunLoopObserverCallBack func,
CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info);
Copy the code
There is something special about Observers. The CFRunLoopObserver API allows you to observe the behavior of a CFRunLoop and whether it is active (processing an event, going to sleep, etc.). Observers are useful for debugging, especially if you want to understand the features of CFRunLoop. In fact, it is useful for some specific purposes, such as CoreAnimation running through observer callouts, which makes sense because it ensures that all UI code is executed and that all animations are executed at once.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void));
Copy the code
Closures are the other side of the CFRunLoopPerformBlock() API and are useful when you want to run code in the “next loop.”
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg);
Copy the code
The Main Dispatch Queue tag is CFRunLoop’s handling of the GCD. Obviously, GCD and CFRunLoop work together, at least on the main thread. Even though GCD can (and will) create threads without a CFRunLoop, when there is a CFRunLoop here, it inserts it.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
CFRunLoopTimerCallBack func,
CFRunLoopTimerRef timer,
void *info);
Copy the code
Timers are relatively easy to understand literally. In iOS and OS, high-level “timer” such as NSTimer or performSelector: afterDelay: through CFRunLoop timer implementation. Starting with iOS 7 and Mavericks, the timer trigger point has a concept of a fault-tolerant interval, which CFRunLoop handles, of course.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
void (*perform)(void *),
void *info);
Copy the code
CFRunLoopSources “Version 0” and “Version 1” are two very different things, although they have a common API.
Version 0 Sources is simply an in-application message processing mechanism that must be handled manually by application code. After sending a signal to the Version 0 Source (via CFRunLoopSourceSignal()), the CFRunloop must be woken up (via CFRunLoopWakeUp()) to process the Source.
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg,
CFIndex size,
mach_msg_header_t **reply,
void (*perform)(void *),
void *info);
Copy the code
Version 1 Sources, on the other hand, uses mach_port to handle kernel events. This is actually the heart of CFRunLoop: most of the time, when your app is just standing there, doing nothing, it will block at this mach_msg(… MACH_RCV_MSG,…). In the call. If you look at any app with the Activity Monitor, chances are you’ll see this:
2718 CFRunLoopRunSpecific (in CoreFoundation) + 296 [0x7fff98bb7cb8]
2718 __CFRunLoopRun (in CoreFoundation) + 1371 [0x7fff98bb845b]
2718 __CFRunLoopServiceMachPort (in CoreFoundation) + 212 [0x7fff98bb8f94]
2718 mach_msg (in libsystem_kernel.dylib) + 55 [0x7fff99cf469f]
2718 mach_msg_trap (in libsystem_kernel.dylib) + 10 [0x7fff99cf552e]
Copy the code
It’s right here in cgrunloop.c. A few lines above, you can see the Apple engineer quoting Hamlet’s soliloquy:
/* In that sleep of death what nightmares may come ... * /Copy the code
Take a sneak peek at cfrunloop.c
Whenever your app runs, the core of CFRunLoop is the __CFRunLoopRun() function, Called via the public API CFRunLoopRun() and CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled).
__CFRunLoopRun() exits for four reasons:
kCFRunLoopRunTimedOut
: Timeout, if the interval is specifiedkCFRunLoopRunFinished
: When it is empty, for example, all resources have been deletedkCFRunLoopRunHandledSource
: if there isreturnAfterSourceHandled
Flag after the event has been sentkCFRunLoopRunStopped
Through:CFRunLoopStop()
Manual stop
It waits and dispatches events until one of the above four causes occurs. Here is an example of a process that includes the various types of events we discussed earlier.
- Call closures (blocks,
CFRunLoopPerformBlock()
API). - Check Version 0 Sources and call their “Perform” functions if necessary.
- Rotation and internal scheduling of queues and
mach_port
And then - If there is nothing to deal with, go to sleep. The kernel wakes us up if anything happens. This section of code is actually more complex because (a) it has been added for Win32 compatibility
#ifdef
#elif
Code b, in the middle of the codegoto
. The main idea is that you can takemach_msg()
Configure to wait on multiple queues and ports.CFRunLoop
You can wait for timer, GCD distribution, manual wake up, or to work on Version 1 Sources at the same time. - Wake up and try to find out why:
- A manual wake up: Keep running the loop, perhaps there is a closure or Version 0 Sources waiting to be processed.
- When one or more timers are triggered: call the method corresponding to the timer.
- GCD needs to work: it is called through the specific “4CF” dispatch_queue API.
- The kernel issues a Version 1 Source. Find it and deal with it.
- Call the closures again.
- Check the exit conditions. (Finished, Stopped, TimedOut, HandledSource)
- Start again.
Easy, right? CoreFoundation is implemented by C, which doesn’t look very modern. My first thought when I saw the code was “This needs refactoring”. On the other hand, this code is battle-tested, so I don’t expect it to be rewritten with Swift anytime soon.
There’s a code pattern that I’ve been using for years, especially in testing. It’s “run loop until this condition is true,” and it’s the foundation of any type of asynchronous unit test. Over time, I’ve probably written many variations of this, using NSRunLoop or CFRunLoop directly, polling, using timeouts, and so on. Now I can write a decent version, and we’ll find out in the next article.