关于iOS开发中NSTimer的一点总结

890 阅读4分钟

前些天接了一个SDK小项目,到手时候已经是那个开发小哥快要闪人的时候,而这项项目也正是刚刚提测之际,这种酸爽,你懂得~ 测试MM一天测出来一个bug,打开工程一看,我的天定时器问题,居然又是NSTimer~搞了一下午,搞好了问题,接下来就是总结一下

至于NSTimer的用法倒是很简单,不过初次应用很难不会遇到各种闪退,各种疑难杂症,究其原因主要还是循环引用在搞事情。下面我就避免NSTimer在日常开发中避免循环引用我知道的4种用法做一总结

这是我验证写的 demo

大神可以绕到了,勿喷

1、分类加block

这种方法我是最先知道也最早用的,因为本人向来对分类就有一种莫名的好感,一看是就在用这种方法


#import <Foundation/Foundation.h>

@interface NSTimer (UnRetainTimer)
+ (NSTimer *)az_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval
                                        repeats:(BOOL)repeats
                                          block:(void(^)(NSTimer *timer))block;
@end

#import "NSTimer+UnRetainTimer.h"

@implementation NSTimer (UnRetainTimer)
+ (NSTimer *)az_scheduledTimerWithTimeInterval:(NSTimeInterval)inerval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block{
    return [NSTimer scheduledTimerWithTimeInterval:inerval target:self selector:@selector(az_blcokInvoke:) userInfo:[block copy] repeats:repeats];
}

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

使用这种方案可以防止timer对类的保留,从而打破了循环引用的产生。但是这种方法在运用的时候要注意一点(敲黑板了~~)

    __weak Test1ViewController *weakSelf = self;
    _timer = [NSTimer az_scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer *timer) {
        __strong Test1ViewController *strongSelf = weakSelf;
        [strongSelf doSomeThing];
//        [self doSomeThing];
    }];

这几行代码一定要注意了,一定要这么写,先搞个weakSelf,然后在strong一下,这样能防止执行块的代码时,类被释放了还不会应用到self。我还试了试,要是这里还用self去调用doSomeThing的话依然会循环引用。最后就是记得在类的dealloc方法中,调用[_timer invalidate]。


#import "Test1ViewController.h"
#import "NSTimer+UnRetainTimer.h"
@interface Test1ViewController (){
    
}
@property (nonatomic,strong) NSTimer *timer;
@property (nonatomic) NSInteger count;
@end

@implementation Test1ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    __weak Test1ViewController *weakSelf = self;
    _timer = [NSTimer az_scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer *timer) {
        __strong Test1ViewController *strongSelf = weakSelf;
        [strongSelf doSomeThing];
//        [self doSomeThing];
    }];
}
- (void)doSomeThing {
    NSLog(@"%ld",(long)self.count++);
}

- (void)dealloc {
    [self.timer invalidate];
}
@end

2、用NSProxy做代理

有NSProxy这么一个好用的根类,NSProxy与NSObject一样是根类,都遵守协议。 它实质上是通过一个代理的模式,从而巧妙的绕开了循环引用的怪圈。

#import <Foundation/Foundation.h>

@interface AZTimerProxy : NSProxy
@property (nonatomic,weak) id obj;
@end

#import "AZTimerProxy.h"

@implementation AZTimerProxy
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *sig;
    sig = [self.obj methodSignatureForSelector:aSelector];
    return sig;
    
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.obj];
}
@end


#import "TestViewController.h"
#import "AZTimerProxy.h"

@interface TestViewController (){
}
@property (nonatomic,strong) AZTimerProxy *proxy;
@property (nonatomic,strong) NSTimer *timer;
@property (nonatomic) NSInteger count;

@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.proxy = [AZTimerProxy alloc];
    self.proxy.obj = self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(doSomeThing) userInfo:nil repeats:YES];
}

- (void)doSomeThing {
    NSLog(@"%ld",(long)self.count++);
}

- (void)dealloc {
    [self.timer invalidate];
}

@end

3、runtime 这个方法主要是参考了大神的方法,在这里做一下列举,可以参考膜拜下。传送门在此 看起来原理跟上一个方法一样,鉴于runtime的理解比起大神还是差之千里,我就简单的验证了一下大神的方法,并做了一下搬运工了~

只是我自己感觉这个方法有些繁琐啊~

#import "Test2ViewController.h"
#import <objc/runtime.h>

@interface Test2ViewController ()
@property (nonatomic,strong) id timerTarget;
@end
static const void * TimerKey = @"TimerKey";
static const void * weakKey = @"weakKey";
@implementation Test2ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // Do any additional setup after loading the view.
    //初始化timerTarge对象
    _timerTarget = [NSObject new];
    //动态创建timerEvent方法
    class_addMethod([_timerTarget class], @selector(timerEvent), (IMP)timMethod, "v@:");
    //创建计时器target对象为_timerTarget
    NSTimer *_timer;
    _timer = [NSTimer timerWithTimeInterval:1 target:_timerTarget selector:@selector(timerEvent) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
    //将self对象与NSTimer对象与_timerTarget对象关联
    objc_setAssociatedObject(_timerTarget, TimerKey, _timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(_timerTarget, weakKey, self, OBJC_ASSOCIATION_ASSIGN);
}

void timMethod(id self,SEL _cmd)
{
    Test2ViewController *vc = objc_getAssociatedObject(self, weakKey);
    [vc performSelector:_cmd];
}

-(void)timerEvent
{
    NSLog(@"%@",NSStringFromClass([self class]));
}
-(void)dealloc
{
    NSTimer *timer = objc_getAssociatedObject(_timerTarget, TimerKey);
    [timer invalidate];
    NSLog(@"%@--dealloc",NSStringFromClass([self class]));
}

@end

4、MSWeakTimer

这个方法看起来屌啊,不过我在项目中还没用过,我试着用了用还是挺不错的,支持pod,用着方便!传送门在此

我这里是直接将MSWeakTimer文件夹拖到工程里面。等到过几天有时间了再来研究一把源码了


#import "Test3ViewController.h"
#import "MSWeakTimer.h"

@interface Test3ViewController ()
@property (nonatomic,strong) MSWeakTimer *timer;
@property (nonatomic) NSInteger count;
@end

@implementation Test3ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    //大神说了,这个可以在任意的线程里面去用啊,这里我就试了试main queue和global queue
//    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    self.view.backgroundColor = [UIColor whiteColor];
    _timer = [MSWeakTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomeThing) userInfo:nil repeats:YES dispatchQueue:queue];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

- (void)dealloc {
    NSLog(@"我销毁了");
}

- (void)doSomeThing {
    NSLog(@"%ld",(long)self.count++);
}
@end

5、苹果爸爸iOS 10 以后提供的新方法

听说苹果爸爸提供了新的方法来方便我的解决内存泄漏的问题,我急忙点开NSTimer类的头文件,看了一波。但由于这个要支持iOS 10 以后,而我们的项目才刚刚说服领导去放弃iOS7 的支持,所以这个方法我自己先积累到这里了,等以后了可以搞起来~~~


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

+ (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));

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

好奇心还是叫我试了试


#import "Test4ViewController.h"

@interface Test4ViewController ()
@property (nonatomic,strong) NSTimer *timer;
@property (nonatomic,assign) NSInteger count;
@end

@implementation Test4ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    __weak Test4ViewController *weakSelf = self;
    _timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf countTimer];
    }];
}

- (void)countTimer{
    NSLog(@"--->%ld",(long)self.count++);
}

- (void)dealloc{
    [_timer invalidate];
}