iOS Share Extension stürzt nach 2 Sekunden ab: Der OOM-Kill und die Lösung
Beim Entwickeln von SnapPress (einer App zum direkten Hochladen von Fotos vom iPhone zu WordPress) habe ich eine Share Extension hinzugefügt, damit Nutzer Fotos direkt aus der Fotos-App teilen können, ohne SnapPress vorher zu öffnen.
Die Extension funktionierte. Meistens. Manchmal öffnete sie sich, zeigte etwa 2 Sekunden lang einen Lade-Spinner, und verschwand dann spurlos. Kein Crash-Log in Xcode. Kein Stack Trace. Keine Fehlermeldung. Einfach weg.
Share Extension vs. Action Extension
Eine Action Extension (com.apple.ui-services) erscheint in der App-Icon-Zeile des Share Sheets. Sie ist dafür gedacht, Inhalte direkt zu transformieren oder darauf zu reagieren.
Eine Share Extension (com.apple.share-services) erscheint in der Aktivitätsliste des Share Sheets. Sie ist dafür gedacht, Inhalte irgendwohin zu senden. Sie läuft als vollständig unabhängige Mini-App mit eigener UI, eigenem Prozess und eigenen Speicherlimits.
Der Absturz: "Invalidation Requested"
Der erste Hinweis kam von Console.app auf dem Mac. Mit verbundenem iPhone und Filter auf den Prozessnamen der Extension fand ich in sharingd:
MetricEvent 'com.apple.sharing.sharesheetCompleted' : {
"success" : false,
"totalShareTimeMs" : 2700,
"activityType" : "app.snappress.SnapPress.ShareExtension",
} Und von Photos:
View service session ended with error:
_UIViewServiceHostSessionErrorDomain Code=4
UserInfo={Message=Invalidation requested} Invalidation requested bedeutet, dass der Extension-Prozess unerwartet beendet wurde. totalShareTimeMs: 2700 bedeutet, dass er genau 2,7 Sekunden lebte. Das ist die Signatur eines OOM-Kills. iOS beendet Prozesse, die das Speicherlimit überschreiten, still und leise ohne Crash-Log oder Signal.
Grundursache: UIImage(data:) dekodiert in voller Auflösung in den Speicher
Die Share Extension lädt Bilder von NSItemProvider, verarbeitet sie und lädt sie zu WordPress hoch. Der Verarbeitungsschritt rief diese Methode auf:
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)
} Das Problem ist UIImage(data: imageData). Dies dekodiert die komprimierte Bilddatei in einen vollauflösenden Pixel-Puffer. Ein 12-Megapixel-iPhone-Foto (4032 × 3024 Pixel) expandiert zu:
4032 × 3024 × 4 Byte (RGBA) = ~47 MB Mit der skalierten Kopie, dem JPEG-Ausgabepuffer, dem SwiftData-Container und Vorschaubildern kann ein einzelner Upload 60 bis 80 MB erreichen. iOS gibt Share Extensions etwa 120 bis 150 MB. Mit einigen großen HEIC-Fotos wird dieses Limit leicht überschritten.
Die Lösung: Direkt zur Zielgröße dekodieren mit CGImageSource
CGImageSource kann beim Dekodieren downsampling durchführen und erzeugt das Bild in Zielgröße, ohne jemals den vollauflösenden Puffer im Speicher zu erstellen.
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)
} Die Speichernutzung pro Bild sinkt von ~60 MB auf ~6 MB. Eine 10-fache Reduzierung. Die Share Extension ist seitdem nicht mehr abgestürzt.
Zusammenfassung
Wenn deine iOS Share Extension nach 2 bis 3 Sekunden ohne Crash-Log abstürzt, ist es fast sicher ein OOM-Kill. Ersetze UIImage(data:) durch CGImageSourceCreateThumbnailAtIndex und setze kCGImageSourceThumbnailMaxPixelSize auf deine Zieldimension. Diesen Fix haben wir in SnapPress eingebaut, und seitdem läuft die Extension stabil.
SnapPress ist im App Store erhältlich.