Introduce

  • Swift 및 Objective-C에서 참조 메모리 관리를 자동으로 해 주는 기능
  • 인스턴스의 참조횟수를 추적해서 더 이상 참조되지 않는 인스턴스를 메모리에서 해제시킴

Why use?

  • 값 타입(struct, enum, literal 등)은 변수, 상수, 프로퍼티 등에 할당될 때 값을 복사하기 때문에, 할당된 값을 변경해도 원래 값은 바뀌지 않는다.
  • 반면에, 참조 타입(인스턴스, 클로저 등)은 값을 복사하지 않고 참조를 전달한다.
  • 원래 변수와 할당된 변수가 같은 인스턴스 메모리에 접근하게 되므로 할당한 변수에서 값을 변경하면 원래 변수의 값에도 영향을 미친다.
// Value Type
var num1 = 10
var num2 = num1
num2 = 15
print(num1, num2)
/* print
 * 10, 15
 */

// Reference Type
class Sample {
  var num1: Int
  var num2: Int

  init(num1: Int, num2: Int) {
    self.num1 = num1
    self.num2 = num2
  }
}

var sample1 = Sample(num1: 10, num2: 20)        // Sample 인스턴스 생성, sample1이 참조함
var sample2 = sample1                            // sample1이 참조하는 인스턴스를 참조
var sample3 = sample2                            // sample2가 참조하는 인스턴스를 참조
var sample4 = Sample(num1: 100, num2: 200)        // Sample 인스턴스 생성. sample1과 다른 인스턴스

print(sample1.num1, sample1.num2)
print(sample2.num1, sample2.num2)
print(sample3.num1, sample3.num2)
print(sample3.num1, sample3.num2)
/* print
 * 10, 20
 * 10, 20
 * 10, 20
 * 100, 200
 */

// sample3에서 변경한 프로퍼티가 sample1과 sample2에 영향을 줌
sample3.num1 = 30
sample3.num2 = 40

print(sample1.num1, sample1.num2)
print(sample2.num1, sample2.num2)
print(sample3.num1, sample3.num2)
print(sample3.num1, sample3.num2)
/* print
 * 30, 40
 * 30, 40
 * 30, 40
 * 100, 200
 */

Automatic reference release

  • 인스턴스를 생성해서 변수에 할당하면 따로 해제해 주기 전까지는 메모리를 차지하고 있기 때문에 사용이 끝난 인스턴스는 메모리에서 해제해야 한다.
  • ARC는 참조된 횟수를 추적해서 더 이상 참조되지 않는 인스턴스를 메모리에서 해제시키는 방법으로 메모리 관리를 도와준다. 개발자가 일일이 인스턴스 참조를 추적해서 적절한 시점에 release 코드를 넣어줘야 하는 수고를 덜어주었다.
  • 규칙을 제대로 알고 사용한다면 효과적인 메모리 관리가 가능하다.
  • 인스턴스를 메모리에서 너무 일찍 해제시켜서 잘못된 메모리 접근 오류를 발생시키거나, 반대로 메모리에서 해제시키지 않아서 사용하지 않는 인스턴스가 누적되어 메모리를 낭비하는 문제가 발생하는 것을 차단할 수 있다.

How to work?

  • 인스턴스가 참조되거나 참조가 해제될 때 횟수를 카운팅한다.
  • 참조 횟수가 0이 되면 인스턴스를 메모리에서 해제시킨다.
  • ARC는 컴파일 할 때 미리 정해진 대로 동작하기 때문에 대략적인 참조 흐름은 예상해 봐야 한다.
  • ARC의 참조 추적 동작을 예측해 볼 수 있으려면 참조 횟수가 증가하고 감소하는 규칙을 이해하고 있어야 한다.

Count Up

  • 변수, 상수, 프로퍼티 등에 인스턴스가 할당될 때 증가한다.
    let sample = Sample()    // 생성된 instance를 상수 sample이 참조하면서 count + 1

Count Down

  • 변수, 상수, 프로퍼티의 생명 주기가 끝나는 시점에 감소한다.
  • 변수 또는 상수로 할당되었을 때는 변수가 메모리에서 해제될 때 감소한다.
    1. 함수 등 실행 블럭 안에서 선언된 지역 변수는 함수 실행이 끝날 때 메모리에서 해제된다.
    2. 옵셔널 타입 변수에 아무 값도 갖지 않음을 나타내는 nil을 할당하면 메모리에서 해제된다.
var sample1: Sample? = Sample1()    // Sample1 ref 1
func execute() {
  let sample2 = sample1             // Sample1 ref 2
  let sample3 = Sample3()            // Sample3 ref 1
}
execute()        // Sample1 ref 1, Sample3 ref 0(release).
sample1 = nil    // Sample1 ref 0(release)
/* print */
// Sample3 is deinitialized
// Sample1 is deinitialized
  • 프로퍼티로 할당되었을 때는 해당 클래스의 인스턴스가 메모리에서 해제될 때 감소한다.
class Sample1 {
  var sample2: Sample2?
}
var sample1: Sample1? = Sample1()    // Sample1 ref 1
sample1?.sample2 = Sample2()            // Sample2 ref 1
sample1 = nil        // Sample1 ref 0(release) -> Sample2 ref 0(release)
/* print */
// Sample1 is deinitialized
// Sample2 is deinitialized

Problem1: Instance 상호 참조에 의한 Strong Reference Cycle

  • 인스턴스의 참조 횟수가 0이 아니면 절대 메모리에서 해제되지 않는다.
  • 서로 다른 인스턴스가 서로를 참조하는 상황에서 인스턴스가 영원히 메모리에서 해제되지 않는 순환 참조 문제가 발생할 수 있다.
class Sample1 {
  var ref2: Sample2?
}

