Mobile Development 17 min read

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.

Sohu Smart Platform Tech Team
Sohu Smart Platform Tech Team
Sohu Smart Platform Tech Team
Why Does Mutating a Collection While Enumerating Crash Your iOS App?

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?

copy

is 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;
}
@end

The 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

iOSCrashObjective‑CCOPYNSMutableSet
Sohu Smart Platform Tech Team
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.