Home / Blog / Share Extension memory limits: the numbers Apple won’t print

Share Extension memory limits: the numbers Apple won’t print

Share Extension is one of the most memory-constrained extensions on iOS. Apple’s docs say “limited” and stop there, without real numbers. I measured four different iPhone models; here are the limits and the crash patterns they trigger. Measured results I measured using XCTest plus the Instruments Memory Graph Debugger. The hard crash ceilings for a […]

Share Extension is one of the most memory-constrained extensions on iOS. Apple’s docs say “limited” and stop there, without real numbers. I measured four different iPhone models; here are the limits and the crash patterns they trigger.

Measured results

I measured using XCTest plus the Instruments Memory Graph Debugger. The hard crash ceilings for a Share Extension:

  • iPhone SE (2nd gen, 3 GB RAM): ~110 MB
  • iPhone 13 (4 GB RAM): ~120 MB
  • iPhone 15 Pro (8 GB RAM): ~180 MB
  • iPhone 16 Pro Max (8 GB RAM): ~180 MB

These are fresh-boot numbers. Under memory pressure the ceiling drops. If a heavy foreground app is alive at the same time (share sheet from Photos, for example), you get kicked much earlier.

Crash pattern: a silent kill

When a Share Extension blows past the memory limit, the crash report is usually not clear. If you see EXC_RESOURCE (RESOURCE_TYPE_MEMORY), that’s it directly. But mostly Xcode just says “extension killed”. Attach to the console and you’ll see a jetsam_event log.

The trap: decoding the photo on share

My first version decoded the user’s shared image with UIImage(data:) and put it on screen. A 12 MP photo takes about 36 MB decoded. Three photos at 108 MB kills the SE instantly.

The fix is to render a thumbnail and keep the original on disk:

import ImageIO

func thumbnail(from url: URL, maxPixel: CGFloat) -> UIImage? {
    guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return nil }
    let options: [CFString: Any] = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceThumbnailMaxPixelSize: maxPixel
    ]
    guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
    else { return nil }
    return UIImage(cgImage: cgImage)
}

CGImageSource reads pixel buffers from disk, not a full decode, only the requested size. A 12 MP photo rendered at 400×400 sits around 1 MB. That’s a 30 MB saving.

NSItemProvider: load to file, not memory

You have to take what arrives at the extension to a file, not into memory. loadItem(forTypeIdentifier:) returns Data in some cases, and if you do work on that Data without writing it to disk, it stays resident:

func handleAttachments() async throws {
    guard let items = extensionContext?.inputItems as? [NSExtensionItem] else { return }
    for item in items {
        for provider in item.attachments ?? [] {
            if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
                let url = try await provider.loadFileRepresentation(for: .image)
                let target = tmpPath(for: url)
                try FileManager.default.moveItem(at: url, to: target)
                let thumb = thumbnail(from: target, maxPixel: 400)
                await MainActor.run { self.previews.append(thumb) }
            }
        }
    }
}

Use loadFileRepresentation, not loadDataRepresentation. The first gives you a URL, the second gives you Data. For anything large, prefer the URL.

SwiftUI inside a Share Extension

SwiftUI on its own is less ergonomic as a scene in an extension. I host a SwiftUI view inside a UIKit container:

class ShareViewController: SLComposeServiceViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let host = UIHostingController(rootView: SharePicker(viewModel: vm))
        addChild(host)
        host.view.frame = view.bounds
        host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(host.view)
        host.didMove(toParent: self)
    }
}

Hand off to the main app via App Group

Don’t trust the extension to do the real work. The safer pattern is to copy the file into an App Group container and let the main app process it on next launch (or via background launch):

let groupURL = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.mycompany.app"
)!
let target = groupURL.appendingPathComponent("Inbox/(UUID().uuidString).jpg")
try FileManager.default.moveItem(at: srcURL, to: target)

When the main app opens, it scans the Inbox folder and processes new files. Do the minimum inside the extension.

Memory telemetry: your own gauge

To watch memory live, I use mach_task_basic_info:

func currentMemoryMB() -> Int {
    var info = mach_task_basic_info()
    var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
    let r = withUnsafeMutablePointer(to: &info) {
        $0.withMemoryRebound(to: integer_t.self, capacity: 1) {
            task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
        }
    }
    return r == KERN_SUCCESS ? Int(info.resident_size / 1024 / 1024) : -1
}

Past 80 MB I log a warning. At 100 MB I run aggressive cleanup. At 110 MB I shut the extension down myself and tell the user “we’ll finish this in the main app”.

Closing advice

Share Extension is a “do one thing fast” UI. Don’t treat it like an app. Show at most one preview, copy the file into the App Group on tap, close the extension. Do the real work when the main app opens. This approach has held at zero OOM crashes across three of my apps.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch