Use scenarios of Runloop

Most of these are references from various technical blogs, and some scenarios add some practical code.

Executing a Block Task

Using the CFRunLoopPerformBlock function, you can specify that block tasks are executed in runloopMode. But it’s not usually used that way.

autoreleasePool

Autoreleasepool is a data structure built from a two-way linked list of stacks as nodes. AutoreleasePool’s relationship to Runloop is a frequently discussed point, but there is no autoreleasePool in the Runloop source code. In the runloop of the main thread, iOS has registered two observers to listen to the Entry and BeforeWaiting states of the Runloop respectively and perform push and pop operations of the autoreleasePool accordingly.

  1. @autoreleasepool {} is equivalent to void * CTX = objc_autoreleasePoolPush();
  2. The code in {} adds an objc_autoreleasePoolPop(CTX) at the end;

The callback event for Entry state is _obj_autoreleasePoolPush() function, creating a new Autoreleasepool. Push operation.

BeforeWaiting State callback events call the _objc_autoreleasePoolPop() function torelease the old Autoreleasepool, Then create a new Autoreleasepool by calling the _obj_autoreleasePoolPush() function. Do pop first and then push.

Print the main thread of the runloop object, you can see below two Observer, the callout function are _wrapRunLoopWithAutoreleasePoolHandler.

observers = (
    "<CFRunLoopObserver 0x600000b00320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x60000345df80 [0x7fff80617cb0]>{type = mutable-small, count = 0, values = ()}}"."<CFRunLoopObserver 0x600000b003c0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x60000345df80 [0x7fff80617cb0]>{type = mutable-small, count = 0, values = ()}}"
),
Copy the code

The first Observer listens to the Entry state and creates a new Autoreleasepool by calling the _obj_autoreleasePoolPush() function. Order = -2147483647 indicates that it has the highest priority, ensuring that its callback occurs before all other callback operations.

The second Observer listens for the BeforeWaiting state and the Exit state. Call the _objc_autoreleasePoolPop() function to free the old Autoreleasepool, All autoRelease objects are released and a new autoreleasepool is created by calling the _obj_autoreleasePoolPush() function. If the status is Exit, the POP operation is called directly. The order = 2147483647 of this Observer indicates the lowest priority, ensuring that it occurs after all callback operations.

Note that AutoReleasepool can be nested, so its push and pop operations will use some tags, called sentinel objects.

Objects added to autoreleasepool do not continue to decrease by 1 when their retainCount is 1, but are marked for release. The release timing is done by listening for the state of the Runloop. Objects that are not added to the AutoReleasepool are released independently of Runloop and only by following ARC.

Incident response

IOS registered a Source1 (based on the Mach port) to the receiving system, the callback is __IOHIDEventSystemClientQueueCallback (). When an event occurs, the IOKit framework generates an IOHIDEvent event, which is received by SpringBoard (a process designed to handle event responses) and forwarded to the corresponding App using A Mach MSG. The Source1 that is then registered triggers a callback. Call _UIApplicationHandleEventQueue () for App internal transfer process.

One of the key points I made before is:

When processing tasks, Source1 usually works with Source0, that is, distributing some tasks to Source0 for execution. For example, the UITouch event is initially handled by Source1 for the task of clicking the screen to capture the event, and then Source1 distributes the event wrapper to Source0 for processing. This is crucial.

Here, _UIApplicationHandleEventQueue () function will IOHIDEvent packaged into UIEvent, distribute to Source0 processing, so we usually see UITouch events, including the UIButton click, touch events, gesture, etc., Often the only thing you see through the function call stack is the callout function of Source0. In fact, the event response is done by Source1 and Source0 together.

Have talked about before an iOS internal use runloopMode: GSEventReceiveRunLoopMode. GSEvent encapsulate all of the system’s events and passes them to the App, such as volume buttons, screen clicks, and so on. UIEvent is just an encapsulation of GSEvent.

UIGesture

The main thread will register an observer, listening BeforeWaiting events, the callback is _UIGestureRecognizerUpdateObserver *.

"<CFRunLoopObserver 0x600000b08000 [0x7fff80617cb0]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x7fff47c2f06a), context = <CFRunLoopObserver context 0x600001110700>}".Copy the code

When _UIApplicationHandleEventQueue () to recognize a gesture, to touch a series of events before the callback method (such as touchesMove) is terminated, then the UIGestureRecognizer flagged for processing. _UIGestureRecognizerUpdateObserver () function of the internal obtain all just marked as pending UIGestureRecognizer, perform their corresponding callback methods. This callback is also executed when the state of the UIGestureRecognizer changes.

Of course, the gesture must also be done by Source1 and Source0.

GCD

