[TOC]

RunLoop responds to events

2020-03-22

In the previous article, Debugging iOS User Interaction Event Response Process, the whole process of iOS event response was debugged, but only the implementation details of the application layer of the event passing between UIKit view levels were covered. The underlying process of where the event is generated and how it is distributed to UIKit layer was not mentioned. This article attempts to explore the underlying process of event response, starting with RunLoop.

XNU kernel and Mach

Before we get into that, let’s talk about the XNU kernel. First, Both Mac OS X and iOS were developed based on Darwin. Darwin is the core operating system part of Mac OS X, and the kernel of Darwin is XNU (X is Not Unix).

1.1 XNU Kernel Architecture

The XNU kernel is a hybrid architecture kernel that is built around the Mach microkernel with the necessary system components such as BSD and I/O Kit added on top. Carry the following three Kernel Architecture diagrams for Mac OS X from the NextPrevious Kernel Architecture Overview. (The same is true for iOS.)

Mach is the kernel within the XNU kernel. Mach provides low-level functions such as CPU management, memory management, and task scheduling, and provides a Mach Message-based communication infrastructure for components at the operating system layer. The more specific functions of Mach are the most basic operating system functions such as process (inter-machine) communication (IPC), remote procedure call (RPC), symmetric multiprocessing scheduling (SMP), virtual memory management, paging support, modular architecture, clock management, etc.

Berkly Software Distribution (BSD) realizes the core functions of all modern operating systems, including file system, network communication system, memory management system, user management system and so on. BSD is part of the kernel environment, but because it provides a rich application layer API to the outside world, it behaves a little bit outside the kernel and at the application layer. Network Kernel Extensions (NKEs) Monitor Network traffic, modify Network traffic, receive asynchronous notifications from drivers, and more.

I/O Kit is an object-oriented encapsulation of external device Driver. In addition to processing signals from I/O devices, it also provides rich KPIs for I/O device Driver development. Generally, iOS development rarely involves driver development, mainly because the core code of the iOS operating system is not open source, and iOS is also very careful in the management of hardware permissions, so it will not develop such a low-level interface on iOS. For content involving hardware interaction, iOS usually provides the relevant application development framework for developers to call; Mac OS X opened up the I/O Kit specifically for the development of peripheral drivers.

In summary, the XNU kernel composition is externally represented as Mach + BSD + I/OKit, with the BSD layer built on top of the I/OKit layer and the Mach kernel as the core running through the two layers. Mach was central to task scheduling and underlying message communication. NSRunLoop Source1 wakes up the RunLoop using a Mach message.

1.2 mach_msg

In the process of program debugging, it is often necessary to suspend the program running breakpoint, the program is suspended by sending mach_MSG message to achieve. From the call stack below, you can see that mach_msG_trap is called in mach_msg. When the App receives mach_MSG_Trap, the syscall directive triggers a system call, and the application moves from user mode to kernel mode.

Entering kernel mode means that the application gets the highest privileges to access system resources, including CPU scheduling, register read and write, memory access, virtual memory management, peripheral access, cross-process access, and so on. These tasks cannot be accomplished in user mode. At this point, the current thread receiving mach_msg suspends the task in hand, saves the context of the current thread, and waits for the system call to complete. After the MSG_MSG message is received to wake up, the thread is back up and running.

Perhaps because Mach was so powerful and so flexible, based on very few apis, the XNU kernel designers added a layer of BSD control over Mach to provide a more specific and uniform specification for operating system kernel development.

When you use the Profile >> System Trace tool to track the CPU usage of an application, you can see that a stationary application is mostly in the Sytem call state (red stripe area) and the main thread is Blocked (gray stripe area). This is because the system calls mach_msg to enter kernel mode and block the main thread when iOS does not receive user events and no running logic is required. At this time, the main thread RunLoop is in sleep state. This is why iOS keeps the CPU running on low power most of the time. Only when the system receives the need to deal with sporadic events (as shown in figure blue stripe area), only from the kernel state back to user mode process events, event handling, of course, if you are in the process of scheduling system resources will be cut to kernel mode, such as the NSLog function call, will block the thread, in the process of I/O output logs, after the completion of the return to user mode.

RunLoop debugging

This section refers to the description of RunLoop in apple’s official documents, and combines LLDB to debug the data structure and workflow of RunLoop, which is the core chapter of this article.

2.1 RunLoop profile

Normally, threads are destroyed as soon as they finish performing a given task. In some cases, however, developers want threads to be resident and enter a state of waiting for tasks when idle. This is where RunLoop is needed to keep threads alive.

The figure below is an incomplete state transition diagram of RunLoop, which can visually show the general working flow of RunLoop. The part marked in red on the right is the main body of RunLoop processing logic, which is obviously a cyclic iterative process of “wake up -> process message -> sleep”. The input source can be thought of as the place where RunLoop receives messages.

Refer to the definitions from the official documentation. RunLoop is used to monitor a task’s input sources and schedule them when they are ready. Common input sources include: user input devices, network connections, clocks, delay-triggered events, asynchronous callbacks, and so on. There are three types of input Sources that RunLoop can monitor: Sources, Timers, and Observers, which have callback functions. When RunLoop receives a firing event from an input source, it executes a callback function for that input source. Before you can monitor an input source, you need to add it to RunLoop. When the monitoring is no longer needed, it is removed from RunLoop and the callback function is no longer fired.

Note: RunLoop is a synchronous call to the callback function of input Sources, not asynchronous, which means that if the processing of the callback function is very time-consuming, it will directly affect the timeliness of other input Sources’ event response.

Sources, Timers, and Observers are associated with one or more run loop modes. Mode defines the range of input sources (events) that the RunLoop monitors as it runs. Before running RunLoop, you need to specify the Mode that RunLoop enters. After running, RunLoop processes only events triggered by monitored objects contained in Mode. In addition, the RunLoop can be run repeatedly, so you can control the RunLoop to be in the right Mode at the right time to handle the right events.

The relationship between RunLoop, Sources and Mode is summarized as shown in the figure below:

2.1.1 Input Sources

Input sources are classified by the type of event that is triggered. Where, Sources’ triggering event is external message (signal); The trigger event of Timer is the clock signal; The triggering event of the Observer is a RunLoop state change. When an input source fires an event, the message it sends is simple enough to be thought of as a pulse 1, which simply marks the input source as being processed. When RunLoop wakes up, it can query which of the input sources in the current Mode needs to be processed and trigger its callback function.

Source0 and Source1

Sources are classified into Source0 and Source1 according to message types.

Source0 is the input source that the application manages itself. The application chooses to call CFRunLoopSourceSignal at the appropriate time to tell RunLoop that there is a Source0 that needs to be processed. For example, after the preparation has been completed on thread A, A signal is sent to Source0 in thread B’s RunLoop, triggering (but not immediately) the main task logic in the Source0 callback function to begin execution. CFSocket is implemented through Source0.

Source1 is an input source managed by RunLoop and the kernel. Source1 needs to associate a Mach port and send a trigger event signal through the Mach port to tell RunLoop that a Source1 needs to be processed. When the Mach port receives a Mach message, the kernel automatically sends a signal to Source1. The contents of the Mach message are also sent to Source1 as the context for triggering the Source1 callback. CFMachPort and CFMessagePort are implemented through Source1.

A single Source can be registered in multiple Runloops or modes at the same time, and when the Source event fires, the callback function for the Source is fired regardless of which RunLoop receives the message first. A single Source added to multiple Runloops can be applied to “worker” thread pool management that processes discrete data sets (with no correlation between the data), such as the “producer-consumer” model of message queues, where when a task arrives, a random thread is automatically triggered to receive the data and process it.

The main differences between Source0 and Source1 are summarized as follows:

  • The methods of sending events are different, but Source0 is passedCFRunLoopSourceSignalTo send event signals, Source1 sends event messages through Mach port.
  • Events vary in complexity, and events on Source0 are context-free (equivalent to simple)1Signal), Source1’s events are contextual (with message content);
  • Source1 has more Mach port members than Source0.

Note: A run loop source can be registered in multiple run loops and run loop modes at the same time. When the source is signaled, whichever run loop that happens to detect the signal first will fire the source.

Timer

A Timer is a RunLoop input source with a preset point at which events are triggered. You can set the Timer to fire only once or to fire repeatedly at a specified interval. A Timer that is repeatedly triggered can manually trigger the next Timer event. CFRunLoopTimer and NSTimer are toll-free bridged.

The Timer is not real time, it fires if the RunLoop is running in the Mode in which the Timer is running. When the Timer’s preset firing time is reached, if the RunLoop is running in another Mode or if the RunLoop is processing a complex callback, the current iteration of the RunLoop will skip the Timer firing event. Wait until the next iteration of RunLoop to check the Timer and fire the event.

