iOS中,常用的定时器有三种:NSTimer,CADisplayLink,GCD。在一定基础之上,做进一步探究。
NSTimer,CADisplayLink
在使用scheduleTimerWithTimeInterval:target:selector:userInfo:repeats:方式创建的定时器会以默认方式添加到当前线程runloop中,无需手动添加。
如果有需求:点击屏幕触发定时器,不需要时点击返回
@interface TestViewController ()
@property(nonatomic,strong)NSTimer *timer;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
-(void)run
{
NSLog(@"%s",__func__);
}
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
@end
经测试,这段代码dealloc方法不会执行,存在循环引用。因为NSTimer内部有强指针target,所以不管外部传入的是weak还是strong,都会将传入的内存地址赋值给形参target,所以不管target存储的是weak的地址还是strong的地址,NSTimer对target都是强引用,所以timer和self产生了循环引用。
通过GNUStep中源码可以看出,传入的object被retain一次,被timer强持有。
NSTimer schedule:target: init代码所产生的循环引用
timer-self循环引用要想解决这个循环引用,使其中一个强引用变成弱引用。
打破循环引用
- 方法一:使用block创建方式
block存储在timer中,timer对block强引用,block对self是弱引用,定时器对self是弱引用。
__weak typeof(self)wself = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[wself run];
}];
- 方法二
如果使用scheduleTimerWithTimeInterval:target:方法,就要引入中间变量,将中间对象的指针置为弱指针。
@interface SSProxy : NSObject
//弱指针,打破循环引用
@property(nonatomic, weak)id target;
+(instancetype)proxyWithTarget:(id)target;
@end
@implementation SSProxy
+(instancetype)proxyWithTarget:(id)target
{
SSProxy *proxy = [[SSProxy alloc] init];
proxy.target = target;
return proxy;
}
//消息转发阶段,返回值不为空,直接给某个对象发送消息
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
@interface TestViewController ()
@property(nonatomic,strong)NSTimer *timer;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[SSProxy proxyWithTarget:self] selector:@selector(run) userInfo:nil repeats:YES];
}
-(void)run
{
NSLog(@"%s",__func__);
}
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
@end
- 方法三:使用CADisplayLink
@interface TestViewController ()
@property(nonatomic,strong)CADisplayLink *timer;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
self.timer = [CADisplayLink displayLinkWithTarget:[SSProxy proxyWithTarget:self] selector:@selector(run)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
-(void)run
{
NSLog(@"%s",__func__);
}
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
@end
存在的弊端:
1.NSTimer和CADisplayLink底层是runloop实现的,所以有可能并不准时,如果runloop任务过于繁重,每一圈的处理就会耗时,就导致不准时。
2.对于CADisplayLink,当CPU忙于其他计算,就无法保证每秒60次的频率执行屏幕绘制。
GCD定时器
GCD定时器直接和系统内核挂钩,不依赖于runloop,相对精准。
@interface TestViewController ()
@property(nonatomic,strong)dispatch_source_t timer;
@end
@implementation TestViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
dispatch_queue_t queue = 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_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@",[NSThread currentThread]);
});
dispatch_resume(timer);
self.timer = timer;
}
-(void)run
{
NSLog(@"%s",__func__);
}
-(void)dealloc
{
NSLog(@"%s",__func__);
}
@end
网友评论