iOS Share Extension이 2초 만에 종료되는 문제: OOM Kill 해결법
iPhone에서 WordPress로 직접 사진을 업로드하는 앱 SnapPress를 개발하면서, 사용자가 SnapPress를 먼저 열지 않고도 사진 앱에서 바로 공유할 수 있도록 Share Extension을 추가했습니다.
Extension은 작동했습니다. 대부분의 경우에는요. 때로는 열렸다가 로딩 스피너를 약 2초 동안 보여준 후 흔적도 없이 사라졌습니다. Xcode에서 크래시 로그 없음. 스택 트레이스 없음. 오류 메시지 없음. 그냥 사라졌습니다.
Share Extension vs Action Extension
Action Extension(com.apple.ui-services)은 공유 시트의 앱 아이콘 행에 나타납니다. 콘텐츠를 그 자리에서 변환하거나 처리하도록 설계되었습니다.
Share Extension(com.apple.share-services)은 공유 시트의 활동 목록에 나타납니다. 콘텐츠를 어딘가로 전송하도록 설계되었습니다. 자체 UI, 자체 프로세스, 자체 메모리 제한을 가진 완전히 독립적인 미니 앱으로 실행됩니다.
크래시: "Invalidation Requested"
첫 번째 단서는 Mac의 Console.app에서 나왔습니다. iPhone을 연결하고 Extension 프로세스 이름으로 필터링하니 sharingd에서 다음을 발견했습니다:
MetricEvent 'com.apple.sharing.sharesheetCompleted' : {
"success" : false,
"totalShareTimeMs" : 2700,
"activityType" : "app.snappress.SnapPress.ShareExtension",
} 그리고 Photos에서:
View service session ended with error:
_UIViewServiceHostSessionErrorDomain Code=4
UserInfo={Message=Invalidation requested} Invalidation requested는 Extension 프로세스가 예기치 않게 종료되었음을 의미합니다. totalShareTimeMs: 2700은 정확히 2.7초 동안 살아있었음을 나타냅니다. 이것은 OOM kill의 특징입니다. iOS는 메모리 제한을 초과한 프로세스를 크래시 로그나 신호 없이 조용히 종료합니다.
근본 원인: UIImage(data:)는 메모리에 전체 해상도로 디코딩한다
Share Extension은 NSItemProvider에서 이미지를 로드하고 처리한 후 WordPress에 업로드합니다. 처리 단계에서 이 메서드를 호출했습니다:
static func process(_ imageData: Data, quality: ImageQuality) -> Data? {
guard let image = UIImage(data: imageData) else { return nil }
let resized = resize(image, maxDimension: quality.maxDimension)
return resized.jpegData(compressionQuality: quality.compressionQuality)
} 문제는 UIImage(data: imageData)입니다. 이것은 아무 처리도 하기 전에 압축된 이미지 파일을 전체 해상도 픽셀 버퍼로 디코딩합니다. 12메가픽셀 iPhone 사진(4032 × 3024 픽셀)은 다음과 같이 확장됩니다:
4032 × 3024 × 4바이트 (RGBA) = ~47 MB 리사이즈된 복사본, JPEG 출력 버퍼, SwiftData 컨테이너, 썸네일 미리보기를 합치면 단일 업로드에서 60~80 MB에 달할 수 있습니다. iOS는 Share Extension에 약 120~150 MB를 제공합니다. 큰 HEIC 사진 몇 장이면 그 한도를 쉽게 초과합니다.
해결책: CGImageSource로 목표 크기에 직접 디코딩하기
CGImageSource는 디코딩 중에 다운샘플링을 수행하여 메모리에 전체 해상도 버퍼를 만들지 않고 목표 크기의 이미지를 생성할 수 있습니다.
static func process(_ imageData: Data, quality: ImageQuality) -> Data? {
let maxDim = Int(quality.maxDimension)
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxDim,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: false,
]
guard
let source = CGImageSourceCreateWithData(imageData as CFData, nil),
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
else { return nil }
return UIImage(cgImage: cgImage).jpegData(compressionQuality: quality.compressionQuality)
} 이미지당 메모리 사용량이 ~60 MB에서 ~6 MB로 줄었습니다. 10배 감소입니다. 그 이후로 Share Extension은 한 번도 크래시되지 않았습니다.
요약
iOS Share Extension이 크래시 로그 없이 2~3초 후에 종료된다면, 거의 확실히 OOM kill입니다. UIImage(data:)를 CGImageSourceCreateThumbnailAtIndex로 교체하고 kCGImageSourceThumbnailMaxPixelSize를 목표 크기로 설정하세요.
SnapPress는 App Store에서 이용 가능합니다.