In the process of application development, we often need to countdown in many places, such as the wheel map, verification code, activity countdown and so on. When implementing these functions, we often encounter many pits that need to be carefully avoided. Because of the content of the article, you are required to have some basic knowledge of Runloop, of course, if not, there is no major problem. I recommend this article by Ibireme.

Without further ado, to the point:

Types of countdown

During development, we basically only used these methods to implement the countdown

  1. PerformSelecter
  2. NSTimer
  3. CADisplayLink
  4. GCD

PerformSelecter

We can use the following code to implement the specified delay after execution:

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;Copy the code

Its methods are described below

Invokes a method of the receiver on the current thread using the default mode after a delay. This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.  If you want the message to be dequeued when the run loop is in a mode other than the default mode, use the performSelector:withObject:afterDelay:inModes: method instead. If you are not sure whether the current thread is the main thread, you can use the performSelectorOnMainThread:withObject:waitUntilDone: or performSelectorOnMainThread:withObject:waitUntilDone:modes:method to guarantee that your selector executes on the main thread. To cancel a queued message, use the cancelPreviousPerformRequestsWithTarget: or cancelPreviousPerformRequestsWithTarget:selector:object:method.

This method is in the nsrunloop. h file in the Foundation framework. When we call NSObject, a Timer is created inside the runloop and added to the current thread’s Runloop. So if the current thread does not have a RunLoop, this method will fail. And there are several major drawbacks:

  • This method must be in NSDefaultRunLoopMode to run
  • Because it is implemented based on RunLoop, it can cause problems with accuracy. This problem arises with the other two methods as well, so we’ll discuss it in detail
  • Memory management is very problematic. When we do [self performSelector: AfterDelay :] increses self’s reference count by one, and after executing this method, increses self’s reference count by one. When the parent view is released, self’s reference count is not reduced to zero and dealloc cannot be called. There has been a memory leak.

Because it has so many flaws, we should not use it, or should not use it in the countdown method.

NSTimer

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo; + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE (macosx (10.12), the ios (10.0), watchos (3.0), tvos (10.0)); + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer * Timer))block API_AVAILABLE(MacOSx (10.12), ios(10.0), Watchos (3.0), TVOs (10.0));Copy the code

The method is described as follows

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object. Timers work in conjunction with run loops. Run loops maintain strong references to their timers, So you don’t have to maintain your own strong reference to a timer after you have added it to a run loop. To use a timer effectively, you should be aware of how run loops operate. See Threading Programming Guide for more information. A timer is not a Real-time mechanism. If a timer’s firing time occurs during a long run loop callout or while the run loop is in a mode that isn’t monitoring the timer, the timer doesn’t fire until the next time the run loop checks the timer. Therefore, the actual time at which a timer fires can be significantly later. See also Timer Tolerance. NSTimer is toll-free bridged with its Core Foundation counterpart, CFRunLoopTimerRef. See Toll-Free Bridging for more information.

This method is in the nstimer.h file in the Foundation framework. An NSTimer object can only be registered in one RunLoop, but can be added to multiple RunLoop modes. Ns trimmer is actually the CS frunlooptimer ref, which is toll-free Bridging between them. Its underlying layer is driven by the MK_timer of the XNU kernel. 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. In the file, the system provides a total of 8 methods, three of which are to directly add the timer to the current runloop DefaultMode, without our own operation, of course, the cost of this is that the runloop can only be the current runloop, mode is DefaultMode:

