最近在復(fù)習(xí)iOS中NSTimer的知識(shí),有一些新的收獲,因此記錄下來。
廢話不多說,先來看看timer最常用的寫法。
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- (void)timerRun {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"%s", __func__);
}
@end
這里的TimerViewController是從上一個(gè)控制器push過來的,當(dāng)進(jìn)入當(dāng)前控制器的時(shí)候,timerRun正常執(zhí)行。
但是當(dāng)回退到上一個(gè)控制器的時(shí)候,發(fā)現(xiàn)并沒有走當(dāng)前控制器的dealloc方法,也就是說當(dāng)前控制器并沒有被釋放,那么當(dāng)前控制器強(qiáng)引用的timer對(duì)象也沒有被釋放,這就造成了內(nèi)存泄漏,如下圖所示。
這里的每一個(gè)箭頭代表著一個(gè)強(qiáng)指針,當(dāng)回退上一個(gè)界面時(shí),NavigationController指向TimerViewController的強(qiáng)引用被銷毀,但是TimerViewController和timer之間互相強(qiáng)引用,內(nèi)存泄漏。

1. 方案一
既然說TimerViewController和timer之間互相強(qiáng)引用,那么如果將之間的一個(gè)強(qiáng)指針改為弱指針也許能解決問題,于是有了下面的代碼
@interface TimerViewController ()
// 這里變?yōu)榱藈eak
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 這里先添加到當(dāng)前runloop再賦值給timer
self.timer = timer;
}
- (void)timerRun {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"%s", __func__);
}
經(jīng)過上面的改造,現(xiàn)在TimerViewController和timer之間有一個(gè)為弱指針,但是運(yùn)行代碼發(fā)現(xiàn),內(nèi)存泄漏的問題仍然沒有得到解決。
因?yàn)檫@里雖然沒有循環(huán)引用,但是RunLoop引用著timer,而timer又引用著TimerViewController,雖然pop時(shí)指向TimerViewController的強(qiáng)指針銷毀,但是仍然有timer的強(qiáng)指針指向TimerViewController,因此仍然還是內(nèi)存泄漏,如下圖所示。

