ARC에 대하여

SOPT·2022년 12월 30일
1

iOS

목록 보기
2/2
post-thumbnail

1. 메모리 구조

운영체제에서는 메모리에 프로그램을 위한 공간을 할당해줍니다!
그 공간을 총 4가지로 나눌 수 있는데욥

https://velog.velcdn.com/images/hello_hidi/post/c16bd056-4b5b-42d0-a307-5ab27d0840f2/image.png

i) code, data 영역

code영역: 실행할 프로그램의 코드가 저장되는 영역으로 텍스트 영역이라고도 부릅니다. CPU는 코드 영역에 저장된 명령어를 하나씩 가져가서 처리하게 됩니다.
-> 간단히 우리가 작성한 소스 코드가 기계어 형태로 저장됨!

data영역: 프로그램의 전역 변수와 정적(static) 변수가 저장되는 영역입니다.
데이터 영역은 프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸합니다.

ii) heap 영역

사용자에 의해 메모리 공간이 동적으로 할당되고 해제되는 영역

  • 사용하고 난 후에는 반드시 메모리 해제를 해줘야 한다. 그렇지 않으면 memory leak이 발생한다.
  • 유일하게 런타임 시에 결정되기 때문에 데이터의 크기가 확실하지 않을 때 사용
  • 메모리의 낮은 주소에서 높은 주소의 방향으로 할당됩니다.
  • Swift에서는 closure, class 등 참조타입의 값이 힙에 자동 할당됨!

iii) stack 영역

함수의 호출과 관계되는 지역 변수와 매개변수가 저장되는 영역

  • 함수의 호출과 함께 할당되며, 함수의 호출이 완료되면 소멸합니다.
  • 컴파일 타임에 결정되기 때문에 무한히 할당할 수 없다.
  • 프로그램이 자동으로 사용하는 임시 메모리 영역이다.

Heap vs Stack

각각의 장단점

Q. 언제 힙을 쓰고, 언제 스택을 쓰나용?

데이터의 크기를 모르거나, 스택에 저장하기에 너무 큰 데이터인 경우 힙에 할당

물론 위에 경우는 클로저와 인스턴스처럼 자동으로 힙에 할당하는 것 외에 직접 할당 경우이당~
만약에 너무 많은 메모리를 스택에 할당하면 우리에게 다른 의미로 익숙한

https://velog.velcdn.com/images/hello_hidi/post/7377365e-0933-415d-b376-c4b82cc6355d/image.png

자 이렇게 메모리 기초를 봤는데 일단 ARC를 보기 전에 옵제씨 사용 틀딱 시절엔 힙 영역에 어케 할당하고 해제했는지를 먼저 보려고 한다. 그 이유는 ARC를 왜? 써야 되는지를 알려면 불편함을 느껴야 되기 때문! 그럼 MRC에 대해 알아보장~

2. MRC

Manual Retain Counter의 준말로 수동으로 RC를 계산하는 기법이다!

i) RC란?

RC는 Retain Counter(MRC에서) 에 준말이며, 참조횟수이다!

Swift에는 값타입과 참조타입이 있으면 참조타입이 이해가 안된다면, 저번에 작성한 블로그를 참고하면 될 거 같다잉~

ii) retianCount

MRC에 경우 개발자가 직접 count를 해서인지 인스턴스에 대한 RC에 접근할 수 있다! .retainCount는 인스턴스의 RC에 접근할 수 있는 프로퍼티이다!

MRR로 RC 다루기

1) 인스턴스를 새로 생성할 때

Objective-C에서 인스턴스를 새로 생성하는 방법에 아래와 같은 메서드들이 있다.

alloc

  • new
  • copy
  • mutableCopy
//아래와 같은 방식으로 인스턴스를 새로 생성할 수 있다.
TestClass *test = [[TestClass alloc] init];

여기서 인스턴스를 새로 생성하면 자동으로 RC가 +1이 된다. 어 그럼 자동으로 되는거자나? 왜 구라깜? 이제 수동적으로 증가시키는 걸 보여줄게용

2) retain 메소드를 사용할 때

retain 메서드를 사용해서 우린 RC를 수동적으로 증가시킬 수 있다

Q. 그럼 언제 쓰냐?
A. 절대로 남발해서는 안된다. 사실 RC값이 늘어나는 경우는 정해져있다. 새로 인스턴스를 생성했을때랑 기존 인스턴스를 참조했을 때. 그러나 새로 인스턴스를 생성할 땐 자동으로 생성해주니까
=> 기존의 인스턴스를 참조할 때 retain 메소드를 사용해 RC값을 사용한다

TestClass *test = [[TestClass alloc] init];
TestClass *test2 = test; // 기존 인스턴스 참조
[test retain]; // test RC +1

