Common Misunderstandings and Pitfalls in iOS Development – Foundation, UIKit, GCD and Best Practices
The article clarifies frequent iOS development misconceptions—from unnecessary observer removal and deprecated NSUserDefaults synchronization to block retain‑cycle tricks, responder‑chain versus hit‑testing differences, GCD queue optimizations, safe main‑queue detection, and proper cancellation of dispatch blocks—offering practical code guidance.
This article collects a number of frequently misunderstood or overlooked points in iOS development, organized into three sections: Foundation, UIKit, and Grand Central Dispatch (GCD). It provides practical guidance and code examples for developers.
0x01 Must call -[NSNotificationCenter removeObserver:] in -dealloc ?
Historically developers removed observers in -[NSObject dealloc] . Since iOS 9 and macOS 10.11 this is no longer required. The official documentation states that observers are automatically deregistered when the observed object is deallocated. However, observers created with -[NSNotificationCenter addObserverForName:object:queue:usingBlock:] still need manual removal because the returned object is retained by the system.
If your app targets iOS 9.0 and later or macOS 10.11 and later, you don't need to unregister an observer in its dealloc method.
Otherwise, you should call removeObserver:name:object: before observer or any object passed to this method is deallocated.0x02 -[NSUserDefaults synchronize] is deprecated
Apple marked -[NSUserDefaults synchronize] as deprecated starting with iOS 7, but the API remained usable up to iOS 13.2 without the API_DEPRECATED macro. The call blocks the current thread until all defaults are flushed to disk, which is costly. Modern iOS implements an internal memory cache, making explicit synchronization unnecessary in most cases.
When you still need to guarantee that data is written before a process exits, use CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication) for non‑app processes (CLI, daemon, etc.).
0x03 Block retain‑cycle pitfalls
A typical weak‑strong dance:
__weak typeof(self) weakSelf = self;
^{
__strong typeof(self) strongSelf = weakSelf;
// do something with `strongSelf`
};Problems:
If the block still captures self directly (e.g., via ivars or macros like NSAssert ), a retain cycle can remain.
The strong reference may be nil if the original object was deallocated before the block runs.
Recommended pattern:
__weak typeof(self) weakSelf = self;
^{
__strong typeof(self) self = weakSelf;
if (!self) {
// error handling
return;
}
// use `self`
};Using typeof(self) does not create a retain cycle because it is a compile‑time construct. Declaring the variable as __strong is not strictly required under ARC, but it makes the weak‑strong conversion explicit.
0x10 UIKit – Responder Chain vs. Hit‑Testing
The responder chain is a singly‑linked list of UIResponder objects linked via -nextResponder . Hit‑testing, on the other hand, traverses the view hierarchy (a tree) using -[UIView hitTest:withEvent:] to find the first responder for a touch event.
Key differences:
Responder chain: linear list of UIResponder objects.
Hit‑testing: tree of UIView objects.
0x20 GCD – Thread optimisation and queue semantics
Calling dispatch_sync on a global concurrent queue from the main thread may execute the block on the current thread, because GCD can optimise by running the work item on the submitting thread when the queue is not the main queue.
dispatch_sync(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
NSLog(@"%@", NSThread.currentThread);
});
// Output: <NSThread: 0x...>{number = 1, name = main}Serial queues block the submitting thread, while concurrent queues do not. The same applies to nested dispatch_sync calls on a global queue – the inner block is executed immediately.
dispatch_queue_global_t globalQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_sync(globalQueue, ^{
dispatch_sync(globalQueue, ^{
NSLog(@"Will this run?"); // Yes, it runs
});
});Determining whether code runs on the main queue cannot rely on [NSThread isMainThread] alone. The recommended approach is to set a specific key on the main queue with dispatch_queue_set_specific and later compare the result of dispatch_get_specific . However, this only tells you that the current thread is bound to the main queue; it does not guarantee the code was dispatched via GCD.
static const void *kMainQueueKey = &kMainQueueKey;
dispatch_queue_set_specific(dispatch_get_main_queue(), kMainQueueKey, (void *)kMainQueueKey, NULL);
void *spec = dispatch_get_specific(kMainQueueKey);
if (spec == kMainQueueKey) {
// running on main queue
} else {
dispatch_sync(dispatch_get_main_queue(), ^{ /* … */ });
}Internally, dispatch_get_specific walks the thread‑specific data (TSD) to retrieve the bound queue, ultimately using pthread_getspecific or a thread‑local variable.
0x23 Cancelling GCD blocks
Since iOS 8, GCD provides dispatch_block_cancel , but it only works for blocks created with dispatch_block_create* . Directly created blocks (e.g., ^{ … } ) cannot be cancelled and will crash if dispatch_block_cancel is called.
dispatch_block_t block = dispatch_block_create(0, ^{
NSLog(@"Will this be cancelled?");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), block);
dispatch_block_cancel(block); // safe cancellationOnly blocks that have not yet started execution can be cancelled, and they must not capture resources that require the block’s execution for cleanup, otherwise those resources will leak.
Amap Tech
Official Amap technology account showcasing all of Amap's technical innovations.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.