iOS NSTimer循環引用的相關問題

前兩天被問到NSTimer的使用場景以及循環引用問題,回過頭來作一些研究,記錄一下。bash

注意:NSTimer使用不當會形成循環引用以至內存泄露

場景:

頁面1跳轉頁面2oop

#import "ViewController.h"
#import "OneViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor redColor];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //頁面1跳轉頁面2
    OneViewController *vc = [[OneViewController alloc] init];
    [self presentViewController:vc animated:true completion:nil];
}
複製代碼

頁面2中有個定時器,經過scheduledTimerWithTimeInterval初始化,實現timer的方法,並在頁面2的dealloc方法中釋放timerui

#import "OneViewController.h"

@interface OneViewController ()

@property (nonatomic,strong) NSTimer *timer;//頁面2中有一個定時器

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                                                  target:self
                                                selector:@selector(timerAction)
                                                userInfo:nil repeats:YES];
}

- (void)timerAction {
    NSLog(@"正在計時...");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self dismissViewControllerAnimated:true completion:nil];
}

- (void)dealloc {
    NSLog(@"dealloc");
    [self.timer invalidate];
    self.timer = nil;
}
複製代碼

結果:

頁面2觸發dismiss方法後dealloc方法並無被調用,說明頁面沒有被釋放,timer也沒有被釋放,雖然頁面已經關閉,定時器方法timerAction仍不斷被調用this

思考:
頁面2沒有走dealloc方法說明沒有被釋放,不斷調用timerAction方法說明timer也沒有被釋放,考慮瑟吉是由於頁面2對timer有強引用,timer也對頁面2有強引用,將timer的屬性設爲weak,嘗試事後結果仍然發生着循環引用; 或者設置__weak typeof(self) weakSelf = self,結果仍是同樣atom

關於NSTimer中target文檔描述:
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated
可知timer對target會有一個強引用,直到timer失效(invalidate)spa

timer要想解除對target的強引用,須要先invalidate,這就形成頁面2的dealloc方法不被調用,進而沒法invalidate,因此循環引用一直保持code

解決辦法:

大概有如下幾種辦法解除這個問題orm

方法1:定義一個weakTarget代替原有target控制器self,改變controller和timer相互強引用關係

@interface WeakTarget : NSObject

@property (nonatomic,assign) SEL selector;
@property (nonatomic,weak) NSTimer *timer;
@property (nonatomic,weak) id target;


+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                    target:(nonnull id)aTarget
                                  selector:(nonnull SEL)aSelector
                                  userInfo:(nullable id)userInfo
                                   repeats:(BOOL)yesOrNo;

@end
複製代碼
@implementation WeakTarget

+(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                    target:(id)aTarget
                                  selector:(SEL)aSelector
                                  userInfo:(id)userInfo
                                   repeats:(BOOL)yesOrNo 
{
    WeakTarget *weakTarget = [[WeakTarget alloc] init];
    weakTarget.target = aTarget; // aTarget = OneViewController
    weakTarget.selector = aSelector; // aSelector = timerAction方法的SEL包裝
    
     // weakTarget.timer對weakTarget有一個強引用
    weakTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                        target:weakTarget
                                                      selector:@selector(fire:)
                                                      userInfo:userInfo
                                                       repeats:yesOrNo];
    return weakTarget.timer;
}

-(void)fire:(NSTimer *)timer {
    if (self.target) {
        //調用了外界傳來的selector,即timerAction方法
        //由OneViewController調用本身的timerAction方法
        //這裏會產生一個警告:PerformSelector may cause a leak because its selector is unknown.
        [self.target performSelector:self.selector withObject:timer.userInfo];
    }else {
        [self.timer invalidate];
    }
}

@end
複製代碼
  • 關於PerformSelector may cause a leak because its selector is unknown警告,這裏

外部調用:cdn

#import "OneViewController.h"
#import "WeakTarget.h"

@interface OneViewController ()

@property (nonatomic,strong) NSTimer *timer;//頁面2

@end

@implementation OneViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor greenColor];
    self.timer = [WeakTarget scheduledTimerWithTimeInterval:2.0
                                                     target:self
                                                   selector:@selector(timerAction)
                                                   userInfo:nil
                                                    repeats:YES];
}
複製代碼
方法2:定義一箇中間件target,經過消息轉發實現(未傳SEL)
@interface WeakTarget1 : NSObject

