after multiple days of banging my head against the wall and having sleepless nights I'm hoping to find some help here. I've gone through various posts here, but none of the answers seem to provide a resolution for me.
In short, my problem is that my App crashes after heavy usage (>10min) of the UIWebView (e.g. opening larger news paper sites in series - one after the other).
To give more details:
I am writing an iPhone App and from the MainViewController I push a browserViewController on the navigationController. The browserViewController loads a nib which contains a UWebView (I do not create the WebView programatically). The UIWebView is wired up using Interface Builder.
When going back to Main and then going again to the browserViewController, I only recreate the browserViewController if it is nil. (I want to keep the content that is loaded i the UIWebView - only if there is a memory warning shoud this view be unloaded and release all memory used).
In both, MainViewController and browserViewController I am responding to memory warnings, but this seems not to provide enough relief.
Looking at Instruments I noticed that for example CFData(store) keeps increasing. And even if I simulate a memory warning (see code below) and call viewDidUnload on browserViewController, CFData remains allocated and does not get freed.
So my biggest question is:
How to free up memory created from "browsing"?
This counts for two cases:
- how do I make sure that viewDidUnload properly frees memory allocated my CFData(store)?
- how to free up memory when the user keeps loading pages in browserViewController?
.
Who manages CFData?
See below for my simplified sample code:
MainViewController.h
// MainViewController.h
#import "myAppDelegate.h"
#import "BrowserViewController.h"
#interface MainViewController : UIViewController {
BrowserViewController *browViewController;
}
- (void) switchToBrowserViewController;
#end
MainViewController.m
// MainViewController.m
#import "MainViewController.h"
#implementation MainViewController
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
[browViewController release];
browViewController = nil;
}
- (void)viewDidUnload {
// Release any retained subviews of the main view.
// e.g. self.myOutlet = nil;
}
- (void)dealloc {
[browViewController release];
browViewController = nil;
[super dealloc];
}
- (void) switchToBrowserViewController {
// create new browViewController if needed
if ( browViewController == nil ) {
browViewController = [[BrowserViewController alloc] initWithNibName:#"BrowserViewController" bundle:nil];
}
browViewController.navigationItem.hidesBackButton = YES;
[((myAppDelegate *)[UIApplication sharedApplication].delegate).navController setNavigationBarHidden:YES animated:NO];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 1];
[UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:
((myAppAppDelegate *)[UIApplication sharedApplication].delegate).navController.view cache:YES];
[((myAppAppDelegate *)[UIApplication sharedApplication].delegate).navController pushViewController:browViewController animated:NO];
[UIView commitAnimations];
}
#end
BrowserViewController.h
// BrowserViewController.h
#import <UIKit/UIKit.h>
#import "myAppDelegate.h"
#interface BrowserViewController : UIViewController <UIWebViewDelegate> {
IBOutlet UITextField *browserURLField;
IBOutlet UIWebView *browserWebView;
}
#property (nonatomic, retain) UIWebView *browserWebView;
- (void) loadURLinBrowser;
#end
BrowserViewController.m
// BrowserViewController.m
#import "BrowserViewController.h"
#implementation BrowserViewController
#synthesize browserWebView;
- (void)viewDidLoad {
[browserWebView setDelegate:self];
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning {
// Releases the view if it doesn't have a superview.
[super didReceiveMemoryWarning];
}
- (void)viewDidUnload {
[super viewDidUnload];
[browserWebView stopLoading];
[browserWebView setDelegate:nil];
[browserWebView removeFromSuperview];
[browserWebView release];
browserWebView = nil;
browserURLField = nil;
}
- (void)dealloc {
[browserURLField release];
browserWebView.delegate = nil;
[browserWebView stopLoading];
browserWebView = nil;
[browserWebView release];
[super dealloc];
}
- (void) switchBackToMainViewController {
[((myAppDelegate *)[UIApplication sharedApplication].delegate).navController setNavigationBarHidden:NO animated:NO];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 1];
[UIView setAnimationTransition:UIViewAnimationTransitionCurlDown forView:((myAppAppDelegate *)[UIApplication sharedApplication].delegate).navController.view cache:YES];
[((myAppAppDelegate *)[UIApplication sharedApplication].delegate).navController popViewControllerAnimated:NO];
[UIView commitAnimations];
}
- (void) loadURLinBrowser {
NSURL *url = [[NSURL alloc] initWithString:browserURLField.text];
NSMutableURLRequest *request = [[NSURLRequest alloc] initWithURL: url];
[browserWebView loadRequest: request];
[request release];
[url release];
}
#end
I have tried various recommendations from other posts. For example:
1) Loading an empty page into the WebView.
NSString *html = #"<html><head></head><body></body></html>";
[browserWebView loadHTMLString:html baseURL:nil];
2) using removeAllCachedResponses on various places in the above code
[[NSURLCache sharedURLCache] removeAllCachedResponses];
3) setSharedURLCache did also not provide relief ( I also used this in the AppDelegate applicationDidFinishLaunching).
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
Unfortunately none of this has helped to "clear the cache" or to free memory allocated by CFData(store).
If anyone could shine some light on this and let me know what I'm missing or doing wrong I would greatly appreciate this.
.
.
Edit:
After the initial reply from KiwiBastard I added a screen shot that shows what I observe in Instruments:
.
.
Edit from June 2010:
I have still not been able to solve this.
In a second attempt, I created the UIWebView completely programmatically.
Still same issue. However I noticed a strange behavior. If I load for example a PDF document into the webView and I do not scroll the PDF page up or down, the webView & data gets successfully released. However as soon as I scroll to the second page, the dealloc won't work any longer and my App ends up running out of memory at some point. This is totally strange and I cannot get this resolved.
Anyone any idea? Help?
To release CFData you only need to call CFRelease(your CFData object name).
I think what could be happening is that your Browser is never deallocated, and the viewDidUnload is probably never being called.
Because your MainViewController has a variable of type BrowserViewController that is never released, that will be resident for the life of your app. Also because you are only switching views, the view will stay in memory too.
Can I suggest you try creating the BrowserViewController variable when you need it, and release it once it has been pushed by the navcontroller eg
BrowserViewController *browViewController = [[BrowserViewController alloc] initWithNibName:#"BrowserViewController" bundle:nil];
browViewController.navigationItem.hidesBackButton = YES;
[((myAppDelegate *)[UIApplication sharedApplication].delegate).navController setNavigationBarHidden:YES animated:NO];
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration: 1];
[UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:
((myAppAppDelegate *)[UIApplication sharedApplication].delegate).navController.view cache:YES];
[((myAppAppDelegate *)[UIApplication sharedApplication].delegate).navController pushViewController:browViewController animated:NO];
[UIView commitAnimations];
[browViewController release];
I know that it will slightly effect performance because it has to load the nib everytime, but you distinctly don't want to cache the vc anyway?
Somewhere I read, this is a well known bug with UIWebView. Some says to use a static webview object to avoid initializing it again and again but couldn't find a proper solution. Even you follows the same approach. Luckily my requirement was a plain web view with an image. So I ended up using a custom controller with a UIImageView and a UITextView without editing. Works fine for me.
Related
I have a head(butt)ache last two days. A dodgy memory leak makes me Hulk.
A lot of leaks appear after loading any url request in custom UIWebView when profiling code on the simulator. But if I use a device like iPhone 5 there is just one leak.
The huge trouble is that Instruments does not show any line of my code.
I remember as I saw a great video tutorial to locate these leaks, but googling has no result in two days :(
Here is a simple project for Xcode 5, it leaks when webView property loaded an url request.
Screenshots: one, two.
UPD: Added whole code.
UPD2: Tiny refactoring.
#import "AKViewController.h"
#interface AKViewController ()
#property (nonatomic, strong, readonly) UIWebView *webView;
#end
#implementation AKViewController
#synthesize webView = _webView;
#define MARGIN_WEB_VIEW_X 15.0f
#define MARGIN_WEB_VIEW_TOP 30.0f
#define MARGIN_WEB_VIEW_BOTTOM 25.0f
#pragma mark - Private methods
- (CGRect)makeRectForWebView {
CGRect appFrame = UIScreen.mainScreen.applicationFrame;
CGRect rectWebView = CGRectMake(MARGIN_WEB_VIEW_X,
MARGIN_WEB_VIEW_TOP,
appFrame.size.width - MARGIN_WEB_VIEW_X * 2,
appFrame.size.height - MARGIN_WEB_VIEW_BOTTOM);
return rectWebView;
}
- (void)presentViews {
[self.view.subviews makeObjectsPerformSelector:#selector(removeFromSuperview)];
self.webView.frame = [self makeRectForWebView];
[self.view addSubview:self.webView];
}
- (NSURLRequest *)makeLoginURLRequest {
NSString *stringUrl = #"http://google.com/";
NSURL *url = [NSURL URLWithString:[stringUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
return request;
}
#pragma mark - Properties
- (UIWebView *)webView {
if (!_webView) {
_webView = [[UIWebView alloc] initWithFrame:UIScreen.mainScreen.applicationFrame];
_webView.scalesPageToFit = YES;
}
return _webView;
}
#pragma mark - Lifecycle
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self presentViews];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
[self.webView loadRequest:[self makeLoginURLRequest]];
}
#end
HELP!
I don't know if you're showing all of the code, so I'm going to guess that you may not be releasing the web view's delegate. The documentation for the UIWebView delegate property says:
Important: Before releasing an instance of UIWebView for which you
have set a delegate, you must first set its delegate property to nil.
This can be done, for example, in your dealloc method.
My app is crashing while releasing view controller object.
Here is my code.
TagCloudWebViewController *controller=[[[TagCloudWebViewController alloc]init]autorelease];
controller.htmlString=[[notification userInfo] valueForKey:#"url"];
[self.navigationController pushViewController:controller animated:YES];
This is my code from wheny above method is called
-(void)viewDidLoad{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(openTextInWebview:) name:#"kTouchTextUrl" object:Nil];
}
and
#pragma mark - UIGestureDelegate
- (void)longPressRecognized:(UILongPressGestureRecognizer *)longPressRecognizer {
CGPoint touchPoint = [longPressRecognizer locationInView:self];
NSArray *subviews = self.subviews;
for (int i=0; i<subviews.count; i++) {
TagView * tagLabel = (TagView *)[subviews objectAtIndex:i];
if ( CGRectContainsPoint( [tagLabel frame], touchPoint ) ) {
NSArray*objectArray=[[[NSArray alloc] initWithObjects:tagLabel.customLink, nil] autorelease];
NSArray*keyArray=[[[NSArray alloc] initWithObjects:#"url", nil] autorelease];
NSDictionary *userInfo = [NSDictionary dictionaryWithObjects:objectArray forKeys:keyArray ];
[[NSNotificationCenter defaultCenter] postNotificationName:#"kTouchTextUrl" object:nil userInfo:userInfo];
//[[UIApplication sharedApplication] openURL:[NSURL URLWithString: tagLabel.customLink]];
break;
}
}
}
and this is notification method
DidLoad method
- (void) viewDidLoad {
[super viewDidLoad];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
_webView.delegate = self;
_webView.autoresizingMask = (UIViewAutoresizingFlexibleWidth
| UIViewAutoresizingFlexibleHeight);
_webView.scalesPageToFit = YES;
[self.view addSubview:_webView];
[self initSpinner];
if (htmlString) {
[self openURL:[NSURL URLWithString:htmlString]];
}
}
WebView delgate method
-(void) webViewDidStartLoad:(UIWebView *)webView {
self.navigationItem.title = #"Loading...";
[spinnerView startAnimating];
isLoading = YES;
}
-(void) webViewDidFinishLoad:(UIWebView*)webView {
self.navigationItem.title = [_webView stringByEvaluatingJavaScriptFromString:#"document.title"];
[self performSelector:#selector(stopSpinner) withObject:nil afterDelay:0.1];
isLoading = NO;
}
-(void) webView:(UIWebView*)webView didFailLoadWithError:(NSError*)error {
[self webViewDidFinishLoad:webView];
[self performSelector:#selector(stopSpinner) withObject:nil afterDelay:0.1];
isLoading = NO;
}
(void) openURL:(NSURL*)URL {
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:URL];
[_webView loadRequest:request];
}
Update: The following answer was in response to the original question of why the UIViewController with a UIWebView was not appearing. The OP's original theory was that it was related to some problem regarding the premature releasing of the view controller object. But, as outlined in my answer below, I suspect the problem was related to the creation of the view controller itself. Anyway, my answer to the original question follows (but it unfortunately has nothing to do with the revised question):
I personally don't suspect the problem has anything to do with the
releasing of the controller. I think it is in the creation of the
controller's view. There's nothing in the above code that causes the
object to be released, so you problem rests elsewhere and you need to
show more code. If you're concerned about your notification center
stuff, try bypassing it and just add a button that does your
TagCloudWebViewController alloc/init, sets htmlString and pushes,
and see if that still causes your program to crash, which I suspect it
will.
I notice that you're creating your controller via alloc and init,
but not initWithNibNamed. According to the UIViewController Class
Reference:
"If you cannot define your views in a storyboard or a nib file,
override the loadView method to manually instantiate a view
hierarchy and assign it to the view property."
So, bottom line, either use a NIB, use a storyboard or define a
loadView. Defining a viewDidLoad doesn't preclude the need for a
loadView. You need to create a view for your controller, which is
done for you if you use NIB or storyboard, or you have to do manually
via loadView, if you don't use NIB or storyboard.
See
iPhone SDK: what is the difference between loadView and viewDidLoad?
for a discussion of the differences between viewDidLoad and
loadView.
I don not see any thing that causing crashing.I think you have changed your question.As per question you not using ViewController any where.Please show more details.
Updated: Please check, you are creating autoreleased object, it might be you are releasing some where by mistake.Try to avoid autoreleased object as it remain in the Pool and later releases ,which may causes a memory issue for you.
The above problem is occurs due to WebView delgate. After pressing back button the reference of the object is deallocating from memory due to this app is crashing while releasing the viewcontroller object. I did some thing like
-(void) viewDidDisappear:(BOOL)animated{
if([_webView isLoading]) {
[_webView stopLoading];
}
[_webView setDelegate:nil];
}
due to above code my crashing has been resolved
I am trying to reload content from local file of UIWebView in viewDidDisappear of UIViewController. Badaccess is caught. But if I write the same code in viewWillDisappear, it works.
What might be the reason?
Once I've heard that UIWebView can't reload its content when it is not visible (not sure about it).
My code (hope it'll be enough):
#interface WebViewController : UIViewController <UIWebViewDelegate> {
ExtendedWebView * webView;
}
#property (nonatomic, retain) ExtendedWebView * webView;
#end
//WebViewController implementation
- (void)loadView
{
[super loadView];
WebViewCachingSingleton * webViewSingleton = [WebViewCachingSingleton sharedService];
ExtendedWebView * newWebView = [webViewSingleton getAvailableWebViewResource];//here I get ExtendedWebView. it works =)
newWebView.frame = CGRectMake(0, 0, 320, 400);
newWebView.delegate = self;
[self.view addSubview:newWebView];
self.webView = newWebView;
}
- (void)viewDidDisappear:(BOOL)animated
{
[[WebViewCachingSingleton sharedService] makeWebViewUnused:self.webView];
}
//WebViewCachingSingleton:
- (void) makeWebViewUnused : (ExtendedWebView *) aWebView
{
aWebView.isFree = YES;
[aWebView reload];
}
It will not work because viewDidDisappear is called when the view is disappeared, so all the subviews are released. The viewWillDisappear is called just before releasing all the objects associated with that view.
So you are trying to call the reload method of a UIWebView that has been already released. That is basically the reason why it crashes.
Hope it helps
The reload can be performed even if not displayed.
I tried to make an example of code to put in your state and have not had any problems.
Try to debug and enable NSZombieEnabled to see what actually happens to your application.
Try to post on any piece of code that might help us give you more details.
I've using shouldStartLoadWithRequest very successfully in one of my programs, but the whole project was a proof of concept and scruffy and I'm starting afresh with a new project.
However shouldStartLoadWithReqest is no longer being invoked for me but I can't see where the important difference between the two projects is (however one difference is the first is using .nibs, in the 2nd I'm not using them).
To get things started I'm using a controller with the UIWebView as its view:
#interface IMSRootController : UIViewController <UIWebViewDelegate> {
UIWebView* webView;
}
(webView is declared as a #property and #synthesized)
- (void)loadView {
[super loadView];
webView = [[UIWebView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];
self.view = webView;
[webView release]; }
- (void)viewDidLoad {
[super viewDidLoad];
[[self navigationController] setNavigationBarHidden:YES animated:NO];
[self displayPage]; }
-(void) displayPage { ... [webView loadHTMLString:self.htmlString baseURL:baseURL]; }
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
...
What's wrong?
Thanks
Your object is not being set as a delegate of the UIWebView object, hence you will not receive any delegate messages. At some point, either in loadView or even displayPage (but before the call to loadHTMLString:baseURL:), do:
webView.delegate = self;
So I have a subclass of UITableViewController that loads some data from the internet and uses MBProgressHUD during the loading process. I use the standard MBProgressHUD initialization.
HUD = [[MBProgressHUD alloc] initWithView:self.view];
[self.view addSubview:HUD];
HUD.delegate = self;
HUD.labelText = #"Loading";
[HUD show:YES];
This is the result:
.
Is there any way to resolve this issue, or should I just abandon MBProgressHUD?
Thanks!
My solution was pretty simple. Instead of using self's view, I used self's navigationController's view.
HUD = [[MBProgressHUD alloc] initWithView:self.navigationController.view];
[self.navigationController.view addSubview:HUD];
This should work for the OP because his picture shows he's using a UINavigationController. If you don't have a UINavigationController, you might add another view on top of your UITableView, and add the HUD to that. You'll have to write a little extra code to hide/show this extra view.
An unfortunate thing with this simple solution (not counting my idea adding another view mentioned above) means the user can't use the navigation controls while the HUD is showing. For my app, it's not a problem. But if you have a long running operation and the user might want to press Cancel, this will not be a good solution.
It's probably because self.view is a UITableView, which may dynamically add/remove subviews including the headers, which could end up on top of the HUD after you add it as a subview. You should either add the HUD directly to the window, or (for a little more work but perhaps a better result) you could implement a UIViewController subclass which has a plain view containing both the table view and the HUD view. That way you could put the HUD completely on top of the table view.
My solution was:
self.appDelegate = (kmAppDelegate *)[[UIApplication sharedApplication] delegate];
.
.
_progressHUD = [[MBProgressHUD alloc] initWithView:self.appDelegate.window];
.
[self.appDelegate.window addSubview:_progressHUD];
Works like a charm for all scenarios involving the UITableViewController. I hope this helps someone else. Happy Programming :)
Create a category on UITableView that will take your MBProgressHUD and bring it to the front, by doing so it will always appear "on top" and let the user use other controls in your app like a back button if the action is taking to long (for example)
#import "UITableView+MBProgressView.h"
#implementation UITableView (MBProgressView)
- (void)didAddSubview:(UIView *)subview{
for (UIView *view in self.subviews){
if([view isKindOfClass:[MBProgressHUD class]]){
[self bringSubviewToFront:view];
break;
}
}
}
#end
A simple fix would be to give the z-index of the HUD view a large value, ensuring it is placed in front of all the other subviews.
Check out this answer for information on how to edit a UIView's z-index: https://stackoverflow.com/a/4631895/1766720.
I've stepped into a similar problem a few minutes ago and was able to solve it after being pointed to the right direction in a different (and IMHO more elegant) way:
Add the following line at the beginning of your UITableViewController subclass implementation:
#synthesize tableView;
Add the following code to the beginning of your init method of your UITableViewController subclass, like initWithNibName:bundle: (the beginning of viewDidLoad might work as well, although I recommend an init method):
if (!tableView &&
[self.view isKindOfClass:[UITableView class]]) {
tableView = (UITableView *)self.view;
}
self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
self.tableView.frame = self.view.bounds;
self.tableView.contentInset = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0);
[self.view addSubview:self.tableView];
Then you don't need to change your code you posted in your question any more. What the above code does is basically seperating the self.tableView from self.view (which was a reference to the same object as self.tableView before, but now is a UIView containing the table view as one might expect).
I've Just solved that issue manually , it has been 2 years since Chris Ballinger asked but maybe someone get used of what is going on here.
In UITableViewController i execute an HTTP method in viewDidLoad , which is running in background so the table view is loaded while the progress is shown causing that miss.
i added a false flag which is changed to yes in viewDidLoad, And in viewDidAppear something like that can solve that problem.
-(void) viewDidAppear:(BOOL)animated{
if (flag) {
[self requestSomeData];
}
flag = YES;
[super viewDidAppear:animated];
}
I had the same problem and decided to solve this by changing my UITableViewController to a plain UIViewController that has a UITableView as a subview (similar to what jtbandes proposed as an alternative approach in his accepted answer). The advantage of this solution is that the UI of the navigation controller isn't blocked, i.e. users can simply leave the ViewController in case they don't want to waiting any longer for your timely operation to finish.
You need to do the following changes:
Header file:
#interface YourViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
- (id)initWithStyle:(UITableViewStyle)style;
#end
Implementation file:
#interface YourViewController ()
#property (nonatomic, retain) UITableView *tableView;
#property (nonatomic, retain) MBProgressHUD *hud;
#end
#implementation YourViewController
#pragma mark -
#pragma mark Initialization & Memory Management
- (id)initWithStyle:(UITableViewStyle)style;
{
self = [super init];
if (self) {
// create and configure the table view
_tableView = [[UITableView alloc] initWithFrame:CGRectNull style:style];
_tableView.delegate = self;
_tableView.dataSource = self;
}
return self;
}
- (void)dealloc
{
self.tableView = nil;
self.hud = nil;
[super dealloc];
}
#pragma mark -
#pragma mark View lifecycle
- (void)loadView {
CGRect frame = [self boundsFittingAvailableScreenSpace];
self.view = [[[UIView alloc] initWithFrame:frame] autorelease];
// add UI elements
self.tableView.frame = self.view.bounds;
[self.view addSubview:self.tableView];
}
- (void)viewWillDisappear:(BOOL)animated
{
// optionally
[self cancelWhateverYouWereWaitingFor];
[self.hud hide:animated];
}
The method -(CGRect)boundsFittingAvailableScreenSpace is part of my UIViewController+FittingBounds category. You can find its implementation here: https://gist.github.com/Tafkadasoh/5206130.
In .h
#import "AppDelegate.h"
#interface ViewController : UITableViewController
{
MBProgressHUD *progressHUD;
ASAppDelegate *appDelegate;
}
In .m
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
appDelegate = (ASAppDelegate *) [UIApplication sharedApplication].delegate;
progressHUD = [MBProgressHUD showHUDAddedTo:appDelegate.window animated:YES];
progressHUD.labelText = #"Syncing To Sever";
[appDelegate.window addSubview:progressHUD];
This should work.
[MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];
And to remove you can try
[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];