RunLoop is a very basic concept in iOS and OSX development. This article will start with the source code of CFRunLoop and introduce the concept of RunLoop and the underlying implementation principle. We’ll look at how Apple uses RunLoop in iOS to automatically release pools, delay callbacks, touch events, screen refreshes, and more.

Index RunLoop Relationship between RunLoop and threads RunLoop External interface RunLoop Mode RunLoop internal logic The underlying RunLoop implements the functions that Apple uses RunLoop to implement AutoreleasePool Event response Gesture recognition interface update timer PerformSelecter About GCD About network request RunLoop Example AFNetworking AsyncDisplayKit

The concept of the RunLoop

Generally speaking, a thread can only execute one task at a time, and when it is finished, the thread exits. If we need a mechanism for threads to handle events at any time without exiting, the usual code logic looks like this:

function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message ! = quit); }Copy the code

This model is often referred to as the Event Loop. Event Loop is implemented in many systems and frameworks, such as Node.js Event processing, Windows message Loop, and OSX/iOS RunLoop. The key points to implement this model are how to manage events/messages and how to make threads sleep when they are not processing messages to avoid resource usage and wake up as soon as a message arrives.

So, a RunLoop is essentially an object that manages the events and messages it needs to process and provides an entry function to execute the logic of the Event Loop above. Once the thread executes the function, it will remain in the “receive message -> wait -> process” loop within the function until the loop ends (such as the incoming quit message) and the function returns.

OSX/iOS provides two such objects: NSRunLoop and CFRunLoopRef. CFRunLoopRef is within the CoreFoundation framework and provides apis for pure C functions, all of which are thread-safe. NSRunLoop is a wrapper based on CFRunLoopRef that provides object-oriented apis, but these apis are not thread-safe.

CFRunLoopRef code is open source, you can opensource.apple.com/tarballs/CF here… Download the entire CoreFoundation source code to view.

Update: With Swift open source, Apple maintains a cross-platform version of CoreFoundation: github.com/apple/swift… This version of the source code may be slightly different from the existing iOS implementation, but it is easier to compile and has been adapted for Linux/Windows.

RunLoop in relation to threads

First, iOS development encounters two thread objects: pthread_t and NSThread. There was an Apple document that stated that NSthreads were simply wrappers of pthread_t, but that document is no longer valid, and now it is possible that they are all wrapped directly from low-level Mach threads. Apple doesn’t provide an interface for converting these two objects to each other, but you can be sure that pthread_T and NSThread correspond one to one. For example, you can get the mainThread by pthread_main_thread_np() or [NSThread mainThread]; The currentThread can also be retrieved by pthread_self() or [NSThread currentThread]. CFRunLoop is managed based on pThreads.

Apple does not allow the creation of runloops directly, and only provides two auto-fetching functions: CFRunLoopGetMain() and CFRunLoopGetCurrent(). The internal logic of these two functions looks something like this:

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).

RunLoop External interface

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. Their relationship is as follows:

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.

CFRunLoopSourceRef is where the event is generated. Source has two versions: Source0 and Source1. • Source0 contains only one callback (function pointer) and does not actively fire events. To use this, you need to call CFRunLoopSourceSignal(source), mark the source as pending, and then manually call CFRunLoopWakeUp(runloop) to wake up the Runloop to handle the event. • Source1 contains a mach_port and a callback (function pointer) that is used to send messages to each other through the kernel and other threads. This Source actively wakes up the RunLoop thread, as described below.

CFRunLoopTimerRef is a time-based trigger that is toll-free bridged and can be mixed with NSTimer. 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.

CFRunLoopObserverRef is the Observer, and each Observer contains a callback (function pointer) that the Observer receives when the state of the RunLoop changes. The following time points can be observed:

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

The Source/Timer/Observer above 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.

The RunLoop Mode

The structure of CFRunLoopMode and CFRunLoop is roughly as follows:

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

