Today we are going to talk about RunLoop. We all know RunLoop means to run a loop, do something when there is something to do, and rest when there is nothing to do. So the question is, what needs to be done? How do you rest when you rest? How should we use it in practice?

What is RunLoop?

First, a thread can only execute one task at a time, and the thread is destroyed when it is finished. However, we need a mechanism for threads to handle events without destroying them. Consider setting up a thread observer object that handles things when it receives a message, goes to sleep when there is nothing to do, and wakes up again when the message arrives without consuming resources. In fact, there is a mechanism for this in every system, it may be called different, but the meaning is similar, in iOS, it is called RunLoop.

So, RunLoop is really just an object. In iOS, when the program starts, a RunLoop is automatically created corresponding to the main thread. This object manages the events and messages it needs to process, and provides an entry function that a thread executes and stays in a “receive -> wait -> process” loop inside the function until the loop ends and the function returns.

RunLoop and thread relationship

Let’s start with a piece of source code

CFRunLoopRef static CFMutableDictionaryRef loopsDic; Static CFSpinLock_t loopsLock; static CFSpinLock_t loopsLock; // get a RunLoop for pthread. CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (! LoopsDic) {// On first entry, initialize global Dic and create a RunLoop for the master thread. loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } // get it directly from the Dictionary. CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (! Loop) {/// Create a loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); // register a callback that destroys its RunLoop when the thread is destroyed. _CFSetTSD(... , thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }Copy the code

As you can see from the code above, there is a one-to-one correspondence between threads and runloops, which is stored in a global Dictionary. There is no RunLoop when the thread is created, and if you don’t grab it, it never will. RunLoop creation occurs at the first fetch and RunLoop destruction occurs at the end of the thread. You can only get runloops inside a thread (except for the main thread).

Apple does not allow the creation of runloops directly, and only provides two auto-fetching functions: CFRunLoopGetMain() and CFRunLoopGetCurrent().

The structure of the RunLoop

There are five classes for RunLoop in CoreFoundation:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

The CFRunLoopModeRef class is not exposed, but is encapsulated through the interface of CFRunLoopRef.

Let’s look at the structure of RunLoop in iOS:

A RunLoop contains many modes, and a Mode contains a series of Soure/Observer/Timer.

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.

A Source/Timer/Observer is collectively referred to as a mode item, and an item can be added to multiple modes at the same time. However, it does not work if an item is repeatedly added to the same mode. If there is no item in a mode, the RunLoop exits without entering the loop.

Take a look at the source code:

struct __CFRunLoopMode { CFStringRef _name; // Mode Name, for example, @"kCFRunLoopDefaultMode" CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ... }; struct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ... };Copy the code

Let’s look at _modes and _commonModeItems, respectively.

_modes

By default, the system provides the following five modes:

  • KCFRunLoopDefaultMode: The default Mode in which the main thread normally runs.

  • UITrackingRunLoopMode: the mode used to track touch event firing (e.g. UIScrollView scrolling up and down) Mainthread This mode is set when touch event firing and can be used to set Timer during control event firing.

  • 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: used to accept the system event, belongs to the internal RunLoop mode.

  • KCFRunLoopCommonModes:

Post YYKit author’s blog quote:

Is a Set of commonModes (sets), and a mode can mark itself as “Commons” (by adding its ModeName to RunLoop’s “commonModes”). Whenever the contents of the RunLoop change, the RunLoop automatically synchronizes the Source/Observer/Timer in _commonModeItems to all modes with the “Common” flag.

For example, if I have a Timer associated with each of these modes, and it is troublesome to register each one, I can use: CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) UITrackingRunLoopMode); Add UITrackingRunLoopMode or any other mode to this kCFRunLoopCommonModes, and then simply associate the Timer with kCFRunLoopCommonModes, It is possible to implement a Timer that fires when a RunLoop runs in any of the modes in the set.