The timeout of a runloop is controlled by the GCD Timer. GCD starts the child thread, and actually uses runloop internally. GCD returns from the child thread to the main thread, triggering the Source1 event of Runloop.

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), Dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{/// GCD throw a delay operation to the child thread. NSLog(@"global after %@", [NSThread currentThread]); });Copy the code

When dispatch_async(main_queue, block), libDispatch sends a message to the runloop on the main thread to wake up the runloop. When the Runloop is woken up, the runloop will get the block from the message. Execute the block task in the callout function CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE. Only the main thread, the child thread dispatch block operation is all done by libDispatch.

runloopMode

dispatch_async(dispatch_get_main_queue(), …) CFRunLoopPerformBlock allows you to specify runloopMode to execute a block.

Whether to wake up runloop

dispatch_async(dispatch_get_main_queue(), …) CFRunLoopPerformBlock does not actively wake up the runloop. If runloop is hibernated, the block of CFRunLoopPerformBlock cannot be executed. You can wake up a Runloop using CFRunLoopWakeUp.

The main queue of GCD is a string queue

The main queue of GCD is a string queue, so the result is dispatch_async(dispatch_get_main_queue(),…) The passed blocks are executed as a whole on the next iteration of the Runloop.

Look at the following code, *** output 1,3,2 *** is the most familiar code, why the second half of the *** output 4,5,6 ***? And the 1s interval is different, which is 1… 32 and 45… 6. Here… Represents interval.

- (void)testGCDMainQueue { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"main queue task 1"); dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"main queue task 2"); }); [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; NSLog(@"main queue task 3"); }); // output 1,3,2 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{NSLog(@"main queue task 4"); CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{ NSLog(@"main queue task 5"); }); [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; NSLog(@"main queue task 6"); }); // output 4,5,6}Copy the code

Dispatch_async experiment of code in the [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 1]]. The runloop of this cycle continues to run for another 1s. But since main queue is a string queue, printing 1, runloop for another 1s, and printing 3 are a task. Print 2 is obviously a different task, so print 1,3,2.

CFRunLoopPerformBlock is a code segment of the experiment that drops a block into a runloop for execution. If the runloop is running, the block will be scheduled for execution. The [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 1]]. The runloop of this cycle continues to run for another 1s, independent of the block execution itself in the runloop, so 5 will print out normally. Of course, if there is no * * * [[NSRunLoop currentRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 1]]. ***, printing 4,6,5 is natural and consistent with the GCD.

CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{ NSLog(@"main queue task 4"); CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{ NSLog(@"main queue task 5"); }); NSLog(@"main queue task 6"); }); /// output 4,6,5Copy the code

CFRunLoopPerformBlock has a similar effect to performSelector:inModes

If you need to specify that blocks execute only in DefaultMode, you usually use performSelector:inModes:. You can also use the CFRunLoopPerformBlock function.

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, ^{/// Will execute only in DefaultMode. // Scrolling does not print logs if GCD after throws a block in the main queue. When stopped, it will not print until DefaultMode is switched. NSLog(@"CFRunLoopPerformBlock kCFRunLoopDefaultMode scrolling stopped"); }); });Copy the code

NSTimer

NSTimer is based on Runloop and can be freely bridged with CFRunLoopTimerRef. After the Timer is registered with the Runloop, the Runloop registers the callback event for its next execution point in time, the __CFArmNextTimerInMode operation. If the timer’s time is up, but runloop is executing a long call out function, etc., the timer will time out. If Tolerance is set to Tolerance, the timer can execute within the Tolerance time. If Tolerance is exceeded, the timer times out.

The accuracy of NSTimer is greatly affected by the running state of runloop, and the use of NSTimer in a child thread must ensure that the child thread is resident, that is, the runloop is always present. A common circular reference problem with NSTimer is that the runloop will hold the timer, and the timer will hold the target, so improper use may cause the target not to be released correctly.

If there are requirements scenarios where the accuracy of the timer is critical, or if the child thread does not have runloop, then the GCD timer is usually available. GCD timers, on the other hand, are kernel dependent, not RunLoop dependent, and are generally more punctual. Also, the GCD Timer can run in a background thread because GCD has its own internal use of runloop.

The accuracy of the GCD Timer and the NSTimer is not much different when used in a thread with a surviving runloop. Both use a Mach MSG and a Mach port to wake up the Runloop to trigger the timer callback. If the current Runloop is blocked, the delay will occur. For details, see iOS RunLoop.

For more details on Timers, including the circular reference solution, see the blog: Comparing the three timers in iOS. CFRunLoopTimerRef is the fourth type of timer.

CADisplayLink

CADisplayLink’s callback function is triggered at the same rate as the screen refresh, which is more accurate than NSTimer, but it also needs to be added to runloop to execute. Its principle is based on CFRunloopTimerRef to achieve, the underlying use of MK_timer. CADisplayLink’s accuracy is also affected if the Runloop is performing a heavy task. Its usage scenarios are less common than NSTimer.

Background resident thread

The child threads created by the developer do not have Runloop enabled by default. One common requirement is child thread preservation, which uses the Runloop trick of keeping the Runloop running. To enable runloop in a child thread, use autoReleasepool.

If you spawn a secondary thread. You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See [Autorelease Pool Blocks and Threads]).

