2

So I'm relative new to objC programming. But not to C. In a more complicated app I think I have a memory leaks. I've programmed this just for make some tests. The app is very simple: it store in a MutableArray a series of integer that rappresent timers scheduled. The app has one NSTimer in the current runloop that check every second if it is the right time to ring comparing a counter with the right element of the MutableArray. Everything works, but memory in debug panel grow up, grow up, grow up… I've try some variants but something still missing for me about ARC. I simply don't understand, since ARC is NOT a garbage collector, why memory grow and what I do wrong. Here is the code:

-(id)initWithLabel:(UILabel *)label {
    self = [super init];
    self.list = [[mtAllarmList alloc]init];
    self.label = label;
    return self;
}

My class init function. I pass a label reference (weak beacause it is own by viewcontroller) to my class. I also allocate and init the class mtAllarmList that contain the MutableArray and other information (in the original app, file to play, volumes, eccetera).

-(void)ClockRun { 
    NSMethodSignature * signature = [mtClockController instanceMethodSignatureForSelector:@selector(check)];
    NSInvocation * selector = [NSInvocation invocationWithMethodSignature: signature];
    [selector setTarget:self];
    [selector setSelector:@selector(check)];

    [[NSRunLoop currentRunLoop] addTimer: self.time = [NSTimer scheduledTimerWithTimeInterval:1
                                                                                   invocation:selector
                                                                                      repeats:YES]
                                 forMode:NSDefaultRunLoopMode];

    [[NSRunLoop currentRunLoop] runUntilDate:[[NSDate alloc]initWithTimeIntervalSinceNow: 30]];
}

ClockRun: is the method the app call to start everything. It simply start the timer that fires every second to check:

-(void)check {
    self.counter++;
    int i = [self.list check:self.counter];
    if(i == 1) {
        [self writeAllarmToLabel:self.label isPlayingAllarmNumber:self.counter];
    }
    else if (i == 2) {
        [self writeAllarmToLabel:self.label theString: @"Stop"];
        [self.time invalidate];
        self.counter = 0;
    }
    else {
        [self writeAllarmToLabel:self.label theString:[[NSString alloc]initWithFormat:@"controllo, %d", self.counter]];
    }
    NSLog(@"controllo %d", self.counter);
}

Check: simply reacts to the return value of [list check: int] methods of mtAllarmList. It returns 1 if timer must ring, 0 if not, and 2 if the sequence ends. In that case self.counter will be set to 0 and the NSTimer will be invalidate.

-(id)init {
    self = [super init];
    self.arrayOfAllarms = [[NSMutableArray alloc]initWithCapacity:0];
    int i;
    for(i=1;i<=30;++i) {
        [self.arrayOfAllarms addObject: [[NSNumber alloc]initWithInt:i*1]];
    }

    for(NSNumber * elemento in self.arrayOfAllarms)
        NSLog(@"ho creato un array con elemento %d", [elemento intValue]);
    return self;
}

In mtAllarmList init method simulates the costruction an array (I've try a variety of patterns) and log all the elements.

-(int)check:(int)second {
    int maxValue = [[self.arrayOfAllarms lastObject] intValue];
    if(maxValue == second){
        self.index = 0;
        return 2;
    } else {
        if ([[self.arrayOfAllarms objectAtIndex:self.index] intValue] == second) {
            self.index++;
            return 1;
        } else {
            return 0;
        }
    }
}

Check methods instead is very elementary and I don't think needs explanations.

So, why this simple very stupid app leaks?

Korsmakolnikov
  • 709
  • 7
  • 15
  • Have you tried the Allocation and Leak instruments? If not in Xcode select "Profile" and run them (read the Xcode docs as needed).What do they show you? – CRD Apr 21 '14 at 18:29
  • @crd Allocation and Leak instruments reports no evident leaks and a constant grown of the heap. Typical of a retain release I think. – Korsmakolnikov Apr 22 '14 at 10:40
  • @Rob all is in the main thread. All seems to work properly. Invalidate is called, I've checked. Actually I don't know which object are not getting release. This is my first match with ARC. All was saying me "ARC is future", "you don't need to worry about abject release" :) – Korsmakolnikov Apr 22 '14 at 10:50
  • @Korsmakolnikov - So Instruments shows a growing heap. Look at what is being recorded, try to narrow down (a) what specifically is growing and (b) what is allocating it. There is not a lot here others can go on to try to help you apart from giving suggestions. Certainly the `runUntilDate` is probably wrong, you should not need this for the task you are doing and its a potential problem. Unfortunately autorelease problems are a legacy of MRC that ARC users have to live with. – CRD Apr 22 '14 at 16:56

2 Answers2

2

Since you're doing this on the main run loop, you can (and should) simplify the ClockRun method:

- (void)ClockRun {
    self.time = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(check) userInfo:nil repeats:YES];
}

That NSInvocation code was unnecessary and the NSRunLoop code could only introduce problems.

Having said that, this is unlikely to be the source of your memory consumption. And nothing else in the provided code snippets looks like an obvious memory problem. If you're 100% confident that the timer is getting invalidated, then the timer is not the problem. I wonder about the object graph between the view controller at this mtClockController. Or perhaps some circular reference in view controllers (e.g. pushing from A to B and to A again). It's hard to say on the basis of what's been provided thus far.

Sadly, there's not much else we can suggest other than the routine diagnostics. First, I'd run the the app through the static analyzer (by pressing shift+command+B in Xcode, or choosing "Profile" from the Xcode "Product" menu).

Second, you should run your app through Leaks and Allocations tools to identify the what precisely is leaking on each iteration. Do you have extra instances of the view controllers? Or just the mtClockController?

