web-dev-qa-db-ja.com

保持サイクルを防ぐためのNSTimerターゲットへの弱い参照

私はこのようなNSTimerを使用しています:

timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];

もちろん、NSTimerは、保持サイクルを作成するターゲットを保持します。さらに、selfはUIViewControllerではないため、タイマーを無効にしてサイクルを中断できるviewDidUnloadのようなものはありません。だから、代わりに弱い参照を使用できるかどうか疑問に思っています:

__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

タイマーmustが無効になっていると聞きました(実行ループから解放することを推測します)。しかし、deallocでそれを行うことができますよね?

- (void) dealloc {
    [timer invalidate];
}

これは実行可能なオプションですか?人々がこの問題に対処する多くの方法を見てきましたが、私はこれを見ていません。

51
bendytree

提案されたコード:

__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

(i)自己への弱い参照が行われるという効果があります。 (ii)NSTimerへのポインターを提供するために、弱い参照が読み取られます。弱い参照を持つNSTimerを作成する効果はありません。そのコードと__strong参照の使用の唯一の違いは、指定された2行の間にselfが割り当て解除された場合、nilをタイマーに渡すことです。

最善の方法は、プロキシオブジェクトを作成することです。何かのようなもの:

[...]
@implementation BTWeakTimerTarget
{
    __weak target;
    SEL selector;
}

[...]

- (void)timerDidFire:(NSTimer *)timer
{
    if(target)
    {
        [target performSelector:selector withObject:timer];
    }
    else
    {
        [timer invalidate];
    }
}
@end

次に、次のようなことをします:

BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

または、クラスメソッドを+scheduledTimerWithTimeInterval:target:selector:...という形式のBTWeakTimerTargetに追加して、そのコードのよりきれいな形式を作成します。おそらく実際のNSTimerを公開して、invalidateできるようにする必要があります。そうでない場合、確立されるルールは次のようになります。

  1. 実際のターゲットはタイマーによって保持されません。
  2. 実際のターゲットが割り当て解除を開始(およびおそらく完了)した後、タイマーが1回起動しますが、その起動は無視され、タイマーは無効になります。
74
Tommy

タイマーイベントのミリ秒の精度にそれほど関心がない場合は、NSTimerの代わりにdispatch_after&__weakを使用してこれを行うことができます。コードパターンは次のとおりです。

- (void) doSomethingRepeatedly
{
    // Do it once
    NSLog(@"doing something …");

    // Repeat it in 2.0 seconds
    __weak typeof(self) weakSelf = self;
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [weakSelf doSomethingRepeatedly];
    });
}

NSTimer @property、無効化/実行ループ、プロキシオブジェクトはありません。単純なクリーンメソッドです。

このアプローチの欠点は、(NSTimerとは異なり)ブロックの実行時間([weakSelf doSomethingRepeatedly];を含む)がイベントのスケジューリングに影響することです。

26
Jay Zhao

iOS 10およびmacOS 10.12 "Sierra"は新しいメソッドを導入しました- +scheduledTimerWithTimeInterval:repeats:block: 。したがって、次のようにselfを簡単にキャプチャできます。

__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
    MyClass* _Nullable strongSelf = weakSelf;
    [strongSelf doSomething];
}];

Swift 3:

_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.doSomething()
}

IOS 9以下をターゲットにする必要がある場合(現時点ではこの方法を使用する必要があります)、このメソッドは使用できないため、他の回答でコードを使用する必要があります。

24
kennytm

Swift 3

アプリのターゲット<iOS 10

カスタムWeakTimer( GitHubGist )実装:

final class WeakTimer {

    fileprivate weak var timer: Timer?
    fileprivate weak var target: AnyObject?
    fileprivate let action: (Timer) -> Void

    fileprivate init(timeInterval: TimeInterval,
         target: AnyObject,
         repeats: Bool,
         action: @escaping (Timer) -> Void) {
        self.target = target
        self.action = action
        self.timer = Timer.scheduledTimer(timeInterval: timeInterval,
                                          target: self,
                                          selector: #selector(fire),
                                          userInfo: nil,
                                          repeats: repeats)
    }

    class func scheduledTimer(timeInterval: TimeInterval,
                              target: AnyObject,
                              repeats: Bool,
                              action: @escaping (Timer) -> Void) -> Timer {
        return WeakTimer(timeInterval: timeInterval,
                         target: target,
                         repeats: repeats,
                         action: action).timer!
    }

