SwiftUI Memory Leak -> Check Retain Cycle

YongJunCha·2022년 1월 25일
0

swift

목록 보기
14/18
post-thumbnail

Memory Problem

  • 어느샌가 앱을 테스트하면 메모리가 해제되는 것이 거의 없고 메모리가 쌓여만 갔다.
    Combine으로 하는 Observing이 계속 쌓이는 것일까? 하는 생각을하고 ViewModel에
    deinit을 달고 print를 전부 찍어봤다.

Reason Why Memory Problem Happend?

우리 앱에서 메모리 관리에서 가장 중요한 것이 deinit이다

 deinit {
        subscription.removeAll()
    }

우리는 deinit에서 옵저빙을 해제시켜준다.
그렇다면 모델에서 deinit이 불리지 않았다는 것은
계속 Init으로 쌓이기만하고 메모리는 해제되지 않았다는 것이다.

ARC?

ARC란 Auto Refernece Counting라는 개념인데 Reference Count가 0이 되면 자연스럽게 메모리에서 해제시켜주는 Swift의 메모리 관리 모델이다.

View가 Disappear 되면서 Reference Count는 0이 되었을텐데 메모리가 계속 쌓인다는 것으로 볼 때
의심할 수 있는 것은 한 가지다. 'Retain Cycle'

Retain Cycle?

강한 참조가 일어나는 부분이 어디일까 하고 우리 앱에 전체에 [weak self]와 [unowned self]를
검색해보았다. 놀랍게도 그 어떤 뷰모델도 weak self와 unowned self를 사용한 곳이 없었다.
Combine을 사용한 부분에서 @published를 참조하고 있고, sink 있는 곳에서 fix가 뜨니깐
무의식적으로 self를 붙이라니깐 fix 버튼을 눌러 모든 곳에 강한 참조를 발생시킨 것이다.

    /// Mark: - setObserver
    /// feature : 유저가 4자리를 입력하면 비밀번호가 맞는지 여부를 체크한다.
    /// 비밀번호가 맞으면 appScreenLockMode의 상태를 바꿔 잠금을 해제하고
    /// 비밀번호가 틀리면 비밀번호를 리셋하고 비밀번호가 흔들리는 shake애니메이션을 실행시킨다.
    func setObserver(appScreenLockOffAction: @escaping () -> Void) {
        let appScreenLockNumberSubscriber = $plainText
            .removeDuplicates()
            .subscribe(on: DispatchQueue.main)
            .sink { [unowned self] value in
                // 비밀번호 4자리를 입력했을 경우
                if value.count == 4 {
                    // 비밀번호를 해쉬한다.
                    let hashedText = value.sha256()
                    print("APP SCREEN LOCK :: PLAINTEXT HASH & PASSWORD CHECK")
                    // 해쉬된 값이 저장되어있는 비밀번호와 같을 경우
                    if hashedText == getUD(key: self.APP_SCREEN_LOCK_PASSWORD) {
                        DispatchQueue.main.async {
                            print("APP SCREEN LOCK :: USER ENTER CORRECT PASSWORD")
                            // Nfilter Data 초기화
                            isNfilterNeedClear.toggle()
                            // 비밀번호 상태 값 초기화
                            resetPasswordString()
                            print("APP SCREEN LOCK :: PROGRAMMATICALLY OPERATE PRESS KEY PAD RELOAD BUTTON ")
                            appScreenLockMode = .OffScreenLock
                            appScreenLockMode?.getMessageAppScreenLockMode()
                            appScreenLockOffAction()
                        }
                    }
                    // 해쉬된 값이 저장되어있는 비밀번호와 다른 경우
                    else {
                        DispatchQueue.main.async {
                            print("APP SCREEN LOCK :: USER ENTER WRONG PASSWORD")
                            // 비밀번호가 틀렸을 경우 Shake Animation 효과
                            isWrongPassword = true
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                                isWrongPassword = false
                            }
                            // Nfilter Data 초기화
                            isNfilterNeedClear.toggle()
                            // 비밀번호 상태 값 초기화
                            resetPasswordString()
                            print("APP SCREEN LOCK :: PROGRAMMATICALLY OPERATE PRESS KEY PAD RELOAD BUTTON ")
                        }
                    }
                }
            }
            .store(in: &subscription)
    }

상기의 테스트 코드에서 [unowned self]를 적용하자 @stateObject의 deinit이 불렸고
Observing을 해제했다.

Review

  • fix가 뜬다고 self 붙이라고 뜨면 무지성으로 self를 누르면 안 된다.
  • weak self의 경우 self를 gurad let으로 한 번 확인해 준 후에 로직을 짜는 편이 좋고
  • unowned self의 경우 뷰나 오브젝트가 확실하게 존재할 때 써야 한다.
  • 프로젝트 전체에 적용해서 Refactoring에 들어가기 전에 Self, unowned self, weak self, ARC에 대해서 세미나를 진행한 후 작업에 들어가야겠다.
  • 세미나를 진행하기 전에 블로그에 정리자료를 올려야겠다.
  • 이렇게라도 발견하고, 개선할 수 있어서 좋은 것 같다.

0개의 댓글