NSTimer
NSTimer is the most commonly used timer in iOS. It is implemented through Runloop, which is generally accurate. However, if the current cycle is time-consuming and there are too many operations, there will be delays. It is also affected by the RunLoopMode of the added RunLoop.
Use:
The selector way
/ / / structure and open (start NSTimer is essentially to add to the RunLoop) + (NSTimer *) scheduledTimerWithTimeInterval aTarget: NSTimeInterval ti target: (id) selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; + (NSTimer *)timerWithTimeInterval (NSTimeInterval)ti Target (id)aTarget Selector (SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; Use of the above two methods: / / method 1 self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @ the selector (doTask) the userInfo: nil repeats:YES]; / / method 2 self. The timer = [NSTimer timerWithTimeInterval: 1.0 target: self selector: @ the selector (doTask) the userInfo: nil repeats: YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];Copy the code
Block way
/ / / structure and open (start NSTimer is essentially to add to the RunLoop) + (NSTimer *) scheduledTimerWithTimeInterval (NSTimeInterval) interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block; + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer) *timer))block; Use of the above two methods: / / method 1 self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) { NSLog(@"%s block ",__func__); }]; / / method 2 self. The timer = [NSTimer timerWithTimeInterval: 1.0 repeats: YES block: ^ (NSTimer * _Nonnull timer) {NSLog (@ % s block. ",__func__); }]; [[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode];Copy the code
stop
Void invalidate; void invalidate; void invalidate; - (void)fire; - (void)fire;Copy the code
CADisplayLink
CADisplayLink is based on screen refresh cycles, so it is generally punctual, refreshing 60 times per second. The essence of this is also through RunLoop, so it’s not hard to see how delays can still occur when RunLoop selects other modes or is overloaded with time-consuming operations.
Create CADisplayLink-> Add to RunLoop -> terminate -> Destroy. The following code
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel; - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode; - (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode; - (void)invalidate; Use: the self. The timer = [CADisplayLink displayLinkWithTarget: self selector: @ the selector (timerTest)]; self.timer2.preferredFramesPerSecond = 1; / / 1 per second [the self timer addToRunLoop: [NSRunLoop currentRunLoop] forMode: NSDefaultRunLoopMode];Copy the code
Since it is based on screen refreshes, it also measures per frame. It provides the frameInterval attribute for setting the interval based on screen refreshes, which is determined by how many frames the screen refreshes. The default is 1, which is called once every 1/60 of a second.
CADisplayLink can even be optimized when used properly in daily development. For example, for the progress bar that requires dynamic calculation of progress, since the feedback of progress is mainly for UI update, when the frequency of calculation of progress exceeds the number of frames, it will cause a lot of unnecessary calculation. If the method for calculating progress is called by binding it to CADisplayLink, the progress is only calculated on each screen refresh, optimizing performance. MBProcessHUD takes advantage of this feature.
Existing problems
With NSTimer and CADisplayLink, if the developer is not careful, the timer can fail to destroy, resulting in memory leaks.
Example code:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.view.backgroundColor = [UIColor whiteColor]; The self. The timer = [NSTimer timerWithTimeInterval: 1.0 target: self selector: @ the selector (timerTest) the userInfo: nil repeats: YES]; [[NSRunLoop currentRunLoop]addTimer:self.timer forMode:NSDefaultRunLoopMode]; } - (void)timerTest { NSLog(@"%s ",__func__); } - (void)dealloc { [self.timer invalidate]; NSLog(@"%s ",__func__); }Copy the code
It will be found that when the Controller is off, the timer is still printing, which means that the timer is not destroyed in time. In this case, there are also:
The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self selector: @ the selector (timerTest) the userInfo: nil repeats:YES];Copy the code
self.timer2 = [CADisplayLink displayLinkWithTarget:self selector:@selector(timerTest)];
self.timer2.preferredFramesPerSecond = 1;
[self.timer2 addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
Copy the code
It should be noted that the timer using block mode will destroy itself after the Controller shuts down.
Why?
The target of the timer strongly references self, and self strongly references timer. The two references each other, resulting in circular references
Solutions:
The first method is block
Define an intermediate class that acts as target and that weakly references self
This intermediate class is NSProxy
NSProxy is an abstract class that must be inherited and instantiated to use it.
NSProxy has high efficiency. If there is a method, it will be directly called. If there is no method, it will directly enter the message forwarding stage, without cache search, message sending, and dynamic method resolution
What is NSProxy:
- NSProxy is an abstract base class, the root class, similar to NSObject
- Both NSProxy and NSObject implement the protocol
- Provides a generic interface for message forwarding
MyProxy inherits from MyProxy
@interface MyProxy : MyProxy + (instancetype)proxyWithTarget:(id)target; @property (weak, nonatomic) id target; @end@implementation MyProxy + (instanceType)proxyWithTarget (id)target {// NSProxy object does not need to call init, Because it doesn't have an init method MyProxy *proxy = [MyProxy alloc]; proxy.target = target; return proxy; } / / borrow message forwarding Make the original class can also call their sel - (NSMethodSignature *) methodSignatureForSelector: (sel) sel {return [self. The target methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.target]; } @endCopy the code
Now when we create a timer using selector:
The self. The timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: [MyProxy proxyWithTarget: self] selector:@selector(doTask) userInfo:nil repeats:YES];Copy the code
When run, you will see that the Timer is also destroyed when the Controller is closed, without creating a circular reference. The same goes for CADisplayLink.
If using timers is this complicated (with the help of other classes), isn’t it easier?
The answer is to use GCD to create timers, and GCD timers do not rely on NSRunLoop, which is more accurate in principle (when the current loop takes a lot of time or the page is a bit slow).
gcd
The GCD timer actually uses a Dispatch source, which listens on system kernel objects and processes them with more precision through system-level calls
// Create timer object GCD to specify queue: Self. timer3 = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); / / / parameters: 1 Timer 2 Task start time 3 Task interval 4 Acceptable error time, Dispatch_source_set_timer (self.timer3, DISPATCH_TIME_NOW, 1.0*NSEC_PER_SEC, 0.0*NSEC_PER_SEC); // Set timer task dispatch_source_set_event_handler(self.timer3, ^{NSLog(@"%s ",__func__); }); // Dispatch_resume (_gcdTimer) is manually started after the GCD timer is created.Copy the code
GCD also has a way of setting timer tasks
dispatch_source_set_event_handler_f(self.timer3, gcdtimerTest); Dispatch_suspend (self.timer3);Copy the code
Package timer
The most accurate of the three timers is GCD, but when used, it will write a lot of code, so simply encapsulate it. (NSTimer is affected by runloop, the running time of each loop is uncertain, so NSTimer is not accurate enough)
Key points:
1. Use a dictionary to store each timer. When canceling, find the corresponding timer according to the key of the timer
2, multi-threading will cause thread insecurity, dictionary read and write operations need to lock
@interface MBTimer : NSObject + (NSString *)execTask:(void(^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async; + (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async; + (void)cancelTask:(NSString *)name; @end @implementation MBTimer static NSMutableDictionary *timers_; dispatch_semaphore_t semaphore_; + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ timers_ = [NSMutableDictionary dictionary]; semaphore_ = dispatch_semaphore_create(1); }); } + (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async { if (! task || start < 0 || (interval <= 0 && repeats)) return nil; Dispatch_queue_t queue = async? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue(); Dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); Dispatch_source_set_timer (timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0); dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER); // Unique identifier of timer NSString *name = [NSString stringWithFormat:@"%zd", timers_.count]; Timers_ [name] = timer; dispatch_semaphore_signal(semaphore_); // Set the callback dispatch_source_set_event_handler(timer, ^{task(); if (! Repeats) {// Unrepeatable task [self cancelTask:name]; }}); // Start timer dispatch_resume(timer); return name; } + (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async { if (! target || ! selector) return nil; return [self execTask:^{ if ([target respondsToSelector:selector]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [target performSelector:selector]; #pragma clang diagnostic pop } } start:start interval:interval repeats:repeats async:async]; } + (void)cancelTask:(NSString *)name { if (name.length == 0) return; dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER); dispatch_source_t timer = timers_[name]; if (timer) { dispatch_source_cancel(timer); [timers_ removeObjectForKey:name]; } dispatch_semaphore_signal(semaphore_); } @endCopy the code
Use:
@property (copy, nonatomic) NSString *task; / / block mode: // self.task = [MBTimer execTask:^{// NSLog(@"%s ",__func__); //} start:0.5 interval:1.0 repeats:YES async:NO]; // Self. task = [MBTimer execTask:self selector:@selector(timerTest) Start :0.5 interval:1.0 repeats:YES async:NO]; // (void) Touch Began:(NSSet< touch *> *) Touches withEvent:(UIEvent *) Event {[MBTimer cancelTask:self.task]; }Copy the code
If there is any mistake above, welcome to correct. Please indicate the source of reprint.