Application of RunLoop in iOS development

Application of RunLoop in iOS development

summary

The application range of RunLoop in iOS development is not as extensive as runtime. From the source code of CFRuntime, we can see that runloop and thread are inseparable. A thread will create a corresponding runloop, but the main thread will automatically run when it is created. The child thread will only be created and will not run automatically. Apple Thread Management also talked about using runloop in threads.

  In addition, runloop is not a simple do-while, as an event loop performance in OSX/iOS system, runloop needs to process message events, sleep when there is no message, and wake up immediately when there is a message event. In summary, from my personal knowledge, runloop first deals with sub-thread running, and the other deals with problems according to different activities of runloop. Of course, I hope that through my brick, I will lead the students to use runloop.

1. CFRunLoopSourceRef event source

In the following code, by customizing the sub-thread thread, the running result shows that hello China will not be printed, and the sub-thread will exit after printing hello world.

{
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadFun) object:nil];
    [thread start];
    self.thread = thread;
    [self performSelector:@selector(selectorFun) onThread:thread withObject:nil waitUntilDone:NO];
    NSLog(@"hello Thread");
}

-(void)threadFun {
    NSLog(@"hello world");
   //_pthread_exit
}

-(void)selectorFun {
    NSLog(@"hello China");
}

Obtaining the stack of the above code, you can see that the life of the child thread ends in an instant. Similar to adding _pthread_exit before the end of the threadFun function block

    frame #0: 0x000000010232f2b0 CoreFoundation`__CFFinalizeRunLoop
    frame #1: 0x000000010232f264 CoreFoundation`__CFTSDFinalize + 100
    frame #2: 0x0000000104e9f39f libsystem_pthread.dylib`_pthread_tsd_cleanup + 544
    frame #3: 0x0000000104e9f0d9 libsystem_pthread.dylib`_pthread_exit + 152
    frame #4: 0x0000000104e9fc38 libsystem_pthread.dylib`pthread_exit + 30
    frame #5: 0x0000000101a36f1e Foundation`+[NSThread exit] + 11
    frame #6: 0x0000000101ab713f Foundation`__NSThread__start__ + 1218
    frame #7: 0x0000000104e9d93b libsystem_pthread.dylib`_pthread_body + 180
    frame #8: 0x0000000104e9d887 libsystem_pthread.dylib`_pthread_start + 286
    frame #9: 0x0000000104e9d08d libsystem_pthread.dylib`thread_start + 13

According to Apple's thread management, you can use the thread to be put into the runloop. We know that the runloop of the child thread is not automatically turned on, and we need to manually turn it on. Apple also provides code examples:

-(void)threadMainRoutine
{
    BOOL moreWorkToDo = YES;
    BOOL exitNow = NO;
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
   //Add the exitNow BOOL to the thread dictionary.
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
   //Install an input source.
    [self myInstallCustomInputSource];
    while (moreWorkToDo && !exitNow)
    {
       //Do one chunk of a larger body of work here.
       //Change the value of the moreWorkToDo Boolean when done.
       //Run the run loop but timeout immediately if the input source isn't waiting to fire.
        [runLoop runUntilDate:[NSDate date]];
       //Check to see if an input source handler changed the exitNow value.
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
    
}

So we can modify our above code to, the program can print out hello China

-(void)threadFun {
     NSLog(@"hello world");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate distantFuture]];
   //_pthread_exit
}

We can modify the code to add a button click event to the interface. The click event comes out of our child thread. At the same time, we delete the selectorFun function logic of our thread. We found that the click event of our trigger button does not print doSomething.

{
 UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(0, 80, 50, 50)];
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(clicked) forControlEvents:UIControlEventTouchUpInside];
 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadFun) object:nil];
    [thread start];
    self.thread = thread;
}
-(void)threadFun {
    NSLog(@"hello world");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop runUntilDate:[NSDate distantFuture]];
   //_pthread_exit
}
-(void)clicked{
    [self performSelector:@selector(doSomething) onThread:self.thread withObject:nil waitUntilDone:NO];
}

-(void)doSomething{
    NSLog(@"doSomething");
}

Because the current runloop running model does not have a modeItem, the prerequisite for run running must ensure that the current model has an item (Source/Timer, one of the two, actually does not require Observer). Modify the code to the following:

-(void)threadFun {
    NSLog(@"hello world");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop runUntilDate:[NSDate distantFuture]];
}

RunLoop only handles two sources: input source and time source. The input source can be divided into: NSPort/custom source/performSelector, the performSelector methods we commonly use are:

