Collections of zeroing weak references under ARC

前端 未结 8 605
粉色の甜心
粉色の甜心 2020-12-23 14:05

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 remov

相关标签:
8条回答
  • 2020-12-23 14:30

    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.

    0 讨论(0)
  • 2020-12-23 14:33

    NSMapTable should work for you. Available in iOS 6.

    0 讨论(0)
  • 2020-12-23 14:36

    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
    
    0 讨论(0)
  • 2020-12-23 14:37

    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.

    0 讨论(0)
  • 2020-12-23 14:45

    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 ();
    }
    
    0 讨论(0)
  • 2020-12-23 14:45
    @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.

    0 讨论(0)
提交回复
热议问题