Runloop is a very basic concept in iOS and OSX development. This article will not cover the basic principles of Runloop, because there are too many blogs on the web. This article will cover the use of Runloop in real projects (you may not have used any of them), but it will certainly be used in medium and large enterprises. Hopefully, after this blog post, you’ll have a bit more depth on how Runloop can be used in real projects!

An overview of the

This article mainly focuses on the application scenarios of Runloop used in the project and the knowledge points derived from it. The following parts will be described:

  • Control the life cycle of threads
  • Solve the problem that NSTimer stops working in the sliding process and derivative problems
  • Monitoring application lag
  • Performance optimization

Thread survival

Thread survival problem, literally, is to protect the life cycle of a thread. Under normal circumstances, when a thread completes a task, it needs to recycle resources. However, when there is a task, it may be called at any time. If it is executed in a sub-thread and the sub-thread is kept alive, in order to avoid the action of creating and destroying the thread many times, the performance consumption is reduced.

Scenario 1

#import <Foundation/ foundation. h> // define thread from NSThread @interface ZXYThread: nsthread@end@implementation ZXYThread // Thread destruction is called - (void)dealloc {NSLog(@"%s", __func__); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.thread = [[ZXYThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [self.thread start]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO]; } - (void)test {NSLog(@"%s %@", __func__, [NSThread currentThread]); } - (void)run { NSLog(@"%s %@", __func__, [NSThread currentThread]); NSLog(@"%s ----end----", __func__); } @endCopy the code

When the above code is executed, it will print the following -[The child thread is destroyed]

However, after running the App, there is no response when clicking the App, which can also prove that the thread has been destroyed. What if improvements were made to keep threads in a state that is ready to receive commands?

Scenario 2

Learned from the Runloop, if there is no any Mode Source0 / Source1 / Timer/Observer, Runloop immediately exit.

So I wonder if I can add one of the above to it.

