说到定时器,在项目中使用最多的可能就是NSTimer了,其实除了NSTimer,在iOS开发中,我们还可以通过CADisplayLink以及GCD的方法来实现定时器功能。
1、NSTimer
1.1NSTimer的简单应用
NSTimer的初始化方法有很多,我们试试用NSInvocation来初始化一个定时器:
- (void)viewDidLoad {
[super viewDidLoad];
//初始化一个Invocation对象
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(myLog)];
NSInvocation *invo = [NSInvocation invocationWithMethodSignature:signature];
[invo setTarget:self];
[invo setSelector:@selector(myLog)];
NSTimer *timer = [NSTimer timerWithTimeInterval:1 invocation:invo repeats:YES];
//加入主循环池中
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//开始循环
[timer fire];
}
- (void)myLog {
}
上面方法创建的定时器在被初始化后并不会立马执行,需要我们手动加入RunLoop并调用fire函数来启动。
NSTimer中的fireDate属性十分重要,我们常用它来进行定时器的开启和停止
如果我们将NSTimer作为视图控制器的属性,那么在这个视图控制器被释放前,将这个定时器停止,甚至置为nil,都不能使这个控制器被释放,原因是系统的循环池中还持有这个对象.我们可以在适当的地方调用NSTimer的invalidate方法来讲定时器从循环池中移除。
NSTimer定时器的实现是基于RunLoop的,当我们启动NSTimer定时器时,其并不是按时间的间隔进行循环调用的。事实上,在定时器注册到RunLoop中后,RunLoop会设置一个一个的时间点进行调用,如果错过了某个时间点,则定时器并不会延时调用,而是直接等待下一个时间点调用,所以NSTimer定时器并不是精准的。
在子线程中使用某些延时函数和选择器时,我们必须手动开启RunLoop.
方法如下:
//@interface NSObject (NSDelayedPerforming)
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
//@interface NSRunLoop (NSOrderedPerform)
- (void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
- (void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
- (void)cancelPerformSelectorsWithTarget:(id)target;
1.2中心化管理NSTimer定时器
我们知道,不当使用NSTimer极易产生内存泄漏。并且在一个应用中激活大量的定时器十分消耗性能。其实对于常规需求,我们只需要在一个定时器中添加不同的任务即可,根本不需要使用过多的计时器。由于将定时器作为属性极易产生内存泄露,因此我们可以考虑使用一个管理中心来统一处理管理定时器任务。
创建一个Task类,将他作为定时器任务类,继承于NSObject,如下:
@interface NSTimerTask : NSObject
- (instancetype)initWithTimerI:(NSUInteger)time handleBlock:(void(^)(void))handle;
//标志
@property (nonatomic, strong, readonly)NSString *taskID;
//时间单位为1/60 秒
@property (nonatomic, assign)NSUInteger timeI;
//要执行的动作
@property (nonatomic, copy)void(^eventBlock)(void);
@end
#import "NSTimerTask.h"
@implementation NSTimerTask
- (instancetype)initWithTimerI:(NSUInteger)time handleBlock:(void (^)(void))handle {
self = [super init];
if (self) {
self.timeI = time;
self.eventBlock = handle;
_taskID = [NSUUID UUID].UUIDString;
}
return self;
}
@end
再创建一个管理类,统一管理定时器任务
NSTimerManager的设计如下:
@class NSTimerTask;
@interface NSTimerManager : NSObject
//单例方法
+ (instancetype)sharedManager;
//运行定时器任务
- (void)runTask:(NSTimerTask *)task;
//取消定时器任务
- (void)cancelTaskWithID:(NSString *)taskID;
@end
#import "NSTimerManager.h"
#import "NSTimerTask.h"
@interface NSTimerManager ()
@property (nonatomic, strong)NSMutableArray *taskArray;
@property (nonatomic, strong)NSTimer *timer;
@property (nonatomic, assign)NSInteger index;
@end
@implementation NSTimerManager
+ (instancetype)sharedManager {
static dispatch_once_t onceToken;
static NSTimerManager *manager = nil;
dispatch_once(&onceToken, ^{
if (!manager) {
manager = [[NSTimerManager alloc]init];
}
});
return manager;
}
- (instancetype)init {
self = [super init];
if (self) {
self.index = 0;
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
return self;
}
//运行定时器任务
- (void)runTask:(NSTimerTask *)task {
//如果任务数组中存在该任务,那么就直接retur,否则将新任务添加入任务数组
for (NSTimerTask *t in self.taskArray) {
if ([t.taskID isEqualToString:task.taskID]) {
return;
}
}
[self.taskArray addObject:task];
}
//取消定时器任务
- (void)cancelTaskWithID:(NSString *)taskID {
for (int i = (int)self.taskArray.count - 1; i >= 0; i --) {
if ([[self.taskArray[i] taskID] isEqualToString:taskID]) {
[self.taskArray removeObjectAtIndex:i];
}
}
}
//定时器运行方法
- (void)runTimer {
if (self.index == 59) {
self.index = 0;
}
for (NSTimerTask *t in self.taskArray) {
if (self.index%t.timeI == 0) {
t.eventBlock();
}
}
self.index ++;
}
#pragma mark -- lazy
- (NSMutableArray *)taskArray {
if (!_taskArray) {
_taskArray = [NSMutableArray array];
}
return _taskArray;
}
- (NSTimer *)timer {
if (!_timer) {
_timer = [NSTimer timerWithTimeInterval:1/60.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
}
return _timer;
}
@end
这样当我们要使用定时器时,将任务加入此中心管理单例即可,无论我们有多少任务,整个工程都只有一个定时器在运行。我们来进行下简单的测试:
#import "NextPageViewController.h"
#import "NSTimerTask.h"
#import "NSTimerManager.h"
@interface NextPageViewController ()
@property (nonatomic, strong)NSString *taskID;
@end
@implementation NextPageViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
NSTimerTask *task1 = [[NSTimerTask alloc]initWithTimerI:10 handleBlock:^{
NSLog(@"event1");
}];
self.taskID = task1.taskID;
NSTimerTask *task2 = [[NSTimerTask alloc]initWithTimerI:15 handleBlock:^{
NSLog(@"event2");
}];
[[NSTimerManager sharedManager] runTask:task1];
[[NSTimerManager sharedManager] runTask:task2];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[[NSTimerManager sharedManager] cancelTaskWithID:self.taskID];
}
-(void)dealloc {
NSLog(@"当前类:%@已释放",NSStringFromClass([self class]));
}
@end
TIPS:我们发现当页面pop的时候,是走了dealloc方法的,因为我们的NSTimer是单例创建的,而当前类未持有单例,所以是未造成循环引用。
2、CADisplayLink类的应用
CADisplayLink也是一种定时器,并且在很多自定义动画中,CADisplayLink有着比NSTimer更好的表现,CADisplayLink的调用频率和设备屏幕的刷新频率一致。
由于CADisplayLink这种特性,我们可以通过它来监控应用程序的帧率:
- (void)viewDidLoad {
[super viewDidLoad];
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(myLog)];
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)myLog {
static NSTimeInterval time = 0;
if (time != 0) {
NSLog(@"%f",1/(self.displayLink.timestamp - time));
}
time = self.displayLink.timestamp;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.displayLink setPaused:YES];
}
通过打印我们可以看出iOS应用的极限屏幕刷新率为60帧/秒。
CADisplayLink的暂停和启用用Paused属性设置,其失效方法和NSTimer一样调用invalidate方法。
3、使用GCD方式的定时器
属性GCD的开发者经常这样来处理延迟任务:
//延迟3秒后,在主线程中执行代码块
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"hi");
});
其实GCD也提供了方法来执行定时任务,并且不受RunLoop的影响:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong)dispatch_source_t timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//设置频率
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL, 0), 1*NSEC_PER_SEC, 0);
//设置执行的代码块
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"hello");
});
//激活
dispatch_resume(self.timer);
}
网友评论