美文网首页
iOS 多种定时器的使用和中心化管理NSTimer

iOS 多种定时器的使用和中心化管理NSTimer

作者: 流云_henry | 来源:发表于2020-06-29 11:49 被阅读0次

说到定时器,在项目中使用最多的可能就是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);
}

相关文章

网友评论

      本文标题:iOS 多种定时器的使用和中心化管理NSTimer

      本文链接:https://www.haomeiwen.com/subject/wtnwfktx.html