There is a concept called “CommonModes” : A Mode can mark itself as a “Common” attribute (by adding its ModeName to the 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.

Application scenario: The main RunLoop has two preset modes: kCFRunLoopDefaultMode and UITrackingRunLoopMode. Both modes have been marked with the “Common” attribute. DefaultMode is the normal state of App, TrackingRunLoopMode is to track the state of ScrollView sliding. When you create a Timer and add DefaultMode, the Timer gets repeated callbacks, but when you slide a TableView, RunLoop switches mode to TrackingRunLoopMode, and the Timer doesn’t get called back. And it doesn’t affect the sliding operation.

Sometimes you need a Timer that gets callbacks in both modes. One way to do this is to add the Timer to both modes. Another option is to add the Timer to the top RunLoop’s “commonModeItems.” CommonModeItems is automatically updated by RunLoop to all modes with the Common property.

CFRunLoop exposes only the following two management Mode interfaces:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName); CFRunLoopRunInMode(CFStringRef modeName, ...) ;Copy the code

Mode exposes the following interfaces for managing Mode items:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);Copy the code

You can only manipulate internal modes by mode name. If you pass in a new mode name and there is no mode in the RunLoop, the RunLoop will automatically create the corresponding CFRunLoopModeRef for you. For a RunLoop, internal modes can only be added, not removed.

There are two modes publicly provided by Apple: kCFRunLoopDefaultMode (NSDefaultRunLoopMode) and UITrackingRunLoopMode. You can use these two Mode names to operate the corresponding modes.

Apple also provides a string to manipulate Common flags: kCFRunLoopCommonModes (NSRunLoopCommonModes). You can use this string to manipulate Common Items or mark a Mode as “Common”. Be careful to distinguish this string from other mode names when using it.

The internal logic of RunLoop

According to Apple’s documentation, the logic inside RunLoop goes something like this:

The internal code is as follows:

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

As you can see, RunLoop is actually just such a function, with a do-while loop inside. When you call CFRunLoopRun(), the thread stays in the loop forever; This function does not return until it times out or is stopped manually.

The underlying implementation of RunLoop

As you can see from the code above, the core of RunLoop is based on Mach Port and the function mach_msg() is called when it goes to sleep. To explain this logic, let’s take a look at the OSX/iOS architecture.

Apple officially divides the system into four layers. The application layer includes graphics apps that users can access, such as Spotlight, Aqua and SpringBoard. The application framework layer is the Cocoa and other frameworks that developers come into contact with. The core framework layer includes various core frameworks, OpenGL and other contents. Darwin is the core of operating system, including system kernel, driver, Shell and other content. This layer is opensource, and all of its source code can be found at opensource.apple.com.

Let’s take a closer look at the core of Darwin:

The three components above the hardware layer, Mach, BSD, and IOKit (along with a few others not noted above), together make up the XNU kernel. The inner ring of the XNU kernel is called Mach, and as a microkernel, it provides only a very small number of basic services such as processor scheduling and IPC (inter-process communication). The BSD layer can be viewed as an outer ring around the Mach layer, providing functions such as process management, file systems, and networking. The IOKit layer is a framework that provides an object-oriented (C++) framework for device drivers.

Mach itself provides very limited apis, and Apple discourages the use of Mach’s apis, but they are so basic that nothing else can be done without them. In Mach, everything was implemented through its own objects, with processes, threads, and virtual memory all referred to as “objects.” Unlike other architectures, Under Mach objects cannot be called directly, only through messaging.” Messages are the most basic concept in Mach. Messages are transmitted between two ports. This is the core of IPC (inter-process communication) in Mach.

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

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:

See Wikipedia for System_call, Trap_(Computing).

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 and pause the App while it’s still, you’ll see that the main thread call stack is stuck at mach_MSg_trap ().

For details on how to use a Mach port to send messages, check out the NSHipster article, or the Chinese translation here.

The history of Mach can be found in this interesting article: The Story behind Mac OS X. Avie Tevanian was the father of Mach.

What Apple does with RunLoop

First, we can look at the status of RunLoop after App startup:

