How can I keep objects from being released when using Objective-C ARC with Xcode 4.2?

11,718

So, I've figured out how to keep this from happening, although I'm still confused on why it makes a difference. In each place where the extra release was happening, it was in a loop. In those places, I took the property declaration out of the for loop, assigned it to a local var that is used in the for loop, and it works fine, now! So, a line that used to be:

for (NSString *day in schedule1.daysOfWeek)

I've changed to 2 lines:

NSArray *daysOfWeek = schedule1.daysOfWeek;
for (NSString *day in daysOfWeek)

Obviously that's going to make a difference in the retain/release calls that will be needed, but I don't see why it ultimately makes a difference in the final retain count... If anyone can shed some insight on why this helps, I'd love to hear it!

Share:
11,718
Elezar
Author by

Elezar

Updated on June 09, 2022

Comments

  • Elezar
    Elezar almost 2 years

    ETA: See the bottom for some more info I got by Profiling the app.

    I have an iPhone app that I just converted to use ARC, and now I'm getting several errors because of zombie objects. Before I switched, I was manually retaining them, and everything was fine. I can't figure out why ARC isn't retaining them. The objects are declared as strong properties, and referenced using dot notation. This is happening in several places, so I think I must have a fundamental misunderstanding of ARC/memory management somewhere.

    Here's an example that's particularly frustrating. I have an NSMutableArray of 3 objects. Each of those objects has a property that's also an NSMutableArray, which in this case always has a single object. Finally, that object has the property that gets released. The reason it's frustrating is that it only happens with the 3rd object from the original array. The first 2 objects are always completely fine. It just doesn't make sense to me how the property of one object would be released when the same property of similar objects created and used in the same way aren't.

    The array is stored as a property on a UITableViewController:

    @interface GenSchedController : UITableViewController <SectionHeaderViewDelegate>
    
    @property (nonatomic, strong) NSArray *classes;
    
    @end
    
    @implementation GenSchedController
    
    @synthesize classes;
    

    The objects that are stored in the classes array are defined as:

    @interface SchoolClass : NSObject <NSCopying, NSCoding>
    
    @property (nonatomic, strong) NSMutableArray *schedules;
    
    @end
    
    @implementation SchoolClass
    
    @synthesize schedules;
    

    The objects that are stored in the schedules array are defined as:

    @interface Schedule : NSObject <NSCopying, NSCoding>
    
    @property (nonatomic, strong) NSMutableArray *daysOfWeek;
    
    @implementation Schedule
    
    @synthesize daysOfWeek;
    

    daysOfWeek is what is getting released. It just contains several NSStrings.

    I can see that during viewDidLoad that all the objects are fine, with no zombies. However, when I tap one of the table cells, and set a breakpoint on the first line of tableView:didSelectRowAtIndexPath:, it has already been released. The specific line that throws the error is the @synthesize daysOfWeek; that is called after the 3rd "for" loop below:

    for (SchoolClass *currentClass in self.classes) {
        for (Schedule *currentSched in currentClass.schedules) {
            for (NSString *day in currentSched.daysOfWeek)
    

    But, again, this only happens on the last Schedule of the last SchoolClass.

    Can anyone point me in the right direction in getting my app to work correctly with ARC?

    As requested, here's more info. First, the stack trace when the exception is thrown:

    #0  0x01356657 in ___forwarding___ ()
    #1  0x01356522 in __forwarding_prep_0___ ()
    #2  0x00002613 in __arclite_objc_retainAutoreleaseReturnValue (obj=0x4e28b80) at /SourceCache/arclite_host/arclite-4/source/arclite.m:231
    #3  0x0000d2fc in -[Schedule daysOfWeek] (self=0x4e28680, _cmd=0x220d6) at /Users/Jesse/Documents/Xcode/Class Test/Schedule.m:18
    #4  0x0001c161 in -[SchedulesViewController doesScheduleOverlap:schedule2:withBufferMinutes:] (self=0x692b210, _cmd=0x22d58, schedule1=0x4e28680, schedule2=0x4e27f10, buffer=15) at /Users/Jesse/Documents/Xcode/Class Test/Classes/SchedulesViewController.m:27
    #5  0x0001c776 in -[SchedulesViewController doesScheduleOverlap:schedule2:] (self=0x692b210, _cmd=0x22d9b, schedule1=0x4e28680, schedule2=0x4e27f10) at /Users/Jesse/Documents/Xcode/Class Test/Classes/SchedulesViewController.m:53
    #6  0x0001cf8c in -[SchedulesViewController getAllowedSchedules] (self=0x692b210, _cmd=0x22dca) at /Users/Jesse/Documents/Xcode/Class Test/Classes/SchedulesViewController.m:78
    #7  0x0001d764 in -[SchedulesViewController viewDidLoad] (self=0x692b210, _cmd=0x97cfd0) at /Users/Jesse/Documents/Xcode/Class Test/Classes/SchedulesViewController.m:121
    #8  0x00620089 in -[UIViewController view] ()
    #9  0x0061e482 in -[UIViewController contentScrollView] ()
    #10 0x0062ef25 in -[UINavigationController _computeAndApplyScrollContentInsetDeltaForViewController:] ()
    #11 0x0062d555 in -[UINavigationController _layoutViewController:] ()
    #12 0x0062e7aa in -[UINavigationController _startTransition:fromViewController:toViewController:] ()
    #13 0x0062932a in -[UINavigationController _startDeferredTransitionIfNeeded] ()
    #14 0x00630562 in -[UINavigationController pushViewController:transition:forceImmediate:] ()
    #15 0x006291c4 in -[UINavigationController pushViewController:animated:] ()
    #16 0x000115d5 in -[GenSchedController tableView:didSelectRowAtIndexPath:] (self=0x4c57b00, _cmd=0x9ac1b0, tableView=0x511c800, indexPath=0x4e2cb40) at /Users/Jesse/Documents/Xcode/Class Test/Classes/GenSchedController.m:234
    #17 0x005e7b68 in -[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] ()
    #18 0x005ddb05 in -[UITableView _userSelectRowAtPendingSelectionIndexPath:] ()
    #19 0x002ef79e in __NSFireDelayedPerform ()
    #20 0x013c68c3 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ ()
    #21 0x013c7e74 in __CFRunLoopDoTimer ()
    #22 0x013242c9 in __CFRunLoopRun ()
    #23 0x01323840 in CFRunLoopRunSpecific ()
    #24 0x01323761 in CFRunLoopRunInMode ()
    #25 0x01aa71c4 in GSEventRunModal ()
    #26 0x01aa7289 in GSEventRun ()
    #27 0x0057ec93 in UIApplicationMain ()
    #28 0x0000278d in main (argc=1, argv=0xbffff5fc) at /Users/Jesse/Documents/Xcode/Class Test/main.m:16
    

    And the exact exception is Class Test[82054:b903] *** -[__NSArrayM respondsToSelector:]: message sent to deallocated instance 0x4e28b80

    And here's the code where everything gets created, loading from disk:

    NSString *documentsDirectory = [FileManager getPrivateDocsDir];
    
    NSError *error;
    NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:documentsDirectory error:&error];
    
    // Create SchoolClass for each file
    NSMutableArray *classesTemp = [NSMutableArray arrayWithCapacity:files.count];
    for (NSString *file in files) {
        if ([file.pathExtension compare:@"sched" options:NSCaseInsensitiveSearch] == NSOrderedSame) {
            NSString *fullPath = [documentsDirectory stringByAppendingPathComponent:file];
    
            NSData *codedData = [[NSData alloc] initWithContentsOfFile:fullPath];
            if (codedData == nil) break;
    
            NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:codedData];
            SchoolClass *class = [unarchiver decodeObjectForKey:@"class"];    
            [unarchiver finishDecoding];
    
            class.filePath = fullPath;
    
            [classesTemp addObject:class];
        }
    }
    
    self.classes = classesTemp;
    

    The initWithCoder: methods are really straightforward. First for SchoolClass:

    - (id)initWithCoder:(NSCoder *)decoder {
        self.name = [decoder decodeObjectForKey:@"name"];
        self.description = [decoder decodeObjectForKey:@"description"];
        self.schedules = [decoder decodeObjectForKey:@"schedules"];
    
        return self;
    }
    

    And for Schedule:

    - (id)initWithCoder:(NSCoder *)decoder {
        self.classID = [decoder decodeObjectForKey:@"id"];
        self.startTime = [decoder decodeObjectForKey:@"startTime"];
        self.endTime = [decoder decodeObjectForKey:@"endTime"];
        self.daysOfWeek = [decoder decodeObjectForKey:@"daysOfWeek"];
    
        return self;
    }
    

    I tried running a Profile on the app using the Zombies template, and compared the object that over-releases to one of the others in the array that is fine. I can see that on the line for (NSString *day in currentSched.daysOfWeek), that it goes into the daysOfWeek getter, which does a retain autorelease. Then after it returns from the getter, it does another retain (Presumably to hold ownership while the loop is processed), and then a release. All of that is the same for the problem object as for the healthy object. The difference is that immediately after that release, the problem object calls release AGAIN. This actually doesn't cause a problem immediately, because the autorelease pool hasn't drained yet, but once it does, the retain count drops to 0, and then of course the next time I try to access it, it's a zombie.

    What I can't figure out is WHY that extra release is getting called there. Because of the outer for loops, the number of times that currentSched.daysOfWeek gets called does vary - it gets called 3 times on the problem object and 5 on the healthy object, but the extra release occurs the first time it's called, so I'm not sure how that would affect it.

    Does this extra info help anyone understand what's happening?

  • Elezar
    Elezar over 12 years
    Yes, as I said, the properties are all defined as strong, and I'm always using dot notation to reference them. I've tried to follow the ownership, but as far as I can tell, they should still be owned at the time they're released. Maybe I'm missing something in the ownership chain, but if so, then I guess my question is "how do I identify what I'm missing in the ownership chain?" Can you clarify what you mean by "draw object graphs"?
  • Becca Royal-Gordon
    Becca Royal-Gordon over 11 years
    This could happen if the object returned by schedule1.daysOfWeek was released during the loop—for example, if you set daysOfWeek inside the loop. The second version assigns daysOfWeek to a strong variable, so it's kept alive until the loop ends; the first version doesn't, so the array's lifetime is at the whim of schedule1.