内存管理 01 - NSTimer、CADisplayLink、GCD 定时器
使用 NSTimer、CADisplayLink 需要注意什么?
NSTimer、CADisplayLink 在失效前会一直对 target 产生强引用,导致 target 没法释放。
NSTimer 的解决方式有三种:使用 NSProxy 中间对象、使用包装类、通过 Category 扩展 NSTimer。
CADisplayLink 的解决方式有两种:使用 NSProxy 中间对象、使用包装类。
1. 使用 NSProxy 中间对象
- 将 NSTimer 的 target 指定为 NSProxy,NSProxy 中对 真正的 target 进行弱引用。
- NSProxy 中利用消息转发机制调用真正的 target 的 Selector;
使用 NSProxy 的优势:
- NSProxy 在消息发送阶段不会到父类中查找方法
- 消息发送失败后也不会尝试动态方法解析。
- 消息发送失败后直接进行消息转发,并且直接调用 methodSignatureForSelector: 方法。
TargetProxy
@interface TargetProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)targetProxyWithTarget:(id)target;
+
@end
@implementation TargetProxy
+ (instancetype)targetProxyWithTarget:(id)target {
TargetProxy *proxy = [self alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
TargetProxy 使用示例
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) CADisplayLink *displayLink;
@end
@implementation ViewController
- (void)dealloc {
[self.timer invalidate];
[self.displayLink invalidate];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:[TargetProxy targetProxyWithTarget:self]
selector:@selector(doWork)
userInfo:nil
repeats:YES];
self.displayLink = [CADisplayLink displayLinkWithTarget:[TargetProxy targetProxyWithTarget:self]
selector:@selector(doWork)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)doWork {
NSLog(@"%s", __func__);
}
@end
2. 包装 CADisplayLink、NSTimer
- 将 NSTimer、CADisplayLink 作为属性包装到自定义类中。
- 通过包装类对外提供 NSTimer、CADisplayLink 的功能。
NoRetainTimer
@interface NoRetainTimer : NSObject
@property (nonatomic, readonly, getter=isValid) BOOL valid;
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
- (void)invalidate;
@end
@interface NoRetainTimer ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation NoRetainTimer
- (void)dealloc {
[self invalidate];
}
- (BOOL)isValid {
return self.timer.isValid;
}
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block {
NoRetainTimer *instance = [[NoRetainTimer alloc] init];
instance.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(__blockInvokeWithTimer:)
userInfo:[block copy]
repeats:repeats];
return instance;
}
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode {
[runloop addTimer:self.timer forMode:mode];
}
- (void)invalidate {
[self.timer invalidate];
}
+ (void)__blockInvokeWithTimer:(NSTimer *)timer {
void (^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
}
@end
NoRetainDisplayLink
@interface NoRetainDisplayLink : NSObject
@property (nonatomic, readonly, getter=isValid) BOOL valid;
+ (instancetype)displayLinkWithBlock:(void(^)(NoRetainDisplayLink *displayLink))block;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;
- (void)invalidate;
@end
@interface NoRetainDisplayLink ()
@property (nonatomic, strong) CADisplayLink *displayLink;
@property (nonatomic, readwrite, getter=isValid) BOOL valid;
@property (nonatomic, copy) void (^block)(NoRetainDisplayLink *displayLink);
@end
@implementation NoRetainDisplayLink
- (void)dealloc {
[self invalidate];
}
- (BOOL)isPaused {
return self.displayLink;
}
+ (instancetype)displayLinkWithBlock:(void(^)(NoRetainDisplayLink *displayLink))block {
NoRetainDisplayLink *instance = [[self alloc] init];
instance.valid = YES;
instance.block = block;
instance.displayLink = [CADisplayLink displayLinkWithTarget:instance selector:@selector(__blockInvoke)];
return instance;
}
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode {
[self.displayLink addToRunLoop:runloop forMode:NSRunLoopCommonModes];
}
- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode {
[self.displayLink removeFromRunLoop:runloop forMode:NSRunLoopCommonModes];
}
- (void)invalidate {
instance.valid = NO;
[self.displayLink invalidate];
}
- (void)__blockInvoke {
self.block(self);
}
@end
NoRetainTimer & NoRetainDisplayLink 使用示例
@interface ViewController ()
@property (nonatomic, strong) NoRetainTimer *timer;
@property (nonatomic, strong) NoRetainDisplayLink *displayLink;
@end
@implementation ViewController
- (void)dealloc {
[self.timer invalidate];
[self.displayLink invalidate];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NoRetainTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer *timer) {
NSLog(@"%s", __func__);
}];
self.displayLink = [NoRetainDisplayLink displayLinkWithBlock:^(NoRetainDisplayLink *displayLink) {
NSLog(@"%s", __func__);
}];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
@end
3. 通过 Category 扩展 NSTimer
- 创建一个 NSTimer 的分类,提供一个 +scheduledTimerWithTimeInterval:repeats:block 方法。
- 将 NSTimer 的 target 指定为 [NSTimer class],selector 为 Category 中的一个类方法,并将 block 作为 NSTimer 的 userInfo。
- 当 Category 的类方法被 NSTimer 回调时,取出 NSTimer 中的 block(userInfo),调用 block。
NSTimer+NoRetain
@interface NSTimer (NoRetain)
+ (instancetype)noretain_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block;
@end
@implementation NSTimer (NoRetain)
+ (instancetype)noretain_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)repeats
block:(void (^)(NSTimer *timer))block {
return [self scheduledTimerWithTimeInterval:ti
target:self
selector:@selector(__blockInvokeWithTimer:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)__blockInvokeWithTimer:(NSTimer *)timer {
void (^block)(NSTimer *timer) = timer.userInfo;
if (block) {
block(timer);
}
}
@end
NSTimer+NoRetain 使用示例
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)dealloc {
[self.timer invalidate];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer noretain_scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer *timer) {
NSLog(@"%s", __func__);
}];
}
@end
GCD 定时器
- NSTimer 依赖于 RunLoop,当 RunLoop 的任务过于繁重时,可能会导致 NSTimer 不准时。
- GCD 定时器基于内核,不依赖于 RunLoop,时间调度会非常准时。
GCDTimer 实现
@interface GCDTimer : NSObject
@property (nonatomic, readonly, getter=isValid) BOOL valid;
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
inBackground:(BOOL)background
block:(void (^)(GCDTimer *timer))block;
- (void)invalidate;
@end
@interface GCDTimer ()
@property (nonatomic, strong) dispatch_source_t timer;
@property (nonatomic, readwrite, getter=isValid) BOOL valid;
@property (nonatomic, copy) void (^block)(GCDTimer *displayLink);
@end
@implementation GCDTimer
- (void)dealloc {
[self invalidate];
}
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
inBackground:(BOOL)background
block:(void (^)(GCDTimer *timer))block {
GCDTimer *instance = [[GCDTimer alloc] init];
// 队列
dispatch_queue_t taskQueue = background
? dispatch_queue_create([GCDTimer.description cStringUsingEncoding:NSUTF8StringEncoding], DISPATCH_QUEUE_SERIAL)
: dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, taskQueue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * interval),
NSEC_PER_SEC * interval, 0);;
// 设置回调
__weak typeof(instance) weakInstance = instance;
dispatch_source_set_event_handler(timer, ^{
weakInstance.block(weakInstance);
// 不重复
if (!repeats) {
[weakInstance invalidate];
}
});
// 启动定时器
instance.timer = timer;
instance.block = block;
instance.valid = YES;
dispatch_resume(timer);
return instance;
}
- (void)invalidate {
self.valid = NO;
dispatch_cancel(self.timer);
}
GCDTimer 使用示例
@interface ViewController ()
@property (nonatomic, strong) GCDTimer *timer;
@end
@implementation ViewController
- (void)dealloc {
[self.timer invalidate];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [GCDTimer scheduledTimerWithTimeInterval:1.0 repeats:YES inBackground:YES block:^(GCDTimer *timer) {
NSLog(@"%s", __func__);
}];
}
@end