前言:最近呢,稍微有点空闲的时间,就细看了下年前接手的代码,年前基本上都是新增功能,改一些前任所留下来的bug,上面一直忙着催进度,所以没有细看具体的各模块代码,看到相关定时器的代码,感觉需要改进的部分还有很大空间,这份代码经历的人手比较多,时间也比较久远,所以整份代码的质量也是层次不齐,看到这里,就简单整理下定时器相关的内容,这里主要侧重循环引用部分,发现问题 --> 部分解决方案 --> 初步解决方案 --> 最终合理的解决方案
iOS定时器分类:
NSTimer
- 创建方式(类对象创建,不需要手动添加至RunLoop)
/**
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];
- 创建方式(类对象创建,需要手动添加至RunLoop)
#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
-
@property (copy) NSDate *fireDate;
如果没有设置改属性,则默认从当前时间启动,该属性常用来管理定时器的启动与停止(这里是停止,而不是销毁定时器)
self.timer.fireDate = [NSDate distantFuture];//停止定时器
self.timer.fireDate = [NSDate distantPast];//重新启动定时器
-
@property (readonly) NSTimeInterval timeInterval;
定时器的时间间隔,即就是上面设置的TimerInterval -
- (void)invalidate;
销毁定时器,一般当我们不用定时器的时候,调用此方法对定时器惊醒销毁,减少不必要的开销(定时器对性能消耗比较大,同时也比较电量) -
@property (readonly, getter=isValid) BOOL valid;
定时器是否可用,1表示可用,0表示不可用
CADIsplayLink
- 创建方式
#pragma mark --- 参数参考上述参数
CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkAction:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
注意:同样创建需要添加至NSRunLoop中,对应的NSRunLoopMode同NSTimer一样
- 其他常用API
/* 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;
- 1.removeFromRunLoop 从给定的RunLoop模式(看注释的意思是会有特殊的情况下,该定时器可能加入了多种RunLoop模式),并且最后一个Runloop模式下时会被释放
- invalidate 从所有的RunLoop模式中移除,并且被释放(一般我都用这种,比较简单快捷,参数比较少,同NSTimer类似)
- 3.frameInterval/preferredFramesPerSecond
* 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,那就相当于默认值。
- 还有一些其他的API,可以参考相关的文档,不怎么常用,这里不做解释
CADisplayLink小结:
4.1、创建必须手动添加至对应的NSRunLoop中,添加让是不同于NSTimer(NSTimer是在RunLoop对应的模式下添加定时器,CADisplayLink是添加到对应模式的RunLoop中,这是定时器的方法),如下所示:[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
4.2、默认情况下,CADispalyLink定时器调用时间间隔与屏幕渲染保持一致,相对NSTimer精度要高很多,目前iOS设备为60FPS,即就是一分钟屏幕渲染60次,也就是说CADisplayLink定时器每隔1/60秒调用一次,因此适合做一些UI的重绘,动画等,个人觉得使用场景不如NSTimer广泛
- 还有一些其他的API,可以参考相关的文档,不怎么常用,这里不做解释
定时器基本就简单介绍到这里(还有一个GCD的定时器,后面再说),接下来说说NSTimer、CADisplayLink使用不当导致的循环引用,这个才是重点呀(敲黑板,看这里,这里以NSTimer为例)
首先来说下dealloc方法(这里只说ARC环境下,系统自动调用),该方法使用场景一般分以下几点:
- 该类被release的时候会自动调用
- 该对象应用计数[retain count]为0的时候会被自动调用
- 该对象被置为nil的时候会被自动调用
通常情况下,我们的定时器都是直接或者间接的依赖于控制器,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];
}];
最终的运行结果如下:
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的效率更好,区别如下:
- 继承自NSObject的JCProxy在查找timerMethod方法时,首先会从自己去找,没找到再去父类中查找,最终也没有timerMethod方法时候才会进行消息转发
- 继承自NSProxy的JCKProxy在查找timerMethod方法时,发现自己没有该方法,它就直接进行消息转发,所以少了去父类查找的这一步骤,耗时会更少,效率会更好
到这里,NSTimer、CADisplayLink 基本上算结束了,希望可以帮到有需要的同学,如有疑问,欢迎私戳
网友评论