NSTimer
NSTimer is the most common timer in iOS development.
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.
Copy the code
- (void)setupNSTimer {
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(onTimerAction) userInfo:nil repeats:YES];
[timer fire];
}
Copy the code
The Timer will hold not only the target but also the userInfo object.
There are also interfaces that use the block argument:
/// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode. /// - parameter: Ti The number of seconds between firings of The timer. If seconds is less than or equal to 0.0, This method puts the nonnegative value of 0.1 milliseconds instead /// -parameter: repeats If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires. /// - parameter: block The execution body of the timer; the timer itself is passed as the parameter to this block when executed to aid in avoiding cyclical references + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))blockCopy the code
In the target-action mode of iOS, UIControl(such as UIButton) retains its Target in a weakRetained manner, so there is no circular reference.
NSTimer holds its target as an autorelease, which means target will check to see if it is released the next time the specified runloop is executed. If repeats parameter is YES, target will not be released if timer is not released, thus causing cyclic references. If the repeats parameter is set to NO, the target can be released without a circular reference.
Reference: iOS target-Action mode memory leak in depth
RunLoop
NSTimer is based on the RunLoop, begin with scheduledTimerWithTimeInterval: method of NSTimer will be added to the current default RunLoop mode.
Methods that start with timerWithTimeInterval: need to use runloop’s addTimer: method and add it manually to the runloop.
Therefore, there is usually one caveat: in the UITrackingMode of runloop, the timer will fail. The solution is to add timers to runloop commonModes.
The essence of NSTimer raising circular references is:
Current RunLoop -> CFRunLoopMode -> sources array -> __NSCFTimer -> _NSTimerBlockTarget -> selfCopy the code
Therefore, the self object can be freed only if NSTimer is guaranteed to execute the invalidate method.
Even if you manually add NSTimer’s destruction method to UIViewController’s Dealloc method, you can’t break the circular reference because the dealloc method is never called.
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
Copy the code
In general, in UIViewController, the timer can be destroyed manually when the interface is closed to remove the circular reference.
A circular reference
In the case of NSTimer, there are usually several ways to remove circular references.
WeakContainer is introduced to weakly hold target objects
The WeakContainer object weakly references the self object, and then the Timer target is set to the WeakContainer object, where the message is forwarded to target for execution.
@interface WeakContainer : NSObject
- (instancetype)initWithTarget:(id)target;
@end
@interface WeakContainer ()
@property (nonatomic, weak) id target;
@end
@implementation WeakContainer
- (instancetype)initWithTarget:(id)target {
self = [super init];
if (self) {
self.target = target;
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.target respondsToSelector:aSelector]) {
return self.target;
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"doesNotRecognizeSelector %@ %@", self.target, NSStringFromSelector(aSelector));
}
@end
Copy the code
- (void)dealloc {
[self removeTimer];
}
- (void)setupTimer {
self.weakTimer = [NSTimer scheduledTimerWithTimeInterval:2
target:[[WeakContainer alloc] initWithTarget:self]
selector:@selector(onTimer)
userInfo:nil
repeats:YES];
[self.weakTimer fire];
}
- (void)removeTimer {
[self.weakTimer invalidate];
self.weakTimer = nil;
}
Copy the code
The release of the target object is no longer affected by NSTimer.
Here, a WeakContainer, inherited from NSObject, is used to weakly hold the target of NSTimer. A more appropriate approach is to use NSProxy.
Use NSProxy abstract classes
NSProxy implements the basic methods required of a root class, including those defined inThe nSObobject Protocol Protocol. However, as an abstract class it doesn't provide an initialization method, And it raises an exception upon receiving any message it doesn't respond. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation(_:) and methodSignatureForSelector: Methods to handle messages that it doesn't implement itselfCopy the code
NSProxy, the other base class besides NSObject, is an abstract class that can only inherit from it, override its message forwarding methods, and forward messages to another object.
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
Copy the code
NSProxy also has no functionality beyond the two methods of overloading the message forwarding mechanism. That is, using NSProxy is destined to forward messages.
- NSProxy can be used to simulate multiple inheritance, with proxy objects handling messages from multiple different Class objects.
- A proxy class that inherits from NSProxy automatically forwards messages, while a proxy class that inherits from NSObject does not, and needs to handle this itself according to the message forwarding mechanism.
- Methods in NSObject’s Category cannot be forwarded
@interface WeakProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
@end
@interface WeakProxy ()
@property (nonatomic, weak) id target;
@end
@implementation WeakProxy
- (instancetype)initWithTarget:(id)target {
self = [WeakProxy alloc];
self.target = target;
return self;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
Copy the code
- (void)dealloc {
[self removeTimer];
}
- (void)setupTimer {
self.weakTimer = [NSTimer scheduledTimerWithTimeInterval:2
target:[[WeakProxy alloc] initWithTarget:self]
selector:@selector(onTimer)
userInfo:nil
repeats:YES];
[self.weakTimer fire];
}
- (void)removeTimer {
[self.weakTimer invalidate];
self.weakTimer = nil;
}
Copy the code
As you can see, the code is almost identical in both ways. Only the characteristics of NSProxy should be carefully understood.
Use the Category trick of NSTimer
@interface NSTimer (WeakTimer) + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(void(^)(void))block; @end @implementation NSTimer (WeakTimer) + (NSTimer *)weak_scheduledTimerWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(void(^)(void))block { return [self scheduledTimerWithTimeInterval:ti target:self selector:@selector(onTimer:) userInfo:[block copy] repeats:yesOrNo]; } + (void)onTimer:(NSTimer *)timer { void (^block)(void) = timer.userInfo; if (block) { block(); } } @endCopy the code
- (void)dealloc {
[self removeTimer];
}
- (void)setupTimer {
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer weak_scheduledTimerWithTimeInterval:2 repeats:YES block:^{
[weakSelf onTimer];
}];
[self.timer fire];
}
- (void)removeTimer {
[self.timer invalidate];
self.timer = nil;
}
Copy the code
The two implementations are different, but essentially they both do two things:
- NSTimer cannot hold self object strongly
- The NSTimer object is destroyed during the execution of the dealloc method of the self object
CADisplayLink
CADisplayLink is a timer that draws content to the screen at a screen refresh rate. It is suitable for constantly redrawing UI, rendering animation or video, etc.
Once CADisplayLink is added to the RunLoop in a specific pattern, the RunLoop calls the selector method on the CADisplayLink bound target whenever the screen needs to be refreshed, The target retrieves the timestamp of each CADisplayLink call to prepare the data for the next frame. Can be used for animation or video. Use CADisplayLink to be aware of circular references as well.
CADisplayLink *displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateAction)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// displayLink.paused = YES;
// [displayLink invalidate];
// displayLink = nil;
Copy the code
NSTimer is less precise than execution, and if NSTimer’s firing time is up and RunLoop is blocked, its firing time is delayed to the next RunLoop cycle. The tolerance property is the delay range used to set the allowable trigger time.
GCD Timer
GCD Timer doesn’t have this problem, but the usage is more complicated.
NSTimer actually relies on RunLoop, and if RunLoop tasks are heavy, NSTimer execution can be very inaccurate. And the use of NSTimer in child threads requires that the child thread be resident, that is, the runloop always exists.
GCD timers, on the other hand, are kernel dependent, not RunLoop dependent, and are generally more punctual.
- (void)setupGCDTimer { dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, 0); self.myGCDTimerQueue = dispatch_queue_create("com.icetime.mygcdtimer", attr); // create GCD timer self.myGCDTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.myGCDTimerQueue); // Set timer uint64_t interval = 1 * NSEC_PER_SEC; dispatch_source_set_timer(self.myGCDTimer, DISPATCH_TIME_NOW, interval, 0); /// dispatch_source_set_event_handler(self.mygcdtimer, ^{NSLog(@"com.icetime.mygcdtimer"); }); // start timer dispatch_resume(self.mygcdtimer); }Copy the code
/ / pause dispatch_suspend (self timer); / / destroyed dispatch_cancel (self. The timer); self.timer = nil;Copy the code