6

I've been working on a new app with no storyboard. All went fine until I tested my application with Instruments: it leaked every time I assigned a string to a label. When I worked with a storyboard, I didn't have leaks like that.

I have read the following resources to find the answer:

The most popular opinion is that is an Instruments bug, but it looks like a too obvious approach to me.

The leak reproduces in an empty application. In the root view controller:

class ViewController: UIViewController {

var label: UILabel?

override func viewDidLoad() {
    super.viewDidLoad()

    label = UILabel()
    view.addSubview(label!)
    var textForLabel: String? = "Hello"
    label?.text = textForLabel

    //attempt to free the memory
    textForLabel = nil
    label = nil

    //EDIT: added after @J.Doe and @Sh-Khan answers, but it's still leaking
    label.removeFromSuperview()

   }
}

While testing this app in Instruments on a real device (iPhone SE 11.2) I see the following:

enter image description here

When I click on _NSContiguousString, I see that memory leak appears in [UILabel setText:].

enter image description here

I tried to set label as weak, but then it becomes nil when I try to add it as a subview.

So, my questions are:

  • how I can eliminate this memory leak now and in the future?
  • should I create UI elements only in .xib/.storyboard files for that reason?

I am new to iOS development, so I think that I'm missing something obvious. I will highly appreciate any help or advice.

EDIT: According to @Sh-Khan and @J.Doe answers (thank you guys so much!), I added label.removeFromSuperview(), but there is still a leak.

EDIT2: With @J.Doe help, I learned that UILabel gets released from memory by calling removeFromSuperview and setting it to nil afterwards. The memory leak in Instruments remained, but I mark his answer accepted because that's what I wanted to know.

PS: After reading about NSString retain count I think the reason of memory leak might be the fact I am using a string literal that cannot be released, according to the discussion.

Cœur
  • 37,241
  • 25
  • 195
  • 267
joliejuly
  • 2,127
  • 1
  • 21
  • 24
  • What happens if you move the var statement inside the viewDidLoad? If you still have the leak then it is likely not from the label as it would be deinitialized at the end of the function. – Zyntx May 12 '18 at 14:18
  • @Zyntx Thanks! Yes, the leak is there, according to the Instruments. But there is no any code in the app except that I posted above (and AppDelegate to set a rootViewController to the window). How there can be any leaks in an almost empty app? I am confused. – joliejuly May 12 '18 at 14:26
  • Can you set textForLabel as a let? -- let textForLabel: String = "Hello" – Zyntx May 13 '18 at 08:15
  • @Zyntx Yes, thank you! I Just tried it out as well. In fact, it's not UILabel fault, as it turned out: it's Swift._NSContiguousString that is leaking (1 leak 48 bytes), and only once. I am not sure, but I have a guess that the reason for leaking might be bridging with NSString (I see also [_SwiftNativeNSStringBase retain] on the top of the stack trace). Do you think it can be the case? – joliejuly May 13 '18 at 11:39
  • Hopefully, yes ?! Honestly, I've limitedly use of this to bad cases and would not have caught this. – Zyntx May 13 '18 at 15:05

2 Answers2

3

Maybe I am wrong, but I think this:

Weak will not increase the reference counter. Therefore, assigning a object label to a weak var label, does not make sense. This is because weak var label will be nil, because the object you created does not have any reference (and therefore it will deinitialize)

Let's count how many reference you have in your code to your created object Label.

label = UILabel() // 1
view.addSubview(label!) // 2
var textForLabel: String? = "Hello"
label?.text = textForLabel

//attempt to free the memory
textForLabel = nil
label = nil // 1

You have 1 reference left in your view to your object Label. Before you do label = nil, call label?.removeFromSuperview(). I think than you have 0 references -> it will deinit.

edit:

Add below subclass of UILabel into your code:

class MyLabel: UILabel {
    deinit {
        print("I am gone!")
    }
}

Change var label: UILabel? to var label: MyLabel?

And

label = UILabel() to label = MyLabel()

And check to logs. Do you see the print "I am gone!"?

Edit2: this prints "I am gone!" in a empty project with this only as code:

import UIKit

class ViewController: UIViewController {

    var label: MyLabel?

    override func viewDidLoad() {
        super.viewDidLoad()

        label = MyLabel()
        view.addSubview(label!)
        let textForLabel: String? = "Hello"
        label?.text = textForLabel

        //EDIT: added after @J.Doe and @Sh-Khan answers, but it's still leaking
        label?.removeFromSuperview()
        label = nil


    }
}

class MyLabel: UILabel {
    deinit {
        print("I am gone!")
    }
}
J. Doe
  • 12,159
  • 9
  • 60
  • 114
  • Yes! I see log now! Thank you! That's crazy but Instruments are still sure there is a leak, though. Did you try it there, too? What did you see? – joliejuly May 12 '18 at 14:19
2

First setting

textForLabel = nil

won't remove or make the label text nil as the label already took a copy of it

second setting

label = nil 

is not enough you have to

label.removeFromSuperview()
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87