Browse Questions
Answer: Instruments diagnoses memory issues through the Leaks instrument for automated heap scans and the Allocations instrument with Mark Generation for tracking persistent abandoned memory.
There is an important distinction between Memory Leaks (unreachable memory with active retain cycles) and Abandoned Memory (reachable memory that continues growing because references are never cleaned up, such as unbounded caches):
-
Leaks Instrument:
- Profile app via
Product > Profile(Cmd + I) and choose the Leaks template. - The Leaks instrument runs periodic scans of the heap, placing red flags at timestamps where unreachable allocations are detected.
- Select a leak event, view the Cycles & Roots graph, and inspect the Call Tree with "Hide System Libraries" and "Invert Call Tree" enabled to pinpoint offending code.
- Profile app via
-
Allocations Instrument & Mark Generation (Heapshot Analysis):
- Open the Allocations instrument.
- Navigate to a starting state in your app (e.g. Home screen).
- Click Mark Generation in the inspector to take a baseline heap snapshot (Generation A).
- Perform the user action (e.g., open a Feed, load images, scroll, and pop back).
- Click Mark Generation again (Generation B).
- Repeat the action and mark Generation C and D.
- If objects from a popped screen persist in subsequent generations (growth > 0), expand that generation to inspect the exact leaked objects and their allocation backtraces.
Code Example:
// Example: Abandoned memory (Cache without eviction)
class ImageCacheManager {
static let shared = ImageCacheManager()
private var cache = [String: UIImage]() // ❌ Grows indefinitely without NSCache eviction
func store(image: UIImage, for key: String) {
cache[key] = image // Allocations instrument will show steady heap growth
}
}
// ✅ Fix using NSCache which evicts under memory pressure:
class OptimizedCacheManager {
static let shared = OptimizedCacheManager()
private let cache = NSCache<NSString, UIImage>()
func store(image: UIImage, for key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
Key Points:
- Always profile memory on a physical device with a Release build configuration for realistic memory metrics and compiler optimizations
- Invert Call Tree places your app's functions at the top of the call stack rather than deep inside
libsystem_malloc.dylib