19

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.

Community
  • 1
  • 1
s3lph
  • 4,575
  • 4
  • 21
  • 38
  • Have you ever tried to inspect with Instruments?? You could pinpoint where the memories are going. – Jason Nam Dec 26 '15 at 11:22
  • @JasonNam please see my edit. – s3lph Dec 27 '15 at 13:26
  • Okay actually one thousands UILabel could hold up some memory. Did you tried to reduce the number of cell to like 100? Dose it affect to the memory usage? – Jason Nam Dec 27 '15 at 14:38
  • There aren't thounsands of UILabels, that's the thing: UICollectionViews (if implemented as above) only initialize as many child views as will appear simultaneously, and then reuse them by changing the content (in this case UILabel.text). The actual number of UILabels initialized is 56 (debug output). And the memory usages rises proportionally to the scrolling. – s3lph Dec 27 '15 at 15:07
  • Aha, ok, I just reminded the reusing cells. Ok let's see – Jason Nam Dec 27 '15 at 15:08
  • which device are you testing this on? also please mention the iOS version – Sumeet Dec 29 '15 at 14:53
  • i guess cell are not being reused...instead just allocates memory – LC 웃 Dec 29 '15 at 18:13
  • see if prepareForReuse method is getting called or not – LC 웃 Dec 29 '15 at 18:21
  • @anishparajuli as I mentioned in an earlier comment, prepareForReuse is being called, and only 5 cells are being created. – s3lph Dec 29 '15 at 19:50
  • @uchiha see my edit. – s3lph Dec 29 '15 at 20:05
  • I'd add a standard boilerplate comment on premature optimisation. Supposing iOS is just intelligently caching glyphs, what's the loss? Wouldn't it be more inefficient _not_ to use memory that'll otherwise just be empty? What evidence do you have that there's an actual problem? Etc, etc. It'd still be academically interesting to figure out who is using the memory, of course, but I think it's false on the evidence given, to assume a problem. – Tommy Dec 29 '15 at 20:28
  • @Tommy The problem is that the system kills the keyboard for using too much memory. As I previously mentioned, I analyzed it with the Leaks tool and noticed it's simply malloc'ing a lot of memory. – s3lph Dec 29 '15 at 20:38
  • @the_Seppi when is it killed? If it's while not the active extension then, well, that's just how iOS is supposed to work. No pagefile included, ideally everything needs to be able to relaunch from any state. – Tommy Dec 29 '15 at 20:41
  • No, mostly during scrolling. Although that ceased on my iPhone after optimizing memory usage elsewhere, it may still occur on other devices, as app extension limitations seem to vary between devices. – s3lph Dec 29 '15 at 20:43

3 Answers3

5

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.

Allen
  • 6,505
  • 16
  • 19
  • Interestingly, commenting this out doesn't change anything in my keyboard. Which device are you testing on? – s3lph Dec 29 '15 at 19:53
  • And how many elements does the data source contain? – s3lph Dec 29 '15 at 20:00
  • i just created an array of 1000 strings, i did it in the simulator, could you give me your emoji string list? cause in my test, i just have repeated same string 1000 times. – Allen Dec 29 '15 at 20:34
  • Here it is: https://dl.dropboxusercontent.com/u/326576/emojis.swift (Doesn't yet contain multi-character emojis) – s3lph Dec 29 '15 at 20:41
  • 2
    started at 28.7MB, goes up to 57.7MB, then back to 49.1MB, the only difference is the emoji string list, i believe this is caused by the emoji font be ing used. – Allen Dec 29 '15 at 20:52
2

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")
Jason Nam
  • 2,011
  • 2
  • 13
  • 22
  • 1
    I'm already doing this in the `init`. However, I'm registering my subclass of UICollectionViewCell, anyhting else wouldn't make much sense. And how is this supposed to reduce memory usage? Cell reuse per se is working without problems. – s3lph Dec 28 '15 at 13:42
1

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.

Kujey
  • 1,122
  • 6
  • 17
  • The problem is not with the labels themselves, but with UIKit's font glyph caching (see my latest edit). The collection view already uses cell reuse, so this is not the problem. However, I see your point with the gesture recognizer, but it's not that much of an issue, as only 56 cells exist simultaneously. Still you are right about that. – s3lph Dec 31 '15 at 13:04