42

How can I get an array of zeroing weak references under ARC? I don't want the array to retain the objects. And I'd like the array elements either to remove themselves when they're deallocated, or set those entries to nil.

Similarly, how can I do that with a dictionary? I don't want the dictionary to retain the values. And again, I'd like the dictionary elements either to remove themselves when the values are deallocated, or set the values to nil. (I need to retain the keys, which are the unique identifiers, at least until the corresponding values are deallocated.)

These two questions cover similar ground:

But neither is asking for zeroing references.

Per the documentation, neither NSPointerArray nor NSHashMap support weak references under ARC. NSValue's nonretainedObjectValue will not work either, as it is non-zeroing.

The only solution I see is to create my own NSValue-like wrapper class with a (weak) property, as this answer mentions, near the end. Is there a better way I'm not seeing?

I'm developing for OS X 10.7 and iOS 6.0.

Community
  • 1
  • 1
paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
  • I don't think you need a _value_ wrapper class; you need your own collection class if you want things to be removed when they're deallocated. – jscs Jan 08 '13 at 06:30
  • That's true and a good point about removing them, though not true for setting them to nil. – paulmelnikow Jan 08 '13 at 06:45
  • I'd suggest to not-fight-the-framework and use NSPointerArray with the NSPointerFunctionsWeakMemory NSPointerFunctionOption. – leviathan Jun 06 '13 at 08:17
  • Per the documentation, that works under 10.8 but not 10.7. – paulmelnikow Jun 06 '13 at 15:03
  • why not using NSPointerFunctionsWeakMemory argument for [NSPointerArray initWithOptions:]? According to this doc: https://developer.apple.com/library/ios/documentation/cocoa/reference/foundation/classes/NSPointerFunctions_Class/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005145-CH1-SW18 it's supported from iOS 6 – Denis Jun 03 '14 at 09:37
  • See just above. It works on iOS 6 as you say. It also works on OS X 10.8, but did not work on 10.7. – paulmelnikow Jun 03 '14 at 14:51

8 Answers8

24

Zeroing weak references require OS X 10.7 or iOS 5.

You can only define weak variables in code, ivars or blocks. AFAIK there is no way to dynamically (at runtime) to create a weak variable because ARC takes effect during compile time. When you run the code it already has the retains and releases added for you.

Having said that you can probably abuse blocks to achieve an effect like this.

Have a block that simply returns the reference.

__weak id weakref = strongref;
[weakrefArray addObject:[^{ return weakref; } copy]];

Note that you need to copy the block to get it copied to the heap.

Now you can walk the array anytime you like, dealloc'ed objects in blocks will return nil. You can then remove those.

You cannot have code automatically be executed when the weak ref is zeroed. If this is what you want then you can make use of the function of associated objects. Those get deallocated at the same time as the object they are associated to. So you could have your own sentry tag which informs the weak collection about the objects demise.

You would have one associated object to watch for the dealloc (if the association is the only reference) and the associated object would have a pointer to the collection watching. Then in the sentry dealloc you call the weak collection to inform it that the watched object has gone.

Here's my writeup on associated objects: http://www.cocoanetics.com/2012/06/associated-objects/

Here's my implementation:

---- DTWeakCollection.h

@interface DTWeakCollection : NSObject

- (void)checkInObject:(id)object;

- (NSSet *)allObjects;

@end

---- DTWeakCollection.m

#import "DTWeakCollection.h"
#import "DTWeakCollectionSentry.h"
#import <objc/runtime.h>

static char DTWeakCollectionSentryKey;

@implementation DTWeakCollection
{
    NSMutableSet *_entries;
}

- (id)init
{
    self = [super init];
    if (self)
    {
        _entries = [NSMutableSet set];
    }
    return self;
}

