A 2 MB JPEG costs how much memory once it is shown, and what does downsampling at decode time change?
Predict first
What does the decoded bitmap cost at full size and as a thumbnail?
import Foundation
import ImageIO
import CoreGraphics
import UniformTypeIdentifiers
func photo(_ w: Int, _ h: Int) -> CGImage {
let ctx = CGContext(data: nil, width: w, height: h, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
let p = ctx.data!.assumingMemoryBound(to: UInt8.self); let rb = ctx.bytesPerRow
var seed: UInt32 = 1
for y in 0..<h { for x in 0..<w {
seed = seed &* 1664525 &+ 1013904223
let o = y * rb + x * 4
p[o] = UInt8((x * 255 / w + Int(seed >> 29)) & 255); p[o+1] = UInt8((y * 255 / h + Int((seed >> 27) & 7)) & 255)
p[o+2] = UInt8(((x + y) * 255 / (w + h)) & 255); p[o+3] = 255
} }
return ctx.makeImage()!
}
let data = NSMutableData()
let dest = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil)!
CGImageDestinationAddImage(dest, photo(4000, 3000), [kCGImageDestinationLossyCompressionQuality: 0.8] as CFDictionary)
CGImageDestinationFinalize(dest)
print("JPEG file is under 8 MB:", data.length < 8_000_000)
let src = CGImageSourceCreateWithData(data, nil)!
let full = CGImageSourceCreateImageAtIndex(src, 0, nil)!
print("full decode: \(full.width)x\(full.height), bitmap \(full.height * full.bytesPerRow / 1000) KB")
let opts: [CFString: Any] = [kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: 300, kCGImageSourceShouldCacheImmediately: true]
let thumb = CGImageSourceCreateThumbnailAtIndex(src, 0, opts as CFDictionary)!
print("thumbnail: \(thumb.width)x\(thumb.height), bitmap \(thumb.height * thumb.bytesPerRow / 1000) KB")
- 4000x3000 bitmap of 48000 KB, thumbnail 300x225 of 270 KB
- Both bitmaps are about 2 MB, the file size
- The thumbnail is 4000x3000 as well
Mental model. A suitcase is the size of what you pack, not of the zipped-up travel card describing it. The JPEG is the card; the bitmap is the unpacked clothes.
A compressed file's size says nothing about its memory cost. A decoded bitmap needs width × height × bytes per pixel (4 for 8-bit RGBA), and the row stride may be padded. In the check a 4000×3000 JPEG that was well under 8 MB on disk decoded to a bitmap of 48,000 KB (about 48 MB), and the same file decoded with a maximum pixel size of 300 became 300×225 and 270 KB: a hundred and seventy-eight times smaller. A list of twenty such full-size images is a gigabyte of pixels, long before any bytes are “cached”.
The fix is to decode to the size you display. CGImageSourceCreateThumbnailAtIndex with kCGImageSourceThumbnailMaxPixelSize (in points times screen scale) and kCGImageSourceShouldCacheImmediately decodes once at the target size, off the main thread, instead of decoding the full bitmap and shrinking it (the usual UIImage(data:) then scale path decodes at full size, lazily, on first draw). Keep the downsampled bitmap in a size-limited cache, keep the original on disk, and decode on a background queue (Q327). Be careful with the numbers you quote: the bitmap size is exact arithmetic, but how much of it the OS reports in your footprint depends on the platform, because decoded buffers can be backed by purgeable or shared surfaces, so this check asserts the pixel arithmetic and does not claim a footprint figure.
Bitmap bytes = width x height x 4; downsampling at decode time shrinks it to the displayed size.
Common traps
- Sizing by file length. A tiny PNG of flat colour can still be a huge bitmap.
- Decoding at full size and scaling down for a thumbnail. The peak memory is the full bitmap.
They might follow up with
- How do I get the pixel size without decoding?
CGImageSourceCopyPropertiesAtIndexreads the dimensions from the header. - Is HEIC cheaper in memory? No: smaller file, same decoded bitmap.
Related questions
- 21.9.1 What counts toward an app’s memory limit: why is reserved memory free, a mapped file cheap, and the same file…
- 21.5.1 Which everyday operations are surprisingly expensive in Swift and iOS code, and how do you find and fix them?
- 3.7.5 What is memory footprint, and what gets an iOS app killed?
← What counts toward an app’s memory limit: why is reserved memory…