It’s the same old story, but RunLoop is really important.

Two good articles are recommended:

IOS multithreading: a thorough summary of “RunLoop”

Understand RunLoop in depth

The summary of these two articles is very detailed, I just take notes here.

RunLoop profile

As we know, if you create a command line project with Xcode, it will end after the main function runs, whereas if you create an iOS project, it will continue running. Let’s compare the two main functions

Command line project:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
    }
    return 0;
}
Copy the code

The main function for iOS projects is:

int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}Copy the code

You can see there’s a lot of UIApplicationMain,

The UIApplicationMain code looks something like this:

int main(){ @autoreleasepool{ int retVal = 0; Int message = sleep_and_wait(); RetVal = process_message(message); }while(0 == retVal); return 0; }}Copy the code

Inside UIApplicationMain we enable RunLoop for the main thread.

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, this model is often referred to as an Event Loop. So is RunLoop in iOS.

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 actually an object that is used within the loop to handle various events (such as touch events, UI refresh events, timer events, Selector events) that occur while the program is running to keep it running. And when there is no event processing, it will enter the sleep mode, so as to save CPU resources and improve program performance.

Apple’s official RunLoop model:

A RunLoop is a loop in a thread that continuously checks for receiving events through two sources: Input sources and Timer sources. It then processes the received event notification thread and rests when there are no events.

Runloop source code analysis

Let’s take a look at the source code

There are two sets of apis in iOS to access and use Runloop:

Foundation: NSRunLoop

Core Foundation : CFRunLoopRef

We’re going to look at the source code in Core Foundation

The structure of the Runloop

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; // Current Mode CFMutableSetRef _modes; Struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart; };Copy the code

Look again at CFRunLoopModeRef of mode type

CFRunLoopModeRef is a pointer to the __CFRunLoopMode structure:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

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 */ }; Focus on the following member variables CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers;Copy the code

Through the above analysis, we know:

CFRunLoopModeRef representative RunLoop Mode of operation, a RunLoop contains several Mode, each Mode and contains several Source0 / Source1 / Timer/Observer, RunLoop starts with only one of these modes as currentMode.

Let’s take a look at one:A RunLoop object (CFRunLoopRef) contains several running modes (CFRunLoopModeRef). Each operating mode contains several input sources (CFRunLoopSourceRef), timing sources (CFRunLoopTimerRef), and observers (CFRunLoopObserverRef).

The system registers five modes by default:

  1. kCFRunLoopDefaultMode: The App’s default Mode in which the main thread is normally run.
  2. UITrackingRunLoopMode: Interface tracking Mode, used for ScrollView tracking touch sliding, to ensure that the interface sliding is not affected by other modes.
  3. UIInitializationRunLoopMode: The first Mode entered at the beginning of App startup will not be used after startup.
  4. GSEventReceiveRunLoopMode: Accepts internal modes for system events, usually not needed.
  5. kCFRunLoopCommonModes: false Mode, this is a placeholder Mode, has no effect.

RunLoop related classes

The Core Foundation framework provides five classes for RunLoop:

  1. CFRunLoopRef: Object representing RunLoop
  2. CFRunLoopModeRef: indicates the running mode of RunLoop
  3. CFRunLoopSourceRef: is the input/event source mentioned in the RunLoop diagram
  4. CFRunLoopTimerRef: is the timing source mentioned in the RunLoop model diagram
  5. CFRunLoopObserverRef: Observer that listens for state changes in RunLoop

We can get RunLoop objects by:

  • Core Foundation
    • CFRunLoopGetCurrent(); // Get the RunLoop object for the current thread
    • CFRunLoopGetMain(); // Get the main thread RunLoop object
  • Foundation
    • [NSRunLoop currentRunLoop]; // Get the RunLoop object for the current thread
    • [NSRunLoop mainRunLoop]; // Get the main thread RunLoop object

See CFRunLoopGetCurrent:

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
Copy the code

