Search code examples
optimizationuiimagecore-graphicsswift5uigraphicscontext

iOS Core Graphics how to optimize incremental drawing of very large image?


I have an app written with RXSwift which processes 500+ days of HealthKit data to draw a chart for the user.

The chart image is drawn incrementally using the code below. Starting with a black screen, previous image is drawn in the graphics context, then a new segment is drawn over this image with certain offset. The combined image is saved and the process repeats around 70+ times. Each time the image is saved, so the user sees the update. The result is a single chart image which the user can export from the app.

Even with autorelease pool, I see spikes of memory usage up to 1Gb, which prevents me from doing other resource intensive processing.

How can I optimize incremental drawing of very large (1440 × 5000 pixels) image?

When image is displayed or saved at 3x scale, it is actually 4320 × 15360.

Is there a better way than trying to draw over an image?

autoreleasepool {

    //activeEnergyCanvas is custom data processing class
    let newActiveEnergySegment = activeEnergyCanvas.draw(in: CGRect(x: 0, y: 0, width: 1440, height: days * 10), with: energyPalette)

    let size = CGSize(width: 1440, height: height)
    UIGraphicsBeginImageContextWithOptions(size, false, 0.0)

    //draw existing image
    self.activeEnergyImage.draw(in: CGRect(origin: CGPoint(x: 0, y: 0),
                                        size: size))

    //calculate where to draw smaller image over larger one
    let offsetRect = CGRect(origin: CGPoint(x: 0, y: offset * 10),
                            size: newActiveEnergySegment.size)

    newActiveEnergySegment.draw(in: offsetRect)

    //get the combined image
    let newImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    //assign combined image to be displayed
    if let unwrappedImage = newImage {
        self.activeEnergyImage = unwrappedImage
    }
}

enter image description here


Solution

  • Turns out my mistake was in passing invalid drawing scale (0.0) when creating graphics context, which defaulted to drawing at the device's native screen scale.

    In case of iPhone 8 it was 3.0 The result is needing extreme amounts of memory to draw, zoom and export these images. Even if all debug logging prints that image is 1440 pixels wide, the actual canvas ends up being 1440 * 3.0 = 4320.

    Passing 1.0 as the drawing scale makes the image more fuzzy, but reduces memory usage to less than 200mb.

    // UIGraphicsBeginImageContext() <- also uses @3x scale, even when all display size printouts show 
    let drawingScale: CGFloat = 1.0
    UIGraphicsBeginImageContextWithOptions(size, true, drawingScale)