CFRunLoop { current mode = kCFRunLoopDefaultMode common modes = { UITrackingRunLoopMode kCFRunLoopDefaultMode } common mode items = { // source0 (manual) CFRunLoopSource {order =-1, { callout = _UIApplicationHandleEventQueue}} CFRunLoopSource {order =-1, { callout = PurpleEventSignalCallback }} CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} // source1 (mach port) CFRunLoopSource {order = 0, {port = 17923}} CFRunLoopSource {order = 0, {port = 12039}} CFRunLoopSource {order = 0, {port = 16647}} CFRunLoopSource {order =-1, { callout = PurpleEventCallback}} CFRunLoopSource {order = 0, {port = 2407, callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}} CFRunLoopSource {order = 0, {port = 1c03, callout = __IOHIDEventSystemClientAvailabilityCallback}} CFRunLoopSource {order = 0, {port = 1b03, callout = __IOHIDEventSystemClientQueueCallback}} CFRunLoopSource {order = 1, {port = 1903, callout = __IOMIGMachPortPortCallback}} // Ovserver CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry callout = _wrapRunLoopWithAutoreleasePoolHandler} CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting callout = _UIGestureRecognizerUpdateObserver} CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit callout = _afterCACommitHandler} CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit callout = _wrapRunLoopWithAutoreleasePoolHandler} // Timer CFRunLoopTimer {firing = No, Interval = 3.1536e+09, tolerance = 0, next fire date = 453098071 (-4421.76019@96223387169499), callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)} }, Modes = {CFRunLoopMode {sources0 = {/* same as 'common mode items' */}, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { /* same as 'common mode items' */ }, sources1 = { /* same as 'common mode items' */ }, observers = { /* same as 'common mode items' */ }, timers = { /* same as 'common mode items' */ }, }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = 0, { callout = FBSSerialQueueRunLoopSourceHandler}} }, sources1 = (null), observers = { CFRunLoopObserver >{activities = 0xa0, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} )}, timers = (null), }, CFRunLoopMode { sources0 = { CFRunLoopSource {order = -1, { callout = PurpleEventSignalCallback}} }, sources1 = { CFRunLoopSource {order = -1, { callout = PurpleEventCallback}} }, observers = (null), timers = (null), }, CFRunLoopMode { sources0 = (null), sources1 = (null), observers = (null), timers = (null), } } }Copy the code

KCFRunLoopDefaultMode: the default Mode of the App, under which the main thread is usually run. 2. UITrackingRunLoopMode: interface tracking Mode, used for ScrollView tracking touch sliding, to ensure that the interface sliding is not affected by other modes. 3. UIInitializationRunLoopMode: when just start the App the first to enter the first Mode, start after the completion of the will no longer be used. 4: GSEventReceiveRunLoopMode: accept system internal Mode of events, usually in less than. 5: KcFRunLoopCommonMode: this is a placeholder Mode and has no effect.

You can see more of Apple’s internal modes here, but those modes are harder to come by in development.

When a RunLoop does a callback, it usually does a long function call out, which is usually visible on the call stack when you debug your code with breakpoints. The following is a condensed version of each function. If you see the long function name in the call stack, look it up here to locate the specific call location:

Observers create AutoreleasePool: _objc_autoreleasePoolPush(); /// Observers create AutoreleasePool: _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry); Do {/// 2. Notify Observers that a Timer callback is about to occur. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers); Notifying Observers that Source (non-port-based,Source0) callback is about to occur. __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// 4. Trigger the Source0 (non-port-based) callback. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0); __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); /// Observers are in this state torelease and create AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting); /// 7. sleep to wait msg. mach_msg() -> mach_msg_trap(); Observers, __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting); /// 8. /// 9. If the Timer wakes up, call Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(Timer); /// 9. Block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block); If Runloop is woken up by Source1 (port based) event, Handle this event __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1); } while (...) ; Observers are about torelease AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit); }Copy the code

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.

Incident 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.

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).

Interface to update

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.

The internal call stack looks something like this:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];Copy the code

The timer

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.

CADisplayLink is a timer with the same refresh rate as the screen (but the implementation is more complicated, unlike NSTimer, which actually operates on a Source). If a long task is executed between screen refreshes, one frame will be skipped (similar to NSTimer) and the interface will feel stuck. Even a frame stalling is noticeable to the user when swiping a TableView quickly. Facebook’s open source AsyncDisplayLink is designed to solve this problem, and it also uses RunLoop internally, which I’ll discuss in a separate blog post later.

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.

