ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift] ARC
    🍏Swift 2023. 2. 26. 13:43

    안녕하세요!

    오늘은 Swift의 메모리 관리 모델인 ARC에 대해서 한번 알아보겠습니다!

     

    메모리 관리 모델

    • 메모리 구조

    힙 영역에 할당되는 데이터는 관리를 해야지만 메모리에서 해제가 됨 할당이 해제되지 않으면 메모리 누수 현상이 발생

     

    • 메모리 관리 모델

    스위프트에서는 RC(참조 숫자)를 세는 것을 통해 메모리 관리 / 컴파일시에 메모리 해제시점을 결정함

    //오래된 프로그래밍 언어인 Objective-C, C와 같은 언어들은 MRC라는 수동으로 세야하는 관리 모델을 사용했었음
    //개발자들은 실제 로직 구현 포함, 메모리 관리에 대한 부담이 있었음
    //따라서 현대적 언어들은 대부분 자동 메모리 관리 모델을 사용
    //스위프트의 경우 컴파일러가 자동으로 retain()할당(+1), release()해제(-1) 코드를 삽입한다고 보면됨
    //컴파일러가 메모리 관리코드를 자동으로 추가해 줌으로써 프로그램의 메모리 관리에 대한 안정성 증가
    //쉽게 말하면 인스턴스를 가르키고 있는 RC가 1이상이면 메모리에 유지되고, 0이되면 메모리에서 제거됨
    
    // MRC(수동 RC관리)의 예시
    //원래는 주석처리한 것들을 실제로 입력해줬어야했음
    class Dog {
        var name: String
        var weight: Double
        
        init(name: String, weight: Double) {
            self.name = name
            self.weight = weight
        }
        
        deinit {
            print("\\(name) 메모리 해제")
        }
    }
    
    var choco: Dog? = Dog(name: "초코", weight: 15.0)  //retain(choco)   RC: 1
    var bori: Dog? = Dog(name: "보리", weight: 10.0)   //retain(bori)    RC: 1
    
    //RC가 0이 되었으니 아래 인스턴스들은 메모리에서 제거됨
    choco = nil   // RC: 0
    //release(choco)
    bori = nil    // RC: 0
    //release(bori)
    
    //ARC(자동 RC)
    var dog1: Dog?
    var dog2: Dog?
    var dog3: Dog?
    
    dog1 = Dog(name: "댕댕이", weight: 7.0)     // RC + 1   RC == 1
    dog2 = dog1                               // RC + 1   RC == 2
    dog3 = dog1                               // RC + 1   RC == 3
    
    dog3?.name = "깜둥이"
    dog2?.name
    dog1?.name
    
    dog3 = nil                                // RC - 1   RC == 2
    dog2 = nil                                // RC - 1   RC == 1
    dog1 = nil                                // RC - 1   RC == 0    // 메모리 해제
    

     

    강한 참조 사이클과 메모리 누수

    • 개념

    강한 참조 사이클이란 객체가 서로를 참조하는 것으로 인해 변수의 참조에 nil을 할당해도 메모리 해제가 되지 않는 메모리 누수의 상황이 발생하는 것

    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) 메모리 해제")
        }
    }
    
    var bori: Dog? = Dog(name: "보리")
    var gildong: Person? = Person(name: "홍길동")
    
    //여기서 강한 참조 사이클이 일어남
    bori?.owner = gildong 
    gildong?.pet = bori
    
    //nil을 할당해도 메모리가 해제되지 않는 메모리 누수가 발생
    bori = nil
    gildong = nil
    

     

    • 해결 방안(약한 참조(weak 키워드))

    가르키는 인스턴스의 RC의 숫자를 올라가지 않게 해주는 weak 키워드를 사용하면 가능함

    class Dog {
        var name: String
        weak var owner: Person?     // weak 키워드 -> 약한 참조
        
        init(name: String) {
            self.name = name
        }
        
        deinit {
            print("\\(name) 메모리 해제")
        }
    }
    
    class Person {
        var name: String
        weak var pet: Dog?         // weak 키워드 -> 약한 참조
        
        init(name: String) {
            self.name = name
        }
        
        deinit {
            print("\\(name) 메모리 해제")
        }
    }
    
    var bori: Dog? = Dog(name: "보리")
    var gildong: Person? = Person(name: "홍길동")
    
    // 강한 참조 사이클이 일어나지 않음
    bori?.owner = gildong
    gildong?.pet = bori
    
    // 메모리 해제가 잘됨(사실 이 경우 한쪽만 weak으로 선언해도 상관없음)
    bori = nil
    gildong = nil
    

     

    💡 weak키워드는 참조하고 있던 인스턴스가 사라지면 nil로 초기화되기 때문에 사용시에 weak var로만 선언이 가능하고 옵셔널 타입만 가능함

     

    • 해결방안(비소유 참조(unowned 키워드))

    가르키는 인스턴스의 RC의 숫자를 올라가지 않게 해주는 unowned 키워드를 사용하면 가능함

    class Dog1 {
        var name: String
        unowned var owner: Person1?    // Swift 5.3 이전버전에서는 비소유참조의 경우, 옵셔널 타입 선언이 안되었음
        
        init(name: String) {
            self.name = name
        }
        
        deinit {
            print("\\(name) 메모리 해제")
        }
    }
    
    class Person1 {
        var name: String
        unowned var pet: Dog1?
        
        init(name: String) {
            self.name = name
        }
        
        deinit {
            print("\\(name) 메모리 해제")
        }
    }
    
    var bori1: Dog1? = Dog1(name: "보리1")
    var gildong1: Person1? = Person1(name: "홍길동1")
    
    // 강한 참조 사이클이 일어나지 않음
    bori1?.owner = gildong1
    gildong1?.pet = bori1
    
    // 메모리 해제가 잘됨(사실 이 경우 한쪽만 unowned로 선언해도 상관없음)
    bori1 = nil
    gildong1 = nil
    

     

    💡 unowned 키워드의 경우 참조하고 있던 인스턴스가 사라지면 nil로 초기화 되지 않음

     

    클로저와 메모리 관리

     

    캡처리스트

    • 형태

    클로저는 힙의 영역에 존재해야하고 클로저 내부에서 외부에 존재하는 변수를 계속 사용해야 하기때문에 캡처 현상이 발생함

    //파라미터가 없는 경우
    { [캡처리스트] in
    			print("프린트")
    }
    
    //파라미터가 있는 경우
    { [캡처리스트](파라미터) -> 리턴형 in
    			print("프린트")
    }
    

     

    • 값 타입 캡처 / 캡처리스트

    캡처: 클로저 외부에 존재하는 값 타입의 참조(변수 주소)를 캡처함(외부요인에 의해 해당 값이 변했을때도 계속 참조)
    캡처리스트: 클로저 외부에 존재하는 값 타입의 값을 복사해서 사용(외부요인에 의해 해당 값의 변경을 방지할때 사용)

    //값 타입 캡처
    //클로저는 자신이 사용할 외부의 변수를 캡처함
    
    var num = 1
    
    let valueCaptureClosure = {
        print("밸류값 출력(캡처): \\(num)")
    }
    
    num = 7
    valueCaptureClosure()   //7 출력
    
    //밸류타입의 참조(메모리주소)를 캡처함
    //값 자체를 복사해서 가지고 있는 것이 아니고
    //num의 주소를 캡처해서 계속 사용하기 때문에 값이 변경될때마다 같이 변경됨
    
    num = 1
    valueCaptureClosure() //1 출력
    
    //값 타입 캡처리스트
    //선언시에 num = 1
    let valueCaptureListClosure = { [num] in      // 캡처리스트에서 밸류(value) 타입 캡처
        print("밸류값 출력(캡처리스트): \\(num)")   
    }
    
    num = 7
    valueCaptureListClosure()      //1 출력
    //1을 출력하는 이유는 캡처리스트를 통해 클로저 선언할 때에 num의 값이 1이 였기 때문 
    //값 타입 캡처리스트에서는 캡처와 다르게 선언할때의 값을 저장해두고 계속 사용함
    //값 변경 방지에 좋음
    
    // 밸류타입의 값을 캡처함
    // (즉, 값 자체를 복사해서 가지고 계속 사용)
    
    // 즉, 값 타입에서는 참조하는 값의 변경을 방지(외부적인 요인에 의한)하고 사용하고자 할때, 사용
    

     

    • 참조 타입 캡처 / 캡처리스트

    캡처: 클로저 외부에 존재하는 참조타입의 참조(변수 주소)를 캡처함(외부요인에 의해 해당 값이 변했을때도 계속 참조)
    캡처리스트: 클로저 외부에 존재하는 참조타입의 주소값을 복사해서 사용(외부요인에 의해 해당 인스턴스의 해제를 방지할때 사용 → 가르키는 인스턴스의 RC를 올라가게 해서 메모리에서 해제될 가능성을 방지해줌)

    //참조 타입 캡처 / 캡처리스트
    //참조 타입 캡처는 값 타입과 같음
    
    class SomeClass {
        var num = 0
    }
    
    var x = SomeClass()
    var y = SomeClass()
    
    print("참조 초기값(시작값):", x.num, y.num) // 0 0
    
    //x - (참조타입) 주소값 캡처, x를 직접참조로 가르킴
    //y - 변수를 캡처해서, y변수를 가르킴(간접적으로 y도 동일)
    
    let refTypeCapture = { [x] in
        print("참조 출력값(캡처리스트):", x.num, y.num)
    }
    
    x.num = 1
    y.num = 1
    
    //x = SomeClass()
    //y = SomeClass()
    
    print("참조 초기값(숫자변경후):", x.num, y.num)      // 1, 1
    
    //참조 타입 캡처와 캡처리스트 모두 주소를 복사하는 것이므로 변경된 값 출력함
    refTypeCapture()                               // 1, 1
    
    print("참조 초기값(클로저실행후):", x.num, y.num)    // 1, 1
    

     

    • 참조 타입 캡처리스트에서의 weak / unowned 키워드

    강한 참조 사이클 문제를 해결할 수 있는 저 키워드들은 참조 타입 캡처리스트에서만 사용 가능함

    var z = SomeClass()
    
    //가르키는 인스턴스의 RC를 올라가지 않게 함
    //weak키워드 변수의 값은 옵셔널 타입을 가짐
    let refTypeCapture1 = { [weak z] in
        print("참조 출력값(캡처리스트):", z?.num) 
    }
    
    refTypeCapture1()                        // Optional(0)
    
    //가르키는 인스턴스의 RC를 올라가지 않게 함
    let refTypeCapture2 = { [unowned z] in
        print("참조 출력값(캡처리스트):", z.num)
    }
    
    refTypeCapture2()                        // 0
    
    //캡처리스트에서 바인딩도 가능
    var s = SomeClass()
    
    let captureBinding = { [r = s] in   // 내부에서 변수명 바꿔서 사용가능 (외부변수와 헷갈리는 것을 방지)
        print("바인딩의 경우:", r.num)
    }
    
    let captureWeakBinding = { [weak r = s] in
        print("Weak 바인딩 경우:", r?.num) //weak 키워드는 옵셔널타입이므로 접근연산자에서 ?표시 해야함
    }
    
    captureBinding()
    captureWeakBinding()
    

     

    클로저의 사용

     

    • 객체 내에서의 사용

    클로저 내에서 객체의 속성 및 메서드에 접근 시에는 self키워드를 반드시 사용해야함(강한 참조를 하고 있다는 것을 표시하기위한 목적) → 여기서는 Dog의 RC를 올리는 역할 +1
    1) self.name
    2) [self]
    구조체의 경우, self를 생략하는 것도 가능

    class Dog {
        var name = "초코"
    
        func doSomething() {
            // 비동기적으로 실행하는 클로저
            // 해당 클로저는 오래동안 저장할 필요가 있음 -> 새로운 스택을 만들어서 실행하기 때문
            DispatchQueue.global().async { //새로운 스택을 만들어서 해당 클로저를 실행함
                print("나의 이름은 \\(self.name)입니다.")
            }
        }
    }
    
    var choco = Dog()
    choco.doSomething()
    
    // 클로저가 choco인스턴스에 대해 강한 참조는 하지만 (RC + 1)
    // 다른 스택에서 클로저의 실행이 완료되고 나면 힙에 있는 클로저도 제거되기 때문에
    // 실제 강한 참조 사이클을 일으키진 않음
    

     

    • 캡처리스트의 실제 사용 예시
    // 강한 참조의 경우
    // 강한 참조 사이클은 일어나지 않지만(인스턴스가 서로를 가르키는)
    // 클로저가 name 변수를 강한 참조를 하기 때문에 뷰컨트롤러를 가르키는 변수가 해제되었음에도
    // 강한 참조로 아직 RC가 1이 남아있기 때문에 3초 뒤에 출력하고 나서
    // 뷰컨의 메모리가 해제됨
    
    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()
    }                              // 이 함수는 이미 종료 ==> vc변수 없음
    
    //localScopeFunction()
    
    // (3초후)
    // 글로벌큐에서 출력하기: 뷰컨
    // 뷰컨 메모리 해제
    
    // 약한 참조의 경우
    // 뷰컨트롤러를 오래동안 잡아두지 않음
    // 뷰컨트롤러가 사라지면 출력하는 일을 계속하지 않도록 할 수 있음
    // (if let 바인딩 또는 guard let 바인딩까지 더해서 return 가능하도록)
    class ViewController1: UIViewController {
        
        var name: String = "뷰컨"
        
        func doSomething() {
            // 강한 참조 사이클이 일어나지 않지만, 굳이 뷰컨트롤러를 길게 잡아둘 필요가 없다면
            // weak self로 선언
            DispatchQueue.global().async { [weak self] in
                //guard let weakSelf = self else { return }
                sleep(3)
                print("글로벌큐에서 출력하기: \\(self?.name)")
            }
        }
        
        deinit {
            print("\\(name) 메모리 해제")
        }
    }
    
    func localScopeFunction1() {
        let vc = ViewController1()
        vc.doSomething()
    }
    
    localScopeFunction1()
    
    // 뷰컨 메모리 해제
    // (3초후)
    // 글로벌큐에서 출력하기: nil
    

     

    혹시 잘못된 내용이 있다면 피드백 주시면 정말 감사합니다! 😊

    '🍏Swift' 카테고리의 다른 글

    [Swift] Classes And Structures  (0) 2023.03.12
    [Swift] self vs Self  (0) 2023.03.05
    [Swift] Optional  (0) 2023.02.19
    [Swift] Enumeration  (0) 2023.02.09
    [Swift] Access Control  (0) 2023.02.06
Designed by Tistory.