Look inside the _CFRunLoopGet0 method

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); if (! __CFRunLoops) { __CFUnlock(&loopsLock); / / create a dict CFMutableDictionaryRef dict = CFDictionaryCreateMutable (kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); RunLoop CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (! OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } // Take it from the dictionary, CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); // If loop is empty, a new loop is created, so runloop creates if (! loop) { CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); Runloop if (! Runloop if (! Runloop if (! loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; } // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&loopsLock); CFRelease(newLoop); } if (pthread_equal(t, pthread_self())) { _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; }Copy the code

There is a one-to-one correspondence between threads and runloops, which is stored in a Dictionary. So when we create a child thread RunLoop, we simply get the RunLoop object from the current thread in the child thread[NSRunLoop currentRunLoop];

If not, the child thread does not create the RunLoop associated with it, and only retrieves its RunLoop inside a thread[NSRunLoop currentRunLoop];

Method is called to see if there is a RunLoop in the dictionary for the child thread. If there is a RunLoop, it returns a RunLoop. If there is no RunLoop, it creates one and stores its child thread to the dictionary. When the thread terminates, the RunLoop is destroyed.

The operation of the RunLoop

Once the RunLoop is created, how does it work?

CFRunLoopRun()

void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; Do {result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped ! = result && kCFRunLoopRunFinished ! = result); }Copy the code

A CFRunLoopRun method exposed for external calls that calls CFRunLoopRunSpecific internally

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { Boolean did = false; if (currentMode) __CFRunLoopModeUnlock(currentMode); __CFRunLoopUnlock(rl); return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); CFRunLoopModeRef previousMode = rl->_currentMode; rl->_currentMode = currentMode; int32_t result = kCFRunLoopRunFinished; // announcements about Observers: Loop if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rL, currentMode, kCFRunLoopEntry); / / core logic result = __CFRunLoopRun Loop (rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // announcements about Observers: Exit Loop if (currentMode->_observerMask & kCFRunLoopExit) __CFRunLoopDoObservers(rL, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result; }Copy the code

Here’s a look at __CFRunLoopRun:

But first, a few states:

/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // About to enter kCFRunLoopBeforeTimers = (1UL << 1),// About to process Timer kCFRunLoopBeforeSources = (1UL << 2),// About to process Source KCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 6) RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU // Listen for all state changes};Copy the code

