Swift: Unit test ile strong reference cycle yakalama
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ı event
i 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
Subscribe to my newsletter
Read articles from Erk Ekin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by