# TIL 21.1.11

simoniful·2022년 1월 11일
0

Swift

목록 보기
8/9

Collection Type

  • Array: 순서가 있는 값의 모음
    • 동일한 타입에 대해선 + 연산자 활용 병합 가능
    • .enumerated() 활용하여 인덱스 파악 가능
  • Set: 순서가 없는 고유값의 모음
    • Set은 Hashable한 것만 가능
    • .sorted() 활용하여 array로 순서 있게 반환 가능
    • .intersection(:), .symmetricDifference(:), .union(:), .subtracting(:)을 통해 두 집합간 원소 비교 가능
    • .isSubset(of:), .isSuperset(of:), isDisjoint(with:)을 통해 두 집합간 관계 비교 가능
  • Dictionary: 순서가 없는 Key: Value의 모음
    • identifier(Key)에 의거한 Hash한 검색 활용
    • Dictionary의 Key는 Hashable한 것만 가능
    • Dictionary의 value 값에 nil을 할당하여 쌍을 제거 가능
    • .updateValue(_:forKey:), .removeValue(forKey:)를 통해 수정과 더불어 수정 이전의 값 반환 가능

App LifeCycle

App State

  • foreground: 사용자의 높은 집중도, CPU를 포함한 system resource에서 우선순위
  • background: 화면 밖의 일

App State가 바뀔 때 UIKit은 적절한 delegate object의 method를 호출하게 된다.

  • iOS 13 이상은 UISceneDelegate를 사용하여 scene-based app에서 발생하는 life-cycle event를 제어할 수 있다.
  • iOS 12 이하는 life-cycle event에 응답하기 위해서 UIApplicationDelegate 개체를 사용하면 된다.

AppDelegate와 SceneDelegate

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. 앱의 가장 중요한 데이터 구조를 초기화하는 것
  2. 앱의 scene을 환경설정(Configuration)하는 것
  3. 앱 밖에서 발생한 알림(배터리 부족, 다운로드 완료 등)에 대응하는 것
  4. 특정한 scenes, views, view controllers에 한정되지 않고 앱 자체를 타겟하는 이벤트에 대응하는 것(앱 시작, 종료)
  5. 애플 푸쉬 알림 서비스와 같이 실행시 요구되는 모든 서비스를 등록하는것

LifeCycle Method CallStack

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(_:) 앱이 사용자에 의해 종료될 때 호출(시스템에 의한 예기치 못한 종료시에는 호출되지 않음).

ARC

ARC의 동작

메모리 영역 중 Heap은 참조형 타입인 클래스, 클로저 등을 저장한다. ARC(Automatic Reference Counting)는 앱의 메모리 사용을 추적하고 관리한다. 참조형 타입에 국한되어 사용되며, 다른 언어에서의 GC과 유사하지만 확실히 다르다.

  • GC: 프로그램 실행 중 런타임 시점에서 Mark and Sweep 프로세스를 통하여 각 객체의 노드를 표시하고 순회하면서 동적으로 감시하고 있다가, 더 이상 사용할 필요가 없다고 여겨지는 것을 메모리에서 삭제한다. 루트로부터 닿을 수 있는(rechable) 노드들을 살려놓고 나머지 객체는 메모리에서 해제한다.

    • 장점: 인스턴스의 참조 해제 확률이 ARC에 비해 높으며, 순환참조에 의한 메모리 누수를 방지하기 쉽다.
    • 단점: 개발자가 참조 해제 시점을 컨트롤하기 어렵고, 추적에 대한 리소스 사용에 오버헤드가 발생하여 성능이 저하될 수 있다
  • 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에서는 두 가지 방법을 제시한다.

  1. weak reference: Optional, 대상이 되는 인스턴스의 수명이 짧을 때 사용
  2. unowned reference: non-Optional, 대상이 되는 인스턴스의 수명이 비교적 길 때 사용

약한 참조(weak reference)

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)로 선언한다.

미소유 참조(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") 
    }
}

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 Optional References

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을 주로 사용하도록 하자

profile
소신있게 정진합니다.

0개의 댓글