Memory growing out of control when loading users photo album in UICollecitonView

1.1k Views Asked by At

I am loading the photos from a users photo album into a collection view similar to how is done in this Apple Sample project. I can not seem to track down why the memory is growing out of control. I use the suggested PHCachingImageManager but all that results are blurry images, freezing scrolling and memory growing out of control until the application crashes.

In my viewDidLoad I run the code below

        PHPhotoLibrary.requestAuthorization { (status: PHAuthorizationStatus) in
            print("photo authorization status: \(status)")
            if status == .authorized && self.fetchResult == nil {
                print("authorized")

                let fetchOptions = PHFetchOptions()
                fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
                var tempArr:[PHAsset] = []
                self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)

                guard let fetchResult = self.fetchResult else{
                    print("Fetch result is empty")
                    return
                }

                fetchResult.enumerateObjects({asset, index, stop in
                    tempArr.append(asset)
                })
//                self.assets = tempArr

                self.imageManager.startCachingImages(for: tempArr, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)

                tempArr.removeAll()
                print("Asset count after initial fetch: \(self.assets?.count)")

                DispatchQueue.main.async {
                    // Reload collection view once we've determined our Photos permissions.
                    print("inside of main queue reload")
                    PHPhotoLibrary.shared().register(self)
                    self.collectionView.delegate = self
                    self.collectionView.dataSource = self
                    self.collectionView.reloadData()
                }
            } else {
                print("photo access denied")
                self.displayPhotoAccessDeniedAlert()
            }
        }

and inside of cellForItemAt: I run the following code

cellForItemAt

  guard let fetchResult = self.fetchResult else{
            print("Fetch Result is empty")
            return UICollectionViewCell()
        }

        let requestOptions = PHImageRequestOptions()
        requestOptions.isSynchronous = false
        requestOptions.deliveryMode = .highQualityFormat
        //let scale = min(2.0, UIScreen.main.scale)
        let scale = UIScreen.main.scale
        let targetSize = CGSize(width: cell.bounds.width * scale, height: cell.bounds.height * scale)

//        let asset = assets[indexPath.item]
        let asset = fetchResult.object(at: indexPath.item)
        let assetIdentifier = asset.localIdentifier

        cell.representedAssetIdentifier = assetIdentifier

        imageManager.requestImage(for: asset, targetSize: cell.frame.size,
                                              contentMode: .aspectFill, options: requestOptions) { (image, hashable)  in
                                                if let loadedImage = image, let cellIdentifier = cell.representedAssetIdentifier {

                                                    // Verify that the cell still has the same asset identifier,
                                                    // so the image in a reused cell is not overwritten.
                                                    if cellIdentifier == assetIdentifier {
                                                        cell.imageView.image = loadedImage
                                                    }
                                                }
        }
1

There are 1 best solutions below

8
BlackMirrorz On

I had a similar problem this week using the Apple Code which for others reference is available here Browsing & Modifying Photos

Memory usage was very high, and then if viewing a single item and returning to root, memory would spike and the example would crash.

As such from our experiments there were a few tweaks which improved performance.

Firstly when setting the thumbnailSize for the requestImage function:

open func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID

We set the scale like so instead of using the full size:

UIScreen.main.scale * 0.75

We also set the PHImageRequestOptions Resizing Mode to .fast.

As well as this we found that setting the following variables of the CollectionViewCell also helped somewhat:

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
isOpaque = true

We also noticed that the updateCachedAssets() in the ScrollViewwDidScroll method was playing some part in this process so we removed that from this callback(rightly or wrongly).

And one final thing was the we kept a reference to the PHCachingImageManager for each cell and if it existed then we called:

open func cancelImageRequest(_ requestID: PHImageRequestID)

As such here is the code for our MediaCell:

extension MediaCell{

