By |
Fix the iOS Share Extension Crash — SnapPress

iOS Share Extension이 2초 만에 종료되는 문제: OOM Kill 해결법

iPhone에서 WordPress로 직접 사진을 업로드하는 앱 SnapPress를 개발하면서, 사용자가 SnapPress를 먼저 열지 않고도 사진 앱에서 바로 공유할 수 있도록 Share Extension을 추가했습니다.

Extension은 작동했습니다. 대부분의 경우에는요. 때로는 열렸다가 로딩 스피너를 약 2초 동안 보여준 후 흔적도 없이 사라졌습니다. Xcode에서 크래시 로그 없음. 스택 트레이스 없음. 오류 메시지 없음. 그냥 사라졌습니다.

SnapPress brand card
SnapPress는 인앱 대량 업로더로 iOS 공유 익스텐션의 메모리 제한을 완전히 우회합니다.

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에서 이용 가능합니다.

저자 소개

Ken Furuta

37Design 창업자 겸 CEO

일본 도쿄에 위치한 웹 & AI 컨설팅 회사 37Design의 창업자 겸 CEO. iPhone 사진을 WordPress에 한 장씩 업로드하는 불편함을 해결하기 위해 SnapPress를 개발했습니다.

37design.co.jp