-
Swift Language) ARC ( Automatic Reference Counting)Swift/Swift Language 2021. 10. 4. 20:47
Swift uses Automatic Reference Counting (ARC) to track and manage your app’s memory usage. In most cases, this means that memory management “just works” in Swift, and you don’t need to think about memory management yourself. ARC automatically frees up the memory used by class instances when those instances are no longer needed.
예전에 위 bold 처리된 부분을 읽고 (메모리 관리가 Swift 에서는 그냥 되기 때문에 신경쓰지 않아도 된다. ) 해당 chapter 를 읽지 않고 넘겼었다..
하지만, iOS 를 Swift 로 개발할 때 매우 많은 경우
- [weak self]
- self?.
- strong reference
- weak reference
- weak var delegate
등을 아주아주 많이 보게되는데, 모두 메모리 관리와 관련이 있고, 이 chapter 에서는 이에 대해 다룬다.
Swift 에서는 메모리를 관리하기 위해 ARC (Automatic Reference Counting ) 라는 것을 사용하는데, 이것 덕분에 메모리 관리에 대해 우리가 고려할 필요가 따로 없다. ARC 는 class instances 가 더이상 필요하지 않을 때 할당된 memory 를 자동으로 해제시켜준다.
그러나, 여러 경우에 ARC 가 제대로 작동하기 위해서는 코드 간 관계에 대한 정보가 필요하다. 이 chapter 에서는 ARC를 어떻게 memory 관리에 활용할 수 있는지에 관한 내용을 다룬다. (Swift 에서의 ARC 와 Objective-C 에서의 ARC 는 매우 유사함)
How ARC Works ( ARC 가 작동하는 방법 )
Class instance 를 하나하나 생성할 때마다 ARC 는 일정량의 메모리를 그 instance 에 대한 정보를 저장하기 위해 할당한다. 이 할당된 메모리에는 instance 의 type 과 함께 이 instance 와 관련된 stored properties 도 함께 저장된다. 해당 instance 가 더이상 필요하지 않을 때 ARC 는 할당되었던 메모리를 해제시켜주며 따라서 그 메모리는 다른 용도로도 사용할 수 있게 된다.
그러나, ARC 가 아직 사용중인 instance 의 메모리를 해제시키면, 그 instance 의 properties, methods 에 대해 더이상 접근할 수 없게게 된다. 만약 이미 해제된 instance 에 접근하게되면 crash 가 발생하는 것을 볼 수 있다. Instances 가 아직 필요한 경우 사라지는 것을 방지하기 위해 ARC 는 properties, constants 또는 variables 이 가리키는 class instance 의 수를 tracking 한다.
이 것을 가능하게 하기 위해서 property, constant, variable에 class instance 를 할당할 때마다
할당된 property, constant, variable 은 그 instance 에 대해 'strong reference' 를 생성한다.
이 reference 가 'strong' reference 라 불리는 것은 가리키는 instance 를 그만큼 확실하게 hold 하기 때문이다. Strong reference 가 남아있는 한 메모리를 임의로 해제시키지는 않는다.
ARC In Action
ARC 가 어떻게 작동하는지 더 자세히 알아보기 위해 예시를 살펴보겠습니다.
class Person { let name: String init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } }
var reference1: Person? var reference2: Person? var reference3: Person?
위에서 세 변수 reference1,2,3 은 Optional (Person?) 이기 때문에 자동으로 nil value 를 갖도록 initialize 되고 ,
따라서 Person class instance 를 가리키지 않는다.
reference1 = Person(name: "Jone Appleseed") // Jone Appleseed is being initialized
이제 reference1 에 Person class 의 값을 갖도록 해주면, 해당 값으로 intialize 되면서 print() 가 호출되고, reference 를 갖게된다.
Person instance 가 reference1 에 할당되었기 때문에 reference1 은 새로 생긴 Person instance 에 대해 strong reference 를 갖게 된다. ( reference1 -> Person ) . 최소 하나의 strong reference 가 존재하기 때문에 ARC 는 이 Person instance 가 memory 에 남아있게( 해제되지 않도록 ) 해준다.
reference2 = reference1 reference3 = reference1
만약 같은 Person instance 를 나머지 두 variables 에도 할당해주면, strong reference 는 두개 더 생겨나게 된다.
이제 세개의 strong reference 가 존재하고, 처음 할당된 reference1 을 포함한 다른 하나의 reference (2) 가 nil 을 할당받는다고 해도 여전히 하나의 strong reference (reference3 -> Person) 는 남아있기 때문에 메모리는 해지되지 않는다.
reference1 = nil reference2 = nil
ARC 는 마지막 남은 strong reference 가 없어질 때 까지, 즉 the Person instance 가 쓰이지 않는다는 것을 확신할 수 있을 때까지
해당 메모리를 해제시키지 않는다. (신기...)
reference3 = nil // Jone Appleseed is being deinitialized
Strong Reference Cycles Between Class Instances
위 예시에서 ARC 는 Person instance 의 references 갯수를 track 하는 방식을 사용하여, instance가 더이상 쓰이지 않을 때 메모리를 해제시킬 수 있었다. 그러나, class 의 instance 가 strong reference 를 0개 가지고 있는 순간이 없도록, 다시 말하면 instance 가 사라지지 않도록 코드가 작성될 수도 있다. 만약 두개의 class instance 가 서로에 대해 strong reference 를 갖게될 때, (서로 사라지지 않도록 유지시킬 때) 이러한 현상이 발생할 수 있고, 이것을 'strong reference cycle' 이라고 부른다.
strong reference cycle 을 해결하기 위해서 classes 사이의 관계를 weak 또는 unowned reference 로 대신 정의할 수 있다. 하지만 그 전에 strong reference cycle 을 해결하기 전에서 이 cycle 이 어떻게 발생되는지 알아보자.
아래는 어떻게 strong reference cycle 이 생기는지에 대한 예시이다. 여기서 두 classes (Person, Aparatment) 를 정의하고, 각각 apartment 와 residents 가 model 로 있다.
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? deinit { print("\(name) is being deinitialized") } } class Apartment { let unit: String init(unit: String) { self.unit = unit } var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
다음으로, john, unit4A variables 를 각각 Person?, Apartment? type 으로 선언했다.
var john: Person? var unit4A: Apartment?
그 후 각 변수에 Person, Apartment class 의 instance 를 strong reference 로 갖게 해주었다.
john = Person(name: "john Appleseed") unit4A = Apartment(unit: "4A")
이제 john 은 Person instance 에 대해, unit4A 는 Apartment instance 에 대해 strong reference 를 가진다.
john , unit4A 가 각 Person, Apartment instance 를 갖는 경우 위는 현재 상황을 나타내는 Diagram 이다. 이제는 두 instance 를 서로 연결시켜 줄 수 있다.
(john 은 apartment instance 를 Apartment? type 의 apartment 변수로 갖고,
unit4A 는 person instance 를 Person? type 의 tenant 변수로 갖도록 해줄 수 있다.)
john!.apartment = unit4A unit4A!.tenant = john
서로를 strong reference 로 갖게 되는 경우 두 instances 를 연결시키면 위와 같이 strong reference cycle 을 갖게된다.
이제 Person instance 는 Apartment instance 를, Apartment instance 는 Person instance 를 strong reference 로 갖는다.
따라서 이 경우 john, uni4A 에 연결된 strong references 를 없애도 reference counts 가 0이 되지 않기 때문에 instances 는 ARC 에 의해 해제되지 않는다.
john = nil unit4A = nil
이렇게 두 변수 john, unit4A 가 모두 nil 값을 다시 갖게되어도 어떠한 instance 의 deinitializer 도 호출되지 않는다.
Strong reference cycle 이 Person, Apartment instances 가 해제되는 것을 막기 때문에 이 경우 memory leak 이 발생한다.
john, unit4A 는 nil 값을 갖게 되었지만 두 class instances 가 서로를 strong reference 로 갖는 경우 두 instances 사이 strong reference 는 여전히 존재하고 references 는 깨질 수 없다.
Resolving Strong Reference Cycles Between Class Instances
Swift 는 class type 으로 작업할 때 생길 수 있는 strong reference 에 대하여
'weak reference, 'unowned reference' 두가지의 해결방법을 제공한다.
weak, unowned reference 는 reference cycle 중 하나의 instance 가 strong 하지 않게 다른 instance 를 refer 할 수 있도록 만들어준다. 이렇게 하면 strong reference cycle 을 생성하지 않으면서, 서로를 refer 할 수 있게 된다.
다른 instance 가 더 짧은 lifetime 을 갖는 경우 weak reference 를 사용하면 된다. (다른 instance 가 먼저 해제)
위 Apartment 예제에서는, apartment 가 tenent 를 갖지 않게 되는 상황이 (더 일찍) 오기때문에 weak reference 가 reference cycle 을 깰 수 있는 적당한 수단이 될 수 있다. (weak var tenent: Person?)
unowned reference 는 다른 instance 가 같은 lifetime 을 갖게 되거나, 자신보다 긴 lifetime 을 가질 때 사용하면 된다.
Weak References
'weak reference' 는 strong 하지 않은 reference 라서, ARC 가 reference 된 instance 를 해제하도록 둔다. 이런 방식으로 reference 가 strong reference cycle 의 일부가 되는 것을 방지한다. weak reference 라는 것을 나타내기 위해, 'weak' keyword 를 variable (or property)를 정의할 때 앞에 써주어야 한다.
'weak reference' 는 refer 하는 instance 를 strong 하게 잡고있지 않으므로, refer 하고있을 때에도 instance 의 메모리가 해제될 수 있다. 그렇기 때문에 ARC 는 refer 하고 있는 instance 의 메모리가 해제될 때 자동으로 weak reference 를 nil 로 설정한다. 또한 weak references 가 runtime 중에 nil 로 될 수 있어야 하기때문에 항상 optional type의 variable 로 선언되어야 한다.
다른 optional value 에서 값이 존재하는지 확인하기 위해 하는 방법들을 weak reference 되고있는 값에도 그대로 적용하면 된다. (guard let , if let 등 )
참고 : ARC 가 weak reference 값을 nil 로 설정할 때 property observer 는 호출되지 않는다.
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? deinit { print("\(name) is being deinitialized") } } class Apartment { let unit: String init(unit: String) { self.unit = unit } weak var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
위 예제는 Apartment class 에서 tenant 가 weak var 로 선언된 것 외에는 전에 있던 코드와 같다.
var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A") john!.apartment = unit4A unit4A!.tenant = john
reference 설정 code 또한 전과 같다.
apartment instance 는 person instance 를 weak refer, 반대의 경우 strong refer Person instance는 여전히 Apartment instance 에 대해 strong reference 를 가지지만,
Apartment instance 는 Person instance 에 대해 weak reference 를 갖는다.
다시 말하면, 만약 john variable 을 nil 로 설정하게 되면 Person instance 를 가리키는 strong reference 는 존재하지 않게 된다.
john = nil // John Appleseed is being deinitialized
Person instance 에 대한 strong instance 는 존재하지 않게 되기 때문에 해당 memory 는 해제되고, ARC 는 unit4A 의 tenant 값을 nil 로 설정한다.
The only remaining strong reference to the Apartment instance is from the unit4A variable. If you break that strong reference, there are no more strong references to the Apartment instance:
유일하게 남은 Apartment instance 에 대한 strong reference 는 unit4A 이 갖는다. 만약 이 strong reference 를 없애게 되면, 이 Apartment instance 에 대한 strong reference 는 존재하지 않게 된다.
print(unit4A?.tenant) // nil unit4A = nil // Apartment 4A is being deinitialized
Unowned References
Weak reference 와 마찬가지로 unowned reference 도 strong 하게 instance 를 가리키지 않는다. Weak reference 와 다른점은 refer 하고 있는 다른 instance 가 같거나 혹은 더 긴 lifetime 을 가질 때 사용한다는 것이다. 'unowned' keyword 를 property 나 variable 선언할 때 앞부분에 써주는 방식으로 사용한다.
Weak reference 와의 다른 차별점은 항상 값을 가져야 한다는 것이다. 결과적으로, 'unowned' 라고 mark 해놓으면 이건 optional 이 아니게되고, ARC 는 unowned reference 의 값을 nil 로 만들지 못한다.
Important
Instance 가 해제되지 않을 것들을 reference 가 가리킬 것이라고 확신할 수 있을 때만 unowned reference 를 써야한다.
그 instance 의 메모리가 해제된 후 unowned reference 의 값에 접근하면 runtime error 를 맞이하게 될것이다.다음 예제에서는 Customer, CreditCard 두 classes 에 대해 정의한다. (은행 고객과 그 고객이 가진 카드에 대한 모델이다.) 두 classes 는 서로에 대한 instance 를 property 에 저장하고, 이 관계는 strong reference cycle 이 될 가능성이 있다.
고객과 신용카드간의 관계는 위 예시 (아파트와 사람 사이) 에서의 관계와는 약간 다르다. 이 모델에서는 고객이 신용카드를 가지지 않을 수도 있지만, 신용카드는 항상 고객과 연관되어있다. 신용카드 instance 는 refer 하고 있는 Customer 보다 lifetime 이 짧을 수밖에 없다. Customer class 는 optional card property 를 갖고, CreditCard class 는 unowned customer property 를 갖도록 모델을 표현할 수 있다. 또한, 새로운 CreditCard instance 는 항상 number 값 과 customer instance 가 CreditCard initializer 에 주어져야만 생성될 수 있다. CreditCard instance 가 생성될 때 항상 customer instance 를 가진다는 것을 보장한다.
Credit card 는 항상 customer 를 가지므로, strong reference cycle 생성을 막기 위해 customer property 를 unowned reference 로 정의한다
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } class CreditCard { let number: UInt64 unowned let customer: Customer init(number: UInt64, customer: Customer) { self.number = number self.customer = customer } deinit { print("Card #\(number) is being deinitialized") } }
더보기NOTE ( ARC 와 무관하여 접음.)
32-bit, 64-bit system 에서 number property 의 capacity 가 16 자릿수 숫자를 저장할만큼 커야한다는 것을 보장하기 위해 CreditCard 의 number 는 Int 가 아닌 UInt64 로 선언되었다.아래는 optional Customer john 에 대해 정의하였다. (특정 customer의 reference 를 저장하기 위해 사용될 것이다.) 해당 값은 nil 을 초기값으로 가지고 있다.
var john: Customer?
이제 Customer instance 를 생성하고, CreditCard instance 를 만드는 데에 이용, 그 후 CreditCard 를 customer 의 card property 에 할당할 수 있다.
john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
이제 두 instances 를 연결하였고, 아래는 어떻게 references 가 생성되는지에 대한 도식이다.
Customer instance 는 CreditCard instance 에 대해 string reference 를 갖고, CreditCard instance 는 Customer instance 에 대해 unowned reference 를 갖는다.
Strong 이 아닌 Unowned customer reference 이기때문에, john 에게 있던 strong reference 를 끊게 되면 Customer instance 에 대한 strong reference 는 더이상 남아있지 않게 된다.
이제는 Customer instance 에 대한 strong references 가 모두 없어졌고, 따라서 메모리가 해제된다. 그에 따라 CreditCard instance 에 대한 strong references 도 사라지며, Card instance 의 메모리 역시 해제된다.
-
john = nil // John Appleseed is being deinitialized // Card #1234567890123456 is being deinitialized
위 code 에서 john 에 nil 이 할당되면서, Customer 와 CreditCard 의 deinitializer message 를 확인할 수 있다. (해제되었다.)
NOTE
위 예제에서는 safe unowned references 를 어떻게 사용하는지 보여주었다. Swift 는 성능 향상을 목적으로 runtime safety check 를 불활성화 시킬 때가 필요할 수 있으므로 unsafe unowned references 또한 제공한다. 이 경우 모든 unsafe operation 에 대한 code 의 safety check 는 본인이 해야한다.
Unsafe unowned reference 는 unowned(unsafe) 를 앞에 써준다. 만약 가리키고 이는 instance 의 메모리가 해제된 후 그 unsafe unowned reference 로 접근을 시도하면, 프로그램은 그 instance 가 있었던 메모리 위치에 접근을 시도할 것이다(unsafe operation) .Unowned Optional References
Class 에 대한 optional reference 를 unowned 로 만들 수 있다. ARC ownership model 에서, unowned optional reference 와 weak reference 는 같은 상황에 대해 사용될 수 있다. 차이점은 unowned optional reference 를 쓸 때에는 reference 가 항상 valid object (!= nil) 이어야 한다는 것에 본인의 책임이 따르고, valid 가 아닐 경우 nil 로 설정된다. 아래는 학교의 어떤 부서에서 제공하는 courses 에 대해 tracking 하는 예시이다.
Here’s an example that keeps track of the courses offered by a particular department at a school:
class Department { var name: String var courses: [Course] init(name: String) { self.name = name self.courses = [] } } class Course { var name: String unowned var department: Department unowned var nextCourse: Course? init(name: String, in department: Department) { self.name = name self.department = department self.nextCourse = nil } }
Department 는, department 에서 제공하는 각 course 에 대해 strong reference 를 갖는다. ARC ownership model 에서, a department 는 그 부서의 courses 를 소유(own) 한다. Course 는 두 unowned references 를 가지고있고, 하나는 제공된 department, 다른 하나는 학생이 수강해야하는 다른 course 에 대한 것이다; course 는 두 objects 중 어떠한 것도 소유하지 않는다.
각각의 course 는 어떤 부서의 일부이고, 따라서 department property 는 optional 이 아니다. 그러나, 어떤 courses 는 다음 course 를 가지고 있지 않을수도 있기 때문에 nextCourse property 는 optional 로 갖는다.
Here’s an example of using these classes:
let department = Department(name: "Horticulture") let intro = Course(name: "Survey of Plants", in: department) let intermediate = Course(name: "Growing Common Herbs", in: department) let advanced = Course(name: "Caring for Tropical Plants", in: department) intro.nextCourse = intermediate intermediate.nextCourse = advanced department.courses = [intro, intermediate, advanced]
(Horticulture: 원예학)
위 코드에서 department 와 그 department 의 세가지 courses 를 생성한다. intro 와 intermediate courses 는 모두 nextCourse property 에 값을 가지고있고, 다음 수강해야할 course 에 대해 unowned optional reference 를 갖는다.
Unowned optional reference 는 strong 하게 class 의 instance 를 붙잡고 있지 않기때문에, ARC 가 그 instance 의 메모리를 해제시키는 것을 막지 않는다. Unowned optional reference 가 nil 이 될 수 있다는 것 외에는 unowned reference 와 ARC 면에서 동일하게 작동한다.
Non-optional unowned references 와 같이, nextCourse 가 항상 deallocated 되지 않을 course 를 refer 하도록 만드는 것은 본인의 책임이다. 예를 들어 이 경우에는, department.courses 에서 course 를 지울 때 그 courses 를 가리키고 있는 다른 references 도 함께 없애주어야 한다.
NOTE
Optional value 의 type 은 Optional 이고, 이는 Swift standard library 에 있는 enumeration 이다. 그러나, optionals 는 'Value types 은 unowned 로 쓰일 수 없다' 는 rule 에서 예외다.
Class 를 감싸는 optional 은 reference counting 을 사용하지 않기 때문에, optional 에 대해 strong reference 를 가질 필요가 없다.Unowned References and Implicitly Unwrapped Optional Properties
위 두개의 weak, unowned references 예제에서 strong reference cycle 을 끊을 필요가 있는 자주 일어나는 경우에 대해 다루었다.
Person 과 Apartment 에서는 모두 nil 이 될 수 있고 strong reference cycle 을 만들 가능성이 있는 두 properties 에 대해 보여준다. 이러한 상황은 weak reference 를 통해 해결되는게 최선의 방법이다.
Customer 와 CreditCard 예시에서, 하나의 property 는 nil 이 될 수 있고 다른 하나는 될 수 없는 두개의 properties 는 strong reference cycle 을 만들 수도 있다. 이러한 상황은 unowned reference 를 통해 해결하는게 좋다.
그러나 두 properties 가 항상 value 를 갖고있어야 하는, 즉 어떤 것도 initialization 이후 nil 이 되면 안되는 상황도 있다. 이러한 상황에서는 하나의 class 에 unowned property , 다른 하나는 implicitly unwrapped optional property 를 조합해서 사용하면 유용하다.
이렇게 하면 reference cycle 를 만들지 않으면서 initialization 이 끝난 후부터 서로 직접적으로 (optional unwrapping 없이) 접근할 수 있다.
아래 예제에서는 Country 와 City 두 classes 를 정의한다. 각각은 instance 에 서로의 class 를 property 에 저장한다. Data model 에서 모든 country 는 반드시 항상 capital city 를 가지고, 모든 city 는 반드시 항상 country 에 소속되어있다. Country class 에는 capitalCity property가, City class 에는 country property 가 이것을 나타낸다.
class Country { let name: String var capitalCity: City! init(name: String, capitalName: String) { self.name = name self.capitalCity = City(name: capitalName, country: self) } } class City { let name: String unowned let country: Country init(name: String, country: Country) { self.name = name self.country = country } }
두 classes 간 상호의존성을 만들기 위해서 City 의 initializer 는 Country instance 를 받고, 그 instance 를 country property 에 저장한다. City 의 initializer 는 Country initializer 의 내부에서 일어난다. 그러나, Country 의 initializer 는 새로운 Country instance 가 완전히 initialized 될 때까지 self 를 City initializer 에 전달할 수 없다. ( Two-Phase Initialization.) 이러한 requirement 를 처리하기 위해 Country 의 capitalCity property 을 implicitly unwrapped optional property 로 정의한다 (City!). 이는 capitalCity property 가 다른 optional 처럼 nil 을 기본값으로 가지고 있지만, unwrap 할 필요 없이 그 값에 접근하다는 것을 의미한다. capitalCity 의 기본값으로 nil 을 가지기 때문에, 새로운 Country insatnce 는 initializer 에서 name property 가 설정되는 순간 완전히 initialized 되었다고 처리된다. 이는 name property 가 정해지자마자 Country initializer 가 implicit self property 를 refer 할 수 있고 넘겨줄 수 있음을 의미한다. 따라서 Country initializer 가 자신의 capitalCity property 를 설정할 때, Country initializer 는 self 를 City initializer 의 parameter 로 줄 수 있다. 결론은. Country 와 City instances 를 strong reference cycle 없이 한번에 만들 수 있다는 것이고, capitalCity property 는 ! 을 써줄 필요 없이 직접적으로 접근 가능하다.
var country = Country(name: "Canada", capitalName: "Ottawa") print("\(country.name)'s capital city is called \(country.capitalCity.name)") // Prints "Canada's capital city is called Ottawa"
위 예시에서, implicitly unwrapped optional 은 모든 two-phase class initializer requirements 가 만족됨을 의미한다. Strong reference cycle 을 만들지 않음과 동시에 capitalCity property 는 initialization 이 끝나면 non-optional value 처럼 접근 및 사용될 수 있다.
Strong Reference cycles for Closures
위에서 두 class instance properties 가 서로에 대해 strong reference 를 가질 때 strong reference cycle 이 어떻게 생기는지, 그리고 strong reference cycles 를 깨기 위해 weak, unowned references 를 어떻게 활용하는지 서술하였습니다. (unowned..)
String reference cycle 은 이 경우 외에도 closure 를 class instance 에 assign 한 후에 closure body 가 해당 instance 를 capture 할 때에도 생길 수 있습니다. 이러한 capture 은 closure 의 body 가 instance 의 property에 접근할 때 또는 instance 의 method 를 호출할 때 생길 수 있습니다. (self.someProperty, self.someMethod() ) 각 경우에 대해, 이러한 접근은 closure 가 self 를 capture 하도록 하고, 결국 strong reference cycle 을 생성합니다.
이러한 strong reference 는 closures 가 classes 처럼 reference type 이기 때문에 생겨날 수 있어요. Clousre 를 perperty 에 할당하는 순간 해당 closure 의 reference 를 할당하게 되는거랍니다. 결국, 아까 경우와 같은 문제가 발생하게 됩니다(두 strong references 가 서로를 꽉 붙잡아서 없어지지 않도록 하는.. ) . 하지만 이번에는 두개의 class Instances 가 아니라, class instance 와 closure 가 서로를 유지시키게 됩니다.
Swift 는 이 문제에 대해 closure capture list 라는 solution 을 제공합니다. 그렇지만 이 solution에 대해 알아보기 전에 cycle 이 어떻게 생기는지 먼저 알아보아요.
아래 예제에서 closure(self 를 refer 하는) 를 사용할 때 strong reference cycle 를 어떻게 생성하는지 보여주고있어요.
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { print("\(name) is being deinitialized") } }
asHTML 이라는 () -> String 타입의 closure 가 self 값을 사용하고 있네요. name 은 HTML tag 이름이네요. p, h1 등과 같이요.
name 이 "p", text 가 "hi" 인 경우를 예로 들면 <p>hi</p> 가 되겠네요. 만약 text 가 주어지지 않으면 <p /> 가 되겠어요.
asHTML propert 에는 String 을 return 하는 closure 가 할당되었어요. text 값이 존재할 때 이를 포함하고, 그렇지 않을 때는 포함하지 않는 값을 반환합니다. asHTML property 가 instance method 처럼 이름지어지고 사용되었어요. 그러나, asHTML 은 instance method 가 아닌 closure 이기 때문에 customize 해서 asHTML 의 기본 값을 바꾸어줄 수 있어요. 예를 들어 text 가 nil 일 때 다른 어떤 기본 값을 갖는 closure 로 설정해줄 수도 있습니다.
let heading = HTMLElement(name: "h1") let defaultText = "Some default text" heading.asHTML = { return "<\(heading.name)>\(heading.text ?? defaultText) </\(heading.name)>" } print(heading.asHTML()) // <h1>Some default text </h1>
MEMO
asHTML property 는 lazy 로 선언되었어요. 왜냐하면 이건 필요할 때만 호출될 필요가 있기 때문이에요. asHTML 이 lazy 로 선언되었기 때문에, self 를 closure 내에서 refer 할 수 있어요. initialization 이 끝나고 , self 가 존재하기 시작한 후에야 접근 할 수 있으니까요!HTMLElement class 는 name( 과 text, optional) 을 필요로 하는 하나의 initializer 를 가져요. 이 class는 언제 instance 의 메모리가 해제되는지 보여주기 위한 deinitializer 도 하나 갖습니다.
HTMLElement class instance 는 다음과 같이 만들고 출력할 수 있습니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML()) //<p>hello, world</p>
HTMLElement class 는 위에 서술한 바와 같이 instance 와 closure 사이 strong reference cycle 을 생성해요.
1. instance 의 asHTML property 가 closure 를 strong reference 로 가짐.
2. closure 가 self 를 capture ( self 를 strong reference 로 가짐)
-> strong reference cycle 생성.
3. var paragraph 가 HTMLElement instance 를 strong reference 로 가짐.
MEMO
closure 는 self 를 여러번 참조하지만, HTMLElemnt instance 에 대한 하나의 strong reference 만 갖습니다.paragraph variable 을 nil 값을 주어서 HTMLElement instance 에 대한 strong reference 를 끊어도, instance와 closure 모두 그대로 메모리가 할당된 채로 있습니다 (strong reference cycle 때문에..)
paragraph = nil // deinitialize 되지 않기 때문에 메시지가 출력되지 않아요.
Resolving Strong Reference Cycles for Closures
Closure 와 class instance 사이 strong reference cycle 은 closure 를 선언하면서 capture list 를 만들어주면 해결할 수 있습니다.
Capture list 는 closure body 안에서 하나 또는 다중의 reference type 을 capture 할 때 어떻게 해야하는지를 정해줘요. 두 class instances 간 strong reference cycle 에서 상황처럼 strong reference cycle 을 만들지 않으려면 각 captured reference 를 weak, 또는 unowned reference 로 선언하시면 됩니다. Weak , unowned 중 어느 것을 사용할 지 결정하는 것은 code 에서의 상황에 따라 달라집니다.
Defining a Capture List
Each item in a capture list is a pairing of the weak or unowned keyword with a reference to a class instance (such as self) or a variable initialized with some value (such as delegate = self.delegate). These pairings are written within a pair of square braces, separated by commas.
Place the capture list before a closure’s parameter list and return type if they’re provided:
capture list 내 각각의 item 은 class instance (self) 또는 어떤 값으로 할당된 variable ( delegate = self.delegate)
에 대한 reference와 함께 weak, 또는 unowned keyword 와 딱을 이뤄요.
이러한 pairings 은 [ ] 안에서 comma (,) 로 구분지어 써주시면 됩니다.
Parameter list와 return type 을 써주시기 이전에 명시해주세요 ~
lazy var someClosure = { [unowned self, weak delegate = self.delegate] (index: Int, stringToProcess: String) -> String in // closure body goes here }
Closure 내 parameter list 나 return type 을 지정해주지 않을 때에는 (Swift 가 문맥상 알아서 추론 할 수 있을 때) closure 의 가장 앞에 써주세요~
lazy var someClosure = { [unowned self, weak delegate = self.delegate] in // closure body goes here }
내 메모 : Closure 내에 self 를 weak reference ([weak self]) 로 선언하면, 해당 class 의 메모리의 해제를 막지 않을 수 있다. (self 를 약하게 holding 하고있으므로 언제든 떠날 준비가 되있는 것으로 생각하면 될 것)
Weak and Unowned References
closure 와 instance(closure 가 capture 하는) 가 항상 서로를 refer 할 때에는
closure 안에 capture 를 unowned reference 로 선언해주시면 항상 둘이 함께 메모리가 해제됩니다.
거꾸로,captured reference 가 언젠가 nil 로 변할 수 있다면 capture 를 weak reference 로 선언해주세요.
Weak references 는 항상 optional type 이기 때문에, closure 가 refer하는 instance 의 메모리가 해제될 때, 자동으로 nil 값을 갖게됩니다. (이걸 이용해서 closure body 에서 nil 인지 아닌지 확인할 수 있어요.)
(( self 를 refer 하는 closure 를 갖는 class 의 instance 가 optional 일 때 weak reference 를 사용하면 되겠네요
captured reference (많은 경우 self..) 는 class 의 instance 일테니까요 ))
MEMO
만약 captured reference 가 절대 nil 이 되지 않는 경우라면 (optional 이 아닌 경우)
반드시 unowned reference 로 !HTMLElement 에서의 strong reference cycle 을 해결하는 적당한 방법은 unowned referece 일거에요 .
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { [unowned self] in if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { print("\(name) is being deinitialized") } }
전에 쓰던 코드와 다른 것은 "[unowned self]" 이고, strong reference cycle 을 막기 위해 생겼어요.
이 code 가 의미하는 바는 self 를 unwoned reference 로 capture 입니다. (strong reference 대신.. )
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML()) // Prints "<p>hello, world</p>"
선언하고 사용하는 방법은 [unowned self] 가 있기 전과 같아요
This time, the capture of self by the closure is an unowned reference, and doesn’t keep a strong hold on the HTMLElement instance it has captured. If you set the strong reference from the paragraph variable to nil, the HTMLElement instance is deallocated, as can be seen from the printing of its deinitializer message in the example below:
이번에는 self 에 대한 capture 가 unowned reference 이기 때문에, strong 하게 instance 를 capture 하지 않습니다.
만약 paragraph variable 을 nil 로 설정하게 된다면 HTMLElement instance 메모리는 해제되고, print 문의 출력을 통해 확인해볼 수 있어요.
paragraph = nil // Prints "p is being deinitialized"
출처: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
'Swift > Swift Language' 카테고리의 다른 글
Structures And Classes (Swift) (0) 2021.11.01 Swift Language ) Closure (0) 2021.10.06 Swift Language ) Generics (0) 2021.09.23 Swift Language ) Access Control (0) 2021.09.16 Swift Language ) Protocol (0) 2021.09.16