Of course, the main thread RunLoop contains kCFRunLoopDefaultMode and UITrackingRunLoopMode by default.

_commonModeItems

1.Source

Source has two versions: Source0 and Source1.

  • Source1: mach_port-based events from the system kernel or other processes or threads that can actively wake up dormant Runloops (we don’t use this actively in iOS interprocess communication development). Just think of mach_port as a mechanism for sending messages between processes.

  • Source0: Non-port-based processing events. What is non-port-based? This means that you did not receive this message directly from another process or kernel.

Take a simple example: An APP stands still in the foreground, and the user clicks the APP interface with his finger. The process is as follows:

We touch the screen, first touch the hardware (screen), and the Event on the screen surface is wrapped as an Event. The Event tells Source1 (mach_port), which wakes up RunLoop, and then distributes the Event to Source0, which then processes it.

If there is no event and no timer, the runloop will sleep, and if there is, the runloop will wake up and run a lap.

2.Timer

CFRunLoopTimerRef is a time-based trigger. It’s toll-free bridged with NSTimer, and can be mixed with it. It contains a length of time and a callback (function pointer). When it is added to the RunLoop, the RunLoop registers the corresponding point in time, and when that point is reached, the RunLoop is woken up to execute that callback.

Note: Timer, source0, and source1 are common sources of RunLoop events, and a GCD is also a common source of RunLoop events. If there are any, I will handle them for you.

3.Observer

CFRunLoopObserverRef is the Observer, and each Observer contains a callback (function pointer) that the Observer receives when the state of the RunLoop changes. Acts as a listener in the message loop, notifying the outside world of the current RunLoop running status.

The observation time is as follows:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), KCFRunLoopBeforeTimers = (1UL << 1), // Timer kCFRunLoopBeforeSources = (1UL << 2), // Source kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), KCFRunLoopExit = (1UL << 7), // About to exit Loop};Copy the code

Summary:

  1. A RunLoop is an object that corresponds one-to-one to a thread and contains a set of five modes, which in turn contain a set of modeItems, which in turn consist of Soure/Timer/Observer.
  2. RunLoop can only run in one fixed Mode. If you need to change Mode, you can only exit the current loop and re-specify another Mode to enter.
  3. At runtime it will only monitor modeItems added in this mode, and if there is no corresponding event source in this mode, RunLoop will return immediately.

RunLoop’s “life Cycle”

We have a pretty good idea of the structure of a RunLoop and what the parts that make up the RunLoop do. It should be easier to understand how RunLoop works next.

Let’s take a look at the flow chart:

If it’s hard to understand, here’s an example to illustrate the above workflow:

  1. When the main thread is started, the system creates the corresponding RunLoop by default, and finally keeps it in kCFRunLoopDefaultMode. At this point, a timer is started 10 seconds later and the RunLoop is in the Sleep state
  2. At 5 seconds, the user clicks the interface (source1 to source0) to wake up RunLoop.
  3. RunLoop notifies observers that observers are about to trigger a Source0 callback, executing the blocks that are added to the RunLoop Block chain.
  4. Execute the user click event (source0) and execute the Block added to the Runloop Block chain.
  5. Determine if a source1 event occurred during this process.
  6. If there is a source1 event, execute source1. After executing the event, determine whether the conditions for exiting Runloop are met. If not, continue Runloop.
  7. If not, go to sleep.
  8. At 10 seconds, a Timer fires, notifying observers that RunLoop is awake.
  9. Execute the timer event to execute the Block added to the Runloop Block chain.
  10. Check whether the conditions for exiting RunLoop are met. If yes, exit RunLoop. If no, continue RunLoop.

The source code is here:

