iOS基础知识内存

解决NSTimer的强引用问题

2020-03-25  本文已影响0人  Sweet丶

在使用定时器时,如果选择使用的NStimerCADisplayLink来实现,比如:

self.timer = [NSTimer scheduledTimerWithTimeInterval:0.1 
                            target:self 
                          selector:@selector(test1) userInfo:nil repeats:YES];

self.displayLink = [CADisplayLink displayLinkWithTarget:self 
                                               selector:@selector(test2)];

这样写会存在的问题是NSTimerCADisplayLink内部会强引用它的target,导致循环引用。

不用其他定时器方案替换的解决方法是:使用自定义的NSProxy子类对象作为target。这个是挺多第三方库(比如YYText、FLAnimatedImage)也采用的解决方式,这种方式不需要关注什么时候调用[timer invalidate].

另一个方法是需要在合适的时候(非dealloc方法里)进行调用[timer invalidate], 原因如下:

// 源码: 会对_target和_info做release
- (void) invalidate
{
  _invalidated = YES;
  if (_target != nil)
    {
      DESTROY(_target);
    }
  if (_info != nil)
    {
      DESTROY(_info);
    }
}
一、NSProxy类介绍

这个类是与基类NSObject平级的基类,是专门用来处理消息转发的,相比NSObject对象的消息转发,这个类的效率更高,因为通过系统NSProxy源码分析可以知道,它只用两步来完成转发,省去了从父类中查找的过程,直接会进入消息转发。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector;
- (void)forwardInvocation:(NSInvocation *)invocation;
二、创建NSProxy子类及调用代码
  1. NSProxy子类
@interface XXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    NSMethodSignature *signature = [self.target methodSignatureForSelector:sel];
    return signature ?: [NSObject methodSignatureForSelector:@selector(init)];
    // 在signature =nil时传nsobject过去是为了报错正确
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}
@end
  1. NSTimer使用方法
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                  target:[XXProxy proxyWithTarget:self]
                                                selector:@selector(timerTest)
                                                userInfo:nil
                                                 repeats:YES];

可以参考第三方库里面的NSProxy,但测试及源码查看之后还是这里的为准。

三、NSTimer和CADisplayLink、GCD创建子线程后添加timer三种比较
  1. NSTimer在使用时如果遇到了runloop执行了耗时的操作如下,会出现timer被延时触发的问题,所以timer是相对不精准的,适合于对时间精确度要求不那么高的情况(如轮播图)。
    CFAbsoluteTime refTime = CFAbsoluteTimeGetCurrent();
    NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer fire %f",CFAbsoluteTimeGetCurrent() - refTime);
    }];
    timer.tolerance = 0.5;
    [timer fire];// 立即触发一次
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"before busy %f", CFAbsoluteTimeGetCurrent() - refTime);
        NSInteger j;
        for (long  i = 0; i< 1000000000; i++) {
            j = i*3;
        }
        NSLog(@"after busy %f", CFAbsoluteTimeGetCurrent() - refTime);
    });

打印如下:

2020-10-24 09:08:57.333 KVO[5366:421594] timer fire 0.000056
2020-10-24 09:09:01.334 KVO[5366:421594] before busy 4.001167
2020-10-24 09:09:03.503 KVO[5366:421594] after busy 6.170045
2020-10-24 09:09:03.503 KVO[5366:421594] timer fire 6.170465
2020-10-24 09:09:07.337 KVO[5366:421594] timer fire 10.004278
2020-10-24 09:09:12.339 KVO[5366:421594] timer fire 15.006128

可见timer的触发被延时到了after busy之后。

  1. CADisplayLink是跟屏幕刷新率相关的定时器,可以设置preferredFramesPerSecond属性每秒多少次触发调用selector, 在当前线程未执行耗时任务时是非常精准的;但如果当前线程的卡顿,会导致CADisplayLink卡顿。CADisplayLink适合用于跟屏幕刷新率相关的操作。
+ (void)beginDisplayLink{
    CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(test:)];
    [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    link.preferredFramesPerSecond = 2;// 1秒打印两次
}

+ (void)test:(CADisplayLink *)link{
    NSLog(@"CADisplayLink触发-----");
// 2秒后加耗时任务卡顿线程
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSInteger j;
        for (long  i = 0; i< 1000000000; i++) {
            j = i*3;
        }
    });
}

打印结果:

2020-10-24 10:17:48.167 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:48.652 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:49.153 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:49.652 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:50.153 KVO[5617:453444] CADisplayLink触发-----
2020-10-24 10:17:52.527 KVO[5617:453444] CADisplayLink触发-----

由上面的时间戳可以看出,前5次打印来看是非常精准的,但加了耗时操作卡顿线程会导致selector触发延时。

  1. GCD创建子线程,在子线程中添加timer,这种情况是非常精准的,不会受其他线程卡顿的影响,适合于对时间精确度敏感的时候(比如发送短信验证码时).
+ (void)GCDSubThreadTimer{
    
    __block NSTimeInterval timeOut = 5;
    dispatch_source_t source_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    dispatch_source_set_timer(source_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0);
    dispatch_source_set_event_handler(source_timer, ^{
        if (timeOut <= 0) {
            dispatch_source_cancel(source_timer);// 在不使用时手动关闭这个定时器事件源
        }else{
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"倒计时:%f", timeOut);
            });
            timeOut--;
        }
    });
    dispatch_resume(source_timer);
}

打印:

2020-10-24 10:52:55.164 KVO[5787:472530] 倒计时:5.000000
2020-10-24 10:52:56.165 KVO[5787:472530] 倒计时:4.000000
2020-10-24 10:52:57.164 KVO[5787:472530] 倒计时:3.000000
2020-10-24 10:52:58.165 KVO[5787:472530] 倒计时:2.000000
2020-10-24 10:52:59.164 KVO[5787:472530] 倒计时:1.000000
上一篇下一篇

猜你喜欢

热点阅读