Unable to Completely Reclaim Memory Usage from UIWebView - ios

I have the following sample code (using ARC) which adds a UIWebView as a subview and subsequently removes it (toggled by a tap gesture on the screen):
- (void)toggleWebViewLoading:(UITapGestureRecognizer *)sender
{
if (webView == nil)
{
webView = [[UIWebView alloc] initWithFrame:CGRectMake(0.0f, 100.0f, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height - 100.0f)];
[webView loadRequest:[[NSURLRequest alloc] initWithURL:[[NSURL alloc] initWithString:#"http://www.google.ca"]]];
[self.view addSubview:webView];
}
else
{
[webView removeFromSuperview];
webView = nil;
}
}
When the app initially loads with a blank UIView, it consumes approximately 1.29 MB (live bytes reading from Instruments.app). Tapping on the blank UIView causes the toggleWebViewLoading: function to fire which in turn creates a new UIWebView, loads Google, and adds it as a subview. Once this sequence of operations have been completed, the memory usage is approximately 3.61 MB. Tapping again executes the second part of toggleWebViewLoading: which removes the UIWebView from its superview and sets it to nil. At this point the memory consumption has dropped to 3.38 MB.
My question is, how can I completely reclaim the memory from UIWebView (i.e. have the memory consumption return to its initial value of 1.29 MB or something similar after the the UIWebView has been removed and set to nil)?
Other Relevant Information
Before someone asks why I care so much about ~2 MB of memory savings, I have I much more complicated situation using Xamarin/MonoTouch where 10+ UIWebView's are consuming 200 MB+ of memory and I can't ever seem to reclaim all of the memory when it is no longer needed. I think the answer boils down to this simple question.

I would suggest monitoring how many threads you have active. UIWebView spawns several threads for itself. I expect they are not cleaned up properly once the UIWebView is deallocated, but it's just a hunch.

Related

Animation Terminating due to Memory even with ARC and imageFilePath

I'm doing a simple animation of png's after my app loads and displays its launch screen. The animation works in the simulator but not on the actual iPhone, where it terminates due to memory pressure (the launch screen does load first though). In the debug, the Memory increases exponentially to like 200MB until it crashes. The instruments show no leaks, All Heap and anonymous Vm 9.61 MB overall. The animation doesn't show at all on the actual iPhone.
I've already stopped using imageNamed to set the animation images and used imageFilePath as suggested by another help topic. It works and the images load on the simulator. I'm just not sure what else to do. Help would be very much appreciated.
In didFinishLaunchingWithOptions:
[self.window makeKeyAndVisible];
self.animationView = [[UIImageView alloc] initWithFrame:CGRectMake(0,0, 320, 480)];
NSArray *animationArray = [[NSArray alloc] initWithObjects:
[UIImage imageFromMainBundleFile:#"/Splash_00000.png"],
[UIImage imageFromMainBundleFile:#"/Splash_00001.png"],
//There's about 150 of these so I'll omit the rest
nil];
self.animationView.animationImages = animationArray;
self.animationView.animationDuration = 5.0f;
self.animationView.animationRepeatCount = 1;
[self.animationView startAnimating];
[self.window addSubview:self.animationView];
[self.window bringSubviewToFront:self.animationView];
In case it's needed, this is the method I'm using that I got from another thread:
+(UIImage*)imageFromMainBundleFile:(NSString*)aFileName
{
NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
return [UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/%#", bundlePath, aFileName]];
}
Putting aside that splash screens aren't recommended, you're not leaking but you're running out of heap (which is why it works on the simulator).
Each of those images is owned by the array and won't be released by ARC until long after the animation has completed. Bare in mind that the PNGs, while compressed on disk, will be uncompressed in memory.
Solutions - there are a couple that spring to mind.
Split the animations into a sequence of discrete phases and ensure
that images are released (using #autoreleasepool) between each phase
More realistically, render the animation as a movie and play that
instead (far less likely to stutter between phases)

How to made the activity indicator appear immediately in ios app?

I'm developing a ios app. I use this code on a view, in order to made an activity indicator and a black background appear in place of the whole view while is loading.
// hiding all the uioutlet in the view
sfondo.hidden=TRUE;
dalText.hidden=TRUE;
alText.hidden=TRUE;
titoloText.hidden=TRUE;
// generating the activity indicator
CGRect screenBound = [[UIScreen mainScreen] bounds];
CGSize screenSize = screenBound.size;
CGFloat screenWidth = screenSize.width;
CGFloat screenHeight = screenSize.height;
imgCaricamento = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
[imgCaricamento setCenter:CGPointMake(screenWidth/2.0, screenHeight/2.0)]; // I do this because I'm in landscape mode
[self.view addSubview:imgCaricamento]; // spinner is not visible until started
[imgCaricamento startAnimating]; // start!
Now the problem is that i launch this code before starting an update and do several things. What happen is that, at the moment this code is launched the view freezes for one-two seconds, only after this delay time, the activity appears. This time it's enough to convey to the user the impression that the app is acting bad... How can i make this code being executed before and immediately?
You are probably doing something that is blocking the main thread, maybe downloading an image, reading something from disk or parsing some data. The main thread is responsible for refreshing the UI and when it is blocked, the UI freezes. You can solve this by doing your processing in the background, on a different thread.
Apart from something maybe blocking the main thread therefore preventing the UI being updated I would prepare the UIActivityIndicatorView within viewWillAppear and execute [imgCaricamento startAnimating]; within viewDidAppear or where ever you start the updating process.

iOS UIScrollView performance

I'm trying to increase the scrolling performance of my UIScrollView. I have a lot of UIButtons on it (they could be hundreds): every button has a png image set as background.
If I try to load the entire scroll when it appears, it takes too much time. Searching on the web, I've found a way to optimize it (loading and unloading pages while scrolling), but there's a little pause in scrolling everytime I have to load a new page.
Do you have any advice to make it scroll smoothly?
Below you can find my code.
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage=(int)(offset.y / 322.0f);
if(lastContentOffset>offset.y){
pageToRemove = currentPage+3;
pageToAdd = currentPage-3;
}
else{
pageToRemove = currentPage-3;
pageToAdd = currentPage+3;
}
//remove the buttons outside the range of the visible pages
if(pageToRemove>=0 && pageToRemove<=numberOfPages && currentPage<=numberOfPages){
for (UIView *view in scrollView.subviews)
{
if ([view isKindOfClass:[UIButton class]]){
if(lastContentOffset<offset.y && view.frame.origin.y<pageToRemove*322){
[view removeFromSuperview];
}
else if(lastContentOffset>offset.y && view.frame.origin.y>pageToRemove*322){
[view removeFromSuperview];
}
}
}
}
if(((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd) || (lastContentOffset>offset.y && lastPageToAdd-1==pageToAdd)) && pageToAdd>=0 && pageToAdd<=numberOfPages){
int tmpPage=0;
if((lastContentOffset<offset.y && lastPageToAdd+1==pageToAdd)){
tmpPage=pageToAdd-1;
}
else{
tmpPage=pageToAdd;
}
//the images are inside the application folder
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
for(int i=0;i<4;i++){
UIButton* addButton=[[UIButton alloc] init];
addButton.layer.cornerRadius=10.0;
if(i + (tmpPage*4)<[imagesCatalogList count]){
UIImage* image=[UIImage imageWithContentsOfFile:[NSString stringWithFormat: #"%#/%#",docDir,[imagesCatalogList objectAtIndex:i + (tmpPage*4)]]];
if(image.size.width>image.size.height){
image=[image scaleToSize:CGSizeMake(image.size.width/(image.size.height/200), 200.0)];
CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2,(image.size.height-159.5)/2, 159.5, 159.5));
image = [UIImage imageWithCGImage:ref];
}
else if(image.size.width<image.size.height){
image=[image scaleToSize:CGSizeMake(200.0, image.size.height/(image.size.width/200))];
CGImageRef ref = CGImageCreateWithImageInRect(image.CGImage, CGRectMake((image.size.width-159.5)/2, (image.size.height-159.5)/2, 159.5, 159.5));
image = [UIImage imageWithCGImage:ref];
}
else{
image=[image scaleToSize:CGSizeMake(159.5, 159.5)];
}
[addButton setBackgroundImage:image forState:UIControlStateNormal];
image=nil;
addButton.frame=CGRectMake(width, height, 159.5, 159.5);
NSLog(#"width %i height %i", width, height);
addButton.tag=i + (tmpPage*4);
[addButton addTarget:self action:#selector(modifyImage:) forControlEvents:UIControlEventTouchUpInside];
[tmpScrollView addSubview:addButton];
addButton=nil;
photos++;
}
}
}
lastPageToAdd=pageToAdd;
lastContentOffset=offset.y;
}
Here's a few recommendations:
1) First, understand that scrollViewDidScroll: will get called continuously, as the user scrolls. Not just once per page. So, I would make sure that you have logic that ensures that the real work involved in your loading is only triggered once per page.
Typically, I will keep a class ivar like int lastPage. Then, as scrollViewDidScroll: is called, I calculate the new current page. Only if it differs from the ivar do I trigger loading. Of course, then you need to save the dynamically calculated index (currentPage in your code) in your ivar.
2) The other thing is that I try not to do all the intensive work in the scrollViewDidScroll: method. I only trigger it there.
So, for example, if you take most of the code you posted and put it in a method called loadAndReleasePages, then you could do this in the scrollViewDidScroll: method, which defers the execution until after scrollViewDidScroll: finishes:
- (void)scrollViewDidScroll:(UIScrollView *)tmpScrollView {
CGPoint offset = tmpScrollView.contentOffset;
//322 is the height of 2*2 buttons (a page for me)
int currentPage = (int)(offset.y / 322.0f);
if (currentPage != lastPage) {
lastPage = currentPage;
// we've changed pages, so load and release new content ...
// defer execution to keep scrolling responsive
[self performSelector: #selector(loadAndReleasePages) withObject: nil afterDelay:0];
}
}
This is code that I've used since early iOS versions, so you can certainly replace the performSelector: call with an asynchronous GCD method call, too. The point is not to do it inside the scroll view delegate callback.
3) Finally, you might want to experiment with slightly different algorithms for calculating when the scroll view has actually scrolled far enough that you want to load and release content. You currently use:
int currentPage=(int)(offset.y / 322.0f);
which will yield integer page numbers based on the way the / operator, and the float to int cast works. That may be fine. However, you might find that you want a slightly different algorithm, to trigger the loading at a slightly different point. For example, you might want to trigger the content load as the page has scrolled exactly 50% from one page to the next. Or you might want to trigger it only when you're almost completely off the first page (maybe 90%).
I believe that one scrolling intensive app I wrote actually did require me to tune the precise moment in the page scroll when I did the heavy resource loading. So, I used a slightly different rounding function to determine when the current page has changed.
You might play around with that, too.
Edit: after looking at your code a little more, I also see that the work you're doing is loading and scaling images. This is actually also a candidate for a background thread. You can load the UIImage from the filesystem, and do your scaling, on the background thread, and use GCD to finally set the button's background image (to the loaded image) and change its frame back on the UI thread.
UIImage is safe to use in background threads since iOS 4.0.
Don't touch a line of code until you've profiled. Xcode includes excellent tools for exactly this purpose.
First, in Xcode, make sure you are building to a real device, not the simulator
In Xcode, choose Profile from the Product menu
Once Instruments opens, choose the Core Animation instrument
In your app, scroll around in the scroll view you're looking to profile
You'll see the real time FPS at the top, and in the bottom, you'll see a breakdown of all function and method calls based on total time ran. Start drilling down the highest times until you hit methods in your own code. Hit Command + E to see the panel on the right, which will show you full stack traces for each function and method call you click on.
Now all you have to do is eliminate or optimize the calls to the most "expensive" functions and methods and verify your higher FPS.
That way you don't waste time optimizing blind, and potentially making changes that have no real effect on the performance.
My answer is really a more general approach to improving scroll view and table view performance. To address some of your particular concerns, I highly recommend watching this WWDC video on advanced scroll view use: https://developer.apple.com/videos/wwdc/2011/includes/advanced-scrollview-techniques.html#advanced-scrollview-techniques
The line that is likely killing your performance is:
addButton.layer.cornerRadius=10.0;
Why? Turns out the performance for cornerRadius is AWFUL! Take it out... guaranteed huge speedup.
Edit: This answer sums up what you should do quite clearly.
https://stackoverflow.com/a/6254531/537213
My most common solution is to rasterize the Views:
_backgroundView.layer.shouldRasterize = YES;
_backgroundView.layer.rasterizationScale = [[UIScreen mainScreen] scale];
But it works not in every situation.. Just try it

UIScrollView scrolling too slowly and never calling scrollViewDidEndDecelerating

The iPad app I'm working on is a book. To jump to a specific page, the user can press a button that overlays a view top of the current view, displaying images of thumbnails of each page in the book.
When the user goes through the book sequentially and displays this thumbnails menu, the scrolling animation is smooth and fine if the user showed the menu . The problem happens if the user calls showBookmarkMenu after having loaded about fifteen pages, the scrollview animation is very very slow, and the scrollview doesn't catch touches anymore.
I noticed that scrollViewDidEndDecelerating gets called when the scrolling animation is normal and smooth (shortly after loading the app), but it doesn't get called after the user has gone through several pages. So one hypothesis is that the CPU is struggling with the animation of the positioning of the scrollview's content. I ran the app using Instruments' Activity Monitor, but there are times when the app uses 97% and more of the CPU and the scrollview scrolls fine...
Any thoughts on this issue? I've posted my code below.
MainClass.m
//Called when user presses the open/close bookmark menu button
-(IBAction)toggleBookmarksMenu{
if([bookMarkMenu isHidden]){
[currentPage.view addSubview:bookMarkMenu];
[bookMarkMenu showBookmarkMenu];
}
else{
[bookMarkMenu hideBookmarksMenu];
}
}
ScrollViewClass.h
#interface BookmarkManager : UIView<UIScrollViewDelegate>{
UIScrollView *thumbnailScrollView;
}
#property (strong, nonatomic) UIScrollView *thumbnailScrollView;
#property (strong) id <BookmarkManagerDelegate> bookmarkManagerDelegate;
-(void)showBookmarkMenu;
-(void)hideBookmarksMenu;
#end
ScrollViewClass.m
-(void)showBookmarkMenu{
[self setHidden:NO];
[UIView animateWithDuration:0.5
animations:^{
self.center = CGPointMake(512, 384);
}
];
}
-(void)hideBookmarksMenu{
[UIView animateWithDuration:1
animations:^{
self.center = CGPointMake(512, -120);
}
completion:^(BOOL finished){
[self setHidden:YES];
[self removeFromSuperview];
}
];
}
-(id)init{
self = [super initWithFrame:CGRectMake(0, 0, 1024, 768)];
if(self){
[self setBackgroundColor:[UIColor clearColor]];
self.center = CGPointMake(512, 0);
thumbnailScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 1024, 120)];
[thumbnailScrollView setBackgroundColor:[UIColor clearColor]];
thumbnailScrollView.showsHorizontalScrollIndicator = NO;
//Add the UIButtons with images of the thumbnails
for(int i = 0; i < totalPages; i++){
UIButton *pageThumbnail = [UIButton buttonWithType:UIButtonTypeCustom];
pageThumbnail.frame = CGRectMake(0, 0, 125, 95);
[pageThumbnail setBackgroundImage:[UIImage imageWithContentsOfFile:[NSString stringWithFormat:#"%#/p%d_thumb.png", [[NSBundle mainBundle] resourcePath], i]] forState:UIControlStateNormal];
[thumbnailScrollView addSubview:pageThumbnail];
[pageThumbnail addTarget:self action:#selector(thumbnailTapped:) forControlEvents:UIControlEventTouchDown];
}
[self addSubview:thumbnailScrollView];
[thumbnailScrollView setContentSize:CGSizeMake(totalPages * 125 + (20*(totalPages+1)), 120)];
[thumbnailScrollView setDelegate:self];
[self setHidden:YES];
}
return self;
}
I have to go with possible low memory issue.
A possible alternative to using a slew of buttons is using UITableView. The way your code is currently working, it loads up ALL the buttons with images. For a large book this could be painful.
Using UITableView you only use as much memory as you see (about). And, since each image is loaded dynamically, your memory usage is only as much as is displayed. That would be how I would go about it (actually, I'm doing that now, just not with a book).
A shot in the dark, based on your observation that the scrolling becomes slow after loading 15 pages or so: possibly your device is busy handling a low memory condition. In such cases, as you possibly know, a system wide notification is sent to a considerable number of apps/objects for them to recover as much memory as possible.
Could you check if at more or less the same time when the scrolling becomes slow your app is executing didReceiveMemoryWarning?
If you confirm that the issue could be related to memory saturation/reclaiming, then I would suggest implementing a lazy loading scheme for your images:
you only load images when you are required to display them;
you only keep in memory 3-5 images total, to ensure a smooth scrolling.
The basic step requires id providing your delegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
implementation. Here you will preload images:
knowing your position, you know your current image (say, image number N);
unload images N-2, N+2;
load images N-1, N+1.
The images to load/unload I provided are fine if you just want one "buffer" image.
In any case, if you google "iso scroll view lazy loading" you will find plenty of info.
Turns out it wasn't a low memory issue, but an overly busy CPU issue.
It is the CPU that does the calculations required for the scrollview's scrolling animations, and when the scrolling becomes this slow I thought I'd try to figure out why I was using 97% of the CPU in the first place. Turns out that past page 15, I had CPU-intensive recursive functions (calculating UIBezierPaths for another part of the app) caught in an infinite loop. The app was calculating hundreds of UIBezierPaths a second, and there reached a point where the CPU just couldn't keep up with the calculations for the scrollview's animation.
Once I made sure the recursive functions stopped calling themselves when they were not needed, CPU usage remained under 20% throughout the app, and the scrollview performed perfectly well.

UIWebView rotation and memory problem

I have an UIWebView that loads a embedded XHTML like this:
body = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
body.scalesPageToFit = YES;
body.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
body.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;
[self.view addSubview:body];
[body loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"article16" ofType:#"xhtml"]isDirectory:NO]]];
This XHTML have a lot of images, and when I rotate the device I got some memory warnings, and sometimes the app crashes.
If I remove the autoresizing mask, specifically the UIViewAutoresizingFlexibleWidth (I have tryed with only UIViewAutoresizingFlexibleLeftMargin and UIViewAutoresizingFlexibleRightMargin with no problems), the memory warning stops, and the app does not crash anymore.
If I remove all autoresizing mask, and set the new webView frame in didRotate or willRotate, I got the same warnings as using the UIViewAutoresizingFlexibleWidth.
There's a app, called Atomic Web that could open the same XHTML and rotate with no memory warnings, and safari can open it as well, but if I create a project with only that UIWebView, the app sometimes crashes.
Someone knows what this may be? Can I set the webview frame in other way? Why can't I use the autoresizing mask?
Thanks for your attention.
I was seeing memory issues as well, but in a slightly different capacity with a UIScrollView. It appears that the [UIColor scrollViewTexturedBackgroundColor] property consumes a very significant amount of memory when your UIScrollView subclass, including UIWebView, applies it as the background color.
I would recommend changing it to just a plain color like gray. I noticed a 7 MB reduction in memory as reported by the activity monitor instrument by just making that simple change.
Try:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
[[NSUserDefaults standardUserDefaults] setInteger:0 forKey:#"WebKitCacheModelPreferenceKey"];
}
From http://blog.techno-barje.fr/post/2010/10/04/UIWebView-secrets-part1-memory-leaks-on-xmlhttprequest

Resources