Memory will not be released in iOS app using ARC
I created a simple petty game using ARC. While profiling memory usage with the Allocations profiling tool in Xcode, I see that memory is not always freed. For one example of a problem, I have a class for an ActivePlayer object:
ActivePlayer.h:
@interface ActivePlayer : NSObject
@property (nonatomic, strong) NSString * name;
@property (nonatomic) NSInteger overallScore;
@property (nonatomic) NSInteger questionScore;
- (id) initWithName:(NSString *)name;
@end
ActivePlayer.m:
#import "ActivePlayer.h"
@interface ActivePlayer ()
@end
@implementation ActivePlayer
- (id) initWithName:(NSString *)name
{
self = [self init];
if (self) {
self.name = name;
self.overallScore = 0;
}
return self;
}
/*
- (void)dealloc
{
self.name = nil;
}
*/
@end
And the ActivePlayer is created in the createPlayer method in the ActiveGame class:
[[ActivePlayer alloc] initWithName:name]
I am doing the following test case: I start a new game (which highlights one ActivePlayer), I answer one question, and then the game ends (and at this point the ActivePlayer is released). Then I can start a new game and repeat this cycle (each cycle is a "game" as described below). When using the Allocations profiling tool, I expect to see that memory was allocated in the middle of the game, but freed after the game was over (no matter how many times I play the game). But I found that this is not always the case:
BTW: each bulleted line below describes a line in the Object List tab of the Allocations tool; this site will not allow me to post a screenshot, hence a text description. All lines are Live; I only view created and universes.
While game # 1 is running, I see the following distributions.
- Category = ActivePlayer; Size = 16; Responsible Caller = - [ActiveGame createPlayer:]
- Category = Malloc 48 bytes; Size = 48; Responsible Caller = - [ActivePlayer initWithName:]
After completing game # 1, I see the following. The ActivePlayer has been released, but 48 bytes are still Live.
- Category = Malloc 48 bytes; Size = 48; Responsible Caller = - [ActivePlayer initWithName:]
If I start game # 2, I see the following while playing. In addition to the game from game # 1, there are two new distributions.
- Category = Malloc 48 bytes; Size = 48; Responsible Caller = - [ActivePlayer initWithName:]
- Category = ActivePlayer; Size = 16; Responsible Caller = - [ActiveGame createPlayer:]
- Category = Malloc 144 bytes; Size = 144; Responsible Caller = - [ActivePlayer initWithName:]
And after finishing game # 2, I see the following. Again, the ActivePlayer has been freed, but the "Malloc X Bytes" allocations still exist.
- Category = Malloc 48 bytes; Size = 48; Responsible Caller = - [ActivePlayer initWithName:]
- Category = Malloc 144 bytes; Size = 144; Responsible Caller = - [ActivePlayer initWithName:]
After that I get unusual results - if I play games # 3, # 4 and # 5, I never see the in-game lines for Category = "Malloc X Bytes", only for the Category = ActivePlayer line, which is released after the game ends. The first two lines of "Malloc", as shown above, continue to persist. I also saw another weird behavior - when testing this yesterday using the iPhone 6.0 simulator, only games # 2 and # 3 remained alive, but not games # 1, # 4 and # 5. So, as long as the memory remains allocated, the times at which it occurs appear to be different on my device and in different versions of the simulator.
And my questions are:
- As far as I understood, after finishing the game, I shouldn't see live memory from the initWithPlayer call and the ActivePlayer object was freed?
- If so, what is causing it and how to free it?
- Or don't I need to worry about it at all?
Notes:
- These screenshots are taken from running my app on an iPhone 4 running iOS 6.1. But I see similar behavior works with iPhone Simulator for 5.1, 6.0 and 6.1 and I saw it on my iPhone running iOS 6.0 before I updated it.
- In ActivePlayer.m, the dealloc method is currently commented out, although I tested it while it was uncommented and verified that it was called (by the system, I don't call the dealloc call anywhere). Either way, the behavior is the same.
- For what's worth, nothing is communicated with the Leaks profiling tool.
- While this is one example that results in 192 bytes of live memory which I believe should be freed up, I see this with many of my classes, i.e. it seems that the memory allocation grows over time, which I think is is the problem.
Your code is ok. It looks like you are still maintaining a link to the original ActivePlayer, somewhere else in your code.
As a side note, your template for creating an ActivePlayer is not the norm - usually the class does not call alloc from the init method. Instead, the caller should do:
[[ActivePlayer alloc] initWithName:@"Bob"];
and your init method should work with the return value
[super init];
I find it very strange that your constructor is static (+ sign). The naming convention specifies that methods with names prefixed with as init
return managed memory objects. I suspect that the results of the inner method are evaluated by their method names.
I think checking the instance count has determined that your code is not leaking ActivePlayers. As an aside, the best form for constructor and inits looks like this:
// .h
@interface ActivePlayer : NSObject
+ (id)activePlayerWithName:(NSString *)name;
@end
// .m
+ (id)activePlayerWithName:(NSString *)name {
return [[self alloc] initWithName:name];
}
// if you want to make this public (include in .h interface) you can
// the callers will have the choice of alloc init pattern, or the factory
//
- (id)initWithName:(NSString *)name {
self = [self init];
if (self) {
_name = name;
}
return self;
}
Then the caller does the following:
ActivePlayer *activePlayer = [ActivePlayer activePlayerWithName:@"Charlie"];