- (void)checkInObject:(id)object
{
    NSUInteger hash = (NSUInteger)object;

    // make weak reference
    NSNumber *value = [NSNumber numberWithUnsignedInteger:hash];
    [_entries addObject:value];

    // make sentry
    DTWeakCollectionSentry *sentry = [[DTWeakCollectionSentry alloc] initWithWeakCollection:self forObjectWithHash:hash];
    objc_setAssociatedObject(object, &DTWeakCollectionSentryKey, sentry, OBJC_ASSOCIATION_RETAIN);
}

- (void)checkOutObjectWithHash:(NSUInteger)hash
{
    NSNumber *value = [NSNumber numberWithUnsignedInteger:hash];
    [_entries removeObject:value];
}

- (NSSet *)allObjects
{
    NSMutableSet *tmpSet = [NSMutableSet set];

    for (NSNumber *oneHash in _entries)
    {
        // hash is actually a pointer to the object
        id object = (__bridge id)(void *)[oneHash unsignedIntegerValue];
        [tmpSet addObject:object];
    }

    return [tmpSet copy];
}

@end

---- DTWeakCollectionSentry.h

#import <Foundation/Foundation.h>
@class DTWeakCollection;

@interface DTWeakCollectionSentry : NSObject

- (id)initWithWeakCollection:(DTWeakCollection *)weakCollection forObjectWithHash:(NSUInteger)hash;

@end

--- DTWeakCollectionSentry.m


#import "DTWeakCollectionSentry.h"
#import "DTWeakCollection.h"

@interface DTWeakCollection (private)

- (void)checkOutObjectWithHash:(NSUInteger)hash;

@end

@implementation DTWeakCollectionSentry
{
    __weak DTWeakCollection *_weakCollection;
    NSUInteger _hash;
}

- (id)initWithWeakCollection:(DTWeakCollection *)weakCollection forObjectWithHash:(NSUInteger)hash
{
    self = [super init];

    if (self)
    {
        _weakCollection = weakCollection;
        _hash = hash;
    }

    return self;
}

- (void)dealloc
{
    [_weakCollection checkOutObjectWithHash:_hash];
}

@end

This would be used like this:

NSString *string = @"bla";

@autoreleasepool {
_weakCollection = [[DTWeakCollection alloc] init];
    [_weakCollection checkInObject:string];

__object = [NSNumber numberWithInteger:1123333];

[_weakCollection checkInObject:__object];
}

if you output allObjects inside the autorelease pool block then you have two objects in there. Outside you only have the string.

I found that in the dealloc of the entry the object reference is already nil, so you cannot use __weak. Instead I am using the memory address of the object as hash. While these are still in _entries you can treat them as actual object and the allObjects returns an autoreleased array of strong references.

Note: this is not thread safe. Do deal with dealloc's on non-main queues/threads you would need to be careful to synchronize accessing and mutating the internal _entries set.

Note 2: This currently only works with objects checking into a single weak collection since a second check in would overwrite the associated sentry. If you needed this with multiple weak collections then the sentry instead should have an array of those collections.

Note 3: I changed the sentry's reference to the collection to weak as well to avoid a retain cycle.

Note 4: Here are a typedef and helper functions which handle the block syntax for you:

typedef id (^WeakReference)(void);

WeakReference MakeWeakReference (id object) {
    __weak id weakref = object;
    return [^{ return weakref; } copy];
}