Until you identify what's not being deallocated, it's hard to remedy it. And Instruments is the best tool for identifying what's not getting released. In WWDC 2012 video iOS App Performance: Memory the demonstration sections of the video give pragmatic demonstrations of using Instruments (as well as a wealth of good background info on memory management).

Third, when I've got a situation where I'm not sure if things are getting deallocated when they should, I sometimes include dealloc methods that tell me when the object is deallocated, e.g.:

- (void)dealloc {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

I'd suggest this not only for your key model objects, but your view controller, too. (Sometimes we agonize over our model objects only to realize that it's the view controller, itself, which is be retained by something else.)

Clearly Instruments is a much richer tool, but this can be used to quickly identify failure to deallocate (and show you what's maintaining the strong references).


I ran you app through Instruments, watching your custom objects, and everything is being deallocated properly. Below, I marked generation A, hit the button, let the timer expire, marked generation B, hit the button again, etc. I did that four times, and I then simulated a memory warning, and did one final generation. Everything looks fine (this is a compilation of six screen snapshots in one, showing the total allocations at each of the six generations):

generations

I inspected your Generations, as well as the Allocations themselves, and none of your objects are in there. Everything is getting released fine. The only things there are internal Cocoa objects associated with UIKit and NSString. Cocoa Touch does all sorts of caching of stuff behind the scenes that we have no control over. The reason I did that final "simulator memory warning" was to give Cocoa a chance to purge what it can (and you'll see that despite what Generations reports, the total allocations fell back down a bit).

Bottom line, your code is fine, and there is nothing to worry about here. In the future, don't worry about incidentally stuff showing up in the generations, but rather focus on (a) your classes; and (b) anything sizable. But neither of those apply here.

In fact, if you restrict Instruments to only record information for your classes with the mt prefix (you do this by stopping a recording of Instruments and tap on the "i" button on the Allocations graph and configure the "Recorded Types"), you'll see the sort of graph/generations that you were expecting:

mt classes only

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • So, I have modified methods as you suggest and I have implemented dealloc: methods on either mtClockController and mtAllarmList, and even on ViewController. But because is a single view app I think viewcontroller never dealloc. Both mtClockController and mtAllarmList dealloc methods is called after all the last check:. So I've ran app all night and Instruments show a constant grown of the heap. Profiling Leaks does not show me evident leaks. Allocation instead show me that my object is deallocated after every iteration. So I really don't understand what is growing up here Rob. – Korsmakolnikov Apr 23 '14 at 11:28
  • @Korsmakolnikov Unfortunately, trying to illustrate how to use Allocations tool is harder to do in S.O. answer than in video, but I took a crack at it in a few unrelated answers I posted to other questions such as http://stackoverflow.com/a/14105056/1271826, http://stackoverflow.com/a/18022482/1271826 and http://stackoverflow.com/a/18663448/1271826 – Rob Apr 23 '14 at 14:12
  • what I have when limit the graph around a mark is a list of allocations, and if I see through retain/release history all this allocations ends with a "free" instruction and a reference count of 0. But in generation marks something grows. I wish I could send you instruments file. I've seen the video, and read advanced memory management documentation. But I think I doing something wrong. Thank you anyway – Korsmakolnikov Apr 23 '14 at 15:21
  • This is only a simulation of the problematic part of a bigger app. There aren't any problems to share code or trace file. I wrote some code just to study the leak and search a solution. I'm very glad of your help. Thank you. https://dl.dropboxusercontent.com/u/8387793/mock.01.medtimer.trace.zip https://dl.dropboxusercontent.com/u/8387793/mock.01.medtimer.zip – Korsmakolnikov Apr 23 '14 at 19:43
  • Thank you very much. I've contact you on your pages so you have may mail now. – Korsmakolnikov Apr 23 '14 at 21:55
  • @Korsmakolnikov I looked at your app in instruments and everything is perfectly fine. You're only seeing incidental stuff that Cocoa caches in the background. Nothing to be worried about here. See revised answer. – Rob Apr 24 '14 at 01:25
  • So I was involved myself in a witch hunt… This byte are under the footprint? Thank you very much Rob. I was really need an help to figure out. I was thinking that ARC mechanism was much more efficient than Java garbage collector, so I was expecting a 0 byte after each iteration. And indeed on some iteration I have that 0, but not always, so… It was confusing. This is my first attempt to an app with NSTimer involved and I wasn't sure of anything. Thanks a lot Robert. – Korsmakolnikov Apr 24 '14 at 17:20
  • @Korsmakolnikov Yeah, it makes sense that you'd expect 0 bytes after each iteration. But I wouldn't characterize these results as any "lack of efficiency" on the part of ARC, but rather simply that Instruments is tracking all memory usage, not just your code's. Apple does a ton of caching other sophisticated performance optimizations within Cocoa, to which Instruments is simply exposing you. If you want, you can configure Instruments to only track classes that start with your `mt` prefix, in which case you'll see it coming back to zero between generations. – Rob Apr 24 '14 at 17:52
0

A couple of observations:

  1. Instead of using the invocation form of scheduledTimerWithInterval, try using the selector form directly, in this case it's a lot simpler and clearer to read.

  2. Since you're call runUntilDate directly, I don't think you're getting any autorelease pools created/drained, which would lead to memory leakage, specifically in the check function. Either don't call runUntilDate and allow the normal run loop processing to handle things (the normal preferred mechanism) or wrap check in an @autoreleasepool block.

David Berry
  • 40,941
  • 12
  • 84
  • 95