iOS程序员移动端开发

iOS 定时器(NSTimer/CADisplayLink)循环

2019-03-11  本文已影响114人  JustEverOnce

前言:最近呢,稍微有点空闲的时间,就细看了下年前接手的代码,年前基本上都是新增功能,改一些前任所留下来的bug,上面一直忙着催进度,所以没有细看具体的各模块代码,看到相关定时器的代码,感觉需要改进的部分还有很大空间,这份代码经历的人手比较多,时间也比较久远,所以整份代码的质量也是层次不齐,看到这里,就简单整理下定时器相关的内容,这里主要侧重循环引用部分,发现问题 --> 部分解决方案 --> 初步解决方案 --> 最终合理的解决方案

iOS定时器分类:

NSTimer

    /**
     NSTimer

     @param NSTimeInterval 等待时间(单位是秒,即就是每个几秒执行一次)
     @param target 执行对象
     @param selector 执行方法(定时器执行的方法)
     @param userInfo 标识信息(一般不用)
     @param repeats 是否重复执行
     @return NSTimer对象(定时器)
     */
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    /**
     NSTimer block的创建方式

     @param TimerInterval 等待时间
     @param repeats 是否重复执行
     @param block 执行的代码
     @return NSTimer对象
     */
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
       
        //重复执行的代码
    }];
#pragma mark --- 这种方式比较冗余,所以开发中使用较少(个人经验)
    SEL action = @selector(timerAction:);
    NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:action]];
    [invocation setTarget:self];
    [invocation setSelector:action];
    NSTimer* timer = [NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES];
#pragma mark --- 参数参考上述参数
    NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
#pragma mark --- 参数参考上述参数
    NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

        NSLog(@"-----");
    }];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
#pragma mark --- 参数参考上述参数
    SEL action = @selector(timerAction:);
    NSInvocation* invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:action]];
    [invocation setTarget:self];
    [invocation setSelector:action];
    NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 invocation:invocation repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

        NSLog(@"----");
    }];

    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

NSTimer创建小结:

1.上述创建方式(类对象创建,不需要手动添加至RunLoop)最终所得的NSTimer对象会被默认添加至NSRunLoop中,所以创建之后就可以启动,也就是执行对应的timerAction方法
2.其他的创建方式最终得到的NSTimer对象需要手动添加至对应的NSRunLoop中,不然定时器无法启动,绝大多数情况我们使用[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];即可
3.NSTimer创建方式,以scheduledTimerWithTimeInterval开头的方式不需要手动添加至NSRunLoop中,否则需要手动添加至NSRunLoop
4.[NSRunLoop mainRunLoop] addTimer:<#(nonnull NSTimer *)#> forMode:(nonnull NSRunLoopMode)这里的NSRunLoopMode解释一下,API里面有2种方式:NSDefaultRunLoopMode/NSRunLoopCommonModes

FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

记得这个NSRunLoopMode底层不止2中,这个后面有机会再说,NSDefaultRunLoopMode为默认模式,上述1.1的创建方式下NSTimer
对应的NSRunLoopMode模式就是NSDefaultRunLoopMode,如下所示


image

(如果页面有交互,最常见的如UIScrollerView、UITableview、UITextView等滑动,定时器会在互动的过程中暂定工作,滑动结束后NSTimer会再次工作),NSRunLoopCommonModes模式下可以避免这种情况,页面滑动不会对定时器产生影响

NSTimer 其他常用API

self.timer.fireDate = [NSDate distantFuture];//停止定时器
self.timer.fireDate = [NSDate distantPast];//重新启动定时器

CADIsplayLink

#pragma mark --- 参数参考上述参数
    CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
    [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

注意:同样创建需要添加至NSRunLoop中,对应的NSRunLoopMode同NSTimer一样

/* Removes the receiver from the given mode of the runloop. This will
 * implicitly release it when removed from the last mode it has been
 * registered for. */

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

/* Removes the object from all runloop modes (releasing the receiver if
 * it has been implicitly retained) and releases the 'target' object. */

- (void)invalidate;
 * display link fires. Default value is one, which means the display
 * link will fire for every display frame. Setting the interval to two
 * will cause the display link to fire every other display frame, and
 * so on. The behavior when using values less than one is undefined.
 * DEPRECATED - use preferredFramesPerSecond. */

@property(nonatomic) NSInteger frameInterval
  API_DEPRECATED("preferredFramesPerSecond", ios(3.1, 10.0), 
                 watchos(2.0, 3.0), tvos(9.0, 10.0));

/* Defines the desired callback rate in frames-per-second for this display
 * link. If set to zero, the default value, the display link will fire at the
 * native cadence of the display hardware. The display link will make a
 * best-effort attempt at issuing callbacks at the requested rate. */

@property(nonatomic) NSInteger preferredFramesPerSecond
    API_AVAILABLE(ios(10.0), watchos(3.0), tvos(10.0));

frameInterval 在iOS10.0以后弃用,改用preferredFramesPerSecond,看注释这个可以大概可以当做回调速率,以每秒的帧数为单位,如果设置为0(默认值)的时候与硬件相关,iOS设备帧数为60FPS,所以就可以解释一秒调用60次了,更改该属性即可控制定时器的间隔时间,例如:如果一秒钟调用2次,那么可以设置preferredFramesPerSecond为30(60/30),一秒钟调用1次,可以设置preferredFramesPerSecond为1(60/60),注意如果preferredFramesPerSecond小于1,那就相当于默认值。

定时器基本就简单介绍到这里(还有一个GCD的定时器,后面再说),接下来说说NSTimer、CADisplayLink使用不当导致的循环引用,这个才是重点呀(敲黑板,看这里,这里以NSTimer为例)
首先来说下dealloc方法(这里只说ARC环境下,系统自动调用),该方法使用场景一般分以下几点:

通常情况下,我们的定时器都是直接或者间接的依赖于控制器,iOS开发中 当控制器出栈的时候我们希望该控制器销毁,也就是说自动调用delloc方法
@interface AnotherViewController ()
/**
 NSTimer定时器
 */
@property (nonatomic, strong) NSTimer* timer;
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod:) userInfo:nil repeats:YES];
}
- (void)timerMethod:(NSTimer*) timer{

    NSLog(@"%s", __func__);
}
-(void)dealloc{

    NSLog(@"%s", __func__);
    [self.timer invalidate];
    self.timer = nil;
}

