NSTimerの潜在的な「保存ループ」の問題を解消
4442 ワード
NSTimerはFoundationフレームワークの使用頻度の高いクラスであるが、その呼び出し中に潜在的な「保留ループ」の問題を導入しやすい.NSTimerが提供するAPIが十分に便利で、この問題が容易に発見されないためかもしれない.このブログはこの問題を述べ、解決方法を提供することを目的としている.
以下のNSTimerが提供する3つの一般的な作成または初期化API:
この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ポインタに設定します.私がコードを書く習慣はこうです.インスタンスコードは次のとおりです.
上記のコードは、典型的なタイマ使用シナリオの1つです.タイマが繰り返しトリガではなく1回の実行である場合、タイマは実行後に自動的に失効し、「保留ループ」の問題もありません.ただし、繰り返しトリガを設定するタイマタイプであれば、NSTimerオブジェクトはMyObjectオブジェクトを強く参照し、現在のクラスもNSTimerオブジェクトを保持しているため、NSTimerがinvalidate設定を呼び出さないと、MyObjectオブジェクトはバックリリースされず、そのdealloc関数も呼び出されるが、NSTimerのinvalidateはMyObjectオブジェクトのdealloc関数で呼び出される.これで2つのオブジェクトは解放されません.
「リザーブリング」が現れる根本的な原因は、NSTimerオブジェクトが作成したAPIでtargetを1回暗黙的に強く参照することにあるため、「リザーブリング」を解除する鍵は、selfポインタに対するNSTimerオブジェクトの強い参照を避けることにある.次のソリューションを示します.
NSTimer+BlockSupported分類
使用方法
上記の解決策はNSTimer+BlockSupported分類を用いてNSTimer原生関数を二次カプセル化し,呼び出し元に必要な実行関数をblockに移行して実行し,_を結合する.WeakポインタはNSTimerによるselfへの強い参照を解除する.NSTimerオリジナルAPI呼び出しは、targetに対して強く参照されますが、このときtargetはTimerクラスオブジェクトになります.クラスオブジェクトのライフサイクルはアプリケーションとともに設定されており、参照カウントに制限されていないので大丈夫です.
このタイプの「保留リング」の問題は隠れているので、分析と記録の価値があり、あなたと共有しています.
GitHub Demo
注:このソリューションはEffective Objective-C 2.0の52条を参考にしており、興味のある学生は自分で調べることができます.
ブログアドレス
以下の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条を参考にしており、興味のある学生は自分で調べることができます.
ブログアドレス