NSTimerの潜在的な「保存ループ」の問題を解消

4442 ワード

NSTimerはFoundationフレームワークの使用頻度の高いクラスであるが、その呼び出し中に潜在的な「保留ループ」の問題を導入しやすい.NSTimerが提供するAPIが十分に便利で、この問題が容易に発見されないためかもしれない.このブログはこの問題を述べ、解決方法を提供することを目的としている.
以下のNSTimerが提供する3つの一般的な作成または初期化API:
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

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

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;


この3つのAPIには共通の点があり、targetパラメータを提供する必要がある.このtargetパラメータは、NSTimerインスタンスオブジェクトがinvalidateメソッドを呼び出した後に失効するまで、作成されたNSTimerインスタンスオブジェクトによって一度強く参照されます.APIドキュメントの原文は以下の通りです.
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.
多くの場合、作成後のNSTimerインスタンスオブジェクトを現在のクラスのインスタンス変数として保存し、NSTimerのtargetパラメータをselfポインタに設定します.私がコードを書く習慣はこうです.インスタンスコードは次のとおりです.
#import 

@interface MyObject : NSObject {
    NSTimer *mTimer;
}
@end

@implementation MyObject

- (id)init {
    if ((self = [super init])) {
        //    repeats = YES;
        mTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFiredFun) userInfo:nil repeats:YES];
        [mTimer setFireDate:[NSDate distantPast]];
     
    }
    return self;
}

- (void)dealloc {
    [mTimer invalidate];
    mTimer = nil;
}

- (void)timerFiredFun{
    NSLog(@"%s" , __func__);
}

@end

int main (int argc , const char * argv[]) {
    
    MyObject *myObjcet = [MyObject new];
    //self       ,       myObjcet      
    [myObjcet self];
    //NSTimer   RunLoop   ,    RunLoop
    while (1) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
  
    return 0;
}

上記のコードは、典型的なタイマ使用シナリオの1つです.タイマが繰り返しトリガではなく1回の実行である場合、タイマは実行後に自動的に失効し、「保留ループ」の問題もありません.ただし、繰り返しトリガを設定するタイマタイプであれば、NSTimerオブジェクトはMyObjectオブジェクトを強く参照し、現在のクラスもNSTimerオブジェクトを保持しているため、NSTimerがinvalidate設定を呼び出さないと、MyObjectオブジェクトはバックリリースされず、そのdealloc関数も呼び出されるが、NSTimerのinvalidateはMyObjectオブジェクトのdealloc関数で呼び出される.これで2つのオブジェクトは解放されません.
「リザーブリング」が現れる根本的な原因は、NSTimerオブジェクトが作成したAPIでtargetを1回暗黙的に強く参照することにあるため、「リザーブリング」を解除する鍵は、selfポインタに対するNSTimerオブジェクトの強い参照を避けることにある.次のソリューションを示します.
NSTimer+BlockSupported分類

#import 

typedef void(^ICETimerScheduleBlock)(void);

@interface NSTimer (BlockSupported)

+ (NSTimer *)ice_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                         block:(ICETimerScheduleBlock)block
                                       repeats:(BOOL)yesOrNo;

@end

#import "NSTimer+BlockSupported.h"

@implementation NSTimer (BlockSupported)

+ (NSTimer *)ice_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
                                         block:(ICETimerScheduleBlock)block
                                       repeats:(BOOL)yesOrNo {
    //Timer  target   ,    target  Timer   。                 ,        ,     。
    return [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:@selector(ice_timerFiredFun:) userInfo:block repeats:yesOrNo];
    
}

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

@end

使用方法
__weak typeof(self) weakSelf = self;
mTimer = [NSTimer zd_scheduledTimerWithTimeInterval:1.0f block:^{
    //         ,     block        self          。    strongSelf          block,      self,       self         。
    //       ,weakSelf    block         self       nil。
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf timerFiredFun];
} repeats:YES];

上記の解決策はNSTimer+BlockSupported分類を用いてNSTimer原生関数を二次カプセル化し,呼び出し元に必要な実行関数をblockに移行して実行し,_を結合する.WeakポインタはNSTimerによるselfへの強い参照を解除する.NSTimerオリジナルAPI呼び出しは、targetに対して強く参照されますが、このときtargetはTimerクラスオブジェクトになります.クラスオブジェクトのライフサイクルはアプリケーションとともに設定されており、参照カウントに制限されていないので大丈夫です.
このタイプの「保留リング」の問題は隠れているので、分析と記録の価値があり、あなたと共有しています.
GitHub Demo
注:このソリューションはEffective Objective-C 2.0の52条を参考にしており、興味のある学生は自分で調べることができます.
ブログアドレス