Autorelease Pool

Let’s start with an example:

#import "SecViewController.h"
__weak NSString *stringA = nil;
__weak NSString *stringB = nil;
__weak NSString *stringC = nil;

@implementation SecViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
    NSString *strA = [NSString stringWithFormat:@"2017 _a biubiu lee"];
    stringA = strA;
    
    @autoreleasepool
    {
        NSString *strB = [NSString stringWithFormat:@"2017 _b biubiu lee"];
        stringB = strB;
    }
    
    NSString *strC= nil;
    
    @autoreleasepool
    {
        strC = [NSString stringWithFormat:@"Biubiu lee 2017 _c"];
        stringC = strC;
    }
    
    NSLog(@"viewDidLoad");
    [self showStrings];
}

-(void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
     NSLog(@"viewWillAppear");
     [self showStrings];
}

-(void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear");
    [self showStrings];
}

-(void)showStrings
{
    NSLog(@"stringA: %@", stringA);
    NSLog(@"stringB: %@", stringB);
    NSLog(@"stringC: %@", stringC);
    NSLog(@"\n");
}

Copy the code

What is the output of this program?

We created an Autoreleased object with [NSString stringWithFormat:@”biubiu Lee 2017_C”]. In order to continue accessing this object in viewWillAppear and viewDidAppear, a global __weak variable is used to point to it. The __weak variable takes advantage of the fact that it does not affect the life cycle of the object it points to.

For stringA

For stringB when an object is created through [NSString stringWithFormat:@”biubiu Lee 2017_C”], the object reference count is 1. When the local variable strB is used to point to the object, the object’s reference count +1 becomes 2. When out of the current scope, the local variable strB becomes nil, so the reference count of the object it points to becomes 1. When out of scope @Autoreleasepool {}, the current Autoreleasepool is drained and the autoreleased object is released. So the reference count for this object becomes zero, and the object is finally released.

The watchpoint can be set by using the watchpoint command of LLDB to observe changes in the value of the global variable stringB, which holds the address of the Autoreleased object we created.

If we see similar output on the console, we have successfully set a watchpoint: stringB (LLDB) Watchpointset variable stringB
Watchpoint created: Watchpoint 1: addr = 0x10255d228 size = 8 state = enabled type = w
    declare @ '/Users/biubiu03/test/biubiuTest/SecViewController.m:12'
    watchpoint spec = 'stringB'
    new value: 0x0000000000000000
(lldb) 
Copy the code

Once you have set up your watch point, click the Continue Program Execution button to Continue running the program

(lldb) watchpoint set variable stringB
Watchpoint created: Watchpoint 1: addr = 0x10255d228 size = 8 state = enabled type = w
    declare @ '/Users/biubiu/1_Code/test/biubiuTest/SecViewController.m:12'
    watchpoint spec = 'stringB'
    new value: 0x0000000000000000

Watchpoint 1 hit:
old value: 0x0000000000000000
new value: 0x00000001c04654c0
2017-10-30 16:45:19.766498+0800 biubiuTest[2606:622193] XPC connection interrupted

Watchpoint 1 hit:
old value: 0x00000001c04654c0
new value: 0x0000000000000000
Copy the code

For stringC when out of scope @autoreleasepool {}, the autoreleased object is released and the reference count of the object becomes 1. When strC goes out of scope, the viewDidLoad method returns, strC points to nil, the reference count of the object it points to becomes zero, and the object is finally released.

AutoreleasePoolPage

Autoreleasepool does not have a separate memory structure and is implemented through a bidirectional linked list with AutoreleasePoolPage as a node. You can find the source code for AutoReleasepool in the nsobject. mm file.

1. The autoreleasepool for each thread is essentially a stack of Pointers; 2. Each pointer represents an object that needs to be released or POOL_SENTINEL (sentinel object, representing an autoreleasepool boundary); 3. A pool token is the memory address of the POOL_SENTINEL corresponding to the pool. When the pool is popped, all objects with a memory address after the pool token are released. 4. The stack is divided into a bidirectional linked list with page nodes. Pages are dynamically added and removed as necessary; 5.Thread-local storage points to the Hot Page where the newly added Autoreleased object is located. To sum up:

4K autorelease pool page per page, bidirectional linked list links between pages, used in stack form, pages lazy creation, each thread has its own autorelease pool, most of the time using pool top (hotPage).Copy the code

