DispatchQueue with weak self
DispatchQueue 를 사용할때 weak self를 사용해야 하는지에 대해..
지금까지는 그냥 자연스럽게 습관적으로 closure 를 사용할때 순환 참조를 막기 위해서 weak self를 사용했다.
꼭 필요한지에 대한 근본적인 질문을 조금이나마 해결해보고자
1. [unowned self] 는 좋은 생각이 아니다.
unowned self 도 강한 순환 참조를 피할 수 있는 방법이지만
self를 강제로 unwrapping하고 할당이 해제된 후에도 내용에 액세스 하려고 시도하기때문에 unowned는 매우 안전하지 않음
weak을 사용하는 것이 훨씬 안전한 방법으로 순환 참조를 피할 수 있게 함
weak는 순환 참조를 피할 수 있게 하는 과정에서 self를 Optional로 만든다. Optional Chaining(self?.)을 사용할 수도 있고, guard let 을 통해 클로저 시작 시 self에 대해 임시 강한 참조를 만들 수 있다.
Optional 처리를 피하고 싶은데 weak보다 unowned를 사용하고 싶다면 클로저 실행 중 참조가 절대 nil이 되지 않는다고 확신될 때만 사용해야 한다. unowned는 optional을 강제로 unwrapping하는 것과 같으며, nil이 되면 충돌이 발생한다. [weak self]가 훨씬 더 안전한 대안
2. Non-escaping closure vs. Escaping closure
[weak self]가 훨씬 안전하다는 사실을 확인했으니 모든 클로저에서 [weak self]를 사용해야 할까?
Non-escaping closure
Non-escaping closure는 범위(scope) 내에서 실행된다.
즉, 코드를 즉시 실행하고 나중에 저장하거나 실행할 수 없다.
예를들어 compactMap과 같은 고차함수 들은 weak, unowned를 사용할 필요가 없다.
// 단순히 return 전에 closure가 실행되는 경우에는 non-escaping
func someFunction(completion: () -> Void = {}) {
completion()
print("someFunc!")
return
}
Escaping closure
Escaping clousre는 저장될 수 있고, 다른 클로저로 전달될 수 있으며 미래의 어느 시점에 실행될 수 있다.
Escaping closure는 다음 두 조건이 모두 충족되는 경우 weak 또는 unowned를 사용해야함
- 클로저는 속성에 저장되거나 다른 클로저로 전달
- 클로저 내부의 객체(예: self)는 클로저(또는 전달된 다른 클로저)에 대해 강력한 참조를 유지한다.
// 함수의 return보다 completion이 늦게 실행되는 경우 (escaping closure)
func someFunction(completion: @escpaing () -> Void = {}) {
self.someDelayProcess {
completion()
}
print("someFunc!")
return
}
Delayed Deallocation
Deplayed Deallocation은 Escaping 및 Non-escaping 클로저에서 나타나는 부작용.
정확히는 메모리 누수는 아니지만 원하지 않는 동작으로 이어질 수 있다.
(Ex, Contorller를 해제했지만 보류 중인 모든 클로저의 작업이 완료될 때까지 메모리 해제되지 않음)
이 경우 [weak self]를 사용해 방지 가능.
3. ‘guard let self = self’ vs. Optional Chaining
[weak self]를 사용할때 Optional Chaining(self?.)을 사용하는 대신 guard let self = self 를 사용하면 발생하는 잠재적인 부작용이 있다.
‘guard let self = self else { return }’
Delayed Deallocation이 나타날 수 있는 클로저에서 ‘guard let self = self’ 구문을 사용하면 Deplayed Deallocation을 방지할 수 없다.
‘guard let’ 구문은 self가 nil과 같은지 여부를 확인, nil이 아닌 경우 동안 self에 대한 일시적인 강한 참조를 생성하는 것
‘guard let’ 구문은 클로저의 수명 동안 self가 할당 해제되는 것을 방지해 self가 유지되도록 보장한다.
‘self?.’
‘guard let’ 구문 대신 Optional Chaninng(self?.)을 사용하는 경우 클로저가 시작할때 강한 참조를 만드는 대신 모든 메서드 호출에 대해 self에 대한 nil 검사를 진행한다.
클로저 실행 중 어느시점에 self가 nil이 되면 자동으로 해당 메서드 호출을 건너뛰고 다음 줄로 이동한다.
// 'guard let' 구문은 임시 강한 참조기 때문에 다 실행되기 전에는 해제되지 않음 -> Delayed Deallocation
func process(image: UIImage, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
guard let self = self else { return }
// perform expensive sequential work on the image
let rotated = self.rotate(image: image)
let cropped = self.crop(image: rotated)
let scaled = self.scale(image: cropped)
let processedImage = self.filter(image: scaled)
completion(processedImage)
}
}
// Optional Chaining(self?.)은 참조카운트가 0이 되어 해제되는 순간 self는 nil이 되기 때문에 실행되지 않음
func process(image: UIImage, completion: @escaping (UIImage?) -> Void) {
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
// perform expensive sequential work on the image
let rotated = self?.rotate(image: image)
let cropped = self?.crop(image: rotated)
let scaled = self?.scale(image: cropped)
let processedImage = self?.filter(image: scaled)
completion(processedImage)
}
}
‘gaurd let’ 구문은 경우에 따라 Delayed Deallocation으로 이어질 수 있다. 때문에 경우에 따라 ‘guard let’, ‘Optional Chaining’ 중 어떤 구문을 사용해야 할지 생각할 필요가 있다.
생각해 봐야 하는 경우 ViewController가 해제(dismiss)된 후 불필요한 작업을 피해야 하는 경우와 반대로 객체가 할당 해제 되기 전 모든 작업이 완료되었는지 확인하려는 경우(Ex: 데이터 손상 방지를 위해)
4. Common situations where [weak self] may or may not be needed.
[weak self]가 필요할 수도 있고 필요하지 않을 수도 있는 상황 몇가지
Gand Central Dispatch(GCD)
GCD 호출은 나중에 실행하기 위해 저장하지 않는 한 순환참조의 위험이 없다.
// 예를 들어, 이러한 호출은 [weak self]가 없어도 즉시 실행되기 때문에 메모리 누수를 일으키지 않음.
func nonLeakyDispatchQueue() {
DisPatchQueue.main.asyncAfter(deadline: .now() + 1.0 {
self.view.backgroundColor = .red
}
DispatcQueue.main.async {
self.view.backgroundColor = .red
}
DispatchQueue.global(qos: .background).async {
print(sekf,bavugatuibUten.description)
}
}
// DispatchWorkItem은 속성(property)에 저장하고 [weak self] 키워드 없이 클로저 내부에서 self를 참조하기 때문에 메모리 누수가 발생한다.
func leakyDispatchQueue() {
let workItem = DispatchWorkItem {
self.view.backgroundColor = .red
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, excute: workItem)
self.workItem = workIte // stored in a property
}
UIView.Animate 및 UIViewPropertyAnimator
GCD와 마찬가지로 애니메이션 호출도 속성(property)에 UIViewPropertyAnimator를 저장하지 않는 한 순환참조의 위험이 없다.
// 순환 참조가 발생하지 않는 호출
func animateToRed() {
UIView.animte(withDuration: 3.0) {
self.view.backgroundColor = .red
}
}
// [weak self]를 사용하지 않고 나중에 사용할 수 있도록 애니메이션을 저장하기 때문에 순환참조를 유발
func setupAnimation() {
let animation = UIViewPropertyAnimator(duration: 2.0, curve: .linear) {
self.view.backgroundColor = .red
}
animation.addCompletion { _ in
self.view.bacgroundColor = .white
}
self.animationStorage = animation
}
Storing a function in a property
한 객체의 클로저가 함수를 다른 객체에 전달하여 속성에 저장하는 상황은 눈에 띄지 않는 메모리 누수를 유발할 수 있다.
// 예를들어, 속성(property)에 클로저를 저장하는 PresentedController가 있다.
class PresentedController: UIViewController {
var closure: (() -> Void)?
}
// 그리고 PresentedController를 가지고 있는 MainViewController가 있고, MainViewController의 printer() 메소드를 PresentedController의 클로저에 저장한다.
class MainViewController: UIViewController {
var presented = PresentedController()
func setupClosure() {
presented.closure = printer
func printer() {
print(self.view.description)
}
}
PresentedController에서 클로저를 호출하면 MainViewController의 self.view.description을 프린트할 수 있다.
하지만 이 코드는 명시적으로 self를 사용하지 않았지만 순환참조를 유발한다.
self는 printer에 암시되어 있다. (printer == self.printer) 따라서 클로저는 self.printer에 대한 강력한 참조를 유지하는 반면 self는 클로저를 소유하는 PresentedController를 소유한다.
따라서 순환참조를 제거하기 위해 [weak self]를 포함하도록 setupClosure를 수정해야한다.
func setupClosure() {
presented.clousure = { [weak self] in
// 범위(scope) 내에서 해당 함수를 호출하기를 원하기 때문에
self?.printer()
}
}
Timer
Timer는 속성(property)에 저장하지 않더라도 문제를 일으킬 수 있다.
- Timer가 반복된다.
- [weak self]를 사용하지 않고 클로저에서 self를 참조한다.
func leakyTimer() {
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
let currentColor = self.view.backgroundColor
self.view.backgroundColor = currentColor == .red ? .blue : .red
}
timer.tolerance = 0.1
RunLoop.current.add(timer, forMode: RunLoop.Mode.common)
}
이 두 조건이 충족되는 한 타이머는 참조된 컨트롤러 또는 객체의 할당 해제를 방지한다.
따라서 기술적으로 메모리 누수보다 Delayed Deallocation에 가깝다.
Delayed Deallocation이 무한정 지속된다.
참조된 객체의 무기한 활성 상태 유지를 피하기 위해 더 이상 필요하지 않을때 타이머를 무효화하고
참조된 객체가 타이머에 대한 강한 참조를 유지하는 경우 [weak self]를 사용해야 한다.