Swift Language Guide: ARC (Automatic Reference Counting)
Automatic Reference Counting
서문
- Swift는 APP의 메모리 usage를 관리하고 추적하기 위해 ARC를 사용한다
- 그래서 대부분의 경우, 메모리는 알아서 관리되므로 고려하지 않아도 된다
- ARC는 Class 인스턴스가 더이상 필요없다고 판단될때 자동으로 메모리를 해제한다
- 하지만 일부 케이스에선, ARC가 코드들 간의 관계에 대한 정보를 요구하기도 한다
- 이 예외 케이스에 대해 구체적으로 알아보고
- ARC가 어떻게 메모리를 자동으로 관리하는지 살펴보자
- 참고로, Reference Count 라는건 참조타입인 Class의 인스턴스에만 해당된다
동작 이해
-
Class의 인스턴스를 생성하면 ARC가 메모리를 할당하게 된다
-
이 메모리에는 인스턴스의 타입이나 저장 프로퍼티의 값들이 저장된다
-
이후에 인스턴스가 더이상 필요없다고 판단되면, 메모리 자원 활용을 위해 해당 인스턴스에 잡혀있던 메모리를 해제한다
-
usage가 남아있는 인스턴스의 메모리를 해제해버리면 APP이 Crash나므로 인스턴스가 필요없는게 맞는지 추적해야 한다
-
각 인스턴스가 현재 얼마나 많은 프로퍼티 + 상.변수에게 참조되고 있는지를 Count 하는 방식으로 추적한다
-
해당 인스턴스에 대해 Active Reference가 1개라도 있다면 해제하지 않는다
-
구체적으론, 인스턴스를 어떤 프로퍼티+상.변수로 참조시킬때마다
강한 참조
라는게 만들어진다 -
"강한"이란 표현을 쓴 이유는 약한참조도 존재한다는 뜻이며 (추후설명) 강한 참조가 남아있다면 메모리는 해제되지 않는다
동작 예시
- 인스턴스를 1개 만들어 3개의 변수가 참조하도록 만들었다
- 3개의 강한 참조가 존재하는 것이며 3개 모두 nil로 참조를 해제한 시점에서야 비로소 인스턴스가 해제되었다
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 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
reference2 = reference1
reference3 = reference1
reference1 = nil
reference2 = nil
reference3 = nil
// Prints "John Appleseed is being deinitialized"
강한 참조 순환 @Class 인스턴스 간
- 강한 참조가 절대 사라질 수 없는 코드가 존재할 수 있다
- 두 Class 인스턴스가 서로를 강한 참조하는 경우이다. 이를 강한 참조 순환이라고 한다
-
이를 해결하는 방법으로, 일부 Class간 관계를 weak 참조 혹은 unowned 참조로 정의할 수 있다
-
약한 참조를 배우기 전에 강한 참조 순환이 어떻게 발생하는지 살펴보자
-
예제
#1
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") } } var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A") john!.apartment = unit4A unit4A!.tenant = john //참고로, 내부 프로퍼티에 접근하려면 Unwrapping 해야한다
-
두 Class 인스턴스가 서로를 참조하고 있다
#2
john = nil unit4A = nil
-
john과 unit4A의 참조를 끊어도 인스턴스간 연결은 끊어지지 않아 메모리가 해제되지 않고 소멸자는 호출되지 않는다
-
강한 참조 순환 해결방법 @Class 인스턴스 간
-
개념
-
2가지 방법을 제공한다.
weak
와unowned
-
두 keyword는 인스턴스 간 참조하여도 강하게 hold하지 않고 강한 참조 Cycle을 형성하지 않는다
-
weak
: 참조하려는 인스턴스가 먼저 해제될 때Ex) 위의 코드에서 Apartment를 살펴보면 자기 자신보다 거주자에 해당하는 Person이 먼저 해제될 것 같으니 weak를 사용할 수 있다
-
unowned
: 참조하려는 인스턴스가 더 나중에 혹은 동시에 해제될 때
-
-
weak
- weak로 참조된 것은 연결을 끊지 않아도 ARC에 의해 해제될 수 있다
- 반드시 Optional 타입의 변수(variable)로 선언해야 한다. ARC가 nil로 값을 변경하여 해제하기 때문
- 그리고 사용할 때는 nil 체크를 동반하면 된다
💡 참고로, ARC에 의한 값변경(→ nil)은 Property Observer를 호출하진 않는다
- 예제 #1
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") } } var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A") john!.apartment = unit4A unit4A!.tenant = john
-
Person 인스턴스를 살펴보자.
-
이전 예시와는 달리 강한 참조는 john이 유일하다
#2
john = nil // Prints "John Appleseed is being deinitialized"
-
유일한 강한 참조인 john을 해제하면 Person 인스턴스를 강한 참조하는 것이 존재하지 않게 되고 Person 인스턴스도 ARC에 의해 해제된다
-
참조하던 인스턴스가 해제되면 자동으로 weak 참조는 nil로 설정된다
#3
unit4A = nil // Prints "Apartment 4A is being deinitialized"
-
unowned
-
weak와 마찬가지로 ARC의 자동 메모리 해제를 막지 않는다
-
하지만 unowned는 weak와 반대로, 참조 대상이 동시에 or 더 늦게 해제될 것 같을 때 사용된다 (=값이 있음을 확신할 수 있을 때)
-
또한, weak와는 달리 참조 대상이 해제되어도 자동으로 nil이 되지 않는다
-
그러므로 unowned는 항상 값이 있다고 가정되므로 Optional 타입일 필요가 없고 상수로도 사용가능하다
-
예제 - 두 인스턴스가 동시에 해제되는
#1
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") } }
-
Customer는 Card가 없을 수 있지만 / Card는 customer가 반드시 있다
= 적어도 Customer가 혼자 먼저 해제되는 경우는 없다
= 동시에 해제되거나 Card가 먼저 해제될 것이다
= Customer를
unowned
로 선언
-
이를 위해, CreditCard의 생성자에 customer를 반드시 받도록 정의하였다
#2
var john: Customer? john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
#3
john = nil // Prints "John Appleseed is being deinitialized" // Prints "Card #1234567890123456 is being deinitialized"
-
john의 강한 참조를 끊으면 Customer 인스턴스에 대한 어떤 강한 참조도 없으므로 Customer 인스턴스가 해제된다
-
동시에, CreditCard 인스턴스에 대한 강한 참조도 사라져 해제된다
-
- Optional
unowned
-
unowned
를 Optional 타입으로 선언하면 사실상weak
와 같은 문맥에서 사용된다 -
Optional 타입이므로 nil check가 동반된다
-
예제
#1
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 } } 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]
- Course는 없을 수 있어도 Department는 반드시 존재해야 한다
- 연계 Course가 아닐 수 있으므로 nextCourse는 없을 수 있어 Optional로 선언
- Department → Course로의 강한 참조만 해제하면 인스턴스가 해제될 수 있다
- 암시적 추출이 필요한 경우가 있다
아래 코드와 같이 생성자 내에서 다른 Class의 생성자를 호출하는 경우이다
capitalCity가 만약 Optional이 아니라면?
City 생성자를 호출할 때 self를 전달하는데 저번에 배웠듯이 fully initialized되기 전에는 self를 사용할 수 없어서 호출이 안된다
✅ Point
Optional로 정의하면 자동으로 nil 초기화되므로 name만 값을 설정해주면 fully initialized 되어 self를 사용할 수 있게 된다
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
}
}
이렇게 서로 참조하는 프로퍼티를 가진 관계의 Class가 생성자 내에서 인스턴스를 생성하려는 경우
self를 사용하기 위해 암시적 추출 Optional을 활용할 수 있다
Strong Reference Cycles for Closures
이전까지는 여러 Class가 서로의 인스턴스를 프로퍼티로 가지는 경우였다
순환참조가 발생하는 또 다른 경우를 살펴보자
바로, self를 참조하는 Closure를 프로퍼티로 가졌을 때이다
이 상황을 Closure가 self를 capture
했다고 표현한다
코드를 먼저 보자.
class HTMLElement {
let name: String
let text: String?
//asHTML 변수는 self를 참조하는 Closure 프로퍼티이다
//참고로 lazy 선언이 없으면 Two-Phase rule에 위배되어
// self를 참조하는 default를 설정할 수 없다
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")
}
}
✅ Point
Closure가 순환참조를 유발할 수 있는 이유는 Closure 역시 참조타입이기 때문이다
본체가 어딘가에 따로 있고 asHTML이 그것을 강한참조하고 있는 것이다
Closure 역시 내부에서 self를 강한참조하고 있으므로 순환참조가 발생한다
❗ 주의
Closure가 self를 여러 번 참조하고 있지만, 1개의 강한참조만 존재한다
Resolving Strong Reference Cycles for Closures
인스턴스와 Closure간 순환참조는 어떻게 해결할까? weak나 unowned??
이 경우엔 다른 해결책이 제시된다.
그것은 바로.. **Closure Capture List
!**
이것 또한 참조를 약하게 만드는 방식 중 하나이다
❗ 주의
Swift는 Closure 내에서 member를 참조할때 self를 붙일 것을 요구한다
Closure가 실수로 self를 capture할 수 있음을 인지하는데 도움을 준다
Defining a Capture List
참조를 약하게 만들고싶은 것들을 weak나 unowned 키워드와 쌍으로 명시해준다
예시를 살펴보자
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
Weak and Unowned References
우선 unowned
를 사용해야 하는 경우는
Instance가 항상 존재한다고 생각할 때이다
이 경우 Instance와 Closure는 운명 공동체이므로 동시에 해제될 것이다
반대로 weak
를 사용해야 하는 경우는
참조하는 Instance가 (미래의 어느 시점?) nil이 될 수 있음을 알고 있다
그래서 weak로 선언하면 Optional 타입으로 접근해야 한다
또한, Instance가 해제되면 자동으로 해당 참조는 nil이 된다
✅ Point
Capture하려는게 사용시점에 nil이 될 수 없다면unowned
를 쓰고
nil이 될 수 있다면weak
를 써라
위 예제에서 참조순환을 풀어보자
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
를 선택했을까?
아직 명확한 정답이 떠오르진 않지만 생각을 말해보면
Closure가 self의 프로퍼티인데 자기자신(self)을 해제하고 사용되는 경우가 존재할까?
Author And Source
이 문제에 관하여(Swift Language Guide: ARC (Automatic Reference Counting)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@yohanblessyou/ARC-Automatic-Reference-Counting저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)