Swift: Unit test ile strong reference cycle yakalama

Erk EkinErk Ekin
3 min read

Burada strong reference cycle'ın ne olduğunu anlatmayacağım ama kısaca bahsedecek olursam, bir reference type instance'ınız olsun A, bunun stored property'lerinden en az birinin kendisi veya nested bir yapı düşünürseniz ağacın yapraklarından biri geri dönüp tekrar A'ya strong reference tutuyorsa, ARC gidip de bu objelerin retain count'larını sıfırlayamıyor dolayısıyla bu objeler hiçbir zaman memory'den temizlenemiyorlar.

Bu durum crash log'lara çok yansımasa da uygulamanızın runtime'ında belirsiz davranışlara neden olacaktır. Örneğin bir view controller'ınız var diyelim, bu tip bir memory issue'sü yüzünden silinemiyorsa, gereksiz yere tekrar tekrar yaratılacaktır. Ek olarak diyelim ki bu controller'dan siz bazı analytics event'leri fırlatıyorsunuz. Aynı eventi dashboard'unuzda tekrarlar biçimde göreceksiniz ama aslında user başına bir event bekliyordunuz. Ya da diğer bir senaryoda backend'e call yapıyorsunuz, bu call'lar tekrarlanacak ve boş yere sunuculara yük olacaktır. Yapacak en iyi şey bu cycle'ları tek tek temizlemektir fakat en iyisi en baştan hiç raise etmemek.

Bu yüzden potansiyel olarak reference cycle olabileceğini düşündüğünüz kısımları unit test ile cover etmek mantıklı. Bunun için çeşitli öneriler getirildi ancak ben autoreleasepool kullanan bir yardımcı method önereceğim.

public func AssertNoStrongReferenceCycle<T: AnyObject>( // 1
   file: StaticString = #file, // 2
   line: UInt = #line, // 3
   act: () throws -> T // 4
 ) rethrows { // 9
   weak var system: T? // 5
   try autoreleasepool { // 6
     system = try act() // 7
   } 

   XCTAssertNil(system, "You've got a reference cycle in \(T.self).", file: file, line: line) // 8
 }

autoreleasepool aslında objc zamanlarından elimize kalan kullanışlı bir kavram, onu da burada çok açıklamayacağım dileyen orijinal dökümantasyonuna ve Bruno Rocha'nun bloguna göz atabilir. Çok kısaca yine özetlemem gerekirse autoreleasepool bir closure alıyor (bkz yorum 6), bu closure içinde allocate edilen objeler method sonunda release ediliyor (bkz yorum 7).

Bu bilgiyi nasıl kullanıyoruz? Eğer release edilen bir objeyi weak bir property'ye atamışsak (bkz yorum 5) sonucunda ilgili property'nin nil olmasını bekleriz (bkz yorum 8), olmuyorsa bir yerlerde birşeyler birbirini bırakmıyor demektir, yani strong reference cycle var ve kaldırmamız lazım.

Yardımcı methodun ayrıntılarına gelecek olursam, method T generic variable bekliyor ve bu type T AnyObject'e conform etmek zorunda (bkz yorum 1). AnyObject reference type tipini zorunlu kılan bir kısıt çünkü bildiğiniz gibi value type'larda reference tutulmadığı için memory leak problemi de yok.

Method üç adet girdi alıyor, file (bkz yorum 2), line (bkz yorum 3) ve asıl önemli kısım olan act (bkz yorum 4). file ve line'ı daha önce testler için yardımcı methodlar yazmış olanlar bilir, Xcode'un hata mesajlarını doğru satırda vermesini sağlayan ayrıntılar, act ise kodunuzun asıl test etmek istediğiniz şüpheli kısım. act closure'u bazen içinde try içerebilir, örneğin XCTUnwrap kullanılabilir bu yüzden throws ekledim (bkz yorum 4), fakat içermeyebilir de, bu iki durumu aynı anda sağlayabilmek için ana methoda rethrows ekledim (bkz yorum 9).

Şimdi bu methodun örnek kullanımına geçeyim. İki adet class'ımız olsun A ve B, bunların birbirlerine referansları olsun. Bir tanesininki optional olsun ki sıra sıra init edebilelim (bkz: yorum 11).

import XCTest

class A {
    weak var b: B?  // 10
    init(b: B?) {
        self.b = b
    }
}

class B {
    let a: A
    init(a: A) {
        self.a = a
    }
}

final class ReferenceTypeTests: XCTestCase {
    func testExample() {
        AssertNoStrongReferenceCycle { () -> A in
            let a = A(b: nil) // 11
            let b = B(a: a)
            a.b = b
            return a
        }
    }
}

Burada en bariz şekilde A ile B arasında strong reference cycle raise etmeyen bir ilişki kurdum, etmeyen dedim çünkü A'nın B ile olan ilişkisi weak modifier'ı ile işaretlenmiş (bkz: yorum 10). Eğer bu weak'i kaldırırsanız hata mesajı alacaksınız. Bu yöntemle karmaşık view controller'ları, reactive property'lerinizi sink ettiğiniz reference type'ları, ya da closure inject ettiğiniz herhangi bir class'ın strong reference raise edip etmediğini test edebilirsiniz.

Referanslar

https://www.hackingwithswift.com/example-code/language/how-to-use-the-rethrows-keyword

https://gist.github.com/erkekin/0f44eefb62f62e9f79b8f544ca2dd46e

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

https://developer.apple.com/documentation/foundation/nsautoreleasepool

https://swiftrocks.com/autoreleasepool-in-swift

1
Subscribe to my newsletter

Read articles from Erk Ekin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erk Ekin
Erk Ekin