__CFRunLoopRun condensed:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; Do {// notify Observers: they are about to handle Timers __CFRunLoopDoObservers(RL, RLM, kCFRunLoopBeforeTimers); // notify Observers: about to deal with Sources __CFRunLoopDoObservers(Rl, RLM, kCFRunLoopBeforeSources); // Handle Blocks __CFRunLoopDoBlocks(rl, RLM); If (__CFRunLoopDoSources0(rl, RLM, stopAfterHandle)) {__CFRunLoopDoBlocks(rl, RLM); } // If there is Sources1, If (__CFRunLoopServiceMachPort(dispatchPort, & MSG, sizeof(MSg_buffer), &livePort, 0, &voucherState, NULL)) { goto handle_msg; } // Notify Observers: about to hibernate __CFRunLoopDoObservers(rl, RLM, kCFRunLoopBeforeWaiting); // Go to sleep and wait for other messages to wake __CFRunLoopSetSleeping(rl); __CFPortSetInsert(dispatchPort, waitSet); do { __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); } while (1); // wake up __CFPortSetRemove(dispatchPort, waitSet); __CFRunLoopUnsetSleeping(rl); // notify Observers: __CFRunLoopDoObservers(rl, RLM, kCFRunLoopAfterWaiting) have been awakened; // Handle Timer __CFRunLoopDoTimers(rl, RLM, mach_absolute_time()); // Handle Timer __CFRunLoopDoTimers(rl, RLM, mach_absolute_time()); } else if (aroused by GCD) {__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(MSG); } else {// __CFRunLoopDoSource1(rl, RLM, RLS, MSG, MSG ->msgh_size, &reply); } // execute Blocks __CFRunLoopDoBlocks(rl, RLM); / / according to the results before, to decide what to do, for the corresponding value if retVal fu (sourceHandledThisLoop && stopAfterHandle) {retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); return retVal; }Copy the code

The code above is how RunLoop runs

Here is a diagram:Each time a RunLoop is opened, the thread’s RunLoop automatically handles the previously unprocessed event and notifies relevant observers.

The specific order is as follows:

  1. Notifies the observer that RunLoop has started
  2. A timer that notifies the observer that it is about to start
  3. Notifies the observer of any non-port-based sources that are about to start
  4. Start any prepared non-port-based sources
  5. If the port-based source is ready and in a waiting state, start immediately; Go to Step 9
  6. Notifies the observer that the thread is asleep
  7. Put the thread to sleep until any of the following events occur:
    • An event reaches a port-based source
    • Timer start
    • The time set by RunLoop has timed out
    • RunLoop is shown waking up
  8. Notify the observer that the thread will be awakened
  9. Handle unprocessed events
    • If the user-defined timer starts, process the timer event and restart RunLoop. Enter Step 2.
    • If the input source is started, the corresponding message is passed
    • If RunLoop is awakened and the time has not expired, restart the RunLoop. Enter Step 2.
  10. Notifies the observer that RunLoop ends.

Add:

  • Source0:
    • Touch event handling
    • performSelector: onThread:
  • Source1:
    • Based on theportThread communication of
    • System Event Capture
  • Timers:
    • NSTimer
    • performSelector:withObject:afterDelay:
  • Observers:
    • Listens forRunLoopThe state of the
    • UI refresh (BeforeWaiting)
    • Autoreleasepool(BeforeWaiting)

The application of the runloop

Timer (fixes NSTimer to stop working when sliding)

We certainly have situations where NSTimer pauses when the interface is scrolling or when a scrollable control is scrolling, but resumes when it is not scrolling.

The reason:

  • When we’re not doing anything,RunLoopIn aNSDefaultRunLoopModeUnder.
  • As we roll,RunLoopendsNSDefaultRunLoopMode, switch toUITrackingRunLoopModeIn this mode, no NSTimer is added, so NSTimer doesn’t work.

Can’t we get NSTimer to work in both modes? Of course, this uses the pseudo mode (kCFRunLoopCommonModes) we talked about before. This is not a real mode, but a marked mode, which means that it can run in the mode marked with CommonModes.

So which Modes are marked Common Modes?

NSDefaultRunLoopMode and UITrackingRunLoopMode.

So we just need to add NSTimer to the kCFRunLoopCommonModes of the current RunLoop, We can make NSTimer work happily without manipulation or scrolling.

Particular way is to add a statement to: [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes];

A quick note about NSTimer:

[NSTimer scheduledTimerWithTimeInterval: 2.0 target: self selector: @ the selector (run) the userInfo: nil repeats: YES];Copy the code

This code calls the timer returned by scheduledTimer, and NSTimer is automatically added to the NSDefaultRunLoopMode of the RunLoop.

This code is equivalent to the following two sentences:

NSTimer * timer = [NSTimer timerWithTimeInterval: 2.0 target: self selector: @ the selector (run) the userInfo: nil repeats: YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];Copy the code

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.

Control the thread lifecycle (thread survival)

When we are developing an application, if background operations are particularly frequent and time-consuming operations (downloading files, playing music in the background, etc.) are performed on the child thread, it is best to keep that thread permanently resident in memory.

- (void)viewDidLoad { [super viewDidLoad]; Self. thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil]; // Start thread [self.thread start]; } - (void) run1 {/ / here writing task NSLog (@ "-- run1 -- -- -- -- --"); // Add the following lines of code to start RunLoop, and then self.thread becomes a resident thread, which can add tasks at any time. And pay the RunLoop processing [[NSRunLoop currentRunLoop] addPort: [NSPort port] forMode: NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; // Test whether RunLoop is enabled. If RunLoop is enabled, you cannot come here because RunLoop is enabled. NSLog(@" RunLoop not enabled "); } -- run1 -- - is printed, and RunLoop is not printed.Copy the code

At this point, we have opened a resident thread, and now we will try to add other tasks. In addition to calling run1 when we created it, we will also call run2 when we click.

So, we’re going to call PerformSelector in touchesBegan, so we’re going to call run2 whenever we click on the screen. The specific code is as follows:

- (void)touchesBegan:(NSSet< uittouch *> *)touches withEvent:(UIEvent *)event { Call the run2 method in the thread of self.thread to perform the task [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO]; } - (void) run2 { NSLog(@"----run2------"); }Copy the code

After running the tests, we can call — run2 — every time we click the screen, in addition to the — run1 — printed earlier.

AFNetworking 2.X and 3.X, thread resident issues, are definitely mentioned

2.X resident thread, used for concurrent requests and handling data callbacks; Avoid the thread overhead of multiple network requests (save one thread without opening one thread);

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

And 3.X doesn’t need resident threads?

Since NSURLSession can specify the callback delegateQueue, NSURLConnection can’t;

One of the major pain points of NSURLConnection is the need to wait for a callback after a request is initiated.

NSURLSession 3.0 solves this problem; NSURLSession requests no longer require callbacks on the current thread. You can specify the callback delegateQueue so that the thread is not held alive waiting for the proxy callback method.

ImageView delays display

Sometimes, we’ll have situations where we have UI tableviews, and each UITableView cell has an image in it. When scrolling through a UITableView, if there are a bunch of images that need to be displayed, there may be a lag.

At this point, we should delay the image display, that is, the ImageView postpones the image display. There are two ways:

  1. Since UITableView inherits from UIScrollView, we can implement a Delegate associated with UIScrollView by listening to the scrolling of UIScrollView.

  2. Use performSelector to set the running mode of the current thread’s RunLoop

Call setImage: on UIImageView using performSelector, and use inModes to set it to NSDefaultRunLoopMode under RunLoop. The code is as follows:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:2.0 inModes:NSDefaultRunLoopMode];
Copy the code

