Skip to content

RecordingViewController 메모리 해제 문제

MINRYUL edited this page Nov 11, 2021 · 15 revisions

RecordingViewController가 메모리에서 해제되지 않는 문제

저자: S009_김민창


시나리오

  1. 측정 화면에 들어가서 기록 종료 버튼을 누른다.
  2. 측정 기록 제목을 입력하는 Modal이 올라온다.
  3. 제목을 설정하고 입력 완료 버튼을 누른다.
  4. RecordingTitleViewController 와 RecordingViewController 가 dismiss 된 이후 크래시 발생.

  • deinit으로 print를 통해 확인해 보면 RecordingTitleViewCoordinator, RecordingViewCoordinator, RecordingTitleViewController는 메모리 해제가 잘 된다.

  • RecordingViewController가 메모리에 해제될 때 문제가 발생한다.

추측1: Coordinator 문제인가??

  • Coordinator에서 강한 연결 때문에 래퍼런스 카운트가 줄어들지 않는 경우??
    • 하지만 위 deinit으로 확인하는 과정에서 위의 Coordinator와 Controller가 정상적으로 해제 되는 것을 보았음.
    • RecordingViewCoordinator에 ViewController를 Strong 으로 갖는 부분을 Weak 으로 갖도록 변경해 보았으나 문제가 해결되지 않았음.
    • 생각해보면 래퍼런스 카운트가 다 줄어들었기 때문에 deinit이 호출 됐겠지..

추측2: 강제 언래핑이나 배열 인덱스 접근 문제인가??

Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)

  • 이러한 경고문은 보통 강제 언래핑이나 배열에 없는 인덱스에 접근할 경우 나온다.
    • 관련된 모든 함수와 코드를 살펴보았지만 강제 언래핑이나 없는 인덱스에 접근하는 코드는 없었음.

추측3: 스레드 문제인가?? 어디서 문제가 됐지??

deinit 문제 참고 링크

  • 구글링을 하던 도중 해당 링크를 발견함. deinit시에 DispatchQueue가 활성화 되어 있고 메모리를 해제하려는 인스턴스가 그것을 가지고 있을 때 크래쉬가 발생할 수도 있다는 이야기.
  • 해당 인스턴스는 측정을 표시하는 ViewController이기 때문에 Timer를 사용한다. 따라서 Timer가 제대로 해제되지 않는다면 문제가 발생할 수 있겠다고 생각됨.
    • 실제로 코드에는 CoreData를 수정해야 해서 Timer를 Cancel 처리하는 부분이 주석처리 되어 있었음.
    • ViewController 메모리를 해제해려고 하는데 Timer가 살아있겠구나..


  • timer를 cancel 하는 함수를 호출


  • timer를 nil로 바꾸는 코드에서 똑같은 크래쉬 발생. (왜?????)


참고 애플 공식 문서

By suspending a dispatch object, your application can temporarily prevent the execution of any blocks associated with that object. The suspension occurs after completion of any blocks running at the time of the call. Calling this function increments the suspension count of the object, and calling dispatch_resume decrements it. While the count is greater than zero, the object remains suspended, so you must balance each dispatch_suspend call with a matching dispatch_resume call.


  • 요약하면 Timer를 사용할 때 suspend와 resume을 사용한 Count를 맞춰 주어야한다는 내용.
  • 왜 그럼 그 전에는 cancel()을 했을 때 크래쉬가 발생하지 않았을까?
    • 일시정지 기능이 없었기 때문.. 일시정지 기능을 구현하지 않은 상태라 suspend 기능을 호출할 일이 없었음

private var timerIsRunning = false

func pause() {
    guard self.timerIsRunning == true else { return }
        
    self.timerIsRunning = false
    self.appendRecord()
    self.timer?.suspend()
    self.locationManager.stopUpdatingLocation()
    self.startDate = nil
}
    
func resume() {
    guard self.timerIsRunning == false else { return }
        
    self.timerIsRunning = true
    self.timer?.resume()
    self.locationManager.startUpdatingLocation()
    self.startDate = Date()
    self.location = [Location]()
}


func cancel() -> Records? {
    guard let records = self.records else { return nil }
    
    if !self.timerIsRunning {
        self.timer?.resume()
    }
        
    self.timer?.cancel()
    self.timer = nil
    self.locationManager.stopUpdatingLocation()
        
    return records
}

  • private var timerIsRunning = false 를 통해 현재 Timer의 상태를 저장한 뒤 저장한 상태에 맞게 timer를 한번 resume 후 timer에 nil을 할당 해 줌.
  • 또한, resume이나 suspend가 두번 연속으로 실행 되지 않도록 로직을 수정 함.
    • 결과적으로 timer는 크래쉬를 뱉지 않게 되었음.
  • Timer가 정상적으로 메모리에서 해제되니 그것을 가지고 있는 RecordingViewController 또한 해제될 수 있게 되었음.

결론

  • RecordingViewController가 deinit 되지 않는 이유는 Timer가 nil을 할당 받지못했으므로 Timer가 메모리에 해제되지 못해 Timer를 가지고 있는 RecordingViewController가 메모리에서 해제 될 때 크래쉬를 뱉는 것이었음.
  • DispatchQueue나 DispatchSourceTimer 등의 스레드를 사용할 때는 메모리 해제를 잘해주자..
Clone this wiki locally