0

I have an app I am developing and the stakeholder using it said that the app becomes slow and unusable/unresponsive after consistent usage all day. Killing it and starting over causes it to run fine.

I don't seem to have this trouble on my device, but I started looking at the memory usage in both simulator/phone in debugger, and observed my memory would steadily increase if I took the basic action of going between screen to screen. These are pretty involved screens, but if I just go forward to the 'add new item' screen, then back to the product listing screen, the memory jumps up 30mb. If I keep doing this same action, over and over and over, I can get it to 1.1gb of memory

I then took it a step further, hooked up my phone, and ran profiler (specifically memory leaks). I found one leak involving my usage of ads, so I just commented out all the code for a test and while the leaks are gone, the memory continues to go up steadily.

I then ran the allocations tool, and after a few min of going back and forth in the same manner, here is the output: XCode Profiler - Allocations

As you can see, it's 1.53GB and if I kept doing the same action I can get it to 2GB+. Oddly enough, my phone never seems to mind, and the screens are just slightly laggy at times otherwise not too bad. Certainly usable.

Before I start ripping out the floor boards, I wanted to confirm this is a likely sign of a problem. Any suggestions on where I can start looking? If persistent memory is the issue, what would be some typical gotchas or pitfalls? What is "anonymous vm?"

Thank you so much if you're reading this far, and appreciate any guidance!

UPDATE/EDIT

After some guidance here, I noticed, oddly enough, that on the "add product" page it causes the memory to jump ~10MB each time I visit it. After commenting out code, I narrowed it down to this section (and even the line of code) causing the jump. Removing this code causes it to remain stable and not increase.

 //Render collection views
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath as IndexPath) 
        
            let member: MemberDto = groupMembers[indexPath.item]
            let contactInitials = cell.viewWithTag(1) as! UILabel
            let contactAvatar = cell.viewWithTag(2) as! UIImageView
            contactAvatar.image = UIImage(named: "anonymous")
            contactInitials.text = member.displayName
            contactAvatar.layer.cornerRadius = contactAvatar.frame.size.width / 2
            contactAvatar.clipsToBounds = true
            contactAvatar.contentMode = UIViewContentMode.scaleAspectFill
            contactAvatar.layer.borderWidth = 5.0

            
             
            if (member.profileImage.trimmingCharacters(in: CharacterSet.whitespaces) != "") {
                UserService.getProfilePicture(userId: member.userId) {
                    response in
                    
                    contactAvatar.image = response.value
                }
            }
          

So, the offending line of code is here:

 contactAvatar.image = response.value

Adding it in, and going back and forth to this tableviewcontroller causes the memory to go up and up and up all the way to 2gb. Removing that one line of code (where I set the image) keeps it stable at ~40-70mb, or it goes up but very very slowly (dozens of repeats only got it to 80mb)

I realized I was not caching this image

I decided to try caching this with my framework, and that immediately resolved the issue. I suppose the line of code was pulling the image into memory or something like that? It doesn't seem like the networking call is the actual issue, since I left that in (and even went so far to make additional calls to my API) and that doesn't seem to do much by way of memory increase.

Just a few pieces of info:

  • From the main screen, you tap on a + symbol in the navigation menu bar to come to this screen.
  • I am using a regular segue on my storyboard, associated with the navigationbutton, to take the user here
  • Placing deinit on this vc does not seem to ever hit, even with print/code in there and breakpoints
  • Making API calls from within my uitableviewcontroller doesn't seem to cause the image to load UNLESS I combine that with SETTING the image. If I make a network call, but don't set the image, it doesn't increase.

What mistake did I make? I feel like caching the image is a bandaid - I recall reading that you're not supposed to make calls to images within a UITableViewController but what is the alternative, to pull all user images from the collection in advance and cache them before the tableview loads?

EDIT 2

As @matt suggested, this was just a bandaid. The true problem still lingered as I knew deinit() was not being called. After pulling out major chunks of code, I found this

lblMessage.addTapGestureRecognizer {
            self.txtMessage.becomeFirstResponder()
        }
        

which maps to an extension class:

public func addTapGestureRecognizer(action: (() -> Void)?) {
        self.isUserInteractionEnabled = true
        self.tapGestureRecognizerAction = action
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture))
        self.addGestureRecognizer(tapGestureRecognizer)
    }
    public func addLongPressGestureRecognizer(action: (() -> Void)?) {
        self.isUserInteractionEnabled = true
        self.longPressGestureRecognizerAction = action
        let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
        self.addGestureRecognizer(longPressGestureRecognizer)
    }
     
    
    // Every time the user taps on the View, this function gets called,
    // which triggers the closure we stored
    @objc fileprivate func handleTapGesture(sender: UITapGestureRecognizer) {
        if let action = self.tapGestureRecognizerAction {
            action?()
        } else {
            print("no action")
        }
    }

