views:

50

answers:

2

Does anyone know why xCode's "build and analyze" would report this line as a "possible memory leak"?

goodSound = [[SoundClass alloc] initializeSound:@"Good.wav"];

///// Here are the 4 files in question:

//  Sounds.h 

#import <Foundation/Foundation.h> 
#import <AudioToolbox/AudioToolbox.h> 

@interface SoundClass : NSObject  
{ 
    SystemSoundID soundHandle; 
} 

-(id) initializeSound:(NSString *)soundFileName; 
-(void) play;    

@end 

/////////////

//  Sounds.m 

#import "Sounds.h" 

@implementation SoundClass 

-(id) initializeSound:(NSString *)soundFileName 
{ 
    self = [super init]; 

    NSString *const resourceDir = [[NSBundle mainBundle] resourcePath]; 
    NSString *const fullPath    = [resourceDir stringByAppendingPathComponent:soundFileName]; 
    NSURL *const url            = [NSURL fileURLWithPath:fullPath]; 

    OSStatus errCode = AudioServicesCreateSystemSoundID((CFURLRef) url, &soundHandle); 

    if(errCode == 0) 
        NSLog(@"Loaded sound: %@", soundFileName); 
    else 
        NSLog(@"Failed to load sound: %@", soundFileName); 

    return self;             
} 

////////////////////////////// 

-(void) play     
{ 
    AudioServicesPlaySystemSound(soundHandle); 
} 

///////////////////////////// 

-(void) dealloc 
{ 
    AudioServicesDisposeSystemSoundID(soundHandle); 
    [super dealloc]; 
} 

///////////////////////////// 

@end 

//////////////

//  MemTestViewController.h 

#import <UIKit/UIKit.h> 

@class SoundClass; 

@interface MemTestViewController : UIViewController  
{ 
    SoundClass *goodSound; 
} 

-(IBAction) beepButtonClicked:(id)sender; 

@end 

///////////

//  MemTestViewController.m 

#import "MemTestViewController.h" 
#import "Sounds.h" 

@implementation MemTestViewController 

- (void)viewDidLoad  
{ 
    NSLog(@"view did load: alloc'ing mem for sound class"); 

    // "build and analyze" says this is possibly a memory leak: 
    goodSound = [[SoundClass alloc] initializeSound:@"Good.wav"]; 

    [super viewDidLoad]; 
} 


-(IBAction) beepButtonClicked:(id)sender 
{ 
    NSLog(@"beep button clicked"); 

    [goodSound play]; 
} 


- (void)didReceiveMemoryWarning  
{ 
    [super didReceiveMemoryWarning]; 
} 


- (void)dealloc  
{ 
    [goodSound release]; 
    [super dealloc]; 
} 

@end 
A: 

The static analyzer is right; your code is a possible memory leak. Because static analysis can't prove that MemTestViewController's dealloc method will always be called (doing so would solve the halting problem, afterall), there is no way for the static analyzer to be sure that goodSound will be correctly released.

Modern Objective-C rarely uses direct ivar access accept in init and dealloc methods. You should declare a @property (retain) for your goodSound, even if it's private, and use that elsewhere in your class, including in the viewDidLoad method. The static analyzer should then correctly recognize this pattern and not flag the line as a warning.

On a side note—just in case you're interested—your -[SoundClass initializeSound:] should be named something like -[SoundClass initWithSoundFileName:] to match convention, and should be written with a guard to test for a nil return from [super init]:

-(id)initWithSoundFileName:(NSString*)soundFileName
{ 
    self = [super init]; 

    if(self != nil) {
      NSString *const resourceDir = [[NSBundle mainBundle] resourcePath]; 
      NSString *const fullPath    = [resourceDir stringByAppendingPathComponent:soundFileName]; 
      NSURL *const url            = [NSURL fileURLWithPath:fullPath]; 

      OSStatus errCode = AudioServicesCreateSystemSoundID((CFURLRef) url, &soundHandle); 

      if(errCode == 0) 
        NSLog(@"Loaded sound: %@", soundFileName); 
      else 
        NSLog(@"Failed to load sound: %@", soundFileName); 
    }

    return self;             
} 

It's always best to add this guard, especially because you are calling into C code that might not handle nil/NULL as nicely as Objective-C.

Barry Wark
I made several of the great suggestions here... but the problem disappeared as soon as I made 1 small change: Rename the method to initWithSoundFileName. I thought that was just a SUGGESTED naming-convention. But it seems like Apple' analyzer shows/hides problems... based on the *NAME* itself. Isn't that dangerous? If I had used that name from the start... I would have never seen any problems... even though there we 2-3 potential issues.
Annette
@Annette - The static analyzer uses some heuristics to help it understand your code, particularly when it comes to retain counts being returned from methods. It follows the standard Cocoa naming conventions (which you should too, as an aid to managing memory later on), so if you don't return the proper retain count from a method starting with -init, -copy, etc. (or vice versa) it will inform you of this. This is not hiding errors, it is merely pointing out potential problems when you aren't following convention in your coding.
Brad Larson
But initializeSound and initWithSoundFile both start with "init". Anyway, thanks everyone for all the great info.
Annette
+3  A: 

It is a possible memory leak. viewDidLoad may be called any number of times if the view is unloaded, in which case you will leak memory each time. To guard, you can either release the memory in viewDidUnload and set the ivar to nil, or you can simply initialize the sound in viewDidLoad only if the ivar actually is nil.

So:

- (void)viewDidLoad
{
  if( !goodSound )
    goodSound = [[SoundClass alloc] initializeSound:@"Good.wav"];
  [super viewDidLoad];
}

And/or:

- (void)viewDidUnload
{
  [goodSound release];
  goodSound = nil;
  [super viewDidUnload];
}
Jason Coco