iOS: NSTimer的循環引用(解決)

首先有兩個概念寫在最前:api

內存泄漏:系統分配的內存空間在使用完畢以後沒有進行及時的回收,稱之爲發生了內存泄漏。bash

內存溢出:指在申請內存的時候,沒有足夠的內存空間可使用,包括棧溢出和堆溢出。函數

下面開始啦: 首先,建立出一個循環引用, 建立一個TestViewController,建立一個timer,ui

_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(fire) userInfo:nil repeats:YES];

-(void)fire{
    NSLog(@"fire");
}

-(void)dealloc
{
    NSLog(@"%@ delloc",self);
}
複製代碼

運行上述代碼,而後TestViewController pop回去以後,你會發現fire一直在打印,此時就會形成一個循環引用的問題,本質的緣由實際上是NSTimer對當前的target(self)是一個強引用,在強引用的過程當中他們相互持有,因此他們之間就沒有辦法正常釋放。可能有同窗會說在析構函數dealloc中將 [_timer invalidate]; _timer = nil; 其實運行下來,你會發現dealloc函數沒有執行。咱們都知道,dealloc是每一個控制器中都有的一個系統方法,由系統響應執行,當前控制器銷燬時,dealloc就會被執行,但循環引用形成的當前類沒有被銷燬。atom

接下來,咱們就去解決這個NSTimer的循環引用問題:spa

1.第一種方法:在合適的時機銷燬NSTimercode

-(void)didMoveToParentViewController:(UIViewController *)parent
{
//parent == nil 當父視圖爲空(iOS8.0以後提供的api,用來管理子視圖的生命週期)
    if (!parent) {
        [_timer invalidate];
        _timer = nil;
    }
}
複製代碼

2.第二種方法:引入中間者對象

其實就是把當前的強引用轉到target,若是當前的viewcontroller可以正常回收,那麼dealloc方法就可以正常執行。生命週期

@property (nonatomic,strong) id target;
//建立一個target對象,
_target = [NSObject new];
對於當前這個_target來講,本質上是要做爲消息的處理者,顯然_target須要一個selector,因此咱們動態添加一個方法到當前對象上來,
class_addMethod([_target class], @selector(fire), class_getMethodImplementation([self class], @selector(fire)), "v@:");
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:_target selector:@selector(fire) userInfo:nil repeats:YES];

-(void)dealloc
{
    NSLog(@"%@ delloc",self);
    [_timer invalidate];
    _timer = nil;  
}
此時viewcontroller的析構函數就能夠正常執行。
複製代碼

3.第三種方法:高級的中間者內存

此時咱們須要藉助一個虛基類NSProxy,(NSProxy其主要用來消息轉發的處理)

//  HZProxy.h
#import <Foundation/Foundation.h>
@interface HZProxy : NSProxy
//仍是要有個target
@property (nonatomic,weak) id target;
@end

//  HZProxy.m
#import "HZProxy.h"

@implementation HZProxy

//獲取當前的方法簽名
-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    return [self.target methodSignatureForSelector:sel];
}
//指定當前消息的處理者
-(void)forwardInvocation:(NSInvocation *)invocation
{
    [invocation invokeWithTarget:self.target];
}

//執行timer
//虛基類只有alloc方法,因此初始化,直接調用alloc
_hzProxy = [HZProxy alloc];
//當前Proxy的target設爲當前的self,由於真正要處理消息的實際上是當前的viewcontroller(其實這個target就至關於delegate)
_hzProxy.target  = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:_hzProxy selector:@selector(fire) userInfo:nil repeats:YES]; 
當前_timer的對象的處理就變成了_hzProxy
運行一下,能夠看到viewcontroller的析構函數能夠正常執行
複製代碼

4.第四種:帶block的timer

在咱們建立timer的時候,蘋果也意識到NSTimer的api是存在必定問題的,因此在iOS10.0以後提供了一種block的方法來去解決NSTimer的循環引用的問題。

__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
        __strong typeof(self) strongSelf = weakSelf;
        [strongSelf fire];
    }];
複製代碼

可是爲了兼容當前的api在iOS10.0以前的狀況,因此這個時候咱們能夠HOOK一下

首先咱們建立一個NSTimer的分類,
//  NSTimer+ZHTimer.h
#import <Foundation/Foundation.h>

@interface NSTimer (ZHTimer)
+(NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(void))block;
@end

//  NSTimer+ZHTimer.m
#import "NSTimer+ZHTimer.h"

@implementation NSTimer (ZHTimer)
+(NSTimer *)zh_scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(void))block
{
    return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(zh_blockHandle:) userInfo:block repeats:YES]; 

//這裏面這個self,其實指的是當前的類對象,在內存中只存在一份,就是以單例的形式存在,因此咱們在每一次建立實例變量都要經過這個類對象來建立,
//因此並不須要擔憂當前的target形成循環引用,由於單例不須要被釋放,只有當APP被Q的時候,存在內存中的單例纔會被釋放掉。
}

+(void)zh_blockHandle:(NSTimer *)timer{
    void(^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}

@end

//調用一下
__weak typeof(self) weakSelf = self;
_timer = [NSTimer zh_scheduledTimerWithTimeInterval:1.0f repeats:YES block:^{
        __strong typeof(self) strongSelf = weakSelf;
        [strongSelf fire];
    }];

複製代碼

以上就是我的對NSTimer循環引用的理解以及處理方法,可能不是很完善,很深刻,有Bug的地方還請指出,很是感謝。

相關文章
相關標籤/搜索