Swift’te Memory Leak

Göktuğ Gümüş
NSIstanbul
Published in
5 min readApr 28, 2019

Yazıda memory leak’in ne olduğundan ve bu durumdan nasıl kaçınabileceğimizden bahsedeceğim.

Biz geliştiricilerin sıkça karşısına çıkan bu problem, ne yazık ki hadi çözdüm tamam artık bitti dediğimizde bitmiyor. Her geçen gün geliştirdiğimiz özellikler ile birlikte uygulamamız büyüyor, tekrardan baş etmemiz gereken bir sorun haline gelebiliyor.

Nedir Bu Memory Leak?

Belleğin bir kısmının sonsuza kadar işgal edilmesi ve bir daha hiç kullanılmamasına memory leak (bellek sızıntısı) denir.

Bir de Apple dokümanındaki açıklamaya bakarsak;

Uygulamanın yaşam döngüsünün bir noktasında kullanıma tahsis edilen belleğin bir kısmının, referans eden bir nesnenin kalmamasına rağmen hafızadan uçurulup, tekrar kullanılamamasına memory leak denir.

Ne kadar deneyimli geliştirici olursak olalım illaki kodumuzun bir yerinde memory leak oluşturabiliriz. Önemli olan bunları nasıl temizleyeceğimizi bilmek ve crash-free uygulamalar tasarlayabilmektedir. Neden mi? Çünkü bu sızıntılar tehlikelidir.

Memory Leak’ler Tehlikelidir!

Bu sızıntılar sadece hafızayı gereksiz şişirmekle kalmaz aynı zamanda istemediğimiz yan etkilere ya da çökmelere yol açabilir.

Genel olarak bakarsak mobil cihazların masaüstü bilgisayarlara göre daha sınırlı bellekleri olduğu aşikar. O yüzden bu sınırlı hafızayı en etkin şekilde kullanmamız gerekiyor. Bundan dolayı oluşturduğumuz nesnelerin işleri bittiğinde (referansları kalmadığında) hafızadan düzgün bir şekilde temizlenmeleri gerekiyor. Eğer bu işlem kitabına uygun bir şekilde gerçekleşmezse; bu nesnelerin çöpten farkı kalmıyor. Bu aksiyonların tekrarlanması sonucunda elimizde bir ton çöp oluşuyor. Bu durum devam ettiği takdirde ise hafıza dolmaya başlıyor ve en sonunda da uygulamamız yavaşlamaya başlıyor ve çökebiliyor.

Aslında uygulamanın çökmesi bizim için iyi bir senaryo bile olabilir. En azından uygulamayı kullanıcı tekrar başlattığında hayatına kaldığı yerden devam edebilir. Asıl sorun; geri döndürülmesi daha zor durumlara yol açabilecek sıkıntılar…

Örnek vermek gerekirse bir nesnemiz olsun ve init metodunun içersinde bir notification’ı dinlemeye başlasın. İlgili notification her tetiklendiğinde veritabanına bir şeyler kaydettiğini ya da back-end serverımıza bir takım veriler yolladığını düşünelim. Bu nesnenin deinit metodunun içine bu notification’ı dinlemeyi durdurmasını söyleyelim.

Buraya kadar her şey güzel. Peki ya bu nesnemizde sızıntı oluşursa ne olur?

Hiçbir zaman ölmez ve notification’ı sonsuza kadar dinlemeye devam eder. Her notification tetiklendiğinde, bizim nesnemizde bunu yakalayıp, ilgili işlemleri yapmaya devam edecektir. Kullanıcı bir takım aksiyonlar sonucunda bu nesneden bir sürü oluşmasına neden olabilir. Bu nesnelerin hepsi de notification’lara reaksiyon gösterip, veritabanındaki verileri bozabilir ve uygulamanızın durumunu geri dönülemeyecek bir hale sokabilir.

Bu tarz bi durumda uygulamanızın crash etmesi belki başınıza gelecek en iyi şey bile olabilir.

Sızıntıya uğramış bi takım objelerin app notification’lara reaksiyon göstermesi, veritabanında işlemler yapması, UI’yı değiştirmesi uygulamanın o an ki durumunun bozulmasına yol açabilir. Bu tarz problemlerin önemini Pragmatic Programmer kitabındaki “Dead programs tell no lies” cümlesi çok güzel vurguluyor.

Bu Memory Leak’ler Nasıl Oluşuyor?

Sızıntılar 3. parti kütüphanelerden ve SDK’lerden gelebilir. Hatta Apple tarafından oluşturulmuş CALayer ve UILabel sınıflarında bile bulunuyor. Bu tarz sızıntılar için güncelleme beklemek ya da o SDK’i kullanmamak dışında pek de yapabileceğimiz bir şey bulunmuyor.

Genellikle bizim kodumuzda oluşan sızıntıların nedeni retain cycle’lar.

Bu tarz sızıntıları önlemek için hafıza yönetimi ve retain cycle’ların ne olduğunu iyi anlamamız gerekiyor.

Retain Cycle

Retain kelimesi Objective-C zamanlarındaki Manual Reference Counting günlerinden geliyor. O zamanlar hafıza yönetimi konusunda biraz daha bilgi sahibi olmamız gerekiyordu. Alloc, copy, retain kavramlarını anlayıp, aldığımız aksiyonların tersini uygulayarak dengelemek gerekiyordu. Yani bir obje oluşturduğumuzda artık o objeden biz sorumluyduk ve işi bittiğinde temizlemekten de biz sorumluyduk.

