An overview of the

RunLoop is a very fundamental concept in iOS and OSX development and is behind many common technologies. Although most developers rarely use RunLoop directly, understanding RunLoop can help developers make better use of the multithreaded programming model, and it can also help developers answer some of the puzzles in daily development. This article will start with RunLoop source code, combined with the practical application of RunLoop to gradually unravel its mystery

RunLoop and RunloopRef

The RunLoop commonly referred to is NSRunloop or CFRunloopRef. CFRunloopRef is a pure C function, while NSRunloop is only the OC encapsulation of CFRunloopRef and does not provide additional functions. Therefore, the CFRunloop source code is easy to find because apple has opened the CoreFoundation source code. __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() : __CFRunloopRun() :

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { CHECK_FOR_FORK(); // If CFRunLoopRef is marked free, return if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; //CFRunLoopRef thread safety lock __CFRunLoopLock(rl); CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); int32_t result = kCFRunLoopRunFinished; // If the Observer of RunLoop listens on kCFRunLoopEntry, Runloop if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rL, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // If the observer of RunLoop listens for kCFRunLoopExit, Runloop if (currentMode->_observerMask & kCFRunLoopExit) __CFRunLoopDoObservers(rL, currentMode, kCFRunLoopExit); // Release thread-safe lock __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); return result; } static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {do{// Notify that timer and source if (RLM ->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // Handle the non-delayed main thread call __CFRunLoopDoBlocks(rl, RLM); Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, RLM, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } /// If a Source1 (port-based) is ready, process the Source1 directly and jump to the message. if (MACH_PORT_NULL ! = dispatchPort && ! didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer; if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) { goto handle_msg; }} /// Notify Observers: threads of RunLoop are about to enter sleep. if (rlm->_observerMask & kCFRunLoopBeforeWaiting) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(MSg_buffer), &livePort, poll? 0 : TIMEOUT_INFINITY); // Wait for... // Wake up from waiting __CFRunLoopUnsetSleeping(rl); if (rlm->_observerMask & kCFRunLoopAfterWaiting) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); If (RLM ->_timerPort! = MACH_PORT_NULL && livePort == rlm->_timerPort){ CFRUNLOOP_WAKEUP_FOR_TIMER(); if (! __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { // Re-arm the next timer, because we apparently fired early __CFArmNextTimerInMode(rlm, rl); }}else if (livePort == dispatchPort){dispatch_async CFRUNLOOP_WAKEUP_FOR_DISPATCH(); __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL); }else{ CFRUNLOOP_WAKEUP_FOR_SOURCE(); __CFRunLoopDoSource1(); // Handle Source1} // Again make sure that there is no synchronous method that needs to call __CFRunLoopDoBlocks(rl, RLM); } while (! stop && ! timeout); }Copy the code

__CFRunLoopRun is actually a do while loop inside, which is the essence of Runloop execution. After executing this function, we are in a wait-and-process loop until the end of the loop. Unlike the loop we wrote ourselves, it uses almost no system resources when it sleeps, because of course the kernel is responsible for implementing it, which is the essence of Runloop

The following figure describes The Runloop running process (basically describes The core process of Runloop above, of course you can check The official description of The Run Loop Sequence of Events:

The whole process is not complicated (note that source0 is not included in the message processing in the yellow area because it is processed at the beginning of the Loop), and the whole process is essentially an implementation of an Event Loop that exists on other platforms but is called a Runloop. But since RunLoop is a message loop, who manages and runs RunLoop? So what kind of messages does it receive? What is the dormancy process like? How do I ensure that the hibernation does not occupy system resources? How do you process these messages and when do you exit the loop? There are still a number of questions to unravel

Note that although CFRunLoopPerformBlock is represented as a wake up mechanism in the figure above, CFRunLoopPerformBlock is actually executed only as a queue until the next RunLoop runs, and CFRunLoopWakeUp must be called if it needs to be executed immediately

Runloop Mode

It is easy to see from the source that Runloop always runs under some particular CFRunLoopModeRef (Mode must be specified every time __CFRunLoopRun() is run). From the definition of the corresponding structure of CFRunloopRef, it is easy to know that each Runloop can contain several modes, and each Mode contains Source/Timer/Observer. A Mode must be specified each time the main function of Runloop __CFRunLoopRun() is called. This Mode is called _currentMode. Then re-enter the Runloop to ensure that the Source/Timer/Observer of different modes do not interfere with each other

struct __CFRunLoop {
   CFRuntimeBase _base;
   pthread_mutex_t _lock;            /* locked for accessing mode list */
   __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp
   Boolean _unused;
   volatile _per_run_data *_perRunData;              // reset for runs of the run loop
   pthread_t _pthread;
   uint32_t _winthread;
   CFMutableSetRef _commonModes;
   CFMutableSetRef _commonModeItems;
   CFRunLoopModeRef _currentMode;
   CFMutableSetRef _modes;
   struct _block_item *_blocks_head;
   struct _block_item *_blocks_tail;
   CFTypeRef _counterpart;
};
	

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};
Copy the code

The default RunLoop Modes provided by the system are kCFRunLoopDefaultMode(NSDefaultRunLoopMode) and UITrackingRunLoopMode. To switch to the corresponding Mode, you only need to pass in the corresponding name. The former is the default Runloop Mode of the system. For example, when you enter the iOS program, you are in this Mode by default without doing anything. If you swipe UIScrollView, the main thread will switch Runloop to UITrackingRunLoopMode. No more event operations will be accepted (unless you set the other Source/Timer to UITrackingRunLoopMode).

However, there is another Mode commonly used by developers, kCFRunLoopCommonModes (NSRunLoopCommonModes). This is not a specific Mode, but a combination of modes. In iOS, NSDefaultRunLoopMode and UITrackingRunLoopMode are included by default. This does not mean that Runloop will run in kCFRunLoopCommonModes, but rather that it will register NSDefaultRunLoopMode and UITrackingRunLoopMode respectively. You can also add custom modes to the kCFRunLoopCommonModes group by calling CFRunLoopAddCommonMode()

CFRunLoopRef and CFRunloopMode, CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef relationship is as follows:

So what are CFRunLoopSourceRef, CFRunLoopTimerRef, and CFRunLoopObserverRef? What role do they play in the Runloop running process?

Source

First, take a look at the official Runloop structure diagram (note that the Input Source Port in the diagram below does not correspond to Source0 in the previous diagram, but to Source1. Source1 and Timer are both Port event sources. The difference is that all timers share the same Port “Mode Timer Port”, and each Source1 has a different Port) :

In combination with the previous RunLoop core running process, we can see that Source0(responsible for intra-app events, and the App is responsible for managing the trigger, such as the UITouch event) and Timer (also known as Timer Source, time-based trigger, NSTimer) are two different Runloop event sources (of course, Source0 is a class of the Input Source, which also includes a Custom Input Source that is issued manually by other threads). When the RunLoop is woken up by these events, it processes and calls the event handling methods (CFRunLoopTimerRef’s callback pointer and CFRunLoopSourceRef both contain the corresponding callback pointer).

However, there is another version of CFRunLoopSourceRef besides Source0 that is Source1, which contains a Mach port in addition to the callback pointer. Unlike Source0, which requires manual firing, Source1 can listen for system ports and other threads to send messages to each other, and it can actively wake up runloops (managed by the operating system kernel, such as CFMessagePort messages). It is also stated that the Source can be customized, so it is more like a protocol for CFRunLoopSourceRef, and the framework already defines two implementations by default

Observer

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;        /* immutable */
    CFIndex _order;            /* immutable */
    CFRunLoopObserverCallBack _callout;    /* immutable */
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};
Copy the code