Void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } // start with the specified Mode, RunLoop timeout allowed int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } int CFRunLoopRunSpecific(RunLoop, modeName, seconds, CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); // If there is no source/timer/observer in mode, return it directly. if (__CFRunLoopModeIsEmpty(currentMode)) return; /// 1. Notify Observers: RunLoop is about to enter loop. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); // the internal function, Loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO; int retVal = 0; Do {/// 2. Notify Observers: RunLoop is about to trigger a Timer callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); /// 3. notify Observers: RunLoop is about to trigger the Source0 (non-port) callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); // Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); /// 4. RunLoop triggers the Source0 (non-port) callback. sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); // Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); /// 5. If there is a port based Source1 in ready state, process the Source1 directly and jump to the message. if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } /// Notify Observers: threads of RunLoop are about to enter sleep. if (! sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } /// 7. Call mach_msg and wait for the message to accept mach_port. The thread will sleep until it is awakened by one of the following events. /// • A port-based Source event. __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } /// 8. Notify Observers: The threads of RunLoop have just been awakened. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); // the message is received and processed. Handle_msg: /// 9.1 If a Timer runs out, trigger the Timer's callback. if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, Mach_absolute_time ())} // 9.2 If there are blocks dispatched to main_queue, execute the block. else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 If a Source1 (port based) emits an event, To deal with this event else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort (runloop currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } // execute the block __CFRunLoopDoBlocks(runloop, currentMode) added to the Loop; If (sourceHandledThisLoop && stopAfterHandle) {/// return when the loop is finished. retVal = kCFRunLoopRunHandledSource; RetVal = kCFRunLoopRunTimedOut;} else if (timeout) {retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) {retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, RetVal = kCFRunLoopRunFinished; } // If there is no timeout, mode is not available and loop is not stopped, continue loop. } while (retVal == 0); } /// 10. Notify Observers: RunLoop is about to exit. __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }Copy the code

The underlying principles of RunLoop

As you can see from the source code above, there are two things worth noting: Mach port and mach_msg(). The core of RunLoop is based on Mach Port, and when it goes to sleep it calls mach_msg(). Let’s explain what those two things are.

Mach is a kernel that provides basic services such as CPU scheduling and interprocess communication. In Mach, everything is implemented through its own objects, and processes, threads, and virtual memory are called “objects.” Unlike other architectures, Under Mach objects cannot be called directly, only through messaging.” Message “is the most basic concept in Mach. A message contains the current port local_port and the destination port remote_port, and messages are transmitted between the two ports. This is the core of Mach IPC. Messages are sent and received using the mach_msg() function in < Mach /message.h>, and the essence of mach_msg() is a call to mach_MSg_trap (), which is equivalent to a system call that triggers a kernel state switch. Put the program to sleep. And this state is:

APP is still in memory, but not take the initiative to apply for CPU resources (here don’t seriously, certainly not without the CPU), and then has to monitor a port (port), waiting for the kernel send messages to the port, after listening to the news, to recover from sleep (restart RunLoop cycle, after processing the event continues to sleep).

Mach messages are defined in the < Mach /message.h> header:

typedef struct {
  mach_msg_header_t header;
  mach_msg_body_t body;
} mach_msg_base_t;
 
typedef struct {
  mach_msg_bits_t msgh_bits;
  mach_msg_size_t msgh_size;
  mach_port_t msgh_remote_port;
  mach_port_t msgh_local_port;
  mach_port_name_t msgh_voucher_port;
  mach_msg_id_t msgh_id;
} mach_msg_header_t;
Copy the code

A Mach message is essentially a binary packet (BLOB) with a header that defines the current port local_port and the destination port remote_port, sending and receiving messages through the same API, and whose options indicate the direction of message delivery:

mach_msg_return_t mach_msg(
			mach_msg_header_t *msg,
			mach_msg_option_t option,
			mach_msg_size_t send_size,
			mach_msg_size_t rcv_size,
			mach_port_name_t rcv_name,
			mach_msg_timeout_t timeout,
			mach_port_name_t notify);
Copy the code

