Swift 'in Otomatik Referans Sayma (ARC)'yı tanıtmadan önce, geliştiriciler retain, release ve autorelease çağrıları ile belleği manuel olarak yönetiyorlardı, bu da sık sık bellek sızıntılarına veya dangling pointer'lara yol açıyordu. Swift 'in ARC 'si, bu işlemi derleme zamanında retain/release çağrılarını ekleyerek otomatik hale getiriyor, ancak closure'lar ile birlikte ince bir karmaşıklık getiriyor. Closure'lar, çevredeki değişkenleri yakalayan referans türleridir. Bu, Swift'e özgü iki referans türünün yıkılamaz bir döngüsel bağımlılık oluşturabileceği yeni bir bellek sorunları sınıfı kattı ve bu durumu kontrol etmek için capture listesi sözdizimini tanıttı.
Bir sınıf örneği bir closure'ı bir özellik olarak sakladığında ve bu closure self veya diğer instance özelliklerini referans aldığında, ARC, closure'ın ömrü boyunca instance'ı hayatta tutmak için referans sayısını artırır. Closure, örneği kendisi referans aldığından, bir retain döngüsü ortaya çıkar: örnek closure'ı güçlü bir şekilde tutar ve closure da örneği güçlü bir şekilde tutar. Hiçbir referans sayısı sıfıra ulaşmadığından, deinit asla çalışmaz ve uygulamanın ömrü boyunca bellek sızıntısı gerçekleşir.
Swift, default yakalama davranışını değiştirmek için closure'ın parametre listesinin önünde köşeli parantezler içinde virgülle ayrılmış ifadelerle capture listelerini sağlar. [weak self] belirlemek, zayıf bir referans oluşturur (opsiyonel, serbest bırakıldığında nil olur), [unowned self] ise sahip olmayacak bir referans oluşturur (var olduğu varsayılır, serbest bırakıldıktan sonra erişim ile çökme yaşanır). Değerler için, [x = x] mevcut değeri yakalar, referansı değil. Bu, güçlü referans döngüsünü açıkça kırar ve ARC'nin dış referanslar kaldırıldığında örneği serbest bırakmasına olanak tanır.
Kod Örneği:
class DataManager { var completionHandler: ((Data) -> Void)? var data: Data = Data() func fetchData() { // Retain döngüsü: self closure'ı tutar, closure self'i tutar completionHandler = { newData in self.data = newData // Self'in güçlü yakalaması } } func fetchDataFixed() { // Çözüm: zayıf yakalama completionHandler = { [weak self] newData in guard let self = self else { return } self.data = newData } } deinit { print("DataManager serbest bırakıldı") } }
Bir üretim iOS uygulamasında, ProfileViewController adında bir sınıfı, UserService sınıfına bağlı olarak profile verilerini asenkron olarak çekmesi için uyguladık. Servis, iptal edilebilir talepleri desteklemek için özellik olarak saklanan closure tabanlı tamamlayıcı işlevler sunuyordu. Profil ekranından ayrıldığımızda ViewController 'ın deinit tetiklenmediğini gözlemledik ve Instruments, görünüm hiyerarşisini tutan kalıcı bir bellek grafiği raporladı.
Bu sızıntıyı çözmek için birkaç mimari yaklaşım değerlendirdik.
Kullanıcı geri navigasyon yaptığında, tam olarak döngüyü kırmak için tamamlayıcı işlevi nil olarak ayarlamayı denedik. Bu teknik olarak durumu kırsa da, kesintiler veya beklenmeyen durum geçişleri için güvenilir olmadığı kanıtlandı. Ayrıca, closure hiç çağrılmadıysa ve görünüm denetleyicisi sistem tarafından bellek baskısı altında kaybolmadan önce serbest bırakıldıysa da sızdı. Bu yaklaşım, aşırı savunmacı programlama gerektirdi ve görünüm denetleyicisinin servisinin iç durumunu yönetmesini zorlayarak tek sorumluluk ilkesini ihlal etti.
Closure'da [unowned self] kullanarak opsiyonel çözme yükünü önlemeyi değerlendirdik. Bu, sentaktik bir temizlik ve sıfır maliyetli soyutlama avantajları sundu. Ancak test sırasında, hızlı navigasyonun ViewController'ı serbest bırakabileceği ve ağ isteği hala uçuşta olduğu için çökme durumları keşfettik; callback serbest bırakılan örneğe erişmeye çalıştığında çökme yaşandı. Üretim ortamında tanımsız davranış riski, performans avantajlarını aştı.
Closure'ı giriş noktasında [weak self] ile bir guard let self = self else { return } kontrolü ile uyguladık. Bu, tüm yaşam döngüsü senaryolarını güvenli bir şekilde ele aldı: callback tetiklenmeden önce görünüm denetleyicisi serbest bırakılırsa, zayıf referans nil olur, guard sessizce başarısız olur ve ARC closure'ı sonra temizler. Bu, biraz daha fazla boilerplate kod gerektirse de ve küçük bir opsiyonel işleme yükü eklese de, bellek güvenliğini ve çökme öncesi işlemi garanti etti.
Zayıf yakalama yaklaşımını kod tabanında evrensel olarak benimsedik. UserService entegrasyonunu [weak self] kullanarak yeniden şekillendirdikten sonra, bellek grafiği hata ayıklaması, ProfileViewController örneklerinin hemen kapanırken serbest bırakıldığını doğruladı. Xcode 'un bellek grafiği hata ayıklayıcısı, closure'dan kalan güçlü referanslar göstermedi ve Instruments sızıntı deteksiyonu, özellikle sızıntı olmadığını bildirdi. Bu kalıp, tüm closure tabanlı asenkron API'lerimiz için standart haline geldi.
Bir closure'da bir struct örneğinin yakalanması, bir sınıf örneğini yakalamaktan nasıl farklıdır ve neden struct'lar retain cycle oluşturamaz?
Birçok aday, closure'da self'in yakalanmasının her durumda retain döngüsü riski taşıdığını yanlış bir şekilde varsayıyor. Struct'lar, Swift'te değer türleridir; yani kopyalanırlar, referans alınmazlar. Bir struct bir closure tarafından yakalandığında, ARC struct'ın değerini closure'ın yakalama listesine kopyalar (veya optimizasyona bağlı olarak değiştirilemez kopyaya referans alır), ama önemli olarak, struct'ın bir referans sayımı yoktur. Closure, bir değer tuttuğundan, heap'te tahsis edilmiş bir nesneye yönelik bir referans bulunmadığı için closure ile orijinal struct örneği arasında döngüsel bir referans olasılığı yoktur.
Tehlike yalnızca self bir sınıfa (referans türü) işaret ettiğinde vardır ve burada closure, heap nesnesine yönelik bir gösterim oluşturur ve referans sayısını artırır. Bu ayrım, SwiftUI görünüm struct'ları ile UIKit görünüm denetleyicileri ile çalışırken yakalama liste değiştiricilerini ne zaman uygulayacaklarına karar vermek için kritik öneme sahiptir.
Objektif ömrü varsayımlarında [weak self] ve [unowned self] arasında kesin fark nedir ve [unowned self] ne zaman çökme yaratır?
Adaylar genellikle bunları birbirinin yerine kullanır. [weak self] yakalamayı opsiyonel bir WeakReference'a dönüştürür, bu da ARC tarafından nesne serbest bırakıldığında otomatik olarak nil olarak ayarlanır. Erişim sağlamak, opsiyonel bağlamayı gerektirir ve nesne hayatta kalmışsa güvenlidir. [unowned self] bir sahip olmayan referans oluşturur ve nesnenin closure'ın tüm ömrü boyunca var olduğunu varsayar; bu da asla nil olarak ayarlanmayan açıkça sarılmış opsiyonel gibi davranır.
Closure nesneden daha uzun yaşarsa (örneğin, bir saklanan tamamlayıcı işlev, görünüm denetleyici poplandıktan sonra çağrılırsa), self'e erişmek, bir dangling pointer'ı derefere eder; bu da EXC_BAD_ACCESS çökmesine neden olur. [unowned self]'i yalnızca closure ile nesnenin aynı ömre sahip olduğu durumlarda (örneğin, kaçış yapmayan closures veya closure'a ait nesnelerin uzun ömürlü olduğu belirli görev örüntülerinde) kullanın.
Yakalama listeleri, closure kapsamının dışında tanımlanan değişkenlerle nasıl etkileşir ve [x] değer türleri için bir kopya mı yoksa referans mı oluşturur?
Yaygın bir yanlış anlama, yakalama listelerinin yalnızca self'i etkilediğidir. { [x] in ... } yazdığınızda, closure'ın oluşturulma noktasındaki mevcut x değerini açıkça yakalarsınız, bu içerideki closure üzerinde değişmez bir gölge kopya yaratır. Yakalama listesi olmadan, closure, orijinal değişken depolama konumuna bir referans tutar, bu da closure'ın oluşturulmasından sonraki mutasyonları görmesine ve eğer x bir referans türü ise döngüsel bir mantığa katılmasına olanak tanır.
Int veya String gibi değer türleri için, [x] bir kopyayı yakalar, bu da closure'ın x üzerindeki dış değişiklikleri gözlemlemesini engeller ve closure'ın davranışının yakalama zamanındaki duruma bağlı olarak belirlenmesini sağlar. Bu ayrım, closure'lar tanımlandıkları kapsamdan kaçarak, orijinal bağlamın çok sonra değiştiği durumlarda kritik hale gelir.