Timers
Learn how to schedule, run, and safely tear down NSTimer instances -- the scheduled-timer convenience constructors and the older target/selector API, how a timer is bound to a specific run loop and run-loop mode, why a repeating timer's retain relationship with its target is a classic source of retain cycles, how accurate NSTimer's firing actually is, and when to reach for a GCD dispatch timer instead.
The Run Loops lesson established that a run loop is a loop that waits for input sources and timers and dispatches them when they're ready, but it deliberately left timers themselves for their own dedicated lesson -- this one. NSTimer is the object-oriented Foundation wrapper around exactly the kind of timer a run loop knows how to schedule and fire: a piece of work that should run once, or repeatedly, after some interval has elapsed. Understanding NSTimer means understanding three things at once: the API surface for creating and canceling a timer, the run-loop machinery a timer is scheduled onto (which is why this lesson leans on Run Loops throughout), and a genuinely common memory-management trap -- a repeating timer that retains its target and is never invalidated, quietly keeping an object alive forever. This lesson also introduces GCD's dispatch timer as a more precise, thread-independent alternative, and gives concrete guidance for choosing between the two mechanisms.
What NSTimer Actually Is: A Scheduled Event on a Run Loop
An NSTimer is not, by itself, a thread or a background clock ticking somewhere independently of your code. It's a small object that describes a future point in time (or a repeating interval) and a block or target/selector to invoke when that time arrives -- and for any of that to actually happen, the timer has to be scheduled onto a run loop, which is the only thing capable of noticing 'enough time has passed' and firing it. This is the same run loop covered in the sibling Run Loops lesson: the loop that waits, wakes for input sources and timers, and dispatches whichever is ready before going back to sleep.
This dependency has a very practical consequence: a timer only fires while the run loop it's scheduled on is actually running and processing events in the mode the timer was scheduled in. A timer scheduled on the main thread's run loop, which is kept alive and pumped by UIKit or AppKit for the whole life of the app, behaves the way most developers expect -- it just fires on schedule. A timer scheduled on a background thread you spun up yourself, however, will never fire at all unless that thread also has an active run loop that you started running, because there is nothing pumping events on that thread otherwise.
The convenience class method [NSTimer scheduledTimerWithTimeInterval:...] does two things in one call: it creates the timer object, and it immediately schedules it on the current thread's run loop, in the default run-loop mode. This is why it's called 'scheduled' -- as opposed to the plain (non-scheduled) initializer, covered later in this lesson, which creates a timer object without scheduling it anywhere, leaving that step to you.
• An NSTimer describes a future fire time and an action -- it does nothing on its own until it's scheduled on a run loop. • A timer only fires while the run loop it's scheduled on is running and processing the mode the timer belongs to -- see the sibling Run Loops lesson for what a run loop mode is. • The main thread's run loop is kept running for you by the app framework, so main-thread timers 'just work'; a manually created background thread needs its own run loop actively running, or its timers never fire. • scheduledTimerWithTimeInterval:... both creates AND schedules the timer, on the current thread's run loop, in one step.
Creating Timers: The Block-Based API and the Older Target/Selector API
Modern code overwhelmingly favors the block-based scheduled constructor: [NSTimer scheduledTimerWithTimeInterval:repeats:block:], which takes the interval in seconds, a BOOL for whether the timer repeats, and a block that receives the firing NSTimer itself as its one argument. Because the fire action is a block, it captures its surrounding variables directly, with no separate target object or selector needed -- which also means it's subject to the same capture-and-retain-cycle rules every other block in this course has covered, a point this lesson returns to below.
The older API, +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:, predates blocks in Objective-C and is still extremely common in existing codebases. Instead of a block, it takes a target object and a SEL naming a method that object implements, taking exactly one argument -- the firing NSTimer -- and returning void. userInfo is an arbitrary object (often nil) you can stash on the timer and retrieve later via the fired timer's own -userInfo accessor, useful for passing a small piece of context without needing an ivar.
Both scheduled constructors return an already-scheduled, already-running timer -- you don't call anything further to 'start' them. There is also a non-scheduled path: +timerWithTimeInterval:repeats:block: (or the target/selector equivalent) creates a timer object without scheduling it on any run loop at all, and you're then responsible for scheduling it yourself with [[NSRunLoop currentRunLoop] addTimer:forMode:], which is exactly the API the sibling Run Loops lesson covers for attaching sources to a run loop. This split matters when you need to schedule a timer on a specific run loop mode other than the default, which is covered next.
• scheduledTimerWithTimeInterval:repeats:block: -- modern, block-based, creates and schedules in one call. • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: -- the older API; the selector takes the firing NSTimer as its one argument and returns void. • userInfo lets you attach arbitrary context to a target/selector timer, retrievable from the fired timer via -userInfo. • timerWithTimeInterval:... (no 'scheduled' prefix) creates a timer WITHOUT scheduling it -- you add it to a run loop yourself with addTimer:forMode:.
Run-Loop Modes and Where Timers Actually Fire
The sibling Run Loops lesson covers run-loop modes in depth -- the mechanism by which a run loop only processes sources and timers registered for whichever mode it's currently running in, most notably the default mode versus the tracking mode UIKit switches into during active scroll-view tracking. Timers are directly subject to this: a timer scheduled in NSDefaultRunLoopMode (which is what the convenience scheduled constructors use) will appear to pause while the user is actively scrolling a UIScrollView on the main thread, because the run loop has switched to UITrackingRunLoopMode and isn't servicing the default mode's timers during that window.
The classic fix, for a timer that genuinely needs to keep firing during scrolling, is to schedule it into NSRunLoopCommonModes instead of a single specific mode -- a pseudo-mode that several real modes (including both the default and tracking modes) are configured to treat as also applicable to them. A timer added under NSRunLoopCommonModes keeps firing regardless of which of those particular modes the run loop happens to be in at the moment.
This is exactly why the pairing with the Run Loops lesson matters: a timer that mysteriously 'stops working while the user is interacting with the UI' is, in the overwhelming majority of cases, not a timer bug at all -- it's a run-loop-mode issue, and the fix lives entirely in which mode (or NSRunLoopCommonModes) the timer was scheduled under.
• A timer only fires while the run loop is actually processing the specific mode the timer was scheduled in. • The default scheduled constructors use NSDefaultRunLoopMode, which UIKit stops servicing during active scroll tracking (UITrackingRunLoopMode) on the main thread. • Scheduling under NSRunLoopCommonModes keeps a timer firing across both the default and tracking modes (and any other mode configured to honor it). • A timer that 'pauses during scrolling' is a run-loop-mode symptom, not a timer bug -- see the sibling Run Loops lesson for what a mode actually is.
The Classic Retain Cycle: A Repeating Timer That Retains Its Target
Both the block-based and target/selector scheduled APIs create a strong reference from the timer to whatever it needs to keep alive in order to fire correctly. The target/selector API retains its target object directly, for as long as the timer itself is valid. The block-based API, in turn, is retained by whatever run loop it's scheduled on for as long as it's valid -- and the block itself, if it captures self (directly, or implicitly by referencing an ivar or calling a method on self inside the block), retains self strongly for as long as the block is retained.
Now put those two retains together with the run loop's own retain of a repeating timer, and the classic cycle appears: if self schedules a repeating timer with target:self (or a block capturing self), and self is also the object that would normally be responsible for invalidating that timer -- say, in its -dealloc method -- self can never actually reach -dealloc in the first place. The run loop retains the timer, the timer retains self (via target or captured block), and self is the only thing that would ever call -invalidate to break the chain. Nothing ever calls it, because self never deallocates, because the timer is still retaining it. The object leaks for the entire remaining lifetime of the app, or for however long the run loop keeps the timer scheduled -- which, for a repeating timer nobody ever invalidates, is forever.
The fix is not exotic: invalidate the timer explicitly, at a point in the object's lifecycle you know will actually run -- typically -dealloc for a plain object that legitimately should live and die with its timer, or, for a view controller, -viewWillDisappear: / -viewDidDisappear: paired with re-creating the timer in -viewWillAppear:, so the timer's lifetime tracks the screen being visible rather than the view controller's entire retain-cycle-blocked lifetime. Calling -invalidate breaks the run loop's retain on the timer and releases the timer's retain on its target, which is exactly the two retains that were keeping self alive.
• A repeating timer is retained by the run loop it's scheduled on, and it retains its target (target/selector API) or whatever self a captured block references (block API). • If self schedules a repeating timer targeting itself and self's -dealloc is the only place that calls -invalidate, self can never reach -dealloc -- the timer keeps self alive, so -dealloc, which would break the cycle, never runs. • The result is a silent leak: self and the timer both stay alive for as long as the run loop keeps the timer scheduled, which for an un-invalidated repeating timer is indefinitely. • Fix: call -invalidate at a real lifecycle point you control -- -dealloc for a plain object, or viewWillDisappear:/viewDidDisappear: (paired with re-creating the timer in viewWillAppear:) for a view controller.
Invalidation, Ownership, and the Weak-Proxy Alternative
-invalidate is the only correct way to stop a timer for good. It removes the timer from whatever run loop it's scheduled on and releases the timer's retain on its target (or captured block), and after it's called the timer will never fire again, even if something is still holding a strong reference to the now-invalid NSTimer object itself. Calling -invalidate more than once is harmless. Simply nilling out your own strong property that points at the timer, without calling -invalidate first, does NOT stop it -- the run loop still has its own separate strong reference and will keep firing it regardless of what your own ivar or property points to.
For cases where you genuinely can't guarantee -invalidate will be called at the right moment -- a shared utility class, or code you don't fully control the lifecycle of -- a weak-proxy pattern avoids the retain cycle at its source instead of relying on cleanup discipline. Introduce a small intermediary object that holds only a WEAK reference to the real target, schedule the timer with the proxy as its target instead of self directly, and have the proxy forward the fired-timer callback on to the real (weakly held) target if it's still alive. Because the timer now retains the proxy rather than self, and the proxy's reference to self is weak, self can deallocate normally even if the timer is never explicitly invalidated -- the proxy's forwarding call simply becomes a no-op (its weak reference resolves to nil) once self is gone.
The two approaches aren't mutually exclusive advice for different situations so much as two layers of the same discipline: explicit -invalidate at a known lifecycle point is the simpler, preferred fix whenever you do control that lifecycle; a weak-proxy is the fallback for the harder case where you can't guarantee that.
• -invalidate removes the timer from its run loop and releases its retain on the target -- it is the only real way to stop a timer, and it's safe to call more than once. • Nilling your own reference to a timer, without calling -invalidate, does nothing -- the run loop's own strong reference keeps it firing. • A weak-proxy target avoids the retain cycle at its source: the timer retains the proxy, the proxy holds self only weakly, so self can deallocate whether or not anyone remembers to invalidate. • Prefer explicit -invalidate at a known lifecycle point (dealloc, viewWillDisappear:) whenever you control it; reach for a weak proxy when you genuinely can't guarantee that.
Timer Accuracy: Why NSTimer Doesn't Fire at the Exact Interval
NSTimer is explicitly not a precision, real-time timer. Its documentation is direct about this: a timer is guaranteed to fire no earlier than its scheduled time, but it makes no promise about firing exactly then -- it fires at the next opportunity the run loop has to service it at or after that time, which depends on what else the run loop happens to be doing at that moment (running other event-handling code, servicing other timers or sources, or simply busy with a long-running block someone dispatched onto it). Under load, a timer's actual fire time can drift noticeably past its nominal interval.
For a repeating timer, this matters in a subtle way beyond a single late fire: NSTimer does not compensate for missed or delayed fires by ever firing 'twice as fast' to catch up, and depending on which repeating constructor is used, a delayed fire can either push every subsequent scheduled fire time later by the same amount, or the timer can skip a fire entirely if the run loop was busy past an entire interval. Code that needs firings to land at true wall-clock intervals -- logging every exact second, for instance -- cannot rely on NSTimer alone to guarantee that; it needs to check the actual elapsed wall-clock time itself, not just count timer fires.
None of this makes NSTimer unsuitable for its common uses -- UI-facing polling, debounce/throttle delays, animation-adjacent scheduling -- where being off by a handful of milliseconds is invisible to a user. It does make it the wrong tool whenever real accuracy matters, which is exactly where the GCD dispatch timer below comes in.
• NSTimer guarantees a timer fires no EARLIER than its scheduled time -- it never guarantees firing exactly on time. • Actual fire time depends on run-loop load: other event handling, other timers, or a long-running block on the same run loop all push a timer's real fire time later. • A repeating timer does not 'catch up' after a delay -- it can either shift every later fire by the delay or skip a fire outright, so fire COUNT is not the same as elapsed wall-clock time. • This inaccuracy is invisible for ordinary UI polling and debouncing, but disqualifies plain NSTimer for anything needing true, tight wall-clock timing.
GCD Dispatch Timers: A More Precise, Thread-Independent Alternative
The sibling Grand Central Dispatch lesson covers dispatch sources in general; a GCD dispatch timer is the timer-specific dispatch source, created with dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, ...) and configured with dispatch_source_set_timer(source, start, interval, leeway). Unlike NSTimer, a dispatch timer is not tied to a run loop at all -- it fires by being scheduled on a GCD queue, which means it works correctly and fires reliably even on a background thread that has no run loop running on it whatsoever, sidestepping the entire 'background thread needs an actively-running run loop' requirement NSTimer has.
The leeway parameter is the other half of what makes dispatch timers a genuine precision improvement: it lets you tell the system exactly how much scheduling flexibility is acceptable (for battery and power-efficiency reasons, the system will still coalesce nearby timer fires across the whole device when leeway allows it), while a leeway of very close to zero requests the tightest possible accuracy the platform can give you -- something NSTimer has no equivalent control for at all.
None of this makes dispatch timers strictly better in every case. They come with their own retain-and-cancellation discipline (a dispatch source must have its handler set and be resumed with dispatch_resume before it does anything, and must eventually be canceled to stop it, mirroring NSTimer's own -invalidate requirement), and they don't integrate with run-loop-mode concepts like NSRunLoopCommonModes at all, because they simply don't go through a run loop. The choice between the two is a genuine tradeoff, covered next.
• A GCD dispatch timer is created via dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, ...) and configured with dispatch_source_set_timer(source, start, interval, leeway). • It fires on a GCD queue, not a run loop -- it works reliably on a background thread with no run loop running at all, unlike NSTimer. • leeway gives explicit control over acceptable timing slack (letting the system coalesce fires for power efficiency), or can be set near zero for the tightest accuracy the platform offers -- NSTimer has no equivalent. • Dispatch timers need their own resume/cancel discipline and don't participate in run-loop modes at all -- they're a genuinely different mechanism, not a drop-in replacement.
Choosing Between NSTimer and a Dispatch Timer
Reach for NSTimer when the work is naturally UI-adjacent and already lives on the main thread's run loop: debouncing a search field, polling for a UI update, auto-dismissing a toast, or any repeating action that's fine landing a handful of milliseconds late under load. Its API is simpler, it integrates directly with run-loop modes when you need a timer to keep firing (or to legitimately pause) during specific run-loop activity like scroll tracking, and most Cocoa/Cocoa Touch code that already assumes a main-thread run loop is available gets no benefit from the extra complexity of a dispatch timer.
Reach for a GCD dispatch timer when the work needs to run on a background thread that has no run loop of its own, when you need tighter accuracy than NSTimer's 'no earlier than' guarantee provides, or when you specifically want fine control over coalescing/leeway for power efficiency in a background-scheduled repeating task. Because dispatch timers fire on whatever queue you target, they're also a natural fit when the timer's work is itself meant to run off the main thread -- avoiding an extra hop from a main-thread timer callback onto a background queue.
In both cases, the retain-cycle discipline from earlier in this lesson still applies in spirit: a dispatch source retained by a long-lived queue and configured with a handler block that captures self strongly can create the exact same kind of leak NSTimer can, just via dispatch_resume/dispatch_cancel instead of scheduling/-invalidate. The mechanism differs; the underlying lesson -- know what retains what, and have an explicit teardown point -- is identical.
• NSTimer: simpler API, integrates with run-loop modes, ideal for main-thread UI-adjacent work where being a few milliseconds late is invisible. • Dispatch timer: needed for reliable background-thread firing with no run loop, tighter accuracy, or explicit leeway/coalescing control. • A dispatch timer whose handler block captures self strongly is just as capable of a retain cycle as NSTimer -- the mechanism (dispatch_cancel vs -invalidate) differs, but the discipline required is the same. • When in doubt for ordinary app UI work, NSTimer is the default; reach for a dispatch timer only once you have a concrete reason (background-thread reliability or accuracy) to need it.
Scheduling and Invalidating a Timer
scheduledTimerWithTimeInterval:... both creates and schedules the timer on the current thread's run loop, in NSDefaultRunLoopMode, in one call. -invalidate is the only real way to stop a timer -- it releases the run loop's hold on it and the timer's hold on its target. Nilling your own variable/property AFTER invalidating is just normal memory hygiene; nilling it INSTEAD of invalidating does not stop the timer.
// Block-based, modern API -- creates AND schedules in one call:
NSTimer *timer =
[NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"Tick");
}];
// Older target/selector API:
NSTimer *legacyTimer =
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(tick:)
userInfo:nil
repeats:YES];
// Stopping a timer for good:
[timer invalidate];
timer = nil;Code Examples
The block captures tickCount by reference (__block) so it can mutate it across fires, and captures 'timer' as the block's own parameter rather than the outer pollTimer variable -- calling [timer invalidate] from inside the block is the safe way to have a timer stop itself after a fixed number of fires, without needing to reach outside the block for the variable that holds it.
if (tickCount >= 10) { [timer invalidate]; } userInfo carries arbitrary context along with the timer without needing an extra ivar -- the fired timer hands it right back via -userInfo. This is a non-repeating (repeats:NO) timer, commonly used for a one-shot timeout: it fires once and then invalidates itself automatically, needing no explicit -invalidate call for the normal-completion case.
NSDictionary *info = timer.userInfo;
self.timer is retained by the run loop (because it's a repeating, scheduled timer), and self.timer retains self (as its target). The only line that would break that cycle -- [self.timer invalidate] -- lives in -dealloc, which can never run, because self can never reach a retain count of zero while the timer keeps retaining it. Every PulseMonitor created this way leaks for as long as the run loop keeps the timer alive, which for an un-invalidated repeating timer is forever.
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(pulse) userInfo:nil repeats:YES];
Common mistake: Scheduling a repeating timer with target:self and relying on -dealloc to invalidate it -- -dealloc never runs, because the timer's retain on self is exactly what's preventing self's deallocation.
Tying the timer's lifetime to the screen being visible, rather than to the view controller's own dealloc, guarantees -invalidate runs at a point in the lifecycle that's actually reached: viewWillDisappear: fires every time the screen goes away, long before -dealloc would ever need to. This is the standard fix for exactly the retain cycle in the previous example, applied to the most common place it shows up -- a view controller driving a repeating UI-refresh timer.
[self.timer invalidate]; self.timer = nil;
This timer runs entirely on a background GCD queue with no run loop involved at all -- it will fire reliably even on a thread that never starts a run loop, which a background-scheduled NSTimer cannot do. dispatch_resume is required before the source does anything (a freshly created dispatch source starts suspended), and dispatch_source_cancel is the dispatch-world equivalent of -invalidate; the retain-cycle risk from a handler block capturing self strongly still applies exactly as it does for NSTimer.
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, 0), 1 * NSEC_PER_SEC, 50 * NSEC_PER_MSEC);
__block NSInteger tickCount = 0;
NSTimer *pollTimer =
[NSTimer scheduledTimerWithTimeInterval:0.5
repeats:YES
block:^(NSTimer * _Nonnull timer) {
tickCount++;
NSLog(@"Poll #%ld", (long)tickCount);
if (tickCount >= 10) {
[timer invalidate];
}
}];NSDictionary *context = @{ @"requestID": @42 };
NSTimer *timer =
[NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(handleTimeout:)
userInfo:context
repeats:NO];
- (void)handleTimeout:(NSTimer *)timer {
NSDictionary *info = timer.userInfo;
NSLog(@"Timed out for requestID %@", info[@"requestID"]);
}@interface PulseMonitor : NSObject
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation PulseMonitor
- (instancetype)init {
self = [super init];
if (self) {
// BUG: target:self on a REPEATING timer, with no invalidation
// anywhere else in this class.
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(pulse)
userInfo:nil
repeats:YES];
}
return self;
}
- (void)pulse {
NSLog(@"pulse");
}
- (void)dealloc {
// Too late: the run loop's retain on self.timer, and self.timer's
// retain on self, mean -dealloc is never reached in the first place.
[self.timer invalidate];
}
@end@implementation PulseViewController
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(pulse)
userInfo:nil
repeats:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
@enddispatch_queue_t queue =
dispatch_get_global_queue(QOS_CLASS_UTILITY, 0);
dispatch_source_t timer =
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, 0),
1 * NSEC_PER_SEC, // interval: 1 second
50 * NSEC_PER_MSEC); // leeway: 50ms
dispatch_source_set_event_handler(timer, ^{
NSLog(@"Precise background tick");
});
dispatch_resume(timer);
// Later, to stop it for good:
dispatch_source_cancel(timer);A Timer's Place in the Run Loop
An NSTimer only fires when the run loop it's scheduled on is actively running and servicing the mode the timer belongs to -- exactly the run-loop mechanics covered in the sibling Run Loops lesson.
Run Loop (thread)
|
|-- Input Sources
|-- Timers <-- NSTimer lives here
|
+-- Mode: Default / Tracking / Common
|
v
Timer fires (block or target/selector)Where This Shows Up in Real Apps
Polling timers on view controllers -- refreshing a countdown label, auto-saving a draft, re-checking a network condition -- are one of the most common sources of quietly leaked view controllers in real Objective-C codebases, precisely because the retain cycle produces no crash and no visible symptom beyond memory slowly growing over the life of the app. Debounce timers on search fields and auto-dismiss timers on transient UI (toasts, banners) are the other common case, and both are exactly where NSTimer's minor timing inaccuracy is completely invisible to users, which is why NSTimer, not a dispatch timer, remains the default choice for ordinary UI work. Dispatch timers show up instead in background sync/heartbeat code that must keep running reliably on a queue with no run loop, and in any code with an explicit accuracy requirement a run-loop-based timer can't promise.
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; //... no -invalidate call anywhere in the class - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.timer invalidate]; self.timer = nil; }
A repeating timer targeting self retains self for as long as the timer is scheduled; without an explicit -invalidate at a lifecycle point that's guaranteed to run, self can never deallocate, because the only code that would invalidate the timer (typically -dealloc) never executes.
self.timer = nil; // "cleaning up" the timer [self.timer invalidate]; self.timer = nil;
Nilling your own property does nothing to the timer itself -- the run loop holds its own separate strong reference and keeps firing it regardless. -invalidate must be called first to actually stop the timer and release the run loop's and the timer's own retains.
What does [NSTimer scheduledTimerWithTimeInterval:...] do that the plain (non-scheduled) [NSTimer timerWithTimeInterval:...] does not? A. It fires immediately instead of waiting for the interval B. It both creates the timer AND schedules it on the current thread's run loop, in one call C. It automatically retries on failure D. It runs on a background thread automatically
B. It both creates the timer AND schedules it on the current thread's run loop, in one call — The 'scheduled' constructors create the timer and immediately add it to the current thread's run loop (in NSDefaultRunLoopMode); the non-scheduled variant only creates the timer object, leaving scheduling to an explicit addTimer:forMode: call.
Why does a repeating NSTimer created with target:self, and relying on -dealloc to invalidate it, cause a leak? A. NSTimer objects are never deallocated by the runtime under any circumstances B. The timer is retained by the run loop and retains self as its target; since -dealloc is the only code that would invalidate the timer, and self can't reach dealloc while the timer retains it, neither one is ever released C. Timers use a special memory zone that garbage collection cannot reach D. Repeating timers always crash the app instead of leaking
B. The timer is retained by the run loop and retains self as its target; since -dealloc is the only code that would invalidate the timer, and self can't reach dealloc while the timer retains it, neither one is ever released — This is a classic retain cycle: the run loop retains the timer, and the timer retains self. The only thing that would break the cycle -- invalidating the timer -- is placed in -dealloc, but -dealloc requires self's retain count to reach zero first, which the timer's retain is preventing.
What does NSTimer actually guarantee about when a scheduled fire happens? A. It fires at exactly the scheduled interval, down to the millisecond, on every platform B. It fires no earlier than the scheduled time, but the exact fire time depends on what else the run loop is doing and may be later C. It fires only when the app is in the foreground, never otherwise, regardless of run loop state D. It fires twice as often if a previous fire was delayed, to catch up
B. It fires no earlier than the scheduled time, but the exact fire time depends on what else the run loop is doing and may be later — NSTimer only guarantees a fire happens no earlier than scheduled -- the actual fire time depends on run-loop load and can drift later. It does not compensate by firing extra times to catch up after a delay.
What is the key structural difference between a GCD dispatch timer and an NSTimer? A. A dispatch timer fires on a GCD queue and does not depend on a run loop at all, so it works reliably even on a background thread with no run loop running B. A dispatch timer can only fire once, never repeatedly C. A dispatch timer cannot be canceled once started D. There is no real difference -- they are two names for the exact same mechanism
A. A dispatch timer fires on a GCD queue and does not depend on a run loop at all, so it works reliably even on a background thread with no run loop running — NSTimer requires an actively running run loop on its scheduled thread; a GCD dispatch timer fires by being scheduled on a dispatch queue instead, which requires no run loop, making it reliable on background threads and giving finer control over accuracy via its leeway parameter.
What actually happens if you set your own strong property pointing at a timer to nil, without first calling -invalidate? A. The timer stops firing immediately, exactly as if -invalidate had been called B. Nothing changes about whether the timer fires -- the run loop holds its own separate strong reference and keeps firing it regardless of what your property points to C. The app crashes immediately D. The timer is paused but can be resumed later by setting the property back
B. Nothing changes about whether the timer fires -- the run loop holds its own separate strong reference and keeps firing it regardless of what your property points to — Your property is just one reference among several; the run loop's own retain on a scheduled timer is independent of it. Only -invalidate actually removes the timer from its run loop and releases its hold on its target, which is what stops it from firing.
Why would NSRunLoopCommonModes matter for a repeating timer in a scroll view screen? A. It makes the timer fire twice as fast B. A timer scheduled only in the default mode stops firing while the run loop is in UITrackingRunLoopMode during active scrolling; scheduling under NSRunLoopCommonModes keeps it firing across both modes C. It has no effect on timers, only on network sources D. It converts the timer into a GCD dispatch timer automatically
B. A timer scheduled only in the default mode stops firing while the run loop is in UITrackingRunLoopMode during active scrolling; scheduling under NSRunLoopCommonModes keeps it firing across both modes — UIKit switches the main run loop into UITrackingRunLoopMode during active scroll tracking, and a timer scheduled only in NSDefaultRunLoopMode isn't serviced during that window. NSRunLoopCommonModes is a pseudo-mode several real modes honor, keeping the timer firing regardless.
A class LiveScoreView schedules a repeating NSTimer with target:self to poll a score every 2 seconds, starting in -init and intending to stop in -dealloc. Explain why this design leaks, then rewrite it two ways: (1) using explicit invalidate/re-create tied to a visibility lifecycle instead of init/dealloc, and (2) using a GCD dispatch timer instead, noting what changes about cancellation.
The design leaks because the repeating timer's retain on self, and the run loop's retain on the timer, together prevent self's retain count from ever reaching zero, so -dealloc (the only place invalidate is called) never runs.\n\n(1) Move scheduling to -viewWillAppear: (or an equivalent 'became visible' hook) and move [self.timer invalidate]; self.timer = nil; to -viewWillDisappear: -- a hook guaranteed to run every time the view leaves, well before dealloc would ever be reached.\n\n(2) With a dispatch timer: dispatch_source_create + dispatch_source_set_timer + dispatch_source_set_event_handler (with the handler capturing self weakly to avoid the same class of cycle) + dispatch_resume to start it, and dispatch_source_cancel in place of -invalidate to stop it -- the run loop is no longer involved at all, so the timer fires reliably even off the main thread.
Ask what retains what: the run loop retains the timer, the timer retains self as its target. The only thing that could break that chain is -dealloc, and -dealloc requires self's retain count to reach zero first -- which the timer's own retain is preventing.