The CFRunloopObserverRef is relatively uncomplicated to understand, acting as a listener in the message loop that notifies the outside world of the current RunLoop’s running status (it contains a function pointer, callout, to inform the observer of the current status). The specific Observer status is as follows:

/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // Enter RunLoop kCFRunLoopBeforeTimers = (1UL << 1), // About to start Timer processing kCFRunLoopBeforeSources = (1UL << 2), KCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), // Wake up kCFRunLoopExit = (1UL << 7), // Exit RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU};Copy the code

Call out

In development, almost all operations are called back through Call out (whether Observer status notification or Timer or Source handling), and the system usually uses the following functions to Call back (in other words, your code is ultimately called through the following functions: Even if you listen to the Observer yourself, the following functions will be called first and then notified indirectly, so you will often see these functions in the call stack) :

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
Copy the code

For example, in the controller’s touchBegin break point to view the stack (since UIEvent is Source0, So you can see a Source0 callout function called CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION) :

RunLoop dormancy

In fact, for Event Loop, the most core thing of RunLoop is to ensure that threads sleep when there is no message to avoid occupying system resources, and wake up when there is a message. This mechanism of RunLoop relies entirely on the system Kernel, specifically Mach in Darwin. Darwin can be found in the lower level of the Kernel below:

Mach is at the heart of Darwin, arguably the core of the kernel, providing basic services such as interprocess communication (IPC) and processor scheduling. In Mach, communication between processes and threads is done in the form of messages between two ports (this is why Source1 is called a Port-based Source, because it is triggered by the system sending a message to a specified Port). Messages are sent and received using the mach_msg() function in < Mach /message.h> (in fact, Apple provides very few Mach apis and does not encourage us to call them directly):

