RunLoop in details
iOS CoreFoundation longRead Estimated reading time: 21 minutesOften we can hear such terms as RunLoop
, MainLoop
, or EventLoop
. But do we know how it works? And what responsibilities it has?
RunLoop
RunLoop
is the implementation of well-known EventLoop pattern - * programming construct or design pattern that waits for and dispatches events or messages in a program*.
while (!end) { }
This pattern has been implemented on many platforms. Thus, the main problems that it should resolve are:
- receive events/messages
- work when works exist and sleep when no work available (correct resource management).
Hight level description of Thread
:
Hight level description of EventLoop
:
iOS/macOS RunLoop
Talking about iOS/macOS we always refer to RunLoop
. To be more correct - 2 classes implement this behavior:
CFRunLoopRef
(open source)NSRunLoop
(based onCFRunLoopRef
)
As you already see, RunLoop
is connected to the thread. You can’t create RunLoop
directly, instead, it’s can be created at the very start of Thread
creating and destroyed at the very end of theThread
lifecycle. There are 2 function that provide access to RunLoop - CFRunLoopGetMain()
and CFRunLoopGetCurrent()
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none. - Apple.
interface
CoreFoundation has 5 classes that represent full interface for work with RunLoop:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
Let’s review each of these types.
CFRunLoopRef
- reference to a run loop object. This object monitors sources of input tasks and dispatches control when they ready to proceed. Three types of objects can be monitored by a run loop: sources (CFRunLoopSource
), timers (CFRunLoopTimer
), and observers (CFRunLoopObserver
). To get any event u need to put any of the supported objects in RunLoop first with an appropriate function call (it’s also possible to remove that object later).
Supported modes for CoreFoundation are:
kCFRunLoopDefaultMode
- observe any object changes when the thread is sitting idle.Default
. This mode good when a thread is created for receiving events.kCFRunLoopCommonMode
- pseudo mode, hold an object and share it with other sets of “common” modes. Thus this is pseudo mode - RunLoop never runs in this mode. Should be used only for a specific set of sources, timers, and observers shared by other modes.
check this from
CFRunLoop
Each Thread
has ONLY one run loop. RunLoop
can’t be created or destroyed on your own - it’s done automatically in CoreFoundation when needed (according to doc). Instead u can get current
RunLoop
mode.
RunLoop
has few Modes with Source/Timer/Observer in it. Only ONE Mode can be active at once, and it’s called current
. To switch between modes u need to exit Loop and set a new mode. Why? just to separate Source/Timer/Observer and make them not affect each other.
CFRunLoopSourceRef
- This is an abstraction of an input source that can be put into the RunLoop. They can create some async events (network message or user action). So this is an abstraction for some events/operations.
There are 2 categories Version 0 and Version 1
Version 0
has only one callback (function pointer), which does not actively trigger an event. In use, you need to call CFRunLoopSourceSignal(source)
first, mark the Source as pending, and then manually call CFRunLoopWakeUp(RunLoop)
to wake up RunLoop and let it handle the event.
Version 1
managed by run loop and kernel. This source use mach_ports
to signal when it’s ready to be executed (automatically). This Source can actively wake up the RunLoop
thread.
A run loop source can be registered in multiple run loops and run loop modes at the same time.
CFRunLoopTimerRef
- timer-based trigger. This is a specialized RunLoop source that can be fired at present and at a future time. Each RunLoop timer can be registered in one RunLoop at a time but can be added to a few modes within one run loop.
CFRunLoopTimer
is “toll-free bridged” with its Cocoa Foundation counterpart, NSTimer
. This means that the Core Foundation type is interchangeable in function or method calls with the bridged Foundation object.
A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. If a timer’s firing time occurs while the run loop is in a mode that is not monitoring the timer or during a long callout, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.
CFRunLoopObserverRef
- provides a general means to receive callbacks at different points within a running run loop. They fire at a specific location and execution of RunLoop. Can be one-time or repeatable.
Observers do not automatically added to the RunLoop, instead, a special call should be executed to add them.
Each run loop observer can be registered in only one run loop at a time, although it can be added to multiple run loop modes within that run loop.
mode
We can check source of CFRunLoop.c and found actual declaration for RunLoop mode:
As was mention previously, commonMode
is pseudo mode - you can see from source code that this implemented via few props in the structure that defines __CFRunLoop
. What does this mean from a practical point of view?
Main thread has 2 mode: kCFRunLoopDefaultMode
UITrackingRunLoopMode
and both of them marked as common
.
Default
- this one in which application is running, but for example when u touch screen and scroll and mode switched to tracking
mode, this mean that if u have a timer attached to default mode and u actively touch (for example scroll table view such as news feed), the timer will not be called. This guarantees that scroll operation will be not affected by other sources, in our case timer.
What to do so both timer and scrolling work without any delay or freeze?. You need to register a timer within multiply modes. Yes, the timer can be added to ONLY one RunLoop
but for few modes (as was mention above). To do so - simple use common
mode - thus is pseudo mode and as we already know, share a resource.
You can find a lot of posts regarding this “problem” (that is correct by design selected by Apple).
Check this post for more info about runLoop and timers
So what modes do we have from Apple?
Mode | Name | Description |
---|---|---|
Default | NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode (Core Foundation) | The default mode is the one used for most operations. Most of the time, you should use this mode to start your run loop and configure your input sources. |
Connection | NSConnectionReplyMode(Cocoa) | Cocoa uses this mode in conjunction with NSConnection objects to monitor replies. You should rarely need to use this mode yourself. |
Modal | NSModalPanelRunLoopMode(Cocoa) | Cocoa uses this mode to identify events intended for modal panels. |
Event tracking | NSEventTrackingRunLoopMode(Cocoa) | Cocoa uses this mode to restrict incoming events during mouse-dragging loops and other sorts of user interface tracking loops. |
Common modes | NSRunLoopCommonModes(Cocoa) kCFRunLoopCommonModes (Core Foundation) | This is a configurable group of commonly used modes. Associating an input source with this mode also associates it with each of the modes in the group. For Cocoa applications, this set includes the default, modal, and event tracking modes by default. Core Foundation includes just the default mode initially. You can add custom modes to the set using the CFRunLoopAddCommonMode function. |
com.apple.securityd.runloop | Communication with security. Used by SpringBoard only. | No |
FigPlayerBlockingRunLoopMode | QuickTime related. | No |
just grab this from official doc
To get even more info - we can check private mode’s:
Mode | Purpose | Part of common modes? |
---|---|---|
kCFRunLoopDefaultMode | The default run loop mode, almost encompasses every sources. You should always add sources and timers to this mode if there’s no special reasons. Can be accessed with the symbol kCFRunLoopDefaultModeand NSDefaultRunLoopMode. | Yes |
NSTaskDeathCheckMode | Used by NSTask to check if the task is still running. | Yes |
_kCFHostBlockingMode _kCFNetServiceMonitorBlockingMode _kCFNetServiceBrowserBlockingMode _kCFNetServiceBlockingMode _kCFStreamSocketReadPrivateMode _kCFStreamSocketCanReadPrivateMode _kCFStreamSocketWritePrivateMode _kCFStreamSocketCanWritePrivateMode _kCFStreamSocketSecurityClosePrivateMode _kCFStreamSocketBogusPrivateMode _kCFURLConnectionPrivateRunLoopMode _kProxySupportLoadingPacPrivateMode _kProxySupportSyncPACExecutionRunLoopMode _kCFStreamSocketSecurityClosePrivateMode | Various private run loop modes used by CFNetwork for blocking operations | No |
UITrackingRunLoopMode | UI tracking. | Yes |
GSEventReceiveRunLoopMode | Receiving system events. | No |
com.apple.securityd.runloop | Communication with securityd. Used by SpringBoard only. | No |
FigPlayerBlockingRunLoopMode | QuickTime related. | No |
implementation
If we check implementation of Apple’s EventLoop, we will find code that in general - do while
cycle (as also described here):
I grab only a small amount of code from CFRunLoop.c, but if u check it - u will be able to find all steps in the process that Apple mention in their doc:
- Notify observers that the run loop has been entered.
- Notify observers that any ready timers are about to fire.
- Notify observers that any input sources that are not port based are about to fire.
- Fire any non-port-based input sources that are ready to fire.
- If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.
- Notify observers that the thread is about to sleep.
- Put the thread to sleep until one of the following events occurs:
- An event arrives for a port-based input source.
- A timer fires.
- The timeout value set for the run loop expires.
- The run loop is explicitly woken up.
- Notify observers that the thread just woke up.
- Process the pending event.
- If a user-defined timer is fired, process the timer event and restart the loop. Go to step 2.
- If an input source is fired, deliver the event.
- If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.
- Notify observers that the run loop has exited.
If go deeper, we can find that Apple divided the whole system into 4 component:
- Application layer
- Application framework layer (Cocoa, CocoaTouch, etc)
- Core framework layer
- Darwin
If we go to Darwin and check how it works, we will find that everything is done using Mach’s API, via messaging.
Message definition from <mach/message.h>
If talking about RunLoop - the core concept is using these messages mach_msg()
from an above-mentioned sequence of work
- An event arrives for a port-based input source.
So RunLoop keeps calls function to receive a message and if no-one responds, kernel push Thread into sleep, while new message becomes available or Thread ends up due to some reason.
functionality
autorelease pool - after the start of the app, few observers registered within the main thread RunLoop.
One is monitors RunLoop enter (_objc_autoreleasePoolPush()
), used for creating atoreleasePool withi highest priority -2147483647, before anything else.
Another observer monitors 2 more event - moment when thread ready to sleep (_objc_autoreleasePoolPop()
) and moment when pool should be recreated (_objc_autoreleasePoolPush()
). These observers come with the lowest priority - 2147483647 - to make sure that it’s will be done after any operations.
The code executed in the main thread is usually written in such things as event callbacks and Timer callbacks. These callbacks are wrapped around the AutoreleasePool created by RunLoop, so there is no memory leak and the developer does not have to display the Create Pool.
system events - one more functionality registered using Version 1 source (__IOHIDEventSystemClientQueueCallback()
).
Events such as shake, touch, volume, screen lock generate IOHIDEvent and sent to SpringBoard. The registered observer then calls _UIApplicationHandleEventQueue()
to proceed next steps within it.
gestures - as was mention above RunLoop responsible for processing gestures using _UIApplicationHandleEventQueue()
call.
In general, Apple firstly registers pending gestures. Later all these pending gestures will proceed within one more observer on RunLoop _UIGestureRecognizerUpdateObserver()
.
interface update - all UI related changes (layout, constraints, layer change, drawing, etc) firstly also marked as pending and send to a special observer-container. Later observer call _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
that iterate over all pending data.
source here
timer - as was mention above about CFRunLoopTimerRef
- NSTimer/Timer
it’s toll-free bridged, so RunLoop control how it works also. CADisplayLink
also use sources from RunLoop interface
check AsyncDisplayLink from Facebook for alternative implementation - it\s allow to execute UI related task on non-main threads
perform selector - this is a family of functions from NSObject, under the hood it creates Timer and so also uses RunLoop.
That’s the reason why sometimes it may fail - this means that calling Thread does not have RunLoop.
GCD - RunLoop use GCD
and GCD
use RunLoop.
When dispatch_async(dispatch_get_main_queue(), block)
is called, libDispatch
will send a message to the main thread’s RunLoop, RunLoop will wake up, get the block from the message, and callback CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUEE
execute this block in (). But this logic is limited to dispatch to the main thread, and dispatch to other threads is still handled by libDispatch.
networking - on iOS there are few layers for work with network:
I believe we all saw that response from the network request come to us from a different thread. This means that underlying Thread uses RunLoop for messaging between different sources/observers/timers.
swiftUI/Combine - if you are already faced with this new technology u probably already create Timer
or use receive(on: options)
for various publishers.
This means that RunLoop is deeply integrated even within new coding approaches provided by Apple.
check interesting thread on Swift forum about Runloop and DispatchQueue
implementation
When we start the application now we know that the main Thread should auto initialize RunLoop. To check this we can simply check the backtrace of the stack during a simple app launch for iOS.
As u can see, the backtrace contains
note - latest call at the top
call to CFRunLoopRunSpecific
. And next action - creating Source 0 / Version 0 and awaiting for next action.
to check this on your side - just put breakpoint on
viewDidLoad
during app launch for the very firstViewController
.
If we check CFRunLoop.c
, we can easily find this function
We can easely inspect what’s going on using source and backtrace, like:
frame #25: 0x00007fff2038b7b6 CoreFoundation`__CFRunLoopDoSources0 + 346
and so on.
Another option to check how RunLoop works - is to check backtrace for event starts.
To do so - just override for example touchesBegan
and add a breakpoint. After entering the bt command in the print area, you can see the complete execution flow
Viewed from bottom to top, the approximate flow of the relevant functions executed is:
UIApplicationMain
CFRunLoopRunSpecific
__CFRunLoopRun
__CFRunLoopDoSources0
- Finally,
touchesBegan:withEvent:
How RunLoop works we can check in actual implementation of this function static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
. The implementation is complicated and require some time to understand, but we can use simlified version from this source:
According to this, we can see that main functions are:
__CFRunLoopDoObservers
: NotificationObserversWhat to do next__CFRunLoopDoBlocks
: ProcessingBlocks__CFRunLoopDoSources0
: processingSources0__CFRunLoopDoSources1
: ProcessingSources1__CFRunLoopDoTimers
: processingTimers- Handling GCD related:
dispatch_async(dispatch_get_main_queue(), ^{ });
__CFRunLoopSetSleeping/__CFRunLoopUnsetSleeping
: sleep waiting/end sleep__CFRunLoopServiceMachPort -> mach-msg()
: transfer control of the current thread
check out the source link above if you would like to dive into more details, thus I just grab a few moments from that
here u can see all 6 functions that is called by CFRunLoop and defined in
CFRunLoop.c
:
practice (usage)
Well, for now, it was almost only theory (except few samples within Timer
), how about practice? Where this all information can be used?.
First of all, understanding how something works is very useful if u can be faced with some unexpected behavior or when u faced with the limitation of the existing implementation. But, to make this all information even more useful, let’s review a few practical approaches.
RunLoop API.
.
The most used stuff:
API to manipulate RunLoop
is not very rich:
It’s possible to run RunLoop
in our custom mode, like:
But note, that without a timer or port RunLoop
will not run
A run loop must have at least one input source or timer to monitor. If one is not attached, the run loop exits immediately. (Apple)
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode)
orCFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes)
Even with this code, nothing will work. Why? Check result of RunLoop.init()
- it’s return nil. RunLoop
should be associated with Thread
, and normally shouldn’t be created manually.
So, how to attach RunLoop
to Thread
?. Well, each we can create Thread
object and access to RunLoop.current
- if no RunLoop
exist, the one will be autocreated for us:
As was mention previously, RunLoop should have at least one source or timer to monitor or it’s will exit. How to add them? We can use one of provided functions for this purpose:
CFFileDescriptorCreateRunLoopSource
CFSocketCreateRunLoopSource
CFMachPortCreateRunLoopSource
CFMessagePortCreateRunLoopSource
Check this post if you are interested in more details or official doc. And this one about source/observer/timer
Add custom observer
to RunLoop for heavy work on the main thread.
this option allows us to execute some heavy computation out of the main thread but change UI when needed on the main thread.
So the idea is quite simple - we just create an observer on the thread which RunLoop we want to use, execute work, and remove the observer when works are done.
Resources
- Apple open source
- RunLoop official doc
- Principles of RunLoop
- Understanding RunLoop
- RunLoop and timers
- NSTimer in RunLoop
- Great article about RunLoop
- CFRunLoopTimerRef
- CFRunLoopSourceRef
- CFRunLoopRef
- CFRunLoopObserverRef
- RunLoop modes
- Understanding RunLoop model
- Mach communication
- System call
- IOHIDFamily
- RunLopp activities
- Realm background Thread with custom RunLoop
- RunLoop
- Custom implementation of RunLoop
- RunLoop with Threads
Share on: