A Critical Weakness

Closed

Last week, I touched on a tricky implementation of an eventListener. One object could register to receive events by another by implementing the appropriate protocol and then adding itself to an eventListeners array.

The code assumes you are utilizing ARC, the automatic reference counting system implemented in the newer objective-c releases. This frees us from the majority of memory management, and banishes the days of retain/release/autorelease. Unfortunately, it also introduces a tricky memory problem here.

CyclicReferences

Because our view controller stores a reference to the CharacterData object, the CharacterData object won’t be freed as long as the view controller exists. But since the view controller has been added to the eventListener array on the CharacterData object, the reverse is also true – the view controller won’t be freed as long as the CharacterData object exists! This is called a circular reference, and prevents either of those objects from every being freed! Obviously if we have a lot of these objects and listeners, this can add up to a real problem.

We can create a simple situation that demonstrates this:

// Dancer.h
  1. // A Dancer
  2. @interface Dancer : NSObject
  3.  
  4. // Strong reference to our dance partner.
  5. @property Dancer* dancePartner;
  6.  
  7. // We log on dealloc.
  8. (void) dealloc;
  9.  
  10. // A static method for creating a dance pair.
  11. + (void) createDancePair;
  12.  
  13. @end
// Dancer.m
  1. @implementation Dancer
  2.  
  3. // We log on dealloc.
  4. (void) dealloc
  5. {
  6.     NSLog(@"Dancer %p was deallocated", self);
  7. }
  8.  
  9. + (void) createDancePair
  10. {
  11.     Dancer* man = [[Dancer alloc] init];
  12.     Dancer* woman = [[Dancer alloc] init];
  13.     man.dancePartner = woman;
  14.     woman.dancePartner = man;
  15.  
  16.     // Leaving scope should dealloc both dancers, but…
  17. }
  18.  
  19. @end

The standard fix to this is to make a weak reference. A weak reference is a reference that is valid as long as the target object exists, but doesn’t count towards keeping the target object alive. That is to say, as long as something else refers to the object, it will exist, but as soon as there are no other references that aren’t weak (aka strong references), the object will be freed.

There are two syntaxes, depending on where you declare it:

  1. @interface WeakReferenceExample : NSObject
  2. {
  3.     __weak NSObject* weakVariable;
  4. }
  5.  
  6. @property (weak) NSObject* weakProperty;
  7. @end

And as an added boon, the weak references will be set to nil when the target object is freed. So we can fix our very contrived example with a simple ‘weak’ keyword like so:

  1. // A Dancer
  2. @interface Dancer : NSObject
  3.  
  4. // Strong reference to our dance partner.
  5. @property (weak) Dancer* dancePartner;
  6.  
  7. // We log on dealloc.
  8. (void) dealloc;
  9.  
  10. // A static method for creating a dance pair.
  11. + (void) createDancePair;
  12.  
  13. @end

Now, when the dancers go out of scope at the end of the createDancePair method, the dealloc method is actually invoked on both, proving that they’ve been correctly destroyed.

Unfortunately, it isn’t immediately obvious how to apply this to our original problem. The issue is with the NSMutableArray, which always takes a strong reference. Making our reference to the array weak doesn’t work, because the array will simply be immediately freed. The solution here requires a wrapper class.

By creating a class whose sole purpose is to store a weak reference to another object, we can circumvent the NSArray problem.

// WeakWrapper.h
  1. @interface WeakWrapper : NSObject
  2.  
  3. // The weak reference to the actual object
  4. @property (weak,atomic,readonly) id object;
  5.  
  6. // Basic initializer with the actual object
  7. (id) initWithObject:(id)object;
  8.  
  9. @end
// WeakWrapper.m
  1. @implementation WeakWrapper
  2.  
  3. // The weak reference to the actual object
  4. @synthesize object;
  5.  
  6. // Basic initializer with the actual object
  7. (id) initWithObject:(id)pobject
  8. {
  9.     if((self = [super init]))
  10.     {
  11.         // Store the weak reference.  This will become nil automatically when this object is disposed of.
  12.         object = pobject;
  13.     }
  14.     return self;
  15. }
  16.  
  17. @end

This does mean we have to be aware of the fact that we’re using this wrapper class. It doesn’t really make sense to expose the raw array and hope that no one does it incorrectly, so we will hide the internals and provide accessor functions.

// CharacterData.m
  1. // NEW: Add a new listener into the list
  2. (void) addEventListener:(NSObject*)listener
  3. {
  4.     // Wrap the incoming object in a weak reference wrapper
  5.     WeakWrapper* wrapper = [[WeakWrapper alloc] initWithObject:listener];
  6.     [eventListeners addObject:wrapper];
  7. }
  8.  
  9. // NEW: Remove the specified listener from the list
  10. (void) removeEventListener:(NSObject*)listener
  11. {
  12.     NSArray* localEventListeners = [eventListeners copy];
  13.     // emit the event to any interested listeners
  14.     for(WeakWrapper* wrapper in localEventListeners)
  15.     {
  16.         // Try to get out the real object into a temporary strong reference
  17.         NSObject* delegate = wrapper.object;
  18.  
  19.         // Remove this if it's nil or our object
  20.         if(delegate == nil || delegate == listener)
  21.         {
  22.             [eventListeners removeObject:wrapper];
  23.         }
  24.     }
  25. }
  26.  
  27. (void) emitPropertyChangedEvent:(NSString*)propertyName
  28. {
  29.     NSArray* localEventListeners = [eventListeners copy];
  30.     // emit the event to any interested listeners
  31.     for(WeakWrapper* wrapper in localEventListeners)
  32.     {
  33.         // Try to get out the real object into a temporary strong reference
  34.         NSObject* delegate = wrapper.object;
  35.  
  36.         // If this is nil, then the object has been deallocated so we can remove the wrapper
  37.         if(delegate == nil)
  38.         {
  39.             [eventListeners removeObject:wrapper];
  40.         }
  41.         // Otherwise send it the event if it responds
  42.         else if([delegate respondsToSelector:@selector(characterData:propertyChanged:)])
  43.         {
  44.             [delegate characterData:self propertyChanged:propertyName];
  45.         }
  46.     }
  47. }

We clean up any dead references whenever we iterate through the event listeners.

Now, objects can listen for events from other objects, but not be kept alive by them. We can implement our original design with impunity.

Example XCode iOS project: WeakReferenceProject

This entry is filed under NicholasMTElliott.com, Programming, Skyward App Company. You can follow any responses to this entry through RSS 2.0. Both comments and pings are currently closed.

Comments are currently closed.