To send and receive messages, the mach_msg() function actually calls a Mach trap, the function mach_MSg_trap (), which is the equivalent of a system call in Mach. When you call mach_MSg_trap () in user mode, the trap mechanism is triggered, switching to kernel mode; The mach_msg() function implemented by the kernel does the actual work, as shown below:

The core of the RunLoop is a mach_msg() (see step 7 above). The RunLoop calls this function to receive the message, and if no one else sends a port message, the kernel puts the thread into a waiting state. For example, if you run an iOS App in the simulator, the next symbol breakpoint mach_MSg_trap can verify that the function is running.

Source0, source1, timer

RunLoop receives events from two different types of Sources, and Input Sources delivers asynchronous events, usually messages from another thread or from a different application. Timer Sources transmits synchronization events that occur at a specific time or at repeated intervals. Both types of sources use application-specific handlers to handle events as they arrive.

Input Sources asynchronously passes events to the thread. The source of the event depends on the type of input source and is usually one of two. Port-based input sources monitor the Mach port of the application. Custom input source A custom source for monitoring events. It does not matter whether the input source is port-based or custom as far as the running loop is concerned. Systems typically implement two types of input sources that can be used as-is. The only difference between the two sources is how they signal. Port-based source code is automatically emitted by the kernel, and custom sources must be manually signaled from another thread.

RunLoop in action

1.NSTimer

NSTimer is essentially a CFRunLoopTimerRef. It’s toll-free bridged between them. Once an NSTimer is registered with a RunLoop, the RunLoop registers events for its repeated time points. Such as 10:00, 10:10, 10:20. To save resources, RunLoop does not call back this Timer at a very precise point in time. The Timer has a property called Tolerance that indicates the maximum error allowed when the time point is up.

If a point in time is missed, such as when a long task is executed, the callback at that point will also be skipped without delay. Like waiting for a bus, if I’m busy with my phone at 10:10 and I miss the bus at that time, I’ll have to wait for the 10:20 bus.

2.AutoreleasePool

The App starts, apple registered in the main thread RunLoop two Observer, the callback is _wrapRunLoopWithAutoreleasePoolHandler ().

The first Observer monitors an event called Entry(about to enter Loop), which creates an automatic release pool within its callback by calling _objc_autoreleasePoolPush(). Its order is -2147483647, the highest priority, ensuring that the release pool is created before all other callbacks.

The second Observer monitors two events: calling _objc_autoreleasePoolPop() and _objc_autoreleasePoolPush() while waiting (ready to sleep) torelease the old pool and create a new one; _objc_autoreleasePoolPop() is called upon Exit(about to Exit the Loop) torelease the automatic release pool. The order of this Observer is 2147483647, the lowest priority, ensuring that its release pool occurs after all other callbacks.

The code that executes on the main thread is usually written inside such callbacks as event callbacks and Timer callbacks. These callbacks are surrounded by AutoreleasePool created by RunLoop, so there is no memory leak and the developer does not have to show that the Pool was created.

For more information about AutoreleasePool, check out other bloggers’ blogs and recommend one.

3. Event response

Apple registered a Source1 (based on the Mach port) to the receiving system, the callback function as __IOHIDEventSystemClientQueueCallback ().

When a hardware event (touch/lock/shake, etc.) occurs, an IOHIDEvent event is first generated by IOKit. Framework and received by SpringBoard. Details of this process can be found here. SpringBoard only receives events such as buttons (lock screen/mute etc.), touch, acceleration and proximity sensor, and then forwards them to the App process using Mach port. Then apple registered the Source1 will trigger the callback, and call the _UIApplicationHandleEventQueue () distribution within the application.

_UIApplicationHandleEventQueue () will wrap IOHIDEvent process, and as a UIEvent processing or distribution, including identifying UIGesture/processing screen rotation/send UIWindow, etc. Usually click event such as a UIButton, touchesBegin/Move/End/Cancel events are completed in the callback.

4. Gesture recognition

