My app has several UIViewControllers in a stack (under a UINavigationController). In certain cases I get memory warnings when I'm in the inner UIViewControllers (mostly happens if the device is overloaded with other applications running in the background). When these memory warnings occur the application keeps on running (most of the times) and then when it gets back to the rootViewController it reloads it but doesn't fill in the view objects (mostly UIButton images).
I wish to implement didReceiveMemoryWarning and applicationDidReceiveMemoryWarning, but I'm not clear regarding to how to do that.
The end result I'm looking for is that when the application does this "reload" after memory warning it will "reload" rootViewController in the same status it was before the user started "diving" into the inner UIViewControllers.
How should I do the implementation?
Should I implement the default didReceiveMemoryWarning in each UIViewController?
Since the memory warning always occur in the inner UIViewControllers, how should I let the rootViewController know that it should run didReceiveMemoryWarning?
How do I tell the rootViewController which settings it should do? In other words - can I keep the settings someplace where didReceiveMemoryWarning doesn't delete them and upon activating didReceiveMemoryWarning "recall" them so the user will see the same screen they started the "levels navigation" from?
Any non-visible view controller on the UINavigationController stack will dump its view. It will also send you the warning. If you have large data structures being held by the view controllers you should dump those if possible.
The root (an other) controllers should appropriately handle viewDidUnload and be able to cycle through another loadView/viewDidLoad phase.
If you want the state to be the same, you need to be persisting all that information. NSUserDefaults is a standard location to do that.
Related
I've noticed that as I navigate across my iOS app memory stack grows with each opened viewController.
My navigation is based on segues and I do self.dismiss() before performing each segue. Still, it looks like viewControllers stack in memory.
How can I avoid this?
In Android finish() kills the Activity (in most cases), so I need an alternative in the iOs.
The memory issue can have several causes and not necessarily a UIViewController. In order to find and solve the issue you have to reduce the scope of the issue from "app" to a certain screen or even class. Then you can check the code and try to figure out where's the suspicious code.
Solving this issue is not a straight up task and there's no "how to fix memory issue for my app" tutorial, you'll have to check your code and compare with potential causes of memory leaks.
Also you'll have to be careful for false positives of memory leaks. Here are some steps I follow when I suspect a memory issue.
Navigate trough the app "till the end", then go back to "home screen", if the memory drops, all good.
If the memory doesn't drop, I do the same navigation several times, if the memory increases with the same step (more or less but close) then there's an issue. If the memory doesn't increase (maybe just a bit, several kb) then it's ok, it means there are some assets cached in memory (images, files, etc). But you will need a way to test this scenario too.
Now we are back to the "memory increased again almost the same as first time", now I do a clean run, and take each screen at a time, I just open the screen go back (dismiss/pop the controller) and observe, if the memory drops then that's not the screen that leaks. When I find the screen that increases the memory and never goes back:
check if the controller is passed as a reference to other objects that won't be deallocated (singleton classes or other, depends on the app).
check if the controller is sent as "delegate" to any other classes and if those delegates are correctly defined (strong delegates are a biiiiig issue).
if all of the above are ok, then I'll simply comment all the code that performs any work and try again. If commenting the code doesn't drop the memory(this happens rarely) then the screen is not the right one.
if the memory drops after commenting the code, I start de-commenting bits of the code and run again, until you'll find the piece of code that creates you issues.
One thing to keep in mind, you have to be patient while investigating memory issues, and to be honest, sometimes you have to be lucky too.
Per documentation on UIViewController.dismiss:
Dismisses the view controller that was presented modally by the view controller.
So calling this would dismiss any view controller shown modally. Based on your question, I have to assume that your segues are push segues and not modal, otherwise you'd be seeing your view controllers disappear.
Your 'view controller stack' might be with regards to the navigation stack on a UINavigationController. If so, then those view controllers remain in memory, as when a view controller is popped off the stack (ie: user swipes from left edge of screen, or hits "Back" in the nav bar), the previous view controller appears.
Without more information on how you're presenting your view controllers or building your navigation, I don't think you'll be able to get more answers.
I wonder if there is a way to check if I have too many views stacked in my app.
My app design is:
Navigation controller -> table view (table view works like root VC)
From the table view I can open the menu VC as a modal segue, and form there I can open the login VC as a modal segue, if I log in I end up on the account page like:
Navigation controller -> table view -> menu -> login -> account page
From the account page I can go deeper:
Navigation controller -> table view -> menu -> login -> account page -> list settings page -> edit settings page
Now I have 7 VC's stacked if I count the navigation controller, even though two of them are displayed as modal VC's.
My app does not crash but is this a good way to do it? If I understand it right apps now have to share CPU when running split screen on ipad, so I am not sure if this way is eating too much memory.
Or should I simply make the account VC become the new root VC and reset the stack? And when going back to the table view make that one the new root VC again.
View Controllers are light weight objects. If your concern is memory you should react to memory warnings. E.g. in the viewcontrollers' didReceiveMemoryWarning method. You could release any cached images, remove views if the VC is not visible. Releasing cached objects and images is not always desirable in viewDidDisappear as the user might come back to the screen and you want to avoid reloading everything if there are no memory issues. didReceiveMemoryWarning is the correct place to help the system in freeing memory without sacrificing user experience.
You can of course check how many view controllers are on a navigation controllers stack by checking the UINavigationController's property viewControllers.count.
Resetting the stack to always only have one root viewcontroller is nice but usually quite complex. It is also not really possible inside a navigation controller.
You should use Instruments to check your memory consumption and to verify that you correctly react to memory warnings. Memory warnings can be manually triggered in the simulator.
So when views are stacked on a navigation controller, they are not fully de-allocated from memory, even when they are offscreen. There isn't a strict maximum number of stacked views but you should be smart about it.
If you are concerned about RAM usage then be sure to utilise viewWillAppear and viewWillDisappear to their full potential, performing cleanup operations such as nulling off gesture recognisers and observers, and stopping any listeners that may be running on a background thread. This will reduce the amount of memory your views are using when offscreen; and is good coding practice anyway.
Hope this helps somewhat.
EDIT: Felix also raises a good point about memory warnings, if iOS is concerned about the memory usage of your app it will issue a memory warning which you can react to in the way felix explains.
I changed navigation in my application from using UITabBarController to u UINavigationController. I.e. former solution (1st version) was based only on the TabBarController - 4 ViewControllers (one simple TableView, one simple custom view and one MapView with many overlays). The second version is based only on the UINavigationController.
In case of TabBarController it was clear and simple, everything worked fine, especially MapView. I mean: the MapView was loaded once (with a significant number of overlays) and when I went to another view and back to the MapView the MapView was still there with its overlays already loaded and displayed (simple check: MapView`s viewDidLoad was called just once per app run, I had some debug messages there).
Now I changed navigation logic to the UINavigationController. Everything works fine for the first look - but: the viewDidLoad (for each view) is called everytime I navigate to the view. It is annoying especially in the case of the MapView - the loading of overlays is performed everytime, it takes some time and it causes app crash in some cases.
OK, my questions:
Is it some kind of "common" behavior of NavigationController?
Can I change this behavior so viewDidLoad will be called just once?
And more - How can I influence the "display sequence" of some view?
I understand the logic is probably more complicated but I appreciate any answer or hint ;)
Some related circumstances:
TabBar and Navigation controllers are not combined.
I use storyboards, segues are designed in the UIB, no manual calling like perfomSegue or prepareForSegue in my code. One button triggers segue to MapView.
I use push segues.
I also tried to use modal segues but without any change of that behavior.
any of viewDidUnload is never called during segues among the views.
No memory warning received.
No memory leaks measured both on simulator and iPhone 4.
I tried to build a very simple temporary project / app that is concerned just about the Nav. Controller and other views without ANY coding, just storyboard. It was the same behavior.
There was an issue that causes app crash when I fast and periodically tapped to navigation button and back button between one view and the MapView. In most cases the app crashed when I tapped the back button on the MapView before it was fully displayed (i.e. its overlays). It was fixed when I added a 1 sec. delay method call in the viewDidDisappeared in the MapView. It is not a fair fix, I know ;)
A UITabBarController and UINavigationController are based on fundamentally different paradigms.
The UITabBarController is intended for the UIViewController on each tab to exist independently of each other and for the user to choose which they want to view. viewDidLoad only gets called once for each UIViewController because it is intended that each tab still exists in memory even as the user switches to a different tab.
The UINavigationController is a stack of UIViewControllers where each is related to the one above and beneath itself. The top UIViewController in the stack is always the one that is visible to the user. When a UIViewController is pushed to the stack, its viewDidLoad gets called because it is being loaded into memory. When the top UIViewControllergets poped off the stack, it is unloaded from memory, and viewDidUnload gets called on the way out (viewDidUnload is deprecated in iOS6 and won't get called, but the controller will still get dumped from memory). This is why viewDidLoad gets called every time that the user pushes a particular UIViewController onto the UINavigationController stack.
I have a UITabBar holding 3 UIViewControllers.
One being a map, the other - a table, and the third - my own custom controller.
I noticed lately that my app has some issues when it comes to operating on low memory, and it's due to me not being clear about what to do when didReceiveMemoryWarning occurs.
Usually, and when with sufficient memory, all functions great. I would alloc all my controllers on the applicationDidFinishLaunchingWithOptions and all controllers work great.
But what happens when the memory is a bit low is that (for some reason) only my table is misbehaving.
First, I can see that the app received a memory warning. and then only my table gets a viewDidUnload.
At first I wasn't sure why my table became *completely empty*, but then I realized that my delegation methods stopped working rendering my reloadData essentially pointless.
So, I can see now that the didReceiveMemoryWarning is cascading through all my viewControllers. But viewDidUnload is called only on my table.
I want to know what is going on?
How do I recover from a viewDidUnload? If my view is nil, who is responsible for bringing it back?
Why is it that only my table is getting the viewDidUnload? I was reading this link and that one, and some of apples documentation but found no explanation to why my view doesn't recover.
What should I be doing in this case?
I'm making an interactive book for the iPad and am using UINavigationController to implement the navigation between a page and the next. When a user turns the page, the next page is pushed on top of the navigation stack.
I'm now 15 pages into the app, and the app crashes when I try to go from page 14 to page 15. No error message in the console, nothing in the device's crash logs neither.
Each view controller's scene in the storyboard has UIImageViews displaying images that are between 5MB and 20MB. The view controllers' viewDidLoad method is called just once. The total size of all the app's assets is below 200B. I'm using ARC.
I've ran the app using Instruments' Memory Monitor. The app's Real Memory consumption increases by about 80MB every time a new page is turned, and crashes when it reaches 800MB (the device is an iPad 3).
Why such an enormous memory consumption? Is this because the UIImageView in the Storyboard's scenes cache the images?
What would be the best way to free up memory when you use a
UINavigationController and ARC?
I tried adding setting all the view controller's subviews to nil in the view controllers' viewDidDisappear: method, but memory consumption stayed the same.
When you use a UINavigationController, each ViewController you push stays in memory forever (well, until your app exits) unless your user presses the back button on that particular ViewController. It keeps a stack of ViewControllers - with the visible one at the top.
So the simple answer is don't use a UINavigationController for this.
I'd suggest building your own ViewController that 'knows' which is the next and previous pages and manually loads and removes them as and when required. This way you can ensure that you only have one page in memory at once (except for during the transitions - maybe you could use this animation for the transitions http://cocoacontrols.com/platforms/ios/controls/xbpagecurl).
You probably don't want to use a UINavigationController for this purpose. You really want one view controller to manage all your pages and render the new page on the same view while removing the old.