【问题标题】:Memory issue when using large UIImage array causes crashing (swift)使用大型 UIImage 数组时的内存问题导致崩溃(swift)
【发布时间】:2015-11-01 02:44:30
【问题描述】:

在我的应用程序中,我有一个图像数组,其中包含在我的相机上拍摄的所有图像。我正在使用 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 以便您可以回收活动视图并仅根据需要加载图像?

标签: ios swift camera uiimage


【解决方案1】:

我自己在自己的应用程序中遇到了内存不足的问题,这些应用程序必须使用许多高分辨率 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。

斯威夫特 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()

【讨论】:

  • 如何从相机胶卷中获取完整尺寸的图像?
  • 谢谢。我可能会这样做。将照片存储为 NSData 是另一种解决方案吗?
  • 有没有办法在拍摄照片时拍摄较低质量的图像而不是完整质量的图像?在我的图像添加到我的数组之前。
【解决方案2】:

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

【讨论】:

  • 你是在建议我将每张图片存储在 CoreData 中吗?
  • @JoshO'Connor 是的,并使用 GCD 在后台执行此操作。无论如何,您不应该只将图像保存在内存中,在应用程序手动关闭或崩溃后,图像将永远消失。
  • 听起来不错。你能提供示例代码或我可以参考的项目吗?我以前从未使用过CoreData。还在学习中。
  • @Josh O'Connor 将图像保存到 CoreData 的示例代码 link
  • @Josh O'Connor 这是关于如何设置 CoreData link 的教程
【解决方案3】:

当您收到来自视图控制器的内存警告时,您可以从数组中删除未显示的照片并将它们保存为文件,然后在需要时再次加载它们等等。或者简单地用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 之类的东西以避免阻塞主线程

我做了一个例子:find it here

【讨论】:

  • 谢谢!我相信这就是我正在寻找的答案... 问题:内存警告是否由于图片正在显示?还是将图片存储到图像阵列中? Aka,如果我根本没有显示这些图片,我应该或不应该有内存问题......
  • 我会在以后有空的时候解决这个问题,如果这是解决方案,我会给你赏金。
  • 内存警告是由于内存中的所有对象,即使它们没有显示,例如,如果你有一个数组中的图像的引用,它将占用空间。该方法(didReceiveMemoryWarning())的目的非常明确:// Dispose of any resources that can be recreated.:D
  • 谢谢!我明天会试试这个。抱歉,测试您的答案并奖励赏金花了这么长时间,我一直在努力工作。非常感激! :D
  • 嘿dGambit。我整天都在做这个,我无法使用 saveImage 函数成功保存图片。您是否有任何可以执行此操作的示例项目,我可以看看?
【解决方案4】:

让我从简单的答案开始:您不应该自己实现成千上万人经历过的东西。有一些很棒的库可以自己解决这个问题,通过实现磁盘缓存、内存缓存、缓冲区......基本上你需要的一切,等等。

我可以向您推荐的两个库如下:

它们都很棒,所以它确实是偏好问题(我更喜欢 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
}];

所有的内存处理和平衡都是由库本身完成的,所以你不必担心任何事情。如果图像很大,您可以选择将其与调整大小的方法结合起来存储较小的图像,但这取决于您。

希望对你有帮助!

【讨论】:

  • 我尝试使用 Haneke,这似乎是解决方案,但文档让我迷失了方向。在您上面的代码中,您包括“SDImageCache”和“initWithNamespace”,这两个东西在 github 文档中根本没有讨论过。当我把你的代码放进去时,它给了我很多错误,比如“使用未声明的类型,SDImageCache”。如果文档中没有提到这样的代码,我怎么知道?
  • 我想使用 Haneke 但我不知道如何使用它。
  • 那么你需要研究一下如何使用库——我怀疑你的问题在那里
  • 哈哈。感谢您的帮助让我失望并且没有提供任何建议!
【解决方案5】:

对我来说最有效的方法是使用 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)


        }
        }
 }

【讨论】:

    【解决方案6】:

    您可以只将原始图像数据存储在一个数组中,而不是所有元数据和多余的东西。我不知道你是否需要元数据,但没有它你也许可以解决。另一种选择是将每个图像写入一个临时文件,然后再检索它。

    【讨论】:

      【解决方案7】:

      使用以下代码在存储时减小图像的大小:

             var newImage : UIImage
             var size = CGSizeMake(400, 300)
             UIGraphicsBeginImageContext(size)
             image.drawInRect(CGRectMake(0,0,400,300))
             newImage = UIGraphicsGetImageFromCurrentImageContext()
             UIGraphicsEndImageContext()
      

      我建议优化您的代码,而不是创建一组照片,只需创建一个 URL 数组(来自 AssetLibrary 的 ios 版本 8.1 照片库)并仅在需要时通过这些 URL 获取图像.即在显示时。

      在将图像存储在数组中的情况下,ARC有时无法正确处理内存管理,并且在很多地方也会导致内存泄漏。

      您可以使用 autoreleasepool 来删除 ARC 无法释放的不必要的引用。

      再补充一点,如果您通过摄像头捕获任何图像,那么存储在数组中的大小远远大于图像的大小(虽然我不确定为什么!)。

      【讨论】:

        猜你喜欢
        • 2012-05-03
        • 2011-03-27
        • 2012-09-20
        • 2019-03-25
        • 2013-05-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多