id WeakReferenceNonretainedObjectValue (WeakReference ref) {
    if (ref == nil)
        return nil;
    else
        return ref ();
}
paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
Cocoanetics
  • 8,171
  • 2
  • 30
  • 57
  • Re note 2: you only ever set an associated object, the key is never used again, so could you not use `sentry` itself as the key? Would that not cope with an object being entered into a collection twice or into different collections? – CRD Jan 08 '13 at 09:12
  • 1
    Thanks for this answer! Using a block as a wrapper is working well. I wrote a typedef and a couple helper functions to make it a little easier to use. I can paste those into the end of your answer if you'd like. – paulmelnikow Jan 08 '13 at 17:21
  • @CRD No, the key needs to be in static memory. – Cocoanetics Jan 08 '13 at 17:34
  • @Cocoanetics - The key doesn't need to be static, it is just that a static is usually used. The docs state "The key is a void pointer. The key for each association must be unique. A typical pattern is to use a static variable." This makes sense, it is probably just using a unique 64-bit value as the key. I think in your case as you never need to retrieve the association you don't need to actually know the key... – CRD Jan 08 '13 at 18:07
  • @Cocoanetics, the code for the self-updating collection is more than I need for this particular application. I was glad to try out the block-based solution which does work, but the wrapper object is easier for me to understand. – paulmelnikow Jan 08 '13 at 18:27
  • @noa oh well, not choosing an answer or not crediting somebody's inout is a method to quickly lose friends on Stackaoverflow. – Cocoanetics Jan 08 '13 at 22:07
  • I added a sentence giving you credit in my answer. Sorry about that. My preference is to use a wrapper object rather than a wrapper block. I can't accept my own answer until tomorrow. – paulmelnikow Jan 08 '13 at 22:25
17

Here's code for an a zeroing weak-referencing wrapper class. It works correctly with NSArray, NSSet, and NSDictionary.

The advantage of this solution is that it's compatible with older OS's and that's it simple. The disadvantage is that when iterating, you likely need to verify that -nonretainedObjectValue is non-nil before using it.

It's the same idea as the wrapper in the first part of Cocoanetics' answer, which uses blocks to accomplish the same thing.

WeakReference.h

@interface WeakReference : NSObject {
    __weak id nonretainedObjectValue;
    __unsafe_unretained id originalObjectValue;
}

+ (WeakReference *) weakReferenceWithObject:(id) object;

- (id) nonretainedObjectValue;
- (void *) originalObjectValue;

@end

WeakReference.m

@implementation WeakReference

- (id) initWithObject:(id) object {
    if (self = [super init]) {
        nonretainedObjectValue = originalObjectValue = object;
    }
    return self;
}

+ (WeakReference *) weakReferenceWithObject:(id) object {
    return [[self alloc] initWithObject:object];
}

- (id) nonretainedObjectValue { return nonretainedObjectValue; }
- (void *) originalObjectValue { return (__bridge void *) originalObjectValue; }

// To work appropriately with NSSet
- (BOOL) isEqual:(WeakReference *) object {
    if (![object isKindOfClass:[WeakReference class]]) return NO;
    return object.originalObjectValue == self.originalObjectValue;
}

@end
paulmelnikow
  • 16,895
  • 8
  • 63
  • 114
  • You actually don't need the blocks in this case because you have weak ivars. – Cocoanetics Jan 08 '13 at 17:48
  • Right. Same idea, without blocks. – paulmelnikow Jan 08 '13 at 18:15
  • No, you misunderstand. You don't need those block wrappers at all. Just use the ivars/properties! – Cocoanetics Jan 09 '13 at 05:48
  • I do understand, that's what I'm saying too! Either use blocks or these wrapper objects. – paulmelnikow Jan 09 '13 at 17:46
  • No, I mean in your WeakReference wrapper objects you don't need the blocks. – Cocoanetics Jan 10 '13 at 06:20
  • There are no blocks in those objects. – paulmelnikow Jan 10 '13 at 16:58
  • Sorry, I meant, you don't need the getters, the properties are all you need. – Cocoanetics Jan 11 '13 at 06:26
  • PS: I don't think that your answer is the optimal one for your question. Having a weak collection like I propose gives you many advantages, the least of which is that you can have the collection deal with removing dealloced objects. In your solution with a wrapper you have to keep checking yourself if the object is still there. – Cocoanetics Jan 11 '13 at 06:30
  • As @Cocoanetics said, this solution is not optimal because you have to manually keep track of the object inside the collection. – David May 31 '13 at 12:47
  • 1
    @David I worked into the answer a description of the disadvantage. Cocoanetics is way more complicated so it's not obviously better. If you're going to downvote the answer perhaps you'd at least upvote the question since you seem to find it useful. – paulmelnikow May 31 '13 at 15:59
  • I'm marking this accepted, as it's the code I'm still using, and it's most accommodating of older versions of OS X and iOS. If you're using the latest versions, you can use NSMapTable. – paulmelnikow May 14 '14 at 02:29
  • If you override isEqual, you should override hash too - in this case just returning originalObjectValue would be a reasonable hash value. – Peter N Lewis Dec 11 '14 at 04:57
  • Note that technically you could allocate an object and wrapper, release the object, allocate another object and wrapper and end up with two difference objects (one released, one new one) with two wrappers that are isEqual. So really you should check the nonretainedObjectValue value for equality as well, but I'm not sure there is a safe way to do that given it could change to nil at any instant. – Peter N Lewis Dec 11 '14 at 05:00
  • 1
    To work correctly, you should probably implement -isEqual: to forward to the underlying object, since right now that would turn a collection of these into straight object pointer equality. If you have weak references to objects which implement -isEqual: themselves, then they will behave very differently when added directly to an NSSet/NSArray/etc. versus when using them wrapped. And as noted, if you override -isEqual:, you need to override -hash -- otherwise they are completely broken in NSSets and as NSDictionary keys. – Carl Lindberg Jul 09 '15 at 20:58
  • Yup. It's been a while since I looked at this code, but that makes sense. Feel free to submit an edit… – paulmelnikow Jul 10 '15 at 01:12
