NSTimer在runloop中我们有接触过,当时只是简单了解,NSTimer其实是很重要的一部分知识,我们需要好好了解一下这部分和循环引用部分的理解
问题引入
我们已几个问题来展开
- 什么是循环引用?
- NSTimer里面为什么要强引用target?
- 有什么更好的方式来解决这种循环引用吗?
循环引用
简单理解的循环引用
简单理解就是
对象A持有对象B,同时对象B也持有对象A
可能还是有点抽象
可以举个例子
@interface FirstPerson : NSObject
@property (nonatomic, strong) SecondPerson *test;
@end
复制代码
@interface SecondPerson : NSObject
@property (nonatomic, strong) FirstPerson *test;
@end
复制代码
如果不是循环引用,就是单向持有,假设是A持有B,也就是B是A的一个属性
若对A发送release消息,发现持有对象B,则向对象B发送release消息,B对象执行dealloc方法,引用计数为0,释放内存,同时A对象引用计数也为0,内存得到正确释放
如果是循环引用
若对A发送release消息,发现持有B对象,则会向B对象发送release消息,等待b释放内存。B收到release消息后,发现持有A,于是也向A发送release消息,等待A释放内存。此时就会发生A等待B释放内存,B又等待A释放内存,造成了死锁,发生内存泄漏
如果我们使用weak就可以破解这个循环引用
若对A发送release消息,发现持有B对象,则会向B对象发送release消息,等待B释放内存。B收到release消息后,虽然持有A(weak),但不会等待A释放内存,此时引用计数为0,执行dealloc,释放内存,同时A的引用计数变成了0,执行dealloc,释放内存
Block中的循环引用强弱共舞
如果我们在某个页面的属性中声明了一个block,那么就相当于我们这个页面的self持有了这个block
如果我们在block再持有self,这个时候就会造成循环引用
怎么解决呢?
持有个“假self”就行了(弱引用一下)
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
__weak typeof(self) weakSelf = self;
self.blk = ^{
//5秒后执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
});
};
self.blk();
[self dismissViewControllerAnimated:YES completion:nil];
}
复制代码
self对象持有block,block里面又持有self,为了破除循环引用,使用weak也没啥问题
但是如果是在新视图中呢?我们在执行完block后直接就把该视图dismiss掉了,这个时候会出现什么问题?
什么都不加的情况,我们dismiss掉了之后,并不会走dealloc,其内部还在相互循环等待,造成页面dismiss了,但是dealloc方法并没有走,导致了内存泄漏。
我们仅仅加上weak
五秒钟之后打印self,就是这个新视图,视图dismiss了,不存在了,再打印self,肯定就是null了
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
__weak typeof(self) weakSelf = self;
self.blk = ^{
__strong typeof(self) strongSelf = weakSelf;
//5秒后执行
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", weakSelf);
});
};
self.blk();
[self dismissViewControllerAnimated:YES completion:nil];
}
复制代码
这样就行了
外部的weak是为了打破循环引用,内部的strongSelf是为了让新页面的对象retain+1,当外部释放对象时,此时其引用计数不为0,所以不能释放。由于strongSelf是在栈上 分配的内存,一旦执行完后,对象的引用计数变成了0,内存得到了释放。
NSTimer
之前学习的循环引用部分我们就结束了,下面步入正题NSTimer
创建NSTimer
创建NSTimer的常用方法是
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
不常用方法是
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
和
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
这几种方法除了创建方式(参数)不同,方法类型不同(类方法,对象方法),还有什么不同?
我们常用的scheduledTimerWithTimeInterval
相比与其他两种方法,其不仅仅是创建了NSTimer对象,还把该对象加入到了当前的runloop中
NSTimer只有被加入到runloop中,才会生效,NSTimer才会真正执行。
这也就是说,如果我们想使用timerWithTimeInterval或initWithFireDate的话,需要使用NSRunloop的以下方法将NSTimer加入到runloop中
- (void)addTimer:(NSTimer *)aTimer forMode:(NSString *)mode
销毁NSTimer
之前没有考虑过什么方法,之前理解的是
我直接置nil不就完事了,系统会帮我们回收
但其实并不是这样的
invalidate与fire
看一下官方文档里这两个的意思
- invalidate
Stops the receiver from ever firing again and requests its removal from its run loop
This method is the only way to remove a timer from an NSRunLoop object
阻止接收器再次触发,并请求将其从runloop中移除
此方法是从NSRunLoop对象中删除计时器的唯一方法
- fire
Causes the receiver’s message to be sent to its target
If the timer is non-repeating, it is automatically invalidated after firing
使接收方的消息发送到其目标
如果计时器不重复,则会在触发后自动失效
总之,如果想要销毁NSTimer,那么一定要使用invalidate方法
invalidate与 = nil
就像销毁其他强引用对象一样,我们能否将NSTimer置nil,让iOS系统帮我们销毁NSTimer呢?
答案是否定的
为什么呢?
我们看一下ARC下的引用计数应该就明白了
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
_timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timerSelector) userInfo:nil repeats:NO];
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
[_timer invalidate];
NSLog(@"Retain count is %ld", CFGetRetainCount((__bridge CFTypeRef)self));
}
- (void) timerSelector {
NSLog(@"----");
}
复制代码
我们打印self(就是当前VC)的引用计数
加入NSTimer后,引用计数从29变成了30
怎么理解呢?
其实就是timer对VC进行了强引用。
因为如果要让timer运行的时候执行VC下面的timerSelector:,timer需要知道target,并且保存这个target。以便于在以后执行[target performSelector:],这里的target就是指VC(self)。
所以,NSTimer和VC时相互强引用的。这样看来就形成了循环引用的问题。
为了解除循环引用,在invalidate
这个方法下,timer之前保存的target被设置成了nil,强制断开了循环引用。这点和直接设置timer = nil是差不多的。但是invalidate还做了另外一个动作,就是解除了runLoop对timer的强引用,使得timer成功停止。
我们从官方文档中可以看到这一点
- 解除强引用
- 从runloop中移除该定时器(解除RunLoop对timer的强引用)
所以在创建NSTimer对象的类的dealloc里面去invalidate timer。这就是想当然的做法,因为是强引用的,在没有被invalidate之前,dealloc都不可能被执行到
面试题
- NSTimer一定会持有self吗?
从官方文档看三种创建方法
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats
这三种方法无一例外,根据官方文档,都会对target产生强引用
所以,只要使用了target-action就会引用self。
苹果应该也发现了timer导致的内存泄漏的问题,所以iOS10之后,这三个出了对应新的API,
调用这些新的API不会导致内存泄漏的问题
- 如何解决NSTimer强持有的问题
合适的时机销毁Timer,或者使用一个代理对象,让timer引用代理对象,代理对象弱引用self。最合适的就是使用代理对象转发,系统提供的NSProxy(消息转发机制)。我们下面说
- 那这里self不持有NSTimer也会出现循环引用吗
感觉像是设套的 你不能回答是或者不是 你只能说和循环引用无关 是大家相互引用,导致无法释放。使用NSTimer你需要加到Runloop中,如果是target-action,NSTimer是需要引用target也就是self。这样就形成了一个runloop->timer->self 都是强引用。大家都释放不了
- 那runloop会持有NSTimer吗
NSTimer只有被加入到runloop中,才会生效,NSTimer才会真正执行,真正执行了RunLoop就会持有NSTimer。
如何解决NSTimer强持有的问题?
这是上面的第二个面试题
中间的代理对象
使用NSObject类实现消息转发
图就长这样
正常使用target-action,当页面销毁时,不走dealloc方法,造成循环引用
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TestProxy proxyWithTarget:self] selector:@selector(timerSelector) userInfo:nil repeats:YES];
@interface TestProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (nonatomic, weak) id target;
@end
#import "TestProxy.h"
@implementation TestProxy
+ (instancetype) proxyWithTarget:(id)target {
TestProxy *proxy = [[TestProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}
@end
复制代码
我们将target的self换成中间类,进行消息转发
成功打印dealloc
使用NSProxy类实现消息转发
这是一个专门用于做消息转发的类,我们需要通过子类的方式来使用它
需要注意
NSProxy的子类需要实现两个方法,就是上面那两个,即:methodSignatureForSelector:和forwardInvocation:
@interface TestNSProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (nonatomic, weak) id target;
@end
#import "TestNSProxy.h"
@implementation TestNSProxy
+ (instancetype)proxyWithTarget:(id)target {
TestNSProxy *proxy = [TestNSProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
复制代码
NSProxy类的优点在于直接执行了消息转发机制三次拯救的第三步的调用methodSignatureForSelector:返回方法签名,如果方法签名不为nil,调用forwardInvocation:来执行该方法,会跳过前面的步骤,提高性能
改变timer引用
我一开始就想着 我们解决block中的循环引用是怎么解决的?__weak typeof(self) weakSelf = self;
,然后持有这个假self。但事实上,我们在这里使用target-action+“假self”并不能解决
为什么?
我们在block说过,如果外面是个强指针,block引用的时候哪股就用强指针保存,如果外面是个弱指针,block引用的时候内部就用弱指针保存,所以对于block我们使用weakSelf有用。
但是对于CADisplayLink和NSTimer来说,无论外面传递的是弱指针还是强指针,都会传入一个内存地址,定时器内部都是对这个内存地址产生强引用,所以传递弱指针没有用。
那么用block就可以达到这个目的
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf doSomething];
}];
复制代码
在合适的地方调用invalidate方法
这算一个比较取巧的。 最合适的就是使用代理对象转发,系统提供的NSProxy。