[iOS] Capture List 획득목록

이전에 학습할 때 순환참조에 의한 메모리 누수, 그리고 그것을 해결하기 위한 약한참조에 대해 알아보았다.

오늘은 살짝 다른 주제에 대해서 정리하려고 한다

클로저의 강한참조 순환

강한 참조 순환 문제는 두 인스턴스끼리의 참조일 때만 발생하는 것 외에도 클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 발생한다.

예를들어, 클로저 내부에서 self.someProperty 처럼 인스턴스의 프로퍼티에 접근할 때나 클로저 내부에서 self.someMethod()처럼 인스턴스의 메서드를 호출할 때 값 획득이 발생할 수 있는데, 두 경우 모두 클로저가 self를 획득하므로 강한참조 순환이 발생한다.

import Foundation

class Person {
  let name: String
  let hobby: String?
  
  lazy var introduce: () -> String = {
    var introduction: String = "My name is \(self.name)"
    guard let hobby = self.hobby else {
      return introduction
    }
    
    introduction += " "
    introduction += "My hobby is \(hobby)"
    
    return introduction
  }
  
  init(name: String, hobby: String? = nil) {
    self.name = name
    self.hobby = hobby
  }
  
  deinit {
    print("\(name) is being deinitialized")
  }
}

var kane: Person? = Person(name: "kane", hobby: "soccer")
print(kane?.introduce() as! String)
kane = nil
//My name is kane My hobby is soccer

코드 마지막에 결과를 보면 kane 변수에 nil을 할당했지만 deinit이 호출되지 않은 것을 볼 수 있다... 메모리 누수가 발생한 것!!

print(kane?.introduce() as! String)
kane = nil

이렇게 한다면???

//kane is being deinitialized

그렇다.. 해제가 된다

자기소개를 하려고 introduce 프로퍼티를 통해 클로저를 호출하면 그 때 클로저는 자신의 내부에 있는 참조 타입 변수등을 획득한다. 문제는 여기부터인데...

클로저는 자신이 호출되면 언제든지 자신 내부의 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지하는데, 이때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킨다.

이렇게 강한참조 순환이 발생하면 자신을 강한참조 프로퍼티로 갖는 인스턴스가 메모리에서 해제될 수 없습니다.



획득목록(Capture List)

우리는 이러한 문제를 획득목록을 통해 해결할 수 있다. 획득목록은 클로저 내부에서 참조 타입을 획득하는 규칙을 제시할 수 있는 기능이다.

그러니깐 앞의 코드에서 self를 약한 참조로 지정할 수 있다!!!! 라는 것이다

var a = 0
var b = 0
let closure = { [a] in
	print(a, b)
    b = 20
}

a = 10
b = 10
closure() // 0 10
print(b) // 20

위 코드를 보면 변수 a의 경우 획득목록을 통해 클로저가 생성될 때 값 0을 획득했지만, b의 경우는 값 획득이 이뤄지지 않았다

나중에 a, b 값이 할당되었지만 closure가 실행될 때 a의 경우만 이전의 0 값을 출력한다.

B.U.T 이것은 a, b가 값 타입이기 때문이다

그럼 참조타입의 경우는?

class SimpleClass {
  var value: Int = 0
}

var x = SimpleClass()
var y = SimpleClass()

let closure = { [x] in
  print(x.value, y.value)
}

x.value = 10
y.value = 10

closure() // 10 10

변수 x의 경우는 획득목록을 통해 값이 지정되었지만, y의 경우는 그렇지 않다.

But, 출력되는 것을 보면 똑같이 참조된다.


class SimpleClass {
  var value: Int = 0
  
  deinit {
    print("deinitialized \(value)")
  }
}

var x: SimpleClass? = SimpleClass()
var y: SimpleClass? = SimpleClass()

let closure = { [x, y] in
  print(x?.value, y?.value)
}

x?.value = 10
y?.value = 10


closure()
x = nil
y = nil
//Optional(10) Optional(10)

그리고 캡처리스트에 x, y값을 넣은 뒤에 nil 값을 할당해주면
deinit 이 출력되지 않는 것을 볼 수 있다

여기서 그럼 약한 참조로 해줘볼까???

약한획득 (weak capture list)의 경우
획득목록에서 획득하는 상수가 옵셔널 상수로 지정된다. 그 이유는 차후에 클로저 내부에서 약한획득한 상수를 사용하려고 할 때 이미 메모리에서 해제된 상태일 수 있기 때문이다. 해제된 후에 접근하려 하면 잘못된 접근으로 오류가 발생하므로 안전을 위해 약한획득은 기본적으로 타입을 옵셔널으로 사용하는 것이다.

class SimpleClass {
  var value: Int = 0
  
  deinit {
    print("deinitialized \(value)")
  }
}

var x: SimpleClass? = SimpleClass()
var y = SimpleClass()

let closure = { [weak x, unowned y] in
  print(x?.value, y.value)
}

x = nil
//deinitialized 0
y.value = 10

closure() // nil 10

획득목록에서 x를 약한참조로, y를 미소유참조하도록 지정 했다
의도한 대로 클로저 내부에서 사용하더라도 클로저는 x가 참조하는 인스턴스의 참조횟수를 증가시키지 않았다. 그렇게 되면 변수 x가 참조하는 인스턴스가 메모리에서 해제되어 클로저 내부에서도 더 이상 참조가 불가능한 것을 볼 수 있다.

y의 경우 미소유 참조를 했기 때문에 클로저가 참조 횟수를 증가시키지 않았지만, 만약 메모리에서 해제된 상태에서 사용하려 한다면 실행 중에 오류로 애플리케이션이 강제로 종료될 가능성이 있다!!



reference) 야곰의 스위프트 프로그래밍

좋은 웹페이지 즐겨찾기