ios开发NSTimer和GCD实现Timer

1,181 阅读4分钟

在iOS开发中,经常会用到定时器,iOS中常用的定时器有三种:NSTimer,GCD,CADisplayLink。

NSTimer创建定时器

 // 创建定时器 方式1
 NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:_target selector:@selector(run) userInfo:nil repeats:YES];
// 停止定时器
[timer invalidate];

// 创建定时器 方式2
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 将定时器添加到runloop中,否则定时器不会启动
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

// 停止定时器
[timer invalidate];

方式1会自动将创建的定时器以默认方式添加到当前线程runloop中,而无需手动添加。但是在此种模式下,当滚动屏幕时runloop会进入另外一种模式,定时器会暂停,为了解决这种问题,可以像方式2那样把定时器添加到NSRunLoopCommonModes模式下。

方式1和方式2在设置后都会在间隔设定的时间(本例中设置为1s)后执行test方法,如果需要立即执行可以使用下面的代码。

[timer fire];

注意:NSTimer创建的定时器,使用时会造成循环引用(target对self做了强引用,self又对timer进行了强引用),从而导致内存泄漏。

1、自定义category用block解决

#import <Foundation/Foundation.h>

@interface NSTimer (TimerBlock)

/**
 分类解决NSTimer在使用时造成的循环引用的问题

 @param interval 间隔时间
 @param block    回调
 @param repeats  用于设置定时器是否重复触发

 @return 返回NSTimer实体
 */
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;

@end



#import "NSTimer+TimerBlock.h"

@implementation NSTimer (TimerBlock)
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)reqeats{
    return [self timerWithTimeInterval:interval target:self selector:@selector(blockSelector:) userInfo:[block copy] repeats:reqeats];
}

+ (void) blockSelector:(NSTimer *)timer{
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
@end

__weak typeof(self) weakSelf = self;    //避免 block 强引用 self
self.timer = [NSTimer block_TimerWithTimeInterval:3 block:^{
//    [weakSelf dosomething];
} repeats:YES];

 iOS10中,定时器的API新增了block方法,实现原理与此类似,这里采用分类为NSTimer添加了带有block参数的方法,而系统是在原始类中直接添加方法,最终的行为是一致的。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

2、给self添加中间件proxy

引入一个对象proxyproxy弱引用 self,然后 proxy 传入NSTimer。即self 强引用NSTimerNSTimer强引用 proxyproxy 弱引用 self,这样通过弱引用来解决了相互引用,此时不会形成环。

/FFProxy.h
@interface FFProxy : NSObject
+(instancetype)proxyWithTarget:(id)target;
@end

//FFProxy.m
#import "FFProxy.h"

@interface FFProxy()
@property (nonatomic ,weak) id target;
@end

@implementation FFProxy

+(instancetype)proxyWithTarget:(id)target
{
    FFProxy *proxy = [[FFProxy alloc] init];
    proxy.target = target;
    return proxy;
}

//仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end

NSTimes不准确的原因

1.定时器被添加在主线程中,由于定时器在一个RunLoop中被检测一次,所以如果在这一次的RunLoop中做了耗时的操作,当前RunLoop持续的时间超过了定时器的间隔时间,那么下一次定时就被延后了。

2.RunLoop模式的影响 为了验证,我们在当前页面上添加一个tableview,在定时器运行时,我们对tableview进行滑动操作,可以发现,定时器并不会触发下一次的定时任务。 原因分析: 主线程的RunLoop有两种预设的模式,RunLoopDefaultMode和TrackingRunLoopMode。 当定时器被添加到主线程中且无指定模式时,会被默认添加到DefaultMode中,一般情况下定时器会正常触发定时任务。但是当用户进行UI交互操作时(比如滑动tableview),主线程会切换到TrackingRunLoopMode,在此模式下定时器并不会被触发。

解决的方式

 1、在子线程中创建timer,在主线程进行定时任务的操作

2、在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作

GCD来实现

GCD创建的定时器不受RunLoop中Modes影响;

注意:将定时器写成属性,是因为内存管理的原因,使用了dispatch_source_create方法,这种方法GCD是不会帮你管理内存的

@property (nonatomic,strong) dispatch_source_t timer;


self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));  

dispatch_source_set_timer(self.timer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), (uint64_t)(1.0 * NSEC_PER_SEC), 0); // 设置回调  

//执行这个以后,会立即执行一次
dispatch_source_set_event_handler(self.timer, ^{  
 });  
dispatch_resume(self.timer);
}

暂停:gcdTimer.suspend()
取消:gcdTimer.cancel()

// 崩溃一:
gcdTimer.suspend()
gcdTimer = nil

// 崩溃二:
gcdTimer.suspend()
gcdTimer.cancel()
gcdTimer = nil

解决方案
先resume再cancel