The prerequisite for a thread’s runloop to run is that there must be a Mode Item, which is either Source, Timer, or Observer.

/// Use NSTimer - (void)addBackgroundRunLoop1 {self_.thread1 = [[NSThread alloc] initWithBlock:^{NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"%@", Self.thread1.name); self.thread1.name); self.thread1.name); {dispatch_async(dispatch_get_main_queue(), ^{/// NSLog(@"main is OK.");});}]; Need to add a source1. Usually the Timer or an empty port. [[NSRunLoop currentRunLoop] addTimer: Timer forMode: NSRunLoopCommonModes], [[NSRunLoop CurrentRunLoop] run]; // [[NSRunLoop currentRunLoop] runUntilDate:[NSDate DateWithTimeIntervalSinceNow: 0.1 f]]; / / the following code is never executed. NSLog (@ "thread runloop");}]; self.thread1.name = @"addBackgroundRunLoop1"; [self.thread1 start]; } // use NSPort - (void)addBackgroundRunLoop2 {self.thread2 = [[NSThread alloc] initWithBlock:^{// Thread alive, Add a source1. If there is no special need, it can be a Timer or an empty port. This thread to survive. And corresponding to remove by removePort addPort [[NSRunLoop currentRunLoop] addPort: [NSPort port] forMode: NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; }]; self.thread2.name = @"addBackgroundRunLoop2"; [self.thread2 start]; NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { [self performSelector:@selector(onTimerInBackgroundRunLoop2) onThread:self.thread2 withObject:nil waitUntilDone:NO]; }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)onTimerInBackgroundRunLoop2 { NSLog(@"%@", self.thread2.name); }Copy the code

AFNetworking 2.x resident thread

In AFNetworking 2.x, this approach is used to keep the child threads alive. AFNetworking 2.x wants to receive the delegate callback in the background thread, so it needs the background thread to persist. We use NSRunLoop’s run method. You must first create a runloopMode item (source or Timer) and add it to the runloop, in this case using NSMachPort. This is just for the background thread to be resident and there is no actual Mach MSG messaging, so empty NSMachPort will do.

