Analyzing ARC Multithreaded Assignment Crashes in iOS Applications
This article examines how ARC‑managed object assignments can produce wild pointers and EXC_BAD_ACCESS crashes in multithreaded iOS apps, explains the underlying reference‑counting steps, demonstrates reproducible demo scenarios, and offers practical mitigation strategies for developers.
1. Principle
In Objective‑C, assigning an object involves creating a new value, retaining the old value, loading the new value, and releasing the old value. Under ARC the compiler automatically inserts retain and release calls, as illustrated by the following code:
NSObject *_instance;
void foo(void) {
_instance = [[NSObject alloc] init];
}The release step can decrement the reference count to zero while another thread is still accessing the object, leading to a dangling pointer.
2. Crash Scenarios
A minimal demo shows three threads (A, B, C) interacting with _instance . Thread A creates the object, B and C read it, B releases it (reference count becomes zero), and C later accesses the already‑destroyed object, causing an EXC_BAD_ACCESS crash.
The reproduction steps are controlled with LLDB thread continue to let only one thread run at a time while the others remain paused.
3. Crash Causes
The crash occurs when the CPU executes an instruction such as ldr x17, [x2, #0x20] that reads the bits field of the object's class structure. After the object is released, the ISA pointer becomes a random value, making the calculated address illegal and triggering the exception.
4. Additional Crash Scenarios
4.1 Crash in objc_retain
If an object is passed as a parameter to a function, ARC retains it at entry. If the object has already been destroyed by another thread, the retain itself crashes.
4.2 Crash in objc_msgSend
Sending a message (e.g., isEqual: ) to a deallocated object causes the runtime to read the class cache via an instruction like ldr x11, [x16, #0x10] . A corrupted class pointer leads to an illegal memory access.
4.3 Crash in objc_autoreleasePoolPop
Objects created without the new/alloc/copy/mutableCopy prefix may be placed in an autorelease pool. When the pool is popped, objc_release is invoked; if the object was already freed, a crash occurs.
4.4 EXC_BREAKPOINT Crash
Pointer authentication instructions can compare a cleaned pointer with a corrupted one, causing a breakpoint exception when the values differ.
5. Typical Business Scenarios
5.1 Global Variable Assignment
Multiple threads simultaneously initialize a global variable (e.g., geckoSettingDict ) without proper synchronization, leading to premature deallocation and crashes. The fix is to use dispatch_once for one‑time initialization.
5.2 Property Assignment
Properties exposed to external callers (e.g., extraParam or autoResolutionParams ) are updated from several threads, causing over‑release. Using atomic or guarding updates with dispatch_once mitigates the issue.
5.3 Lazy‑Loaded Property
Lazy‑loaded properties (e.g., _interceptUrls , _userCache ) may be initialized from multiple threads, resulting in race‑condition deallocation. Initializing such properties in init or protecting the lazy‑load with a lock prevents the problem.
6. Summary
Although the underlying cause—over‑release of an object during ARC assignment—is simple, it is hard to detect in large apps with many threads and copied code. Understanding the ARC semantics, common crash patterns, and applying synchronization (e.g., dispatch_once , atomic properties) are essential for building robust iOS code.
ByteDance Terminal Technology
Official account of ByteDance Terminal Technology, sharing technical insights and team updates.
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.