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.
Related
I'm a newbie in iOS programming, i am trying to make an iOS library that can be useful on my future applications. The app will have a button that will call the library and will load a website(the address link will come from the application).
I tried searching but none of it is working.
WebLibrary.h
#import <Foundation/Foundation.h>
#interface WebLibrary : NSObject
- (void)showUIWebView:(NSURL*)urlToOpen
{
/* UIViewController *myVC = [self.navigationController.viewControllers lastObject];
//This is your last view in the navigationController hierarchy.
UIWebView *newWebView = [[UIWebView alloc] initWithFrame:myVC.view.frame];
[myVC.view addSubview:newWebView];
*/
}
#end
WebLibrary.m
#import "WebLibrary.h"
#implementation WebLibrary
/* -(void) showUIWebView:(NSURL*)urlToOpen
{
//some codes here
}
*/
#end
If you want to load an URL on your webview, you need to call the loadRequest: method to perform it, example:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:_urlPath]];
[request addValue:#"YES" forHTTPHeaderField:#"Mobile-App"];
[_webView loadRequest:request];
You can add the webview to your viewcontroller in viewDidLoad method:
-(void) viewDidLoad{
[super viewDidLoad];
//custom your view
_webView = [[UIWebView alloc] initWithFrame: self.view.frame];
_webView.scalesPageToFit = YES;
[self.view addSubView: _webView];
}
You should read about UIViewController and the methods in it to understand clearly.
In my app, I am trying to make a splash image appear as my UIWebView loads so it is not just a blank screen. However my webViewDidFinishLoad method will not work. This means that the splash image appears but does not disappear from the screen once the UIWebView has loaded.
My code for the method is:
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(#"content loading finished");
[loadingImageView removeFromSuperview];
}
Any help on why the method will not work would be appreciated greatly.
My .h:
#interface ViewController : UIViewController
-(IBAction)makePhoneCall:(id)sender;
#property (nonatomic, strong) IBOutlet UIWebView *webView;
#property(nonatomic, strong) UIImageView *loadingImageView;
#end
My ViewDidLoad and webViewDidFinishLoading:
- (void)viewDidLoad {
UIWebView *mWebView = [[UIWebView alloc] init];
mWebView.delegate = self;
mWebView.scalesPageToFit = YES;
[super viewDidLoad];
}
//**************** Set website URL for UIWebView
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.sleafordpizza.com/food"]]];
//**************** Add Static loading image to prevent white "flash" ****************/
UIImage *loadingImage = [UIImage imageNamed:#"LittleItalyLogo.png"];
loadingImageView = [[UIImageView alloc] initWithImage:loadingImage];
loadingImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"LittleItalyLogo.png"],
nil];
[self.view addSubview:loadingImageView];
}
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(#"content loading finished");
// Remove loading image from view
[loadingImageView removeFromSuperview];
}
Hi probably you do not set proper delegate.
This is small code tip for you.
-(void)viewDidLoad {
mWebView = [[UIWebView alloc] init];
mWebView.delegate = self;
mWebView.scalesPageToFit = YES;
}
-(void)webViewDidFinishLoad:(UIWebView *)webView {
[loadingImageView removeFromSuperview];
NSLog(#"finish");
}
In you're .h file add.
#interface MyView: UIViewController <UIWebViewDelegate> {
UIWebView *webView;
}
Code fixes.
For .h file
#interface ViewController : UIViewController<UIWebViewDelegate>
-(IBAction)makePhoneCall:(id)sender;
#property (nonatomic, strong) IBOutlet UIWebView *webView;
#property(nonatomic, strong) UIImageView *loadingImageView;
#end
For .m file
- (void)viewDidLoad
{
[super viewDidLoad];
webView.delegate = self;
//**************** Set website URL for UIWebView
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.sleafordpizza.com/food"]]];
//**************** Add Static loading image to prevent white "flash" ****************/
UIImage *loadingImage = [UIImage imageNamed:#"LittleItalyLogo.png"];
loadingImageView = [[UIImageView alloc] initWithImage:loadingImage];
loadingImageView.animationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"LittleItalyLogo.png"],
nil];
[self.view addSubview:loadingImageView];
}
At certain times, this delegate method actually never gets fired. I have had severe problems with the same thing in some of my projects.
At one occasion, I actually had to solve it with a timer, checking the state of the web view every second or so to see if I could proceed.
In that particular case, I just needed a certain element to be present. Still, the view did not trigger the finish loading event, due to external script errors being injected.
So, I just started a trigger when the web view begun loading, then called a method every now and then to see if the web view contained the element in question.
- (void)methodCalledByTimer {
if (<I still do not have what I need>) {
//The web view has not yet finished loading; keep checking
} else {
//The web view has finished loading; stop the timer, hide spinners and proceed
}
}
You could also check if the web view is actually loading, if that is absolutely necessary:
- (void)methodCalledByTimer {
if (self.webView.isLoading) {
//The web view has not yet finished loading; keep checking
} else {
//The web view has finished loading; stop the timer, hide spinners and proceed
}
}
Then, naturally, I'd check for the finishedLoading event as well, just to be sure. Remember to also implement the webView:didFailLoadWithError: method as well.
When waiting for a web page to finish loading, there are some things to keep in mind.
For instance, do you really need it to stop loading, or is there anything else you can do? In my case, I needed an element. Being able to properly execute a script is another thing that may be required.
Second, is the loading page using any external resources? I once had external script errors causing the webViewDidFinishLoad: method to not being called at all. If I removed the external scripts, it worked.
Third, if the page is using external resources, you are exposed not only to the loading capacity of your own resources, but that of the external resources as well. Tracking scripts, ads etc...if one resource provider is delivering content sloooowly (or not at all), you could page could be stuck in loading state forever.
So, I'd go with checking for something else. :)
I see you aren't handling errors. If there is an error, all subsequent delegate calls will not happen. I was surprised to find that this is true when the webview uses a plugin too. It calls this error method telling you that the webview handed off to the delegate, in my case the movie player.
implement this and see if that is it.
-(void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if (error.code == 204) {
//request was handled by a plugin instead of by the webview directly
...
}
else
{
NSLog(#"didFailLoadWithError. ERROR: %#", error);
}
}
I was able to do all the remaining loading work in this method instead of the webviewdidfinishLoad
I have implemented a browser in my application by using UIWebView, by default I'm loading google page in my browser.
When I search something in the google page ,the UIWebViewDelegate's webView:shouldStartLoadWithRequest:navigationType: method is called.
The problem is when I tap on the back button from this search page no delegates are getting called, so I am having a problem disabling my back button.
This problem happens only in an iPad application not in an iPhone application.
This code may help u...
A UIWebView is a UIView that can load a web page while remaining in the user's application.
Navigation to other webpages is allowed through the use of imbedded links in a web page itself. Forward and backward navigation through history can be set up with instance methods goForward and goBack, but the programmer must supply the buttons.
The following example uses a UIWebView, and
1) adds forward and backward buttons. The buttons are enabled and highlighted using UIWebViewDelegate optional methods webViewDidStartLoad: and webViewDidFinishLoad:
2) adds a UIActivityIndicatorView which displays while the web page is loading
In the .h file for the WebViewController :
Declare the UIWebView, Optionally : add buttons to control moving forward and backward through browsing history and IBActions for pressing the buttons, Optionally again : add a UIActivityIndicatorView.
#interface WebViewController : UIViewController <UIWebViewDelegate>
{
UIWebView *webView;
UIButton *back;
UIButton *forward;
UIActivityIndicatorView *activityIndicator;
}
#property(nonatomic,retain)IBOutlet UIWebView *webView;
#property(nonatomic,retain)IBOutlet UIButton *back;
#property(nonatomic,retain)IBOutlet UIButton *forward;
#property(nonatomic,retain)IBOutlet UIActivityIndicatorView *activityIndicator;
-(IBAction)backButtonPressed: (id)sender;
-(IBAction)forwardButtonPressed: (id)sender;
#end
//In the .m file for the WebViewController
#implementation WebViewController
#synthesize webView;
#synthesize back;
#synthesize forward;
#synthesize activityIndicator;
//method for going backwards in the webpage history
-(IBAction)backButtonPressed:(id)sender {
[webView goBack];
}
//method for going forward in the webpage history
-(IBAction)forwardButtonPressed:(id)sender
{
[webView goForward];
}
//programmer defined method to load the webpage
-(void)startWebViewLoad
{
//NSString *urlAddress = #"http://www.google.com";
NSString *urlAddress = #"http://cagt.bu.edu/page/IPhone-summer2010-wiki_problemsandsolutions";
//Create a URL object.
NSURL *url = [NSURL URLWithString:urlAddress];
//URL Requst Object
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
//Load the request in the UIWebView.
[webView loadRequest:requestObj];
}
// acivityIndicator is set up here
- (void)viewDidLoad
{
//start an animator symbol for the webpage loading to follow
UIActivityIndicatorView *progressWheel = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
//makes activity indicator disappear when it is stopped
progressWheel.hidesWhenStopped = YES;
//used to locate position of activity indicator
progressWheel.center = CGPointMake(160, 160);
self.activityIndicator = progressWheel;
[self.view addSubview: self.activityIndicator];
[self.activityIndicator startAnimating];
[progressWheel release];
[super viewDidLoad];
//call another method to do the webpage loading
[self performSelector:#selector(startWebViewLoad) withObject:nil afterDelay:0];
}
- (void)dealloc
{
[webView release];
[back release];
[forward release];
[activityIndicator release];
[super dealloc];
}
#pragma mark UIWebViewDelegate methods
//only used here to enable or disable the back and forward buttons
- (void)webViewDidStartLoad:(UIWebView *)thisWebView
{
back.enabled = NO;
forward.enabled = NO;
}
- (void)webViewDidFinishLoad:(UIWebView *)thisWebView
{
//stop the activity indicator when done loading
[self.activityIndicator stopAnimating];
//canGoBack and canGoForward are properties which indicate if there is
//any forward or backward history
if(thisWebView.canGoBack == YES)
{
back.enabled = YES;
back.highlighted = YES;
}
if(thisWebView.canGoForward == YES)
{
forward.enabled = YES;
forward.highlighted = YES;
}
}
#end
/*****************************/
//In viewDidLoad for the class which adds the WebViewController:
WebViewController *ourWebVC = [[WebViewController alloc] initWithNibName:#"WebViewController" bundle:nil];
ourWebVC.title = #"WebView";
[self.view addSubview:ourWebVC];
//release ourWebVC somewhere else
In your case ,You have to ignore/avoid "caching data". Following lines of code may help.
NSURLRequest *requestObj = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.google.com"] cachePolicy: NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:10.0];
[webView loadRequest:requestObj];
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;
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.