Коллекции обнуления слабых ссылок в ARC

Как я могу получить массив обнуления слабых ссылок под ARC? Я не хочу, чтобы массив сохранял объекты. И я хотел бы, чтобы элементы массива либо удалялись, когда они были освобождены, либо устанавливали эти записи в nil.

Аналогично, как я могу это сделать со словарем? Я не хочу, чтобы словарь сохранял значения. И снова, я хотел бы, чтобы словарные элементы либо удалялись, когда значения освобождались, либо устанавливали значения в nil. (Мне нужно сохранить ключи, которые являются уникальными идентификаторами, по крайней мере до тех пор, пока соответствующие значения не будут освобождены.)

Эти два вопроса охватывают сходное основание:

Но ни один из них не запрашивает ссылки на обнуление.

В документации ни NSPointerArray, ни NSHashMap не поддерживают слабые ссылки в ARC. NSValue nonretainedObjectValue не будет работать, поскольку он не обнуляется.

Единственное решение, которое я вижу, - создать мой собственный класс оболочки NSValue с свойством (weak), как этот ответ упоминается в конце. Есть ли лучший способ, который я не вижу?

Я разрабатываю для OS X 10.7 и iOS 6.0.

Ответ 1

Здесь приведен код для обморочного класса обморочной слабой ссылки. Он корректно работает с NSArray, NSSet и NSDictionary.

Преимущество этого решения в том, что оно совместимо со старой ОС и что оно простое. Недостатком является то, что при итерации вам, вероятно, нужно проверить, что -nonretainedObjectValue не равен нулю, прежде чем использовать его.

Это та же идея, что и обертка в первой части ответа Cocoanetics, которая использует блоки для выполнения того же самого.

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

Ответ 2

Для устранения слабых ссылок требуется OS X 10.7 или iOS 5.

Вы можете определять только слабые переменные в коде, ivars или блоках. AFAIK нет возможности динамически (во время выполнения) создать слабую переменную, поскольку ARC вступает в силу во время компиляции. Когда вы запускаете код, у него уже есть сохраненные и выпущенные вами версии.

Сказав, что вы, вероятно, можете злоупотреблять блоками для достижения такого эффекта.

Имейте блок, который просто возвращает ссылку.

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

Обратите внимание, что вам нужно скопировать блок, чтобы скопировать его в кучу.

Теперь вы можете ходить по массиву в любое удобное для вас время, объекты dealloc'ed в блоках будут возвращать нуль. Затем вы можете удалить их.

Вы не можете автоматически запускать код при сбросе слабой ссылки ref. Если это то, что вам нужно, вы можете использовать функцию связанных объектов. Те получают освобождение в то же время, что и объект, к которому они привязаны. Таким образом, у вас может быть свой собственный тег чата, который информирует слабую коллекцию о кончине объектов.

У вас есть один связанный объект для наблюдения за dealloc (если ассоциация является единственной ссылкой), и связанный объект будет иметь указатель на просмотр коллекции. Затем в часовом dealloc вы вызываете слабую коллекцию, чтобы сообщить ей, что просмотренный объект ушел.

Здесь моя запись по связанным объектам: http://www.cocoanetics.com/2012/06/associated-objects/

Здесь моя реализация:

---- 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

Это будет использоваться следующим образом:

NSString *string = @"bla";

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

__object = [NSNumber numberWithInteger:1123333];

[_weakCollection checkInObject:__object];
}

если вы выведете allObjects внутри блока пула автозаполнения, тогда у вас есть два объекта. Снаружи у вас есть только строка.

Я обнаружил, что в dealloc записи ссылка на объект уже равна нулю, поэтому вы не можете использовать __weak. Вместо этого я использую адрес памяти объекта как хэш. Хотя они все еще находятся в _entries, вы можете рассматривать их как действительный объект, а allObjects возвращает автореализованный массив сильных ссылок.

Примечание. Это не безопасно для потоковой передачи. Поступайте с dealloc в неосновных очередях/потоках, которые вам нужно будет соблюдать, чтобы синхронизировать доступ и изменение внутреннего набора _entries.

Примечание 2: В настоящее время это работает только с проверкой объектов в одной слабой коллекции, поскольку вторая проверка будет перезаписывать связанное часовое. Если вам нужно было это с несколькими слабыми коллекциями, тогда у сторожа вместо этого должен быть массив этих коллекций.

Примечание 3: Я изменил ссылку чата на сборку на слабые, чтобы избежать цикла сохранения.

Примечание 4: Вот функции typedef и helper, которые обрабатывают синтаксис блока для вас:

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 ();
}

Ответ 3

NSMapTable должен работать на вас. Доступно в iOS 6.

