이전에 1편 정리햇던 건 전에 새싹 면접 보고 나서 넘 아쉬워가지구 ARC 정리했던 글인데 ㅋㅋㅋㅋㅋㅋ ㅠㅠ
r걍 웃기다 이제는 합격해서 쓰고잇네요... ^^
최근에 정리하려고 2편을 작성했던 거 같은데 걍 창 지우면서 날렸나봄
그것도 열심히 정리했던 거 같은데 아까버 죽겟다 ..
아무튼 1편은 여기!!
https://blog.naver.com/01unknown/223147022924
[Swift] ARC(Automatic Reference Counting) (1/2)
면접을 봤는데, 분명!!!! ㅠㅠ 알고 있는 지식이라고 생각했는데 긴장해서 어버버~ 하느라 대답을 잘 못한 ...
blog.naver.com
이전에 weak / unowned 참조 소개를 하고? 끝났던 것 같당.
weak 내용에 집중하느라 unowned를 거의 안 보고 끝낸듯?!
시작하기 전에 간단하게 ARC를 정리해 보자면, 다음과 같다.
우리는 옵젝씨 시절에는 MRC라고 불리는 ReferenceCount을 했다.
엥? 그게 뭔데?! 싶으면 1편을 읽구 오시길. ㅋ.ㅋ
암튼~~! RC, 즉 ReferenceCounting은 인스턴스가 참조하는 횟수를 스스로 세는 것을 말한다. 나라는 클래스를 어떤 변수가 가리키고 있는지 몇 명이 나를 가리키고 있는지 체크한 후에 나를 가리키는 변수가 아무도 없게 되면 스스로 메모리에서 사라지는 것이다~ (명확히는 사라진다기보다는 해제한다고 봐야겠지만~!!)
이전에는 C언어의 malloc과 free처럼 하나하나 retain과 release를 해야 했다고 보면 댄다.
어, 여기 변수 있네? 메모리 사용하네? 등록. 헐... 이제 안쓰네. 해제.
이때, 런타임 시점에서 이 코드가 기록됐다 보니 앱이 커지면 버벅이는 경우도 있었다.
근데 지금은? ARC라고 해서 컴파일러에서 컴파일 시점에서!! 자동으로 해 주게 댄다. 우리가 이전에 MRC라고 불렀던 것들을 대신 컴파일러가 대신 해 주고 있는 것이다.
여기서 그냥 완벽하게 대신 해 주고 있다면 배울 필요도 없었을 텐데 ㅇㅂㅇ 왜 우리는 ARC가 있음에도 각 클래스가 제대로 사라졌는지 한 번 더 체크하고 관리해 줘야 할까?
울 컴파일러는 못 잡는 게 있다.
그게 머냐면~~ 각 클래스간에 서로를 끝없이 가리키면서 일어나는 순환 참조이다! 순환참조가 무엇인지는 공식 문서에 있는 예시를 보면서 1편에서 자세하게 했던 것 같으니 참고할 것.
뭐, 그래두 간단하게 설명해 보자면 각각의 클래스 안의 변수에서 서로를 가리키고 있다면 본래 선언했던 변수가 nil이 되어도(두 변수를 가리키고 있지 않게 되어도 클래스 내의 변수에서 자기들끼리 가리키고 있기 때문에 변수가 없어진 것도 모르고 RC가 1로 유지되어 메모리에서 사라지지 않는다는 것이다.
그들은 영영... ... 누가 자기를 가리키고 있는 줄 알고 쓰이지도 않으면서 메모리에 남아있는 것이다.
이러한 현상을 메모리 누수, Memory Leak라고 하고 이미지로 보면 서로밖에 없는 둘은 아래와 같다.
암튼!
우리는 순환참조를 막기 위해서 weak과 unowned를 사용한다고 했다.
weak과 unowned는 비슷하지만 다른 점이 명확하다.
먼저 동일한 점은 weak과 unowned를 사용하게 된다면 인스턴스 참조가 발생하더라도 RC를 카운팅하지 않는다!
그럼 차이점은 몰까?
가리키고 있는 변수를 바라보는 관점이 다르다.
말만 들었을 때는 엥? 싶은데 정말 들어 보면 글쿤. 싶다.
weak의 경우에는 가리키고 있는 변수가 런타임에서도 nil이 될 가능성을 생각한다. 그렇기 때문에 항상 변수로 선언해야 하며, nil이 될 가능성이 있다면 Optional 타입으로 선언해 주어야 하기에 항상!!! 옵셔널 타입으로 선언해 주어야 한다.
이 친구는 가리키고 있는 변수가 nil일 수도 있다는 걸 항상 생각하고 있다구 했다. 언제든 사라질 수 있다고 생각했기에 진짜!! 해당 클래스가 메모리에서 해제된다면 헐~ 진짜 없어졌네 하고 자동으로 해당 값에 nil을 넣어준다.
weak을 사용할 때는 메모리를 해제하는 순서가 영향을 미친다. 순환을 참조하고 있는 두 인스턴스가 메모리에서 동시에 해제된다면 상관없지만, 특정 인스턴스에 대한 참조만 해제되는 경우 다른 인스턴스가 메모리에서 해제되지 않을 수 있다!!
헐... 몬 소리야 싶다 기달 예시를 가지고 오겠다
class Guild {
var name: String // 길드 이름
weak var owner: User?
init(name: String) {
self.name = name
print("Guild Init")
}
deinit {
print("Guild Deinit")
}
}
class User {
var nickname: String
var guild: Guild?
init(nickname: String) {
self.nickname = nickname
print("User Init")
}
deinit {
print("User Deinit")
}
}
서로를 참조하고 있는 Guild와 User라는 Class를 만들어 주었다.
이때, Guild는 User를 약하게 참조하고 있다!
var nickname: User? = User(nickname: "행복한해파리") // RC: +1
var guild: Guild? = Guild(name: "찌릿") // RC: +1
nickname?.guild = guild
guild?.owner = nickname
이후, 서로가 서로를 참조하도록 해 줬다.
흠. 만약에 길드가 없어졌다고 해 보자
guild = nil
그런데? 결과적으로는 아무것도 해제되지 않았다고 뜬다.
User Init
Guild Init
이렇게 뜬다는 말이다 ... ...
왜 그럴까?
가장 처음 정의한 그림은 다음과 같다.
이후 Guild를 가리키는 변수가 사라지면...
여전히 User가 Guild를 가리키고 있어 둘 다 RC가 1로 유지되고 있으므로 해제되지 않는 것이다!
그럼 weak은 어느 때 사용할까?
다른 예시도 있지만, delegate를 사용하는 예시를 들고 싶다 ... ... ^_^
protocol PassTextDelegate {
func receiveText(text: String)
}
class PickerViewController: BaseViewController {
let list = ["성별을 선택해 주세요", "Male", "Female", "They / Them"]
let pickerView = {
let picker = UIPickerView()
return picker
}()
var delegate: PassTextDelegate?
override func configureView() {
super.configureView()
view.addSubview(pickerView)
title = "Gender"
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "저장", style: .plain, target: self, action: #selector(saveButtonClicked))
pickerView.delegate = self
pickerView.dataSource = self
}
@objc func saveButtonClicked() {
navigationController?.popViewController(animated: true)
}
override func setConstraints() {
pickerView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide).inset(10)
make.horizontalEdges.equalTo(view.safeAreaLayoutGuide)
}
}
}
class ProfileViewController: BaseViewController {
let mainView = ProfileView()
var settingList = SettingList().user
override func loadView() {
self.view = mainView
}
... ...
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let setting = settingList[indexPath.row].name
if setting == .Gender {
let vc = PickerViewController()
// delegate 연결 후 실행은 내가 하겠음!!
vc.delegate = self
navigationController?.pushViewController(vc, animated: true)
tableView.reloadRows(at: [indexPath], with: .automatic)
} else {
let vc = EditingViewController()
vc.setting = setting
vc.completionHandler = { text in
self.settingList[indexPath.row].user = text
tableView.reloadRows(at: [indexPath], with: .none)
}
navigationController?.pushViewController(vc, animated: true)
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
}
extension ProfileViewController: PassTextDelegate {
func receiveText(text: String) {
for index in settingList.indices {
let setting = settingList[index]
if setting.name == .Gender {
settingList[index].user = text
mainView.tableView.reloadData()
return
}
}
}
}
이렇다.
이때, PickerVC에서 weak으로 delegate를 선언하지 않을 경우 메모리에서 인스턴스가 제거되지 않는다! (아까 봤던 것처럼) 따라서, weak으로 선언해 주어야 한다.
다만, 이때 protocol을 weak으로 선언하게 되면 기존의 프로토콜은 struct, enum, class 모두 채택할 수 있기 때문에 참조가 가능한 클래스만 가리킨다는 뜻에 AnyObject를 해당 프로토콜에 채택해 주어야 한다.
아까 weak는 내가 가리키고 있는 변수가 nil일 가능성도 생각한다고 했다. unowned는 전혀 아니다. 내가 가리키고 있는 변수를 완전 믿고 있다. 내가가리키고잇는변수는절대나보다일찍사라질리없다고 생각한다. 따라서! optional로 선언되지 않는다. 왜냠. 당연히. nil일 리가 없으니까!!! 미소유참조라는 이름과 달리 해당 변수를 진득하게 소유하고싶어한다...
근데 unowned를 대체 언제 쓸 수 있을까???
내가 다른 예시를 들기보다, 공식 문서에서는 unowned의 예시를 Customer Class와 CreditCard Calss로 들고 있는데, 정말 딱 맞는 예시 같다. 그러니까....
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit {
print("Goodbye, \(name)!")
}
}
class CreditCard {
let number: Int
unowned let customer: Customer
init(number: Int, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Goodbye, \(number)!")
}
}
이런 친구들이 있다고 하자.
var yeoni: Customer? = Customer(name: "yeoni")
yeoni!.card = CreditCard(number: 12345678, customer: yeoni!)
여니라는 유저 하나를 추가하고, 그 카드를 크래딧카드로 추가해 줬다.
해당 그림은 다음과 같다!
CreditCard Class는 unowned로 Customer를 참조하고 있고, Customer는 강하게 CreditCard를 참조하고 있어 각각의 RC는 1이다.
이어서,
yeoni = nil
이 되면...
Customer를 가리키고 있던 변수가 사라지고, Customer Class 자체도 RC가 0이 되어 사라지게 된다.
이후!! CreditCard Class를 가리키고 있던 건 Customer 하나였으니 뒤이어 CreditCard의 RC도 0이 된다.
헉. 이러면...
그렇다. 둘 다 사라지게 된다!
다만, 서로 상호참조 하고 있을 때... ... 만약 한쪽이 먼저 사라지면?
unowned는 한쪽이 사라질 걸 예상하지 못했으므로 해당 값은 클래스의 빈 메모리값을 가리키고 있다. (해당 클래스의 본래 주솟값!!)
그렇기 때문에 그 변수에 접근하게 된다면 에러를 뱉고 죽는다.
근데 여기까지가 1편까지 내용 아녓나?
암튼. 복습겸 서론이었다.
순환 참조와 비슷하지만 클로저에서 일어나는 Capture 현상을 이야기하고자 한다.
이 현상은 중첩 함수를 사용할 때 발생한다. 그러니까, 함수 안에 클로저를 사용하는 경우에 말이다!
클로저는 값을 캡쳐할 때, 캡쳐할 값이 값 타입이든 참조 타입이든 관계 없이 해당 변수를 캡쳐하게 된다. 왜? 이후에 나는 실행되어야 하는데 해당 변수가 없어졌을지도 모르니까!
캡쳐라는 것은 말 그대로 사진찍듯이 그 값을 저장하는 것이기 때문에 외부에서 값을 변경해도 그대로 유지된다.
먼저 일반적인 함수를 보자!
func calculate(number: Int) -> Int {
var sum = 0
func square(num: Int) -> Int {
sum += (num * num)
return sum
}
let result = square(num: number)
return result
}
calculate(number: 10) // 100
calculate(number: 20) // 400
calculate(number: 30) // 900
반면, 내부에 변수가 캡쳐된 함수를 보자.
func calculateFunc() -> ((Int) -> Int) {
var sum = 0
func square(num: Int) -> Int {
sum += (num * num)
return sum
}
return square
}
var squareFunc = calculateFunc()
squareFunc(10) // 100
squareFunc(20) // 500
squareFunc(30) // 1400
이렇게 ...
본래 나와야 할 값이 나오지 않고 그 위에 차곡차곡 쌓이게 된다.
내부 함수 square에 sum 값이 필수로 필요하다 보니 내부에서 sum 값을 임의로 저장하여 해당 값을 계속 사용하기 때문에 이러한 현상이 일어난다!!
긍데 뜬금없이 왜 클로저? 라는 말이 나올 수 있는데... ...
처음에 봤던 weak과 unowned를 captureList에 활용하여 클로저의 캡쳐를 방지할 수 있다.
비슷한 예를 보자!!
class HTMLElement {
let name: String
let text: String?
lazy var article: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
내부의 클로저 변수를 unowned self로 선언했다!
만약에 unowned로 선언하지 않고 self로 선언했다면 이후에 paragraph를 해제해도 메모리에서 해제됐다는 문구가 뜨지 않을 것이다.
왜냐하면 내부에서 변수를 캡쳐하고 있어 내부 변수의 RC가 0으로 떨어지지 않으니까~~!!
몬소리지? 싶다면 그림으로 보자.
그림으로 보자면 다음과 같다!!
원래 unowned로 선언되지 않았다면 클로저가 클래스를 강하게!! 참조하고 있어 변수가 클래스를 참조하고 있지 않아도 RC가 1이 되기 때문에 그대로 살아있을 것이다.
paragraph = nil
// Prints "p is being deinitialized"
짠~
변수에 nil을 넣으니 그대로 해제되었당.
unowned 말고도 클래스의 구조에 따라서 weak으로도 선언하여 사용할 수 있을 것 같다 ^___^
캡쳐 리스트에 관련해서는 조금 더 알아보고 공부해야 할 것 같음
캡쳐 개념은 명확히 알고 있는데 캡쳐 리스트로 활용은 못하는 느낌이라구 할까?!?!!
아무튼
드디어 미뤘던 정리 끝~~~~!!! ^____^ b
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/
'iOS' 카테고리의 다른 글
[Swift] 정규표현식 (2) | 2023.11.10 |
---|---|
[iOS] Advances in UICollectionVIew (0) | 2023.09.23 |
[Swift] Singleton Pattern은 왜 class로만 만들까? (0) | 2023.08.13 |
[SeSAC] August 9, 2023 (0) | 2023.08.09 |
[Swift] instance / Type (0) | 2023.08.01 |