- 백그라운드에서 앱이 크래시 나거나 종료되는 경우가 있다.
- 이 경우, 앱의 진행 상황을 제대로 저장하지 못하게 되고, 데이터가 유실될 수 있다.
- 종료되는 이유는 여러가지다.
- 크래시
- CPU 리소스 부족
- 와치독(감시자)
- 메모리 제한 초과
- 백그라운드 태스크 시간 초과
- 메모리 압력으로 인한 종료
- iOS 14부터 새로운 MetricKit API가 추가된다. -> MXBackgroundExitData
- 크래시
- 원인
- 세그멘테이션 폴트
- 잘못된 인스트럭션
- 단정문 & 처리되지 않은 예외
- 추적
- Xcode Organizer를 통해서 자동으로 수집된다.
- 기기별 Diagnostics를 할 수 있는 MXCrashDiagnostic API를 사용한다.
- Statck trace
- Signal
- Exception Code
- Termination Reason
- 와치독 이벤트
- 앱의 중요 이벤트 처리가 늦어질 때, 처리를 종료시킨다.
- 중요 이벤트 -> 20초의 시간 제한
- didFinishLaunchingWithOptions
- DidEnterBackground
- WillEnterForeground
- 시뮬레이터나 디버거가 붙은 상황에서는 비활성화되어있다.
- 와치독 이벤트는 로직에 심각한 결함이 있음을 암시한다.
- 데드락
- 무한 루프,
- 메인 스레드에서 끝나지 않는 동기 작업 등…
- MXCrachDiagnostic을 통해
- 백그라운드 태스크에서의 CPU 리소스 제한
- 백그라운드 작업은 CPU 사용에 제한이 걸린다. -> 이 상태가 되면 시스템이 Energy exception report를 내보내고, 이 상태가 너무 길어지면 앱을 종료시킨다.
- Xcode Organizer로 확인 가능
- MXCPUExceptionDiagnostic으로 확인 가능
- 콜 스택을 통해서 가장 에너지를 많이 쓰는 곳을 찾아낼 수 있다.
- 정말로 필요하다면, BGProcessingTask로 작업을 옮기는 것을 고려해보라
- BGProcessingTask를 쓰면 밤에 충전중인 상황에서, CPU 제한 없이 몇분씩 돌 수 있게 한다.
- 메모리 사용량 초과
- 앱이 너무 많은 메모리를 써도 죽는다.
- 이 제한은 백그라운드나 포그라운드나 같다
- Instrument나 memory debugger를 통해서 디버깅하라
- 오래된 디바이스를 고려하라
- 6S 기준으로 200MB이하를 유지할 수 있도록…
- 메모리 압력으로 인한 종료(jetsam)
- 버그가 아니라 일반적인 종료상태
- 시스템은 활성화된 앱을 위해서 백그라운드에 있는 앱을 종료한다.
- 이를 막기 위해서는 백그라운드에서의 메모리 사용을 줄여야 한다 -> 하지만 완전히 막을 수는 없다.
- 애플 기준 50MB, 작을수록 좋다.
- 이를 위해 디스크에 데이터를 쓴 뒤 날리거나, 이미지 데이터나 캐시를 클리어 하는 등의 작업을 수행하면 된다.
- 막을 수는 없기 때문에, 백그라운드에 진입할 때는 앱의 상태를 저장해놓을 필요가 있다.
- UIKit의 Restoration API를 사용하면 된다.
- 백그라운드 타임 아웃
- UIApplication.beginBackgroundTask(expirationHandler:)를 호출하면 작업이 시작된다.
- UIApplication.endBackgroundTask(_:) 를 통해서 작업이 종료됨을 알려야 한다.
- 이걸 호출하지 않으면 백그라운드 작업이 종료되었다는 것을 시스템이 인지하지 못하고 작업이 타임아웃되서 앱이 종료된다. -> 약 30초 정도의 시간만 있다.
- 호출하면 정상적으로 suspend된다.
- MXBackgroundExitData를 통해서 관련 정보를 수집할 수 있다.
- 이러한 것은 막을수 있다.
- named API로 갈아타자 -> beginBackgroundTask(withName: expirationHandler:)를 쓰자. 식별이 좀 더 잘 된다.
- 13.4부터 관련 콘솔 메시지가 좀 더 세련되게 나온다.
- expirationHandler를 구현하고 그 안에서 endBackgroundTask(_:) 를 호출하자.
- 그 안에서 무거운 작업 새로 하지는 말자.
- 어떤 expirationHandler가 호출되었는지 정보를 측정하자
let handle = MXMetricManager.makeLogHandle(category: "DatabaseExpirationHandler")
mxSignpost(.event, log: handle, name: "Entered")
cancelOperations()
closeDatabase()
mxSignpost(.event, log: handle, name: "Exited")
UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
- 안전망이지만, 너무 거기에 의존하지는 말자
- background에서 새로운 작업을 시작하려면, 남은 시간을 잘 체크하자
- 시간이 많이 남았을 때만(약 5초 이상) 시작할 것
let minimumTimeRemaining = min(5, estimateProcessingTime(inputData))
if UIApplication.shared.backgroundTimeRemaining > minimumTimeRemaining {
return UIApllication.shared.beginBackgroundTask { ... }
} else {
registerProcessingTask(inputData) // 나중으로 미루라.
return .invalid
}
- backgroundIdentifier가 유실되지 않도록 하자
- instance변수보다는 local 변수로 만들어서 클로저에 캡쳐되도록 하자