Dive into CFRunLoop
Background
Sometimes, you may want to collect on-device performance metrics in main thread to know how our App performs and help you find more clues to analyse performance issue. MetricKit is a useful utility framework to achieve that. It starts accumulating reports for your app after being called for the first time and delivers reports at most once per day. The reports contain the metrics from the past 24 hours and any previously undelivered daily reports. Then, you can go to Xcode->Organizer->Metric
panel to check these info. However, you may want your in-house App Performance Monitoring framework to gain more controls on when to collect metrics, or how to upload it, or collect what you want. Earlier days, Tencent
launched a iOS framework called matrix to monitor App performance metrics. When I explored this library, I saw they use CFRunLoop
to detect hitch in the thread, like main thread. This intrigued me. So I investigated CFRunLoop
to learn more.
What is RunLoop in iOS?
Before talking about RunLoop in iOS, we may have to know something about event loop and thread. In OS/360 Multiprogramming with a Variable Number of Tasks (MVT) in 1967, threads made an early appearance under the name of “tasks”. A thread in computer science is short for a thread of execution. Once the tasks in one Thread is all done, the thread finishes its job and exits. Sometimes, we need a way to keep it alive and handling events. Then, comes the event loop. The psudo code for event loop is like this:
1 | function loop |
In Wikipedia, event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. In this event loop, it keeps waiting events -> receive events -> handle events
until the exit condition is met.
In Apple’s doc, this kind of event loop is implemented by CFRunLoop
in low-level. In cocoa, the object is an instance of NSRunLoop
There is exactly one run loop per thread.
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.
A CFRunLoop object monitors sources of input to a task and dispatches control when they become ready for processing.
It can handle
user input devices
port objects
network connections
periodic or time-delayed events
asynchronous callbacks
Apple provides two APIs to get runloop object
CFRunLoopGetMain()
// the main CFRunLoop objectCFRunLoopGetCurrent()
// CFRunLoop object for the current thread
RunLoop Mode
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. Each time you run your run loop, you specify (either explicitly or implicitly) a particular “mode” in which to run.During that pass of the run loop, only sources associated with that mode are monitored and allowed to deliver their events. — doc
A run loop mode contains a set of CFRunLoopSource
, a list of CFRunLoopTimer
and CFRunLoopObservers
. They are all inputs for runloop.
Inputs
Three kinds of inputs can be monitored by a run loop
- CFRunLoopSource
- CFRunLoopTimer
- CFRunLoopObservers
Source - CFRunLoopSource
A CFRunLoopSource object is an abstraction of an input source that can be put into a run loop. Input sources typically generate asynchronous events, such as messages arriving on a network port or actions performed by the user.
1 | struct __CFRunLoopSource { |
_context
is a union
type. A union looks like a structure, but it will use the memory space for just one of the fields in its definition. So the _context
is either an CFRunLoopSourceContext
structure or CFRunLoopSourceContext1
structure.
Two categories
As it is mentioned in this doc, we mainly care about two categories, port-base input sources, source1, and non-port-based input sources, source0.
- Version 0 sources, so named because the
version
field of their context structure is 0, are managed manually by theapplication
.- When a source is ready to fire, some part of the
application
, perhaps code on a separatethread
waiting for an event, must callCFRunLoopSourceSignal(_:)
to tell the run loop that the source is ready to fire.
- When a source is ready to fire, some part of the
- Version 1 sources are managed by the run loop and kernel.
- These sources use
Mach ports
to signal when the sources are ready to fire. - A source is automatically
signaled by the kernel
when a message arrives on the source’s Mach port.
- These sources use
bits
field
It seems that bits
field is used to mark the status of the CFRunLoopSouceRef
.
1 | CF_INLINE Boolean __CFRunLoopSourceIsSignaled(CFRunLoopSourceRef rls) { |
CFRunLoopSourceSignal is used to signals a version 0 source
, marking it as ready to fire. It actually updated the bits
in the CFRunLoopSouceRef
structure.
1 | void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) { |
CFRunLoopTimer
What is CFRunLoopTimer
A CFRunLoopTimer object represents a specialized run loop source that fires at a preset time in the future. Timers can fire either only once or repeatedly at fixed time intervals.
There are two conditions for a timer to be fired:
- one of the run loop modes to which the timer has been added is running
- 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.
RunLoopObserver
How to use observer
- We can use these two APIs to create RunLoopObserver and associated it with handlers.
- CFRunLoopObserverCreate(_:_:_:_:_:_:)
- CFRunLoopObserverCreateWithHandler(_:_:_:_:_:)
add the observer into the runloop
1
2
3
4
5
6
CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// handler code here
});
CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, kCFRunLoopDefaultMode);Observe specific RunLoop Activity
RunLoop Activity
The run loop stages in which an observer is scheduled are selected when the observer is created with
CFRunLoopObserverCreate
. -doc
There are several kinds of RunLoop Activity for CFRunLoop. You can associate run loop observers with these RunLoopActivity
1 | /* Run Loop Observer Activities */ |
https://developer.apple.com/documentation/corefoundation/cfrunloopactivity
Run Loop Sequence of Events
According to apple doc, when runloop running in a thread, it processes pending events and generates notifications for attached observers. Briefly, it works as the follow diagram shows.
The implementation is in CFRunLoopRunSpecific
and __CFRunLoopRun
in CFRunloop.c
.
Use case in App Performance Monitoring
In Tencent matrix, it leverages the Run Loop notifications to record timestamp when these notifications sent.
create and add RunLoopObserver to current RunLoop CFRunLoopAddObserver
record timestamp in callback function invoked when the observer runs
So, I did a small experiments. I added a RunLoop Observer to the runloop in main thread. Then calculate the time gap between kCFRunLoopBeforeTimers
notification in two continuous loop.
1 | // 1. create runloop observer |
1 | // 3. implemented callback for RunLoop observer |
Theoretically, the time diff between two continuous kCFRunLoopBeforeTimers
notification should be within 16.67ms
to achieve smooth user experience in main thread, which means RunLoop
runs 60 times per second. In the following log, one frame takes about 72ms
to executed.
1 | [RY]kCFRunLoopBeforeTimers called 3.628173828125 |
- https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
- https://blog.ibireme.com/2015/05/18/runloop/
- https://opensource.apple.com/tarballs/CF/
- https://developer.apple.com/documentation/corefoundation
- https://github.com/apple/swift-corelibs-foundation/
Author : RY Zheng
Link : https://suelan.github.io/2021/02/13/20210213-dive-into-runloop-ios/
License : MIT