3) release 메소드를 사용할 때

release 메서드를 사용해서 우린 RC를 수동적으로 감소시킬 수 있다

[test release];
test2 = nil;

결론

그냥 할당할때마다 옵제씨 시절에는 할당,해제를 주구장창~~~ 수동으로 해줘야했다. 벌써 불편하자나 그래서 우린 드디어 ARC를 배우러 간다.

MRC결론 : 후 너네는 이런거 하지마라~

Untitled

드디어 ARC이다!!!!

참조타입과 Heap

ARC를 제대로 배울라면 저번주차에서 다뤘던 참조타입과 메모리 구조에서 배운 Heap에 대한 이해가 있어야 된다!
간략하 설명하자면

Swift의 클래스와 클로저는 Heap영역에 저장되는 참조타입이다.

  • 참조타입의 특징은 지역변수는 단지 스택에 저장되어 인스턴스의 주소값을 가지고 있고
  • 스택의 지역변수가 힙 영역의 실제 인스턴스를 참조하는 형태이다.
![https://velog.velcdn.com/images/hello_hidi/post/1a733fa6-d646-45d5-a46f-f4b6f6447269/image.png](https://velog.velcdn.com/images/hello_hidi/post/1a733fa6-d646-45d5-a46f-f4b6f6447269/image.png)

메모리 해제

우리는 지금까지 걍 마음대로 클래스 만들어서 인스턴스 퐉퐈고파ㅗ가퐈 찍어내고 마음껏 사용했음. but Heap에 특징! 사용하고 난 후 반드시 메모리 해제를 해줘야 한다! 근데 한 사람????

https://velog.velcdn.com/images/hello_hidi/post/8a019bce-2ad1-45d1-b2cb-db0a0f6f4d50/image.png

아무도 없을것이다! 왜냐? 바로 ARC가 해주기 때문이다!

3. ARC

Automatic Reference Counting의 약자로, 클래스 인스턴스가 더 이상 필요하지 않을 경우 메모리를 자동으로 해제해주는 것이 주기능이다!

ARC 작동원리

ARC는 인스턴스에 대한 정보를 저장하기 위해 메모리의 청크에 할당합니다.

  • 메모리 청크: malloc()으로 할당 받는 영역과 header를 포함한 영역

컴파일 시점에 언제 참조되고 해제되는지 결정되어 retain, release를 삽입을 한다!

  1. 런타임 때 결정되어 그대로 실행한다
![https://velog.velcdn.com/images/hello_hidi/post/25b17636-ee4d-4b97-b683-ee78987709fa/image.png](https://velog.velcdn.com/images/hello_hidi/post/25b17636-ee4d-4b97-b683-ee78987709fa/image.png)

ARC에서 RC 다루기

i) RC값이 증가하는 경우

인스턴스를 새로 생성할 때

  • 기존 인스턴스를 다른 변수에 대입할 때

ii) RC값이 감소하는 경우

인스턴스를 가리키던 변수가 메모리에서 해제되었을 때

  • nil이 지정되었을 때
  • 변수에 다른 값을 대입한 경우
  • 프로퍼티의 경우, 속해있는 클래스 인스턴스가 메모리에서 해제될 때

예시 코드

class Ayomi {
    var name: String?
    var age: Int?

    init(name: String? = nil, age: Int? = nil) {
        self.name = name
        self.age = age
    }

    deinit {print("당신의 아요미 인생은 여기까지~")}
}

var hidi: Ayomi? = .init(name: "hidi", age: 23) // hidi Instance RC: 1
var hidiclone = hidi // hidi Instance RC: 2, hidiclone Instance RC: 2

hidiclone = nil // hidi Instance RC: 1
hidi = nil // hidi Instance RC: 0

var min: Ayomi? = .init(name: "min", age: 76) // min Instanc RC: 1
var minClone: Ayomi? = .init(name: "minClone", age: 5) // minClone Instance RC: 1

min = minClone // min Instance RC: 0, minClone Instance RC: 2

class Ayo {
    var level: Int?
    var ayomi: Ayomi? = .init(name: "ayomi", age: 20)

    init(level: Int? = nil) {
        self.level = level
    }
}

var ayo: Ayo? = .init(level: 31)
ayo = nil

ARC의 단점 중 하나는 순환 참조가 발생 시 영구적으로 메모리가 해제되지 않을 수 있다. 아니 근데 저기요 순환 참조가 뭐죠? 그걸 알아보러 가보자잉~

강한참조

우리가 지금까지 했던 힙의 할당하는 방식이 강한 참조 방식이다!

