前言
今天看別人的代碼, 發(fā)現(xiàn)用到了NSProxy這個(gè)類, 就查了一下, 然后就發(fā)現(xiàn), 自己用了這么久的定時(shí)器NSTimer
, 居然大部分都會(huì)有內(nèi)存問題, 就覺得必須記錄一下, 如果你也像我一樣用的NSTimer
, 那你可能就要注意了, 請(qǐng)看如下問題代碼:
@property (nonatomic, weak) NSTimer *timer;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// 定時(shí)器 重不重復(fù)沒影響
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerRunning) userInfo:nil repeats:YES];
// 這句話 是為了讓滑動(dòng)scrollView的時(shí)候定時(shí)器不會(huì)停止, 加不加對(duì)今天的問題沒影響
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)timerRunning
{
NSLog(@"1");
}
- (void)dealloc
{
NSLog(@"FirstViewController --dealloc");
[self.timer invalidate];
}
上面的用法是有問題的, NSTimer必須要調(diào)用invalidate
方法, 才能釋放, 然而上面的- dealloc
方法就不會(huì)走, 所以定時(shí)器也不會(huì)釋放。(不信的可以親自試試, 一定要用target-action
的模式, 用block
是不會(huì)出現(xiàn)這個(gè)問題的, 想知道為什么, 繼續(xù)往下看)
無法釋放的原因如下原因如下:
其實(shí)產(chǎn)生循環(huán)引用
的根本就是引用計(jì)數(shù)
, 然而上面的情況并不僅僅是循環(huán)引用
, 如果不用屬性保存NSTimer
, pop
控制器后, 依然會(huì)造成定時(shí)器
在后臺(tái)打印。 我們用引用計(jì)數(shù)
來解釋原因:
- 控制器push或者present過來, 控制器的引用計(jì)數(shù)+1
- 添加控制器相當(dāng)于在
Runloop
中注冊(cè)timer
,Runloop
會(huì)強(qiáng)引用定時(shí)器
-
定時(shí)器
通過target-action
的方式引用控制器,target-action
的設(shè)計(jì)模式中, 對(duì)target
的引用應(yīng)該是弱引用的, 為什么會(huì)造成強(qiáng)引用, 我猜測(cè)(知道真相的小伙伴可以留言告訴我)可能是NSTimer
把控制器交給runloop
進(jìn)行強(qiáng)引用, 以便于在到達(dá)注冊(cè)時(shí)間
時(shí)發(fā)送消息, 因此即便用弱引用的weakSelf
修飾控制器, 依然無法解決內(nèi)存無法釋放的問題, 因?yàn)槟憧刂破鞯闹羔槀鞯搅藃unloop手里, runloop就將控制器的引用計(jì)數(shù)+1
了拆讯。 - 當(dāng)
pop
或者dismiss
的時(shí)候, 控制器引用計(jì)數(shù)-1
, 然后界面消失, 你就再也找不到控制器了, 然而控制器的引用計(jì)數(shù)
還有1
呢, 控制器的內(nèi)存就泄露啊, 沒有被銷毀; 因?yàn)?code>引用計(jì)數(shù)從1到0的時(shí)候才會(huì)調(diào)用dealloc
方法, 因此,定時(shí)器
也沒有被invalidate
, 它會(huì)在后臺(tái)一直循環(huán)打印, 不勝其煩!
那么這個(gè)問題怎么解決呢?
我之前的解決方式是, 在viewDidDisappear
的時(shí)候調(diào)用invalidate
, 雖然解決了問題, 但是還是有新問題的, 因?yàn)椴恢故窍У臅r(shí)候viewDidDisappear
會(huì)走, push
和present
控制器的時(shí)候viewDidDisappear
也會(huì)走啊, 那么怎么辦?
NSProxy就是你的曙光了
NSProxy
是iOS開發(fā)中一個(gè)消息轉(zhuǎn)發(fā)的基類驯耻,它不繼承自NSObject
。因?yàn)樗彩?code>Foundation框架中的基類, 通常用來實(shí)現(xiàn)消息轉(zhuǎn)發(fā)
, 我們也可以用它來包裝控制器, 達(dá)到弱引用的效果崔慧。PS: 如果你只是想要解決以上的問題, 可以完全不用理解消息轉(zhuǎn)發(fā)機(jī)制
, 直接使用代碼就夠了, 用法超級(jí)簡(jiǎn)單; 如果你想了解, 請(qǐng)點(diǎn)擊這里
NSProxy是一個(gè)抽象類, 需要使用它的子類, 然后需要實(shí)現(xiàn)init以及消息轉(zhuǎn)發(fā)的相關(guān)方法拂蝎。
// 當(dāng)一個(gè)消息轉(zhuǎn)發(fā)的動(dòng)作NSInvocation到來的時(shí)候,在這里選擇把消息轉(zhuǎn)發(fā)給對(duì)應(yīng)的實(shí)際處理對(duì)象
- (void)forwardInvocation:(NSInvocation *)anInvocation
// 當(dāng)一個(gè)SEL到來的時(shí)候惶室,在這里返回SEL對(duì)應(yīng)的NSMethodSignature
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
// 是否響應(yīng)一個(gè)SEL
+ (BOOL)respondsToSelector:(SEL)aSelector
首先創(chuàng)建一個(gè)NSProxy
的子類WeakProxy
, 并在.h
文件中聲明以下屬性和方法
@interface WeakProxy : NSProxy
@property (weak,nonatomic,readonly)id target;
+ (instancetype)proxyWithTarget:(id)target;
- (instancetype)initWithTarget:(id)target;
@end
.m
里實(shí)現(xiàn)如下
@implementation WeakProxy
- (instancetype)initWithTarget:(id)target{
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target{
return [[self alloc] initWithTarget:target];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = [invocation selector];
if ([self.target respondsToSelector:sel]) {
[invocation invokeWithTarget:self.target];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [self.target methodSignatureForSelector:aSelector];
}
- (BOOL)respondsToSelector:(SEL)aSelector{
return [self.target respondsToSelector:aSelector];
}
@end
以上代碼就可以用了, 用法如下:
@property (nonatomic, weak) NSTimer *timer;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[WeakProxy proxyWithTarget:self] selector:@selector(timerRunning) userInfo:nil repeats:YES];
}
- (void)timerRunning
{
NSLog(@"1");
}
- (void)dealloc
{
NSLog(@"FirstViewController --dealloc");
[self.timer invalidate];
}
以上, 我們就解決了定時(shí)器不釋放的問題, 解決原理如下:
我們依然從引用計(jì)數(shù)的角度分析:
-
push
或present
進(jìn)入控制器,引用計(jì)數(shù)+1
- 定時(shí)器向
NSRunloop
注冊(cè)事件,NSRunloop
強(qiáng)引用WeakProxy
對(duì)象,WeakProxy
弱引用控制器(因?yàn)槭侨跻? 引用計(jì)數(shù)不變), 當(dāng)?shù)竭_(dá)定時(shí)器注冊(cè)的時(shí)間
時(shí),Runloop
會(huì)向WeakProxy
對(duì)象發(fā)送消息,WeakProxy
觸發(fā)消息轉(zhuǎn)發(fā), 把消息轉(zhuǎn)發(fā)給弱引用的控制器處理 - 當(dāng)
pop
或dismiss
控制器時(shí),引用計(jì)數(shù)-1 變?yōu)?
觸發(fā)-dealloc
方法, 調(diào)用定時(shí)器的invalidate
注銷了定時(shí)器, 同時(shí)Runloop
注銷掉對(duì)WeakProxy
對(duì)象的強(qiáng)引用, 至此, 所有的內(nèi)存都被釋放了, 問題就解決了