这个问题的答案揭示了一些关于工作方式UIImage
和cgImage
工作方式的有趣事情。UIImage 对象不直接保存图像数据,而是保存对数据外部存储的引用。
正如Apple 的文档所指出的,您可以通过 或 上的任何一个属性来访问它cgImage
,ciImage
因此UIImage
您会认为访问这些属性和调用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 答案和此处的Swift 版本,这是此代码的基础。他们回答了一个相关问题,其中发布者有不同的原因想要进行深度复制,但他们自己的用例实际上是与这里的问题相同。建议将其作为对UIImage
谎言的扩展而归咎于我。在那张纸条上,您实际上可以删除self
扩展中的所有引用,它会工作得很好......只是在代码中看起来更清晰示例以包括它们。)