즉 인스턴스의 주소값이 변수에 할당될 때, RC가 증가하면 강한 참조!
애초에 default 값이 strong~
하지만 strong에 문제는 바로 순.환.참.조

순환참조

예시를 들어보자! 요즘 내가 킹받고 있는 꽃보다 남자로 예시를 들어보겠다. 먼저 준표랑 잔디를 등장시켜보겠다

class Man {
    var name: String
    var girlfriend: Woman?

    init(name: String) {
        self.name = name
    }
    deinit { print("맨킬뎃쉿스껄!") }
}

class Woman {
    var name: String?
    var boyfriend: Man?

    init(name: String) {
        self.name = name
    }
    deinit { print("우맨킬뎃쉿스껄!") }
}

var junpyo: Man? = .init(name: "준표")
var zandi: Woman? = .init(name: "잔디")

그리고 이 둘은 서로가 서로의 남친 여친임을 표시해준다.
메모리에서 볼 때 둘의 관계는 아래 그림과 같다.

junpyo?.girlfriend = zandi
zandi?.boyfriend = junpyo

그러면서 이 둘의 RC도 하나씩 증가했다.

https://velog.velcdn.com/images/hello_hidi/post/43dcd9d4-e5ef-4e42-8d0b-6b23c7dce098/image.jpeg

4. 순환참조의 문제점

자~ 이제 순환참조의 문제점에 대해 알아보자! 이 메모리의 주인이 나는 둘의 사이가 너무 킹받아서 죽여버리기로 결심을 했따. 물론 메모리 상에서만 ㅎㅎ

junpyo = nil
zandi = nil

이렇게 하면 나의 예상대로 메모리가 깔끔하게 사라져야 되는데 현실은 RC의 값이 둘다 1임으로 메모리 상에 살아서 아직도 나를 킹받게 하는 것이다...

https://velog.velcdn.com/images/hello_hidi/post/22c55e0f-3000-415f-a77a-427e0026eedf/image.jpeg

즉 strong으로 선언된 변수들이 순환참조 됐을 시 큰 문제는

서로가 서로를 참조하고 있어서 RC가 0이 되지 못한다는 것이다!

  • 또한 해당 인스턴스를 가리키던 변수도 nil로 지정했기 때문에 인스턴스 접근도 할 수 없어 메모리 해제도 못하는 최악의 상황이 펼쳐진다.

이것들을 해결하기 위해서 우리는 weak과 unowned를 알아야 한다.

weak

weak 약한참조, 즉 인스턴스를 참조할 시, RC를 증가시키지 않는다.

참조하던 인스턴스가 메모리에서 해제된 경우, 자동으로 nil이 할당되어 메모리가 해제된다!

  • weak은 프로퍼티를 선언한 이후, 나중에 nildㅣ 할당된다는 관점에서
    무조건 옵셔널 타입의 변수
  • 둘 중에 수명이 더 짧은 인스턴스를 가리키는 애를 약한 참조로 선언함.
class Man {
    var name: String
    weak var girlfriend: Woman?

    init(name: String) {
        self.name = name
    }
    deinit { print("맨킬뎃쉿스껄!") }
}

class Woman {
    var name: String?
    var boyfriend: Man?

    init(name: String) {
        self.name = name
    }
    deinit { print("우맨킬뎃쉿스껄!") }
}

var junpyo: Man? = .init(name: "준표")
var zandi: Woman? = .init(name: "잔디")

junpyo?.girlfriend = zandi
zandi?.boyfriend = junpyo

https://velog.velcdn.com/images/hello_hidi/post/6824b330-04fa-486f-b9d9-3d49acc852dc/image.jpeg

여기서 준표랑 잔디를 죽이면

  1. zandi, junpyo가 스택에서 가리키는 인스턴스 RC 둘다 1
  2. 잔디의 RC가 0이 되면서 kill
  3. 준표의 여자친구 속성이 0이 되면서 RC 0되면서 킬
  4. 메모리 깨-끗>

unowned

weak과 비슷하게 RC값을 증가시키지 않아서 강한 순환 참조를 해결하지만,
인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하는게 핵심임!

참조하던 인스턴스가 만약 메모리에서 해제된 경우에도 nil을 할당받지 못하고, 해제된 메모리 주소값을 계속 들고 있음

  • 둘 중에 수명이 더 긴 인스턴스를 가리키는 애를 unowned 선언해야됨!
//week으로 선언될 경우
junpyo?.girlfriend / 결과값 : nil
//unowned으로 선언될 경우
junpyo?.girlfriend / 결과값 : error: Execution was interrupted

작성자
IN SOPT, YB iOS 류희재

profile
IT 대학생벤처창업동아리 SOPT의 공식 블로그입니다.

0개의 댓글