上面的代码由于循环引用导致控制器出栈的时候没有被调用,简单分析如下:


图片.png

NSTimer与Controller相互引用,导致没有办法释放,所以最终的结果就是dealloc方法没有调用,这里主要是由于NSTimer的target参数,现提供一种思路,借助第三方对象来打破这种引用,具体思路如下:


图片.png

思路有了,具体代码实现就相对比较容易了,如下所示:

#import <Foundation/Foundation.h>
@interface JCProxy : NSObject
@property (weak, nonatomic) id target;
+(instancetype) proxyWithTarget:(id) target;

@end
#import "JCProxy.h"

@implementation JCProxy

+(instancetype) proxyWithTarget:(id) target{
    JCProxy* proxy = [[JCProxy alloc] init];
    proxy.target = target;
    return proxy;
}

#pragma mark --- 消息转发
-(id)forwardingTargetForSelector:(SEL)aSelector{
    return self.target;
}
//
//-(NSMethodSignature*) methodSignatureForSelector:(SEL)aSelector{
//    
//}
//
//-(void)forwardInvocation:(NSInvocation *)anInvocation{
//    
//}

@end

注意点:
1、target的声明使用weak关键字
2、我们希望NSTimer的方法最终实现在Controller中,这里采用消息转发(我们只需要实现forwardingTargetForSelector即可),消息转发后面再说,目前主要解决循环引用
这里最终NSTimer的使用就采用如下方式,其他与上面完全一致

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

至此,当前Controller出栈时就会自动调用dealloc方法,运行结果如下:


图片.png

到这里,基本上已经解决NSTimer/CADisplayLink循环引用的问题了,问题是解决了,但是不够专业,iOS中我们有专门处理这类问题的NSProxy
具体代码实现如下:

#import <Foundation/Foundation.h>
@interface JCKProxy : NSProxy

@property (weak, nonatomic) id target;

+(instancetype) proxyWithTarget:(id) target;

@end
#import "JCKProxy.h"

@implementation JCKProxy

+(instancetype) proxyWithTarget:(id) target{
    
#pragma mark --- NSProxy没有init方法,直接alloc就可使用
    JCKProxy* proxy = [JCKProxy alloc];
    proxy.target = target;
    return proxy;
}

#pragma mark --- 消息转发
//返回方法签名
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel{
    
    return [self.target methodSignatureForSelector:sel];
}

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

#pragma mark --- NSProxy这里没有这个方法
//-(id)forwardingTargetForSelector:(SEL)aSelector{
//    
//    return self.target;
//}

@end

最终的使用如下:

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

当前Controller出栈时候同样会自动调用dealloc方法,运行结果如下:


图片.png

这里补充一点,很多时候循环引用可以通过weak来解决,在NSTimer的使用中,weak仅仅可以解决很少的一部分,不具有代表性,weak解决循环引用一般使用在block中,因为NSTimer的创建中是可以通过block创建的,如下方法所示:

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

这种情况下,其实也是可以使用weak来打破循环引用的,如下所示:

#pragma mark --- weakSelf
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

        //这里timer weakSelf.time都是一样的
//        [weakSelf timerMethod:timer];
        [weakSelf timerMethod:weakSelf.timer];
    }];

最终的运行结果如下:

图片.png
NSTimer 解决循环引用总结:
1、block方式创建的NSTimer,可以使用__weak typeof(self) weakSelf = self;解决,其他方式创建的不行,CADisplayLink没有block创建方式,所以不适用
2、推荐方式 NSProxy方式解决因为NSTimer、CADisplayLink引起的循环引用
NSProxy注意事项:
有争议的解释.png

1、上面这张图是网上的解释,这里必须说明一点,这张图是有错误的,NSProxy是没有init、initWith方法的,何来实现之说
2、这个类专门用来做消息转发的(效率高),所以必须实现
-(NSMethodSignature*)methodSignatureForSelector:(SEL)sel -(void)forwardInvocation:(NSInvocation *)invocation,没有-(id)forwardingTargetForSelector:(SEL)aSelector 方法
3、上面解决循环引用的2种方式,一种继承自NSObject,一种继承自NSProxy,均采用消息转发,但是继承自NSProxy的效率更好,区别如下:

到这里,NSTimer、CADisplayLink 基本上算结束了,希望可以帮到有需要的同学,如有疑问,欢迎私戳

上一篇下一篇

猜你喜欢

热点阅读