iosswiftuikitcore-imagecore-foundation

Rare crashes when setting UIImageView with a UIImage backed with CIImage


First of all, I want to emphasize that this bug concerns only about 1% of the user base according to Firebase Crashlytics.

I have a xcasset catalog with many heic images. I need to display some of those images as such (original version) and some of them blurred.

Here is the code to load and display a normal image or a blurred image.

// Original image
self.imageView.image = UIImage(named: "officeBackground")!

// Blurred image
self.imageView.image = AssetManager.shared.blurred(named: "officeBackground")

I use a manager to cache the blurred images so that I don't have to re-generate them every time I display them.

final class AssetManager {
    static let shared = AssetManager()
    
    private var blurredBackground = [String: UIImage]()

    func blurred(named: String) -> UIImage {
        if let cachedImage = self.blurredBackground[from] {
            return cachedImage
        }
        let blurred = UIImage(named: named)!.blurred()!
        self.blurredBackground[from] = blurred
        return blurred
    }
}

And finally the blur code

extension UIImage {
    func blurred() -> UIImage? {
        let ciimage: CIImage? = self.ciImage ?? CIImage(image: self)
        guard let input = ciimage else { return nil }
        let blurredImage = input.clampedToExtent()
            .applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 13])
            .cropped(to: input.extent)
        return UIImage(ciImage: blurredImage, scale: self.scale, orientation: .up)
    }
}

And here are the 2 types of crashes I get

  1. CoreFoundation with CFAutorelease. Crashlytics has an additional info about it:
crash_info_entry_0:
*** CFAutorelease() called with NULL ***

CoreFoundation CFAutorelease stack trace crash

  1. CoreImage with recursive_render. Crashlytics has also this additional info about it:
crash_info_entry_0: 
Cache Stats: count=14 size=100MB non-volatile=0B peakCount=28 peakSize=199MB peakNVSize=50MB

CoreImage recursive_render stack trace crash

The only common point I found between all users is that they have between 30 - 150 Mo of RAM at the time of crash (according to Firebase, if this info is even reliable?).

At this point, I am honestly clueless. It seems like a bug with CoreImage / CoreFoundation with how it handles CIImage in memory.

The weird thing is that because I'm using the AssetManager to cache the blurred images, I know that during the time of crash the user already has a cache version available in RAM, and yet when setting the UIImageView with the cached image, it crashes because of low memory (?!). Why is the system even trying to allocate memory to do this?


Solution

  • In my experience, using a UIImage that is created from a CIImage directly is very unreliable and buggy. The main reason is that a CIImage is not really a bitmap image, but rather a receipt that contains the instructions for creating an image. It is up to the consumer of the UIImage to know that it's backed by a CIImage and render it properly. UIImageView theoretically does that, but I've seen many reports here on SO that it's somewhat unreliable. And as ohglstr correctly pointed out, caching that UIImage doesn't help much since it still needs to be rendered every time it's used.

    I recommend you use a CIContext to render the blurred images yourself and cache the result. You could for instance do that in your AssetManager:

    final class AssetManager {
        static let shared = AssetManager()
        
        private var blurredBackground = [String: UIImage]()
        private var ciContext: CIContext()
    
        func blurred(named name: String) -> UIImage {
            if let cachedImage = self.blurredBackground[name] {
                return cachedImage
            }
    
            let ciImage = UIImage(named: name)!.blurred()!
            let cgImage = self.ciContext.createCGImage(ciImage, from: ciImage.extent)!
            let blurred = UIImage(cgImage: cgImage)
    
            self.blurredBackground[name] = blurred
            return blurred
        }
    }