Crash when rendering CALayer using UIGraphicsImageRenderer on background thread

Hello!

I’m experiencing a crash in my iOS/iPadOS app related to a CALayer rendering process. The crash occurs when attempting to render a UIImage on a background thread. The crashes are occurring in our production app, and while we can monitor them through Crashlytics, we are unable to reproduce the issue in our development environment.

Relevant Code

I have a custom view controller that handles rendering CALayers onto images. This method creates a CALayer on the main thread and then starts a detached task to render this CALayer into a UIImage. The whole idea is learnt from this StackOverflow post: https://stackoverflow.com/a/77834613/9202699

Here are key parts of my implementation:

class MyViewController: UIViewController {
    
    @MainActor
    func renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) async -> UIImage? {

    // Create CALayer and add it to the view.
    CATransaction.begin()
    let customLayer = MyDrawingLayer()
    customLayer.setupContent(itemsToDraw: itemsToDraw)
    // Position the frame off-screen to it hidden.
    customLayer.frame = CGRect(
        origin: CGPoint(x: -100 - size.width, y: -100 - size.height),
        size: size)
    customLayer.masksToBounds = true
    customLayer.drawsAsynchronously = true
    view.layer.addSublayer(customLayer)
    CATransaction.commit()

    // Render CALayer to UIImage in background thread.
    let image = await Task.detached {
      customLayer.setNeedsDisplay()
      let renderer = UIGraphicsImageRenderer(size: size)
      let image = renderer.image { // CRASH happens on this line
        let cgContext = $0.cgContext
        cgContext.saveGState()
        cgContext.concatenate(transform)
        customLayer.render(in: cgContext)
        cgContext.restoreGState()
      }
      return image
    }.value

    // Remove the CALayer from the view.
    CATransaction.begin()
    customLayer.removeFromSuperlayer()
    CATransaction.commit()

    return image
    }
}

class MyDrawingLayer: CALayer {
    var itemsToDraw: [MyDrawingItem] = []

    func setupContent(itemsToDraw: [MyDrawingItem]) {
        self.itemsToDraw = itemsToDraw
    }

    override func draw(in ctx: CGContext) {
        for item in itemsToDraw {
            // Render the item to the context (example pseudo-code).
            // All items are thread-safe to use.
            // Things to draw may include CGPath, CGImages, UIImages, NSAttributedString, etc.
            item.draw(in: ctx)
        }
    }
}

Crash Log

The crash occurs at the following location:

Crashed: com.apple.root.default-qos.cooperative

0  MyApp                           0x5cb300 closure #1 in closure #1 in MyViewController.renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) + 4313002752 (<compiler-generated>:4313002752)
1  MyApp                           0x5cb300 closure #1 in closure #1 in MyViewController.renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) + 4313002752 (<compiler-generated>:4313002752)
2  MyApp                           0x1a4578 AnyModifier.modified(for:) + 4308649336 (<compiler-generated>:4308649336)
3  MyApp                           0x7b4e64 thunk for @escaping @callee_guaranteed (@guaranteed UIGraphicsPDFRendererContext) -> () + 4315008612 (<compiler-generated>:4315008612)
4  UIKitCore                      0x1489c0 -[UIGraphicsRenderer runDrawingActions:completionActions:format:error:] + 324
5  UIKitCore                      0x14884c -[UIGraphicsRenderer runDrawingActions:completionActions:error:] + 92
6  UIKitCore                      0x148778 -[UIGraphicsImageRenderer imageWithActions:] + 184
7  MyApp                           0x5cb1c0 closure #1 in MyViewController.renderToUIImage(size: CGSize, itemsToDraw: [MyDrawingItem], transform: CGAffineTransform) + 100 (FileName.swift:100)
8  libswift_Concurrency.dylib     0x60f5c swift::runJobInEstablishedExecutorContext(swift::Job*) + 252
9  libswift_Concurrency.dylib     0x62514 swift_job_runImpl(swift::Job*, swift::SerialExecutorRef) + 144
10 libdispatch.dylib              0x15ec0 _dispatch_root_queue_drain + 392
11 libdispatch.dylib              0x166c4 _dispatch_worker_thread2 + 156
12 libsystem_pthread.dylib        0x3644 _pthread_wqthread + 228
13 libsystem_pthread.dylib        0x1474 start_wqthread + 8

Questions

  1. Is it safe to run UIGraphicsImageRenderer.image on the background thread?

  2. Given that I want to leverage GPU rendering, what are some best practices for rendering images off the main thread while ensuring stability?

  3. Are there alternatives to using UIGraphicsImageRenderer for background rendering that can still take advantage of GPU rendering?

  4. It is particularly interesting that the crash logs indicate the error may be related to UIGraphicsPDFRendererContext (crash log line number 3). It would be very helpful if someone could explain the connection between starting and drawing on a UIGraphicsImageRenderer and UIGraphicsPDFRendererContext.

Any insights or guidance on this issue would be greatly appreciated. Thanks!!!

It is safe to use UIGraphicsImageRenderer from a background thread. The crash here appears to be somewhere in your drawing code so it is unclear what is going wrong.

However this is not a reasonable way to leverage GPU rendering. In particular this path is likely to be extremely inefficient relative to just drawing your content directly as these code paths are optimized for drawing directly to the screen, not to drawing to an image.

There is no quick solution to leveraging GPU drawing efficiently for things like this – you would need to leverage a drawing engine that is specifically designed for this. While CoreAnimation can do it, that is not its intended usec ase and so not what it is designed for. Additionally since you mention the PDF, you should note that CoreAnimation always produces bitmaps, which are not ideal when you have the drawing commands that produce those bitmaps.

The crash log unfortunately appears to be indicating something for which there is not an exported symbol (hence the large offset from the nearest known function). As such it is difficult to understand what may actually be crashing.

Crash when rendering CALayer using UIGraphicsImageRenderer on background thread
 
 
Q