When the above _UIApplicationHandleEventQueue () to identify a gesture, the first call will Cancel the current touchesBegin/Move/End series callback to interrupt. The system then marks the corresponding UIGestureRecognizer as pending.

Apple registered a Observer monitoring BeforeWaiting (Loop entering hibernation) events, the callback function Observer is _UIGestureRecognizerUpdateObserver (), Internally it gets all GestureRecognizer that has just been marked for processing and executes the GestureRecognizer callback.

This callback is handled when there are changes to the UIGestureRecognizer (create/destroy/state change).

5. Update the interface

When in operation the UI, such as changing the Frame, update the UIView/CALayer level, or manually invoked the UIView/CALayer setNeedsLayout/setNeedsDisplay method, The UIView/CALayer is marked to be processed and submitted to a global container.

Apple registers an Observer to listen for BeforeWaiting(about to go to sleep) and Exit (about to Exit Loop) events, calling back to perform a long function: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (). This function iterates through all the UIViews/Calayers to be processed to perform the actual drawing and adjustment, and update the UI.

6.PerformSelecter

When calling NSObject performSelecter: afterDelay: after its internal will actually create a Timer and add to the current thread RunLoop. So if the current thread does not have a RunLoop, this method will fail.

When the performSelector: onThread: when, in fact, it will create a Timer is added to the corresponding thread, in the same way, if the corresponding thread no RunLoop this method will fail.

7.NSURLConnection

Usually with NSURLConnection, you pass in a Delegate, and when [Connection start] is called, the Delegate will receive event callbacks. In fact, the start function gets the CurrentRunLoop internally and adds four source0s (sources that need to be triggered manually) to DefaultMode. CFMultiplexerSource handles the various Delegate callbacks, CFHTTPCookieStorage handles the various cookies.

When the network transmission, we can see NSURLConnection created two new thread: com. Apple. NSURLConnectionLoader and com. Apple. CFSocket. Private. The CFSocket thread processes the underlying socket connection. The NSURLConnectionLoader thread uses RunLoop internally to receive events from the underlying socket and notify the upper Delegate via the previously added Source0.

The RunLoop in the NSURLConnectionLoader receives notifications from the underlying CFSocket through some Mach Port-based Source. When received, it sends notifications to Source0 such as CFMultiplexerSource when appropriate, and wakes up the Delegate thread’s RunLoop to handle the notifications. The CFMultiplexerSource performs the actual callback to the Delegate in the Delegate thread’s RunLoop.

conclusion

Through this passage, we need to know:

  1. A RunLoop is an object that has a one-to-one correspondence with a thread.
  2. A RunLoop contains a set of modes, which in turn contain a set of ModeItems, which in turn are made up of Source/Timer/Observer.
  3. There are five modes commonly used to handle different things. Commons, in particular, is a set, and you can add other modes to Commons via methods to reduce the repetition associated with modes.
  4. RunLoop can only run in one fixed Mode. If you need to change Mode, you can only exit the current loop and re-specify another Mode to enter
  5. Soure has two classes, Source1 and Source0. Source1 handles events that come from the system kernel or other processes or threads, and Source0 handles events that are not sent directly to you by other processes or kernels.
  6. When the APP starts, the main thread RunLoop starts with it, and contains kCFRunLoopDefaultMode and UITrackingRunLoopMode by default. The Observer can listen for timers and various interactive events and interface refreshes. When an event arrives, event processing is triggered by sending a notification, and the event block is thrown to the block processing queue. After the transaction is processed, it will sleep and wait for the next awakening. If any of the conditions meet, exit RunLoop, and continue RunLoop if not.
  7. The underlying RunLoop is implemented by sending messages between the kernel and thread based on Mach Port. To send and receive messages, the mach_msg() function actually calls the mach_MSg_trap () function.
  8. RunLoop has applications for timers, auto-release pools, interthread communication, event responses, and UI refreshes.