//main thread
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
///Specify thread
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
///For the current thread
performSelector:withObject:afterDelay:         
performSelector:withObject:afterDelay:inModes:
///Cancel, in the current thread, corresponding to the above two methods
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

And the following are not the source of the event, they are equivalent to calling [self xxx]

-(id)performSelector:(SEL)aSelector;
-(id)performSelector:(SEL)aSelector withObject:(id)object;
-(id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

The run operation function is mainly the following 3

-(void)run;
-(void)runUntilDate:(NSDate *)limitDate;
-(BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

Once the first run loop is opened, it cannot be closed, and the subsequent code cannot be executed. It is mentioned in the api document: If no input source and timing source are added to the runloop, the runloop will exit immediately, otherwise the runloop will run in the NSDefaultRunLoopMode mode by frequently calling the -runMode:beforeDate: method. The second run runs in NSDefaultRunLoopMode mode and has a timeout limit. It actually keeps calling the -runMode:beforeDate: method to make the runloop run in NSDefaultRunLoopMode mode until the timeout period is reached. Calling CFRunLoopStop(runloopRef) cannot stop the Run Loop. This method will only end the current -runMode:beforeDate: call, and the subsequent runMode:beforeDate: call will continue. Until timeout. correspond

CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)

The third run is better than the second method in that you can specify the running mode, execute it only once, and exit after execution. You can use CFRunLoopStop (runloopRef) to exit runloop. It is mentioned in the api document: Runloop exits after the first input source (non-timer) is processed or reaches the limitDate, corresponding

CFRunLoopRunInMode(mode,limiteDate,true)

1.1 The child thread is resident

Add an event source to the runbloop mode of the current child thread to achieve thread resident. All about this will take the code of AF2.X to illustrate this resident case. If the student develops iOS a little bit older or antique code will use the network third-party library ASIHTTPRequest, and also use CFRunLoopAddSource to make the current network thread permanent.

 + (NSThread *)threadForRequest:(ASIHTTPRequest *)request
{
    if (networkThread == nil) {
        @synchronized(self) {
            if (networkThread == nil) {
                networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequests) object:nil];
                [networkThread start];
            }
        }
    }
    return networkThread;
}

+ (void)runRequests
{
   //Should keep the runloop from exiting
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

    BOOL runAlways = YES;//Introduced to cheat Static Analyzer
    while (runAlways) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        CFRunLoopRun();
        [pool release];
    }

   //Should never be called, but anyway
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

1.2 The program crash pop-up prompt

This is because I actually came into contact with runloop. When the user is operating our APP, the data will be abnormal, and the program will crash instantly. In fact, it is a very bad experience from the product point of view, but it is a very bad experience for coders. It is said that it is impossible to know the stack information of the current program crash. By using the thread resident method of runloop, when an exception occurs in the program, the exception is captured and a prompt box pops up instead of crashing immediately. At the same time, the user can also upload the crash log. In the early days, I still saw APP using this kind of technology. Now the crash collection mechanism is getting more and more perfect. At present, it is almost used in this way.

-(void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex
{
    if (anIndex == 0)
    {
        dismissed = YES;
    }else if (anIndex==1) {
        NSLog(@"ssssssss");
    }
}

-(void)handleException:(NSException *)exception
{
    [self validateAndSaveCriticalApplicationData];
    
    UIAlertView *alert =
    [[[UIAlertView alloc]
      initWithTitle:NSLocalizedString(@"Sorry, there was an exception in the program", nil)
      message:[NSString stringWithFormat:NSLocalizedString(
                                                           @"If you click to continue, there may be other problems in the program, it is recommended that you click the exit button and reopen\n\n"
                                                           @"The reason for the exception is as follows:\n%@\n%@", nil),
               [exception reason],
               [[exception userInfo] objectForKey:UncaughtExceptionHandlerAddressesKey]]
      delegate:self
      cancelButtonTitle:NSLocalizedString(@"Exit", nil)
      otherButtonTitles:NSLocalizedString(@"Continue", nil), nil]
     autorelease];
    [alert show];
    
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
    
    while (!dismissed)
    {
        for (NSString *mode in (NSArray *)allModes)
        {
            CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
        }
    }
    
    CFRelease(allModes);
    
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
    
    if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
    {
        kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
    }
    else
    {
        [exception raise];
    }
}

2 CFRunLoopObserverRef

The iOS system will monitor the enter/sleep and exit activities of the runloop in the main thread to process the autoreleasepool, which is also the question of when the autorelease pool will be released.

 <CFRunLoopObserver 0x7fb064418b50 [0x10e005a40]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e18c4c2), context = <smallArray 0x7fb0610189e = mutable = mu40] 0, values ​​= ()))
