Understanding iOS RunLoop: Architecture, Modes, and Message Handling
This article provides an in‑depth technical overview of iOS RunLoop, explaining its relationship to threads, event loops, RunLoop modes, sources, timers, observers, and the underlying Mach message mechanisms, while including original source code excerpts for reference.
RunLoop
RunLoop is a fundamental part of the thread infrastructure in iOS. Unlike a simple thread that exits after completing its work, the main thread must stay alive to continuously handle events such as user touches. RunLoop enables a thread to work when there are tasks and to sleep when idle, implementing the classic Event Loop pattern.
void CFRunLoopRun(void) {
int result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(),
KCFRunLoopRunLoopDefaultMode, ...);
} while (KCFRunLoopRunStopped != result && KCFRunLoopRunFinished != result);
}An Event Loop is a design pattern that waits for and dispatches events or messages. When the event loop becomes the central control flow of a program, it is often called the main loop. iOS implements this concept with the RunLoop object, which manages events and provides an entry point for processing them.
let runloop = RunLoop.current()
runloop.add(Port.init(), forMode: .common)
runloop.run()In iOS there are two classes that manage RunLoop: the high‑level RunLoop class and the lower‑level CFRunLoopRef . The latter is a CoreFoundation wrapper that is thread‑safe; RunLoop is built on top of it but its API is not thread‑safe.
RunLoop and Threads
iOS threads are typically represented by Thread or pthread_t . Thread is a Swift wrapper around pthread_t , which in turn wraps the underlying Mach thread. The Swift RunLoop object is a wrapper around CFRunLoop , and CFRunLoop is managed using pthread_t .
typedef pthread_mutex_t CFLock_t;
static CFMutableDictionaryRef loopsDic = NULL; // global dictionary: key = pthread_t, value = CFRunLoopRef
static CFLock_t loopLock = (pthread_mutex_t)PTHREAD_MUTEX_INITIALIZER;
CFRunLoopRef _CFRunloopGet0(pthread_t thread) {
pthread_mutex_lock(&loopLock);
if (!loopsDic) {
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_up());
CFDictionarySetValue(loopsDic, pthread_main_thread_up(), mainLoop);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(loopsDic, thread);
if (!loop) {
loop = __CFRunLoopCreate(thread);
CFDictionarySetValue(loopsDic, thread, loop);
}
// associate the loop with thread‑specific data (TSD)
if (pthread_equal(t, thread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunloopCntr)) {
_CFSetTSD(_CFTSDKeyRunLoopCntr, __CFFinalizeRunLoop);
}
}
return loop;
}Each thread has at most one RunLoop, stored in a global dictionary. A RunLoop is created lazily on the first request and destroyed when the thread terminates, via the __CFFinalizeRunLoop function.
Thread‑Specific Data (TSD)
Thread‑Specific Data (TSD) is a per‑thread storage mechanism identified by a pthread_key_t . Each thread can store multiple values under the same key, effectively forming a two‑dimensional array where the key is the row and the thread ID is the column.
Keys
T1 Thread
T2 Thread
T3 Thread
T4 Thread
K1(__CFTSDKeyRunLoop)
6
56
4
3
K2
87
21
0
9
K3
23
12
61
2
K4
11
76
47
88
The RunLoop instance is stored in TSD using the key _CFTSDKeyRunLoop , allowing each thread to retrieve its own RunLoop quickly.
RunLoopMode
iOS defines several core RunLoop classes: CFRunLoop , CFRunLoopMode , CFRunLoopSource , CFRunLoopObserver , and CFRunLoopTimer . A RunLoop can contain multiple modes, and each mode can contain multiple sources, observers, and timers.
struct CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};
struct CFRunLoopMode {
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};Only one mode runs at a time; the active mode is called the Common Mode . Switching modes requires exiting the current RunLoop and re‑entering with a different mode, which isolates the sets of sources, timers, and observers.
CFRunLoopSourceRef
Sources are the data inputs for a RunLoop. There are two kinds:
Source0 : a callback‑based source that cannot wake the RunLoop directly; it must be signaled with CFRunLoopSourceSignal and then manually woken up with CFRunLoopWakeUp .
Source1 : a Mach‑port‑based source managed by the kernel (e.g., CFMachPort , CFMessagePort , NSSocketPort ).
// Source0 example
CFRunLoopSourceSignal(rs);
CFRunLoopWakeUp(RunLoop.main.getCFRunLoop());CFRunLoopTimerRef
Timers are time‑based triggers, toll‑free bridged with NSTimer . They fire only when the RunLoop is in a mode that contains the timer; otherwise the fire event is deferred.
CFRunLoopObserverRef
Observers watch RunLoop state changes. The activity flags include entry, before timers, before sources, before waiting, after waiting, and exit.
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1U << 0),
kCFRunLoopBeforeTimers = (1U << 1),
kCFRunLoopBeforeSources = (1U << 2),
kCFRunLoopBeforeWaiting = (1U << 5),
kCFRunLoopAfterWaiting = (1U << 6),
kCFRunLoopExit = (1U << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};Common Modes
Modes can be marked as Common by adding them to the CFRunLoopCommonModes set. Items placed in the common‑mode list are automatically added to every mode that has the common flag, ensuring that, for example, a timer fires both in the default mode and while scrolling.
let mode = CFRunLoopMode(rawValue: "fff" as CFString)
CFRunLoopAddCommonMode(RunLoop.main.getCFRunLoop(), mode)RunLoop Internal Logic
The core of the RunLoop is a while loop that repeatedly processes observers, sources, timers, and blocks, then sleeps waiting for Mach messages. The sleep is performed via CFRunLoopServiceMachPort , which ultimately calls mach_msg to receive messages from a port set.
int CFRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
int CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
CFRunLoopModeRef currentMode = __CFRunLoopMode(rl, modeName, false);
if (currentMode == NULL || CFRunLoopModeIsEmpty(currentMode)) return;
// notify observers, process sources, timers, etc.
...
CFRunLoopDoObservers(rl, currentMode, kCFRunLoopBeforeWaiting);
CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, timeout, NULL, NULL);
...
}During each iteration the RunLoop notifies observers of the following events:
Entry – about to start the loop.
BeforeTimers – timers are about to be processed.
BeforeSources – non‑port sources (Source0) are about to be processed.
BeforeWaiting – the loop is about to sleep.
AfterWaiting – the loop has been woken.
Exit – the loop is about to finish.
When waking, the RunLoop determines which port caused the wake‑up by inspecting msg->msgh_local_port . It then dispatches the appropriate handler (timer, source1, GCD dispatch, or explicit wake‑up).
Mach Message Reception
The function CFRunLoopServiceMachPort calls mach_msg with MACH_RCV_MSG to block until a message arrives on any port in the waitSet . The received message is placed in an ipc_kmsg structure, which is a doubly‑linked list node used by the XNU kernel to manage message queues.
struct ipc_kmsg {
mach_msg_size_t ikm_size;
struct ipc_kmsg *ikm_next; // next message on port/discard queue
struct ipc_kmsg *ikm_prev; // previous message on port/discard queue
mach_msg_header_t *ikm_header;
ipc_port_t ikm_prealloc; // port we were pre‑allocated from
ipc_port_t ikm_voucher; // voucher port carried
...
};After mach_msg returns, livePort = msg->msgh_local_port tells the RunLoop which source (timer, dispatch port, wake‑up port, etc.) caused the wake‑up, and the RunLoop routes the event accordingly.
Historical Context
iOS is derived from macOS (formerly OS X), sharing the same Darwin kernel, XNU hybrid kernel, and CoreFoundation infrastructure. Understanding RunLoop therefore also sheds light on the broader OS architecture, including Mach IPC, BSD POSIX layers, and the user‑space frameworks that sit atop them.
References
1. Apple open‑source CoreFoundation RunLoop implementation (GitHub). 2. Mach message documentation (MIT). 3. "Deep Dive into RunLoop" – blog post by ibireme. 4. Apple Threading Programming Guide. 5. "Mac OS X and iOS Internals" – book by Jonathan Levin. 6. XNU source code on opensource.apple.com.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.