---
title: 'Bellek Sızıntısı Tespiti'
source: 'https://academia.sh/tr/kurslar/ios-uretim/bellek-sizintisi-tespiti'
course: 'Mimari, Test ve Yayınlama'
language: tr
updated: '2026-10-21T05:03:07+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tutma döngüsünün iki nesnenin birbirini güçlü tutmasıyla oluştuğunu anlatır'
    - 'Zayıf referansın döngüyü nasıl kırdığını ve güçlü yakalamanın neden döngü yarattığını açıklar'
    - 'Ekranın doğru veriyi göstermesine bakan denetimin sızıntıyı göremediğini, serbest kalmaya bakan denetimin gördüğünü ölçer'
    - 'Sayacın hangi nesneyi saydığının sızıntıyı görüp görmemeyi belirlediğini gösterir'
---

# Bellek Sızıntısı Tespiti

Tutma döngüsünün nasıl oluştuğu, zayıf referansla nasıl kırıldığı ve nesnenin gerçekten serbest kalıp kalmadığının sınanması; iki sızıntı denetiminin doğru ve kusurlu sürümü ayırıp ayırmadığının ölçümü.

Önceki derste (Profil Çıkarma Araçları) görünen çıktısı doğru olan ama gereğinden çok iş yapan bir hücreyi ölçtük. Bu derste kusur zamanda değil bellekte. Bir ekran kapanır, kullanıcı ona bir daha dönmez, ama ekranın modeli bellekte kalır. Ekran doğru veriyi göstermiştir ve hiçbir test kızmaz; yalnız uygulamanın bellek kullanımı her ekran açılışında biraz büyür.

Ölçü ekseni **ayrımdır**: aynı denetimi doğru sürüme ve kusurlu sürüme uygularız. **Ayıran** denetim doğru sürümde geçer, kusurlu sürümde kalır. **Kör** denetim ikisinde de geçer. **Gürültülü** denetim ikisinde de kalır. **Ters** denetim doğru sürümde kalır, kusurlu sürümde geçer. Çekirdek önceki derslerle aynıdır; her ders kendi başına çalıştığı için yeniden veriyoruz.

```swift
// Separation.swift — bir denetimin dogru surumu kusurlu surumden ayirip ayirmadigini siniflandirir
struct Contrast<Output: CustomStringConvertible> {
    let label: String
    let correct: Output
    let faulty: Output
    let passesCorrect: Bool
    let passesFaulty: Bool

    var separation: String {
        switch (passesCorrect, passesFaulty) {
        case (true, false): return "ayiran"
        case (true, true): return "kor"
        case (false, false): return "gurultulu"
        case (false, true): return "ters"
        }
    }

    func describe() -> String {
        let onCorrect = passesCorrect ? "gecti" : "kaldi"
        let onFaulty = passesFaulty ? "gecti" : "kaldi"
        print("\(label) | dogru=\(correct)/\(onCorrect) kusurlu=\(faulty)/\(onFaulty) -> \(separation)")
        return separation
    }
}

func contrast<Output>(_ label: String, correct: Output, faulty: Output, check: (Output) -> Bool) -> Contrast<Output> {
    Contrast(label: label, correct: correct, faulty: faulty, passesCorrect: check(correct), passesFaulty: check(faulty))
}

func countSeparations(_ results: [String]) {
    for name in ["ayiran", "kor", "gurultulu", "ters"] {
        print("\(name): \(results.filter { $0 == name }.count)")
    }
}
```

`Contrast` iki sürümün çıktısını ve denetimin her biri hakkındaki kararını tutar; `separation` etiketi bu iki karardan türer. `contrast` aynı kapanışı (closure) iki çıktıya uygular, `countSeparations` dört kategoriyi sayar. **Kural nedeni:** bir denetimin değeri geçmesinde değil, iki sürümü ayırmasında yatar.

## Tutma Döngüsü

Swift'te nesnelerin ömrünü **referans sayımı** belirler. Her nesne kaç yerden güçlü tutulduğunu sayar ve sayı sıfıra inince bellek serbest kalır. Bir nesneyi başka bir nesne güçlü tutuyorsa ve ikincisi de birincisini güçlü tutuyorsa, ikisinin sayısı da hiçbir zaman sıfıra inmez. Buna **tutma döngüsü** (retain cycle) denir. Dışarıdan kimse nesnelere ulaşamaz, ama nesneler birbirini tutmaya devam eder: **bellek sızıntısı** budur.