8

NSMapTable should work for you. Available in iOS 6.

Tricertops
  • 8,492
  • 1
  • 39
  • 41
  • The 10.7 docs explicitly state that NSMapTable does not support weak references under ARC. It is available in 10.8, however – see the `NSMapTableWeakMemory` option. – paulmelnikow Apr 04 '13 at 03:26
3
@interface Car : NSObject
@end
@implementation Car
-(void) dealloc {
    NSLog(@"deallocing");
}
@end


int main(int argc, char *argv[])
{
    @autoreleasepool {
        Car *car = [Car new];

        NSUInteger capacity = 10;
        id __weak *_objs = (id __weak *)calloc(capacity,sizeof(*_objs));
        _objs[0] = car;
        car = nil;

        NSLog(@"%p",_objs[0]);
        return EXIT_SUCCESS;
    }
}

Output:

2013-01-08 10:00:19.171 X[6515:c07] deallocing
2013-01-08 10:00:19.172 X[6515:c07] 0x0

edit: I created a sample weak map collection from scratch based on this idea. It works, but it's ugly for several reasons:

I used a category on NSObject to add @properties for the key,next map bucket, and a reference to the collection owning the object.

Once you nil the object it disappears from the collection.

BUT, for the map to have a dynamic capacity, it needs to receive an update the number of elements to calculate the load factor and expand capacity if needed. That is, unless you want to perform a Θ(n) update iterating the whole array each time you add an element. I did this with a callback on the dealloc method of the sample object I'm adding to the collection. I could edit the original object (which I did for brevity) or inherit from a superclass, or swizzle the dealloc. In any case, ugly.

However, if you don't mind having a fixed capacity collection, you don't need the callbacks. The collection uses separate chaining and assuming an uniform distribution of the hash function, performance will be Θ(1+n/m) being n=elements,m=capacity. But (more buts) to avoid breaking the chaining you would need to add a previous link as a category @property and link it to the next element in the dealloc of the element. And once we are touching the dealloc, it's just as good to notify the collection that the element is being removed (which is what is doing now).

Finally, note that the test in the project is minimal and I could have overlooked something.

Jano
  • 62,815
  • 21
  • 164
  • 192
3

If you are working with at least MacOS X 10.5, or iOS6, then:

  • NSPointerArray weakObjectsPointerArray/pointerArrayWithWeakObjects is a weak reference standin for NSArray
  • NSHashTable hashTableWithWeakObjects/weakObjectsHashTable is a weak reference standin for NSSet
  • NSMapTable is a weak reference standin for NSDictionary (can have weak keys and/or weak values)