__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern 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

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. When the program is stationary, RunLoop stays at __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(MSG_buffer), &livePort, poll? 0: TIMEOUT_INFINITY, &voucherState, &voucherCopy), and inside this function mach_msg is called to put the program to sleep

Runloop and thread relationship

Runloop is managed based on pThreads, the c-based underlying API for cross-platform multithreading. It is a high-level encapsulation of Mach threads and corresponds to NSThreads (which are an object-oriented API, so we rarely use pThreads directly in iOS development).

The interface developed by Apple does not directly create a Runloop interface. If you need to use the Runloop methods CFRunLoopGetMain() and CFRunLoopGetCurrent(), you can also see the above source code. The core logic is in _CFRunLoopGet_), and it is not hard to see from the code that the thread’s Runloop is created for the first time and stored in the global Dictionary (thread and Runloop are one to one) only if we use thread’s method to actively get Runloop. By default, threads do not create runloops (the main thread Runloop is special, and any thread is created to ensure that the main thread already has a Runloop), and the corresponding Runloop is destroyed when the thread terminates

In iOS development, NSRunloop is more commonly used by developers, which provides three common run methods by default:

- (void)run; 

- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

- (void)runUntilDate:(NSDate *)limitDate;
Copy the code
  • The run method corresponding to CFRunLoopRun in the CFRunloopRef above does not exit unless CFRunLoopStop() is called; This method is usually used if you want to never exit the RunLoop, otherwise you can use runUntilDate
  • RunMode :beforeDate: corresponds to the CFRunLoopRunInMode(mode,limiteDate,true) method, which is executed only once and exited after execution. Usually used to manually control runloops (for example in a while loop)
  • RunUntilDate: method is actually CFRunLoopRunInMode (kCFRunLoopDefaultMode limiteDate, false) performed does not exit, continue to the next RunLoop until the timeout

RunLoop application

NSTimer

The Timer Source has been mentioned as the event Source. In fact, its upper counterpart is NSTimer (actually CFRunloopTimerRef), which is often used by developers (the underlying implementation is based on mk_timer). Even a lot of developers get their start with RunLoop from NSTimer. In fact, NSTimer timer is triggered based on RunLoop, so before using NSTimer, you must register with RunLoop. However, RunLoop does not call timer at exact time to save resources. If a task takes a long time to execute, (NSTimer provides a tolerance property to set tolerance, which can be set if you really want to use NSTimer and want to be as accurate as possible)

NSTimer is usually created in two ways, although both are class methods, one is timerWithXXX and the other scheduedTimerWithXXX

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
Copy the code

The biggest difference between the two is that in addition to creating a timer, NSDefaultRunLoopModeMode is automatically added to the current thread RunLoop, and NSTimer will not work without being added to the RunLoop. For example, the following code will not work if timer2 is not added to the RunLoop. Note that scrolling UIScrollView (UITableView, UICollectionview are similar) will not work, but NSDefaultRunLoopMode will work if you change it to NSRunLoopCommonMode. This also explains the Mode content introduced earlier

#import "ViewController1.h"@interface ViewController1 () @property (nonatomic,weak) NSTimer *timer1; @property (nonatomic,weak) NSTimer *timer2; @end @implementation ViewController1 - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blueColor]; // Timer1 is automatically added to the current RunLoop with the default NSDefaultRunLoopMode. So I can work a normal self. Timer1 = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @ the selector (timeInterval:) userInfo:nil repeats:YES]; NSTimer * tempTimer = [NSTimer timerWithTimeInterval: 1.0 target: self selector: @ the selector (timeInterval:) the userInfo: nil repeats:YES]; // Timer2 will not work properly without adding timer2 to the RunLoop (note that if you want timer2 to work properly while scrolling UIScrollView, change NSDefaultRunLoopMode to NSRunLoopCommonModes) [[NSRunLoop currentRunLoop] addTimer:tempTimerforMode:NSDefaultRunLoopMode];
    self.timer2 = tempTimer;
        
    CGRect rect = [UIScreen mainScreen].bounds;
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
    [self.view addSubview:scrollView];
        
    UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
    contentView.backgroundColor = [UIColor redColor];
    [scrollView addSubview:contentView];
    scrollView.contentSize = contentView.frame.size;
}
    