The essence of the Timer input source is to register the trigger time in RunLoop based on the clock signal. When RunLoop wakes up and enters the iteration, it checks whether the Timer has reached the trigger time and calls the Timer’s callback function if it has. The registration time of a Timer is always arranged according to the firing time policy specified when the Timer is initialized. For example, if a Timer starts at 2020-02-02 12:00:00 and fires every 5s, its 2020-02-02 12:00:05 trigger event is delayed until 2020-02-02 12:00:06. Then the next trigger point of the Timer is still 2020-02-02 12:00:10, instead of adding 5s to the delayed trigger point. In addition, if multiple trigger time points are skipped during the Timer delay, RunLoop will only trigger the Timer callback function once when checking the Timer at the next trigger time point.

Note that a Timer can only be added to a RunLoop, but a Timer can be added to multiple Modes of a RunLoop.

Note: A timer can be registered to only one run loop at a time, although it can be in multiple modes within that run loop.

Observer

In the previous input source, the event of Source0 comes from the manual trigger signal, the time of Source1 comes from the Mach ports of the kernel, and the event of Timer comes from the clock signal sent by the kernel through the Mach port. The events of the Observer are state changes from the RunLoop itself.

The state of a RunLoop is represented by the CFRunLoopActivity type, including

  • kCFRunLoopEntry,
  • kCFRunLoopBeforeTimers,
  • kCFRunLoopBeforeSources,
  • kCFRunLoopBeforeWaiting,
  • kCFRunLoopAfterWaiting,
  • kCFRunLoopExit,
  • kCFRunLoopAllActivities(the collection of all states).

When building a RunLoop Observer, you need to specify the target RunLoop state that it observes. The state is the bit domain. You can specify multiple target states that the Observer observes through the bitwise and operation of CFRunLoopActivity. When the state of the RunLoop observed by the Observer changes accordingly, the RunLoop fires the callback function of the Observer.

Note that an Observer can only be added to a RunLoop, but an Observer can be added to multiple Modes of a RunLoop.

Note: A 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.

2.1.2 Modes

Previously mentioned Modes range the input sources that a RunLoop’s running process needs to process. By default, RunLoop is specified to enter the default RunLoop Mode (kCFRunLoopDefaultMode). The default RunLoop Mode is used to process events from the input source when the application (thread) is idle. But there’s no limit to the variety of RunLoop Modes. Developers can even create custom modes. The modes are distinguished by the Mode name string. The only modes that Core Foundation discloses are:

  • kCFRunLoopDefaultMode
  • kCFRunLoopCommonModes

Foundation exposes more modes:

  • NSDefaultRunLoopMode
  • NSRunLoopCommonModes
  • NSEventTrackingRunLoopMode
  • NSModalPanelRunLoopMode
  • UITrackingRunLoopMode
Common Modes

Core Foundation also defines a special Mode, CommonModes (kCFRunLoopCommonModes), which is used to associate Sources, Timers, and Observers with multiple modes at the same time. Each RunLoop has its own set of common modes, but the default mode must be one of them. Common modes are saved with collection data types (hash tables). You can use CFRunLoopAddCommonMode to specify a Mode as common Mode.

Let me give you an example. When the NSTimer is added to the NSDefaultRunLoopMode of the RunLoop main thread, the Timer is only associated with the default mode. The selector registered by NSTimer is not triggered when the user is scrolling through the interface. Because the main thread RunLoop enters UITrackingRunLoopMode when the user scrolls through the interface, there is no Timer input source, so the Timer event does not fire. One solution is to add NSTimer to NSRunLoopCommonModes of the main thread RunLoop.

To debug the difference between adding a Timer to default mode and adding it to common modes, use the following code to debug. And in the NSLog (@ “”); Hit a breakpoint and run.

CFRunLoopTimerRef defaultTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, 0.1.0.0And ^ (CFRunLoopTimerRef timer) {
    static int tick = 0;
    NSLog(@"Timer in default mode tick: %d", tick++);
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), defaultTimer, kCFRunLoopDefaultMode);
    
CFRunLoopTimerRef commonTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, 0.2.0.0And ^ (CFRunLoopTimerRef timer) {
    static int tick = 0;
    NSLog(@"Timer in common modes tick:%d", tick++);
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), commonTimer, kCFRunLoopCommonModes);

