Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

High memory usage in UICollectionView [duplicate]

My current assignment is a iOS keyboard extension, which among other things offers all iOS-supported Emoji's (yes, I know iOS has a builtin Emoji keyboard, but the goal is to have one included in the keyboard extension).

For this Emoji Layout, which is basically supposed to be a scroll view with all emojis in it in a grid order, I decided to use an UICollectionView, as it only creates a limited number of cells and reuses them. (There are quite a lot of emojis, over 1'000.) These cells simply contain a UILabel, which holds the emoji as its text, with a GestureRecognizer to insert the tapped Emoji.

However, as I scroll through the list, I can see the memory usage exploding for somewhere around 16-18MB to over 33MB. While this doesn't trigger a memory warning on my iPhone 5s yet, it may as well on other devices, as app extensions are only dedicated a very sparse amount of resources.

EDIT: Sometimes I do receive a memory warning, mostly when switching back to the 'normal' keyboard layout. Most times, the memory usage drops below 20MB when switching back, but not always.

How can I reduce the amount of memory used by this Emoji Layout?


class EmojiView: UICollectionViewCell {

    //...

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.userInteractionEnabled = true
        let l = UILabel(frame: self.contentView.frame)
        l.textAlignment = .Center
        self.contentView.addSubview(l)
        let tapper = UITapGestureRecognizer(target: self, action: "tap:")
        self.addGestureRecognizer(tapper)
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        //We know that there only is one subview of type UILabel
        (self.contentView.subviews[0] as! UILabel).text = nil
    }
}

//...

class EmojiViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    //...

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        //The reuse id "emojiCell" is registered in the view's init.
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("emojiCell", forIndexPath: indexPath)
        //Get recently used emojis
        if indexPath.section == 0 {
            (cell.contentView.subviews[0] as! UILabel).text = recent.keys[recent.startIndex.advancedBy(indexPath.item)]
        //Get emoji from full, hardcoded list
        } else if indexPath.section == 1 {
            (cell.contentView.subviews[0] as! UILabel).text = emojiList[indexPath.item]
        }
        return cell
    }

    //Two sections: recently used and complete list
    override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 2
    }

}

let emojiList: [String] = [
    "\u{1F600}",
    "\u{1F601}",
    "\u{1F602}",
    //...
    // I can't loop over a range, there are
    // unused values and gaps in between.
]

Please let me know if you need more code and/or information.

Edit: My guess is that iOS keeps the rendered emojis somewhere in the memory, despite setting the text to nil before reuse. But I may be completely wrong...

EDIT: As suggested by JasonNam, I ran the keyboard using Xcode's Leaks tool. There I noticed two things:

  • VM: CoreAnimation goes up to about 6-7MB when scrolling, but I guess this may be normal when scrolling through a collection view.
  • Malloc 16.00KB, starting at a value in the kilobytes, shoots up to 17MB when scrolling through the whole list, so there is a lot of memory being allocated, but I can't see anything else actually using it.

But no leaks were reported.

EDIT2: I just checked with CFGetRetainCount (which still works when using ARC) that the String objects do not have any references left once the nil value in prepareForReuse is set.

I'm testing on an iPhone 5s with iOS 9.2, but the problem also appears in the simulator using a iPhone 6s Plus.

EDIT3: Someone had the exact same problem here, but due to the strange title, I didn't find it up to now. It seems the only solution is to use UIImageViews with UIImages in the list, as UIImages in UICollectionView's are properly released on cell reuse.

like image 452
s3lph Avatar asked Dec 16 '15 16:12

s3lph


3 Answers

it's pretty interesting, in my testing project, i commented out the prepareForReuse part in the EmojiView, and the memory usage became steady, project started at 19MB and never goes above 21MB, the (self.contentView.subviews[0] as! UILabel).text = nil is causing the issues in my test.

like image 87
Allen Avatar answered Nov 14 '22 09:11

Allen


I think you don't use storyboard to design the collection view. I searched around and found out that you need to register the class with identifier before you populate the collection view cell. Try to call the following method on viewDidLoad or something.

collectionView.registerClass(UICollectionViewCell.self , forCellWithReuseIdentifier: "emojiCell")
like image 42
Jason Nam Avatar answered Nov 14 '22 09:11

Jason Nam


Since you have memory issues you should try lazy loading your labels.

// Define an emojiLabel property in EmojiView.h
var emojiLabel: UILabel!

// Lazy load your views in your EmojiView.m
lazy var emojiLabel: UILabel  = {
    var tempLabel: UIImageView = UILabel(frame: self.contentView.frame)
    tempLabel.textAlignment = .Center
    tempLabel.userInteractionEnabled = true
    contentView.addSubview(tempLabel)

    return tempLabel;
}()

override func prepareForReuse() {
    super.prepareForReuse()
    emojiLabel.removeFromSuperview()
    emojiLabel = nil
}

//...

class EmojiViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    //...

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        //The reuse id "emojiCell" is registered in the view's init.
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier("emojiCell", forIndexPath: indexPath) as! EmojiView
        //Get recently used emojis
        if indexPath.section == 0 {
            cell.emojiLabel.text = recent.keys[recent.startIndex.advancedBy(indexPath.item)]
        //Get emoji from full, hardcoded list
        } else if indexPath.section == 1 {
            cell.emojiLabel.text = emojiList[indexPath.item]
        }
        return cell
    }

That way you're certain that the label is released when you scroll.

Now I have one question. Why do you add a gesture recognizer to your EmojiViews ? UICollectionView already implements this functionality with its didSelectItemAtIndexPath: delegate. Allocating extra gestureRecognizers for each loaded cell is pretty heavy.

func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath){

    let cell : UICollectionViewCell = collectionView.cellForItemAtIndexPath(indexPath) as! EmojiView
    // Do stuff here
}

To sum up, I would recommand to get rid of your whole init function in EmojiViews.m, use lazy loading for the labels and didSelectItemAtIndexPath: delegate for the selection events.

NB : I'm not used to swift so my code might contain a few mistakes.

like image 1
Kujey Avatar answered Nov 14 '22 07:11

Kujey