ARC

인생노잼시기·2021년 6월 11일
0

🦅 스위프트 문법

목록 보기
13/13

메모리 관리 관점에서
strong과 weak를 사용하는 이유는?
weak를 사용하면 객체가 사라졌을 때 자동으로 메모리가 해제되기 때문이다

class Child {
	weak var age = "17"
}
var john = Child()
//이후에 john을 사용하지 않으면 age 변수도 메모리에서 해제된다

Automatic Reference Counting, or ARC

메모리 관리
힙 객체의 생명주기가 언제 끝나는 것인가

메모리 누수 예시

💡실행 결과
💡User John was initialized
User 객체가 범위를 벗어나지 않기 때문에 메모리에서 해제되지 않는다
view controller that contains this object never goes out of scope, the object is never removed from memory.


import UIKit

class MainViewController: UIViewController {
  
  let user = User(name: "John")
  
  override func viewDidLoad() {
    super.viewDidLoad()
  }
}

class User {
  let name: String
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }
  
  deinit {
    print("Deallocation user named: \(name)")
  }
}

메모리 누수 해결 - scope 지정

💡실행 결과
💡User John was initialized
💡Deallocation user named: John
User객체를 메소드에 넣고 viewDidLoad()에서 호출하면
메소드 내부로 범위가 정해졌기 때문에
deallocation이 발생한다

import UIKit

class MainViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    runScenario()
  }
  
  func runScenario() {
    let user = User(name: "John")
  }
  
}

class User {
  let name: String
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }
  
  deinit {
    print("Deallocation user named: \(name)")
  }
}

객체의 생명주기

  1. Allocation: Takes memory from a stack or heap.
  2. Initialization: init
  3. Usage.
  4. Deinitialization: deinit
  5. Deallocation: Returns memory to a stack or heap.

memory leak 예시

  • strong reference cycle

variable1과 variable2가 더 이상 쓰이지 않고
object1과 object2이 더 이상 필요하지 않지만
object1과 object2가 strong하게 참조하기 때문에
ARC가 0이 될 수 없고 메모리 누수가 발생한다


weak 예시

weak var owner: User?

import UIKit

class MainViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    runScenario()
  }
  
  func runScenario() {
    let user = User(name: "John")
    let iPhone = Phone(model: "iPhone Xs")
    user.add(phone: iPhone)
  }
  
}

class User {
  let name: String
  private (set) var phones: [Phone] = []
  
  func add(phone: Phone) {
    phones.append(phone)
    phone.owner = self
  }
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }
  
  deinit {
    print("Deallocation user named: \(name)")
  }
}

class Phone {
  let model: String
  weak var owner: User? // Phone이 User를 약한 참조한다
  
  init(model: String) {
    self.model = model
    print("Phone \(model) was initialized")
  }
  
  deinit {
    print("Deallocation phone named: \(model)")
  }
}
결과
User John was initialized
Phone iPhone Xs was initialized
Deallocation user named: John
Deallocation phone named: iPhone Xs

unowned vs. weak

weak는 항상 옵셔널이고, ARC에 의해 nil이 될 수 있어 var과 함께 쓰여야 한다.
unowned는 ARC에 의해 카운트가 증가하지 않는다. 절대 옵셔널이 될 수 없다. 만약 deinitialized된 객체에 접근하면 nil이 된 옵셔널 강제 추출하는 것에 상응해 런타임 에러가 발생된다.

unowned 사용 전

import UIKit

class MainViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    runScenario()
  }
  
  func runScenario() {
    let user = User(name: "John")
    let iPhone = Phone(model: "iPhone Xs")
    user.add(phone: iPhone)
    
    let subscription = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "3145678", user: user)
    iPhone.provision(carrierSubscription: subscription) // unowned
  }
  
  
}

class User {
  let name: String
  
  var subscriptions: [CarrierSubscription] = [] // [unowned] CarrierSubscription의 배열 저장
  
  private (set) var phones: [Phone] = []
  
  func add(phone: Phone) {
    phones.append(phone)
    phone.owner = self
  }
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }
  
  deinit {
    print("Deallocation user named: \(name)")
  }
}

class Phone {
  let model: String
  weak var owner: User? // [weak] Phone이 User를 약한 참조한다 -> User가 없으면 Phone도 deallocate
  
  var carrierSubscription: CarrierSubscription? // unowned

  func provision(carrierSubscription: CarrierSubscription) {  // unowned
    self.carrierSubscription = carrierSubscription
  }

  func decommission() { // unowned
    carrierSubscription = nil
  }
  
