I have a line of UILabel text in a UIView which is regularly updated via NSTimer. This code is supposed to write a status item near the bottom of the screen every so often. The data comes from outside of its control.
My app runs out of memory really fast because it seems the UILabel is not being released. It seems that dealloc is never called.
Here is a very compressed version of my code (error checking etc removed for clarity.):
File:SbarLeakAppDelegate.h
#import <UIKit/UIKit.h>
#import "Status.h"
#interface SbarLeakAppDelegate : NSObject
{
UIWindow *window;
Model *model;
}
#end
File:SbarLeakAppDelegate.m
#import "SbarLeakAppDelegate.h"
#implementation SbarLeakAppDelegate
- (void)applicationDidFinishLaunching:(UIApplication *)application
{
model=[Model sharedModel];
Status * st=[[Status alloc] initWithFrame:CGRectMake(0.0, 420.0, 320.0, 12.0)];
[window addSubview:st];
[st release];
[window makeKeyAndVisible];
}
- (void)dealloc
{
[window release];
[super dealloc];
}
#end
File:Status.h
#import <UIKit/UIKit.h>
#import "Model.h"
#interface Status : UIView
{
Model *model;
UILabel * title;
}
#end
File:Status.m
This is the where the problem lies. UILabel just does not seem to be released, and quite possible the string as well.
#import "Status.h"
#implementation Status
- (id)initWithFrame:(CGRect)frame
{
self=[super initWithFrame:frame];
model=[Model sharedModel];
[NSTimer scheduledTimerWithTimeInterval:.200 target:self selector:#selector(setNeedsDisplay) userInfo:nil repeats:YES];
return self;
}
- (void)drawRect:(CGRect)rect
{
title =[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 12.0f)];
title.text = [NSString stringWithFormat:#"Tick %d", [model n]] ;
[self addSubview:title];
[title release];
}
- (void)dealloc
{
[super dealloc];
}
#end
File: Model.h (this and the next are the data sources, so included only for completeness.) All it does is update a counter every second.
#import <Foundation/Foundation.h>
#interface Model : NSObject
{
int n;
}
#property int n;
+(Model *) sharedModel;
-(void) inc;
#end
File: Model.m
#import "Model.h"
#implementation Model
static Model * sharedModel = nil;
+ (Model *) sharedModel
{
if (sharedModel == nil)
sharedModel = [[self alloc] init];
return sharedModel;
}
#synthesize n;
-(id) init
{
self=[super init];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:#selector(inc) userInfo:nil repeats:YES];
return self;
}
-(void) inc
{
n++;
}
#end
The problem is that you are never removing the UILabel from the Status UIView. Let's take a look at your retain counts in drawRect:
(void)drawRect:(CGRect)rect {
title =[[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 12.0f)];
Here, you have created a UILabel with alloc, which creates an object with a retain count of 1.
[self addSubview:title];
[title release];
Adding the UILabel to Status view increases title's retain count to 2. The following release results in a final retain count of 1. Since the object is never removed from its superview, the object is never deallocated.
Essentially, you are adding one UILabel on top of another, each time the timer is fired, until memory runs out.
As suggested below, you should probably create the UILabel once when the view loads, and just update the UILabel's text with [model n].
As a housekeeping note, you might also want to make sure that you are properly deallocating any left over objects in your dealloc methods. 'model' and 'title' should be released in Status' dealloc, just as 'model' should be in SbarLeakAppDelegate.
Hope this helps.
Edit [1]:
It sounds like you have the memory issue pretty well handled at this point. I just wanted to suggest another alternative to the two timers you are using.
The timer you have running in your Status object fires every .2 seconds. The timer which actually increments the 'model' value, n, fires only once each second. While I believe you are doing this to ensure a more regular "refresh rate" of the Status view, you are potentially re-drawing the view 4 or 5 times per second without the data changing. While this is may not be noticeable because the view is fairly simple, you might want to consider something like NSNotification.
With NSNotification, you can have the Status object "observe" a particular kind of notification that will be fired by the Model whenever the value 'n' changes. (in this case, approximately 1 per second).
You can also specify a callback method to handle the notification when it is received. This way, you would only call -setNeedsDisplay when the model data was actually changed.
There are 2 problems with your code.
Problem 1
In -drawRect you add a subview to the view hierarchy every time the view is drawn. This is wrong for 2 reasons:
Every time the view is drawn, the number of subviews increases by 1
You are modifying the view hierarchy at draw time - this is incorrect.
Problem 2
Timers retain their targets. In the initializer for your Status object, you create a timer which targets self. Until the timer is invalidated, there is a retain cycle between the timer and the view, so the view will not be deallocated.
If the approach of using a timer to invalidate the view is really the correct solution to your problem, you need to take explicit steps to break the retain cycle.
One approach to doing this is to schedule the timer in -viewDidMoveToWindow: when the view is being put in a window [1], and invalidate the timer when the view is being removed from a window.
[1] Having the view invalidate itself periodically when it isn't displayed in any window is otherwise pointless.
Instead of calling -setNeedsDisplay with your NSTimer in the view controller, why not create a method that calls "title.text = [NSString stringWithFormat:#"Tick %d", [model n]] ;"? That way instead of re-creating the label every time the timer fires you can just update the value displayed.
Related
I have a major problem when trying to access a UIWebView that was created during ViewDidLoad, the UIWebView appears null
here is how i declare the property
#property (nonatomic, retain) UIWebView *detailsView;
the implementation
#implementation iPadMainViewController
#synthesize detailsView;
- (void)viewDidLoad
{
[super viewDidLoad];
detailsView = [[UIWebView alloc] initWithFrame:CGRectMake(500, 0, 512, 768)];
[self.view addSubView:detailsView];
}
When accessing from
- (void)loadDetailedContent:(NSString *)s
{
NSLog(#"%#", detailsView);
}
I get NULL, is it a normal behavior or am i doing something wrong?
here is the touchesBegan that is being called, from the views subclass that is being touched,
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
iPadMainViewController *mycontroller = [[iPadMainViewController alloc] init];
self.delegate = mycontroller;
[self.delegate loadDetailedContent:NewsId];
}
1st of all change you synthesize to
#synthesize detailsView = _detailsView
and allocate your uiwebvie
_detailsView = [[UIWebView alloc] initWithFrame:CGRectMake(500, 0, 512, 768)];
And try to push your ipadMainController in touchBegan to main screen and in vieDidLoad call loadDetailedContent
Are you sure that loadDetailedContent is not called before viewDidLoad? Set breakpoints in both methods and see which one gets hit first.
EDIT Since you updated the question with the code, it is easy to see why this problem happens. In touchesBegan, you instantiate the UIViewController and then call loadDetailedContent. This means that loadDetailedContent will be called before viewDidLoad.
viewDidLoad is called first when the view it controls has been created.
I have been trying for days to get this code to work, but I have no idea what I am doing wrong. Everytime the app wakes up from sleep, or the user closes the app and opens it again (without closing the app from multitasking), I want a label value to change.
In my applicationDidBecomeActive, I am running a counter, which I want to display on whatever viewcontroller is open at that moment.
Code:
- (void)applicationDidBecomeActive:(UIApplication *)application {
counter = counter + 1;
W1G1 *view1 = [[[W1G1 alloc] initWithNibName:#"W1G1" bundle:nil] retain];
[view1 setlabel];
}
In my viewcontroller W1G1, I have the following code:
Code:
- (void) setlabel {
NSString *string = [NSString stringWithFormat:#"%d", counter];
vocabword.text = string;
}
I have imported W1G1 in my appdelegate, but the code does not run :( Please help!
Thanks
In the AppDelegate.m file, where you have
- (void)applicationDidBecomeActive:(UIApplication *)application {
counter = counter + 1;
W1G1 *view1 = [[[W1G1 alloc] initWithNibName:#"W1G1" bundle:nil] retain];
[view1 setlabel];
}
the variable counter being incremented is confined to the AppDelegate. In other words, your view controller doesn't know that it has been incremented.
I would suggest that you use NSUserDefaults to store the value of counter so that you can easily pass it between these view controllers. Either that, or you could allow for an input into the method setLabel, e.g.
- (void) setlabel:(int)counter {
NSString *string = [NSString stringWithFormat:#"%d", counter];
vocabword.text = string;
}
and then in the AppDelegate you'll want to do:
- (void)applicationDidBecomeActive:(UIApplication *)application {
counter = counter + 1;
W1G1 *view1 = [[[W1G1 alloc] initWithNibName:#"W1G1" bundle:nil] retain];
[view1 setlabel:counter]; // <-- now you're using counter
[self.window addSubview:view1];
}
1) When you say 'the code does not run' do you mean that? That is, if you put NSLogs in applicationDidBecomeActive: and in setLabel does it show the code is run?
2) I would suspect the code is running. But your code won't "show the counter on whatever view controller is open at that moment". Your code creates a new view (view1), but that view won't be displayed. It is not added as a subview to anything. Your code will also leak. You create a W1G1 object, but it is never released and you throw away any reference you have to it.
To achieve what you want, you could add a subview to the application's window. Depending how your app delegate is set up, something like the following should do the trick:
counter++;
W1G1 *viewController1 = [[W1G1 alloc] initWithNibName:#"W1G1" bundle:nil];
[viewController1 setlabel: counter];
[[self window] addSubview: [viewController1 view]]
// you'll want to save a reference to the viewController somehow so you can release it at a later date
Then in W1G1
- (void) setlabel: (int) counter;
{
NSString *string = [NSString stringWithFormat:#"%d", counter];
vocabword.text = string;
}
There are, of course, lots of other approaches you could take towards this problem. And you'll need some strategy for removing the W1G1 view that you are adding at some stage, otherwise you'll just get more and more views added.
Update: You ask (in comments) how to keep track of your viewController throughout lifetime of the app... One approach is to keep track of it in your appDelegate. In the header have something like:
#class W1G1;
#interface MyAppDelegate : : NSObject <UIApplicationDelegate>
{
// other decelerations
int counter;
W1G1 * _myW1G1
}
#property (nonatomic, retain) W1G1* theW1G1
In the .m file include
#synthesize theW1G1 = _myW1G1;
Probably in application:didFinishLaunchingWithOptions: create the viewController, set the property to refer to it, and add its view to the view hierarchy.
W1G1* theViewController = [[W1G! alloc] initWithNibName: #"W1G1" bundle: nil];
[[self window] addSubview: [theViewController view]];
[self setTheW1G1: theViewController];
[theViewController release];
Then when you want to access the viewController again from with the app delegate use [self theW1G1], e.g.
[[self W1G1] setlabel: counter];
My main class MMAppViewController has an "IBOutlet UIImageView *padlock". This controller pushes a Level1View view which is my quiz game. The MMAppViewContoller has 2 buttons level 1 and level 2. Level 2 has the padlock on it and will unlock when a certain score is reached. When the MMAppViewController is pushed back, is there a way to hide the padlock. I know the following code will do this but my problem lies in where to put the code:
if(theScore>4){
[padlock setHidden:TRUE];
}
With my Level1View i can put code in the "viewdidload()" section, but it does not work with my main view because it only seems to load once! I tried puting the code in my Level1View class but keep getting errors about tokens or it being undeclared:
[MMAppViewController padlock setHidden:TRUE];
or
[padlock setHidden:TRUE];
Is there a way of either putting this code in my Level1View class, or is there a way of having the code in my MMAppViewContoller class that will work when Level1View is "unpushed"?? (not sure of terminology)
Not knowing more about the structure of your program it's hard to know the right way to achieve this.
There are several possible approaches, but viewDidLoad is only going to be called once and should be used for setting up the view initially, and not for this sort of repeated logic. You probably have a model object somewhere that holds the score. (If you don't, i.e. if theScore is an instance variable on your ViewController, as your snippets might imply, you should move it to it's own model object.) The best way to go about this would be for your ViewController to "observe" the model object that holds the score using Key-Value Observing. Here's how you might achieve that:
Let's say you have the following model object to hold your game session data (here, only the current score):
#interface GameSession : NSObject
#property (readwrite) double score;
#end
... and its corresponding implementation ...
#implementation GameSession
#synthesize score;
#end
And then assuming you have a ViewController declaration that looks something like this:
#class GameSession;
#interface MyViewController : UIViewController
{
GameSession *game;
IBOutlet UIImageView *padlock;
}
#end
You could set up the following methods on the ViewController, such that every time the score value of the model object is modified, the ViewController will automatically update the hidden state of the padlock image view:
- (void)viewDidLoad
{
[super viewDidLoad];
game = [[GameSession alloc] init];
[game addObserver:self forKeyPath:#"score" options:NSKeyValueObservingOptionInitial context: [RootViewController class]];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == [RootViewController class])
{
if ([keyPath isEqualToString: #"score"])
{
NSNumber* newValue = [change objectForKey: NSKeyValueChangeNewKey];
double currentScore = [newValue doubleValue];
[padlock setHidden: (currentScore < 4.)];
}
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (void)dealloc
{
[game removeObserver:self forKeyPath:#"score"];
[game release];
game = nil;
[super dealloc];
}
For a full explanation of Key-Value Observing, see this web page: http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/KeyValueObserving/
Let me know if this isn't clear.
The simple option is to put the code in viewWillAppear:.
I am making a simple game project involving the use of Cocos2d. Now as defined by Ray Wenderlich's example, i have completed the whole tutorial but added an extra bit of code myself to check total number of melons, when they reach 3, i replace screen with "You Win" screen to notify the user that he has won using [[CCDirector sharedDirector] replaceScene:gameoverscreen];.
The problem is that i get EXC_BAD_ACCESS everytime i call this from ccTouchEnded coz my condition is checked here. But the same thing works if i use [[CCDirector sharedDirector] pushScene:gameoverscreen];
Cant understand what the problem is!!
the code for gameoverscreen screen is:
#import "GameOverScene.h"
#import "HelloWorldScene.h"
#implementation GameOverScene
#synthesize _layer = layer;
- (id)init {
if ((self = [super init])) {
self._layer = [GameOverLayer node];
[self addChild:layer];
}
return self;
}
- (void)dealloc {
[layer release];
layer = nil;
[super dealloc];
}
#end
#implementation GameOverLayer
#synthesize _label = label;
-(id) init
{
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
CGSize winSize = [[CCDirector sharedDirector] winSize];
self._label = [CCLabel labelWithString:#"" fontName:#"Arial" fontSize:32];
label.color = ccc3(0,0,0);
label.position = ccp(winSize.width/2, winSize.height/2);
[self addChild:label];
[self runAction:[CCSequence actions:
[CCDelayTime actionWithDuration:3],
[CCCallFunc actionWithTarget:self selector:#selector(gameOverDone)],
nil]];
}
return self;
}
- (void)gameOverDone {
[[CCDirector sharedDirector] replaceScene:[[[HelloWorld alloc] init] autorelease]];
}
- (void)dealloc {
[label release];
label = nil;
[super dealloc];
}
#end
and the Header file of GameoverScene contains the following!
#import "cocos2d.h"
#interface GameOverLayer : CCColorLayer {
CCLabel *label;
}
#property (nonatomic, retain) CCLabel *_label;
#end
#interface GameOverScene : CCScene {
GameOverLayer *layer;
}
#property (nonatomic, retain) GameOverLayer *_layer;
#end
i call the scene from HelloWorld class using the following syntax!
GameOverScene *gameoverscene = [GameOverScene node];
[gameoverscene._layer._label setString:#"You WON!"];
[[CCDirector sharedDirector] pushScene:gameoverscene];
I see several issues in your code.
One is the CCLabel object, you initialize it as autorelease object using cocos2d's static initializer:
self._label = [CCLabel labelWithString:#"" fontName:#"Arial" fontSize:32];
But in the dealloc method you release it even though its an autorelease object:
- (void)dealloc {
[label release];
label = nil;
[super dealloc];
}
You should not release the label since it is set to autorelease by cocos2d! This is a guaranteed crash!
Then you make things more complicated than needed:
[[CCDirector sharedDirector] replaceScene:[[[HelloWorld alloc] init] autorelease]];
The alloc/init/autorelease is completely superfluous because you can simply write [HelloWorld scene] if the HelloWorld class has a +(id) scene method (it normally should). If not, then use [HelloWorld node]. Always prefer cocos2d's static autorelease initializers before using alloc/release on cocos2d objects. The only time you ever need to alloc a cocos2d class is when you explicitly don't add it as a child to some other node, which is rare.
Finally, this is very bad style:
-(id) init
{
if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {
If the super implementation of initWithColor calls [self init] - which is often the case and even if not, could change with future releases of cocos2d - it would call your implementation of init, resulting in an endless loop (stack overflow). To fix this simply either rename your init method or call [super init] and provide the parameters some other way, usually there will be a property or setter method to do so.
And a minor issue: Apple advises against using leading underscores as member variable prefix. In fact, many other compiler vendors advice against that too since often system internal variables use one or two underscores as prefix. The cocos2d style with trailing underscores is preferred which would have you write label_ instead of _label.
EXEC_BAD_ACCESS means you are using data that has been released. Does the youwin scene use data from the current scene? If so, it needs to retain the data. When replaceScene: is called, the current scene is not held in memory but when pushScene: is called, both scenes remain in memory.
EDIT:
Let's say you have two scenes, A and B. When you call pushScene:, A continues to exist in memory and B is added. When you call replaceScene:, A is removed and no longer exists, only the B scene. That is why A's data would disappear, but only when replacing.
The general rule when it comes to memory dealing is to release whatever you alloced or retained. In your case you are instantiating a CCLabel object with a convenience method (thus, not calling alloc) and you are not retaining it. So, that [label release] in your dealloc method must not be there in this case.
I also account with such thing,the reason for it may be you release something that are autorelease,so you can try it again by not to release some object in the dealloc method!
I want to organize somehow my iPhone game's level-views, but I simply cannot (without expanding Object Allocations). I made a really "skeleton" of my code (this game has 2 levels, the goal is to release the iPhone display). I just cannot dealloc the previous level, so Instrunments shows incrementing BGTangramLevel instances.
Please, take a look on it, I need some helpful ideas on designing (my 3rd question on it).
viewcontroller.h
#interface compactTangramViewController : UIViewController
{
//The level.
BGTangramLevel *currentLevel;
UIColor *levelColor;
}
//It is to be just a reference, therefore I use assign here.
#property (nonatomic, retain) BGTangramLevel *currentLevel;
-(void) notificationHandler: (NSNotification*) notification;
-(void) finishedCurrentLevel;
#end
viewcontroller.m
#implementation compactTangramViewController
#synthesize currentLevel;
//Initializer functions, setting up view hierarchy.
-(void) viewDidLoad
{
//Set up levelstepper.
levelColor = [UIColor greenColor];
//Set up "state" classes.
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(notificationHandler:) name:#"finishedCurrentLevel" object:nil];
//Attach level 1.
currentLevel = [BGTangramLevel levelWithColor: levelColor frame:self.view.frame];
[self.view addSubview:currentLevel];
[super viewDidLoad];
}
//Release objects.
-(void) dealloc
{
[currentLevel release];
[super dealloc];
}
//Notification handling.
-(void) notificationHandler: (NSNotification*) notification
{
//Execute level swap.
if ([notification name] == #"finishedCurrentLevel") [self finishedCurrentLevel];
}
-(void) finishedCurrentLevel
{
//Remove previous level.
[currentLevel removeFromSuperview];
//[currentLevel release];
//Step level.
if (levelColor == [UIColor greenColor]) levelColor = [UIColor blueColor]; else levelColor = [UIColor greenColor];
//Attach level 2.
currentLevel = [BGTangramLevel levelWithColor: levelColor frame:self.view.frame];
[self.view addSubview:currentLevel];
}
#end
BGTangramLevel.h
#interface BGTangramLevel : UIView
{
BOOL puzzleCompleted;
}
//Initializer.
+(BGTangramLevel*)levelWithColor: (UIColor*) color frame: (CGRect) frame;
//Test if the puzzle is completed.
-(void) isSolved;
#end
BGTangramLevel.m
#implementation BGTangramLevel
//Allocated instance.
+(BGTangramLevel*)levelWithColor: (UIColor*) color frame: (CGRect) frame
{
BGTangramLevel *allocatedLevel = [[BGTangramLevel alloc] initWithFrame:frame];
allocatedLevel.backgroundColor = color;
return allocatedLevel;
}
//Finger released.
-(void) touchesEnded: (NSSet*)touches withEvent: (UIEvent*)event
{
//The completement condition is a simple released tap for now...
puzzleCompleted = YES;
[self isSolved];
}
//Test if the puzzle is completed.
-(void) isSolved
{
//"Notify" viewController if puzzle has solved.
if (puzzleCompleted) [[NSNotificationCenter defaultCenter] postNotificationName:#"finishedCurrentLevel" object:nil];
}
-(void) dealloc
{
NSLog(#"Will ever level dealloc invoked."); //It is not.
[super dealloc];
}
#end
So what should I do? I tried to mark autorelease the returning level instance, release currentLevel after removeFromSuperview, tried currentLevel property synthesized in (nonatomic, assign) way, but Object Allocations still grow. May I avoid Notifications? I'm stuck.
You need to follow retain/release rules more closely. You definitely should not experimentally add retain and release and autorelease in places just to find something that works. There's plenty written about Cocoa memory management already, I won't repeat it here.
Specifically, BGTangramLevel's levelWithColor:frame: method should be calling [allocatedLevel autorelease] before returning allocatedLevel to its caller. It doesn't own the object, it's up to the caller to retain it.
You also need to know the difference between accessing an instance variable and accessing a property. Cocoa's properties are just syntactic-sugar for getter and setter methods. When you reference currentLevel in your view controller you are dealing with the instance variable directly. When you reference self.currentLevel you are dealing with the property.
Even though you've declared a property, currentLevel = [BGTangram ...] simply copies a reference into the variable. In viewDidLoad, you need to use self.currentLevel = [BGTangram ...] if you want to go through the property's setter method, which will retain the object (because you declared the property that way). See the difference?
I think your leak is happening in finishedCurrentLevel. If you had used self.currentLevel = [BGTangram ...], the property's setter method would be called, which would release the old object and retain the new one. Because you assign to the instance variable directly, you simply overwrite the reference to the old level without releasing it.
Calling [currentLevel release] in the dealloc method of your view controller is correct.