这个问题的答案揭示了UIImage 和cgImage 工作方式的一些有趣的事情。 UIImage 对象不直接保存图像数据,而是保存对数据外部存储的引用。
作为Apple's Documentation notes,您可以通过UIImage 上的cgImage 或ciImage 属性访问它,因此您会认为访问这些属性并调用myImage.cgImage.copy() 会给您一份这个数据。它没有。复制 cgImage 和 ciImage 结构会为您提供这些结构的副本,但副本仍指向原始数据存储,在本例中为磁盘上的文件。
这意味着您会得到最初看起来相当令人惊讶的结果。您可以从磁盘成功创建UIImage,删除磁盘上的文件,您仍然可以返回一个非可选的UIImage,它“工作”非常愉快,除了图像不再可以用于的次要实用性访问或显示有用的数据。
答案是在创建新UIImage的过程中“深拷贝”底层数据。使其易于使用的一种方法是在 UIImage 本身上创建一个扩展:
extension UIImage {
func deepCopy() -> UIImage? {
UIGraphicsBeginImageContext(self.size)
self.draw(in: CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height))
let copiedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return copiedImage
}
}
这很好用。除了消耗大量内存的图形上下文之外,如果您在循环中使用它。这很有趣,因为在过去您需要在这种情况下进行一些手动内存管理,但从 Xcode 12.5 开始,如果您尝试过,您会被告知基础管理这些对象的内存。这似乎是真的,只要这些对象在技术上没有泄露。但是它们也不会在有用的时间尺度上发布,并且内存是针对您自己的应用程序计算的。据推测,这些分配最终会被系统耗尽,但如果这样的速度不足以循环数千张图像,并根据我的经验将它们从磁盘复制到核心数据管理中......而您的应用程序不会因内存使用而关闭。不要害怕,有更好的选择:
extension UIImage {
func deepCopy() -> UIImage? {
guard let data = self.pngData() else { return nil }
return UIImage(data: data, scale: self.scale)
}
}
它使用内存的速度不如第一种方法...尽管它仍然积累到足以需要考虑如何为大型集合主动管理它。但对于单张图片或少量图片,可以这样使用:
if let FileManager.default.fileExists(atPath: workingCopyPath),
let workingCopy = UIImage(contentsOfFile: imagePath),
let securedImage = workingCopy.deepCopy() {
do {
try FileManager.default.removeItem(atPath: imagePath)
return securedImage
} catch let error {
print(error.localizedDescription)
}
}
UIImage 默认情况下不能以这种方式工作当然是有原因的,或者甚至提供了一种方法来做到这一点。您正在将数据读入内存(在第一种情况下实际绘制图像)以创建深层副本。这对于这个问题的意图是必要的 - 考虑到它对性能的影响,请注意何时使用这种方法很重要。
如果在循环中使用这种方法,您需要采取几个额外的步骤来主动管理内存。最重要的是将进程包装在本地自动释放池中,这很简单。然而,这个池的内存只会在你的循环完成时被耗尽,所以如果你正在处理大量的图像,那么就批量处理它们。下面是一个如何做到这一点的例子,它使用一个枚举器,但每次只在内循环中处理 50 个图像:
while try FileManager.default.contentsOfDirectory(atPath: path).count > 0 {
let enumerator = FileManager.default.enumerator(atPath: peopleImagesURL.path)
autoreleasepool {
var count = 0
while let fileName = enumerator?.nextObject() as? String {
// Batch to only 50 images before draining pool, to cap memory use.
guard count < 50 else { break }
count += 1
if let workingCopy = UIImage(contentsOfFile: imagePath),
let securedImage = workingCopy.deepCopy() {
do {
... // do whatever you are going to do with the secured image in the loop.
try FileManager.default.removeItem(atPath: imagePath)
} catch let error {
print(error.localizedDescription)
}
}
}
}
此代码示例的一个限制是它不能处理内部循环由于某种原因无法删除图像的情况。例如,如果您由于某种原因无法将特定图像写入数据库,则在上面的... 中抛出错误,对于该文件,您将永远无法到达FileManager.default.removeItem(atPath:)。结果,外部循环总是会在目录中找到超过零个文件,并且在处理完所有文件之后,您将不断地重新循环内部循环,如果该特定文件永远无法被重新抛出,则可能会重复抛出相同的错误处理。如果这是一种可能性,那么重新设计上述内容以解决此问题是有意义的。
(感谢原始 Stack Overflow 答案 here 和 Swift 版本 here 这是此代码的基础。他们回答了一个相关问题,其中发布者有不同的原因想要进行深度复制,但他们的自己的用例实际上与这里的问题相同。建议将其作为UIImage 上的扩展名归咎于我。在那张纸条上,您实际上可以删除扩展名中的所有self 引用,它会工作得很好......在包含它们的代码示例中似乎更清晰。)