Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARC in Swift: Basics and beyond #1

Open
kimscastle opened this issue Apr 12, 2023 · 0 comments
Open

ARC in Swift: Basics and beyond #1

kimscastle opened this issue Apr 12, 2023 · 0 comments
Assignees
Labels
Swift 주제 WWDC21 WWDC 21년영상 의성

Comments

@kimscastle
Copy link
Contributor

kimscastle commented Apr 12, 2023

1️⃣WWDC 영상 요약

Value type, Reference type

Value type

  • struct(기본 자료형), enum, tuple 등
  • 스택 영역에 저장 (항상은 아니다!)
  • LIFO
  • 컴파일 타임에 크기가 결정

Reference type

  • class, function, closure 등
  • 힙 영역에 저장
  • FIFO
  • 런 타임에 크기가 결정
  • ARC를 통해 자동으로 할당 및 해제됨

Strong, Weak, Unowned

strong

  • 해당 인스턴스의 소유권을 가짐
  • 값 지정 시 retain, 참조 종료 시 release 명령어를 추가하므로 RC값을 증감시킴
  • 참조 키워드 설정이 없다면 디폴트

weak

  • 해당 인스턴스의 소유권을 가지지 않음
  • retain, release 명령어를 추가하지 않으므로 RC값을 증감시키지 않음
  • 메모리 해제 시 nil로 초기화됨
  • nil이 될 수 있으므로 optional 타입으로 선언

unowned

  • 해당 인스턴스의 소유권을 가지지 않음
  • retain, release 명령어를 추가하지 않으므로 RC값을 증감시키지 않음
  • 메모리 해제 시 dangling pointer(빈 포인터)를 가리킴
  • nil이 될 수 없으므로 optional 타입으로 선언하면 안됨
  • 사라지지 않을거라고 보장되는 객체에만 사용

약한 참조가 필요한 경우 weak을 사용하고 옵셔널 바인딩으로 안전하게 추출하는 것이 권장됨

Automatic Reference Counting (ARC)

  • 컴파일 시 실행되어 코드를 분석해 자동으로 retain, release 코드 삽입
  • 추가된 retain, release를 사용하며 RC를 추적하다 RC가 0이 되면 메모리에서 해제

image Screen Shot 2022-06-09 at 22 59 38

Object lifetimes in Swift are use-based.

마지막 사용 이후 RC가 0이 되며 메모리에서 해제되므로 최소한의 lifetime을 보장받음

Examples

Memory leak 발생 예시

Screen Shot 2022-06-09 at 23 09 24 Screen Shot 2022-06-09 at 23 09 36 Screen Shot 2022-06-09 at 23 09 56

Person, Account의 RC가 1씩 남으므로 Memory leak 발생

weak 사용으로 해결

Screen Shot 2022-06-09 at 23 17 12 Screen Shot 2022-06-09 at 23 17 25 Screen Shot 2022-06-09 at 23 17 36

Person 해제 -> Account RC = 0 -> Account 해제

또 다른 문제 발생 예시

Screen Shot 2022-06-09 at 23 21 12 Screen Shot 2022-06-09 at 23 21 21 Screen Shot 2022-06-09 at 23 21 38

showAccount( ) 실행 전에 Person이 메모리에서 해제되어 weak으로 선언된 account.person이 nil로 초기화되어 문제 발생 강제 언래핑의 문제로 생각하여 옵셔널 바인딩으로 해결하려 한다면 보이지 않는 버그로 상황이 더 악화됨

Safe Techniques

withExtendedLifetime( )

Screen Shot 2022-06-09 at 23 25 48 Screen Shot 2022-06-09 at 23 26 20 Screen Shot 2022-06-09 at 23 26 35

defer 키워드를 사용하면 현재 스코프의 끝까지 Lifetime을 연장시킬 수 있음 하지만 정확성을 전적으로 개발자에게 부담시키고 유지보수 비용이 증가할 수 있음 따라서 더 좋은 API로 클래스를 다시 디자인하는 것을 권장

Redesign to access via strong reference

Screen Shot 2022-06-09 at 23 29 13 Screen Shot 2022-06-09 at 23 29 23 Screen Shot 2022-06-09 at 23 29 33

Redesign to avoid weak / unowned reference

사이클을 해결하려 하는 것보다 사이클 자체를 만들지 않는 것이 좋음 사이클 구조 -> 트리 구조로 변환

Screen Shot 2022-06-09 at 23 35 32

2️⃣ARC정리

스위프트의 메모리 관리 모델

  • MRC(수동 RC관리)와 ARC(자동 RC)
class Dog {
    var name: String
    var weight: Double
    
    init(name: String, weight: Double) {
        self.name = name
        self.weight = weight
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}

인스턴스를 하나씩 찍어내면 heap영역에 choco인스턴스 bori인스턴스가 올라감

→ 옵셔널 타입으로 설정한 이유는 nil을 할당할 수 있어야하기 때문

var choco: Dog? = Dog(name: "초코", weight: 15.0)
var bori: Dog? = Dog(name: "보리", weight: 10.0)

choco의 RC(레퍼런스 카운트)는 1이 되고 bori의 RC도 1이 된다

choco와 bori변수는 메모리 주소를 가지고 있는데 nil로 할당해주면 RC가 0이 된다

choco = nil   // RC: 0
//release(choco)
bori = nil    // RC: 0
//release(bori)

→ deinit이 호출된다

Memory Leak(메모리 누수) 현상

class Dog {
    var name: String
    var owner: Person?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}

class Person {
    var name: String
    var pet: Dog?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}

인스턴스를 각자만들면

  1. bori변수는 bori의 RC를 1증가시킴
  2. gildong변수는 gildong의 RC를 1증가시킴
  3. bori의 owner가 gildong의 RC를 1증가시킴(gildong RC = 2)
  4. gildong의 pet이 bori의 RC를 1증가시킴(bori RC = 2)

이런 상황에서 bori변수와 gildong변수에 nil을 할당하더라도 RC가 1씩 줄어들어도

서로가 서로를 가리켜 메모리에서 해제되지 않는다

💡 객체가 서로를 참조하는 강한 참조 사이클로 인해

변수의 참조에 nil을 할당해도 메모리 해제가 되지 않는

메모리 누수(Memory Leak)의 상황이 발생

Memory Leak의 해결방안

  • RC를 고려하여, 참조 해제 순서를 주의해서 코드 작성 → 실수할 가능성이 높음 비추
  • Weak Reference (약한 참조)
    • 가리키지만 RC를 증가시키지 않는 방법

메모리 누수(Memory Leak)현상의 해결

  1. 약한참조
weak var owner: Person?
weak var pet: Dog?

→ Person을 가리키지만 RC를 증가시키지 않음

→ 약한 참조의 경우, 참조하고 있던 인스턴스가 사라지면, nil로 초기화 되어있음

gildong = nil
bori?.owner   // gildong만 메모리 해제시켰음에도 ===> nil

밸류(Value) 타입 캡처와 캡처리스트

  • 클로저는 자신이 사용할 외부의 변수를 캡처함
var num = 1

// 클로저를 변수에 담는 순간 클로저는 heap영역에 저장된다
// num이라는 변수를 캡처한다 -> 변수의 주소값을 가리키고 있다
let valueCaptureClosure = {
    print("밸류값 출력(캡처): \(num)")
}
num = 7
valueCaptureClosure()  // result : 밸류값 출력(캡처): 7

num = 1
valueCaptureClosure()  // result : 밸류값 출력(캡처): 1 

→ 만약에 캡쳐 리스트를 사용하게 되면 주소값이 아닌 값 자체를 복사해서 가지고 있다

// 이미 위에서 1이라는 num의 값을 복사해서 가지고 와 있는 상태
let valueCaptureListClosure = { [num] in
    print("밸류값 출력(캡처리스트): \(num)")
}

// num에 7을 할당하더라도 num 1을 복사해서 가지고 있기 때문에 1을 출력
num = 7
valueCaptureListClosure()      // 몇을 출력할까요? = 1

참조(Reference) 타입 캡처와 캡처리스트

class SomeClass {
    var num = 0
}

var x = SomeClass()
var y = SomeClass()

// x라는 변수를 가리킨다(RC를 증가시키지 않음)
print("참조 초기값(시작값):", x.num, y.num)

// 참조타입을 캡처리스트에 담는 순간 x자체를 가리키게된다(RC증가)
let refTypeCapture = { [x] in
    print("참조 출력값(캡처리스트):", x.num, y.num)
}

강한 참조 사이클 문제의 해결

  • weak의 사용
  • weak를 사용하게 되면 메모리가 해제가 되더라도 그 인스턴스를 가리키고 있기때문에 nil을 할당할 수 있어야한다
  • [weak z]라면 z라는 인스턴스가 nil일 가능성이 있기때문에 옵셔널 체이닝을 적용해서 z?.name이라고 해야한다(이때 name은 옵셔널의 가능성이 있어도 ?안붙임)
var z = SomeClass()

let refTypeCapture1 = { [weak z] in
    print("참조 출력값(캡처리스트):", z?.num)
}

⭐️객체 내에서 클로저의 사용⭐️

  • 일반적인 클로저의 사용(객체 내에서의 사용, self키워드)
class Dog {
    var name = "초코"
    func doSomething() {
        DispatchQueue.global().async {
            print("나의 이름은 \(self.name)입니다.")
        }
    }
}

✅클로저 내에서 객체의 속성 및 메서드에 접근 시에는 "self키워드"를 ⭐️반드시⭐️ 사용해야함✅

💡 무조건 self를 붙여야하는데 방식이 두가지가 있다 1. self.name 2. [self] in name

→ 비동기 처리를 진행할때 변수의 주소를 캡쳐하고 있으면 2번 쓰레드에서 독립적으로 작업을 할 수가 없게된다 그렇기 때문에 객체를 가리키고 있어야한다(강한참조가 필요)

캡처리스트 실제 사용 예시

class ViewController: UIViewController {
    
    var name: String = "뷰컨"
    
    func doSomething() {
        DispatchQueue.global().async {
            sleep(3)
            print("글로벌큐에서 출력하기: \(self.name)")
        }
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}
func localScopeFunction() {
    let vc = ViewController()
    vc.doSomething()
} 
localScopeFunction()

함수 실행의 결과 → 글로벌큐에서 출력하기, 뷰컨, 뷰컨 메모리 해제

weak self라면 → 뷰컨 메모리 해제, 글로벌큐에서 출력하기: nil

Reference

@kimscastle kimscastle added WWDC21 WWDC 21년영상 Swift 주제 의성 labels Apr 12, 2023
@kimscastle kimscastle self-assigned this Apr 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Swift 주제 WWDC21 WWDC 21년영상 의성
Projects
None yet
Development

No branches or pull requests

1 participant