Swift Memory Part.1

만사·2021년 2월 6일
0

메모리

목록 보기
2/4
post-thumbnail

쉽게 메모리 누수를 찾아 수정하는 방법

메모리로부터 referencd types 제거

  • Referencd Types : 데이터를 서로 공유하는 인스턴스. 예) 클래스
class MyClass {
    var myInt = 8
}

let first = MyClass()

first 인스턴스는 메모리 주소(예를들어) 0x000123을 참조하는데, 이 주소안의 데이터 값은 8이다. 또 다른 인스턴스를 생성해보면,

let second = first

first를 복사한 second 인스턴스 역시 주소 0x000123을 참조하며 이 주소 안 데이터 값은 8이다.

second.myInt = 5

이렇게 second 인스턴스의 myInt 값을 5로 바꾸면 0x000123주소의 8이 5로 바뀌게 되며, 데이터를 같이 공유중인 first.myInt로 5로 변경된다. 이러한 특징으로 인하여 클래스 인스턴스는 메모리를 참조한다고 말하는데, 나는 second의 값만 5로 변경시키고 싶었는데 부득의하게 first의 값도 변경될 수 있기에 적절하게 메모리에서 참조를 제거해주어야 한다.

Swift는 언제 메모리에서 참조 유형을 제거할까?

: 참조 유형을 가리키는 인스턴스 변수가 더 이상 없을 때!

위 예제를 보면 0x000123이라는 메모리를 first와 second 인스턴스 변수가 가리키고 있다. first와 second가 nil이 되어 인스턴스를 메모리에서 제거하게 되면 0x000123 메모리는 제거된다.

참조 유형을 가리키는 변수가 더 이상 없을 때 Swift는 어떻게 알 수 있을까?

: ARC(Automatic Referece Counting)을 사용하여!

  • ARC : 자동으로 메모리를 관리해주는 방식. 프로그래머가 메모리 관리에 신경을 덜 쓸 수 있기에 편리하다.
class MyClass {
    var myInt = 8    // 0x000123 value : 8
}

var first: MyClass? = MyClass() // 0x000123참조 Reference Count: 1

var second = first // 0x000123참조 Reference Count: 2

var third = first // 0x000123참조 Reference Count: 3

first = nil // 0x000123참조해제 Reference Count: 2

이런 방법으로 몇개의 클래스 인스턴스가 참조하는지 카운팅하고 더 이상 필요하지 않는 인스턴스는 메모리에서 해제한다.

  • 실제 코드 예제

MyClass 클래스를 참조하는 3개의 클래스 인스턴스를 선언하고 차례대로 참조를 제거하는 코드다. MyCLass의 deinit은 무엇을까?

  • deinit : 클래스 인스턴스가 할당 해제되기 전에 deinitializer가 호출됨

즉, MyClass가 메모리에서 해제되는 순간 호출되는 친구이다.

  • allocate(메모리 할당) : 특정 목적을 위해 메모리를 예약하거나 따로 설정
  • deallocate(메모리 할당 해제) : 특정 목적을 위해 할당 된 메모리 리소스를 해제합니다.

앱을 실행하면 19라인에서 브레이크포인트가 걸려 18라인까지 코드가 실행된 후 멈추는데, 밑에 로그를 보면 first, second, third 클래스 인스턴스가 같은 0x00006000012cce20 주소를 참조하고 있다. 이 뜻은 MyClass Reference Count가 3이라는 뜻이다. 19라인부터 한줄씩 실행해보면,


first가 참조를 해제하면서 Reference Count가 2로 바뀌고

second가 참조를 해제하면서 Reference Count가 1로 바뀌고

마지막으로 third가 참조를 해제하면서 Reference Count가 0이 되면서 deinit은 MyClass가 메모리에서 할당 해제가 됐다고 알려준다. 반대로 init은 클래스, 구조체, 열거형 등이 새롭게 생성되었을 때 메모리를 할당해주는 역할이다.

종속성이 있는 참조유형 제거

Parent와 Child 클래스를 생성하고, Parent클래스 안에서 child 클래스 인스턴스를 생성했다. 여기서 종속성이 생긴다. child인스턴스가 Parent클래스에 종속됐다.

viewDidLoad에서 mom 클래스 인스턴스를 옵셔널로 선언 후 초기화되는 16번째 라인에 브레이크 포인트를 건 후 실행하고 16번째 라인을 실행해보면