- (void)timeInterval:(NSTimer *)timer {
    if (self.timer1 == timer) {
        NSLog(@"timer1...");
    } else {
        NSLog(@"timer2...");
    }
}
    
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:true completion:nil];
}
    
- (void)dealloc {
    NSLog(@"ViewController1 dealloc...");
}
@end
Copy the code

Note that UIViewController1 does not have a strong reference to Timer1 and Timer2 in the above code. For ordinary objects, After the viewDidLoad method is executed, both should be released, but they are not. To ensure that the timer works properly, the system will perform a retain operation on the NSTimer after it is added to the RunLoop (note: Timer2 is not directly assigned to timer2 because timer2 is weak. If it is assigned to timer2 directly, it will be released immediately because the NSTimer created by timerWithXXX method does not add RunLoop by default. Reference to Timer2 can only be made after a RunLoop is added.

But even with weak references, ViewController1 in the code above can’t be freed properly because the target is self when NSTimer2 is created, which results in timer1 and Timer2 having a strong reference to ViewController1. There are usually two ways to solve this problem: one is to separate the target into an object (create NSTimer in this object and use the object itself as the target of NSTimer), and the controller uses NSTimer indirectly through this object; Another way to think about it is again to shift the target, but you can just add the NSTimer extension, make the NSTimer itself the target, and you can wrap the selector in a block. The latter is relatively elegant and is currently the most commonly used solution (there are a number of similar packages, such as NSTimer+Block). Apple is clearly aware of this, and you can use iOS 10’s new system-level block scheme if you can ensure that your code only runs on iOS 10 (it’s already posted in the code above).

Of course, using the second method above can solve the problem that the controller cannot be released, but you will find that both timers are still running even when the controller is released. To solve this problem, you need to call the invalidate method of NSTimer (note: Either a repeated timer or a one-time timer will be invalidated if it calls the invalidate method, but a one-time timer will automatically call the Invalidate method when it completes its operation.

The modified code is as follows:

#import "ViewController1.h"@interface ViewController1 () @property (nonatomic,weak) NSTimer *timer1; @property (nonatomic,weak) NSTimer *timer2; @end @implementation ViewController1 - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor blueColor]; Self. Timer1 = [NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {NSLog (@"timer1..."); }]; NSTimer * tempTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {NSLog (@"timer2...");
    }];
    [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
    self.timer2 = tempTimer;
    
    CGRect rect = [UIScreen mainScreen].bounds;
    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
    [self.view addSubview:scrollView];
    
    UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
    contentView.backgroundColor = [UIColor redColor];
    [scrollView addSubview:contentView];
    scrollView.contentSize = contentView.frame.size;
}
    
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:true completion:nil];
}
    
- (void)dealloc {
    [self.timer1 invalidate];
    [self.timer2 invalidate];
    NSLog(@"ViewController1 dealloc...");
}
@end
    
Copy the code

Actually, another problem with timers that people often encounter is that NSTimer is not a real-time mechanism, The official documentation makes it clear that NSTimer may have errors in a loop if the RunLoop is not recognized (within 50-100ms) or if the current RunLoop is performing a long call out (for example, performing a loop operation). RunLoop continues to check in the next loop and determines whether to execute as the case may be. (NSTimer execution times are always fixed at certain intervals, such as 1:00:00, 1:00:01, 1:00:02, and 1:00:05 skipping the fourth and fifth runs of the loop.)

To illustrate this problem, see the following example (note: Some examples might have a timer start in one thread and a time-consuming task start in the main thread to demonstrate this problem. If tested, this problem may not be obvious, because iphones are now multi-core computing, which makes this problem more complicated. So the following example chooses to add timers and execute time-consuming tasks in the same RunLoop.)

