Why Does NSString Leak in Long‑Running Threads? Exploring ARC and AutoreleasePool
An in‑depth analysis of a memory‑growth issue observed in a Kuaishou live‑stream app reveals how ARC, autorelease pools, and NSString’s class‑cluster implementation interact, why @autoreleasepool blocks resolve the leak, and how compiler optimizations and thread‑local pools affect object release in long‑running C++ threads.
Background
The article starts with a live‑stream OOM problem in the Kuaishou app, where the host's memory grew 1.5 GB in three hours, leading to crashes. Adding an @autoreleasepool block solved the issue.
ARC and Autorelease
ARC inserts retain/release automatically. Core APIs involved are objc_autorelease , objc_retainAutorelease , objc_autoreleaseReturnValue , and objc_retainAutoreleasedReturnValue . These functions allow the compiler to optimize away unnecessary retain/release when the caller immediately retains the returned object.
AutoreleasePool
Autorelease pools existed before ARC. They are stacks of pointers; each pool boundary separates objects to be released. In a C++ thread without an explicit pool, the runtime creates a default pool bound to the thread’s TLS, which is only drained when the thread exits.
Because the TCP long‑connection loop never ends, the default pool never drains, causing continuous memory growth.
Exploring NSString
Different NSString creation methods behave differently:
+[NSString stringWithCString:encoding:] ultimately calls CFStringCreateWithBytes , which does not use autorelease.
+[NSString stringWithFormat:] creates objects that are autoreleased and, due to its variadic implementation, does not benefit from the ARC return‑value optimization, leading to delayed release.
Tests showed that +[NSString stringWithFormat:] objects accumulate in Instruments as CFString(Immutable), while +[NSString stringWithCString:encoding:] objects are released promptly.
Clang Optimization Levels
In Release builds Clang uses -Os , while Debug builds use -O0 . Optimizations affect ARC’s ability to insert the return‑value optimizations; disabling them can reproduce the leak.
Verification
Three APIs were verified:
<code>NSString *hello = [NSString stringWithString:@"hello world"];</code>
<code>char *tempCStr = "hello world"; hello = [NSString stringWithCString:tempCStr encoding:NSUTF8StringEncoding];</code>
<code>hello = [NSString stringWithFormat:@"hello world"];</code>Symbolic breakpoints on autorelease showed different call stacks for constant strings versus objects created via stringWithFormat: , confirming the differing autorelease behavior.
Summary
Key takeaways:
NSString’s class‑cluster contains many concrete subclasses; their memory‑management behavior can differ.
The observed “leak” is not a retain cycle but delayed release of autoreleased objects.
In long‑running threads without explicit @autoreleasepool , the runtime‑created pool never drains, so autoreleased objects persist. Explicit pools are required for correct memory management.
Understanding ARC, autorelease pools, and compiler optimizations is essential for diagnosing memory‑growth issues in iOS apps.
Kuaishou Frontend Engineering
Explore the cutting‑edge tech behind Kuaishou's front‑end ecosystem
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.