1. Magic is used to check whether the structure of AutoreleasePoolPage is complete. 2. Next points to the next location of the newly added Autoreleased object, initialized to begin(); 3. Thread refers to the current thread. 4. Parent points to the parent, and the parent value of the first node is nil; 5. Child refers to a child, and the last child is nil; 6. Depth means depth. It starts at 0 and increases by 1. 7. Hiwat stands for High water Mark. 8. AutoreleasePoolPage is null when next == begin(); When next == end(), AutoreleasePoolPage is full.

RunLoop

RunLoop concept

When the APP starts, the system automatically creates a thread to execute tasks, which is called the master thread or UI thread. When the main thread is created, the system also creates and starts a mechanism for the main thread (essentially an object that is related to the application’s life cycle) called a RunLoop, which is called a 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 that allows threads __ to handle events __ at any time without exiting, the usual code logic would look 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.

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

For easy tracking and viewing, create an Xcode project and drag the source code into it.

RunLoop in relation to threads

There are two thread objects encountered in iOS development: 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_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 a pthread. CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); / / lockif(! 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 loopsDic. CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));if(! Loop) {// If not, create a loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); // Register a callback to destroy the corresponding RunLoop when the thread is destroyed. _CFSetTSD(... , thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); / / unlockreturn loop;
}
Copy the code
// Get MainRunLoop CFRunLoopRefCFRunLoopGetMain() 
{
    return _CFRunLoopGet(pthread_main_thread_np());
}
Copy the code
// Get the current RunLoop CFRunLoopRefCFRunLoopGetCurrent()
 {
    return _CFRunLoopGet(pthread_self());
}
Copy the code

RunLoop External interface

There are five classes for RunLoop in CoreFoundation:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
Copy the code

The CFRunLoopModeRef class is not exposed, but is encapsulated through the interface of CFRunLoopRef. The relationship is as follows:

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
    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 as “Common” properties. 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.

According to Apple’s documentation, the internal logic of RunLoop is as follows:

Internal code implementation of RunLoop

Void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10,false);
}
Copy the code
// Start with the specified Mode, Int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
Copy the code
Int CFRunLoopRunSpecific(RunLoop, modeName, seconds, CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName,false); // If mode does not existsource/timer/observer returns directly.if (__CFRunLoopModeIsEmpty(currentMode))
       return; // 1. Notify Observers: RunLoop is about to enter loop. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); __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); 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 processing 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 to wait for a 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() __CFRunLoopServiceMachPort()waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait forReceive MSG} // 8. Notify Observers: RunLoop threads have just been awakened. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); // The message is received and processed. Handle_msg: // 9.1 If a Timer runs out of time, trigger the Timer's callback.if(msg_is_timer) {__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())} Perform block.else if(msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 If a Source1 (port-based) event is emitted, handle the eventelse {
                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; }else ifRetVal = kCFRunLoopRunTimedOut; retVal = kCFRunLoopRunTimedOut; }else if(__CFRunLoopIsStopped(runloop)) {retVal = kCFRunLoopRunStopped; }else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                // source/timer/ Observer lost none 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

The core of RunLoop is based on Mach Port, and when it goes to sleep it calls mach_msg(). To explain this logic, let’s take a look at the OSX/iOS architecture.

Apple officially divides the entire system into four levels:

1. The application layer includes graphics apps that users can access, such as Spotlight, Aqua, SpringBoard, etc.

2. The application framework layer is the Cocoa and other frameworks that developers are exposed to.

3. Core framework layer includes various core frameworks, OpenGL and other contents.

4.Darwin is the core of operating system, including system kernel, driver, Shell and other contents. This layer is opensource, and all its source code can be found in apple opensource.

Look at the core architecture of Darwin

Above the hardware layer are three components: Mach, BSD, and IOKit (along with a few others not noted above), which 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, Mach cannot call objects directly, but can only communicate with them through messaging. Messages are the most basic concept in Mach. Messages are transmitted between two ports. This is the core of Mach IPC (interprocess communication).

Mach’s message definition is in a header file and is 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 really just a binary packet (BLOB) whose header defines the current port local_port and the destination port remote_port,

Sending and receiving messages are done through the same API, 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

As you can see, there are five modes registered by default:

  1. KCFRunLoopDefaultMode: The default Mode of the App, in which the main thread is normally run.
  2. UITrackingRunLoopMode: interface tracing Mode, used by ScrollView to track touch sliding, ensuring that the interface sliding is not affected by other modes.
  3. 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.
  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 in CFRunLoop, 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 towaitmsg. 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); __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __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 for this function 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 uses RunLoop internally.

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

In fact RunLoop also uses GCD stuff, for example RunLoop is a Timer implemented with dispatch_source_t. 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 callback CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(). But this logic is limited to dispatches to the main thread, dispatches to other threads are still handled by libDispatch.