Note that the collections may not immediately notice that objects have gone away, so counts could still be higher, and keys could still exist even though the associated object is gone, etc. NSPointerArray has a -compact method which should in theory get rid of any nilled pointers. NSMapTable docs note that the keys for weakToStrong maps will remain in the table (even though effectively nil) until it is resized, meaning the strong object pointers can remain in memory even if no longer logically referenced.

Edit: I see the original poster asked about ARC. I think it was indeed 10.8 and iOS 6 before those containers could be used with ARC -- the previous "weak" stuff was for GC, I think. ARC wasn't supported until 10.7, so it's really a question if you need to support that release and not 10.6, in which case you would need to roll your own (or perhaps use custom functions with NSPointerFunctions, which can then in turn be used with NSPointerArray, NSHashTable, and NSMapTable).

Carl Lindberg
  • 2,902
  • 18
  • 22
  • See my comment above: per the documentation, `+weakObjectsPointerArray`/`NSPointerFunctionsWeakMemory ` is only available in 10.8. – paulmelnikow Jul 09 '15 at 21:36
  • No, it was available in 10.5 per the header. They changed the preferred method name from pointerArrayWithWeakObjects to weakObjectsPointerArray in 10.8, that's all -- so you'd have to use the older method name on the older platforms. The old constant name was NSPointerFunctionsZeroingWeakMemory ; the preferred constant has similarly changed as well, but it was essentially available in 10.5. – Carl Lindberg Jul 10 '15 at 17:55
2

I just create non-threadsafe weak ref version of NSMutableDictionary and NSMutableSet. Code here: https://gist.github.com/4492283

For NSMutableArray, things is more complicated because it cannot contain nil and an object may be added to the array multiple times. But it is feasible to implemented one.

Bryan Chen
  • 45,816
  • 18
  • 112
  • 143
2

Just add a category for NSMutableSet with following code:

@interface WeakReferenceObj : NSObject
@property (nonatomic, weak) id weakRef;
@end

@implementation WeakReferenceObj
+ (id)weakReferenceWithObj:(id)obj{
    WeakReferenceObj *weakObj = [[WeakReferenceObj alloc] init];
    weakObj.weakRef = obj;
    return weakObj;
}
@end

@implementation NSMutableSet(WeakReferenceObj)
- (void)removeDeallocRef{
    NSMutableSet *deallocSet = nil;
    for (WeakReferenceObj *weakRefObj in self) {
        if (!weakRefObj.weakRef) {
            if (!deallocSet) {
                deallocSet = [NSMutableSet set];
            }
            [deallocSet addObject:weakRefObj];
        }
    }
    if (deallocSet) {
        [self minusSet:deallocSet];
    }
}

- (void)addWeakReference:(id)obj{
    [self removeDeallocRef];
    [self addObject:[WeakReferenceObj weakReferenceWithObj:obj]];
}
@end

Same way to create a category for NSMutableArray and NSMutableDictionary.

Remove dealloc reference in didReceiveMemoryWarning will be better.

- (void)didReceiveMemoryWarning{
    [yourWeakReferenceSet removeDeallocRef];
}

Then, what you should do is to invoke addWeakReference: for your container class.

simalone
  • 2,768
  • 1
  • 15
  • 20
0

See the BMNullableArray class, which is part of my BMCommons framework for a full solution to this problem.

This class allows nil objects to be inserted and has the option to weakly reference the objects it contains (automatically nilling them when they get deallocated).

The problem with automatic removal (which I tried to implement) is that you get thread-safety issues, since it is not guaranteed at which point in time objects will be deallocated, which might as well happen while iterating the array.

This class is an improvement over NSPointerArray, since it abstracts some lower level details for you and allows you to work with objects instead of pointers. It even supports NSFastEnumeration to iterate over the array with nil references in there.

Werner Altewischer
  • 10,080
  • 4
  • 53
  • 60