#import "ViewController.h"@interface ViewController () @property (nonatomic,weak) NSTimer *timer1; @property (nonatomic,strong) NSThread *thread1; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor redColor]; // Because the following method does not get a reference to NSThread, //[NSThread detachNewThreadSelector:@selector(performTask) toTarget:self withObject:nil]; self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil]; [self.thread1 start]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self.thread1 cancel]; [self dismissViewControllerAnimated:YES completion:nil]; } - (void)dealloc { [self.timer1 invalidate]; NSLog(@"ViewController dealloc."); } - (void)performTask {// Create a timer that is automatically added to the current thread's RunLoop, but not to the main thread's RunLoop by default. You must manually call __weak Typeof (self) weakSelf = self; Self. Timer1 = [NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {if ([NSThread currentThread].isCancelled) {
            //[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(caculate) object:nil];
            //[NSThread exit];
            [weakSelf.timer1 invalidate];
        }
        NSLog(@"timer1...");
    }];
    
    NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]); / / to distinguish direct calls and "performSelector: withObject: afterDelay:" the difference, the following direct call whether RunLoop can perform the same operation, but the latter is not. //[self caculate]; [the self performSelector: @ the selector (caculate) withObject: nil afterDelay: 2.0]; // Unregister selector in the current RunLoop (note: Just the current RunLoop, So can only cancel) / / in the RunLoop [NSObject cancelPreviousPerformRequestsWithTarget: self selector: @ the selector (caculate) object: nil]; NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]); [[NSRunLoop currentRunLoop] run]; NSLog(@"Note: this code does not execute if the RunLoop does not exit (running). The RunLoop itself is a loop.");
    
    
}
    
- (void)caculate {
    for(int i = 0; i < 9999; ++i) { NSLog(@"%i,%@",i,[NSThread currentThread]);
        if ([NSThread currentThread].isCancelled) {
            return;
        }
    }
}
    
@end
Copy the code

If you run and do not exit the above program, it will be found that the NSTimer can be normally executed in the first two seconds, but after two seconds, the timer skips the chance of intermediate execution until the caculator loop is completed due to the execution of the loop operation in the same RunLoop. This also shows that the NSTimer is not a real-time system mechanism.

But there are a few points to note about the above procedure:

  1. NSTimer strongly references Target until the task ends or after exit. If the above program does not perform a thread cancel to terminate the task then the controller will be shut down and cannot be released properly
  2. Non-mainline runloops do not run automatically (note also that non-mainline runloops are not created automatically by default until they are first used). Runloops must run after NSTimer or Source0, Sourc1, Observer input is added or exit. For example, if run is placed before NSTimer is created, it will neither execute a scheduled task nor a loop
  3. performSelector:withObject:afterDelay: The essence of the execution is to create an NSTimer and add it to the current thread’s RunLoop. Similarly performSelector: onThread: withObject: afterDelay:, it will just be in another thread RunLoop create a Timer), so this method in fact can form to trigger the object before the task execution of the reference, Release when the task is done (for example, a reference is made to ViewController, note that performSelector withObject is the same thing as a direct call, which works differently)
  4. The code above also fully demonstrates the fact that RunLoop is a loop and that code after the run method does not execute immediately until RunLoop exits
  5. In case of sudden dismiss during the operation of the above program, the actual execution process of the program should be considered in two cases: If caculate hasn’t started it will stop Timer1 in Timer1 (stop the first task in the thread), wait for Cacer1 to execute and break (stop the second task in the thread) and release the reference to the controller. If caculate is in the middle of executing, the caculate task is finished and wait for timer1 to run in the next period (because the RunLoop of the current thread did not exit). When the timer1 reference counter is not zero), the invalidate method is executed (the second task also ends), and the thread releases the reference to the controller

CADisplayLink is a timer that executes at the same FPS rate as a screen refresh (preferredFrame persecond can be modified to change the refresh rate), and it also needs to be added to the RunLoop to execute. Like NSTimer, CADisplayLink is implemented based on CFRunloopTimerRef and uses mk_timer at the bottom. It is more accurate than NSTimer (although NSTimer can also modify the accuracy), but like NSTimer it still loses frames for large tasks. In general, CADisaplayLink is used to build frame animations and looks relatively smooth, while NSTimer is more widely used.

AutoreleasePool

AutoreleasePool is another runloop-related topic that has been discussed a lot. AutoreleasePool is not directly related to RunLoop. The main reason for this discussion is that two observers are registered to manage and maintain AutoreleasePool after the iOS application is launched. Might as well in the application has just started to print currentRunLoop can see the system default registered a lot of the Observer, there are two of the Observer is the callout _wrapRunLoopWithAutoreleasePoolHandler, These are the two listeners associated with auto-release pools.

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

