iOS Share Extension 2秒後崩潰問題:OOM Kill 的診斷與解決方法
在開發 SnapPress(一款從 iPhone 直接上傳照片到 WordPress 的應用程式)時,我新增了一個 Share Extension,讓使用者可以直接從相片應用程式分享照片,而不需要先開啟 SnapPress。
Extension 可以運作,但不是每次都正常。有時它開啟後,顯示約 2 秒的載入轉圈,然後無聲無息地消失。Xcode 中沒有崩潰日誌,沒有堆疊追蹤,沒有錯誤訊息,就是消失了。
Share Extension 與 Action Extension 的差異
Action Extension(com.apple.ui-services)顯示在分享面板的應用程式圖示列中,設計用於就地轉換或處理內容。
Share Extension(com.apple.share-services)顯示在分享面板的活動清單中,設計用於將內容傳送到某處(社群媒體、CMS、筆記應用程式等)。它作為完全獨立的迷你應用程式運作,擁有自己的 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)。這會在進行任何處理之前,將壓縮的圖片檔案解碼為完整解析度的像素緩衝區。一張 1200 萬像素的 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 上架。