ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • swift closure에서 사용하는 weak self 정체 알기
    카테고리 없음 2021. 6. 23. 09:56

    swift에서 이런 것을 본 적이 있을 것이다.

     

    let alertAction = UIAlertAction(title: Title, style: .default, handler: { [weak self]
            guard let self = self else { return }
            self.DoDefaultAction();
        })

    handler 파라미터는 () -> Void 타입인 closure이다.

    closure 안에 [weak self]라는 것이 있는데...?

     

    이번 글에서는 저 weak self의 정체가 무엇인지 알아보자.

     

    일단, weak self의 정체를 알기 전에, 메모리 구조부터 이해할 필요가 있다.

     

    일단 c나 c++같은 초기 언어의 메모리 구조를 먼저.

    링크: 코딩의 시작, TCP School

     

    아래는 c++의 예시.

    int main()
    {
        int* pa = doA();
        int* pb = doB();
        // a값 10을 얻을 수 없다.
        // b값 20을 얻을 수 있다.
        
        // 메모리를 직접 해제해줘야 한다.
        delete pb;
        return 0;
    }
    
    int* doA()
    {
        int a = 10;
        return &a;
    }
    
    int* doB()
    {
        int* b = new int(20);
        return b;
    }

    로컬 변수로 정의되어 stack에 저장되는 변수 a는 a가 선언된 블록을 벗어나면 자동으로 사라진다.

    동적 할당으로 생성되어 heap에 저장되는 b는 b가 선언된 블록을 벗어나도 계속 데이터가 유지된다.

    더이상 사용할 일이 없게 되면 사용자가 직접 delete를 통해서 메모리를 해제해줘야 한다. 만약 사용자가 메모리 해제를 까먹는다면, 사용하지도 않는 데이터가 메모리 공간을 차지하고 있어 메모리가 낭비되는 일이 발생한다. 이것을 메모리 누수(memory leak)라고 한다.

     

    단순히 메모리를 해제하는 것을 까먹어서 메모리 누수가 발생하는 일들을 방지하고자 c++에서는 스마트포인터를 제공한다.

    C++] 스마트 포인터에 대하여... (tistory.com)

    위 링크에서 잘 봐둬야 할 것은 weak_ptr이다. 주소가 없으면 정상적인 방법으로는 접근을 할 수가 없으니까, 아무도 그 주소를 참조하지 않는다면 더이상 사용하지 않는 것으로 간주하고 메모리를 자동으로 해제해주는 것이 shared_ptr인데, shared_ptr는 재수가 없으면 순환 참조로 절대로 count가 0이 되지 않는 문제가 발생하므로, 순환 참조가 일어날만한 상황이라면 weak_ptr를 대신 사용해서 해결할 수 있다.는 것만 알면 된다.

     

    https://yaboong.github.io/java/2018/05/26/java-memory-management/
    자바같은 경우는 참조형은 무조건 heap에 할당됨. 대신 알아서 메모리를 관리해주는 시스템이 있어서, 해제를 직접 할 필요가 없다. (garbage collector)
    garbage collector는 이번 글에서 다루려는 주제가 아니므로 넘어간다.

     

    c++에서 동적 할당을 하기 위해서 new라는 연산자를 사용했는데, swift에는 new가 없다. 그렇다면 동적 할당은 없고 모든 데이터는 stack에 저장되는 것일까? 그렇지 않다. swift에서는 enum, struct와 같은 value type은 stack에 저장하고, 클래스 같은 reference type은 heap에 저장한다.

    출처: https://babbab2.tistory.com/25

    (일반적으로. 항상 그렇지는 않다고 한다. https://sujinnaljin.medium.com/ios-swift%EC%9D%98-type%EA%B3%BC-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EC%A0%80%EC%9E%A5-%EA%B3%B5%EA%B0%84-25555c69ccff)

     

    swift에서는 메모리를 관리하기 위해서 Automatic Reference Counting이라는 것을 사용한다.

    참고링크: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

     

    ARC tracks how many properties, constants, and variables are currently referring to each class instance. ARC will not deallocate an instance as long as at least one active reference to that instance still exists.

    To make this possible, whenever you assign a class instance to a property, constant, or variable, that property, constant, or variable makes a strong reference to the instance.

    누군가가 어느 인스턴스를 참조하면 strong reference를 만들고, (c++의 shared_ptr처럼) Strong Reference의 개수가 0이 되면 알아서 메모리를 해제해준다.
    shared_ptr와 마찬가지로 순환 참조 문제가 있다. Strong Reference cycle.

    예시 링크: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID51

    swift에서는 strong reference cycle 문제를 피하면서도 참조를 유지할 수 있는 방법으로 strong reference 외에 weak reference, unowned reference를 함께 제공한다.

     

    weak reference, unowned reference 둘 다 strong reference가 아니니까 ARC에서 카운트 되지 않는다. 즉, 둘 다 참조가 유지되고 있는 상황에서도 언제든지 메모리 해제가 가능하다.

    * 장점: 서로 순환 참조하면서 서로가 해제되는 것을 막는 상황은 피해갈 수 있다.

    * 단점: 아직 필요한데 데이터가 없어져버릴수가 있다.

     

    자세한 설명은 링크를 통해서 읽어보도록 하고, https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID52

    핵심적인 내용을 요약만 하자면,

     

    1. weak reference

    * 변수 선언할 때 weak var 이런 식으로 weak 키워드를 붙여서 사용한다.

    * 메모리가 해제되고 나면 그 주소를 참조하고 있는 weak var 변수를 nil로 바꿔준다.

    * 언제든지 nil로 변경될 수 있기 때문에 optional(nil이 될 수 있는) var(상수 아니고 변수)이어야 한다.

    * 서로 순환 참조할 대상들의 lifetime이 관계가 없을 때 사용한다. (무엇이 먼저 없어져도 상관 없는 경우)

     

    2. unowned reference

    * 변수(혹은 상수로) 선언할 때 unowned var 이런 식으로 unowned 키워드를 붙여서 사용한다.

    * 메모리가 해제된 뒤에 값을 참조하려고 시도하면 runtime error가 발생한다.

    * unowned하는 대상이 unowned되는 인스턴스보다 오래 살아남거나 적어도 같이 사라지는 경우에 사용한다.

     

    closure에서 발생하는 strong reference cycle

    https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html#ID56

     

    class MyClass
    {
        func void OurMainFunction()
        {
            PrintSomething(message: "Print Me!") { in
                self?.DoSomethingAfterCompletion1()
                self?.DoSomethingAfterCompletion2()
            }
        }
        
        func void PrintSomething(message: String, completionHandler: () -> Void)
        {
            print message
            completionHandler()
        }
        
        func void DoSomethingAfterCompletion1()
        {
            something...
        }
    
        func void DoSomethingAfterCompletion2()
        {
            something...
        }
    }
    

     

    This strong reference cycle occurs because closures, like classes, are reference types. When you assign a closure to a property, you are assigning a reference to that closure. In essence, it’s the same problem as above—two strong references are keeping each other alive.

    클래스 인스턴스는 closure를 strong 참조하고, closure는 자기 자신의 클래스 인스턴스를 strong reference로 참조하고있다.

     

    순환 참조를 해결하기 위해서 self를 weak나 unowned로 참조하면 된다.

    class MyClass
    {
        func void OurMainFunction()
        {
            PrintSomething(message: "Print Me!") { [weak self] in
                self?.DoSomethingAfterCompletion1()
                self?.DoSomethingAfterCompletion2()
            }
        }
        
        func void PrintSomething(message: String, completionHandler: () -> Void)
        {
            print message
            completionHandler()
        }
        
        func void DoSomethingAfterCompletion1()
        {
            something...
        }
    
        func void DoSomethingAfterCompletion2()
        {
            something...
        }
    }
    

     

    댓글

Designed by Tistory.