Automatic release tank

Timer and Source are also variables that take up a little bit of storage, so we have to free them up, and if we don’t free them up, we’re just going to keep building up and taking up more and more memory, which is obviously not what we want.

So when and how?

The RunLoop has an auto-release pool inside it. When the RunLoop is on, an auto-release pool is automatically created. When the RunLoop is resting, the auto-release pool is released, and a new empty auto-release pool is created. New events such as Timer,Source, etc. are placed in the new auto-release pool and released when the RunLoop exits. Note: Only the main thread RunLoop starts by default. This means that the auto-release pool is created automatically, and the child thread needs to add the auto-release pool manually in the thread scheduling method.

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.

performSelector

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.

Monitoring application lag

The delay is caused by tasks taking too long on the main thread, so we need to listen on the main thread. The thread’s message events depend on the runLoop, so we need to listen for the status of the main thread corresponding to the runLoop.

Stall is a blocked thread. What state is the runLoop in that we consider blocked?

1. The runLoop method is executed for a long time, causing the system to fail to go to sleep

2. After the runLoop is woken up, it takes too long to receive the message and cannot go to the next step

So kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting are two states that need to be listened on.

Implementation: We need to create a runLoop observer, add this observer to the main runLoop’s common mode, and create a child thread to periodically listen for the main runLoop status.

You can set a time threshold above which you are considered stuck.

Roughly as follows:

Dispatch_async (dispatch_get_global_queue(0, 0)) While (YES) {long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore) dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC)); if (semaphoreWait ! = 0) { if (! runLoopObserver) { timeoutCount = 0; dispatchSemaphore = 0; runLoopActivity = 0; return; } / / BeforeSources and AfterWaiting to detect whether the two caton the if (runLoopActivity = = kCFRunLoopBeforeSources | | runLoopActivity = = KCFRunLoopAfterWaiting) {//end activity}// end semaphore wait timeoutCount = 0; }// end while });Copy the code

If there is any mistake above, welcome to correct. Please indicate the source of reprint.