Apple provides a robust set of tools to help us optimize an app to levels I was never used to before as a web-based developer. For those that don’t know, my roots were born in a combination of traditional art training (illustration to be specific, traditional animation/graphic design/film to not be specific) and what another hobby I had. Flash websites. There’s an entire article just waiting to be written about that, but I’ll hold off, and I’m certain you’ll thank me for it.
Now back to those tools, most notably, Instruments. It’s an incredibly powerful and amazing program that, when wielded properly, can bring down colossi along with entire galaxies. Unfortunately, I haven’t yet become that proficient and, even if I did, I think I’d hold off on the destroyer of worlds level. This is another part of my maturity on the iOS and Xcode platform that has continued to flourish and has helped in our efforts to make Visn what you all want and hope it can be.
The Footprint
The most embarrassing thing about releasing 2.0 is that, from a development standpoint, I chose to focus more on making it iPad while fixing crashes/bugs reported by users along with Greg & JT. It was great but the entire time during those late night shifts, I could feel a little poke telling me I was missing something. After the release, a good friend of mine who had been eagerly awaiting the iPad version downloaded it on his first generation. His first thoughts brought the realization rushing to me as fast as a sledgehammer crushing through a pane of glass. “It keeps restarting”.
Shit. I’ve done it again. Another release had gone awry and, this time, it would cost us even more users that managed to hang on this long, approximately 5 months after the first release, in hopes that we would not let them down.
For the first month after release, I was diagnosing and testing everything I knew how to see what I could change or improve, studying the entire codebase again to see what areas were not as efficient as they could be, etc. After mucking around, I found some mediocre solutions but nothing that really made a big difference. What was the memory footprint of Visn in 2.0? Well, according to Activity Monitor, it was sometimes running as high as, I really hate to admit this, but using around 180 MB (this primarily sky rocketed in list view, which will make more sense in a moment).
The first generation iPad has 256 MB of RAM and the second has 512 MB. First, an image browsing application (or any application) using that much memory should be held to criminal charges. Excessive memory use might be a good name for it.
When multitasking was introduced, Apple brought about a system that was meant for users not to tamper with. Multiple applications could be suspended and restored in the state the user left off, with very little lag time in between switching. This was something so many were excited about, but at the same time, it absolutely terrified some developers. Before, your application could run in the foreground with the assumption that it was king and when it was pushed into the background it was peon. Prior to iOS multitasking, when your app went away, it was completely shut down. That meant that offensive use of memory wasn’t as obvious because the app didn’t have to share available RAM with others. Let’s just finish this up with saying, now you share resources so it’s much more obvious if you aren’t being as efficient as possible.
When at the Tech Talk in Austin at the end of January, I finally got the gaps filled in on my understanding and use of Instruments. This gave me a plan to find what was going on under the hood and vanquish the problems I ran into.
Instruments
I had been using it before but mostly for finding zombies, overseeing allocations and checking for leaks. As far as those diagnostics were concerned, Visn looked pretty good. After running some other that I learned about, though, that perception changed a bit. With the help of Time Profiler, VM Tracker and Activity Monitor, on top of Allocations/Leaks, I brought my awareness of where the underlying issues were to exactly where I wanted.
Activity Monitor and VM Tracker were the first instruments I ran, naturally, to get an idea what the memory/cpu usage of Visn was like. Although AM is not meant to be an exact representation of what exactly your application absolutely needs to be using at one point, it is a good way to see if it is just going a lot higher than you think it should. This is also where I got that 180 MB number. It wasn’t consistently using that much, but it was always running over 100 at most points. VM Tracker was essential in seeing how much dirty resident memory there was, which for reference is memory that has been allocated and used in the app. I was seeing a trend that it was significantly higher than it should be.
Time Profiler was focused on next and it really helped identify areas that cause the app to jitter or freeze. What it does is tell you how long, in milliseconds, it took to run specific methods. This instrument, along with VM Tracker, tends to be the most misunderstood. They have specific settings that are very helpful in making them much more “human-readable”, especially if your understanding of the underlying foundation is still growing. Typically, here are the best settings to just make sure you have activated for both:
Time Profiler (Check the following to “on” under Call Tree)
- Separate by Thread
- Show Obj-C Only
- Flatten Recursion
VM Tracker
- On the tool itself, click the information icon. Make sure your style is set to “Dirty, Resident Sizes”
- While it seems obvious, this tool works like leaks where you need to either snapshot manually or automatically. For more rigorous testing, set automatic. Usually anytime between 3-10 seconds is plenty.
The Findings
For those that are still making it through this epic, I applaud you. After running all these tests and diagnostics, here’s what I found.
Storing images inefficiently
My logic initially was to parse through feeds and process all images I ran across into what I refer to as a “Feed Collection”. This collection contains Feed Items from all sources that are currently or have been parsed. A Feed Item typically contained a feed url, feed id, image url and a cacheable image data property.
Whenever I was loading imagery, I would both display a version on screen and then store the UIImage I created from incoming data into the associated Feed Item. When, and if, the user decided to go into detail view, I would just display the already stored image. Once the panel that displayed that feed item was gone, I would clear it’s cached image, while still preserving the other properties. This seemed to be efficient, until I learned a bit more about how image data was stored on the device. Despite the compression of an image when it comes from the web or any other source, once it is displayed on screen, it is effectively uncompressed. That means that the actual pixel size was directly related, no matter what, to the memory consumption.
This came as a shock when I found it out but helped me move in another direction. For 2.0.1, I removed the cached object entirely and now fully rely on NSURLRequest with a caching policy of NSURLRequestReturnCacheDataElseLoad. This step alone saved me a ton of memory. Visn was technically using this connection before, which made matters even worse. NSURLRequest was caching the data while the app was then caching the data, in another form, again. What the cache policy on the request does, is it attempts to load data from a url. If that data has already been loaded, the system will pull from its managed cache. If the data has not, it will go through the normal process and give me what I need to display the image.
Changing to this method of storage also allowed for much more graceful handling of memory warnings. Again, I’m not hoping to get any but if I do, it’s good to know that the system is managing the cache of images so, if a memory warning comes around, I know it will be able to bring down usage without defaulting to either an immediate crash or making images disappear randomly.
Not properly cropping/resizing imagery
This was also a big one and is primarily what led to some of the most offensive instances of memory consumption in list view. In my previous post, I mentioned how scaled images negatively impact performance. Well, they also make a dent on memory, as well. In grid view, I was fortunately cropping the imagery correctly, so it was technically optimized from that standpoint. List view was a different story. All images being displayed in that view were being scaled down (instead of resized). The difference between the two is that when you resize an image, you are decreasing its width and height directly, so when you put it in a 325 x 325 slot, the system is drawing an image that is the same dimensions into that spot. When it is scaled, let’s say you have a 1024 x 1024 image going in that same slot. Now the system is not only having to compute each time what it “should” look like in that slot, it is also storing a larger image, which increases memory.
To repair this problem, I made sure all images are resized or cropped, stripped of unnecessary alpha channels and interpolated when necessary.
Not utilizing threads as well as I could
Time Profiler identified many areas that were taking too long in the main thread, which was causing user interaction to be blocked at too many points. I cleared it up by checking all of the methods that were taking the most time to complete. I either made sure they were running on a separate thread or utilizing another route to get a similar result.
This was most prominent when I was actually now resizing then repositioning imagery. When using Core Graphics to resize an image, there’s a certain amount of processing that needs to take place, and that can block user interaction. Making sure that was taking place on a separate thread was essential to maintain a smoother experience. While the iPhone version still has a ways to go in this aspect, iPad is holding up rather nicely.
So, that’s memory
My next and last bit of this segment will focus on how we dealt with existing bugs and interpreting user feedback.