Skip to content

Latest commit

 

History

History
407 lines (301 loc) · 11.8 KB

1、定时器.md

File metadata and controls

407 lines (301 loc) · 11.8 KB

定时器

有三种常见定时器

  • 1、NSTimer
  • 2、CADisplayLink
  • 3、GCD

NSTimer

NSTimer是iOS中最常用的定时器。其通过Runloop来实现,一般情况下比较准确。但是当前循环耗时操作较多时,会出现延迟问题。同时,也受所加入的RunLoop的RunLoopMode影响。

/// 构造并开启(启动NSTimer本质上是将其加入RunLoop中)
// "scheduledTimer"前缀的为自动启动NSTimer的,如:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

/// 构造但不开启
// "timer"前缀的为只构造不启用的,如:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block

//定时器的释放一定要先将其终止,而后才能销毁对象
- (void)invalidate;
//立即执行(fire)
//我们对定时器设置了延时之后,有时需要让它立刻执行,可以使用fire方法:
- (void)fire;

简单使用

//方法1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doTask) userInfo:nil repeats:YES];

//方法2
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doTask) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

CADisplayLink

CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,每秒刷新60次。其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。

其使用步骤为 创建CADisplayLink->添加至RunLoop中->终止->销毁。代码如下

self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(doTask)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

//在dealloc中
// 终止定时器
[disLink invalidate];
// 销毁对象
disLink = nil;

同时,由于其是基于屏幕刷新的,所以也度量单位是每帧,其提供了根据屏幕刷新来设置间隔的frameInterval属性,其决定于屏幕刷新多少帧时调用一次该方法,默认为1,即1/60秒调用一次。

如果我们想要计算出每次调用的时间间隔,可以通过frameInterval * duration求出,后者为屏幕每帧间隔的只读属性。

在日常开发中,适当使用CADisplayLink甚至有优化作用。比如对于需要动态计算进度的进度条,由于起进度反馈主要是为了UI更新,那么当计算进度的频率超过帧数时,就造成了很多无谓的计算。如果将计算进度的方法绑定到CADisplayLink上来调用,则只在每次屏幕刷新时计算进度,优化了性能。MBProcessHUB则是利用了这一特性

GCD

GCD定时器实际上是使用了dispatch源(dispatch source),dispatch源监听系统内核对象并处理,通过系统级调用,更加精准

/** 创建定时器对象
* para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
* para2-3: 中间两个参数对定时器无用
* para4: 最后为在什么调度队列中使用
*/
_gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
/** 设置定时器
* para2: 任务开始时间
* para3: 任务的间隔
* para4: 可接受的误差时间,设置0即不允许出现误差
* Tips: 单位均为纳秒
*/
dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
/** 设置定时器任务
* 可以通过block方式
* 也可以通过C函数方式
*/
dispatch_source_set_event_handler(_gcdTimer, ^{
static int gcdIdx = 0;
NSLog(@"GCD Method: %d", gcdIdx++);
NSLog(@"%@", [NSThread currentThread]);

if(gcdIdx == 5) {
	// 终止定时器
	dispatch_suspend(_gcdTimer);
}
});
// 启动任务,GCD计时器创建后需要手动启动
dispatch_resume(_gcdTimer);

内存泄露

我们简单的做一个能够销毁的viewcontroller,然后里面写一个NSTimer定时器,然后在dealloc中销毁定时器。我们发现在我们viewcontroller销毁的时候,dealloc并没有执行,所以其实使用dealloc是存在内存泄漏的。

timer

timer强引用self,self又强引用timer 造成循环引用。

解决方案

  • 1、在释放timer的时候,写在viewWillDisappear里面
  • 2、在引入一个类来接管target

对于第一种方案,没有什么好说的,我们就来研究一下第二种方案吧。

timer1

利用NSProxy解决NSTimer内存泄漏问题

NSProxy是一个抽象类,必须继承实例化其子类才能使用。NSproxy具体使用参考官方示例,在上面示例中通过消息转发实现了同时对NSProxy发送NSMutableString和NSMutableArray类型的消息间接的实现了多重继承。

什么是NSProxy:

  • NSProxy是一个抽象的基类,是根类,与NSObject类似
  • NSProxy和NSObject都实现了协议
  • 提供了消息转发的通用接口

如何使用NSProxy来转发消息?

  • 1.需要继承NSProxy
  • 2.重写如下的2个方法:
    • methodSignatureForSelector:
    • forwardInvocation:

代码使用,写一个继承自NSProxyMyProxy

@interface MyProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

@implementation MyProxy
+ (instancetype)proxyWithTarget:(id)target
{
	// NSProxy对象不需要调用init,因为它本来就没有init方法
	MyProxy *proxy = [MyProxy alloc];
	proxy.target = target;
	return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
	return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
	[invocation invokeWithTarget:self.target];
}

@end

使用

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[MyProxy proxyWithTarget:self] selector:@selector(doTask) userInfo:nil repeats:YES];

现在在dealloc中销毁定时器,发现会执行dealloc方法

GCD封装

这三种定时器中最准确的还是GCD,但是我们发现在使用的时候,我们会写很多代码,所以为了下一次使用的时候不写那么多的代码,我们就简单的封装一下。

接口 在写具体的代码之前,我们首先需要考虑的就是我们应该提供什么接口

  • 1、肯定需要里面执行事件,所以需要一个回调
  • 2、设置什么时候开始启动定时器
  • 3、设置定时器的时间间隔
  • 4、是否重复执行
  • 5、是否多线程执行
  • 6、提供取消任务的接口 其实提供这么多的信息基本上已经够用了。然后我们就可以开始写代码了。
+ (void)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;

+ (void)cancelTask;

初步封装

+ (void)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async{

if (!task || start < 0 || (interval <= 0 && repeats)) return ;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : 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(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
dispatch_source_cancel(timer);
}
});

// 启动定时器
dispatch_resume(timer);
}

思考 我们在创建多个定时器的时候,我们需要取消某一个定时器,我们应该怎么找到该定时器

为了解决上面问题,我们可以考虑到用一个字典存储每一个定时器,在取消的时候,根据定时器的key找到相应的定时器。所以我们需要把接口改成这个样子

+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;

+ (void)cancelTask:(NSString *)name;

具体的实现 取消任务,吧key移除

+ (void)cancelTask:(NSString *)name{
if (name.length == 0) return;
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
}

在添加任务的时候,同时需要像字典中添加键值对

// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
static NSMutableDictionary *timers_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];

});
}


+ (void)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async{

if (!task || start < 0 || (interval <= 0 && repeats)) return ;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : 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(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);

// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;

// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});

// 启动定时器
dispatch_resume(timer);
}


 
+ (void)cancelTask:(NSString *)name{
if (name.length == 0) return;
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
}

思考 在进行多线程操作的时候,字典的添加移除操作会不会造成线程的不安全 答案是肯定的,多线程会造成线程不安全,为了解决这个问题,我们在对字典读写操作的时候需要加锁

最终封装

static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;

+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}

+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;

// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : 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(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);


dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);

// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();

if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});

// 启动定时器
dispatch_resume(timer);

return name;
}




+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;

dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);

dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}

dispatch_semaphore_signal(semaphore_);
}