About the GCD

RunLoop is dispatch_source_t Timer. (NSTimer is mk_timer using XNU kernel. NSTimer is indeed driven by mk_timer, not GCD). However, some GCD interfaces also use RunLoop, such as dispatch_async().

When dispatch_async(dispatch_get_main_queue(), block) is called, libDispatch sends a message to the main thread’s RunLoop, the RunLoop wakes up and takes the block from the message, This block is executed in the __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() callback. But this logic is limited to dispatches to the main thread, dispatches to other threads are still handled by libDispatch.

About Network Request

In iOS, the interface for network requests has the following layers from bottom to top:

CFSocket
CFNetwork       ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession    ->AFNetworking2, AlamofireCopy the code

• CFSocket is the lowest level interface and is only responsible for socket communication. • CFNetwork is the upper layer encapsulation based on CFSocket and ASIHttpRequest works on this layer. • NSURLConnection is a higher level encapsulation based on CFNetwork that provides an object-oriented interface. AFNetworking works at this layer. • NSURLSession is a new interface in iOS7, ostensibly alongside NSURLConnection. But the underlying still use the NSURLConnection features (such as com. Apple. NSURLConnectionLoader thread), AFNetworking2 and Alamofire work in this layer.

The following describes the working process of 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.

Examples of RunLoop applications

AFNetworking

The AFURLConnectionOperation class is built on NSURLConnection and is expected to receive Delegate callbacks in the background thread. AFNetworking creates a separate thread for this purpose and starts a RunLoop in this thread:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}Copy the code

Before RunLoop starts, there must be at least one Timer/Observer/Source, so AFNetworking creates a new NSMachPort and adds it to it before [RunLoop run]. Normally, the caller needs to hold this NSMachPort (mach_port) and send messages inside the loop through this port from the external thread; But port is added here only to keep the RunLoop from exiting, not to actually send messages.

- (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread]  withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } [self.lock unlock]; }Copy the code

When need the background threads execute tasks, AFNetworking by calling [NSObject performSelector: onThread:..] Throw the task into the RunLoop of the background thread.

AsyncDisplayKit

AsyncDisplayKit is Facebook’s framework for keeping interfaces smooth. It works as follows:

Heavy tasks in the UI thread will cause the interface to stall. These tasks are usually divided into three categories: typesetting, drawing, and UI object manipulation.

Typesetting usually involves calculating the view size, calculating the text height, recalculating the layout of a subgraph, and so on. Drawing generally includes text drawing (such as CoreText), image drawing (such as pre-decompression), element drawing (Quartz) and other operations. UI object operations typically involve creating, setting properties, and destroying UI objects such as UIView/CALayer.

The first two types of operations can be thrown into background threads by various methods, while the last type of operations can only be done in the main thread, and sometimes subsequent operations depend on the results of previous operations (for example, TextView creation may require the size of the text to be calculated in advance). What ASDK does is try to put in the background tasks that can be put in the background, and postpone those that can’t (such as view creation, property adjustments).

To do this, ASDK creates an object called ASDisplayNode and internally encapsulates UIView/CALayer, which has properties similar to UIView/CALayer, such as frame, backgroundColor, etc. All of these properties can be changed in the background thread, and developers can manipulate its internal UIView/CALayer via Node alone, thus putting layout and drawing into the background thread. But no matter how you do it, these properties need to be synchronized to the UIView/CALayer of the main thread at some point.

ASDK implements a similar interface update mechanism modeled after the QuartzCore/UIKit framework: Add an Observer to the RunLoop of the main thread, listen for kCFRunLoopBeforeWaiting and kCFRunLoopExit events, and, upon receiving a callback, iterate over all previously queued tasks and execute them. The code can be found here: _ASAsyncTransactionGroup.

The last

I haven’t written a blog for a long time. I moved my blog from AWS to Linode a few days ago, and then I seriously changed my blog theme. The layout looks ok ~ 😳