/ * AFN 2 x resident USES the child thread, in AFURLConnectionOperation. M file. In the start method of AFURLConnectionOperation, drop the operationDidStart method in the child networkRequestThread for execution. [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; In the operationDidStart method:  self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; [self.connection scheduleInRunLoop:runLoop forMode:runLoopMode]; [self.connection start]; In this way, it is possible to initiate network requests and data parsing in the child thread. The child thread is always resident and never stops. Network requests initiated by NSURLConnection need to be processed and data parsed in AFURLConnectionOperation. Remove the resident thread from AFN 3.x. Run when used and stop when finished. This is because NSURLSession maintains its own thread pool for scheduling and managing request threads. Instead of needing to parse requests and data in the current thread, you can specify the delegateQueue to call back. */ + (NSThread *)networkRequestThread { static NSThread *_networkRequestThread = nil; static dispatch_once_t onceToken; Dispatch_once (&onceToken, ^{// Why __block is not used, because block intercepts static local variables as Pointers *** *. _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(onNetworkRequstThread) object:nil]; [_networkRequestThread start]; }); } + (void)onNetworkRequstThread { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }}Copy the code

Recommend ways

The above methods have an obvious drawback: using the [[NSRunLoop currentRunLoop] run] method, once the runloop is running, it cannot be stopped. To control runLoop running, add a variable and use the runMode:beforeDate: interface instead.

/* Add a BOOL switch for control. BOOL shouldKeepRunning = YES; // global NSRunLoop *rl = [NSRunLoop currentRunLoop]; while (shouldKeepRunning && [rl runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); */ - (void)addBackgroundRunLoop3 { __block BOOL shouldKeepRunning = YES; self.thread3 = [[NSThread alloc] initWithBlock:^{ [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; while (shouldKeepRunning) { @autoreleasepool { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } } }]; self.thread3.name = @"addBackgroundRunLoop3"; [self.thread3 start]; NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { [self performSelector:@selector(onTimerInBackgroundRunLoop3) onThread:self.thread3 withObject:nil waitUntilDone:NO]; }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ shouldKeepRunning = NO; }); } - (void)onTimerInBackgroundRunLoop3 { NSLog(@"%@", self.thread3.name); }Copy the code

A Runloop must have a Mode item to survive, and the outside can send messages to the runloop via a port. Through the performSelector: onThread: can put this task in the child thread that executes.

peformSelector

The essence of this type of method is to use NSTimer. PeformSelector: afterDelay: and performSelector: onThread: call, internal will create an NSTimer, added to the specified thread runloop. If the background thread does not have a runloop, it will fail. So for child threads, only dispatch_after can be used to delay the operation, because GCD starts the child thread and actually uses runloop internally.

You can use runloopMode, such as setting only the image of UIImageView in Default Mode, so that the scrolling of UIScrollView is not affected. For example, when the slide stops, the pictures are displayed one by one…

PerformSelector: withObject: afterDelay: inModes: method can be specified on a mission in the runloopMode, such as only give under DefaultMode UIImageView set of pictures. When UIScrollView is scrolling, the task of setting the image will not be performed to ensure smooth scrolling. Set the image once you stop processing DefaultMode.

Can use cancelPreviousPerformRequestsWithTarget: and cancelPreviousPerformRequestsWithTarget: selector: object: to are lining up to the task will be cancelled.

UI update

When UI operations, CALayer level updates, layoutIfNeeded methods, etc., the corresponding UIView and CALayer are marked as dirty, that is, to be processed, and submitted to a global container. IOS registers an Observer to listen for the main thread’s BeforeWaiting state, which sends a drawRect: message to all view objects that are dirty. So, the rendering of the iOS interface is really only updated for views that have been modified and need to be redrawn.

Look at two observers:

"<CFRunLoopObserver 0x600000b001e0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 1999000, callout = _beforeCACommitHandler (0x7fff480bc2eb), context = <CFRunLoopObserver context 0x7fe8da300720>}"."<CFRunLoopObserver 0x600000b00280 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2001000, callout = _afterCACommitHandler (0x7fff480bc354), context = <CFRunLoopObserver context 0x7fe8da300720>}".Copy the code

The CoreAnimation will listen for the state of the Runloop, for which there should be two callout functions: _beforeCACommitHandler and _afterCACommitHandler.

For example:

UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)]; btn.backgroundColor = [UIColor redColor]; [self.view addSubview:btn]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ CGRect frame = btn.frame; Y += 200; [UIView animateWithDuration:1 animations:^{ btn.frame = frame; [btn setNeedsDisplay]; }]; // Move frame. Origin. x += 200; [UIView animateWithDuration:1 animations:^{ btn.frame = frame; [btn setNeedsDisplay]; }]; });Copy the code

If you do that, you won’t achieve your expectations. This is because UI updates collect the pending UI views and then draw them uniformly. So the result is just an animation that moves to the bottom right corner

Caton monitoring

Add an Observer to monitor the status of the Runloop.

Caton reason

The main reasons are:

  1. Complex UI, text and text mixed row and other drawing too large. A CPU task or GPU task that times out will stall during the period (1/60 of a second) of a V-sync signal on the screen.
  2. The main thread does network requests, database operations, IO operations, and so on.
  3. Deadlock etc.

If the runloop thread takes too long to execute the method before it goes to sleep, or it takes too long to receive the message after it wakes up to enter the next Runloop loop, the thread is blocked. If it is the main thread, it is stuck.

So, the question is which state to monitor? After the BeforeSources and AfterWaiting states, this is when runloop actually executes the task. That is, the source0 callback is triggered and the mach_port message is received.

Add an observer to the commonModes of the main thread runloop and change the time value when you observe both states. Create a resident subthread to monitor the time value, and if the timer runs out, the main thread has timed out. When the monitored status is reached, the time threshold is changed. If the time threshold does not change, the stack information is dumped and uploaded. Dump stacks can be used using PLCrashReporter.

Ping the main thread

The implementation principle is relatively simple, the child thread periodically throws a task to the main thread to execute to complete the ping operation. The main code is as follows:

var timeoutOfANR: TimeInterval = 0.5
private lazy var anrObserver: CFRunLoopObserver = { makeANRObserver() }()
private lazy var pingMainQueue: DispatchQueue = { return makePingMainQueue() }()
private lazy var pingMainLock: DispatchSemaphore = { return makePingMainLock() }()

func start(a) {
  if isMonitoring { return }
  isMonitoring = true
  
  CFRunLoopAddObserver(CFRunLoopGetMain(), anrObserver, CFRunLoopMode.commonModes)
  
  pingMainQueue.async {
      while self.isMonitoring {
          var timeout = true
          DispatchQueue.main.async {
              timeout = false
              self.pingMainLock.signal()
          }
          Thread.sleep(forTimeInterval: self.timeoutOfANR)
          if timeout {
              print("ANR")}self.pingMainLock.wait()
      }
  }
}

func stop(a) {
  guard isMonitoring else { return }
  isMonitoring = false
  
  CFRunLoopRemoveObserver(CFRunLoopGetMain(), anrObserver, CFRunLoopMode.commonModes)
}

private func makeANRObserver(a) -> CFRunLoopObserver {
  let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                    CFRunLoopActivity.allActivities.rawValue,
                                                    true.0)
  { (observer, activity) in
      self.currentActivityMainRunloop = activity
      self.recordActivityLock.signal()
      
      self.debugRunloopActivity(activity)
  }
  return observer!
}

