Runloop
Runloop is an iOS event loop that ensures that your program does not exit after main. It can be loosely interpreted as a while(true) loop, but its implementation is not that simple. It is actually an NSRunLoop object, which maintains an event loop inside the object. When there are no events to handle, the Runloop hands the thread controller to the system from user -> kernel -> user – when it is woken up, so that it does not consume CPU resources during hibernation.
Basic concept
As mentioned in the introduction, a program will exit automatically after it finishes running. For example, if we create a macOS CommandLine project in Xcode, the program will exit when the main function returns. However, our APP obviously can’t do this, so we want our APP to be able to respond without quitting. Such a mechanism is usually implemented using an Event Loop, or Runloop in iOS. Unlike Runtime, Runloop is an actual object that corresponds to the NSRunloop class of the Foundation framework and the CFRunloop class of the Core Foundation framework. NSRunloop is based on the upper layer encapsulation of CFRunloop.
Runloop core
As we mentioned earlier, Runloop can be summarized as a while(true) loop, but in reality such an implementation would cause a lot of unnecessary idle on the CPU. Therefore, the core of the Runloop mechanism is to ensure that threads wake up when there are events to process and sleep when there are no events. When there is an event, the system kernel sends the event to the corresponding Runloop through mach_msg() or Mach port method. After receiving the event, the Runloop switches from the sleep state to the wake state. And from kernel -> user – mode.
How do I wake up Runloop
Source
Source is an important concept in Runloop and represents the events mentioned above. In Runloop, sources fall into two categories
- Source0: This class Source is an internal event of the App and does not have the ability to wake up Runloop independently. A Source0 needs to be processed when it needs to be
CFRunLoopSourceSignal()
The function is marked as pending and is calledCFRunLoopWakeUp
Function to wake up Runloop,CFRunLoopWakeUp
The function goes through one_wakeUpPort
This is a Mach port. The Runloop can only be woken up by a Mach port and mach_msg(). Wake up through the call__CFRunLoopDoSources0
Function to process the Source0 event and later mark it as handled. - Source1: This class of sources are sources generated by hardware events, such as touch, shake, rotate, and so on. This type of Source wakes up Runloop.
Timer
Tasks registered for execution using the NSTimer API fall into this category
Observer
An Observer can listen for runloop state changes and react to them
Runloop in relation to threads
- Runloops are one-to-one correspondence to threads, and runloops of child threads cannot obtain runloops of other child threads. The one-to-one correspondence is stored in a global dictionary with key-values. in
CFRunloop
, onlyCFRunloopGetMain
andCFRunloopGetCurrent
Two functions can get Runloop - The main thread automatically creates runloops in response to events, but child threads do not automatically create runloops. Since the NSTimer object needs mode added to the Runloop, it is called in the child thread
performSel afterdelay
Methods are not called because they register an NSTimer into a Runloop, and child threads do not have runloops by default. - The Runloop is destroyed when the thread is destroyed
CFRunLoopMode
Modes, which manage the bridge between Runloop and source/ Timer/Observer, register five modes at the beginning
- KCFRunLoopDefaultMode: The default Mode of the App, in which the main thread is normally run.
- UITrackingRunLoopMode: interface tracing Mode, used by ScrollView to track touch sliding, ensuring that the interface sliding is not affected by other modes. NSTimer is added to default mode by default, so when sliding Runloop switches to tracking mode, Timer callback in default mode is not called, so NSTimer is not as accurate as CADisplayLinker.
- UIInitializationRunLoopMode: in the first to enter the first Mode when just start the App, start after the completion of the will no longer be used.
- GSEventReceiveRunLoopMode: accept system internal Mode of events, usually in less than.
- Kcfrunloopcommonmode: This is a placeholder Mode and has no effect.
- If you need to add an event to more than one mode, you register it with commonMode, which is actually a collection of multiple modes.
- For the purpose of separating source/timer/ Observer, runloops can only run in one mode at a time, and when run, the currentMode property of the RunLoop is marked with the currently running mode. To switch mode, RunLoop must exit and re-enter a mode selected to switch mode. When switching modes, events added to commonModes are copied once into the running mode.
Source validation
__CFRunLoop
The corresponding class for RunLoop in Core Foundation isCFRunLoopRef
And its corresponding structure is__CFRunLoop
You can see that the structure contains the mode mentioned above, correspondingCFRunLoopModeRef
, its structure is shown as follows
Source, Observer, and Timer are the three types that can wake up runloops. Of course, only sources1 can wake up runloops independently. The relationship between Mode and source,observer, and timer is shown below
A RunLoop contains several modes, each of which contains several sources/timers/observers. Only one Mode can be specified each time RunLoop’s main function is called, and this Mode is called CurrentMode. If you need to switch Mode, you can only exit Loop and specify another Mode to enter. The main purpose of this is to separate the Source/Timer/Observer groups from each other.
_CFRunloopGet0()
In Core Foundation, two interfaces are provided to get runloopsCFRunloopGetMain
andCFRunloopGetCurrent
First, take a look at their source code implementation.
As you can see, both functions actually call the _CFRunLoopGet0() method, which takes the thread pthread_t. In CFRunLoopGetCurrent(), if a Runloop exists for the current thread, it is found and returned in the _CFGetTSD() function.
Let’s move on_CFRunLoopGet0()
Function, which is obviously the key function to get the RunLoop. Let’s take a look at part one.
Notice here__CFRunLoops
The variable, it’s aCFMutableDictionaryRef
Type, key for thread, value for CFRunLoopRef
On the first entry, the _CFRunLoopGet0() function creates a dictionary variable of type CFMutableDictionaryRef, which is clearly the global RunLoop table. This also supports the one-for-one correspondence between threads and runloops mentioned earlier. Then we can see that the __CFRunLoopCreate() function is called to create the main thread RunLoop, so the RunLoop will not be created until it is fetched, and if not, it will not be created. The RunLoop is stored in the global RunLoop table with key as thread and value as CFRunLoopRef.
Then look at the second part, which is the code that goes through the process, if not for the first time.
Loops (loops, loops, loops, loops, loops, loops, loops, loops, loops, loops, loops, loops); If no RunLoop is found for this thread in __CFRunLoops, create a RunLoop by calling __CFRunLoopCreate() and add it to __CFRunLoops.
__CFRunLoopSource
In mode, Source corresponds to the __CFRunLoopSource structure. __CFRunLoopSource () __CFRunLoopSource () __CFRunLoopSource () __CFRunLoopSource () CFRunLoopSourceContext is used to distinguish different sources, where version0 and version1 correspond to source0 and source1 respectively.
CFRunLoopSourceContext and CFRunLoopSourceContext1 One obvious difference between CFRunLoopSourceContext1 and CFRunLoopSourceContext0 is that CFRunLoopSourceContext1 has a variable of type mach_port_t. From here you can see why Source0 can’t wake RunLoop independently but Source1 can. In the previous article we mentioned that only Mach port and mach_msg() can wake RunLoop independently.
__CFRunLoopCreate(_CFThreadRef t)
This function is used to create a RunLoop, which is called in _CFRunloopGet0() to create a new RunLoop if the thread’s RunLoop is not available. You can see that the input parameter is a variable of type _CFThreadRef, which represents a thread, since threads are one-to-one with RunLoop.
_CFRuntimeCreateInstance() is called to create an instance of CFRunLoopRef. As you can see from the name and code of the function, the __CFRunLoop is allocated and initialized. RunLoop is created to take advantage of the Runtime’s ability to create classes dynamically.
CFRunLoopWakeUp
This function is used to wake up RunLoop in CFRunLoop. As you can see under TARGET_OS_MAC, the key call is __CFSendTrivialMachMessage(), which uses the _wakeUpPort property in CFRunLoop.
You can see that inside the __CFSendTrivialMachMessage function, mach_msg is indeed used to send a message to the Mach port to wake up the RunLoop.
Event response of Runloop application
From the user touching the screen to our app responding to the touch, it actually needs to go through a multi-step process, and involves hardware -> software communication. Source1 is a hardware-generated event, so use touch events as an example to see how Runloop handles event responses.
Event response chain
First let’s comb through the event response chain.
- User-triggered event
- The system forwards the event to the event queue of the corresponding APP
- APP retrieves the event from the message queue header
- Send the message to Main Window for distribution
- If no Responder is found, the Responder is returned to the APP layer along the Responder chain, and the event is discarded.
From the above five steps, we can see that in fact 1 and 2 are independent of the app and need to involve the hardware. From the third step, events are sent to the app for processing.
User-triggered event
This step starts when the user taps the screen, and the system’s IOKit. Framework generates an IOHIDEvent event, which is received by the Spring Board.
Events are forwarded to app
When IOHIDEvent is received, it will be forwarded to the corresponding APP process through Mach port. And then, in the app, a com. Apple. Uikit. Eventfetch – thread thread has registered a Source1, the callback is __IOHIDEventSystemClientQueueCallback () function.
The message queue
IOHIDEvent is Mach port forwarding to the corresponding app after the process, is awakened com. Apple. Uikit. Eventfetch Runloop – thread thread, And call the corresponding __IOHIDEventSystemClientQueueCallback Source1 () callback. This callback fetches the event from the message queue, sets Source0 corresponding to main thread__handleEventQueue to pending, and wakes up the Runloop of the main thread.
Message delivery
Now began to call __eventQueueSourceCallback function for message distribution, _UIApplicationHandleEventQueue will encapsulate IOHIDEvent into UIEvent and distributed, start hitTest function calls, such as And TouchBegan/end/move/cancel function is invoked in the callback.
In short
The IOKit. Framework generates an IOHIDEvent event and receives it from the SpringBoard, which uses the Mach port to generate source1, To awaken the target APP com. Apple. Uikit. RunLoop eventfetch – thread. The Eventfetch thread sets the source0 corresponding to __handleEventQueue in the main runloop to excursion = Yes and wakes up the main Runloop. MainRunLoop calls __eventQueueSourceCallback for event queue processing.
Runloop and autorelease pool
Runloop can register several to monitor their state change of callback, when Runloop enter an event loop, will be called AutoreleasePoolPage: : push () method to create a new automatic release of pool and the incoming sentry object, Objects in the release pool pop out automatically when exiting or hibernating.
Write in the last
The main function to run CFRunLoopRun() is a bit long… Leave it for the next analysis
Welcome to point out any mistakes or omissions
Tino Wu
more at tinowu.top