Image in collectionView cell causing Terminated due to memory issue
Asked Answered
K

1

1

I looked at several answers for this problem but none helped.

vc1 is a regular vc, I grab 20 images from firebase, in vc2 I use ARKit to display those images upon user selection. I have a collectionView in vc2 that paginates 20 more images from firebase. The problem is when the next 20 images are loading and I start scrolling, the app crashes with Message from debugger: Terminated due to memory issue. When scrolling those new images, I look at the memory graph and it shoots up to 1 gig, so that's the reason for the crash. ARKit and the nodes I have floating around also contribute to the memory bump but they are not the reason for the crash as stated below.

1- Inside the cell I use SDWebImage to display the image inside the imageView. Once I comment out SDWebImage everything works, scrolling is smooth, and no more crashes but of course I can't see the image. I switched to URLSession.shared.dataTask and the same memory issue reoccurs.

2- The images were initially taken with the iPhone full screen camera and saved with jpegData(compressionQuality: 0.3). The cell size is 40x40. Inside the the SDWebImage completion block I tried to resize the image but the memory crash still persists.

3- I used Instruments > Leaks to look for memory leaks and a few Foundation leaks appeared but when I dismiss vc2 Deinit always runs. Inside vc2 there aren't any long running timers or loops and I use [weak self] inside all of the closures.

4- As I stated in the second point the problem is definitely the imageView/image because once I remove it from the process everything works fine. If I don't show any images everything works fine (40 red imageViews with no images inside of them will appear).

What can I do to fix this issue?

Paginating to pull more images

for child in snapshot.children.allObjects as! [DataSnapshot] {

    guard let dict = child.value as? [String:Any] else { continue }
    let post = Post(dict: dict)

    datasource.append(post)

    let lastIndex = datasource.count - 1
    let indexPath = IndexPath(item: lastIndex, section: 0)
    UIView.performWithoutAnimation {
        collectionView.insertItems(at: [indexPath])
    }
}

cellForItem:

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

    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellId, for: indexPath) as! PostCell

    cell.resetAll()
    cell.post = dataSource[indexPath.item]

    return cell
}

PostCell

private lazy var imageView: UIImageView = {
    let iv = UIImageView()
    iv.translatesAutoresizingMaskIntoConstraints = false
    iv.contentMode = .scaleAspectFill
    iv.layer.cornerRadius = 15
    iv.layer.masksToBounds = true
    iv.backgroundColor = .red
    iv.isHidden = true
    return iv
}()

private lazy var spinner: UIActivityIndicatorView = {
    let actIndi = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.medium) // ...
}()

var post: Post? {
    didSet {

        guard let urlSr = post?.urlStr, let url = URL(string: urlSr) else { return }

        spinner.startAnimating()

        // if I comment this out everything works fine
        imageView.sd_setImage(with: url, placeholderImage: placeHolderImage, options: [], completed: { 
            [weak self] (image, error, cache, url) in

            // in here I tried resizing the image but no diff

            DispatchQueue.main.async { [weak self] in
               self?.showAll()
            }
         })

        setAnchors() 
    }
}

func resetAll() {
     spinner.stopAnimating()
     imageView.image = nil
     imageView.removeFromSuperView()
     imageView.isHidden = true
}

func showAll() {
     spinner.stopAnimating()
     imageView.isHidden = false
}

