Mobile Development 23 min read

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.

Kuaishou Frontend Engineering
Kuaishou Frontend Engineering
Kuaishou Frontend Engineering
Why Does NSString Leak in Long‑Running Threads? Exploring ARC and AutoreleasePool

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.

iOSMemory ManagementAutoreleasePoolObjective-CARCNSString
Kuaishou Frontend Engineering
Written by

Kuaishou Frontend Engineering

Explore the cutting‑edge tech behind Kuaishou's front‑end ecosystem

0 followers
Reader feedback

How this landed with the community

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