class Sample2 {
  var ref1: Sample1?
}

var sample1: Sample1? = Sample1()        // Sample1 ref 1
var sample2: Sample2? = Sample2()        // Sample2 ref 1
sample1?.sample2 = sample2        // Sample2 ref 2
sample2?.sample1 = sample1        // Sample1 ref 2
sample1 = nil        // Sample1 ref 1
sample2 = nil        // Sample2 ref 1
  • sample1sample2nil을 할당하면서 각각 인스턴스 참조 횟수가 감소했지만, 프로퍼티 ref1, ref2sample1sample2를 할당받으면서 증가시킨 참조 횟수가 아직 남아있다.
  • nil을 할당해서 해제하려고 해도 sample1sample2nil을 할당했기 때문에 ref1ref2에 접근할 수 없다. 두 instance는 참조 횟수가 절대로 0이 될 수 없기 때문에 영원히 메모리에 남아있게 된다.
  • ref1ref2에 먼저 nil을 넣어서 해제시킨 뒤에 sample1sample2를 해제시켜도 되지만, 애초에 ref1ref2가 instance를 참조할 때 참조 횟수가 증가하지 않았다면 위와 같은 간결한 코드로 해결이 가능하다.
  • Instance를 참조하지만 참조 횟수를 증가시키지 않도록 해서 문제를 해결할 수 있다.

Strong, Weak, Unowned

  • 참조 횟수 추적 옵션을 설정할 수 있다.
    1. 강한 참조(Strong) : 인스턴스 할당 시 기본 옵션
    2. 약한 참조(Weak) : 인스턴스 할당 시 참조 횟수를 증가시키지 않음. 반드시 옵셔널 타입이어야 함.
    3. 미소유 참조(Unowned) : Weak와 같은 기능. 옵셔널이 아니어도 됨.
  • Instance를 약한 참조하는 변수는 instance가 메모리에서 해제되면 자동으로 nil을 할당한다. 아직 instance가 참조되고 있는데 참조 횟수가 0이라서 메모리에서 해제되는 상황이 발생할 수 있다. 이 때, 잘못된 메모리에 접근하는 오류가 발생할 가능성이 있으므로 nil을 할당해서 오류를 막는다.
  • 미소유 참조는 참조 횟수를 증가시키지 않지만 instance가 메모리에서 해제되기 전에 참조가 해제될 것임이 분명할 때 weak 대신 사용할 수 있다.

Resolve with Weak Reference

  • 위의 예제에서 ref2를 약한 참조 옵션으로 설정하면 sample2nil을 할당할 때 Sample2 instance의 참조 횟수가 0이 되어 참조 순환이 깨지고 양 쪽의 instance가 정상적으로 메모리에서 해제된다.
  • Sample2의 instance가 먼저 해제되고, 프로퍼티 ref1도 메모리에서 해제되면서 Sample1의 참조 횟수도 감소시킨다.
    ```swift
    class Sample1 {
    weak var sample2: Sample2? // Sample2를 약한 참조
    }

class Sample2 {
var sample1: Sample1?
}

var sample1: Sample1? = Sample1(value: 10) // Sample1 ref 1
var sample2: Sample2? = Sample2(value: 20) // Sample2 ref 1
sample1?.sample2 = sample2 // Sample2 ref 1
sample2?.sample1 = sample1 // Sample1 ref 2
sample1 = nil // Sample1 ref 1, Sample2 ref 1
sample2 = nil // Sample2 ref 0, Sample1 ref 0
/* print

  • Sample2 is deinitialized
  • Sample1 is deinitialized
    */
    ```

Problem2: Instance와 Closure간 상호 참조에 의한 Reference Cycle

  • 클로저도 참조 타입이기 때문에 인스턴스처럼 참조 횟수에 의해 순환 참조 문제가 발생할 수 있다.
  • 클로저가 클래스 안에서 다른 클래스 멤버를 사용할 때 self를 이용해 접근하는데, 이 떄 클로저의 값 획득에 의해 instance를 참조한다.
  • 클로저가 클래스의 프로퍼티로 할당되면 instance가 할당된 클로저를 참조한다. 프로퍼티로 할당된 클로저가 내부에서 self를 사용하면 상호 참조 관계가 성립된다.

image.png

class Sample {
  var value = 10
  lazy var closure: () -> Int = {    // Sample ref 1, Closure ref 1
    self.value = 20
    return self.value
  }
}

var sample: Sample? = Sample()    // Sample ref 2
print(sample?.closure())
sample = nil    // Sample ref 1

Resolve

  • 클로저 내부에서 클래스의 다른 멤버를 사용하면, 해당 멤버에 상관없이 언제든 클로저를 실행할 수 있도록 self를 참조해 두고 사용한다.(self를 여러번 사용해도 참조 횟수는 한 번만 증가한다.)
  • Problem1과 마찬가지로 self에 대해 참조 카운팅 옵션을 사용해서 문제를 해결할 수 있다.
  • 클로저에서는 참조를 획득할 목록을 지정할 수 있는데, 이 획득 목록에서 참조 방식을 설정할 수 있다. self의 참조 획득 방식을 약한 참조(weak)로 설정하면 클로저가 instance를 약한 참조하게 되고 상호 참조 관계를 깨트릴 수 있다.

image.png

class Sample {
  var value = 10
  lazy var closure: () -> Int = { [weak self] in    // Sample ref 0, Closure ref 1
    guard let `self` = self else {        // 약한 참조로 설정하면 옵셔널 타입으로 획득
      return -1
    }
    self.value = 20
    return self.value
  }
}

var sample: Sample? = Sample()    // Sample ref 1
print(sample?.closure())
sample = nil    // Sample ref 0
/* print */
// Sample is deinitialized