func setAnchors() {

    contentView.addSubview(imageView)
    imageView.addSubview(cellSpinner)
    // imageView is pinned to all sides
}
Kielty answered 12/9, 2020 at 0:21 Comment(6)
“The images were initially taken with the iPhone full screen camera” Well that’s it right there. You cannot possibly load 20 camera images at once. You say your cell size is 40x40 so your image size needs to be 40x40 (or less).Drogin
@Drogin Inside the SDWebImage callback I resized the image using this https://mcmap.net/q/144842/-how-to-resize-image-in-swift. It didn’t workKielty
You think adding the spinner over and over is causing any of this?. I don’t get the point of adding and removing the views over and over. It’s probably the images but something about adding the views over and over makes me wonder. Why not leave them?Protuberant
@Protuberant I completely commented out the spinner to see what happens, same exact problem. It is 100% because of the image. I never leave them because of cell reuse and the wrong image never appears on the wrong cell. I built about 7 apps or something and this is the same procedure I use in all of the apps, I never had a problem before. ARKit and the nodes make a major difference in memory consumption. It definitely has something to do with the image because when I use a generic image it works fine.Kielty
“It didn’t work” Meaning what? If you’re running out of memory because of the images, which is very easy to know using Instruments, then it didn’t resize them. I know nothing of SDWebImage but I do know to load from disk a small version of a big image file, without caching and without ever having the big image in memory.Drogin
@Drogin once again you were 100% correct :) I have no idea why that that other link didn't work but I used a different one https://mcmap.net/q/244275/-square-thumbnail-from-uiimagepickerviewcontroller-image. I combined it with URLSession.dataTask and the image shrunk so no more crashes. I did run into another issue because if I scroll to fast by the time dataTask returns an image it returns the image on the wrong cell. But that's another problem. I'll try to tackle it with prefetching and if no bueno I'll post a question. Thanks for the help!!!Kielty
K
0

In the comments @matt was 100% correct , the problem was the images were too large for the collectionView at size 40x40, I needed to resize the images. For some reason the resize link in my question didn't work so I used this to resize image instead. I also used URLSession and this answer

I did it in 10 steps, all commented out

// 0. set this anywhere outside any class
let imageCache = NSCache<AnyObject, AnyObject>()

PostCell:

private lazy var imageView: UIImageView = { ... }()
private lazy var spinner: UIActivityIndicatorView = { ... }()

// 1. override prepareForReuse, cancel the task and set it to nil, and set the imageView.image to nil so that the wrong images don't appear while scrolling
override func prepareForReuse() {
    super.prepareForReuse()
    
    task?.cancel()
    task = nil
    imageView.image = nil
}

// 2. add a var to start/stop a urlSession when the cell isn't on screen
private var task: URLSessionDataTask?

var post: Post? {
    didSet {

        guard let urlStr = post?.urlStr else { return }

        spinner.startAnimating()

        // 3. crate the new image from this function
        createImageFrom(urlStr)

        setAnchors() 
    }
}

func createImageFrom(_ urlStr: String) {
    
    if let cachedImage = imageCache.object(forKey: urlStr as AnyObject) as? UIImage {
        
        // 4. if the image is in cache call this function to show the image
        showAllAndSetImageView(with: cachedImage)
        
    } else {
        
        guard let url = URL(string: urlStr) else { return }

        // 5. if the image is not in cache start a urlSession and initialize it with the task variable from step 2
        task = URLSession.shared.dataTask(with: url, completionHandler: {
            [weak self](data, response, error) in
            
            if let error = error { return }
            
            if let response = response as? HTTPURLResponse {
                print("response.statusCode: \(response.statusCode)")
                guard 200 ..< 300 ~= response.statusCode else { return }
            }
            
            guard let data = data, let image = UIImage(data: data) else { return }
            
            // 6. add the image to cache
            imageCache.setObject(image, forKey: photoUrlStr as AnyObject)
            
            DispatchQueue.main.async { [weak self] in

                // 7. call this function to show the image
                self?.showAllAndSetImageView(with: image)
            }
            
        })
        task?.resume()
    }
}

func showAllAndSetImageView(with image: UIImage) {

    // 8. resize the image
    let resizedImage = resizeImageToCenter(image: image, size: 40) // can also use self.frame.height

    imageView.image = resizeImage

    showAll()
}

// 9. func to resize the image
func resizeImageToCenter(image: UIImage, size: CGFloat) -> UIImage {
    let size = CGSize(width: size, height: size)

    // Define rect for thumbnail
    let scale = max(size.width/image.size.width, size.height/image.size.height)
    let width = image.size.width * scale
    let height = image.size.height * scale
    let x = (size.width - width) / CGFloat(2)
    let y = (size.height - height) / CGFloat(2)
    let thumbnailRect = CGRect.init(x: x, y: y, width: width, height: height)

    // Generate thumbnail from image
    UIGraphicsBeginImageContextWithOptions(size, false, 0)
    image.draw(in: thumbnailRect)
    let thumbnail = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return thumbnail!
}

func resetAll() { ... }
func showAll() { ... }
func setAnchors() { ... }
Kielty answered 12/9, 2020 at 12:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.