2. 方案二
既然從左邊不行,那么試試從右邊可不可以。
在這里加入了一個(gè)中間代理對(duì)象LJProxy,TimerViewController不直接持有timer,而是持有LJProxy實(shí)例,讓LJProxy實(shí)例來弱引用TimerViewController,timer強(qiáng)引用LJProxy實(shí)例,直接看代碼
@interface LJProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJProxy
+ (instancetype) proxyWithTarget:(id)target
{
LJProxy *proxy = [[LJProxy alloc] init];
proxy.target = target;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
Controller里只修改了下面一句代碼
- (void)viewDidLoad {
[super viewDidLoad];
// 這里的target發(fā)生了變化
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
- 首先當(dāng)執(zhí)行pop的時(shí)候,1號(hào)指針被銷毀,現(xiàn)在就沒有強(qiáng)指針再指向TimerViewController了,TimerViewController可以被正常銷毀。
- TimerViewController銷毀,會(huì)走dealloc方法,在dealloc里調(diào)用了[self.timer invalidate],那么timer將從RunLoop中移除,3號(hào)指針會(huì)被銷毀。
- 當(dāng)TimerViewController銷毀了,對(duì)應(yīng)它強(qiáng)引用的指針也會(huì)被銷毀,那么2號(hào)指針也會(huì)被銷毀。
-
上面走完,timer已經(jīng)沒有被別的對(duì)象強(qiáng)引用,timer會(huì)銷毀,LJProxy實(shí)例也就自動(dòng)銷毀了。
image.png
這里需要注意的有兩個(gè)地方:
1.- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
了解iOS消息轉(zhuǎn)發(fā)的朋友肯定知道這個(gè)東西,不了解的可以去這個(gè)博客看看
(http://m.itdecent.cn/p/eac6ed137e06)。
簡(jiǎn)單來說就是如果當(dāng)前對(duì)象沒有實(shí)現(xiàn)這個(gè)方法,系統(tǒng)會(huì)到這個(gè)方法里來找實(shí)現(xiàn)對(duì)象。
本文中由于LJProxy沒有實(shí)現(xiàn)timerRun方法(當(dāng)然也不需要它實(shí)現(xiàn)),讓系統(tǒng)去找target實(shí)例的方法實(shí)現(xiàn),也就是去找TimerViewController中的方法實(shí)現(xiàn)。
2.timer的invalidate方法的具體作用參考蘋果官方,這個(gè)方法會(huì)停止timer并將其從RunLoop中移除。
This method is the only way to remove a timer from an [NSRunLoop]object. The NSRunLoop object removes its strong reference to the timer, either just before the [invalidate] method returns or at some later point.
3. 方案三
經(jīng)過上面的改造,似乎timer已經(jīng)沒有什么問題了,但是在瀏覽yykit源碼的時(shí)候發(fā)現(xiàn)了一個(gè)以前一直沒有注意過的類NSProxy,這是一個(gè)專門用于做消息轉(zhuǎn)發(fā)的類,我們需要通過子類的方式來使用它。
參考YYTextWeakProxy的寫法,寫了如下的代碼
@interface LJWeakProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJWeakProxy
+ (instancetype)proxyWithTarget:(id)target {
LJWeakProxy *proxy = [LJWeakProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
Controller里修改了如下代碼
- (void)viewDidLoad {
[super viewDidLoad];
// 這里的target又發(fā)生了變化
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJWeakProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
看上去這次的LJWeakProxy和前面的LJProxy似乎沒有什么區(qū)別,好像LJWeakProxy還更復(fù)雜一些,但是還是稍微整理一下
- LJProxy的父類為NSObject,LJWeakProxy的父類為NSProxy。
- LJProxy只實(shí)現(xiàn)了forwardingTargetForSelector:方法,但是LJWeakProxy沒有實(shí)現(xiàn)forwardingTargetForSelector:方法,而是實(shí)現(xiàn)了methodSignatureForSelector:和forwardInvocation:。
下面是NSProxy的Apple文檔說明,簡(jiǎn)單來說提供了幾個(gè)信息
- NSProxy是一個(gè)專門用來做消息轉(zhuǎn)發(fā)的類
- NSProxy是個(gè)抽象類,使用需自己寫一個(gè)子類繼承自NSProxy
- NSProxy的子類需要實(shí)現(xiàn)兩個(gè)方法,就是上面那兩個(gè)
NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation: and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forwardInvocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. methodSignatureForSelector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethodSignature object accordingly. See the NSDistantObject, NSInvocation, and NSMethodSignature class specifications for more information
說了那么多,到底NSProxy的好處在哪呢?
如果了解了OC中消息轉(zhuǎn)發(fā)的機(jī)制,那么你肯定知道,當(dāng)某個(gè)對(duì)象的方法找不到的時(shí)候,也就是最后拋出doesNotRecognizeSelector:的時(shí)候,它會(huì)經(jīng)歷幾個(gè)步驟
- 消息發(fā)送,從方法緩存中找方法,找不到去方法列表中找,找到了將該方法加入方法緩存,還是找不到,去父類里重復(fù)前面的步驟,如果找到底都找不到那么進(jìn)入2。
- 動(dòng)態(tài)方法解析,看該類是否實(shí)現(xiàn)了resolveInstanceMethod:和resolveClassMethod:,如果實(shí)現(xiàn)了就解析動(dòng)態(tài)添加的方法,并調(diào)用該方法,如果沒有實(shí)現(xiàn)進(jìn)入3。
- 消息轉(zhuǎn)發(fā),這里分二步
- 調(diào)用forwardingTargetForSelector:,看返回的對(duì)象是否為nil,如果不為nil,調(diào)用objc_msgSend傳入對(duì)象和SEL。
- 如果上面為nil,那么就調(diào)用methodSignatureForSelector:返回方法簽名,如果方法簽名不為nil,調(diào)用forwardInvocation:來執(zhí)行該方法
從上面可以看出,當(dāng)繼承自NSObject的對(duì)象,方法沒有找到實(shí)現(xiàn)的時(shí)候,是需要經(jīng)過第1步,第2步,第3步的操作才能拋出錯(cuò)誤,如果在這個(gè)過程中我們做了補(bǔ)救措施,比如LJProxy就是在第3步的第1小步做了補(bǔ)救,那么就不會(huì)拋出doesNotRecognizeSelector:,程序就可以正常執(zhí)行。
但是如果是繼承自NSProxy的LJWeakProxy,就會(huì)跳過前面的所有步驟,直接到第3步的第2小步,直接找到對(duì)象,執(zhí)行方法,提高了性能。
有人可能覺得那為什么不在第3步的第1小步來做補(bǔ)救呢?
但是很不巧,NSProxy只有methodSignatureForSelector:和forwardInvocation:這兩個(gè)方法,官方的文檔里也是讓實(shí)現(xiàn)這兩個(gè)方法。
4. 方案四
上面說了那么多,其實(shí)還有一種更加簡(jiǎn)單的方式來解決這里內(nèi)存泄漏的問題,代碼如下
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerRun];
}];
}
- (void)timerRun {
NSLog(@"%s", __func__);
}
- (void)dealloc {
[self.timer invalidate];
NSLog(@"%s", __func__);
}
上面這種方式雖然TimerViewController強(qiáng)引用timer,但是timer并沒有強(qiáng)引用TimerViewController(由于這里的weakSelf),因此不會(huì)出現(xiàn)內(nèi)存泄漏的問題。
如何在子線程使用NSTimer
有的時(shí)候需要在子線程使用NSTimer,下面是代碼
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) LJThread *thread;
@property (assign, nonatomic) BOOL stopTimer;
@end
@implementation TimerViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJWeakProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES] ;
__weak typeof(self) weakSelf = self;
self.thread = [[LJThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSDefaultRunLoopMode];
// 這里需要注意不要使用[[NSRunLoop currentRunLoop] run]
while (weakSelf && !weakSelf.stopTimer) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.thread start];
}
- (void)timerRun {
NSLog(@"thread - %@ - %s", [NSThread currentThread], __func__);
}
- (void)dealloc {
[self stop];
}
- (void)stop {
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用于停止子線程的RunLoop
- (void)stopThread {
// 設(shè)置標(biāo)記為YES
self.stopTimer = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
// 清空線程
self.thread = nil;
}
這里L(fēng)JThread繼承了NSThread,可以看到上面代碼中在LJThread的Block中沒有通過[[NSRunLoop currentRunLoop] run]來開啟當(dāng)前線程的RunLoop,而是使用了runMode: beforeDate:。
這是由于通過run方法開啟的RunLoop是無法停止的,但在控制器pop的時(shí)候,需要將timer,子線程,子線程的RunLoop停止和銷毀,因此需要通過while循環(huán)和runMode: beforeDate:來運(yùn)行RunLoop。
通過上面的總結(jié)可以看到,雖然只是一個(gè)小小的Timer,也有這么多地方需要我們注意。
其他:
- Timer到底準(zhǔn)不準(zhǔn)?
可以參考這篇文章:http://m.itdecent.cn/p/d5845842b7d3
