使用大型 UIImage 数组时的内存问题导致崩溃 (swift)
Memory issue when using large UIImage array causes crashing (swift)
在我的应用程序中,我有一个图像数组,其中包含我的相机拍摄的所有图像。我正在使用 collectionView 来显示这些图像。但是,当此图像阵列达到第 20 个左右图像时,它会崩溃。我认为这是由于内存问题造成的。我如何以内存高效的方式将图像存储在图像数组中?
Michael Dauterman 使用缩略图提供了答案。我希望除此之外还有解决方案。也许将图片存储到 NSData 或 CoreData 中?
Camera.swift:
//What happens after the picture is chosen
func imagePickerController(picker:UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject:AnyObject]){
//cast image as a string
let mediaType = info[UIImagePickerControllerMediaType] as! NSString
self.dismissViewControllerAnimated(true, completion: nil)
//if the mediaType it actually is an image (jpeg)
if mediaType.isEqualToString(kUTTypeImage as NSString as String){
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
//Our outlet for imageview
appraisalPic.image = image
//Picture taken, to be added to imageArray
globalPic = image
//image:didFinish.. if we arent able to save, pass to contextInfo in Error Handling
if (newMedia == true){
UIImageWriteToSavedPhotosAlbum(image, self, "image:didFinishSavingWithError:contextInfo:", nil)
}
}
}
NewRecord.swift
var imageArray:[UIImage] = [UIImage]()
viewDidLoad(){
//OUR IMAGE ARRAY WHICH HOLDS OUR PHOTOS, CRASHES AROUND 20th PHOTO ADDED
imageArray.append(globalPic)
//Rest of NewRecord.swift is code which adds images from imageArray to be presented on a collection view
}
我自己 运行 在我自己的应用程序中遇到了低内存问题,这些应用程序必须使用许多高分辨率 UIImage 对象。
解决方案是将图像的 缩略图 保存在 imageArray 中(这会占用更少的内存),然后显示这些图像。如果用户确实需要查看全分辨率图像,您可以让他们点击图像,然后重新加载并显示相机胶卷中的全尺寸 UIImage。
下面是一些允许您创建缩略图的代码:
// image here is your original image
let size = CGSizeApplyAffineTransform(image.size, CGAffineTransformMakeScale(0.5, 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.drawInRect(CGRect(origin: CGPointZero, size: size))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageArray.append(scaledImage)
和more information about these techniques can be found in this NSHipster article.
Swift 4 -
// image here is your original image
let size = image.size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.draw(in: CGRect(origin: .zero, size: size))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
最佳做法是使 imageArray 简短。该数组应该只用于缓存当前滚动范围内的图像(以及为了更好的用户体验而即将显示的图像)。您应该将其余部分保留在 CoreData 中,并在滚动期间动态加载它们。否则,即使使用缩略图,应用程序最终也会崩溃。
当您从视图控制器收到内存警告时,您可以从阵列中删除未显示的照片并将它们保存为文件,然后在需要时再次加载它们等等。或者简单地检测它们何时消失 collectionView:didEndDisplayingCell:forItemAtIndexPath
将它们保存在这样的数组中:
var cachedImages = [(section: Int, row: Int, imagePath: String)]()
使用:
func saveImage(indexPath: NSIndexPath, image: UIImage) {
let imageData = UIImagePNGRepresentation(image)
let documents = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let imagePath = (documents as NSString).stringByAppendingPathComponent("\(indexPath.section)-\(indexPath.row)-cached.png")
if (imageData?.writeToFile(imagePath, atomically: true) == true) {
print("saved!")
cachedImages.append((indexPath.section, indexPath.row, imagePath))
}
else {
print("not saved!")
}
}
让他们回来:
func getImage(indexPath indexPath: NSIndexPath) -> UIImage? {
let filteredCachedImages = cachedImages.filter({ [=12=].section == indexPath.section && [=12=].row == indexPath.row })
if filteredCachedImages.count > 0 {
let firstItem = filteredCachedImages[0]
return UIImage(contentsOfFile: firstItem.imagePath)!
}
else {
return nil
}
}
也可以使用类似this answer的东西来避免阻塞主线程
我举了个例子:find it here
让我从一个简单的答案开始:你不应该自己实施成千上万人都经历过的事情。有一些很棒的库可以通过实现磁盘缓存、内存缓存、缓冲区来解决这个问题。基本上你需要的一切,等等。
我可以向您推荐的两个库如下:
它们都很棒所以这真的是偏好问题(我更喜欢 Haneke),但它们允许您在不同的线程上下载图像,无论是从 Web 还是从您的包,或者从文件系统。他们还有 UIImageView 的扩展,允许您使用 1 行函数轻松加载所有图像,当您加载这些图像时,他们关心加载。
缓存
对于您的特定问题,您可以使用使用这些方法的缓存来处理问题,如下所示(来自文档):
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
现在当你把它放在这个缓存中时,你可以很容易地检索它
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
// image is not nil if image was found
}];
所有内存处理和平衡都由库本身完成,因此您不必担心任何事情。如果图像很大,您可以选择将它与调整大小的方法结合使用以存储较小的图像,但这取决于您。
希望对您有所帮助!
对我有用的最佳途径是使用 PHPhotoLibrary 以完整比例存储一组图像。 PHLibrary 带有缓存和垃圾收集功能。其他解决方案对我的目的不起作用。
ViewDidLoad:
//Check if the folder exists, if not, create it
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
let collection:PHFetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .Any, options: fetchOptions)
if let first_Obj:AnyObject = collection.firstObject{
//found the album
self.albumFound = true
self.assetCollection = first_Obj as! PHAssetCollection
}else{
//Album placeholder for the asset collection, used to reference collection in completion handler
var albumPlaceholder:PHObjectPlaceholder!
//create the folder
NSLog("\nFolder \"%@\" does not exist\nCreating now...", albumName)
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let request = PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(albumName)
albumPlaceholder = request.placeholderForCreatedAssetCollection
},
completionHandler: {(success:Bool, error:NSError!)in
if(success){
println("Successfully created folder")
self.albumFound = true
if let collection = PHAssetCollection.fetchAssetCollectionsWithLocalIdentifiers([albumPlaceholder.localIdentifier], options: nil){
self.assetCollection = collection.firstObject as! PHAssetCollection
}
}else{
println("Error creating folder")
self.albumFound = false
}
})
}
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
//what happens after the picture is chosen
let mediaType = info[UIImagePickerControllerMediaType] as! NSString
if mediaType.isEqualToString(kUTTypeImage as NSString as String){
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
appraisalPic.image = image
globalPic = appraisalPic.image!
if(newMedia == true){
UIImageWriteToSavedPhotosAlbum(image, self, "image:didFinishSavingWithError:contextInfo:", nil)
self.dismissViewControllerAnimated(true, completion: nil)
picTaken = true
println(photosAsset)
}
}
}
您可以只将原始图像数据存储在一个数组中,而不是所有元数据和多余的东西。我不知道你是否需要元数据,但你可以在没有它的情况下四处走动。另一种方法是将每个图像写入一个临时文件,然后再检索它。
使用以下代码在存储图像时减小图像的大小:
var newImage : UIImage
var size = CGSizeMake(400, 300)
UIGraphicsBeginImageContext(size)
image.drawInRect(CGRectMake(0,0,400,300))
newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
我建议优化您的代码,而不是创建一个照片数组,只需创建一个 URL's(ios 版本 < 8.1 来自 AssetLibrary)/localIdentifier(版本 >8.1照片库)并仅在需要时通过这些 URL 获取图像。即显示时。
ARC 有时在将图像存储在数组中的情况下无法正确处理内存管理,并且在许多地方也会导致内存泄漏。
您可以使用 autoreleasepool 来删除 ARC 无法释放的不必要的引用。
进一步补充,如果你通过相机捕获任何图像,那么存储在数组中的大小远远大于图像的大小(虽然我不确定为什么!)。
在我的应用程序中,我有一个图像数组,其中包含我的相机拍摄的所有图像。我正在使用 collectionView 来显示这些图像。但是,当此图像阵列达到第 20 个左右图像时,它会崩溃。我认为这是由于内存问题造成的。我如何以内存高效的方式将图像存储在图像数组中?
Michael Dauterman 使用缩略图提供了答案。我希望除此之外还有解决方案。也许将图片存储到 NSData 或 CoreData 中?
Camera.swift:
//What happens after the picture is chosen
func imagePickerController(picker:UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject:AnyObject]){
//cast image as a string
let mediaType = info[UIImagePickerControllerMediaType] as! NSString
self.dismissViewControllerAnimated(true, completion: nil)
//if the mediaType it actually is an image (jpeg)
if mediaType.isEqualToString(kUTTypeImage as NSString as String){
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
//Our outlet for imageview
appraisalPic.image = image
//Picture taken, to be added to imageArray
globalPic = image
//image:didFinish.. if we arent able to save, pass to contextInfo in Error Handling
if (newMedia == true){
UIImageWriteToSavedPhotosAlbum(image, self, "image:didFinishSavingWithError:contextInfo:", nil)
}
}
}
NewRecord.swift
var imageArray:[UIImage] = [UIImage]()
viewDidLoad(){
//OUR IMAGE ARRAY WHICH HOLDS OUR PHOTOS, CRASHES AROUND 20th PHOTO ADDED
imageArray.append(globalPic)
//Rest of NewRecord.swift is code which adds images from imageArray to be presented on a collection view
}
我自己 运行 在我自己的应用程序中遇到了低内存问题,这些应用程序必须使用许多高分辨率 UIImage 对象。
解决方案是将图像的 缩略图 保存在 imageArray 中(这会占用更少的内存),然后显示这些图像。如果用户确实需要查看全分辨率图像,您可以让他们点击图像,然后重新加载并显示相机胶卷中的全尺寸 UIImage。
下面是一些允许您创建缩略图的代码:
// image here is your original image
let size = CGSizeApplyAffineTransform(image.size, CGAffineTransformMakeScale(0.5, 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.drawInRect(CGRect(origin: CGPointZero, size: size))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
imageArray.append(scaledImage)
和more information about these techniques can be found in this NSHipster article.
Swift 4 -
// image here is your original image
let size = image.size.applying(CGAffineTransform(scaleX: 0.5, y: 0.5))
let hasAlpha = false
let scale: CGFloat = 0.0 // Automatically use scale factor of main screen
UIGraphicsBeginImageContextWithOptions(size, !hasAlpha, scale)
image.draw(in: CGRect(origin: .zero, size: size))
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
最佳做法是使 imageArray 简短。该数组应该只用于缓存当前滚动范围内的图像(以及为了更好的用户体验而即将显示的图像)。您应该将其余部分保留在 CoreData 中,并在滚动期间动态加载它们。否则,即使使用缩略图,应用程序最终也会崩溃。
当您从视图控制器收到内存警告时,您可以从阵列中删除未显示的照片并将它们保存为文件,然后在需要时再次加载它们等等。或者简单地检测它们何时消失 collectionView:didEndDisplayingCell:forItemAtIndexPath
将它们保存在这样的数组中:
var cachedImages = [(section: Int, row: Int, imagePath: String)]()
使用:
func saveImage(indexPath: NSIndexPath, image: UIImage) {
let imageData = UIImagePNGRepresentation(image)
let documents = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0]
let imagePath = (documents as NSString).stringByAppendingPathComponent("\(indexPath.section)-\(indexPath.row)-cached.png")
if (imageData?.writeToFile(imagePath, atomically: true) == true) {
print("saved!")
cachedImages.append((indexPath.section, indexPath.row, imagePath))
}
else {
print("not saved!")
}
}
让他们回来:
func getImage(indexPath indexPath: NSIndexPath) -> UIImage? {
let filteredCachedImages = cachedImages.filter({ [=12=].section == indexPath.section && [=12=].row == indexPath.row })
if filteredCachedImages.count > 0 {
let firstItem = filteredCachedImages[0]
return UIImage(contentsOfFile: firstItem.imagePath)!
}
else {
return nil
}
}
也可以使用类似this answer的东西来避免阻塞主线程
我举了个例子:find it here
让我从一个简单的答案开始:你不应该自己实施成千上万人都经历过的事情。有一些很棒的库可以通过实现磁盘缓存、内存缓存、缓冲区来解决这个问题。基本上你需要的一切,等等。
我可以向您推荐的两个库如下:
它们都很棒所以这真的是偏好问题(我更喜欢 Haneke),但它们允许您在不同的线程上下载图像,无论是从 Web 还是从您的包,或者从文件系统。他们还有 UIImageView 的扩展,允许您使用 1 行函数轻松加载所有图像,当您加载这些图像时,他们关心加载。
缓存
对于您的特定问题,您可以使用使用这些方法的缓存来处理问题,如下所示(来自文档):
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
现在当你把它放在这个缓存中时,你可以很容易地检索它
SDImageCache *imageCache = [[SDImageCache alloc] initWithNamespace:@"myNamespace"];
[imageCache queryDiskCacheForKey:myCacheKey done:^(UIImage *image) {
// image is not nil if image was found
}];
所有内存处理和平衡都由库本身完成,因此您不必担心任何事情。如果图像很大,您可以选择将它与调整大小的方法结合使用以存储较小的图像,但这取决于您。
希望对您有所帮助!
对我有用的最佳途径是使用 PHPhotoLibrary 以完整比例存储一组图像。 PHLibrary 带有缓存和垃圾收集功能。其他解决方案对我的目的不起作用。
ViewDidLoad:
//Check if the folder exists, if not, create it
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %@", albumName)
let collection:PHFetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .Any, options: fetchOptions)
if let first_Obj:AnyObject = collection.firstObject{
//found the album
self.albumFound = true
self.assetCollection = first_Obj as! PHAssetCollection
}else{
//Album placeholder for the asset collection, used to reference collection in completion handler
var albumPlaceholder:PHObjectPlaceholder!
//create the folder
NSLog("\nFolder \"%@\" does not exist\nCreating now...", albumName)
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let request = PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(albumName)
albumPlaceholder = request.placeholderForCreatedAssetCollection
},
completionHandler: {(success:Bool, error:NSError!)in
if(success){
println("Successfully created folder")
self.albumFound = true
if let collection = PHAssetCollection.fetchAssetCollectionsWithLocalIdentifiers([albumPlaceholder.localIdentifier], options: nil){
self.assetCollection = collection.firstObject as! PHAssetCollection
}
}else{
println("Error creating folder")
self.albumFound = false
}
})
}
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) {
//what happens after the picture is chosen
let mediaType = info[UIImagePickerControllerMediaType] as! NSString
if mediaType.isEqualToString(kUTTypeImage as NSString as String){
let image = info[UIImagePickerControllerOriginalImage] as! UIImage
appraisalPic.image = image
globalPic = appraisalPic.image!
if(newMedia == true){
UIImageWriteToSavedPhotosAlbum(image, self, "image:didFinishSavingWithError:contextInfo:", nil)
self.dismissViewControllerAnimated(true, completion: nil)
picTaken = true
println(photosAsset)
}
}
}
您可以只将原始图像数据存储在一个数组中,而不是所有元数据和多余的东西。我不知道你是否需要元数据,但你可以在没有它的情况下四处走动。另一种方法是将每个图像写入一个临时文件,然后再检索它。
使用以下代码在存储图像时减小图像的大小:
var newImage : UIImage
var size = CGSizeMake(400, 300)
UIGraphicsBeginImageContext(size)
image.drawInRect(CGRectMake(0,0,400,300))
newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
我建议优化您的代码,而不是创建一个照片数组,只需创建一个 URL's(ios 版本 < 8.1 来自 AssetLibrary)/localIdentifier(版本 >8.1照片库)并仅在需要时通过这些 URL 获取图像。即显示时。
ARC 有时在将图像存储在数组中的情况下无法正确处理内存管理,并且在许多地方也会导致内存泄漏。
您可以使用 autoreleasepool 来删除 ARC 无法释放的不必要的引用。
进一步补充,如果你通过相机捕获任何图像,那么存储在数组中的大小远远大于图像的大小(虽然我不确定为什么!)。