iOS Share Extension Travava Após 2 Segundos: A Solução para o OOM Kill
Ao desenvolver o SnapPress (um app para fazer upload de fotos do iPhone diretamente para o WordPress), adicionei uma Share Extension para que os usuários pudessem compartilhar fotos do app Fotos sem precisar abrir o SnapPress primeiro.
A extensão funcionava. Na maioria das vezes. Às vezes ela abria, mostrava um spinner de carregamento por cerca de 2 segundos, e desaparecia sem deixar rastro. Sem crash log no Xcode. Sem stack trace. Sem mensagem de erro. Simplesmente sumia.
Share Extension vs Action Extension
Uma Action Extension (com.apple.ui-services) aparece na linha de ícones do share sheet. É projetada para transformar ou agir sobre conteúdo no momento.
Uma Share Extension (com.apple.share-services) aparece na lista de atividades do share sheet. É projetada para enviar conteúdo para algum lugar (uma rede social, um CMS, um app de notas). Ela roda como um mini-app independente com sua própria UI, seu próprio processo e seus próprios limites de memória.
O Crash: "Invalidation Requested"
A primeira pista veio do Console.app no Mac. Com o iPhone conectado e filtrando pelo nome do processo da extensão, encontrei isso no sharingd:
MetricEvent 'com.apple.sharing.sharesheetCompleted' : {
"success" : false,
"totalShareTimeMs" : 2700,
"activityType" : "app.snappress.SnapPress.ShareExtension",
} E no Photos:
View service session ended with error:
_UIViewServiceHostSessionErrorDomain Code=4
UserInfo={Message=Invalidation requested} Invalidation requested significa que o processo da extensão foi encerrado inesperadamente. totalShareTimeMs: 2700 significa que viveu exatamente 2,7 segundos. Esta é a assinatura de um OOM kill. O iOS encerra silenciosamente processos que excedem o limite de memória, sem crash log ou sinal.
Causa Raiz: UIImage(data:) Decodifica em Resolução Completa na Memória
A Share Extension carrega imagens do NSItemProvider, processa e faz upload para o WordPress. O passo de processamento chamava este método:
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)
} O problema é UIImage(data: imageData). Isso decodifica o arquivo de imagem comprimido em um buffer de pixels em resolução completa. Uma foto de 12 megapixels do iPhone (4032 × 3024 pixels) expande para:
4032 × 3024 × 4 bytes (RGBA) = ~47 MB Com a cópia redimensionada, o buffer de saída JPEG, o container SwiftData e as miniaturas, um único upload pode chegar a 60 a 80 MB. O iOS dá às Share Extensions aproximadamente 120 a 150 MB. Com algumas fotos HEIC grandes, esse limite é facilmente ultrapassado.
A Solução: Decodificar Diretamente para o Tamanho Alvo com CGImageSource
CGImageSource pode fazer downsampling durante a decodificação, produzindo a imagem no tamanho alvo sem nunca criar o buffer em resolução completa na memória.
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)
} O uso de memória por imagem cai de ~60 MB para ~6 MB. Uma redução de 10x. A Share Extension não travou mais desde então.
Resumo
Se sua Share Extension iOS falha após 2 a 3 segundos sem crash log, quase certamente é um OOM kill. Substitua UIImage(data:) por CGImageSourceCreateThumbnailAtIndex e defina kCGImageSourceThumbnailMaxPixelSize para a sua dimensão alvo.
SnapPress está disponível na App Store.