  init(model: String) {
    self.model = model
    print("Phone \(model) was initialized")
  }
  
  deinit {
    print("Deallocation phone named: \(model)")
  }
}

class CarrierSubscription {
  let name: String
  let countryCode: String
  let number: String
  let user: User  // User 객체
  
  init(name: String, countryCode: String, number: String, user: User) {
    self.name = name
    self.countryCode = countryCode
    self.number = number
    self.user = user
    
    user.subscriptions.append(self) // [unowned] user객체의 subscriptions배열에 CarrierSubscription 추가
    
    print("CarrierSubscription \(name) is initialized")
  }

  deinit {
    print("Deallocating CarrierSubscription named: \(name)")
  }
}
결과
User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized

unowned 사용 후
unowned let user: User

import UIKit

class MainViewController: UIViewController {

  override func viewDidLoad() {
    super.viewDidLoad()
    runScenario()
  }
  
  func runScenario() {
    let user = User(name: "John")
    let iPhone = Phone(model: "iPhone Xs")
    user.add(phone: iPhone)
    
    let subscription = CarrierSubscription(name: "TelBel", countryCode: "0032", number: "3145678", user: user)
    iPhone.provision(carrierSubscription: subscription) // unowned
  }
  
  
}

class User {
  let name: String
  
  var subscriptions: [CarrierSubscription] = [] // [unowned] CarrierSubscription의 배열 저장
  
  private (set) var phones: [Phone] = []
  
  func add(phone: Phone) {
    phones.append(phone)
    phone.owner = self
  }
  
  init(name: String) {
    self.name = name
    print("User \(name) was initialized")
  }
  
  deinit {
    print("Deallocation user named: \(name)")
  }
}

class Phone {
  let model: String
  weak var owner: User? // [weak] Phone이 User를 약한 참조한다 -> User가 없으면 Phone도 deallocate
  
  var carrierSubscription: CarrierSubscription? // unowned

  func provision(carrierSubscription: CarrierSubscription) {  // unowned
    self.carrierSubscription = carrierSubscription
  }

  func decommission() { // unowned
    carrierSubscription = nil
  }
  
  init(model: String) {
    self.model = model
    print("Phone \(model) was initialized")
  }
  
  deinit {
    print("Deallocation phone named: \(model)")
  }
}

class CarrierSubscription {
  let name: String
  let countryCode: String
  let number: String
  unowned let user: User  // [unowned] User 객체
  
  init(name: String, countryCode: String, number: String, user: User) {
    self.name = name
    self.countryCode = countryCode
    self.number = number
    self.user = user
    
    user.subscriptions.append(self) // [unowned] user객체의 subscriptions배열에 CarrierSubscription 추가
    
    print("CarrierSubscription \(name) is initialized")
  }

  deinit {
    print("Deallocating CarrierSubscription named: \(name)")
  }
}

결과
User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized
Deallocation user named: John
Deallocation phone named: iPhone Xs
Deallocating CarrierSubscription named: TelBel

Closures capture


객체가 클로저를 참조하고, 클로저가 self(객체)를 strong 참조한다

CarrierSubscription 객체의 내부
self.countryCode와 self.number는 CarrierSubscription이 초기화된 후에 사용할 수 있기 때문에 lazy 키워드를 써준다

lazy var completePhoneNumber: () -> String = {
  self.countryCode + " " + self.number
}
print(subscription.completePhoneNumber)

함수를 실행하면 결과는 아래와 같다
CarrierSubscription이 메모리에서 해제되지 않는다.

User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized
(Function)
Deallocation user named: John
Deallocation phone named: iPhone Xs

해결방법 - capture list

var x = 5
var y = 5

let someClosure = { [x] in
  print("\(x), \(y)")
}
x = 6
y = 6

someClosure()        // Prints 5, 6
print("\(x), \(y)")  // Prints 6, 6

x는 closure capture list에 있어서 captured by value된다.
y는 capture list에 없어서 captured by reference된다.
y는 클로저 안에 있더라도 값이 변경될 수 있다.

weak나 unowned가 사용될 수 있는데
CarrierSubscription의 인스턴스가 사라지면 클로저가 없어질 수 있기 때문에 unowned가 적절하다.

Capture lists come in handy for defining a weak or unowned relationship between objects used in a closure. In this case, unowned is a good fit, since the closure cannot exist if the instance of CarrierSubscription has gone away.

self를 unowned reference로 capture한다.
새로운 self 변수를 생성하고, 원래 self의 의미를 shadows하고, 생성된 self는 클로저 범위 안에서만 존재한다.