private func makePingMainQueue(a) -> DispatchQueue {
  let queue = DispatchQueue(label: "com.icetime.AppMonitor.pingMainQueue",
                            qos: DispatchQoS(qosClass: .default, relativePriority: 0),
                            attributes: .concurrent,
                            autoreleaseFrequency: .inherit,
                            target: nil)
  return queue
}

private func makePingMainLock(a) -> DispatchSemaphore {
  return DispatchSemaphore(value: 0)}Copy the code

Get the call stack

To get the call stack, use the open source frameworks PLCrashReporter or KSCrash.

Network request

CFSocket: The bottom layer, only responsible for socket communication CFNetwork -> based on CFSocket encapsulation, ASIHttpRequest based on this layer NSURLConnection -> NSURLConnection is based on CFNetwork OC encapsulation. NSURLSession AFNetworking 2 x - > NSURLSession part is still used in the underlying the NSURLConnection (such as com. Apple. NSURLConnectionLoader thread). AFNetworking 3.x, AlamofireCopy the code

Runloo receives notifications from the underlying CFSocket through a number of Mach Port-based sources.

Once the NSURLConnection is started, a delegate method is called to receive data. This continuous action is run in a 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. Where CFHTTPCookieStorage is used to process cookies; The CFMultiplexerSource is responsible for the various delegate callbacks and wakes up the RunLoop (usually the main thread) within the delegate 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, an empty thread was created inside AFNetworking and a RunLoop was started. When a task needs to be performed using the background thread, AFNetworking places the task in the RunLoop of the background thread using performSelector: onThread:.Copy the code
Normally when you use NSURLConnection, you pass in a Delegate, and when you call connection start, the Delegate will receive repeated event callbacks. In fact, the start function internally gets CurrentRunLoop and adds four source0s to DefaultMode (Source that needs to be triggered manually). CFMultiplexerSource is responsible for the various Delegate call-back, and CFHTTPCookieStorage is responsible for 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 handles the underlying socket connection. The NSURLConnectionLoader thread internally uses a RunLoop to receive events from the underlying socket and to notify the upper-level Delegate via the previously added Source0.Copy the code

The RunLoop in NSURLConnectionLoader receives notifications from the underlying CFSocket through a number of Mach Port-based sources. When a notification is received, it sends the notification to Source0, such as CFMultiplexerSource, at the appropriate time, and wakes up the Delegate thread's RunLoop to process the notification. The CFMultiplexerSource performs the actual callback to the Delegate in the Delegate thread's RunLoopCopy the code

Realized that you can merge network requests using Runloop? Guess should be the use of CFSocket, directly receive the system underlying network request related information, network request splicing; After receiving the network return result, the returned data is disassembled and redistributed to the respective network request business party. Just a guess.

Set the image when UIScrollView stops scrolling

By taking advantage of the isolation between runloopModes, imageView can be set only in DefaultMode. When the UIScrollView is in UITrackingMode, the image is not set.