<CFRunLoopObserver 0x7fb064418bf0 [0x10e005a40]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e18c4c2), context = <CFArray 0x7fb0644189-type = 040] , values ​​= ()))

2. 1 CFRunLoopObserverRef function

Through CFRunLoopObserverRef we can monitor the running status of the current runloop. Reference YYKit's writing: the priority is set to the smallest 32-bit-0x7fffffff and the largest 32-bit 0x7fffffff

static void YYRunloopAutoreleasePoolSetup() {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    
    CFRunLoopObserverRef pushObserver;
    pushObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopEntry,
                                           true,//repeat
                                           -0x7FFFFFFF,//before other observers
                                           YYRunLoopAutoreleasePoolObserverCallBack, NULL);
    CFRunLoopAddObserver(runloop, pushObserver, kCFRunLoopCommonModes);
    CFRelease(pushObserver);
    
    CFRunLoopObserverRef popObserver;
    popObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                          true,//repeat
                                          0x7FFFFFFF,//after other observers
                                          YYRunLoopAutoreleasePoolObserverCallBack, NULL);
    CFRunLoopAddObserver(runloop, popObserver, kCFRunLoopCommonModes);
    CFRelease(popObserver);
}

The other is the block method

//Create observer
  CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----Monitoring the status change of RunLoop---%zd", activity);
    });
   //Add observer: monitor the status of RunLoop
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);   
   //Release Observer
    CFRelease(observer);

2.2 Use idle time to cache data

Sunnyxx, the author of UITableView+FDTemplateLayoutCell, used to optimize the height calculation of UITableViewCell. mentioned the use of runloop to cache the height of the cell .

The author said the code is as follows:

But this code was removed after version 1.4. Sunnyxx explained:

2.3 Detect UI freeze

The first method monitors the runLoop of the main thread through the sub-thread, and judges whether the time-consuming between the two state areas reaches a certain threshold. ANREye sets the flag to YES in the child thread, and then sets the flag to NO in the main thread. Use the threshold duration of the child thread to determine whether the flag bit is successfully set to NO.

private class AppPingThread: Thread {
    
    func start(threshold:Double, handler: @escaping AppPingThreadCallBack) {
        self.handler = handler
        self.threshold = threshold
        self.start()
    }
    
    override func main() {
        
        while self.isCancelled == false {
            self.isMainThreadBlock = true
            DispatchQueue.main.async {
                self.isMainThreadBlock = false
                self.semaphore.signal()
            }
            
            Thread.sleep(forTimeInterval: self.threshold)
            if self.isMainThreadBlock {
                self.handler?()
            }
            
            self.semaphore.wait(timeout: DispatchTime.distantFuture)
        }
    }
    
    private let semaphore = DispatchSemaphore(value: 0)
    
    private var isMainThreadBlock = false
    
    private var threshold: Double = 0.4
    
    fileprivate var handler: (() -> Void)?
}

The NSRunLoop call method is mainly between kCFRunLoopBeforeSources and kCFRunLoopBeforeWaiting, and after kCFRunLoopAfterWaiting, that is, if we find that these two times are too long, then we can determine that the main thread is stuck at this time. The following code snippets are sourced from iOS real-time Caton monitoring

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    MyClass *object = (__bridge MyClass*)info;
    
   //record status value
    object->activity = activity;
    
   //send signal
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);
}
-(void)registerObserver
{
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                                            kCFRunLoopAllActivities,
                                                            YES,
                                                            0,
                                                            &runLoopObserverCallBack,
                                                            &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
   //Create signal
    semaphore = dispatch_semaphore_create(0);
    
   //Monitor the duration in the child thread
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
           //Assuming that 5 consecutive timeouts of 50ms are considered to be stuck (of course, it also includes a single timeout of 250ms)
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                    if (++timeoutCount <5)
                        continue;
                    
                    NSLog(@"It seems a bit stuck");
                }
            }
            timeoutCount = 0;
        }
    });

