使用 CIImage 支持的 UIImage 设置 UIImageView 时很少发生崩溃

Rare crashes when setting UIImageView with a UIImage backed with CIImage

首先,我想强调的是,根据 Firebase Crashlytics,此错误仅涉及 1% 的用户群

我有一个包含许多 heic 图像的 xcasset 目录。 我需要显示一些这样的图像(原始版本)和一些模糊的图像。

这是加载和显示正常图像或模糊图像的代码。

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

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

我使用管理器来缓存模糊图像,这样我就不必在每次显示它们时都重新生成它们。

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
    }
}

最后是模糊代码

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)
    }
}

这是我遇到的两种崩溃类型

  1. 带有 CFAutorelease 的 CoreFoundation。 Crashlytics 有关于它的附加信息:
crash_info_entry_0:
*** CFAutorelease() called with NULL ***

  1. CoreImage 与 recursive_render。 Crashlytics 还有关于它的附加信息:
crash_info_entry_0: 
Cache Stats: count=14 size=100MB non-volatile=0B peakCount=28 peakSize=199MB peakNVSize=50MB

我发现所有用户之间唯一的共同点是在崩溃时他们有 30 - 150 Mo 的 RAM(根据 Firebase,这个信息是否可靠?)。

说到这里,我真的是一头雾水。这似乎是 CoreImage / CoreFoundation 的一个错误,它如何处理内存中的 CIImage。

奇怪的是,因为我使用 AssetManager 来缓存模糊图像,我知道在崩溃期间用户已经在 RAM 中有可用的缓存版本,但是当设置 UIImageView 时缓存图像,它因内存不足而崩溃(?!)。为什么系统甚至试图分配内存来执行此操作?

根据我的经验,直接使用从 CIImage 创建的 UIImage 是非常不可靠和错误的。主要原因是CIImage 不是真正的位图图像,而是包含创建图像说明的收据 . UIImage 的消费者知道它由 CIImage 支持并正确呈现它。 UIImageView 理论上是这样的,但我在这里看到很多关于 SO 的报道,它有点不可靠。正如 ohglstr 正确指出的那样,缓存 UIImage 并没有多大帮助,因为它仍然需要在每次使用时进行渲染。

我建议您使用 CIContext 自己渲染模糊图像并缓存结果。例如,您可以在 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
    }
}