[self.imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
Copy the code

The setImage operation must be performed on the main thread and involves two phases: image decoding and rendering. Frequent calls or time-consuming image decoding can easily affect the user experience. This is a great way to optimize the experience. It’s also a good idea to decode the image asynchronously, even when the Runloop is idle.

UI Task decomposition

For a lot of UITableViewCell drawing, a runloop cycle will try to load all the images on a screen, and it’s easy to get stuck when sliding quickly. Tasks can be split based on the principle of Runloop. Listen for runloop’s BeforeWaiting event, and each runloop loads an image. This requires the use of blocks to wrap a loadImageTask.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell  *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell"]; cell.selectionStyle = UITableViewCellSelectionStyleNone; for (NSInteger i=1; i<=5; i++) { [[cell.contentView viewWithTag:i] removeFromSuperview]; } /// before: load all images on a screen in one runloop cycle, resulting in a lag // [self addImage1ForCell:cell]; // [self addImage2ForCell:cell]; // [self addImage3ForCell:cell]; /// after: optimize /// / split heavy UI tasks based on the runloop principle: listen to the runloop loop and load one image at a time. /// Use block to wrap a loadImageTask. __weak typeof(self) weakSelf = self; [self addLoadImageTask:^{ [weakSelf addImage1ForCell:cell]; }]; [self addLoadImageTask:^{ [weakSelf addImage2ForCell:cell]; }]; [self addLoadImageTask:^{ [weakSelf addImage3ForCell:cell]; }]; return cell; }Copy the code

The following is how to add a Runloop observer:

typedef void(^BlockTask)(void); Static void *ViewControllerSelf; @property (nonatomic, strong) NSMutableArray<BlockTask> *loadImageTasks; - (void)addRunloopObserver {/// This callback is triggered when runloop is about to go to sleep; Each runloop cycle has a time when it is about to go to sleep, so the callback will always be called as the user scrolls. /// If there is no user action, the runloop goes to sleep when it is still, and the callback will not be triggered. CFRunLoopObserverContext context = { 0, (__bridge void *)self, &CFRetain, &CFRelease, NULL }; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &RunloopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes); If the timer is added, the callback will be called until the user stops scrolling. Because the Timer wakes up the Runloop. NSTimer * timer = [NSTimer timerWithTimeInterval: 0.0001 repeats: YES block: ^ (NSTimer * _Nonnull timer) {/ / / nothing}]. [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } /// How to provide loadImageTask's task (which requires instance objects of the ViewController) to the callback function. void RunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { NSLog(@"RunloopObserverCallBack"); /// Each time the callback is triggered, one task is executed out of the tasks, that is, one image is loaded per callback. /// Method 1 uses static variables to store the self object. ViewController *self = (__bridge ViewController *)ViewControllerSelf; /// Method 2, use CFRunLoopObserverContext to pass the self object. if (self.loadImageTasks.count == 0) { return; } BlockTask task = self.loadImageTasks.firstObject; task(); [self.loadImageTasks removeObjectAtIndex:0]; }Copy the code

Layout calculation

UITableView+FDTemplateLayoutCell uses an Observer to listen for runLoop BeforeWaiting state, that is, layout calculation tasks are performed when runloop is idle. The computing task is suspended while the user is swiping (UITrackingMode). The layout calculation mainly calculates the height of the UITableViewCell and precaches it.

Because of the isolation of runloopMode and the introduction of UITrackingMode, all tasks in other modes (DefaultMode) are suspended while the interface is sliding to ensure smooth sliding. This can be achieved without strong coupling with the UITableView code, but also can optimize the layout calculation task, reduce the CPU burden.

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
    // TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
Copy the code

UITableView+FDTemplateLayoutCell also uses the technique of using Runloop to decompose tasks. If the interface is not displayed, the height calculation tasks of other cells that need to be cached can be decomposed into each runloop loop for execution.

NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy; CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) { if (mutableIndexPathsToBePrecached.count == 0) { CFRunLoopRemoveObserver(runLoop, observer, runLoopMode); CFRelease(observer); // Return; } NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject; [mutableIndexPathsToBePrecached removeObject:indexPath]; [self performSelector:@selector(fd_precacheIndexPathIfNeeded:) onThread:[NSThread mainThread] withObject:indexPath waitUntilDone:NO modes:@[NSDefaultRunLoopMode]]; });Copy the code

In this way, it is not necessary to calculate the full cache height in a runloop cycle, and height calculation is performed only in DefaultMode. The slide pauses the calculation task. Reference: Those things that optimize UITableViewCell height calculation.

This idea can be used to break down tasks such as performing calculations while idle, counting memory, and so on. Such as picture asynchronous decoding. This is an asynchronous operation and has nothing to do with runloop.

Custom Source0

To use source0 as a developer, it’s easy to create a CFRunLoopSourceContext context with a pointer to the function being executed. Then pass the context as a construction parameter to CFRunLoopSourceCreate to create a source, and then bind the source to a runloopMode through CFRunLoopAddSource.

The custom source0 event is executed when __CFRunLoopDoSources0 is called.

CFRunLoopAddSource
CFRunLoopRemoveSource
Copy the code

What is the use of this in a real project?? Temporary not clear

NSNotificationQueue

NSNotificationQueue NSNotificationQueue acts as a buffer in the NSNotificationCenter. Although NSNotificationCenter has already distributed notifications, notifications placed in the queue may not be sent until the current Runloop ends or the Runloop is idle. The specific policy is determined by the following parameters. If you have multiple identical notifications, you can merge them in NSNotificationQueue so that only one notification is sent. The NSNotificationQueue maintains an instance of the NSNotification on a first-in, first-out basis. When the notification instance is at the head of the queue, the notification queue sends it to the notification center, which in turn sends notifications to all registered observers. Each thread has a notification queue associated with the Default Notification Center by default. By calling the initWithNotificationCenter and external NSNotificationCenter associated, finally also manage notification sent, by NSNotificationCenter register. In addition, there are two enumeration values that need special attention. NSPostingStyle: used to configure when notifications are sent NSPostASAP: notify when the current notification is called or the timer ends NSPostWhenIdle: notify when the Runloop is idle NSPostNow: notify immediately after the merge notification completes. NSNotificationCoalescing (note that this is a NS_OPTIONS) : used to configure how to merge notice NSNotificationNoCoalescing: no merger notification NSNotificationNoCoalescing: In accordance with the notice name merge notice NSNotificationCoalescingOnSender: according to the incoming object merge noticeCopy the code

The UI rendering

UI rendering flow

  1. CPU: Object creation/destruction (alloc/dealloc), layout calculation (layout), typesetting (calculate view size, text height, etc.), drawing content (drawRect/drawLayer), preparing data for decoding (Decompress image), etc. Submit the required data (layer and animation data) to the render service, the render service deserializes the data into the Render tree, calculates the intermediate value, generates the texture, etc.
  2. GPU: Texture rendering, view blending, rendering visual textures to the screen.
  3. CPU has a strong versatility, a variety of data types, logic judgment, branch adjustment, interrupt processing, etc., the internal structure is very complex. Computing power is only part of the CPU’s role, but it is also good at various logic controls and common data types. A CPU has a small number of cores, including registers, multilevel Cache, and so on.
  4. However, Gpus are good at processing large-scale parallel computing tasks (there is no logical dependence on each other), and the data types are highly unified. There are many cores of Gpus, but they are all required for repeated computation. The GPU has no Cache.

Asynchronous drawing: You can perform the operation of CGBitmapxxx in the asynchronous thread, generate the bitmap, read the image, and finally assign it to layer.contents in the main thread. Avoid the main thread running setImage: on UIImageView while decoding and rendering image data.

The CALayer determines whether its delegate implements a displayLayer method for asynchronous drawing. If not, the CALayer processes the drawing process. If there is, enter the process, can do asynchronous drawing.

The iOS system registers an observer to listen for the BeforeWaiting event

Register an observer, listen for BeforeWaiting events, and call back methods to draw and render all dirty view/ layers. That is, the CALayer’s display method is executed in the callback to enter the real drawing work.

If the print start App after the main thread RunLoop can find another callout for _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv * * * * of the Observer, This listener is responsible for updating UI changes, such as frame changes, UI hierarchy adjustments (UIView/CALayer), or manual SettingssetNeedsDisplay/setNeedsLayout then commits these operations to the global container. This Observer listens for the main thread RunLoop's impending sleep and exit states and, once in both states, iterates over all UI updates and commits for actual drawing updates. _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]; Often this approach is perfect because in addition to system updates, you can take advantage of itsetMethods such as NeedsDisplay manually trigger updates for the next RunLoop run. However, if a lot of logical operations are currently being performed, UI updates may compare cards, so Facebook has introduced AsyncDisplayKit to solve this problem. AsyncDisplayKit actually puts UI layout and drawing operations in the background as much as possible, puts the final UI update operations in the main thread (which must also be done in the main thread), and provides a set of UIView or CALayer properties to keep developers as much in the developer's habit as possible. In this process AsyncDisplayKit adds an Observer to the main thread RunLoop to listen for both hibernating and exiting RunLoop states. When receiving a callback, the AsyncDisplayKit traverses the queue for tasks to be processed one by one.Copy the code

AsyncDisplayKit

The UI rendering process has been described above. For these tasks, except for those that must be performed on the main thread, such as UI object manipulation and layout, all other tasks should be performed in the background thread as much as possible. In particular, time-consuming operations, such as CPU object creation and destruction, text calculation, layout calculation, image codec, etc., as far as possible in the background thread execution. Do only the necessary operations on the main thread.

What the ASDK does is put the tasks that can be put in the background as much as possible, and put off the ones that can’t (such as view creation, property tuning). To this end, ASDK creates an object named ASDisplayNode and encapsulates UIView/CALayer internally. It has similar properties to UIView/CALayer, such as frame, backgroundColor, etc. All of these properties can be changed in the background thread, and developers can manipulate the internal UIView/CALayer through Node alone, thus putting layout and drawing into the background thread. But no matter what you do, these properties need to be synchronized to the main thread’s UIView/CALayer at some point.

ASDK implements a similar interface update mechanism modeled on the QuartzCore/UIKit framework: That is, add an Observer in the RunLoop of the main thread, listen for kCFRunLoopBeforeWaiting and kCFRunLoopExit events, and when the callback is received, iterate over all the pending tasks previously placed in the queue, and then execute one by one.

In fact, this set of ideas is widely used. The front-end framework, such as the Virtual DOM Tree in Vue, is also used to temporarily save the UI state to be modified, and then process and update the interface information uniformly.

Resurrect Crash’s App

There are two types of App crashes:

  1. SIGABRT with a signal, which is usually an over-release object, or an unrecognized selector
  2. EXC_BAD_ACCESS accesses the freed memory, that is, the wild pointer error.

The SIGABRT is sent to the App by the system with the Signal. After receiving the Signal, the App will kill the runloop of the main thread, that is, crash is generated. To get the App to shine, we need to catch the abort() call made by libsystem_sim_c.dylib to terminate the program, and then let it execute the method we defined to handle the signal. In the method, we need to start a RunLoop to keep the main thread from exiting.

To be supplemented after practice!!

Some of the QA

A run loop is an abstraction that (among other things) provides a mechanism to handle system input sources (sockets, ports, files, keyboard, mouse, timers, etc). Each NSThread has its own run loop, which can be accessed via the currentRunLoop method. In general, you do not need to access the run loop directly, though there are some (networking) components that may allow you to specify which run loop they will use for I/O processing. A run loop for a given thread will wait until one or more of its input sources has some data or event, then fire the appropriate input handler(s) to process each input source that is "ready.". After doing so, it will then return to its loop, processing input from various sources, and "sleeping" if there is no work to do. That's a pretty high level description (trying to avoid too many details). EDIT An attempt to address the comment. I broke it into pieces. it means that i can only access/run to run loop inside the thread right? Indeed. NSRunLoop is not thread safe, and should only be accessed from the context of the thread that is running the loop. is there any simple example how to add event to run loop? If you want to monitor a port, you would just add that port to the run loop, and then the run loop would watch that port for activity. - (void)addPort:(NSPort *)aPort forMode:(NSString *)mode You can also add a timer explicitly with - (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode what means it will then  return to its loop? The run loop will process all ready events each iteration (according to its mode). You will need to look at the documentation to discover about run modes, as that's a bit beyond the scope of a general answer. is run loop inactive when i start the thread? In most applications, the main run loop will run automatically. However, you are responsible for starting the run loop and responding to incoming events for threads you spin. is it possible to add some events to Thread run loop outside the thread? I am not sure what you mean here. You don't add events to the run loop. You add input sources and timer sources (from the thread that owns the run loop). The run loop then watches them for activity. You can, of course, provide data input from other threads and processes, but input will be processed by the run loop that is monitoring those sources on the thread that is running the run loop.  does it mean that sometimes i can use run loop to block thread for a time Indeed. In fact, a run loop will "stay" in an event handler until that event handler has returned. You can see this in any app simply enough. Install a handler for any IO action (e.g., button press) that sleeps. You will block the main run loop (and the whole UI) until that method completes. The same applies to any run loop.Copy the code
Look at the "Run Loops" chapter of Apple's Threading Programming Guide. In brief:

There is one run loop associated with each thread.
The run loop has to be run to do anything. Apple's application main function takes care of this for you on the main thread.
A run loop is run in a specific mode. The "common mode" is actually a set of modes, and there is an API for adding modes to that set.
A run loop's main purpose is to monitor timers and run loop sources. Each source is registered with a specific run loop for a specific mode, andwill only be checked at the appropriate time when the runloop is running in that mode. The run loop goes through several  stages in each go around its loop, such as checking timersand checking other event sources. If it finds that any source is ready to fire, it triggers the appropriate callback.
Aside from using ready-made run loop tools, you can create your own run loop sources as well as registering a run loop observer to track the progress of the run loop.
One major pitfall is forgetting to run the run loop while waiting for a callback from a runloop source. This is sometimes a problem when you decide to busy-wait for something to happen on the main thread, but you're most likely to run into it when you create your own thread and register a runloop source with that runloop. You are responsible for establishing an autorelease pool and running the runloop if needed on non-main threads, since the application main function will not be there to do it for you.

You would do better to read Apple's Concurrency Programming Guide instead, which suggests alternatives to the runloop mechanism such as operation queues and dispatch sources. The "Replacing Run-Loop Code" section of the "Migrating Away from Threads" chapter suggests using dispatch sources instead of runloop sources to handle events.
Copy the code

The resources

  1. Event loop
  2. Run Loops
  3. NSRunLoop
  4. CFRunLoop
  5. CFRunLoopPerformBlock
  6. CFRunLoopPerformBlock vs dispatch_async
  7. RunLoop.subproj
  8. Kernel Programming Guide: Mach Overview
  9. mach_msg
  10. CFPlatform.c
  11. AsyncDisplayKit, which has been renamed Texture
  12. Learn more about RunLoop
  13. Decryption Runloop
  14. IOS gets to the bottom – get to the bottom of RunLoop
  15. Optimize the UITableViewCell height calculation for those things
  16. Go back to the RunLoop principle
  17. IOS RunLoop,