The second method is FPS monitoring. App refresh rate should be kept at 60fps, and the current FPS can be calculated by recording the interval between two refreshes through CADisplayLink.

 _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkTick:)];
  [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

-(void)displayLinkTick:(CADisplayLink *)link {
    if (lastTime == 0) {
        lastTime = link.timestamp;
        return;
    }
    count++;
    NSTimeInterval interval = link.timestamp-lastTime;
    if (interval <1) return;
    lastTime = link.timestamp;
    float fps = count/interval;
    count = 0;
  NSString *text = [NSString stringWithFormat:@"%d FPS",(int)round(fps)];

3 CFRunLoopModeRef

Each time you start RunLoop, you can only specify one of the Modes, and this is CurrentMode. To switch Mode, you can only exit Loop and re-designate a Mode to enter. The system registers 5 modes by default, the following two are more commonly used: 1.kCFRunLoopDefaultMode (NSDefaultRunLoopMode), the default mode 2.UITrackingRunLoopMode, scrollview is in this mode when sliding. Ensure that the interface is not affected by other modes when sliding. CFRunLoop exposes only the following two management Mode interfaces:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
struct __CFRunLoop {
    CFMutableSetRef _commonModes;//Set
    CFMutableSetRef _commonModeItems;//Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;//Current Runloop Mode
    CFMutableSetRef _modes;//Set
    ...
};

3.1 Solve the entanglement between NSTime and scrollView

If you use the scrollView type of automatic advertising scroll bar, you need to add the timer to the current runloop mode NSRunLoopCommonModes

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    if (self.autoScroll) {
        [self invalidateTimer];
    }
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (self.autoScroll) {
        [self setupTimer];
    }
}
 (void)setupTimer
{
    [self invalidateTimer]; 
    
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:self.autoScrollTimeInterval target:self selector:@selector(automaticScroll) userInfo:nil repeats:YES];
    _timer = timer;
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

-(void)invalidateTimer
{
    [_timer invalidate];
    _timer = nil;
}

3.2 RunLoopCommonModes

A mode can be marked as the common attribute (using the CFRunLoopAddCommonMode function), and then it will be saved in _commonModes. The main thread has kCFRunLoopDefaultMode and UITrackingRunLoopMode, which are already CommonModes, and the child thread has only kCFRunLoopDefaultMode.

The source, observer, timer, etc. stored in _commonModeItems will be synchronized to the Modes with the Common flag each time the runLoop runs. For example: [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; is to put the timer in the commonItem.

kCFRunLoopCommonModes is a mode for placeholders, it is not a real mode. If you want to open the runloop in the thread, it is wrong to write like this:

[[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];

The runMode beforeDate above calls the CFRunLoopRunSpecific function of CFrunloop. In the function, the current operation mode is found according to the current name, but there is no CommonMode at all.

image.png

3.3 Implement smooth scrolling and delay loading pictures in TableView

By the way, I didn't use this in development. It is to use the characteristics of CFRunLoopMode to load the picture into the mode of NSDefaultRunLoopMode, so that it will not be loaded and affected when scrolling the mode of UITrackingRunLoopMode. This is mainly affected by Github's RunLoopWorkDistribution ,

DWURunLoopWorkDistribution_demo.gif

The main code snippets are as follows:

-(instancetype)init
{
    if ((self = [super init])) {
        _maximumQueueLength = 30;
        _tasks = [NSMutableArray array];
        _tasksKeys = [NSMutableArray array];
        _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(_timerFiredMethod:) userInfo:nil repeats:YES];
    }
    return self;
}
static void _registerObserver(CFOptionFlags activities, CFRunLoopObserverRef observer, CFIndex order, CFStringRef mode, void *info, CFRunLoopObserverCallBack callback) {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopObserverContext context = {
        0,
        info,
        &CFRetain,
        &CFRelease,
        NULL
    };
    observer = CFRunLoopObserverCreate( NULL,
                                            activities,
                                            YES,
                                            order,
                                            callback,
                                            &context);
    CFRunLoopAddObserver(runLoop, observer, mode);
    CFRelease(observer);
}

static void _runLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    DWURunLoopWorkDistribution *runLoopWorkDistribution = (__bridge DWURunLoopWorkDistribution *)info;
    if (runLoopWorkDistribution.tasks.count == 0) {
        return;
    }
    BOOL result = NO;
    while (result == NO && runLoopWorkDistribution.tasks.count) {
        DWURunLoopWorkDistributionUnit unit = runLoopWorkDistribution.tasks.firstObject;
        result = unit();
        [runLoopWorkDistribution.tasks removeObjectAtIndex:0];
        [runLoopWorkDistribution.tasksKeys removeObjectAtIndex:0];
    }
}
Reference: https://cloud.tencent.com/developer/article/1438304 RunLoop application in iOS development-Cloud + Community-Tencent Cloud