Weak Self란 강한 참조로 인해 발생하는 메모리 누수를 방지할 수 있도록 합니다
[weak self]을 사용하지 전
바로 코드로 설명하겠습니다.
먼저 2개의 뷰를 만듭니다. 하나는 다른 화면으로 이동하는 뷰이고 다른 하나는 이동된 2번째 뷰입니다. 이때 overlay로 AppStorage에 count 키값으로 저장된 값을 우측 상단에 표시합니다.
struct ContentView: View {
@AppStorage("count") var count: Int?
init() {
count = 0
}
var body: some View {
NavigationStack {
NavigationLink {
WeakSelfSecondScreen()
.navigationTitle("Screen 1")
} label: {
Text("Navigate")
}
}
.overlay(alignment: .topTrailing) {
Text("\(count ?? 0)")
.font(.largeTitle)
.padding()
.background(Color.green)
}
}
}
다음으로는 2번째 뷰로 WeakSelfSecondScreenViewModel의 data를 화면에 출력합니다. 아래에서 나오겠지만 @StateObject로 프로퍼티를 만들게 되면 WeakSelfSecondScreenViewModel에서 init 이 수행됩니다. 그리고 뷰가 사라질 때 deinit 이 수행됩니다.
struct WeakSelfSecondScreen: View {
@StateObject var vm = WeakSelfSecondScreenViewModel()
var body: some View {
VStack {
Text("Second View")
.font(.largeTitle)
.foregroundStyle(.red)
if let data = vm.data {
Text(data)
}
}
}
}
다음은 WeakSelfSecondScreenViewModel 클래스로 클래스가 만들어질 때 count 키값의 UserDefault의 값이 1 증가합니다. 만일 클래스가 없어질 때에는 deinit이 수행되어 1이 감소합니다.
class WeakSelfSecondScreenViewModel: ObservableObject {
@Published var data: String? = nil
init() {
print("INITIALIZE NOW")
let currentCount = UserDefaults.standard.integer(forKey: "count")
UserDefaults.standard.set(currentCount + 1, forKey: "count")
getData()
}
deinit {
print("DEINITIALIZE NOW")
let currentCount = UserDefaults.standard.integer(forKey: "count")
UserDefaults.standard.set(currentCount - 1, forKey: "count")
}
func getData() {
self.data = "NEW DATA!!!"
}
}
[weak self]를 사용하지 않았을 때 발생하는 문제점
self.data를 사용하게 되면 data가 WeakSelfSecondScreen 클래스와 강한 참조로 엮기게 됩니다.
self는 WeakSelfSecondScreen을 의미
self.data로 설정하면 data를 사용하기 위해서는 반드시 WeakSelfSecondScreen 이 살아있어야 함을 의미합니다.
만일 인터넷으로부터 엄청 많은 데이터를 받아오는 작업을 수행함과 동시에 유저가 화면에서 특정 동작을 하고 있을 수 있습니다, 이때 데이터를 받아오는 동안에 [weak self] 를 사용하지 않으면 값을 받는 변수 하나 때문에 사용하지 않는 클래스는 살아있게 되고 이는 메모리 누수로 이어지게 되어 성능에 영향을 끼칩니다.
다음은 인터넷에서 대용량 데이터를 받아오는 작업 상황을 비슷하게 구현하여 100초 뒤에 작업이 수행되도록 하는 코드를 만들었습니다.
func getData() {
DispatchQueue.main.asyncAfter(deadline: .now() + 100) {
self.data = "NEW DATA!!!"
}
}
위에서 말했듯이 [weak self]를 사용하지 않고 self.data 를 사용하여 함수를 실행하여 self.data에 값을 넣게 되는 경우 self.data 작업이 완료되기 전까지 강한 참조를 하기 때문에 아래처럼 동작하여 init()으로 증가시킨 이후 WeakSelfSecondScreenViewModel 클래스가 해제되지 않으므로 deinit이 실행되지 않아 count의 숫자가 감소되지 않는 것을 확인할 수 있습니다.
[weak self]로 문제 해결 방법
하지만 [weak self]를 사용하여 강한 참조를 없앤다면 해결됩니다. self? 는 작업을 하는 동안에 클래스가 반드시 살아있지 않아도 된다는 것을 의미합니다. 즉 강한 참조를 없애게 되어 뷰를 이동하였을 때 성공적으로 WeakSelfSecondScreen 클래스를 해제하여 deinit이 수행됩니다.
DispatchQueue.main.asyncAfter(deadline: .now() + 100) {[weak self] in
self?.data = "NEW DATA!!!"
}
전체 코드
import SwiftUI
struct ContentView: View {
@AppStorage("count") var count: Int?
init() {
print("??")
count = 0
}
var body: some View {
NavigationStack {
NavigationLink {
WeakSelfSecondScreen()
.navigationTitle("Screen 1")
} label: {
Text("Navigate")
}
}
.overlay(alignment: .topTrailing) {
Text("\(count ?? 0)")
.font(.largeTitle)
.padding()
.background(Color.green)
}
}
}
struct WeakSelfSecondScreen: View {
// @Environment(WeakSelfSecondScreenViewModel.self) var vm
@StateObject var vm = WeakSelfSecondScreenViewModel()
var body: some View {
VStack {
Text("Second View")
.font(.largeTitle)
.foregroundStyle(.red)
if let data = vm.data {
Text(data)
}
}
}
}
class WeakSelfSecondScreenViewModel: ObservableObject {
@Published var data: String? = nil
init() {
print("INITIALIZE NOW")
let currentCount = UserDefaults.standard.integer(forKey: "count")
UserDefaults.standard.set(currentCount + 1, forKey: "count")
getData()
}
deinit {
print("DEINITIALIZE NOW")
let currentCount = UserDefaults.standard.integer(forKey: "count")
UserDefaults.standard.set(currentCount - 1, forKey: "count")
}
func getData() {
// self.data로 설정하게 되면 WeakSelfSecondScreen 클래스와 강한 참조로 엮이게 된다
// self.data로 설정하면 data를 사용하기 위해서는 반드시 WeakSelfSecondViewModel이 살아있어야 함을 의미합니다.
// 만일 인터넷으로부터 엄청 많은 데이터를 받아야 하는 상황이 생길 때 유저가 화면에서 특정 동작을 하고 있을 수 있습니다.
// 만일 데이터를 가져오는 동안 유저가 뷰를 돌아다니거나 가져온 데이터가 별로 필요가 없어질때 데이터가 도착했을 수 있습니다.
// DispatchQueue.global().async {
// self.data = "NEW DATA!!!"
// }
// 아래처럼 설정하게 되면 뷰가 사라질 때 deinit이 실행이 안됩니다.
// 실행이 안되는 이유는 강한 참조를 하고 있기 때문에 self.data 작업이 완료되기 전까지 deinit이 실행되지 않습니다.
// self.data를 사용하려면 WeakSelfSecondScreenViewModel이 반드시 필요하기 때문에
// 만약에 8개의 init으로 클래스가 만들어 졌을 경우 8개의 클래스가 작업이 끝나기 전까지 모두 백그라운드에 메모리를 차지하기 때문에
// 효율적이지 못하고 결국에는 앱 성능에 영향을 미칠 것이다.
// DispatchQueue.main.asyncAfter(deadline: .now() + 100) {
// self.data = "NEW DATA!!!"
// }
// self? 는 작업을 하는 동안에 WeakSelfSecondScreenViewModel 이 반드시 살아있어야 하지는 않는다는 것을 의미합니다
// 데이터를 다운받는 동작이 오래걸리는 비동기 코드를 사용할 때는 weak self를 사용하는 것이 메모리 누수를 방지할 수 있습니다.
DispatchQueue.main.asyncAfter(deadline: .now() + 100) {[weak self] in
self?.data = "NEW DATA!!!"
}
}
}
#Preview {
ContentView()
}
'SwiftUI' 카테고리의 다른 글
JSON Data (0) | 2024.04.08 |
---|---|
Typelias (0) | 2024.04.08 |
CoreBluetooth로 아두이노 불 켜기 (1) | 2024.04.07 |
Background Threads, Queues (0) | 2024.04.07 |
버튼 커스텀화하기, Custom ButtonStyle (0) | 2024.04.07 |