+
연산자 활용 병합 가능 App State가 바뀔 때 UIKit은 적절한 delegate object의 method를 호출하게 된다.
iOS 12 이전, 하나의 앱이 하나의 process와 하나의 UI 객체로 매칭되었던 시절에는 Process LifeCycle과 UI LifeCycle을 모두 AppDelegate에서 처리했다.
iOS 13 이후, 생태계의 변화에 따른 개발 환경의 변화로 멀티윈도우 작업이 가능하게 되면서, 결과적으로 앱들이 하나의 process를 사용하긴 하지만 다수의 UI, scene과 연결된 scene delegate, scene session들을 동시에 활성화할 수 있게 되었다.
Scene Session은 앱에서 생성한 모든 scene의 정보를 관리한다. 그리고 Scene을 감시하여 생성/삭제에 대해서 AppDelegate에게 정보를 전달한다.
앱의 주요 생명 주기 이벤트를 관리 역할이 SceneDelegate로 위임되면서 iOS13부터 AppDelegate가 하는 일은 다음과 같다.
1. 사용자가 앱을 실행합니다: Not Running → In-Active → Active
application(_:didFinishLaunchingWithOptions:)
앱이 실행되고 앱을 화면에 띄우기 위한 모든 설정이 완료된 뒤, 실제로 화면에 나타나기 직전에 호출.
⬇️
앱 실행 후 UIKit에 Scene 연결 과정을 밟음
application(_:configurationForConnecting:options:)
새로운 Scene을 만들고 UIKit과 연결하기 위한 configuration을 지정.
해당 메서드는 앱 시작시 호출되지 않고, 새로운 Scene 혹은 새 window을 가져야 하는 경우에만 호출.
⬇️
scene(:willConnectTo:options:)
Scene이 연결될 것임을 delegate에 알림.
기존에 application(:didFinishLaunchingWithOptions:)에서 했던 UIWindow 생성 작업을 해당 method에서 수행 가능.
⬇️
sceneDidBecomeActive(_:)
앱이 Inactive에서 Active 상태로 전환되었을 때 호출(화면에 나타남).
2. 앱 실행 도중 홈으로 나갑니다: Active → In-Active → Background
sceneWillResignActive(_:) 앱이 Active에서 Inactive 상태로 전환될 때 호출.
⬇️
sceneDidEnterBackground(_:) 앱이 Background 상태로 전환되었을 때 호출.
2-1. 앱을 다시 켭니다: Background → Active
sceneWillEnterForeground(_:) 앱이 Background에서 Inactive 상태로 전환될 때 호출.
⬇️
sceneDidBecomeActive(_:) 앱이 Inactive에서 Active 상태로 전환될 때 호출.
2-2. 앱을 종료합니다 : Background or Suspended → Not Running
iOS 12까지는 multi window를 지원하지 않았기 때문에 멀티 태스킹 창에서 Swipe-up을 통해 앱을 종료. 하지만 iOS 13부터 multi window를 지원하고 만약 앱이 둘 이상의 Scene Window를 갖는다면 Swipe-up은 앱을 종료시키는 것이 아니라 Scene을 해제. 그렇게 Scene이 모두 해제 되면 앱이 종료!
sceneDidDisconnected(_:) UIKit에 연결된 Scene의 연결 해제를 delegate에 요청.
⬇️
application(_:didDiscardSceneSessions:) 사용자가 멀티태스킹 화면에서 한개 이상의 Scene을 종료시켰을 때 호출.
⬇️
applicationWillTerminate(_:) 앱이 사용자에 의해 종료될 때 호출(시스템에 의한 예기치 못한 종료시에는 호출되지 않음).
메모리 영역 중 Heap은 참조형 타입인 클래스, 클로저 등을 저장한다. ARC(Automatic Reference Counting)는 앱의 메모리 사용을 추적하고 관리한다. 참조형 타입에 국한되어 사용되며, 다른 언어에서의 GC과 유사하지만 확실히 다르다.
GC: 프로그램 실행 중 런타임 시점에서 Mark and Sweep 프로세스를 통하여 각 객체의 노드를 표시하고 순회하면서 동적으로 감시하고 있다가, 더 이상 사용할 필요가 없다고 여겨지는 것을 메모리에서 삭제한다. 루트로부터 닿을 수 있는(rechable) 노드들을 살려놓고 나머지 객체는 메모리에서 해제한다.
ARC: 프로그램 실행 중 컴파일 시점에서 참조/해제가 결정되어
런타임 때는 실행만 된다. 각 객체의 RC(참조 카운트)값을 기준으로 메모리 해제 여부를 판단한다. 참조하고 있는 변수, 상수 등이 없는 경우 해제하게 된다.
두 가지 class의 선언으로 인원과 주거지의 관계를 나타내보자. 두 클래스를 기반으로 인스턴스를 선언하고 프로퍼티를 연결한 후에 nil 값을 주었지만 deinit이 실행되지 않는다.
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
john = nil
unit4A = nil
순환참조는 두 개의 class 객체가 strong reference를 유지하며 서로를 참조하는 활성 상태를 계속 유지하는 경우에 발생한다. 위와 같이 변수와 인스턴스 간의 연결이 끊겨도 여전히 참조가 남게 되므로 메모리에서 해제되지 못해 메모리 누수가 발생하게 된다.
Swift에서는 두 가지 방법을 제시한다.
- weak reference: Optional, 대상이 되는 인스턴스의 수명이 짧을 때 사용
- unowned reference: non-Optional, 대상이 되는 인스턴스의 수명이 비교적 길 때 사용
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
john = nil
unit4A = nil
weak reference는 참조하는 인스턴스를 강하게 참조하지 않는다. 따라서, weak reference가 참조하는 동안에는 해당 인스턴스는 메모리로부터 할당 해제가 가능하다.
ARC는 바라보고 있던 인스턴스가 할당 해제되면 weak reference를 자동으로 nil로 설정하며 메모리에서 해제한다. 그렇기에 선언에 있어서도 nil로 변경할 수 있도록 항상 상수(let)이 아닌 변수(var)로 선언한다.
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")
}
}
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
john = nil
unowned reference 는 weak reference 와 같이 참조하는 인스턴스를 강하게 참조하지 않는다. unowned reference 의 경우에는 항상 값이 있다고 생각한다. 따라서 선언을 non-Optional 타입으로 상수/변수를 선언하며, ARC는 unowned reference를 nil로 재설정하지 않는다.
즉, unowned reference는 절대로 할당 해제가 되지 않을 인스턴스를 참조하는 경우에만 사용하며, 만약 unowned reference로 선언된 할당이 해제된 인스턴스를 참조하려고 한다면 runtime error가 발생하게 된다.
위의 예시를 보면 고객은 credit card를 가지고 있거나 그렇지 않을 수는 있지만 credit card는 항상 고객과 연결이 되어있어야만 한다. 따라서, Customer 클래스의 card 프로퍼티는 optional 타입으로 선언되고, 반면에 CreditCard 클래스의 customer 프로퍼티는 non-Optional 타입에 unowned reference 타입으로 선언이 되어있다.
CreditCard 인스턴스는 number와 customer의 정보를 init시 넘겨주어야만 생성이 된다. 따라서, CreditCard 인스턴스가 생성될 때 항상 customer 인스턴스를 참조하기 때문에 순환참조를 방지하기 위해서 unowned reference를 사용한다.
Swift5 이상부터는 unowned reference의 경우에도 optional 타입이 될 수 있다. 즉, weak reference 와 unowned reference는 이제 같은 맥락에서 사용이 가능해졌다.
사용에 있어서 경계가 구분이 잘 되지 않겠지만, weak reference의 경우에는 이전과 동일하게 unowned optional reference는 이제 항상 값이 있는 유효한 객체를 참조하거나(let으로 선언된 상수의 경우도 마찬가지다), nil로 설정이 되어있는 경우에 사용을 하면 된다.
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
}
deinit {
print("Course #\(name) is being deinitialized")
}
}
var department: Department?
department = Department(name: "Computer Science Engineering")
var intro: Course?
var intermediate: Course?
var advanced: Course?
intro = Course(name: "Welcome to C language", in: department!)
intermediate = Course(name: "Welcome to C++ language", in: department!)
advanced = Course(name: "Welcome to Swift", in: department!)
intro!.nextCourse = intermediate
intermediate!.nextCourse = advanced
department!.courses = [intro!, intermediate!, advanced!]
department!.courses = [intro!, intermediate!]
advanced = nil
Department(학과)는 course(강의)와 strong reference 를 유지한다. Course(강좌)의 경우에는 2개의 unowned reference 를 가지는데, 하나는 department(학과)이며 다른 하나는 학생이 다음으로 수강해야 할 nextCourse(다음강좌)이다. department 프로퍼티는 non-optional 타입이지만 어떠한 강좌(course)들은 다음으로 수강해야할 강좌가 없을 수도 있기 때문에 optional 타입을 가지게 된다.
unowned optional reference 는 nil이 될 수 있다는 점을 제외하고는 ARC의 측면에서는 unowned reference와 동일하게 동작한다. 따라서, unowned reference와 마찬가지로 unowned optional reference는 nextCourse가 항상 메모리에서 할당 해제된 것을 참조하지 않도록 관리를 해야하며 위의 예시에서는 만약 department(컴퓨터공학과)에서 course(강좌)를 삭제하는 경우에 삭제되는 강좌를 참조하는 관계를 모두 제거해 주어야한다.
먼저 unowned 는 weak와 다르게 let으로 선언될 수 있다.
weak의 경우에는 runtime동안에 ARC에 의해서 nil값으로 변경이 되기 때문에 변수로만 선언이 가능했다면 unowned는 옵셔널이 가능한 타입과 그렇지 않은 타입으로 선언이 가능하다. 옵셔널이 불가능한 타입의 경우에는 let으로 선언이 가능하며, 옵셔널이 가능한 타입은 weak 타입과 마찬가지로 변수로만 선언이 가능하다.
weak의 경우에는 ARC가 계속해서 인스턴스를 추적하다가 객체가 사라질 때 nil로 값을 변경하게되는데 이 과정은 오버헤드라고 할 수 있기 때문에 unowned optional을 사용하면 이러한 오버헤드를 줄일 수 있다.
unowned optional을 주로 사용하도록 하자