Şu an bu işler çok daha kolay fakat bilmemiz gereken bazı konseptler var.

Swift’te bir objenin başka bir objeyle sıkı bir bağı (Strong Association) varsa orada bir retain durumu vardır. Bu arada obje dediğimde Reference Type’lardan yani Class’lardan bahsettiğimi unutmayın.

Struct ve Enum’lar value type’tır. Value type’ların retain cycle oluşturması mümkün değildir. Bu tiplerdeki objeler sakladıkları verilerin referanslarını değil onların değerlerinin kopyalarını tutarlar. Referans tutmadıkları için de retain cycle oluşturamazlar.

Bir obje, ikinci bir objeye referans ederse artık ona sahiptir. Bu konsepte Strong Reference denir. İlk obje yok olduğunda ikinci obje de yok olacaktır çünkü ilk obje, ikinci objenin sahibidir.

Retain cycle örneği

A, B’yi barındırır ve B de A’yı barındırırsa bu durumda retain cycle oluşur.

A 👉 B + A 👈 B = 🌀

Yukarıdaki örnekte de retain cycle durumu olduğu için hem Book hem de Page objesinin hafızadan yok olması mümkün değildir.

Bir objenin hafızadan yok olması için önce tüm bağlılıklarının yok olması gerekmektedir. Eğer objenin bağlılıklarından biri kendisi ise bu durumda hiçbir zaman hafızadan yok olamaz.

Referanslardan birisi weak ya da unowned olarak tanımlanırsa, bu retain cycle durumunu kırabiliriz. Kodlama yaparken bu cycle yani birbirine referans etme durumu ilişkilerin doğası gereği gereklidir. Buradaki problem tüm ilişkilerin strong (güçlü) bir durumda olmaması gerekmektedir. Bir taraf weak (zayıf) olarak tanımlanmalıdır.

Weak ya da Unowned anahtar kelimesi retain cycle’ı kırar

Retain cycle oluşturabilecek durumlardan bazılarına bakarsak;

1. Closure

Sınıflar arasındaki strong reference cycle’ların leak durumu oluşturması dışında leak oluşumuna çok açık diğer bir yapı da closure’lardır. Closure’lar tanımlandığında geçici bir sınıf (class) yaratır ve kod bloğunun içinde kullanılacak reference type türündeki objelerin referenslarını tutar, value type objelerin ise kopyalarını tutar.

Örnek A

Yukarıdaki örneğe bakarsak tapHandler closure’ı self objesine ihtiyaç duymaktadır. O yüzden aralarında bir strong reference cycle durumu vardır. Bu durum self objesinin hafızadan yok olmasını engelleyecektir. Yok olduğu durumda ise closure, self e ulaşmaya çalıştığında artık hafızada olmadığı için uygulamamız crash edecektir.

Kendimize her zaman sormamız gereken soru; “Bu closure’ın sahibi kim?”

Örnek B

UITableView’de kullanmak için içinde bir tane buton bulunan custom bir cell oluşturalım.

Daha sonra View Controller’ımız içerisine cell’in içindeki butona basıldığında alınmasını istediğim aksiyonu tanımlayalım.

Bu closure’ın sahibi kim?; Closure’ı CustomCell’in içinde tanımladığımız için sahibi CustomCell’dir. Fakat CustomCell, TableView’e aittir. TableView ise TableViewController’a aittir ve CustomCell’in içindeki butona tıkladığında ise closureself yani TableViewController objesine ulaşmaya çalışacaktır. Burada ilişki göz önüne alındığında bi cycle durumu olduğunu görüyoruz.

Bu durumda bu Closure ile TableViewController arasındaki güçlü bağı zayıflatıp, döngü durumunu kırmak için weak ya da unowned anahtar kelimelerini kullanmalıyız.

Eğer closure’ın içindeki sakladığı objelerden daha uzun süre yaşamayacağına eminseniz unowned anahtar kelimesini kullanabilirsiniz. Eğer emin değilseniz weak anahtar kelimesini kullanın. Aksi taktirde uygulamanız crash edebilir. Bizim senaryomuzda; cell ve closure, TableViewController’den daha uzun yaşamayacağı için unowned anahtar kelimesinin kullanımının bir sakıncası yoktur.

2. Delegation Tasarım Deseni

Delegation, Apple’ın da çok kullandığı basit ve güçlü tasarım desenlerinden biridir. Bu desenin prensibi; nesnenin yerine getirmesi gereken görevi kendisi yapmak yerine başka bir nesneye görevi delege ederek yaptırmasıdır.

Book, Page örneğindeki gibi aynı durumdan dolayı strong reference oluşturmamak için yukarıdaki örnekteki gibi delegate objemizi weak olarak tanımlamamız gerekmektedir.

Protokolleri weak olarak tanımlayabilmek için AnyObject ya da class ‘tan türetmemiz gerekmektedir.

Bu yazımı burada sonlandırıyorum. Diğer yazıda memory leak oluşturan durumları bulmak için nasıl yöntemler izleyebiliriz onlara değineceğim.

Yazıyı beğendiyseniz beğeni atmayı ve beni takip etmeyi unutmayın.

--

--