Bir kapanış, kendi içinde kullandığı nesneleri varsayılan olarak güçlü yakalar. Bir ekran modeli kapanışı kendi özelliğinde saklıyor ve kapanış `self`'i kullanıyorsa, model kapanışı, kapanış da modeli tutar.

```swift
// ekran modeli — KAYNAK
final class OrderDetailViewModel {
    var onUpdate: (() -> Void)?

    func bind(service: OrderService) {
        service.observe { order in
            self.apply(order)
        }
        onUpdate = {
            self.refresh()
        }
    }
}
```

`onUpdate` kapanışı `self.refresh()` çağırdığı için `self`'i güçlü yakalar; `self` de `onUpdate`'i güçlü tutar. Ekran kapansa bile sayı sıfıra inmez. Çözüm kapanışa bir **yakalama listesi** vermektir ve `self`'i zayıf yakalamaktır.

```swift
// ekran modeli, duzeltilmis — KAYNAK
final class OrderDetailViewModel {
    var onUpdate: (() -> Void)?

    func bind(service: OrderService) {
        service.observe { [weak self] order in
            self?.apply(order)
        }
        onUpdate = { [weak self] in
            self?.refresh()
        }
    }
}
```

`[weak self]` kapanışa `self`'in **zayıf referansını** (weak reference) verir. Zayıf referans nesnenin sayısını artırmaz; nesne serbest kalınca referans kendiliğinden `nil` olur. Bu yüzden kapanışın içinde `self` isteğe bağlı bir değerdir. Bir de `unowned` var: o da sayıyı artırmaz ama nesne gittiyse kullanımı çöker. Nesnenin kapanıştan uzun yaşayacağı kesinse kullanılır; emin olunmadığında `weak` seçilir.

Her kapanış döngü yaratmaz. Bir işlevin parametresi olarak verilen ve hemen çalışıp biten bir kapanış nesneyi bir yerde saklamaz. Döngü, kapanışın saklandığı ve saklayanın da yakalanan nesne olduğu durumlarda oluşur.

## Sızıntıyı Bulmak

Sızıntıyı bulmanın iki yolu var. Birincisi geliştirme ortamının hata ayıklama alanındaki **bellek grafiği** görünümüdür: koşan uygulamayı durdurur, canlı nesneleri ve aralarındaki bağları oklarla gösterir. İkincisi test içinde yapılır. Test bir nesne kurar, ona zayıf bir gözlemci bağlar, nesnenin güçlü referansını bırakır ve gözlemcinin `nil` olduğunu denetler.

```swift
// sizinti testi — KAYNAK
import XCTest

final class LeakTests: XCTestCase {
    func testViewModelIsReleased() {
        var model: OrderDetailViewModel? = OrderDetailViewModel()
        weak var observer = model
        model?.bind(service: StubOrderService())
        model = nil
        XCTAssertNil(observer)
    }
}
```

Bu parça çalıştırılmaz; yukarıdaki türler ve `StubOrderService` burada tanımlı değildir. Test bir model kurar, ona `weak var observer` ile ikinci bir referans verir ve `model = nil` ile tek güçlü referansı bırakır. Döngü yoksa nesne o anda serbest kalır ve `observer` `nil` olur. Döngü varsa nesne yaşar ve `XCTAssertNil` kalır. Birden çok testte aynı denetimi tekrar etmek için `addTeardownBlock(_:)` kullanılabilir: test bittiğinde nesnenin serbest kalıp kalmadığını denetleyen bir kapanış her testin sonunda koşar.

## Sızıntı Modeli

Aşağıdaki **model**, serbest kalmanın iki sürüme ne dediğini kurar; bellek grafiği görünümünün kendisi değildir. Model gerçek nesneler ve gerçek referans sayımı kullanır: tutma döngüsü bu ölçümde gerçekten oluşur ve gerçekten kırılır. Bayt, adres ya da bellek miktarı basılmaz; yalnız bir nesnenin serbest kalıp kalmadığı ve kaç nesnenin canlı olduğu sayılır.

Ekran modeli `bind` ile bir kapanış saklar. Doğru sürüm kapanışta `[weak self]` kullanır. Kusurlu sürüm `self`'i güçlü yakalar.

```swift
// Models.swift — ekran modelinin dogru ve kusurlu surumu, canli nesne sayaci
final class LiveCount {
    var screens = 0
    var loaders = 0
}

final class LoaderModel {
    let counts: LiveCount
    var completion: (() -> Void)?

    init(counts: LiveCount) {
        self.counts = counts
        counts.loaders += 1
    }

    deinit { counts.loaders -= 1 }

    func arm(leaking: Bool) {
        if leaking {
            completion = { _ = self.counts }
        } else {
            completion = { [weak self] in _ = self?.counts }
        }
    }
}

final class DetailViewModel {
    let title: String
    let counts: LiveCount
    var onUpdate: (() -> Void)?

    init(title: String, counts: LiveCount) {
        self.title = title
        self.counts = counts
        counts.screens += 1
    }

    deinit { counts.screens -= 1 }

    func bind(captureStrongly: Bool) {
        if captureStrongly {
            onUpdate = { _ = self.title }
        } else {
            onUpdate = { [weak self] in _ = self?.title }
        }
    }

    func startLoading(leaking: Bool) {
        let loader = LoaderModel(counts: counts)
        loader.arm(leaking: leaking)
    }
}

func shownTitle(captureStrongly: Bool) -> String {
    let model = DetailViewModel(title: "siparis", counts: LiveCount())
    model.bind(captureStrongly: captureStrongly)
    return model.title
}

func closeScreen(captureStrongly: Bool) -> Bool {
    weak var observer: DetailViewModel?
    do {
        let model = DetailViewModel(title: "siparis", counts: LiveCount())
        model.bind(captureStrongly: captureStrongly)
        observer = model
    }
    return observer == nil
}

func closeAndCount(leaking: Bool) -> LiveCount {
    let counts = LiveCount()
    do {
        let model = DetailViewModel(title: "siparis", counts: counts)
        model.startLoading(leaking: leaking)
    }
    return counts
}
```

`closeScreen` testin modelidir: `weak var observer` zayıf gözlemcidir, `do` bloğunun bitişi ekranın kapanışıdır ve dönen değer gözlemcinin `nil` olup olmadığıdır. `shownTitle` ekranın gösterdiği başlığı verir. Birincisi **serbest kalmaya**, ikincisi **görünen veriye** bakar. `LiveCount` ve `startLoading` bu dersin ikinci ölçümü içindir.

```swift
// main.swift — gorunen veri ve serbest kalma
let title = contrast("ekran-verisi", correct: shownTitle(captureStrongly: false), faulty: shownTitle(captureStrongly: true), check: { $0 == "siparis" }).describe()
let released = contrast("ekran-serbest-kaldi", correct: closeScreen(captureStrongly: false), faulty: closeScreen(captureStrongly: true), check: { $0 }).describe()

countSeparations([title, released])
```

```
ekran-verisi | dogru=siparis/gecti kusurlu=siparis/gecti -> kor
ekran-serbest-kaldi | dogru=true/gecti kusurlu=false/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

`ekran-verisi` kör: iki sürüm de aynı başlığı gösterir. Ekranın görünen davranışı iki sürümde birebir aynıdır. `ekran-serbest-kaldi` ayıran: doğru sürümde zayıf gözlemci `nil` olur, kusurlu sürümde nesne yaşamaya devam eder ve gözlemci hâlâ bir nesneyi gösterir.

Bu yüzden bellek sızıntısı sıradan davranış testlerine görünmez. Ekranın ne gösterdiğine bakan hiçbir denetim, ekranın kapandıktan sonra nerede olduğunu söylemez. Döngü modelde de gerçektir: kusurlu sürümde nesne süreç bitene dek kalır, çünkü onu serbest bırakacak kimse yoktur.

**TS1.** Model tek bir kapanış yakalamasını ölçer. Aynı döngü iki nesnenin birbirine verdiği güçlü özelliklerle ya da bir zamanlayıcıyla da oluşabilir; bunlar ayrıca ölçülmelidir.

**TS2.** `closeScreen` kusurlu sürümde gerçekten sızıntı bırakır. Ölçüm tek bir süreçte, kısa ömürlü olduğu için bir zarar vermez; gerçek bir uygulamada her ekran açılışı bir sızıntı daha ekler.

## Neyin Sayıldığı

Test içinde sızıntıyı bulmanın bir ikinci biçimi, **canlı nesne sayısına** bakmaktır: ekran açılmadan önce sıfır, kapandıktan sonra yine sıfır olmalıdır. Sayaç basit ve güçlü görünür. Ama sayacın neyi saydığı sonucu belirler.

Kapanışlar yalnız ekranın kendisini yakalamaz. Ekran bir yükleyici nesne başlatabilir ve yükleyicinin tamamlanma kapanışı yükleyiciyi yakalıyor olabilir. Bu durumda ekran serbest kalır ama yükleyici sızar.

Aşağıdaki ölçüm bu durumu kurar. Ekran `startLoading` ile bir yükleyici başlatır. Kusurlu sürümde yükleyicinin kapanışı yükleyiciyi güçlü yakalar; doğru sürümde zayıf yakalar. Ekran iki sürümde de kendi başına serbest kalır. Aynı sayaçtan iki değer okunur: canlı ekran sayısı ve canlı yükleyici sayısı.

```swift
// main.swift — ayni sayac, iki sayilan nesne
let correct = closeAndCount(leaking: false)
let faulty = closeAndCount(leaking: true)