// Let RunLoop hold the Timer
CFRelease(defaultTimer);
CFRelease(commonTimer);

NSLog(@ "");
Copy the code

After the program hits a breakpoint, enter the following LLDB command in the red box to print both timers and the current RunLoop object. As shown in the following figure, there is too much information about the RunLoop object. Run Command+F to search for the memory addresses of the two Timer objects in the debug log.

Firstly, the defaultTimer added to default mode is searched, and it is found that the defaultTimer is only added to kCFRunLoopDefaultMode, as shown in the figure below

Then search for the commonTimer added to the Common Modes and find that the commonTimer has been added in three places:

  • common modes item

  • UITrackingRunLoopMode

  • kCFRunLoopDefaultMode

The common modes of the current RunLoop contain two kCFRunLoopDefaultMode and UITrackingRunLoopMode. This means that when an input source is added to a RunLoop’s kCFRunLoopCommonModes, the input source is also added to all modes contained in RunLoop’s common modes, It is also added to RunLoop’s Common Items for archival purposes. The point is that by adding the Timer to kCFRunLoopCommonModes, the UITrackingRunLoopMode marked as common Mode will also add the Timer. This is why the Timer’s event can be triggered even when the RunLoop is running in UITrackingRunLoopMode while scrolling the page. The Timer added to kCFRunLoopDefaultMode does not trigger because it was added only to kCFRunLoopDefaultMode.

You can further try searching for any of the input sources in the Common Items, and multiple results will be hit in the debug window log.

Note: Once a mode is added to the set of common modes, it cannot be removed.

2.2 RunLoop and Thread

By default, threads (except the main Thread) do not have runloops, which means they can be destroyed when they finish their tasks. Cocoa also does not provide API for creating RunLoop, which can only be obtained by CFRunLoopGetMain() and CFRunLoopGetCurrent(). When detecting that a thread has not created a RunLoop instance, the system automatically creates a RunLoop for it.

RunLoop exposes two sets of interfaces. NSRunLoop and CFRunLoop can be switched between toll-free bridging. The code for CFRunLoop is open source. Note that NSRunLoop is not thread-safe and that Apple Documentation has the following Warning stating that the methods of the RunLoop cannot be called on a thread other than the thread of the RunLoop.

Warning: The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an NSRunLoop object running in a different thread, as doing so might cause unexpected results.

2.3 the RunLoop API

There are two sets of APIS published by RunLoop: NSRunLoop and CFRunLoop. The LATTER is more complete. Therefore, this chapter only describes the API of CFRunLoop. The Warning posted on Apple Documentation means that NSRunLoop is not thread safe, You can’t call NSRunLoop’s methods outside of the NSRunLoop thread. (not being able to call the performSelector:XXX interface outside of the NSRunLoop thread seems to make the NSRunLoop interface a little clunky.) This chapter makes a simple classification of the public APIS of CFRunLoop. Most of the API uses can be known from the interface, so only a part of the API is annotated.

2.3.1 RunLoop Operation API

Run the RunLoop
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
Copy the code