Ответ 4

@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;
    }
}

Вывод:

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

edit. Я создал образец слабой коллекции карт с нуля на основе этой идеи. Он работает, но он уродлив по нескольким причинам:

Я использовал категорию в NSObject, чтобы добавить @properties для ключа, следующего картографического ведра и ссылку на коллекцию, владеющую объектом.

Как только вы потеряете объект, он исчезнет из коллекции.

НО, для того чтобы карта имела динамическую емкость, ей необходимо получить обновление количества элементов для расчета коэффициента загрузки и, если необходимо, увеличить пропускную способность. То есть, если вы не хотите выполнять обновление Θ (n), итерацию всего массива каждый раз, когда вы добавляете элемент. Я сделал это с обратным вызовом метода dealloc образца объекта, который я добавляю в коллекцию. Я мог бы отредактировать исходный объект (который я сделал для краткости) или наследовать от суперкласса, или swizzle dealloc. В любом случае, уродливый.

Однако, если вы не возражаете против сбора фиксированной емкости, вам не нужны обратные вызовы. В коллекции используется отдельная цепочка и при условии равномерного распределения хэш-функции, производительность будет равна (1 + n/m) как n = элементы, м = емкость. Но (больше), чтобы избежать разрыва цепочки, вам нужно добавить предыдущую ссылку как категорию @property и связать ее со следующим элементом в dealloc элемента. И как только мы касаемся dealloc, так же хорошо уведомлять коллекцию, что элемент удаляется (что и делается сейчас).

Наконец, обратите внимание, что тест в проекте минимален, и я мог что-то пропустить.

Ответ 5

Я просто создаю non-threadsafe слабую версию ref NSMutableDictionary и NSMutableSet. Код здесь: https://gist.github.com/4492283

Для NSMutableArray все сложнее, потому что он не может содержать nil, и объект может быть добавлен в массив несколько раз. Но возможно реализовать один из них.

Ответ 6

Просто добавьте категорию для NSMutableSet со следующим кодом:

@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

Такой же способ создания категории для NSMutableArray и NSMutableDictionary.

Удалить ссылку dealloc в didReceiveMemoryWarning будет лучше.

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

Затем вам нужно вызвать addWeakReference: для вашего класса контейнера.

Ответ 7

Если вы работаете хотя бы с MacOS X 10.5 или iOS6, то:

  • NSPointerArray weakObjectsPointerArray/pointerArrayWithWeakObjects является слабым эталоном для NSArray
  • NSHashTable hashTableWithWeakObjects/weakObjectsHashTable является слабым эталоном для NSSet
  • NSMapTable является слабым эталоном для NSDictionary (может иметь слабые клавиши и/или слабые значения)

Обратите внимание, что коллекции не могут сразу заметить, что объекты ушли, поэтому счетчик все равно может быть выше, и ключи все еще могут существовать, даже если связанный объект ушел, и т.д. NSPointerArray имеет метод -compact, который теоретически должен получить избавиться от любых нулевых указателей. Документы NSMapTable отмечают, что ключи для слайд-карт TongStrong останутся в таблице (хотя и эффективно ноль) до тех пор, пока они не будут изменены, что означает, что сильные указатели объектов могут оставаться в памяти, даже если они больше не логически ссылаются.

Изменить: я вижу, что оригинальный плакат спросил о ARC. Я думаю, что действительно было 10.8 и iOS 6 до того, как эти контейнеры могли использоваться с ARC - предыдущий "слабый" материал был для GC, я думаю. ARC не поддерживалась до 10.7, так что это действительно вопрос, если вам нужно поддерживать этот выпуск, а не 10.6, и в этом случае вам нужно будет сворачивать свои собственные (или, возможно, использовать пользовательские функции с NSPointerFunctions, которые затем могут быть использованы с NSPointerArray, NSHashTable и NSMapTable).

Ответ 8

Смотрите класс BMNullableArray, который является частью моей базы BMCommons для полного решения этой проблемы проблема.

Этот класс позволяет вставлять ненулевые объекты и имеет возможность слабо ссылаться на содержащиеся в нем объекты (автоматически их заполнять, когда они освобождаются).

Проблема с автоматическим удалением (который я пытался реализовать) заключается в том, что вы получаете проблемы с обеспечением безопасности потоков, так как не гарантируется, что в момент времени объекты будут освобождены, что также может произойти при повторении массива.

Этот класс является улучшением по сравнению с NSPointerArray, поскольку он абстрагирует некоторые детали более низкого уровня для вас и позволяет вам работать с объектами вместо указателей. Он даже поддерживает NSFastEnumeration для итерации по массиву с нулевыми ссылками там.