lazy var completePhoneNumber: () -> String = { [unowned self] in
  return self.countryCode + " " + self.number
}

self와 completePhoneNumber는 unowned 관계이다.
만약 클로저에서 참조받는 객체(여기에서는 CarrierSubscription)가 deallocate하지 않으면 unowned를 사용할 수 있다. 하지만, deallocate가 발생하면 문제가 발생한다.

결과: CarrierSubscription도 해제되었다.
User John was initialized
Phone iPhone Xs was initialized
CarrierSubscription TelBel is initialized
Deallocation user named: John
Deallocation phone named: iPhone Xs
Deallocating CarrierSubscription named: TelBel

긴 버전: 클로저의 바깥 범위에서 self는 원래의 의미를 갖는다.

var closure = { [unowned newID = self] in
  // Use unowned newID here...
}

newIDselfunowned한 복사본이다.

Using Unowned With Care

Fatal error: Attempted to read an unowned reference but object 0x600000f0de30 was already deallocated2019-02-24 12:29:40.744248-0600 Cycles[33489:5926466] Fatal error: Attempted to read an unowned reference but object 0x600000f0de30 was already deallocated

클로저는 self.who 가 유효하길 바라지만 실행시키면서 mermaiddo 블록의 범위를 벗어났기 때문에 deallocated 되어 오류가 발생한다.

class WWDCGreeting {
  let who: String
  
  init(who: String) {
    self.who = who
  }

  lazy var greetingMaker: () -> String = { [unowned self] in
    return "Hello \(self.who)."
  }
}
override func viewDidLoad() {
    super.viewDidLoad()
    runScenario()
}

func runScenario() {
    
    let greetingMaker: () -> String
    
    do {
      let mermaid = WWDCGreeting(who: "caffeinated mermaid")
      greetingMaker = mermaid.greetingMaker
    }
    
    print(greetingMaker())	//TRAP
  }

Disarming the Trap 해결방안

weak로 변환하고 self는 guard 처리 해주기

lazy var greetingMaker: () -> String = { [weak self] in
  guard let self = self else {
    return "No greeting available."
  }
  return "Hello \(self.who)."
}

//결과
//Hello nil.

self 가 클로저 밖에서도 사는 것이 명확하지 않을 때,
[weak self] and guard let self = self else { return } 구문을 사용해 객체의 생명주기를 연장시키는 것이 [unowned self]를 사용하는 것보다 낫다.

Xcode에서 메모리 누수 확인하기


Contact와 Number가 서로 참조하고 있는 상황에서
ContactNumber 없이도 존재할 수 있지만
NumberContact 없이 존재할 수 없다.

해결방안
1. ContactNumber를 weak 관계로 바꾼다.
2. NumberContact에 의해 unowned 되도록 바꾼다.
컨벤션에 따르면 parent 객체가 child 객체를 strong하게 hold하는 것을 권장하기 때문에 Number가 Contact에 의해 unowned되도록 만들었다.

class Number {
  unowned var contact: Contact
  // Other code...
}

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmPractical.html

참조타입과 값타입

Self reference is not an issue for classes.
클래스에서 self 참조는 괜찮지만 Not deallocated된다.

class Person {
  var name: String
  var friends: [Person] = []
  init(name: String) {
    self.name = name
    print("New person instance: \(name)")
  }

  deinit {
    print("Person instance \(name) is being deallocated")
  }
}
do {
  let ernie = Person(name: "Ernie")
  let bert = Person(name: "Bert")
  
  ernie.friends.append(bert) // Not deallocated
  bert.friends.append(ernie) // Not deallocated
}

해결책
friends배열이 unowned되도록 만든다
Make the friends array unowned and Xcode will show an error: unowned only applies to class types.

class Unowned<T: AnyObject> {
  unowned var value: T
  init (_ value: T) {
    self.value = value
  }
}
var friends: [Unowned<Person>] = []
do {
  let ernie = Person(name: "Ernie")
  let bert = Person(name: "Bert")
  
  ernie.friends.append(Unowned(bert))	//deallocate
  bert.friends.append(Unowned(ernie))	//deallocate
}

friends 배열은 더 이상 Person의 콜렉션이 아니다.
Person의 wrapper인 Unowned객체의 콜렉션이라서 아래와 같이 접근한다.
To access the Person object within Unowned, use the value property, like so:

let firstFriend = bert.friends.first?.value // get ernie 

출처
https://www.raywenderlich.com/966538-arc-and-memory-management-in-swift

profile
인생노잼

2개의 댓글