The CFRunLoopRunInMode command is used to run the RunLoop in the specified mode. CFRunLoopRunInMode can be called recursively, meaning that the developer can call CFRunLoopRunInMode in any callback function within a RunLoop to form a nested RunLoop activation pattern on the call stack of the thread in which the RunLoop resides. This means that within the RunLoop callback function, developers are free to call CFRunLoopRunInMode to switch between RunLoop Mode as needed, with few side effects.

  • The seconds argument indicates the length of time that RunLoop will run. If seconds is 0, RunLoop will only process events from one input source (if it happens to be source0, There is a possibility that one more event (TODO) has been handled and no matter what returnAfterSourceHandled the developer has made it happen.

  • ReturnAfterSourceHandled specifies whether the RunLoop exits immediately after executing the source. If it is NO, the source will not exit until the seconds point is reached.

  • Returns the reason why RunLoop exited.

    • kCFRunLoopRunFinished: No input source in RunLoop;
    • kCFRunLoopRunStoped: RunLoopCFRunLoopStopFunction termination;
    • kCFRunLoopRunTimedOut:secondsWhen the time arrives, the timeout exit;
    • kCFRunLoopRunHandledSource: Processing an input source has been completed. The return value will only be inreturnAfterSourceHandledParameters fortrueWill appear when.

CFRunLoopRun runs RunLoop in default mode.

Note: You must not specify the kCFRunLoopCommonModes constant for the mode parameter. Run loops always run in a specific mode. You specify the common modes only when configuring a run-loop observer and only in situations where you want that observer to run in more than one mode.

Wake up the RunLoop

CFRunLoopWakeUp is used to wake up a RunLoop. The RunLoop is asleep when no event is emitted from the input source, and will remain asleep until it times out or is explicitly woken up. When you modify the RunLoop, such as adding an input source, you must wake up the RunLoop to process the modification. When a signal is sent to Source0 and the RunLoop is expected to process it immediately, you can call CFRunLoopWakeUp to wake up the RunLoop immediately.

Suspend the RunLoop

CFRunLoopStop is used to stop RunLoop running and return control to the function that invoked CFRunLoopRun or CFRunLoopRunInMode to activate RunLoop running. If the function is a callback to RunLoop, that is, CFRunLoopRunInMode nesting, only the RunLoop activated by the innermost CFRunLoopRunInMode call will be aborted.

Wait status of RunLoop

If the RunLoop’s input source has no events to process, the RunLoop goes to sleep until the RunLoop is either explicitly woken up by CFRunLoopWakeUp or by a Mach_port message. CFRunLoopIsWaiting can be used to check whether the RunLoop is asleep, is processing an event, or has not yet started running, all of which return false. Note that this function is only used to query the RunLoop state of an external thread, because a query for the current RunLoop state will only return false.

2.4 RunLoop process

One can think of two ways to explore the RunLoop process: LLDB debugging and source code interpretation. The former is pretty straightforward, so let’s start with that.

2.4.1 LLDB Debugging the RunLoop process

Source0 debugging

Still using the simple Demo of Debugging iOS User Interaction Event Response Process, But block all the custom hitTest:withEvent:, nextResponder, touchesBegan:withEvent:, touchesBegan:withEvent: code in it. You don’t need to see these prints to debug. Then plant a breakpoint in the didClickBtnFront: Click “Button in front of me” click event callback. Click “Click on my front Button” program interrupt.

Use the BT command to view the call stack as shown below, and extract the runloop-related calls as shown in the red box below. It turns out that the user interaction event in iOS is a CFRunLoop object run by calling the CFRunLoopRunSpecific function in GSEventRunModal. When the click event fires, it wakes up the RunLoop, RunLoop starts running a Source0 callback function by calling __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ with __CFRunLoopDoSource0. Then comes the event response flow.

However, “some CFRunLoop object” is the specific RunLoop, the specific mode of RunLoop running, and the “some Source0” is the specific Source0. None of these details are yet available.

First try the details of CFRunLoop. First, find the stack frame that calls CFRunLoopRunSpecific from the call stack, in this case, stack frame 16, and frame select 16 into that stack frame. The register $RBX assigned before the call to CFRunLoopRunSpecific is printed. The result is kCFRunLoopDefaultMode, which runs RunLoop at the default mode.

Keep going to frame 15 and see if there’s any surprises. Print it to register R13. Yoo-hoo, dare YOU. By seeing the familiar face of kCFRunLoopDefaultMode and also finding a “suspiciously looking” callback function named __handleEventQueue Source0, Notice that frame 10 of the call stack happens to be __handleEventQueueInternal and that’s the Source0 we’re looking for.

The bigger surprise came later. Print register R15. Huh? That’s the RunLoop we’re looking for. And it also contains the kCFRunLoopDefaultMode printed earlier. Po CFRunLoopGetMain() prints the main thread RunLoop to confirm that the RunLoop is the main thread RunLoop.

The Source0 trigger process is as follows:

->CFRunLoopRunXXX ->__CFRunLoopRun ->__CFRunLoopDoSources0 ->__CFRunLoopDoSource0 ->__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

However, the details of adding source and adding mode seem to be missing, which should be done when the application is initialized and is basically a call to the corresponding API implementation of CFRunLoop, so this part is not debugged.

So who sends the trigger to Source0? The next step is to find out who is behind this. As we know from the previous introduction to Source0, CFRunLoopSourceSignal is used to send the trigger. Breakpoint set -n CFRunLoopSourceSignal breakpoint set -n CFRunLoopSourceSignal When you are ready, click “Tap my front Button”. The breakpoint then hits many times, and each time BT looks at the call stack and sees that the previous hits are related to event firing.

UIEventFetcher’s _receiveHIDEventInternal method is used to receive user interaction events sent from the IOHID (I/O Hardware Interface Device) layer. The next step is to debug where the user event comes from the first time the breakpoint is hit. Frame Select 1 enters frame 1 and prints the key register data. It can be inferred that the user interaction event is sent by the underlying layer through IOHIDEventSystemClient and HIDServiceClient.

So, what is the form of the event sent from the bottom? Let’s tentatively print the register contents. When I try the RBX register, I find a “thing” that looks like an event, which looks like it represents a touch event, and further Po [$RDX class] prints the type HIDEvent. This looks like the user touch event sent from the IOHID layer.

Presumably HIDEvent is received through UIEventFetcher and converted into UIEvent and sent to UIKitCore framework for processing. It is also important to note that the Thread that collects HIDEvent is not the main Thread, as can be seen from Thread 6 in the above debugging process. This means that collecting HIDEvent from the IOHID layer and processing UIEvent events are in different threads, and the latter is in the main thread.

Cfrunloopdosource1 is triggered by sending a Mach port message. The kernel is responsible for __CFRunLoopDoSource1.

Finally, who wakes up the main thread to handle Source0 or does it not need to? To verify this problem, the next global breakpoint of CFRunLoopWakeUp is found to trigger CFRunLoopWakeUp to wake the RunLoop after clicking the button. Which RunLoop is this? We tried to print the register contents again and found that the rDI register happened to hold a RunLoop object, as shown in the figure below. By printing the main thread RunLoop object via Po CFRunLoopGetMain(), you can confirm that it is the main thread RunLoop that has been awakened.

At this point, the process of the main thread receiving and triggering UIEvent through Source0 can be connected.

Source1 debugging

Following on from the previous section, explore the process by which Source1 receives HIDEvent from the underlying source. When the breakpoint is reached, use the BT command to check the call stack, and you can see that the Source1 triggering process is as follows. __CFMachPortPerform is the function that __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ calls to trigger the Source1 callback event for the Mach port. A run iteration of RunLoop that does not trigger __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ if no Mach port message to be processed is detected.

->CFRunLoopRunXXX ->__CFRunLoopRun ->__CFRunLoopDoSource1 ->__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ ->__CFMachPortPerform

Focus on stack frames 3 and 5. By tentatively printing the contents of the register, you can see the NSMachPort object that received the IOHID event and its corresponding Source1 contents. Source1 callback is __IOHIDEventSystemClientQueueCallback, corresponding to the above no. 2 stack frame in the call stack, triggered by __CFMachPortPerform.

As for how IOHID event messages are sent to Mach Port, the process can not be intercepted by sendPort, sendBeforeDate, receivePort breakpoints. It is estimated that the implementation is implemented by directly calling the Kernel’s Mach Port message sending API. However, this part of the process is obvious, so I won’t continue debugging here. All you need to know is that if the Source1 input source is a custom one, you need to specify an NSMachPort object for the input source. The sendPort, sendBeforeDate, and receivePort apis are used to send and receive messages.

Guess: As to why the Mach port message was sent without being caught after trying various breakpoints, it is most likely because the message was sent from another process in the system, most likely SpringBoard, which is the desktop APP for iOS. SpringBoard takes the lead in handling events from the accelerometer, handling landscape and portrait switches, receiving the lock button, volume button, and so on. In addition, starting with iOS 6.0, Apple introduced the BackBoard to share some of the functions of SpringBoard, for example, to handle the signal from the light sensor to adjust the screen brightness, desktop APP icon tap and long-press events. BackBoard, like SpringBoard, is a Daemon process.

The Timer debugging

To debug the Timer, add a line in the Demo that uses CFRunLoopTimer. In fact, you can write just about any NSTimer, since both are toll-free bridged as mentioned earlier.

/ / debugging the Timer
CFRunLoopTimerRef defaultTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, 0.1.0.0And ^ (CFRunLoopTimerRef timer) {
    static int tick = 0;
    NSLog(@"Timer in default mode tick: %d", tick);
});
CFRunLoopAddTimer(CFRunLoopGetCurrent(), defaultTimer, kCFRunLoopDefaultMode);