+(instancetype)initWithTarget:(id)target;

@end
複製代碼
@interface WeakTarget1()

@property (nonatomic,weak) id target;

@end

@implementation WeakTarget1

+(instancetype)initWithTarget:(id)target {
    WeakTarget1 *weakTarget = [[WeakTarget1 alloc] init];
    weakTarget.target = target;
    return weakTarget;
}

//爲了保證中間件能響應外部self的事件,須要經過消息轉發機制,讓實際的響應target仍是外部的self
// 消息轉發,簡單來講就是若是當前對象沒有實現這個方法,系統會到這個方法裏來找實現對象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}

複製代碼

外部調用:中間件

#import "OneViewController.h"
#import "WeakTarget1.h"

@interface OneViewController ()

@property (nonatomic,strong) NSTimer *timer;//頁面2

@end

@implementation OneViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor greenColor];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                                                  target:[WeakTarget1 initWithTarget:self]
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
}
複製代碼
方法3:block解決循環引用(iOS10系統提供了timer的block初始化方法)
@interface OneViewController ()

@property (nonatomic,strong) NSTimer *timer;//頁面2

@end

@implementation OneViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor greenColor];
   /*
     爲了不在 block 的執行過程當中,忽然出現 self 被釋放的狀況,先定義了一個弱引用,令其指向self,
    而後使block捕獲這個引用,而不直接去捕獲普通的self變量。也就是說,self不會爲計時器所保留。
    當block開始執行時,馬上生成strong引用,以保證明例在執行期間持續存活。
     */
    __weak typeof(self)weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        __strong typeof(self)strongSelf = weakSelf;
        [strongSelf timerAction];
    }];
複製代碼
方法4:給NSTimer添加category,增長一個帶block參數的初始化方法(相似方法3)
@interface NSTimer (block)

+(NSTimer *)zq_scheduledTimerIntervl:(NSTimeInterval)interval
                               block:(void(^)(void))block
                             repeats:(BOOL)repeats;

@end
複製代碼
#import "NSTimer+block.h"

@implementation NSTimer (block)

+ (NSTimer *)zq_scheduledTimerIntervl:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats {
    /*userInfo文檔描述:The user info for the timer. 
      The timer maintains a strong reference to this object until it (the timer) is invalidated. 
      This parameter may be nil.
     計時器的用戶信息。計時器保持對該對象的強引用,直到它(計時器)失效。此參數能夠爲nil。
     由於如今是假定iOS10以前的系統版本,timer沒有自帶block參數的實例化方法,手動實現,將block做爲timer的信息賦值給userInfo
     */
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(zq_blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
}

+(void)zq_blockInvoke:(NSTimer *)timer {
    //在timer方法中執行block
    void (^block)(void) = timer.userInfo;
    if (block) {
        block();
    }
}
複製代碼

外部調用:

#import "OneViewController.h"
#import "NSTimer+block.h"

@interface OneViewController ()

@property (nonatomic,strong) NSTimer *timer;//頁面2

@end

@implementation OneViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor greenColor];
    __weak typeof(self)weakSelf = self;
    self.timer = [NSTimer zq_scheduledTimerIntervl:2.0 block:^{
        __strong typeof(self)strongSelf = weakSelf;
        [strongSelf timerAction];
    } repeats:YES];
}
複製代碼

打印:

2019-08-31 15:18:08.924239+0800 NSTimer相關[65510:4082744] 正在計時...
2019-08-31 15:18:11.376765+0800 NSTimer相關[65510:4082744] dealloc
2019-08-31 15:18:11.376948+0800 NSTimer相關[65510:4082744] 已經失效

另外:

當timer設置爲repeats = NO時候,不會存在上述問題

repeats參數文檔描述:
If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
若是設置爲YES,計時器將不斷循環,直到無效。若是設置爲NO,計時器將在觸發後失效。(至關於設置爲NO時自動調用invalidate方法)

-(void)invalidate方法文檔解釋: 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. If it was configured with target and user info objects, the receiver removes its strong references to those objects as well. 此方法是從NSRunLoop對象中刪除計時器的惟一方法。在invalidate方法返回以前或稍後某個時間點,NSRunLoop對象將刪除對計時器的強引用 若是有的話,定時器的target和userInfo對象的強引用也會被一塊兒刪除

相關文章
相關標籤/搜索