Description
On iOS, makeImageFromView leaks the snapshot's pixel buffer. Each call adds roughly width × height × 4 bytes to the app's physical footprint that is never reclaimed — not after the JS SkImage handle is .dispose()d, not after it is GC'd, and not after the component that took the snapshot unmounts. On a screen that snapshots repeatedly (e.g. a page-curl / transition that snapshots a view per interaction) the footprint grows without bound until the app is jetsammed.
Where
apple/ViewScreenshotService.mm → -screenshotOfViewWithTag: (called from RNSkApplePlatformContext::takeScreenshotFromViewTag, which backs the JS makeImageFromView).
The pixel buffer is a copy:
// ~W*H*4-byte copy of the rendered bitmap:
auto dataRef = CGDataProviderCopyData(CGImageGetDataProvider(cgImage));
...
auto skData = SkData::MakeWithProc(
data, length,
[](const void *ptr, void *context) {
CFDataRef dataRef = (CFDataRef)context;
CFRelease(dataRef); // frees the copy — but only when the SkData dies
},
(void *)dataRef);
return SkImages::RasterFromData(info, skData, bytesPerRow);
The release proc itself is correct: the CFDataRef copy is CFReleased when the SkData is destroyed. The problem is that the returned SkImage is apparently never destroyed, so that release proc never runs and every CGDataProviderCopyData copy stays resident.
Evidence (Release build, iOS Simulator)
footprint -p <pid> climbs by ~3.7 MB (one full-screen BGRA8888 bitmap) per makeImageFromView call and never comes back down, including after the snapshotting screen is fully unmounted.
malloc_history (with SIMCTL_CHILD_MallocStackLogging=1) attributes the resident allocations to:
RNSkApplePlatformContext::takeScreenshotFromViewTag → -[ViewScreenshotService screenshotOfViewWithTag:] → CGDataProviderCopyData → create_protected_copy → mmap.
leaks <pid> reports no classic leak (~1 KB total) → the buffers are still referenced, i.e. retained, not orphaned. Something keeps the SkImage / SkData alive.
- App-side mitigations that did not free the memory (each measured before/after on Release builds): calling
SkImage.dispose() on every code path; an LRU cache that disposes evicted snapshots; gating + disposing on screen unmount. Footprint still climbed ~40 MB per open/close cycle, and the takeScreenshotFromViewTag allocations stayed resident with the screen unmounted.
Expected
After the JS SkImage returned by makeImageFromView is dispose()d (or GC'd), its backing SkData should be destroyed and the CFData pixel copy CFReleased, returning the memory. Repeated snapshots should reach a steady-state footprint rather than grow unbounded.
Reproduction
- Mount a
Canvas and repeatedly call makeImageFromView(viewRef) (e.g. on a gesture / navigation / per "page turn").
dispose() each returned image when finished with it.
- Watch
footprint -p <pid> (or vmmap physical footprint). It grows ~one bitmap per call and never recovers — even after the screen is unmounted.
(Happy to put together a minimal repro repo if it would help.)
Environment
- react-native-skia 2.5.2. Note: the
ViewScreenshotService.mm source above is unchanged through 2.6.4 — only a null-data guard, macOS support, and the ios→apple rename have landed since, none of which touch the lifetime of the returned image.
- React Native 0.84.1, New Architecture (Fabric / bridgeless).
- iOS 26 Simulator (iPhone 17 Pro), Apple Silicon (arm64).
Questions
- Is
SkImage.dispose() expected to synchronously drop the underlying C++ sk_sp<SkImage> so the SkData release proc runs? On iOS the backing image appears to be retained past dispose().
- Is there an internal cache / dependency-manager registry that keeps snapshot
SkImages alive for the lifetime of the JS runtime?
Possibly related, but distinct from: #2909 (Image re-rendering memory), #605.
Description
On iOS,
makeImageFromViewleaks the snapshot's pixel buffer. Each call adds roughlywidth × height × 4bytes to the app's physical footprint that is never reclaimed — not after the JSSkImagehandle is.dispose()d, not after it is GC'd, and not after the component that took the snapshot unmounts. On a screen that snapshots repeatedly (e.g. a page-curl / transition that snapshots a view per interaction) the footprint grows without bound until the app is jetsammed.Where
apple/ViewScreenshotService.mm→-screenshotOfViewWithTag:(called fromRNSkApplePlatformContext::takeScreenshotFromViewTag, which backs the JSmakeImageFromView).The pixel buffer is a copy:
The release proc itself is correct: the
CFDataRefcopy isCFReleased when theSkDatais destroyed. The problem is that the returnedSkImageis apparently never destroyed, so that release proc never runs and everyCGDataProviderCopyDatacopy stays resident.Evidence (Release build, iOS Simulator)
footprint -p <pid>climbs by ~3.7 MB (one full-screen BGRA8888 bitmap) permakeImageFromViewcall and never comes back down, including after the snapshotting screen is fully unmounted.malloc_history(withSIMCTL_CHILD_MallocStackLogging=1) attributes the resident allocations to:RNSkApplePlatformContext::takeScreenshotFromViewTag→-[ViewScreenshotService screenshotOfViewWithTag:]→CGDataProviderCopyData→create_protected_copy→mmap.leaks <pid>reports no classic leak (~1 KB total) → the buffers are still referenced, i.e. retained, not orphaned. Something keeps theSkImage/SkDataalive.SkImage.dispose()on every code path; an LRU cache that disposes evicted snapshots; gating + disposing on screen unmount. Footprint still climbed ~40 MB per open/close cycle, and thetakeScreenshotFromViewTagallocations stayed resident with the screen unmounted.Expected
After the JS
SkImagereturned bymakeImageFromViewisdispose()d (or GC'd), its backingSkDatashould be destroyed and theCFDatapixel copyCFReleased, returning the memory. Repeated snapshots should reach a steady-state footprint rather than grow unbounded.Reproduction
Canvasand repeatedly callmakeImageFromView(viewRef)(e.g. on a gesture / navigation / per "page turn").dispose()each returned image when finished with it.footprint -p <pid>(orvmmapphysical footprint). It grows ~one bitmap per call and never recovers — even after the screen is unmounted.(Happy to put together a minimal repro repo if it would help.)
Environment
ViewScreenshotService.mmsource above is unchanged through 2.6.4 — only a null-data guard, macOS support, and theios→applerename have landed since, none of which touch the lifetime of the returned image.Questions
SkImage.dispose()expected to synchronously drop the underlying C++sk_sp<SkImage>so theSkDatarelease proc runs? On iOS the backing image appears to be retained pastdispose().SkImages alive for the lifetime of the JS runtime?Possibly related, but distinct from: #2909 (Image re-rendering memory), #605.