使用大型UIImage数组时出现内存问题导致崩溃(Swift)

8
在我的应用程序中,我有一个图像数组,其中包含所有在相机上拍摄的图像。我正在使用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
}

你是否考虑过使用图像路径数组,并使用collectionView来回收活动视图,只在需要时加载图像? - Aggressor
7个回答

8
我自己的应用程序也遇到了低内存问题,因为它们需要处理许多高分辨率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)

这些技术的更多信息可以在NSHipster文章中找到。

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

我该如何从相机胶卷中获取原始大小的图像? - Josh O'Connor
在这个相关的问题中,可能有一个解决方案可以帮助你:https://dev59.com/vm855IYBdhLWcg3wPBtx。 - Michael Dautermann
谢谢。我可能会这样做。将照片存储为NSData是否是另一种解决方案? - Josh O'Connor
有没有办法在拍照时拍摄低质量的图像而不是全质量的图像?在我的图像添加到数组之前。 - Josh O'Connor

6
最佳实践是保持imageArray的长度较短。数组应该仅用于缓存当前滚动范围内的图像(以及即将显示的图像,以获得更好的用户体验)。您应该将其余部分保存在CoreData中,并在滚动期间动态加载它们。否则,即使使用缩略图,应用程序最终也会崩溃。

@JoshO'Connor 是的,可以使用GCD在后台进行。无论如何,你都不应该仅仅将图像保存在内存中,因为当应用程序手动关闭或崩溃时,图像将永远消失。 - zhubofei
好的。你能提供示例代码或项目供我参考吗?我以前从未使用过CoreData。还在学习中。 - Josh O'Connor
@Josh O'Connor 保存图像到CoreData的示例代码 链接 - zhubofei
@Josh O'Connor 这是一篇关于如何设置CoreData的教程 链接 - zhubofei
Parse已经在其SDK中内置了本地数据存储 https://www.parse.com/docs/ios/guide#local-datastore。你应该毫不犹豫地使用他们的解决方案,而不是自己编写。还有一个实用的ParseUI pod https://github.com/ParsePlatform/ParseUI-iOS可供参考。它包括一个PFQueryCollectionViewController以及一个PFImageView类,帮助你处理图像检索。 - zhubofei
显示剩余2条评论

3
让我从简单的答案开始:你不应该自己实现已经被成千上万人使用过的东西。有一些很棒的库可以通过实现磁盘缓存、内存缓存、缓冲区等来解决这个问题。基本上你所需要的一切,还有更多。
我可以向你推荐两个库:
- Haneke - SDWebImage 两个库都非常好,只是个人偏好不同(我更喜欢 Haneke),但它们允许你在不同的线程上下载图像,无论是从 Web、从你的 bundle 还是从文件系统中获取。它们也为 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
}];

所有的内存处理和平衡都由库本身完成,所以您不必担心任何事情。您可以选择将其与调整大小方法结合使用,以存储较小的图像(如果这些图像很大),但这取决于您自己。希望这有所帮助!

我尝试使用Haneke,它似乎是解决方案,但文档让我感到困惑。在您上面的代码中,您包括了“SDImageCache”和“initWithNamespace”,这两个东西在github文档中根本没有提到。当我把您的代码放进去时,它给我带来了大量的错误,比如“未声明类型的使用,SDImageCache”。如果文档中没有提到这样的代码,我该怎么知道呢? - Josh O'Connor
我想使用Haneke,但我不知道如何使用它。 - Josh O'Connor
然后你需要了解如何通常使用库——我猜测你的问题就出在那里。 - Jiri Trecak
LOL。谢谢你的帮助,让我感到沮丧,并没有提供任何建议!! - Josh O'Connor

1
当您从视图控制器收到内存警告时,您可以从数组中删除未显示的照片并将它们保存为文件,然后在需要时重新加载它们。或者只需使用 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({ $0.section == indexPath.section && $0.row == indexPath.row })

    if filteredCachedImages.count > 0 {
        let firstItem = filteredCachedImages[0]
        return UIImage(contentsOfFile: firstItem.imagePath)!
    }
    else {
        return nil
    }
}

为避免阻塞主线程,也可以使用类似 this answer 的东西。

我制作了一个示例:在此处找到它


谢谢!我相信这就是我要找的答案...问题:内存警告是由于显示的图片吗?还是由于将图片存储到图像数组中?也就是说,如果我根本不显示这些图片,我应该或者不应该有内存问题... - Josh O'Connor
我会在稍后有空的时候处理这个问题,如果这是解决方案,我会给你奖励。 - Josh O'Connor
1
内存警告是由于所有对象都在内存中,即使它们没有显示出来。例如,如果您在数组中引用了一张图片,它也会占用空间。该方法(didReceiveMemoryWarning())的目的非常明确:// 释放可以重新创建的任何资源。:D - dGambit
谢谢!我明天会试一下。很抱歉测试你的答案并奖励赏金花费了这么长时间,我一直忙于工作。非常感谢!:D - Josh O'Connor
嘿,dGambit。我已经在这上面工作了一整天,但是我无法成功使用saveImage函数保存图片。你有任何可以让我参考的能够达到此目的的示例项目吗? - Josh O'Connor
我编辑了答案,并在底部添加了一个谷歌云盘文件:D - dGambit

1
使用以下代码来在存储图像时减小其大小:
       var newImage : UIImage
       var size = CGSizeMake(400, 300)
       UIGraphicsBeginImageContext(size)
       image.drawInRect(CGRectMake(0,0,400,300))
       newImage = UIGraphicsGetImageFromCurrentImageContext()
       UIGraphicsEndImageContext()

我建议优化你的代码,而不是创建一个照片数组,只需创建一个URL数组(ios版本<8.1从AssetLibrary / localIdentifier(版本> 8.1 Photos Library)),并且仅在需要时通过这些URL获取图像。即在显示时。
ARC在存储图像数组时有时无法正确处理内存管理,这也会导致许多地方出现内存泄漏。
您可以使用autoreleasepool来删除无法由ARC释放的不必要引用。
此外,如果您通过相机捕获任何图像,则存储在数组中的大小远大于图像的大小(尽管我不确定为什么!)。

0
你可以将原始图像数据存储在数组中,而不是所有的元数据和多余的内容。我不知道你是否需要元数据,但你可能可以不用它来解决问题。另一个选择是将每个图像写入临时文件,然后稍后检索它。

-3
我发现最好的方法是使用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)


        }
        }
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接