caton

The observer registers with RunLoop to receive notifications in the following stages

kCFRunLoopEntry

kCFRunLoopBeforeTimers

kCFRunLoopBeforeSources

kCFRunLoopBeforeWaiting

kCFRunLoopAfterWaiting

kCFRunLoopExit

kCFRunLoopAllActivities

__CFRunLoopRun source code analysis can be seen in this blog, according to the source extract Runloop main flow as follows:

The event sources processed by RunLoop are categorized

The total number of event sources handled by RunLoop can actually be divided into two categories: source0 and MachPort-based messages.

Normally UIView event handling and block calls defined in the code belong to source0, while messages between threads are communicated via the GCD party

Tasks sent to the main thread and NSTimer are implemented based on MachPort messages. So in the flow chart, I have the second type

Stand out, which means they are a large type. In the official documentation, it is divided into Timer, Source, Oberserver, and source

For source0 and source1, separate out the timer.

What states of RunLoop are worth paying attention to

The state of RunLoop at source0 before the task is executed is KCFRunLoopBeforeSources

The state of RunLoop before machPort-based task execution is kCFRunLoopBeforeSources or

KCFRunLoopAfterWaiting so only need to listen for the status of Runloop in CFRunLoopBeforeSources and

CFRunLoopAfterWaiting whether the two states generate the execution time of the specified time can determine whether the delay occurs.

After CFRunLoopBeforeWaiting, the Runloop goes to sleep and has no logic to process other event sources.

If this state is monitored, it will be judged to be stalled when the main thread is sleeping, resulting in false positives.

Monitor the status of the main thread Runloop

Start a child thread that detects the status of the main thread Runloop. Passes each time the Runloop status changes

Dispatch_semaphore_signal sends a signal that increases the value of the semaphore by 1. Let the child threads keep an eye on semaphore changes,

If the main thread stays in one of the Runloop states for too long, semaphore blocks the child thread until it times out, which is a stall.

// Global or member variable CFRunLoopActivity currentActivity; // dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); / / test the main thread state Runloop CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler (kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { if (self ! = nil && semaphore ! = NULL) { currentActivity = activity; dispatch_semaphore_signal(semaphore); }}); CFRunLoopAddObserver(CFRunLoopGetMain(), observerRef, kCFRunLoopCommonModes); CFRelease(observerRef); // Use the semaphore to calculate whether the execution in a certain state has exceeded the specified time, here is 1s while (! self.cancelled) { long status = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 1000 * NSEC_PER_MSEC)); if (status ! = 0) { if (self.cancelled || semaphore == NULL) { return; } if (currentActivity == kCFRunLoopBeforeSources || currentActivity == kCFRunLoopAfterWaiting ) { if (self.cancelled) return; NSLog(@" stuck "); if (self.cancelled) return; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); }}}Copy the code

Finally, a diagram shows the whole process:

FPS

CADisplayLink is a timer that synchronizes with the screen refresh to calculate the screen frame rate in real time.

Ideally, at 60 frames per second, the CADdisplayLink callback interval is 1/60 ≈ 0.0167 seconds.

During each iteration of the Runloop, if there are time-consuming tasks, the CADdisplayLink callback time will be increased, relatively speaking, calculated

The frame rate will be lower. A frame hop occurs while the screen is drawing.

CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(p_tick:)]; [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - (void)p_tick:(CADisplayLink *)link { self.count++; if (! self.lastTime) { self.lastTime = CACurrentMediaTime(); } else { NSTimeInterval currentTime = CACurrentMediaTime(); NSTimeInterval duration = currentTime - self.lastTime; if (duration > 1) { NSString *fps = [NSString stringWithFormat:@"%.2f", (self.count / duration)]; Self.fpslabel. text = [NSString stringWithFormat:@" frame rate: %@", FPS]; self.count = self.lastTime = 0; }}}Copy the code