  /// Populates The Cell From The PHAsset Data
  ///
  /// - Parameter asset: PHAsset
  func populateCellFrom(_ asset: PHAsset){

    livePhotoBadgeImage = asset.mediaSubtypes.contains(.photoLive) ? PHLivePhotoView.livePhotoBadgeImage(options: .overContent) : nil

    videoDuration = asset.mediaType == .video ? asset.duration.formattedString() : ""

    representedAssetIdentifier = asset.localIdentifier
  }


  /// Shows The Activity Indicator When Downloading From The Cloud
  func startAnimator(){
    DispatchQueue.main.async {
      self.activityIndicator.isHidden = false
      self.activityIndicator.startAnimating()
    }
  }


  /// Hides The Activity Indicator After The ICloud Asset Has Downloaded
  func endAnimator(){
    DispatchQueue.main.async {
      self.activityIndicator.isHidden = true
      self.activityIndicator.stopAnimating()
    }
  }

}

final class MediaCell: UICollectionViewCell, Animatable {

  @IBOutlet private weak var imageView: UIImageView!
  @IBOutlet private weak var livePhotoBadgeImageView: UIImageView!
  @IBOutlet private weak var videoDurationLabel: UILabel!
  @IBOutlet weak var activityIndicator: UIActivityIndicatorView!{
    didSet{
      activityIndicator.isHidden = true
    }
  }

  var representedAssetIdentifier: String!
  var requestIdentifier: PHImageRequestID!

  var thumbnailImage: UIImage! {
    didSet {
      imageView.image = thumbnailImage
    }
  }

  var livePhotoBadgeImage: UIImage! {
    didSet {
      livePhotoBadgeImageView.image = livePhotoBadgeImage
    }
  }

  var videoDuration: String!{
    didSet{
     videoDurationLabel.text = videoDuration
    }
  }

  //----------------
  //MARK:- LifeCycle
  //----------------

  override func awakeFromNib() {
    layer.shouldRasterize = true
    layer.rasterizationScale = UIScreen.main.scale
    isOpaque = true
  }

  override func prepareForReuse() {
    super.prepareForReuse()
    imageView.image = nil
    representedAssetIdentifier = ""
    requestIdentifier = nil
    livePhotoBadgeImageView.image = nil
    videoDuration = ""
    activityIndicator.isHidden = true
    activityIndicator.stopAnimating()
  }

}

And the code for the cellForItem:

 override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let asset = dataViewModel.assettAtIndexPath(indexPath)

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaCell", for: indexPath) as! MediaCell

    if let requestID = cell.requestIdentifier { imageManager.cancelImageRequest(requestID) }

    cell.populateCellFrom(asset)

    let options = PHImageRequestOptions()
    options.resizeMode = .fast
    options.isNetworkAccessAllowed = true

    options.progressHandler = { (progress, error, stop, info) in

      if progress == 0.0{
        cell.startAnimator()
      } else if progress == 1.0{
        cell.endAnimator()
      }
    }

    cell.requestIdentifier = imageManager.requestImage(for: asset, targetSize: thumbnailSize,
                                                       contentMode: .aspectFill, options: options,
                                                       resultHandler: { image, info in

                                                        if cell.representedAssetIdentifier == asset.localIdentifier {

                                                          cell.thumbnailImage = image


                                                        }

    })

    return cell
  }

One additional area is in the updateCachedAssets() funtion. You are using:

self.imageManager.startCachingImages(for: tempArr, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)

It would probably be best to set a smaller size here e.g:

imageManager.startCachingImages(for: addedAssets,
                                targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)

Whereby thumbnail size e.g:

/// Sets The Thumnail Image Size
private func setupThumbnailSize(){

let scale = isIpad ? UIScreen.main.scale : UIScreen.main.scale * 0.75
let cellSize = collectionViewFlowLayout.itemSize
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)

}

All of these tweaks helped to ensure that the memory usage remained fair constant, and in our testing ensured that there were no exceptions thrown.

Hope it helps.