    @objc fileprivate func fire(timer: Timer) {
        if target != nil {
            action(timer)
        } else {
            timer.invalidate()
        }
    }
}

使用法:

let timer = WeakTimer.scheduledTimer(timeInterval: 2,
                                     target: self,
                                     repeats: true) { [weak self] timer in
                                         // Place your action code here.
}

timerは標準クラスTimerのインスタンスであるため、使用可能なすべてのメソッドを使用できます(例:invalidatefireisValidfireDateなど)。
timerインスタンスは、selfが割り当て解除されるか、タイマーのジョブが完了すると割り当て解除されます(例:repeats == false)。

アプリのターゲット> = iOS 10
標準タイマーの実装:

open class func scheduledTimer(withTimeInterval interval: TimeInterval, 
                               repeats: Bool, 
                               block: @escaping (Timer) -> Swift.Void) -> Timer

使用法:

let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
    // Place your action code here.
}
8
Vlad Papko

SwiftでWeakTimerヘルパークラスを定義しました:

/// A factory for NSTimer instances that invoke closures, thereby allowing a weak reference to its context.
struct WeakTimerFactory {
  class WeakTimer: NSObject {
    private var timer: NSTimer!
    private let callback: () -> Void

    private init(timeInterval: NSTimeInterval, userInfo: AnyObject?, repeats: Bool, callback: () -> Void) {
      self.callback = callback
      super.init()
      self.timer = NSTimer(timeInterval: timeInterval, target: self, selector: "invokeCallback", userInfo: userInfo, repeats: repeats)
    }

    func invokeCallback() {
      callback()
    }
  }

  /// Returns a new timer that has not yet executed, and is not scheduled for execution.
  static func timerWithTimeInterval(timeInterval: NSTimeInterval, userInfo: AnyObject?, repeats: Bool, callback: () -> Void) -> NSTimer {
    return WeakTimer(timeInterval: timeInterval, userInfo: userInfo, repeats: repeats, callback: callback).timer
  }
}

そして、次のように使用できます:

let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in
  // Your code here...
}

返されるNSTimerにはselfへの弱い参照があるため、invalidateでそのdeinitメソッドを呼び出すことができます。

4
shadowmatter

weakSelfが弱いことは重要ではありません。タイマーはオブジェクトを保持しているため、保持サイクルが残っています。タイマーは実行ループによって保持されるため、タイマーへの弱いポインターを保持することができます(そして、推奨します)。

NSTimer* __weak timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target: self selector:@selector(tick) userInfo:nil repeats:YES];

invalidateについては、正しい方法です。

3
Ramy Al Zuhouri

理論と実践により、トミーのソリューションは機能しません。

理論的には、__ weakインスタンスはパラメーターとして、の実装で

[NSTimer scheduledTimerWithTimeInterval:target:selector: userInfo: repeats:],

ターゲットはまだ保持されます。

Selfを呼び出す弱参照と転送セレクターを保持するプロキシを実装し、プロキシをターゲットとして渡すことができます。 YYWeakProxyなど。

0
user2190117

Swift 4バージョン。無効化は、deallocの前に呼び出す必要があります。

class TimerProxy {

    var timer: Timer!
    var timerHandler: (() -> Void)?

    init(withInterval interval: TimeInterval, repeats: Bool, timerHandler: (() -> Void)?) {
        self.timerHandler = timerHandler
        timer = Timer.scheduledTimer(timeInterval: interval,
                                     target: self,
                                     selector: #selector(timerDidFire(_:)),
                                     userInfo: nil,
                                     repeats: repeats)
    }

    @objc func timerDidFire(_ timer: Timer) {
        timerHandler?()
    }

    func invalidate() {
        timer.invalidate()
    }
}

使用法

func  startTimer() {
    timerProxy = TimerProxy(withInterval: 10,
                            repeats: false,
                            timerHandler: { [weak self] in
                                self?.fireTimer()
    })
}

@objc func fireTimer() {
    timerProxy?.invalidate()
    timerProxy = nil
}
0
rockdaswift

Swift=を使用している場合、ここに自動キャンセルタイマーがあります。

https://Gist.github.com/evgenyneu/516f7dcdb5f2f73d792

タイマーはdeinitで自動的にキャンセルされます。

var timer: AutoCancellingTimer? // Strong reference

func startTimer() {
  timer = AutoCancellingTimer(interval: 1, repeats: true) {
    print("Timer fired")
  }
}
0
Evgenii