mom 인스턴스가 참조하는 Parent클래스 메모리 주소는 0x00006000003b02c0이고 종속된 child인스턴스가 참조하는 Child클래스 메모리 주소는 0x00006000001b0200이다.

그 다음 18번째 라인인 mom인스턴스를 nil로 만들어 메모리 참조를 제거한다.

메모리 할당해제 순서가 중요한데, 먼저 Parent클래스가 메모리에서 할당해제가 된 후 Parent에 종속되어있던 child 역시 할 일이 없어졌기 때문에 child 인스턴스가 참조하던 Child클래스의 메모리 할당도 해제가 되었다.

메모리 할당 해제할 때의 문제

필요하지 않는 클래스 인스턴스를 자동으로 할당해제 시켜주는 것은 개발자 입장에서는 정말 편리하지만, 그만큼 신경을 못쓰기 때문에 생기는 문제가 있다. 위 예제처럼 깔끔하게 메모리 할당해제가 이루어지면 좋겠지만 여전히 참조가 남아있는 일명 "고아"가 된 클래스 인스턴스들이 있다. 두 객체가 서로 상호참조를 하는 상황의 코드를 통해 알아보자.

class Job {
    var person: Person?
}

class Person {
    var job: Job?
}

Job, Person 클래스를 생성하고 Job클래스는 person이라는 종속성을 지니고, Person클래스는 job이라는 종속성을 가진다.

var kim: Person? = Person()

kim이라는 클래스 인스턴스를 선언하면, kim은 Person(데이터: var job)을 참조할 것이고 Person메모리의 RC는 1이 된다.

var dev: Job? = Job()

dev라는 클래스 인스턴스를 선언하면, dev는 Job(데이터: person)을 참조할 것이고 Job메모리의 RC는 1이 된다.

kim?.job = dev

kim의 job을 dev로 선언하면, Person메모리의 데이터 job이 Job메모리를 참조하면서 Job메모리의 RC는 2가 된다.

dev?.person = kim

dev의 person을 kim으로 선언하면, Job메모리의 데이터 person이 Person메모리를 참조하면서 Person메모리의 RC는 2가 된다.

kim = nil

먼저 kim인스턴스를 nil로 바꾸면, kim의 참조가 제거되고 Person메모리의 RC가 1이 된다.

dev = nil

이후 dev인스턴스를 nil로 바꾸면, dev의 참조가 제거되고 Job메모리의 RC가 1이 된다.

여기서 문제가 발생한다. 분명 kim과 dev 인스턴스 메모리 참조를 제거했는데 메모리 할당이 여전히 존재한다. 메모리는 할당 받고 있지만 아무 일을 하지 않는 고아가 된 것이다. 이런 상황을 Retain Cycle이라고 한다.

  • Retain Cycle: 메모리 할당해제가 되지 않아 메모리 누수가 되는 현상(Reference Cycle, Retention Cycle, Circular Reference)

Leaks Profiler

xcode Instruments 중에 memory leak을 profile하는 도구가 있다.

Xcode -> Product -> Profile


Leaks 를 선택!

좌상단에 빨간색 실행 버튼을 눌러 앱을 실행시키고 메모리 누수를 프로파일한다.

앱을 동작하는 동안에 Leaks에 두건의 메모리 누수가 감지되었다.

확인해보니, Job과 Person에서 메모리 누수가 발생했다.

보기 편하게 옵션을 "Cycles & Roots"로 확인해보자.!

예상한대로 Person과 Job이 서로 상호간에 참조를 하고있는데 메모리 할당해제가 안된 것을 확인했다.

이렇게 프로파일 도구를 활용하여 메모리 누수를 잡을 수 있다. 사소한 기능 하나하나를 개발하면서 수시로 확인해야 작은 누수부터 안전하게 개발을 할 수 있을 것이다.

Retain Cycle을 수정하는 방법

위 예처럼 Person과 Job클래스는 서로 상호 참조를 하고있는데, 클래스가 다른 클래스를 참조할 때 기본적으로 "강한 참조(Strong Reference"를 한다고 한다. Retain Cycle을 수정하는 방법은 강한 참조를 하지 않으면 된다.

기본적으로 클래스 간의 참조시 부모와 자식 클래스로 나뉘는데, 강한 참조를 없애려면
1. 자식 클래스를 알아야 한다.

