2024 SwiftDeveloper Tools
WWDC24 · 33 min · Swift / Developer Tools
Analyze heap memory
Dive into the basis for your app’s dynamic memory: the heap! Explore how to use Instruments and Xcode to measure, analyze, and fix common heap issues. We’ll also cover some techniques and best practices for diagnosing transient growth, persistent growth, and leaks in your app.
Watch at developer.apple.com ↗Chapters
- 0:00 — Introduction
- 1:05 — Heap memory overview
- 3:45 — Tools for inspecting heap memory issues
- 7:40 — Transient memory growth overview
- 10:34 — Managing autorelease pool growth in Swift
- 13:57 — Persistent memory growth overview
- 16:00 — How the Xcode memory graph debugger works
- 20:15 — Reachability and ensuring memory is deallocated appropriately
- 21:54 — Resolving leaks of Swift closure contexts
- 24:13 — Leaks FAQ
- 26:51 — Comparing performance of weak and unowned
- 30:44 — Reducing reference counting overhead
- 32:06 — Cost of measurement
- 32:30 — Wrap up
Code shown on screen · 23 snippets
ThumbnailLoader.makeThumbnail(from:) implementation
func makeThumbnail(from photoURL: URL) -> PhotoThumbnail {
validate(url: photoURL)
var coreImage = CIImage(contentsOf: photoURL)!
let sepiaTone = CIFilter.sepiaTone()
sepiaTone.inputImage = coreImage
sepiaTone.intensity = 0.4
coreImage = sepiaTone.outputImage!
let squareSize = min(coreImage.extent.width, coreImage.extent.height)
coreImage = coreImage.cropped(to: CGRect(x: 0, y: 0, width: squareSize, height: squareSize))
let targetSize = CGSize(width:64, height:64)
let scalingFilter = CIFilter.lanczosScaleTransform()
scalingFilter.inputImage = coreImage
scalingFilter.scale = Float(targetSize.height / coreImage.extent.height)
scalingFilter.aspectRatio = Float(Double(coreImage.extent.width) / Double(coreImage.extent.height))
coreImage = scalingFilter.outputImage!
let imageData = context.generateImageData(of: coreImage)
return PhotoThumbnail(size: targetSize, data: imageData, url: photoURL)
} ThumbnailLoader.loadThumbnails(with:), with autorelease pool growth issues
func loadThumbnails(with renderer: ThumbnailRenderer) {
for photoURL in urls {
renderer.faultThumbnail(from: photoURL)
}
} Simple autorelease example
print("Now is \(Date.now)") // Produces autoreleased .description String Autorelease pool growth in loop
autoreleasepool {
// ...
for _ in 1...1000 {
// Autoreleases into single pool, causing growth as loop runs
print("Now is \(Date.now)")
}
// ...
} Autorelease pool growth in loop, managed by nested pool
autoreleasepool {
// ...
for _ in 1...1000 {
autoreleasepool {
// Autoreleases into nested pool, preventing outer pool from bloating
print("Now is \(Date.now)")
}
}
// ...
} ThumbnailLoader.loadThumbnails(with:), with nested autorelease pool growth issues fixed
func loadThumbnails(with renderer: ThumbnailRenderer) {
for photoURL in urls {
autoreleasepool {
renderer.faultThumbnail(from: photoURL)
}
}
} C++ class with virtual method
class Coconut {
Swallow *swallow;
virtual void virtualMethod() {}
}; C++ class without virtual method
class Coconut {
Swallow *swallow;
}; ThumbnailRenderer.faultThumbnail(from:), caching thumbnails incorrectly
func faultThumbnail(from photoURL: URL) {
// Cache the thumbnail based on url + creationDate
let timestamp = UInt64(Date.now.timeIntervalSince1970) // Bad - caching with wrong timestamp
let cacheKey = CacheKey(url: photoURL, timestamp: timestamp)
let thumbnail = cacheProvider.thumbnail(for: cacheKey) {
return makeThumbnail(from: photoURL)
}
images.append(thumbnail.image)
} ThumbnailRenderer.faultThumbnail(from:), caching thumbnails correctly
func faultThumbnail(from photoURL: URL) {
// Cache the thumbnail based on url + creationDate
let timestamp = cacheKeyTimestamp(for: photoURL) // Fixed - caching with correct timestamp
let cacheKey = CacheKey(url: photoURL, timestamp: timestamp)
let thumbnail = cacheProvider.thumbnail(for: cacheKey) {
return makeThumbnail(from: photoURL)
}
images.append(thumbnail.image)
} Code creating reference cycle with closure context
let swallow = Swallow()
swallow.completion = {
print("\(swallow) finished carrying a coconut")
} PhotosView image loading code, with leak
// ...
let renderer = ThumbnailRenderer(style: .vibrant)
let loader = ThumbnailLoader(bundle: .main, completionQueue: .main)
loader.completionHandler = {
self.thumbnails = renderer.images // implicit strong capture of renderer causes strong reference cycle
}
loader.beginLoading(with: renderer)
// ... PhotosView image loading code, with leak fixed
// ...
let renderer = ThumbnailRenderer(style: .vibrant)
let loader = ThumbnailLoader(bundle: .main, completionQueue: .main)
loader.completionHandler = { [weak renderer] in
guard let renderer else { return }
self.thumbnails = renderer.images
}
loader.beginLoading(with: renderer)
// ... Intentional leak of manually-managed allocation
let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16)
// intentional mistake: missing `oops.deallocate()` Loop over intentional leak of manually-managed allocations
for _ in 0..<100 {
let oops = UnsafeMutablePointer<Int>.allocate(capacity: 16)
// intentional mistake: missing `oops.deallocate()`
} Nonreturning function which can see leaks of allocations owned by local variables
func beginServer() {
let singleton = Server(delegate: self)
dispatchMain() // __attribute__((noreturn))
} Fix for reported leak in nonreturning function
static var singleton: Server?
func beginServer() {
Self.singleton = Server(delegate: self)
dispatchMain()
} Weak reference example
weak var holder: Swallow? Unowned reference example
unowned let holder: Swallow Implicit use of self by method causes reference cycle
class ByteProducer {
let data: Data
private var generator: ((Data) -> UInt8)? = nil
init(data: Data) {
self.data = data
generator = defaultAction // Implicitly uses `self`
}
func defaultAction(_ data: Data) -> UInt8 {
// ...
}
} Break reference cycle cause day implicit use of self by method, using weak
class ByteProducer {
let data: Data
private var generator: ((Data) -> UInt8)? = nil
init(data: Data) {
self.data = data
generator = { [weak self] data in
return self?.defaultAction(data)
}
}
func defaultAction(_ data: Data) -> UInt8 {
// ...
}
} Break reference cycle cause day implicit use of self by method, using unowned
class ByteProducer {
let data: Data
private var generator: ((Data) -> UInt8)? = nil
init(data: Data) {
self.data = data
generator = { [unowned self] data in
return self.defaultAction(data)
}
}
func defaultAction(_ data: Data) -> UInt8 {
// ...
}
} Struct with non-trivial init/copy/deinit
struct Nontrivial {
var number: Int64
var simple: CGPoint?
var complex: String // Copy-on-write, requires non-trivial struct init/copy/destroy
} Resources
Related sessions
-
38 min -
29 min -
51 min -
35 min -
22 min