So somewhere in here the problem must lie. I'm taking this to a new thread: Deinit not calling - Cannot find why something is retaining (code provided)

Thanks! Hope this helps someone.

NullHypothesis
  • 4,286
  • 6
  • 37
  • 79
  • [Memory Deep Dive](https://developer.apple.com/videos/play/wwdc2018/416/), [Reducing Your Memory Footprint](https://developer.apple.com/documentation/metal/frame_capture_debugging_tools/improving_memory_and_resource_usage/reducing_your_memory_footprint/), [Reducing Memory Use](https://developer.apple.com/documentation/xcode/improving_your_app_s_performance/reducing_your_app_s_memory_use/), [Gathering Information About Memory](https://developer.apple.com/documentation/xcode/improving_your_app_s_performance/reducing_your_app_s_memory_use/gathering_information_about_memory_use/), etc. – Rob Apr 14 '21 at 22:37
  • 2
    My first tool is generally to run the app, exercise it, go back to quiescent state (i.e. the app's “home screen”), repeat, and then look at the “Debug Memory Graph” and look for any of my own objects listed that I know should have been released. This way I focus on my own strong reference cycles (before I worry about OS objects), i.e. strong reference cycles I may have introduced. That having been said, your memory is growing fast enough that I'd pay special attention to your own large assets (images, videos, etc.) that might not be getting released and/or not responding to memory pressure. – Rob Apr 14 '21 at 22:41
  • But there's just not enough here for us to help you. We need reproducible examples of the problem. – Rob Apr 14 '21 at 22:42
  • But you have to decide whether the problem is true leak (rare in Swift), abandoned memory (e.g. strong reference cycles) or unused memory (e.g. sloppy or over-aggressive cache practices). – Rob Apr 14 '21 at 23:13
  • Thanks @Rob I just added code and provided some additional details on the offending line of code. Any ideas? – NullHypothesis Apr 15 '21 at 13:43
  • "Placing deinit on this vc does not seem to ever hit, even with print/code in there and breakpoints" That, as I said in my answer, is the key point. The image is the canary in the coal mine, just because it is a sizeable chunk of memory, but otherwise it is a red herring; it isn't going out of existence because the vc isn't going out of existence when you go "back". You need to concentrate on discovering _why_ the vc isn't going out of existence. I have given, in my answer, the two main reasons why that _usually_ happens. – matt Apr 15 '21 at 21:59

1 Answers1

2

Yes it's a problem, and yes you need to fix it. The two usual causes of this sort of thing are:

  • You've got a retain cycle such that at least some of your view controllers are never able to go out of existence.

  • You've designed the storyboard (or manual segue) sequence incorrectly, so that (for example) you present from view controller A to view controller B, and then in order to get "back" you present from controller B to view controller A. Thus you are not actually going "back"; instead, you are piling up a second view controller A on top of the first one, and so on, forever.

Either way, you can rapidly test that that sort of thing is going on just by implementing deinit to print(self) in all your view controllers. Then play with the app. If you don't see the printout in the log every time you go "back", you've got a serious memory problem, because the view controller is not being released when it should be, and you need to fix it.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • thank you for taking the time to respond I just added an update with actual code and some details I found in my investigation. Any guidance / pointing in the right direction would be very much appreciated. Thank you!!! @matt – NullHypothesis Apr 15 '21 at 13:42
  • thanks for the help. I can't seem to find a way to get deinit() to call in some of my VC's. I went so far as to strip out major areas of code, renamre vars to weak, remove delegates, and despite that it still doesn't hit. Any other tools I can use such as in the xcode suite for profiling? ty! – NullHypothesis Apr 17 '21 at 12:17
  • I found the line of code that was causing it: lblMessage.addTapGestureRecognizer { self.txtMessage.becomeFirstResponder() } – NullHypothesis Apr 17 '21 at 12:26
  • moving this to a new question https://stackoverflow.com/questions/67138887/deinit-not-calling-cannot-find-why-something-is-retaining-code-provided – NullHypothesis Apr 17 '21 at 13:56
  • Yup, just as I said in my answer: “You've got a retain cycle”. – matt Apr 17 '21 at 21:39