Sunday, February 01, 2009

Cocoa Garbage Collection and GData

Several days ago I lost almost all night debugging a nasty bug with GData client for Objective-C. The bug was so stupid that I was about to hit my head against a wall when learned what's wrong.

But let's start from the beginning. Generally, my goal was to fetch Google Calendar calendars, find the specified one and add an entry. It could(!) be done with Google's API the following rather simple way:

- (GDataServiceGoogleCalendar *)calendarService {
static GDataServiceGoogleCalendar* service = nil;
if (!service) {
service = [[GDataServiceGoogleCalendar alloc] init];
[service setUserAgent:@"My-App-1.0"];
[service setShouldCacheDatedData:YES];
[service setServiceShouldFollowNextLinks:YES];
}
[service setUserCredentialsWithUsername:username
password:password];
return service;
}

- (void)fetchCalendars {

GDataServiceGoogleCalendar* service = [self calendarService];
[service fetchCalendarFeedWithURL:[NSURL URLWithString: kGDataGoogleCalendarDefaultOwnCalendarsFeed]
delegate:self
didFinishSelector:@selector(calendarListFetchTicket:finishedWithFeed:)
didFailSelector:@selector(calendarListFetchTicket:failedWithError:)];
}

- (void)calendarListFetchTicket:(GDataServiceTicket *)ticket
finishedWithFeed:(GDataFeedCalendar *)feed {
// Wow, it works!
}

- (void)calendarListFetchTicket:(GDataServiceTicket *)ticket
failedWithError:(NSError *)error {
// Oops...
}

Simply saying you get GDataServiceGoogleCalendar object and ask it to fetch calendars passing it an object and a pair of selectors. The service asynchronously fetches calendars in background and calls an appropriate selector when done. The code above is almost the same code as in the Google's example application.

But... it rejected to work in my project. For some unknown to me reason callbacks was never called. When I tried to add breakpoints inside the framework, even NSURLConnection callbacks was not called.

After some tries to figure out a difference between my app and the exemplary, I found that my application uses Garbage Collection and the exemplary not. I immediately supposed that GData framework doesn't support GC. However after I enabled GC for the example it continued to work perfectly.

I had read some blogs and forums but still hadn't found anything interesting. And now I'll unveil an important detail missed in the code above. In fact, fetchCalendarFeedWithURL: returns a ticket, GDataServiceTicket object. Ticket allows you to check in callbacks which fetch request was finished. Since at that moment I was experimenting and thinking that there will be only one ticket, I wasn't doing anything with fetchCalendarFeedWithURL's return value.

"OK", — I said and wrote it the following way:

GDataServiceTicket * ticket = [service fetchCalendarFeedWithURL:[NSURL URLWithString: kGDataGoogleCalendarDefaultOwnCalendarsFeed]
delegate:self
didFinishSelector:@selector(calendarListFetchTicket:finishedWithFeed:)
didFailSelector:@selector(calendarListFetchTicket:failedWithError:)];
[ticket retain];

Apparently I decided that ticket was being disposed by garbage collector and Google's framework was dying because of that. Actually, I was absolutely right. But I wrote a completely stupid fix. Because if you read documentation on Cocoa's GC you know that I had changed nothing. Because retain/release call don't do anything if garbage collecting is enabled. I don't know why (maybe because it was 3 am) I supposed that it'll help.
So, finally, the correct solution is to do fTicket = [ticket retain] (if you want to support classical memory management) or fTicket = ticket (if you target on Leopard only), where fTicket is an instance variable, for example. This way garbage collector won't release ticket object.

Finally, I should note that exemplary app was doing everything right. But for some reason I hadn't noted that. So if you're using Cocoa with GC don't lazy to read Apple's guide on it.

2 comments:

  1. I cannot reproduce this on Mac OS X 10.5.6. In my tests, the callbacks are called even if no reference is kept to the ticket by the application. (NSURLConnection should keep a reference to the fetcher object, and that keeps a reference to the GData ticket.) Please follow up on the library's discussion group.

    ReplyDelete
  2. Hi, Greg.
    I've tested everything again on 10.5.5 (which I had on my development machine) and 10.5.6. And everything works fine on 10.5.6. But stably fails the way I described above on 10.5.5.
    So there must be some difference in 10.5.5's and 10.5.6's garbage collectors (and thus 10.5.5 is working wrong). Or I'm missing something important.
    P.S.: I use GData Client v.1.5.0 (by adding Common and Calendar folders to my project).

    ReplyDelete