In a recent client iPad app we learned ways to significantly reduce memory usage and improve speed by converting views to images and reusing frequently used view controllers.
The app required a scrollable grid composed of rectangles of various sizes containing text and images taken from articles from a number of RSS feeds. The grid can be refreshed with new content and redrawn in any orientation.
In the original implementation each grid item was set up using its own instance of the same view controller class with one of twelve different NIBs depending on the size of the grid item. After populating the UILabels and UIImageViews the view controller’s main view was added to a UIScrollView as a subview. Loading the grid, scrolling, and orientation changes were quite slow with this implementation. I ran the app through the Activity Monitor tool in Instruments and found that memory usage was very large:
Memory usage after one grid load:
The grid configuration and drawing process is repeated on an orientation change and the previous subviews are persisted so that future orientation changes with the same set of articles can occur faster.
Memory usage after first orientation change:
Changing sets of articles a few times increased memory usage dramatically. After about 5 category changes the memory usually plateaued and the OS often killed the app due to this extremely high memory usage:
Memory usage after 5 category changes:
To cut down on memory usage I decided to
- Reuse the grid item view controllers and views instead of allocating view controllers for each grid item
- Convert the views to images before drawing them onto the scroll view.
In order to reuse the grid items, a single view controller for each grid item type is allocated:
articleLandscapeSquareNoImage = [[ArticleViewController alloc] initWithNibName:@"Article1x1TextLandscape" bundle:nil]; articleLandscapeVerticalNoImage = [[ArticleViewController alloc] initWithNibName:@"Article1x2TextLandscape" bundle:nil]; articleLandscapeHorizontalImage = [[ArticleViewController alloc] initWithNibName:@"Article2x1ImageLandscape" bundle:nil]; articleLandscapeTwoByOneNoImage = [[ArticleViewController alloc] initWithNibName:@"Article2x1TextLandscape" bundle:nil]; articleLandscapeSquareImage = [[ArticleViewController alloc] ...
After a grid item view controller is configured with new article data, the main view is converted to an image using the QuartzCore framework:
UIGraphicsBeginImageContext(viewController.view.bounds.size); [viewController.view.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();
The resulting image is then set as the background of the grid item (a UIButton). After configuring it and adding its image representation to a button on the UIScrollView, the same view controller can be reused and reconfigured for the next article of its NIB style.
This approach reduces memory usage and improves speed significantly as only twelve view controllers need to be allocated (as opposed to 50+ in the initial implementation) and the resulting grid items are drawn to the UIScrollView as individual images and not multiple views (UIViews, UIImageViews, UILabels, etc.). Below are memory usage statistics from the same scenarios as the statistics that were gathered from the original implementation:
Memory usage after one grid load:
Memory usage after one orientation change:
Memory usage after five category changes:
It became clear while working on this app that commonly used view objects should be reconfigured and reused whenever possible to reduce memory usage and improve stability and performance. Furthermore, if the views’ subviews are not altered often or involve much user interaction, converting them to images will also reduce memory usage and increase drawing speed. I expect us to frequently use these techniques in future apps that rely heavily on custom views.