Memory leak production’da uzun süreli kullanılan app’lerde ortaya çıkıyor. User 30 dakika app’te geziniyor, RAM yavaş yavaş dolup app yavaşlıyor veya crash ediyor.
Debug etmek zor çünkü reproducing için specific user flow gerekli. Instruments bu tür sorunları yakalamanın yolu. 3 gerçek senaryoyu anlatacağım: her biri farklı leak pattern’i.
Instruments’ı tanımak
Xcode’da Product > Profile (Cmd+I). App release build’lenip Instruments açılıyor.
Kritik tool’lar:
- Leaks: Actual memory leak’leri tespit ediyor (reference cycle, etc).
- Allocations: Hangi object’ler memory tutuyor, live count.
- Time Profiler: CPU hangi fonksiyona harcanıyor.
- Energy Log: Battery impact.
- Core Animation: UI performance.
Memory leak için Leaks + Allocations kombinasyonu en güçlü.
Senaryo 1: Closure retain cycle
Dentii’de TabBar’dan bir view’a navigate ediyordu user. Sonra back’e basıyordu, view dealloc olmuyordu. Her navigation’da bir instance leak.
Instruments’ta:
- Leaks açtım
- Navigate yaptım, back yaptım (5 kere)
- Leaks tool’unda 5
DashboardViewControllerinstance’ı görünüyordu
Allocations cycle görüntüsü:
DashboardViewController
└─ self.dataFetcher.callback
└─ captures self (strong reference)Retain cycle.
Kodda:
class DashboardViewController: UIViewController {
let dataFetcher = DataFetcher()
override func viewDidLoad() {
super.viewDidLoad()
dataFetcher.callback = { result in
self.handleResult(result) // BUG: self strong capture
}
}
}Controller dataFetcher’ı tutuyor, dataFetcher callback’inde self’i tutuyor. Cycle.
Fix:
dataFetcher.callback = { [weak self] result in
self?.handleResult(result)
}[weak self] ile cycle kırılıyor. Controller dealloc olabiliyor.
Prevention pattern: Her escaping closure’da [weak self] default kullan. Sadece closure view controller’dan önce bitecek ise [unowned self].
Senaryo 2: Timer repeat without invalidation
Snoozio’da background’a çıkıldığında app kullanmaya devam ediyor. 8 saat sonra kullanıcı app’i tekrar açıyor, memory inanılmaz yüksek.
Instruments Allocations:
- App açıldığında: 50MB memory
- 30 dakika sonra: 150MB
- 1 saat sonra: 300MB
Linear büyüme. Timer kaynaklı.
Kodda:
class SleepTracker {
func start() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
self.checkSleepState()
}
}
}Timer selfi tutuyor, self timer’ı tutmuyor ama timer runloop’ta active. Dealloc edilmiyor. Her start() yeni timer.
Fix:
class SleepTracker {
private var timer: Timer?
func start() {
stop()
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.checkSleepState()
}
}
func stop() {
timer?.invalidate()
timer = nil
}
deinit {
stop()
}
}Timer reference tutuluyor, invalidate edilebiliyor, weak self ile cycle yok.
Prevention: Scheduled timer’lar her zaman invalidate() ile bitmeli. deinit’te cleanup garanti.
Senaryo 3: Observer registration without removal
CVCrafter’da keyboard notification observer’ı register ediliyordu her screen’de. Ama bazı screen’ler close olunca remove etmiyordu.
Instruments Leaks bunu direct göstermedi (leak değil technically), ama Allocations memory büyümesi.
Observers counter’ı kontrol ettim:
do {
let token = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { notification in ... }
}Token’ı store etmediğim için remove edemiyordum. Her screen keyboard observer ekliyordu, hiçbiri silinmiyordu.
Fix:
class FormViewController: UIViewController {
private var observers: [NSObjectProtocol] = []
override func viewDidLoad() {
super.viewDidLoad()
let token = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { [weak self] notification in
self?.handleKeyboard(notification)
}
observers.append(token)
}
deinit {
observers.forEach { NotificationCenter.default.removeObserver($0) }
}
}Token store, deinit’te remove. Observer lifecycle temiz.
Alternative: Swift 5.5+ ile Notification Center async sequence:
for await notification in NotificationCenter.default.notifications(named: .keyboard...) {
// task cancelled olduğunda observation otomatik stop
}Instruments workflow
Yaklaşımım:
Step 1: Allocations profile al.
- App’i launch et
- Initial memory’yi kaydet (baseline)
- Leak olacak suspected flow’u execute et (navigate, scroll, whatever)
- Initial state’e dön
- “Persistent allocation” counter’ını check et
İdeal: counter baseline’a döner. Değerse leak var.
Step 2: Leaks tool’unu çalıştır.
Active leak’leri tespit ediyor. Stack trace gösteriyor.
Step 3: Cycle visualize et.
Allocations’da object type filter’la. Retain cycle graph view’ı.
Step 4: Fix.
Weak/unowned, invalidation, removal.
Step 5: Re-profile.
Fix’in çalışıp çalışmadığını confirm et.
Xcode’da leak detection automation
Instruments manuel, ama Xcode’da da tool var:
Address Sanitizer (Asan): Memory corruption, buffer overflow.
Thread Sanitizer (TSan): Data race, concurrent access.
Undefined Behavior Sanitizer (UBSan): Undefined behavior.
Malloc Stack Logging: Every allocation’s stack trace.
Debug scheme’de bu sanitizer’ları aç. Normal test sırasında leak’leri yakalıyor, Instruments’a gerek kalmadan.
SwiftUI-specific leaks
SwiftUI UIKit’ten farklı memory model’e sahip. View’lar struct, state @StateObject/@ObservableObject.
Common SwiftUI leak:
struct ContentView: View {
@StateObject var viewModel: ViewModel = {
let vm = ViewModel()
vm.onUpdate = {
// captures vm in closure
vm.refresh()
}
return vm
}()
}ViewModel kendi callback’inde kendini capture ediyor.
Fix: [weak vm] pattern.
SwiftUI’da @State objeleri identity korumuyor her re-render’da yenileniyor gibi görünüyor ama reality @StateObject veya @State ile persistent.
Performance impact of leaks
Leak’lerin gerçek impact’i:
- Memory pressure: 500MB+ ise iOS app’i kill edebiliyor
- Battery drain: leaked objects’in cleanup attempts
- Slow performance: garbage collection benzer zombies
- Crash in low-memory conditions
Production’da leak’ler sessizce etkili. Crash reporter’da “terminated due to memory pressure” görünüyor.
Testing strategy
Memory leak için test suite:
Unit tests: XCTest’te weak reference pattern. Object create, scope’tan çıkar, weak reference nil olmalı.
func testViewModelDeallocates() {
weak var viewModel: ViewModel?
autoreleasepool {
let vm = ViewModel()
viewModel = vm
vm.start()
}
XCTAssertNil(viewModel, "ViewModel should be deallocated")
}Integration tests: Navigate flow, memory check. Before/after comparison.
Instruments as part of CI: Advanced. Automated profiling release candidate’lar için.
When to profile
Her proje’de milestone’larda:
- Pre-release: Release build’i Instruments ile profile et. Baseline memory, peak memory, leak count.
- After major feature: Yeni büyük feature eklediysen memory impact check.
- User-reported performance issues: “App yavaş/çok RAM kullanıyor” feedback.
- Annual health check: Yıl’da bir tüm app genel performance review.
Sonuç
Memory leak iOS app’in sinsi düşmanı. Instruments Leaks + Allocations ikilisi ana araç.
3 yaygın pattern: closure retain cycle, timer/observer cleanup eksik, third-party SDK lifecycle. Bu 3’e dikkat ediyorsan çoğu leak’i önlersin.
Weak/unowned default mentality, invalidation discipline, notification removal. Bu pratikler migration kolay.
Ability to use Instruments iOS developer’ın fundamental skill’i. Yılda 1-2 deep dive session yapmak bile big impact.