Skip to content

RecordingViewController 메모리 해제 문제

MINRYUL edited this page Nov 10, 2021 · 15 revisions

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


시나리오

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

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

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

추측1

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

추측2

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

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

추측3

deinit 문제 참고 링크

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


  • 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를 맞춰 주어야한다는 내용.

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()
    } else {
        self.timer?.suspend()
    }
        
    self.timer?.cancel()
    self.timer = nil
    self.locationManager.stopUpdatingLocation()
        
    return records
}

  • private var timerIsRunning = false 를 통해 현재 Timer의 상태를 저장한 뒤 저장한 상태에 맞게 timer를 한번 resume 후 timer에 nil을 할당 해 줌.
    • 결과적으로 timer는 크래쉬를 뱉지 않게 되었음.

결론

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