let screens = contrast("sayilan-ekran", correct: correct.screens, faulty: faulty.screens, check: { $0 == 0 }).describe()
let loaders = contrast("sayilan-yukleyici", correct: correct.loaders, faulty: faulty.loaders, check: { $0 == 0 }).describe()

countSeparations([screens, loaders])
```

```
sayilan-ekran | dogru=0/gecti kusurlu=0/gecti -> kor
sayilan-yukleyici | dogru=0/gecti kusurlu=1/kaldi -> ayiran
ayiran: 1
kor: 1
gurultulu: 0
ters: 0
```

Canlı ekran sayısı iki sürümde de sıfırdır, çünkü ekran serbest kaldı. Bu denetim kör: sızıntıyı göremez. Canlı yükleyici sayısı doğru sürümde sıfır, kusurlu sürümde birdir ve denetim ayırır. Sayaç aynı, kusur aynı; yalnız sayılan nesne değişti.

Burada ders şudur: bir sızıntı testi, sızdıran nesneyi saymıyorsa sızıntıyı bulamaz. Hangi nesnenin sızabileceğini önceden bilmek zordur; bu yüzden bellek grafiği görünümü, tüm canlı nesnelere bakarak testin atladığı yerleri gösterir. Test sorulan soruyu cevaplar, görünüm sorulmamış soruyu da gösterebilir.

**TS3.** Model yükleyiciyi ekranın bir yardımcısı olarak kurar. Gerçek uygulamada sızıntı çoğunlukla ekranın hiç bilmediği bir katmanda doğar.

**TS4.** Sayaç yalnız iki nesne türünü sayar. Başka türde bir sızıntı bu ölçümde görünmez.

## Sık Yapılan Hatalar

- Kapanışta `self`'i yakalama listesi vermeden kullanmak ve kapanışı özellikte saklamak.
- Her kapanışa `[weak self]` yazmak. Hemen çalışıp biten bir kapanışta gereksizdir.
- Nesnenin ömrü belirsizken `unowned` kullanmak. Nesne gittiyse uygulama çöker.
- Yalnız ekranın serbest kaldığına bakıp ekranın başlattığı yardımcı nesneleri atlamak.
- Sızıntıyı yalnız davranış testleriyle aramak. Davranış doğru kalırken bellek büyür.

## Özet

- Tutma döngüsü iki nesnenin birbirini güçlü tutmasıdır; referans sayısı sıfıra inmez ve bellek sızar.
- Kapanış `self`'i güçlü yakalar; `[weak self]` döngüyü kırar, `unowned` ise nesne gittiyse çöker.
- İki denetimden biri kör, biri ayıran çıktı: görünen veri iki sürümde aynıydı, serbest kalma yalnız doğru sürümde gerçekleşti.
- Aynı canlı nesne sayacı, ekranı sayınca kör, sızdıran yükleyiciyi sayınca ayıran oldu.
- Sızıntıyı bulan test, sızdıran nesneyi saymalıdır; bellek grafiği görünümü atlanan nesneleri gösterir.
- Bu dersin ayrımı: **ayıran**.

## Sonraki Adım

Sızıntı bir kusurdu; başlangıçta yapılan gereksiz iş ise başka bir kusur. Sıradaki derste Başlangıç ve Kaydırma Başarımı, kayıtlı bir temel değerle karşılaştırılan başarım denetiminin hangi sürümden alındığına göre nasıl tersine dönebildiğini ölçecek.