//Parent Class
class Person {          <-  부모
    var job: Job?
}

//Child class
class Job {              <- 자식
    var person: Person?
}

우리 예에서는 Person클래스를 부모로 Job클래스를 자식으로 둔다

  1. 자식 클래스가 weak 또는 unowned 상태의 참조를 가지도록 부모클래스에 요청해야한다.

중요한 점은, weak또는 unowned 상태의 참조는 RC를 증가시키지 않는다.!!

//Parent Class
class Person {          <-  부모
    var job: Job?
}

//Child class
class Job {              <- 자식
   weak var person: Person? <- 자식 클래스의 인스턴스를 weak로 선언
}

위 코드에서 Job클래스를 자식클래스로 두고 자식 클래스에 종족성이 있는 person 인스턴스를 weak로 선언한다. 그 후 21번 라인에 브레이크 포인트를 걸고 실행!

일단 Job , Person클래스가 서로 상호참조를 하고 있다. 하지만 자식 클래스 Job의 person인스턴스는 Person 부모 클래스를 강한 참조가 아닌 weak(약한) 참조를 하고 있기 때문에 RC는 1이다. 먼저 kim 인스턴스를 없앤다.

Person클래스를 참조하는 인스턴스가 하나도 없기 때문에 Person클래스는 메모리에서 할당해제가 잘 되었다! 그다음 dev인스턴스도 없앤다.

Job클래스 역시 dev라는 참조유형 하나만 있었기에 RC가 1이므로 dev인스턴스만 없애니 Job클래스는 정상적으로 메모리에서 할당해제가 되었다. 프로파일로도 확인해보자.

다행히도 메모리 누수가 없었다!!

강한(Strong), 약한(weak)그리고 소유하지 않은(Unowned) 이게 뭔데?

  • Strong : 인스턴스간의 참조를 할 때, 디폴트로 설정된 참조 유형. 부모 클래스와 자식 클래스간의 참조가 이루어 질 때, 부모 클래스가 존재하는 한 자식 클래스는 없어지지 않는다.

  • weak : 자식 클래스가 존재할 수도 있고 없을 수도 있습니다. 부모 클래스가 메모리에서 제거되면 존재하지 않는다.

  • unowned : 자식 클래스는는 확실히 항상 존재하지만 부모 클래스가 제거되면 제거된다.

  • weak와 unowned를 선언하는 방법

weak를 사용하여 인스턴스를 초기화 할 때는 옵셔널로 선언되어야 한다. 이유는 weak의 개념 자체가 옵셔널과 마찬가지로 "있을 수도 있고 없을 수도 있는" 성격을 가지기 때문이다. 그럼 weak let 으로 선언해보자.

대충 짐작은 했지만, 역시 안된다. weak로 선언된 클래스 인스턴스는 참조가 없으면 런타임 과정에서 nil로 변하기 떄문에 상수로 선언할 수 없다.

다음으로 Unowned를 알아보자 Unowned는 비옵셔널 선언이 될까?

Unowned의 특성대로 반드시 자식 클래스는 항상 존재하므로 옵셔널이 아니어도 선언이 된다.
그리고. Unowned 역시 let으로 선언할 수 없다.


노란 경고는 person 인스턴스가 unowned로 선언되었기 때문에 즉시 메모리 할당해제를 시킨다고 했다. 그렇기에 메모리를 할당하려면 당연히 person인스턴스의 데이터를 주어야하는데 let으로 선언하면 kim이라는 데이터가 들어가지 못한다.

그럼 Weak 랑 Unowned는 언제 써야하지?

두 키워드의 차이점은 옵셔널인지 옵셔널이 아닌지로 구분할 수 있다. 그러므로 참조가 프로그램 수명의 어느 시점에서 nil이 될 수 있는 경우 weak를, 일단 설정되면 참조가 절대로 nil이되지 않는다는 것을 알고있을 때는 unowned를 사용하여 메모리 할당해제를 시켜주자.

결론

참조 카운트(RC)가 줄어들지 않아 메모리 누수를 만드는 경우 "Retain Cycle"이 존재하므로, retain cycle이 도는 클래스 중 1. 자식 클래스를 찾고 2. 자식 클래스 내에서 선언하는 인스턴스가 프로그램 수명 내에 옵셔널이 되는 인스턴스인지 아닌지를 확인하여 weak or unowned로 선언하자.!

0개의 댓글