Why Does Mutating a Collection While Enumerating Crash Your iOS App?
This article walks through a real iOS crash caused by mutating an NSMutableSet during enumeration, explains how the Objective‑C copy attribute works, compares shallow and deep copies for strings and collections, and shows the underlying runtime implementation with concrete code examples.
Crash Caused by Single Traversal
Prologue: describing the hardest technology with the simplest language.
This record documents a problem discovered while unit‑testing a static library; please contact [email protected] if any statements are vague or inaccurate.
Table of Contents
Story Background
Problem Identification
Solution
Principle
Extension: Shallow vs Deep Copy
Reference Documents
Conclusion
Story Background
Environment and scenario:
Compilation environment: Xcode 12.5.1 On a day in August 2021, Augus was debugging requirement A, which required integrating an SDK for data collection.
Operation flow:
At the very start of the program, initialize the SDK and allocate memory.
During one launch the following error appeared:
Trapped uncaught exception 'NSGenericException', reason: '*** Collection <__NSSetM: 0x2829f9740> was mutated while being enumerated.'Initial guess: I first ruled out my own code because I was not using global breakpoints, so the crash was missed.
To reproduce the issue: NSMutableSet some methods were hook ed.
Enabled a global breakpoint.
Final diagnosis: The crash was caused by the third‑party SDK that enumerated an NSMutableSet while simultaneously reading and writing the collection.
Problem Identification
The root cause is that the imported SDK uses NSMutableSet enumeration and performs read‑write operations on the original mutable collection at the same time.
Reproducing the same crash:
NSMutableSet *mutableSet = [NSMutableSet setWithObjects:@"1", @"2", @"3", nil];
for (NSString *item in mutableSet) {
if ([item integerValue] < 3) {
[mutableSet removeObject:item];
}
}Solution
Summary of the cause: you cannot modify a mutable collection (e.g., NSMutableArray, NSMutableDictionary, NSMutableSet) while iterating over it.
Fix: perform a copy of the collection before iterating.
The principle is that the system treats the iteration as an atomic operation to avoid accessing invalid memory; different systems implement it differently but the underlying idea is the same – protect the object from concurrent mutations.
Principle
copy – What Is It?
copyis an Objective‑C property attribute used for objects such as Block or any class prefixed with NS. It triggers a copy of the object when the property is set.
copy – How Is It Implemented?
A class that wants to support copy must conform to the NSCopying protocol, which defines the method: - (id)copyWithZone:(NSZone *)zone; Example implementation with a Person class:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject <NSCopying>
- (instancetype)initWithName:(NSString *)name;
@property (nonatomic, copy) NSString *name;
- (void)addPerson:(Person *)person;
- (void)removePerson:(Person *)person;
@end
NS_ASSUME_NONNULL_END
@implementation Person
#pragma mark - Initialization Methods
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (!self) return nil;
if (!name || name.length < 1) {
name = @"Augus";
}
_name = name;
_friends = [NSMutableSet set];
return self;
}
#pragma mark - Private Methods
- (void)addPerson:(Person *)person {
if (!person) return;
[self.friends addObject:person];
}
- (void)removePerson:(Person *)person {
if (!person) return;
[self.friends removeObject:person];
}
#pragma mark - Copy Methods
- (id)copyWithZone:(NSZone *)zone {
Person *copy = [[Person allocWithZone:zone] initWithName:_name];
return copy;
}
- (id)deepCopy {
Person *copy = [[[self class] alloc] initWithName:_name];
copy->_persons = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES];
return copy;
}
@endThe copy attribute makes the assigned value immutable regardless of whether the original object was mutable.
Underlying Implementation of copy
Using clang -rewrite-objc on a simple class shows the generated getter and setter functions. The setter ultimately calls objc_setProperty, which decides whether to invoke copy, mutableCopy, or a simple retain based on the shouldCopy flag.
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
objc_setProperty_non_gc(self, _cmd, offset, newValue, atomic, shouldCopy);
}
void objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) {
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) {
id *slot = (id *)((char *)self + offset);
if (copy) {
newValue = [newValue copyWithZone:NULL];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:NULL];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
id oldValue = *slot;
*slot = newValue;
} else {
spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
_spin_lock(slotlock);
id oldValue = *slot;
*slot = newValue;
_spin_unlock(slotlock);
}
objc_release(oldValue);
}When copy is YES, the setter finally executes newValue = [newValue copyWithZone:NULL];. If both copy and mutableCopy are NO, it simply retains the new value.
Extension: Shallow vs Deep Copy
For non‑collection objects (e.g., NSString), copy performs a shallow copy (the same immutable instance), while mutableCopy creates a new mutable instance.
NSString *str = @"augusStr";
NSString *copyAugus = [str copy];
NSString *mutableCopyAugus = [str mutableCopy];
NSLog(@"str:%@", str);
NSLog(@"copyAugus:%@", copyAugus);
NSLog(@"mutableCopyAugus:%@", mutableCopyAugus);Output shows that str and copyAugus share the same memory address (shallow copy), while mutableCopyAugus has a different address and class ( NSMutableString).
For collection objects ( NSSet, NSMutableSet), copy yields a shallow copy (same underlying elements, same class for immutable collections), whereas mutableCopy creates a new mutable collection with the same elements (deep copy of the container only).
NSSet *set = [[NSSet alloc] initWithArray:@[p1, p2, p3]];
NSSet *copySet = [set copy];
NSMutableSet *mutableCopySet = [set mutableCopy];
NSLog(@"set:%@", set);
NSLog(@"copySet:%@", copySet);
NSLog(@"mutableCopySet:%@", mutableCopySet);Console logs confirm that set and copySet share the same memory address (shallow copy), while mutableCopySet has a different address and class ( __NSSetM).
Key Takeaways
Never use copy on mutable collection properties ( NSMutable*) because the resulting property becomes immutable.
For collection classes, copy and mutableCopy only duplicate the container; the elements themselves are not copied (single‑level deep copy).
Reference Documents
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Collections/Articles/Copying.html
https://gist.github.com/Catfish-Man/bc4a9987d4d7219043afdf8ee536beb2
https://opensource.apple.com/source/objc4/objc4-723/runtime/objc-accessors.mm.auto.html
Class Clusters documentation (same gist link as above)
Conclusion
One crash investigation, one source‑code journey, and a series of copy operations can clarify the whole problem; when faced with an issue, dig deep because the end of inquiry is endless illumination.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Sohu Smart Platform Tech Team
The Sohu News app's technical sharing hub, offering deep tech analyses, the latest industry news, and fun developer anecdotes. Follow us to discover the team's daily joys.
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.