Copy the code

The first Observer listens for the RunLoop to enter by calling objc_autoreleasePoolPush() to add a sentinel object flag to the current AutoreleasePoolPage to create the automatic release pool. The order of this Observer is -2147483647, the highest priority, ensuring that it occurs before any callback operation.

The second Observer listens for the RunLoop when it is asleep and when it is about to exit. When going to sleep, objc_autoreleasePoolPop() and objc_autoreleasePoolPush() are called to clean up from the newly added object until the sentry object is encountered. When exiting RunLoop, objc_autoreleasePoolPop() is called torelease objects in the pool automatically. The order of this Observer is 2147483647, which has the lowest priority and is guaranteed to occur after all callbacks.

Other operations on the main thread are usually kept within the AutoreleasePool (in the main function) to minimize memory maintenance operations (although you can create AutoreleasePool yourself if you want to explicitly free (e.g. loop) otherwise you don’t need to).

In fact, after the application started, the system also registered other observers and multiple source1s (for example, Source1 whose context is CFMachPort is used to receive hardware event responses and distribute them to the application all the way to the UIEvent), which will not be detailed here.

The UI update

If the print start App after the main thread RunLoop can find another callout for ___ZN2CA11Transaction17observer_callbackEP19_CFRunLoopObservermPv Observer, The monitor for the UI changes after the update, such as changed the frame, adjusted the UI level (UIView/CALayer) or manually after the setNeedsDisplay/setNeedsLayout these operations will be submitted to the global container. This Observer listens for the main thread RunLoop to go to sleep and exit, and once in both states it iterates through all UI updates and commits them for the actual drawing update. This is usually perfect because, in addition to system updates, you can also manually trigger the update for the next RunLoop run using methods like setNeedsDisplay. However, if you’re doing a lot of logical operations, the UI update might get stuck, so the asynchronous drawing framework Texture was created to solve this problem. Texture is basically a Texture that puts the UI layout and drawing in the background as far as possible, the final UI update in the main thread (which must also be done in the main thread), and a set of properties like UIView or CALayer to keep the developer as comfortable as possible. The Texture adds an Observer to the main thread RunLoop to listen for both impending sleep and exit RunLoop states, and to iterate through the queue of pending tasks when a callback is received

NSURLConnection

Once started, the NSURLConnection calls the Delegate method to receive data, and such a continuous action is run based on the RunLoop.

Once the NSURLConnection to set the delegate would immediately to create a thread com. Apple. NSURLConnectionLoader, start at the same time internal RunLoop and add four Source0 NSDefaultMode mode. CFHTTPCookieStorage is used to process cookies. The CFMultiplexerSource is responsible for the various Delegate callbacks and wakes up the RunLoop inside the delegate (usually the main thread) in the callback to perform the actual operation.

Earlier versions of the AFNetworking library were also implemented based on NSURLConnection. In order to receive a delegate callback in the background, AFNetworking created an empty thread inside and started RunLoop. AFNetworking uses performSelector: onThread to place the task in the RunLoop of the background thread when it needs to execute a task using the background thread

GCD and RunLoop

GCD is used in the RunLoop source code, but RunLoop itself is not directly related to GCD. When dispatch_async(dispatch_get_main_queue(), <^(void)block>) is called libDispatch sends a message to the main RunLoop to wake up the RunLoop. The RunLoop gets the block from the message. And execute this block in the __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ callback. However, this operation is limited to the main thread, the other thread dispatch operations are all libDispatch driven.

More RunLoop use

RunLoop has been used in many applications and some well-known third-party libraries, so can we use RunLoop properly to help us do something in the actual development process?

A RunLoop contains multiple modes, and its Mode is customizable. It can be used by Source1, Timer, and Observer developers. However, in general, the Timer is not customized, let alone a complete Mode is not customized. In fact, the switch between Observer and Mode is used more.

For example, many of you are familiar with using perfromSelector to set an image in default mode, [[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] selector :@selector(setImage: withObject:myImage afterDelay:0.0 inModes: @nsdeFaultrunloopMode]

Sunnyxx’s UITableView+FDTemplateLayoutCell uses the Observer to calculate the height of the UITableViewCell in the idle state of the interface and cache it. PerformanceMonitor monitors iOS real-time lag, also using the Observer to monitor RunLoop.