iOS实战-更精准的定时器

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 *)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 *)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 *)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 *)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

你可能感兴趣的:(iOS实战-更精准的定时器)