// Do not forget to manually Release the CF resource. At this point, the Timer is held by RunLoop, so it can be released directly after adding it
CFRelease(defaultTimer);
Copy the code

The Demo program will fall into a breakpoint shortly after running. At this time, BT looks at the call stack and can see that the triggering process of calling timer source is also quite simple. The process is as follows:

->CFRunLoopRunXXX ->__CFRunLoopRun ->__CFRunLoopDoTimers ->__CFRunLoopDoTimer ->__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__

As mentioned earlier, the essence of a Timer is to register a point in time in a RunLoop. Looking through the RunLoop source code, the reference for this time is the Uint64_t mach_absolute_time(void) function from the kernel. Point-in-time registration is an indirect call to dispatch_time. It seems that both NSTimer and CFRunLoopTimer timers are implemented by GCD Timer in essence.

CF_PRIVATE dispatch_time_t __CFTSRToDispatchTime(uint64_t tsr) {
    uint64_t tsrInNanoseconds = __CFTSRToNanoseconds(tsr);
    if (tsrInNanoseconds > INT64_MAX - 1) tsrInNanoseconds = INT64_MAX - 1;
    return dispatch_time(1, (int64_t)tsrInNanoseconds);
}
Copy the code
The Observer debugging

Debug CFRunLoopObserver with the following code. If YOU put a breakpoint on NSLog, you’re going to hit a breakpoint pretty quickly. The triggering process of the Observer is as follows:

->CFRunLoopRunXXX ->__CFRunLoopRun ->__CFRunLoopDoObservers ->__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAfterWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    NSLog(@"");
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
Copy the code

Autorelease Pool is very closely related to RunLoop. When the user clicks a button on the interface, the main thread changes from blocked to running (not considering the ready intermediate state), and the main thread RunLoop also triggers a kCFRunLoopAfterWaiting state change. Similarly, when the APP is stationary, the main thread RunLoop enters kCFRunLoopBeforeWaiting. RunLoop will call objc_autoreleasePoolPop once to clean up the AutoRelease pool, and then call objc_autoreleasePoolPush to create an AutoRelease pool. And send mach_MSG message into the kernel mode, the main thread into the blocked state.

2.4.2 Interpreting the Source code of RunLoop

The article is still too long, and then write down the long wife. Here’s amway Ibireme’s [In-depth Understanding of RunLoop]. His blog post is fairly succinct in its extract of RunLoop’s key code. Here is a nice flow chart of RunLoop processing Input Sources summarized in the Ibireme article.

RunLoop and event response

This article was originally intended to be a sequel to Debugging the iOS User Interaction Event Response Process. It was originally titled Event Response and RunLoop.

When debugging RunLoop in the second part, we have used event response as an example to debug the response logic of various input source events of RunLoop. Here, we can directly sort out the flow of user events processed by iOS through RunLoop:

Four,

  • RunLoop is a method of thread survival, which corresponds to threads one to one.
  • A RunLoop contains several modes, and a mode contains several input sources. Mode means that when a RunLoop is executed in mode, it only responds to the input sources in mode. RunLoop can be nested, that is, a callback function call at the input sourceCFRunLoopRunXXX, enabling the RunLoop to switch between various modes freely;
  • Common modes are a special mode. Marking mode as Common means that Common Mode items in the RunLoop will be synchronized to that mode.
  • Each input source contains a callback function. The user handles the receiving event, and the event handling logic is in the callback function.
  • Input Sources include Sources, Timers, and Observers, Source0 and Source1.
  • throughCFRunLoopSourceSignalSend an event signal to Source0, which is called if RunLoop wants to process the event immediatelyCFRunLoopWakeUpWake up the RunLoop;
  • Source1 is associated with a specific Mach port and triggers the Source1 event by sending a Mach Port message to the Mach port.
  • The essence of a Timer is to register a point in time in a RunLoop and fire the Timer’s callback function when the time point arrives,CFRunLoopTimerThe essence is achieved through the GCD Timer;
  • An Observer can observe state changes in a RunLoop and trigger a callback function to the Observer.
  • User interaction events first generate HIDEvent at the IOHID layer and then send HIDEvent messages to the Source1 Mach port of the event processing thread. The callback function of Source1 converts events into UIEvent and filters the events to be processed and pushes them into the event queue to be processed. Signals are sent to the event processing Source0 of the main thread, and the main thread wakes up. After the main thread checks the signal to be processed by the event processing Source0, Trigger the callback function of Source0, extract UIEvent from the event queue to be processed, and finally enter the UIEvent event response flow such as hit-test.