- (void)run {NSLog(@"%s %@", __func__, [NSThread currentThread]); // Add Source Timer Observer [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; // [[NSRunLoop currentRunLoop] addTimer:[[NSTimer alloc]init] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run]; NSLog(@"%s ----end----", __func__); }Copy the code

Keep the thread immortal by adding the above code to the run method and printing the screen interface:

It seems that the above requirements have been satisfied to reach the thread deathless state, but can you destroy the timer when the page controller is destroyed, and stop the timer at any time?

Scenario 3

Knowledge:

** How do I stop runloop? The timer can be stopped by CFRunLoopStop(CFRunLoopGetCurrent()), but for [[NSRunLoop currentRunLoop] [[NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] [[NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] [NSRunLoop currentRunLoop] —-[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; **

The following is A direct code to explain, which has been written code ideas, the following is A page ->B page ->A page

@interface ViewController () // Property (strong, nonatomic) ZXYThread *thread; Stopped: @property (assign, NonAtomic, getter=isStoped) BOOL STOPPED; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; //NSThread uses the block method, eliminating the loop reference __weak Typeof (self) weakSelf = self; self.stopped = NO; self.thread = [[ZXYThread alloc] initWithBlock:^{ NSLog(@"%@----begin----", [NSThread currentThread]); // Add Source Timer Observer [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && ! weakSelf.isStoped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  } NSLog(@"%@----end----", [NSThread currentThread]); }]; [self.thread start]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if (! self.thread) return; [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO]; } - (void)test {NSLog(@"%s %@", __func__, [NSThread currentThread]); } - (void) stop { if (! self.thread) return; // After the child thread calls stop (waitUntilDone is set to YES, indicating that the child thread is finished executing the code, [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES]; } // RunLoop - (void)stopThread {// Set the flag to YES self.stopped = YES; RunLoop CFRunLoopStop(CFRunLoopGetCurrent()); NSLog(@"%s %@", __func__, [NSThread currentThread]); // Empty thread self.thread = nil; } - (void)dealloc { NSLog(@"%s", __func__); [self stop]; } @endCopy the code

What if you want to extract the above code?

Scenario 4

The encapsulated utility classes here do not inherit directly from NSThread, but from NSObject because you do not want someone to be able to call the methods in NSThread

#import <Foundation/Foundation.h> typedef void (^ZXYPermenantThreadTask)(void); @interface ZXYPermenantThread: NSObject /** Execute a task in the current child thread */ - (void)executeTask:(ZXYPermenantThreadTask)task; /** stop the thread */ - (void)stop; @end #import "ZXYPermenantThread.h" /** ZXYThread **/ @interface ZXYThread : NSThread @end @implementation ZXYThread - (void)dealloc{ NSLog(@"%s", __func__); } @end /** ZXYPermenantThread **/ @interface ZXYPermenantThread() @property (strong, nonatomic) ZXYThread *innerThread; @property (assign, nonatomic, getter=isStopped) BOOL stopped; @end @implementation ZXYPermenantThread #pragma mark - public methods - (instancetype)init{ if (self = [super init]) { self.stopped = NO; __weak typeof(self) weakSelf = self; self.innerThread = [[ZXYThread alloc] initWithBlock:^{ [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && ! weakSelf.isStopped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } }];  [self.innerThread start]; } return self; } - (void)executeTask:(ZXYPermenantThreadTask)task{ if (! self.innerThread || ! task) return; [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO]; } - (void)stop{ if (! self.innerThread) return; [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES]; } - (void)dealloc{ NSLog(@"%s", __func__); [self stop]; } #pragma mark - private methods - (void)__stop{ self.stopped = YES; CFRunLoopStop(CFRunLoopGetCurrent()); self.innerThread = nil; } - (void)__executeTask:(ZXYPermenantThreadTask)task{ task(); } @endCopy the code

This is the first real development scenario for Runloop, so have we used it or seen it in any good open source projects?

Extension [AFNetworking also uses Runloop thread survival]

The ANURLConnectionOperation in AFNetworking is built on NSURLConnection and is essentially expected to receive a Delegate callback in the background thread. AFNetworking creates a separate thread and opens a Runloop in this 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

AFNetworking must have at least one Timer/Observer/Source before Runloop starts.

I created NSMachPort and I added it. Normally the caller needs to hold the NSMachPort and send messages inside the loop through this port from an external thread

- (void)start { [self.lock lock]; if ([self isCancelled]) { [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread]  withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } else if ([self isReady]) { self.state = AFOperationExecutingState; [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]]; } [self.lock unlock]; }Copy the code

When need the background threads execute tasks, AFNetworking by calling [NSObject performSelector: onThread:..] Throw the task into the RunLoop of the background thread

Second, NSTimer problem

In daily development, lists often use NSTimer countdown questions, or Interview will be asked by the interviewer: is NSTimer on time? Today on the launch of about the reasons and the scheme, and finally about the derivation of the problem cycle quote! Try to solve the problem of NSTimer once and for all?

Problem 1, NSTimer is not accurate

why

  • NSTimer is added to mainRunloop in NSDefaultRunLoopMode, MainRunloop is responsible for all main thread events, such as UI operations, that cause the current Runloop to last longer than the timer interval, and the next timer will be delayed, causing a timer block

  • Mode switch. When the timer created is added to NSDefaultRunLoopMode, the mode of runloop will be switched to TrackingRunloopMode when sliding UIScrollView is performed, and tiEMR will stop the callback

The solution

  1. Change the Mode Mode to TrackingRunloopMode

  2. Create a timer in the child thread and perform timed task operations in the main thread, or create a timer in the child thread and perform timed task operations in the child thread. Switch to the main thread when UI operations are required

  3. GCD operations: dispatch_source_create and depatch_resume

Plan a

There are two main Runloop modes used in the main thread, NSDefaultRunLoopMode and TrackingRunloopMode

Add timer to main thread CommonMode

[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
Copy the code

Scheme 2

The child thread creates a timer, the main thread executes a timer or the child thread creates a timer, and when the child thread executes a timer, it needs to refresh to the main thread

The child thread starts NSTimer

__weak __typeof(self) weakSelf = self; dispatch_async(dispatch_get_global_queue(0, 0), ^{ __strong __typeof(weakSelf) strongSelf = weakSelf; if (strongSelf) { strongSelf.countTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:strongSelf selector:@selector(countDown) userInfo:nil repeats:YES]; NSRunLoop *runloop = [NSRunLoop currentRunLoop]; [runloop addTimer:strongSelf.countTimer forMode:NSDefaultRunLoopMode]; [runloop run]; }});Copy the code

The main thread updates the UI

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^ {[self jumpBTN setTitle: [nsstrings stringWithFormat: @ "skip % typical vmlinux.lds", (long) the self. The count] forState: UIControlStateNormal]; });Copy the code

Plan 3

Use the GCD timer. The GCD timer is tied directly to the kernel and does not rely on RunLoop, so it is very punctual.

dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL); Dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // Set the time (start: execute after a few seconds; Uint64_t start = 2.0; // uint64_t interval = 1.0; // Execute dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC) every 1s. interval * NSEC_PER_SEC, 0); // Dispatch_source_set_event_handler (timer, ^{NSLog(@"%@",[NSThread currentThread])); }); // Start timer dispatch_resume(timer); NSLog(@"%@",[NSThread currentThread]); self.timer = timer;Copy the code

NSTimer circular reference

Common sense

These three methods directly add the timer to the current runloop default mode without us doing it ourselves. Of course, the cost of this is that the runloop can only be the current runloop and the mode is default mode:

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

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

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

The following five creations are not automatically added to the runloop and require a call to addTimer:forMode:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;  + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo; - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui  repeats:(BOOL)rep; - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;Copy the code

In real project development, NSTimer was used to solve the need for timed delivery of tasks, but circular references were still generated. Today I will describe the solution in this project.

Circular references refer to two objects that are strongly referenced to each other and cannot be released on time, resulting in memory leaks. As follows:

It is found that the two references to each other, and neither can be released, causing a circular reference

Option 1: Add middleware to Self

Self strongly references NSTimer. NSTimer strongly references proxy. Proxy weakly references self. The method used in this project is to introduce the intermediate control HCCProxy1

Define an intermediate proxy object HCCProxy1 that inherits from NSObject. Instead of holding a timer, the ViewController holds an instance of HCCProxy1. Let the instance weakly reference the ViewController. Timer strongly references HCCProxy1 instance as follows:

@interface HCCProxy1 : NSObject
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation HCCProxy1
+ (instancetype)proxyWithTarget:(id)target{
    HCCProxy1 *proxy = [[HCCProxy1 alloc] init];
    proxy.target = target;
    return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}
@end
Copy the code

The following are used in the project:

- (void)viewDidLoad { [super viewDidLoad]; The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: [HCCProxy1 proxyWithTarget: self] selector:@selector(timerTest) userInfo:nil repeats:YES]; }Copy the code

Development:

- (id) forwardingTargetForSelector: (SEL) aSelector is what? Message forwarding simply means that if the current object does not implement the method, the system will look for the implementation object in the method. In this article, because the current target is HCCProxy1, but HCCProxy1 has no implementation method (of course, it does not need to implement it), the system is asked to find the target instance method implementation, that is, to find the ViewController method implementation.Copy the code

Scheme 2. Use message forwarding inherited from NSProxy class HCCProxy

@interface HCCProxy : NSProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end@implementation HCCProxy + (instancetype)proxyWithTarget:(id)target{ Because it doesn't have an init method HCCProxy *proxy = [HCCProxy alloc]; proxy.target = target; return proxy; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ return [self.target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation{ [invocation invokeWithTarget:self.target]; } @endCopy the code

The following are used in the project:

- (void)viewDidLoad { [super viewDidLoad]; The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: [HCCProxy proxyWithTarget: self] selector:@selector(timerTest) userInfo:nil repeats:YES]; }Copy the code

Three, monitoring is stuck

The main problem is the failure of the main thread to respond to user interaction. If an App gives you a delay from time to time, and sometimes does not respond for a long time, will you continue to use it? The answer, of course, is obvious

For iOS development, monitoring caton is to find out what the main thread is doing. The thread’s message events depend on NSRunloop, so you can start with NSRunloop to know what methods are called on the main thread. You can listen for the state of the NSRunloop to see if the calling method is taking too long to execute and thus determine if there is a stutter. Therefore, the recommended solution is to monitor the status of Runloop to determine whether the lag occurs

Let’s take a look at the basics of Runloop

1. Knowledge -Runloop principle

The purpose of Runloop is to keep the thread busy when there is something to do and to put it to sleep when there are no events to do. Here is the CFRunloop source to share the principle of Runloop

The first step:

Notify observers: Runloop that they are about to start entering the loop, followed by loop, as follows:

// notify observers if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); / / into the loop result = __CFRunLoopRun (rl, currentMode, seconds, returnAfterSourceHandled, previousMode);Copy the code

The second step

Start a do while to keep the thread alive. Notifying Observers: RunLoop will trigger Timer callbacks, Source0 callbacks, and the added blocks.

// Notify Observers RunLoop to trigger Timer callback if (currentMode->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); // Notify Observers RunLoop to trigger the Source0 callback if (currentMode->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); // Execute block __CFRunLoopDoBlocks(runloop, currentMode);Copy the code

Next, the Source0 callback is fired, and if Source1 is ready, it jumps to HANDLE_MSG to process the message

if (MACH_PORT_NULL ! = dispatchPort ) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; }Copy the code