+ (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 other five methods, which are not automatically added to the RunLoop, also call 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

If we turn on NSTimer and it doesn’t run, we can check that RunLoop is running and running in the correct Mode

NSTimer and PerformSelecter have many similarities, for example, both creation and destruction must be on the same thread, memory management has the risk of leakage, accuracy issues. Let’s talk about the last two.

Memory leaks

When we use NSTimer, the RunLoop will force an NSTimer, which in turn holds a self target, and the controller holds an NSTimer object, creating a circular reference. Although the system provides an invalidate method to release NSTimer from the RunLoop and remove strong references, there is often no place to place it. The way we solve this problem is simply to initialize the NSTimer by replacing the target that fires the event with a separate object, and then the SEL method of the NSTimer that fires in that object will be implemented in the current view self. Use RunTime to dynamically create SEL method in target. Then target associates SEL with the current view self. When target executes SEL, extract the associated object self and tell self to execute the method. The implementation code is as follows:

.h
#import <Foundation/Foundation.h>@interface NSTimer (Brex) /** * Creates a Timer */ + for loop execution that does not cause loop references (instancetype)brexScheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo; @end .m#import "NSTimer+Brex.h"
@interface BrexTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation PltTimerTarget
- (void)brexTimerTargetAction:(NSTimer *)timer
{
    if(self. Target) {[self target performSelector: self. The selector withObject: timer afterDelay: 0.0]; }else {
        [self.timer invalidate];
        self.timer = nil;
    }
}
@end
@implementation NSTimer (Brex)
+ (instancetype)brexScheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo
{
    BrexTimerTarget *timerTarget = [[BrexTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    NSTimer *timer = [NSTimer timerWithTimeInterval:ti target:timerTarget selector:@selector(brexTimerTargetAction:) userInfo:userInfo repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    timerTarget.timer = timer;
    return timerTarget.timer;
}
@end
Copy the code

Of course, when it is really used, it still needs to be verified through testing.

Precision problem

As we mentioned above, NSTimer is not very accurate. NSTimer is not really a time mechanic. It only fires when added to the RunLoop. If the timer is not detected in one RunLoop, it is checked in the next RunLoop without delay. In other words, we can say: “This train is late. Let’s wait for the next one.” Also, sometimes RunLoop is doing a very demanding operation, such as iterating over a very, very large array, and you might “forget” to check the timer. The train is behind schedule. Of course, both of these cases actually behave as NS mer inaccuracies. Therefore, the actual timer is not the time set by the timer itself, but the time at which a RunLoop might be added. Also, NSRunLoop is not really thread-safe, and if NSTimer does not operate in a thread, it can trigger unexpected consequences.

Warning The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an NSRunLoop object running in a Different thread, as doing so might cause unexpected results. The NSRunLoop class is not generally considered thread-safe, and its methods should only be called in the current thread. You should not attempt to call a method of an NSRunLoop object that is running in a different thread, because doing so may result in unexpected results.

CADisplayLink

Create methods self. DisplayLink = [CADisplayLink displayLinkWithTarget: self selector: @ the selector (handleDisplayLink:)]; [self.displayLink addToRunLoop:[NSRunLoop currentRunLoop]forMode:NSDefaultRunLoopMode]; Stop method [self.displayLink invalidate]; self.displayLink = nil;Copy the code

CADisplayLink is a timer class that allows us to draw specific content onto the screen at a rate that keeps pace with the refresh rate of the screen. It is somewhat similar in implementation to NSTimer. The difference, however, is that runloop sends the specified selector message to CADisplayLink’s target every time the screen refreshes, whereas NSTimer registers with Runloop in the specified mode, and every time the specified period expires, The runloop sends the specified selector message once to the specified target. Of course, CADisplayLink suffers from precision problems for the same reasons as NSTimer, but CADisplayLink is a bit higher in terms of accuracy alone. And that’s what happens when you drop frames. We usually use it for continuous redrawing of the interface, such as video playback needs to constantly fetch the next frame for interface rendering, animation drawing and so on.

GCD

Finally, we get to the point: the GCD countdown

dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 10*NSEC_PER_SEC, 1*NSEC_PER_SEC); // Dispatch_source_set_event_handler (timer, ^{block executed when timer is triggered}); dispatch_resume(timer);Copy the code

To understand how GCD countdown works, it’s best to read the libDispatch source code. Of course, if you don’t want to read, just read on. The dispatch_source_create API allocates memory and initializes a ds structure of type dispatch_source_t, and then returns it.

Let’s examine these lines of code from the perspective of the underlying source code. The first is the dispatch_source_create function, which is similar to the create function we’ve seen before, and does some initialization of the dispatchsourcet object:

dispatch_source_t ds = NULL;  
ds = _dispatch_alloc(DISPATCH_VTABLE(source), sizeof(struct dispatch_source_s));  
_dispatch_queue_init((dispatch_queue_t)ds);  
ds->do_suspend_cnt = DISPATCH_OBJECT_SUSPEND_INTERVAL;  
ds->do_targetq = &_dispatch_mgr_q;  
dispatch_set_target_queue(ds, q);  
return ds;  
Copy the code

There are two queues involved, where Q is the queue specified by the user to indicate the queue on which the event-triggered callback is executed. _dispatch_mgr_q indicates which queue manages the source. MGR is short for manager.

Dispatch_source_set_timer,

void dispatch_source_set_timer(dispatch_source_t ds, dispatch_time_t start, uint64_t interval, uint64_t leeway) { ...... struct dispatch_set_timer_params *params; . dispatch_barrier_async_f((dispatch_queue_t)ds, params, _dispatch_source_set_timer2); }Copy the code

This code first filters and resets the parameter and then creates a pointer to dispatch_set_timer_params:

Struct dispatch_set_timer_params {dispatch_source_t ds; uintptr_t ident; struct dispatch_timer_source_s values; };Copy the code

The last call

dispatch_barrier_async_f((dispatch_queue_t)ds, params, _dispatch_source_set_timer2);  
Copy the code

Then call the _dispatch_source_set_timer2 method:

static void _dispatch_source_set_timer2(void *context) {  
    // Called on the source queue
    struct dispatch_set_timer_params *params = context;
    dispatch_suspend(params->ds);
    dispatch_barrier_async_f(&_dispatch_mgr_q, params,
            _dispatch_source_set_timer3);
}
Copy the code

Then the _dispatch_source_set_timer3 method is called:

static void _dispatch_source_set_timer3(void *context) { // Called on the _dispatch_mgr_q struct dispatch_set_timer_params *params = context; . _dispatch_timer_list_update(ds); . }Copy the code

The _dispatch_timer_LIST_update function sorts the timer according to the time of the next trigger.

Next, the block originally dispatched to the manager queue will be executed, going to the _dispatch_mgr_invoke function with the following code:

r = select(FD_SETSIZE, &tmp_rfds, &tmp_wfds, NULL, sel_timeoutp);
Copy the code

As you can see, the underlying GCD timer is implemented by the SELECT method in the XNU kernel. Those of you who are familiar with socket programming may be familiar with this method. This method can be used to deal with blocking, sticky packets, etc.

Because the method comes from the bottom up, the GCD countdown is the most accurate.

Is it possible to be inaccurate? The answer is maybe! Let’s look at a picture here

Concurrent Programming: APIs and Challenges


conclusion

If you don’t have very high requirements for time accuracy, such as the wheel map, you can choose to use NSTimer; To create animations and things like that, you can use CADisplayLink; To pursue high precision, you can use GCD countdown; As for Form Selecter, forget it.

practice

1. The round figure

I used to use a wheel view as a tableView’s headerView. When testing, we found a problem that everyone might encounter, when sliding the TableView rotation graph does not slide. The problem is easy to solve,

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

Change the mode of RunLoop.

2. Detection of FPS

I was inspired by the FPSLabel and expanded on it, just making a View that slides on the top of the page. It is mainly used for testing in debug mode. It shows the FPS of the page itself, App version number, iOS version number, phone model and so on. In general, we think that FPS between 55 and 60 is smooth, and below 55, we need to find problems and solve them. Of course, the addition of the view itself also affects the FPS of the current page. The observer effect.

3. Countdown of multiple events

I came across a requirement to implement multiple countdown cells on a TableView. I started with NSTimer one at a time, but later found that when I had more cells, the page became very sluggish. In order to solve this, I came up with a solution: I realized a countdown singleton, every 1 second will issue a corresponding block of pages (there are several pages), and a general notice, it contains only one current timestamp, and publicly opened the countdown and closed the countdown. Thus, a page can be implemented using only one countdown. Each cell only needs to hold a countdown to the end time. That’s when I started working on countdowns and even implemented a countdown singleton myself using the SELECT function.

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [[NSThread currentThread] setName:@"custom Timer"]; . fd_set read_fd_set; FD_ZERO(&read_fd_set); FD_SET(self.fdCustomTimerModifyWaitTimeReadPipe, &read_fd_set); struct timeval tv; tv.tv_sec = self.customTimerWaitTimeInterval; tv.tv_usec = 0; . long ret = select(self.fdCustomTimerModifyWaitTimeReadPipe + 1, &read_fd_set, NULL, NULL, &tv); / / core self. CustomTimerSelectTime = [[NSDate date] timeIntervalSince1970]; .if(ret == 0){
                NSLog(@"Select the timeout! \n");
                NSLog(@"self.customTimerWaitTimeInterval:%lld", self.customTimerWaitTimeInterval);
                if(self.customTimerNeedNotification)
                {
                    dispatch_sync(dispatch_get_main_queue(), ^{
                        [[NSNotificationCenter defaultCenter] postNotificationName:customTimerIntervalNotification object:nil];
                    });
                }
                if(self.auctionHouseDetailViewControllerTimerCallBack) { dispatch_sync(dispatch_get_main_queue(), ^{ self.auctionHouseDetailViewControllerTimerCallBack(); }); }}Copy the code

Later, I thought about it for the sake of the stability of the project, or I went back to GCD to re-implement. Later, another possible problem was found in the test. The time of the user’s mobile phone may not be accurate, or the time has been modified and there is a big gap with the server time. This leads to a ridiculous situation: the activity started at 8 o ‘clock, due to the inaccurate time of the phone itself, there should be an hour, but it shows only 40 minutes. This is awkward. To solve this problem, we have modified the method:

When entering the page, we need to return a server time and get a local time, calculate the difference between the two, and take this difference into account when calculating the countdown to keep the time relatively accurate. At the same time, if the user enters the background mode and returns to the foreground mode on this page, we receive the current server time through an interface. If the time difference between the two is roughly equal, we do not deal with it. If the time difference has changed significantly (mainly to prevent users from changing the system time), force the page to refresh.

Methods using

I read this article by MrPeak and learned another way:

ServerTime (Unix time) and lastSyncLocalTime of the current client are recorded each time. When calculating the local time later, curLocalTime is first used to calculate the offset. Add serverTime to get the time:

uint64_t realLocalTime = 0;
if(serverTime ! = 0 && lastSyncLocalTime ! = 0) { realLocalTime = serverTime + (curLocalTime - lastSyncLocalTime); }else {
    realLocalTime = [[NSDate date] timeIntervalSince1970]*1000;
}
Copy the code

If it never synchronizes with the server’s time, it will have to use the local system time, which hardly matters, indicating that the client hasn’t started using it yet.

The key is that if you get the local time, you can use a trick to get how long the system is currently running, and use the system running time to record the current client time:

//get system uptime since last boot
- (NSTimeInterval)uptime
{
    struct timeval boottime;
    int mib[2] = {CTL_KERN, KERN_BOOTTIME};
    size_t size = sizeof(boottime);
    
    struct timeval now;
    struct timezone tz;
    gettimeofday(&now, &tz);
    
    double uptime = -1;
    
    if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
    {
        uptime = now.tv_sec - boottime.tv_sec;
        uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
    }
    return uptime;
}
Copy the code

Both getTimeofDay and sysctl are affected by system time, but the values obtained by subtracting them are independent of system time. This prevents the user from changing the time. Of course, if the user shuts down and starts up again after a period of time, the obtained time will be slower than the server time. In real scenarios, the time slower than the server time usually has little impact. We generally worry that the client time is faster than the server time.

This method is similar to mine in principle, but the number of requests is less than mine, but the disadvantage is mentioned above: it may cause the time we get to be slower than the server time.

4. The verification code

After sending the verification code, users mistakenly touch the exit page and then re-enter. Many apps will refresh the button for sending the verification code. Of course, due to the protection mechanism, the second verification code will not be sent soon. Since I have implemented a singleton of the countdown before, I set the end time of the countdown on this page as a singleton property of the countdown before going to the next step. When you re-enter the page, do what you did in the previous section to determine.

Thanks to:

GCD Timer and libDispatch iOS deal with time