The third step

Once the callback is fired, notify Observers: RunLoop threads that they are in a sleep state.

Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); if (! poll && (currentMode->_observerMask & kCFRunLoopBeforeWaiting)) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); }Copy the code

The fourth step

After hibernation, it waits for a message from mach_port to wake up again. It will be awakened again only when the following four events occur:

  • Port-based Source events;
  • Timer Indicates that the time is up.
  • RunLoop timeout;
  • The caller wakes up.

The code waiting to wake up is as follows:

do { __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {// Port based Source event, caller wake up if (modeQueuePort! = MACH_PORT_NULL && livePort == modeQueuePort) { break; } // If (currentMode->_timerFired) {break; } } while (1);Copy the code

Step 5

When awakened, notify the Observer that the thread of RunLoop has just been awakened. The following code

if (! poll && (currentMode->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);Copy the code

Step 6

The RunLoop is woken up to start processing the message:

  • If the Timer time is up, the Timer callback is triggered.

  • If it is dispatch, block is executed;

  • If it is a source1 event, this event is processed.

After the message is executed, the block added to the loop is executed. The code is as follows:

handle_msg: // If the Timer runs out, If (msg-IS-timer) {__CFRunLoopDoTimers(runloop, currentMode, Mach_absolute_time ())} block else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } // Source1 event, Will deal with this event else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort (runloop currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); }}Copy the code

Step 7

Determine whether to proceed to the next loop based on the current RunLoop state. When an external force stops or loop times out, the next loop is not continued; otherwise, the next loop continues. The code is as follows:

If (sourceHandledThisLoop && stopAfterHandle) {/ / events have been processed retVal = kCFRunLoopRunHandledSource; } else if (timeout) {// timeout retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) {retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {retVal = kCFRunLoopRunFinished; }Copy the code

The full internal code is as follows:

Void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); } // start with the specified Mode, RunLoop timeout allowed int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) { return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); } int CFRunLoopRunSpecific(RunLoop, modeName, seconds, CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false); // If there is no source/timer/observer in mode, return it directly. if (__CFRunLoopModeIsEmpty(currentMode)) return; /// 1. Notify Observers: RunLoop is about to enter loop. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry); // the internal function, Loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO; int retVal = 0; Do {/// 2. Notify Observers: RunLoop is about to trigger a Timer callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); /// 3. notify Observers: RunLoop is about to trigger the Source0 (non-port) callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); // Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); /// 4. RunLoop triggers the Source0 (non-port) callback. sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle); // Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); /// 5. If there is a port based Source1 in ready state, process the Source1 directly and jump to the message. if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg) if (hasMsg) goto handle_msg; } /// Notify Observers: threads of RunLoop are about to enter sleep. if (! sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } /// 7. Call mach_msg and wait for the message to accept mach_port. The thread will sleep until it is awakened by one of the following events. /// • A port-based Source event. __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) { mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg } /// 8. Notify Observers: The threads of RunLoop have just been awakened. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting); // the message is received and processed. Handle_msg: /// 9.1 If a Timer runs out, trigger the Timer's callback. if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, Mach_absolute_time ())} // 9.2 If there are blocks dispatched to main_queue, execute the block. else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } /// 9.3 If a Source1 (port based) emits an event, To deal with this event else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort (runloop currentMode, livePort); sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg); if (sourceHandledThisLoop) { mach_msg(reply, MACH_SEND_MSG, reply); } // execute the block __CFRunLoopDoBlocks(runloop, currentMode) added to the Loop; If (sourceHandledThisLoop && stopAfterHandle) {/// return when the loop is finished. retVal = kCFRunLoopRunHandledSource; RetVal = kCFRunLoopRunTimedOut;} else if (timeout) {retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(runloop)) {retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(runloop, RetVal = kCFRunLoopRunFinished; } // If there is no timeout, mode is not available and loop is not stopped, continue loop. } while (retVal == 0); } /// 10. Notify Observers: RunLoop is about to exit. __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); }Copy the code

RunLoop is actually just such a function, with a do-while loop inside. When you call CFRunLoopRun(), the thread stays in the loop forever; This function does not return until it times out or is stopped manually.

The whole Runloop process can be summarized in the following image

2. How to monitor caton

To listen on the RunLoop, you first need to create a CFRunLoopObserverContext observer as follows:

CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities,YES,0,&runLoopObserverCallBack,&context);
Copy the code

Add the created runLoopObserver to the common mode of the main RunLoop. A persistent child thread is then created specifically to monitor the RunLoop status of the main thread.

Once it is found that the state of kCFRunLoopBeforeSources before entering sleep or the state of kCFRunLoopAfterWaiting after awakening does not change within the set time threshold, it can be judged as being stuck. We can then dump the stack information to further analyze which methods are taking too long.

The code to enable the monitoring of a child thread is as follows:

Dispatch_async (dispatch_get_global_queue(0, 0)) While (YES) {long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore) dispatch_time(DISPATCH_TIME_NOW, 3 * 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

Below is a packaged utility class, HCCMonitor, for caton monitoring

#import <Foundation/Foundation.h> @interface HCCMonitor : NSObject + (instancetype)shareInstance; - (void)beginMonitor; // Start monitoring caton - (void)endMonitor; H "#import" hccCallstack. h" #import "hcccpumonitor. h" @interface HCCMonitor() {int timeoutCount; CFRunLoopObserverRef runLoopObserver; @public dispatch_semaphore_t dispatchSemaphore; CFRunLoopActivity runLoopActivity; } @property (nonatomic, strong) NSTimer *cpuMonitorTimer; @end @implementation HCCMonitor #pragma mark - Interface + (instancetype)shareInstance { static id instance = nil; static dispatch_once_t dispatchOnce; dispatch_once(&dispatchOnce, ^{ instance = [[self alloc] init]; }); return instance; } - (void) beginMonitor {/ / the self monitoring CPU consumption. CpuMonitorTimer = [NSTimer scheduledTimerWithTimeInterval: 3 target: the self selector:@selector(updateCPUInfo) userInfo:nil repeats:YES]; If (runLoopObserver) {return; } dispatchSemaphore = dispatch_semaphore_create(0); CFRunLoopObserverContext Context = {0,(__bridge void*)self,NULL,NULL}; runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes); // Add the observer to the observation in common mode of the main runloop. Dispatch_async (dispatch_get_global_queue(0, 0)) While (YES) {long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore) dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC)); if (semaphoreWait ! = 0) { if (! runLoopObserver) { timeoutCount = 0; dispatchSemaphore = 0; runLoopActivity = 0; return; } // The state of the two runloops, BeforeSources and AfterWaiting can detect whether the two state interval time caton the if (runLoopActivity = = kCFRunLoopBeforeSources | | runLoopActivity = = KCFRunLoopAfterWaiting) {if (++timeoutCount < 3) {continue; // } NSLog(@"monitor trigger"); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // [HCCCallStack callStackWithType:HCCCallStackTypeAll]; }); } //end activity }// end semaphore wait timeoutCount = 0; }// end while }); } - (void)endMonitor { [self.cpuMonitorTimer invalidate]; if (! runLoopObserver) { return; } CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes); CFRelease(runLoopObserver); runLoopObserver = NULL; } #pragma mark - Private static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){ HCCMonitor *lagMonitor = (__bridge HCCMonitor*)info; lagMonitor->runLoopActivity = activity; dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore; dispatch_semaphore_signal(semaphore); } - (void)updateCPUInfo { thread_act_array_t threads; mach_msg_type_number_t threadCount = 0; const task_t thisTask = mach_task_self(); kern_return_t kr = task_threads(thisTask, &threads, &threadCount); if (kr ! = KERN_SUCCESS) { return; } for (int i = 0; i < threadCount; i++) { thread_info_data_t threadInfo; thread_basic_info_t threadBaseInfo; mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX; if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) { threadBaseInfo = (thread_basic_info_t)threadInfo; if (! (threadBaseInfo->flags & TH_FLAGS_IDLE)) { integer_t cpuUsage = threadBaseInfo->cpu_usage / 10; If (cpuUsage > 70) {NSString *reStr = HCCStackOfThread(threads[I]); / / record in the database / / [[[HCCDB shareInstance] increaseWithStackString: reStr] subscribeNext: ^ (id) x {}]. NSLog(@"CPU useage overload Thread stack: \n%@",reStr); } } } } } @endCopy the code

Fourth, performance optimization

When the tableView cell has multiple imageViews, and the image is large, it will cause a lag when sliding.

Using Runloop, we can add images one at a time.

/* Why it should be optimized: Runloop draws all the points on the screen in one loop. Loading too many images will result in too many points needing to be drawn, making the loop take too long and causing the UI to get stuck. * /Copy the code

Listening to the Runloop

// Add a runloop listener - (void)addRunloopObserver{// Get the current runloop ref - pointer CFRunLoopRef current = CFRunLoopGetCurrent(); // Define a RunloopObserver CFRunLoopObserverRef defaultModeObserver; // context /* typedef struct {CFIndex version; // Version number long void * info; Const void *(*retain)(const void *info); // fill in &cfretain void (*release)(const void *info); // fill in &cgfrelease CFStringRef (*copyDescription)(const void *info); //NULL } CFRunLoopObserverContext; */ CFRunLoopObserverContext context = { 0, (__bridge void *)(self), &CFRetain, &CFRelease, NULL }; NULL 2 mode kCFRunLoopEntry = (1UL << 0), kCFRunLoopBeforeTimers = (1UL << 1), kCFRunLoopBeforeSources = (1UL << 2), kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 7), KCFRunLoopAllActivities = 0x0FFFFFFFU 3 Repeat - YES 4 nil or NSIntegerMax - 999 5 callback 6 Context */ // Create observer defaultModeObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, NSIntegerMax - 999, &Callback, &context); // Add the current runloop to observe CFRunLoopAddObserver(current, defaultModeObserver, kCFRunLoopDefaultMode); / / release CFRelease (defaultModeObserver); } @endCopy the code

The callback method

static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, ZXYRunloop * runloop = (__bridge ZXYunloop *)info; If (runloop.tasks.count == 0) {return; } BOOL result = NO; While (result = = NO && runloop. Tasks. Count) {/ / remove the task RunloopBlock unit = runloop. Tasks. FirstObject; // Execute task result = unit(); // Delete the task [runloop.tasks removeObjectAtIndex:0]; }}Copy the code

Using the above two methods, we can listen for the Runloop loop and what needs to be done each time. In this case, we only need to provide a method to add tasks, which is stored in an array.

// Add task add task - (void)addTask:(RunloopBlock)unit withId:(id)key{// Add task to array [self.tasks addObject:unit]; [self.taskKeys addObject:key]; If (self.tasks. Count > self.maxQueue) {[self.tasks removeObjectAtIndex:0]; [self.taskKeys removeObjectAtIndex:0]; }Copy the code

Set the initialization object and basic information in the ZXYRunloop initialization method

- (instancetype)init{ self = [super init]; If (self) {// Initialize the object/basic information self.maxQueue = 20; self.tasks = [NSMutableArray array]; self.taskKeys = [NSMutableArray array]; The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 0.001 repeats: YES block: ^ (NSTimer * _Nonnull timer) {}]. // addRunloopObserver [self addRunloopObserver]; } return self; }Copy the code

Use in TableViewCell:

[[ZXYRunloop shareInstance] addTask:^BOOL{
        [ViewController addCenterImg:cell];
        return YES;
    } withId:indexPath];
Copy the code

Sum up the idea

  1. The code for loading images is saved, not executed directly, as an array of blocks

  2. Listen to our Runloop CFRunloop CFRunloopObserver

  3. Each time the Runloop tells it to load an image or other task from the array

The above scenarios may be used in the project, I hope to help you and improve! I will update my blog regularly in the future, and I will be more professional. Welcome to like and pay attention to me, thank you!!