40 votes

Collections de références faibles de mise à zéro sous ARC

Comment puis-je obtenir un tableau de Remise à zéro des références faibles sous ARC ? Je ne veux pas que le tableau conserve les objets. Et j'aimerais que les éléments du tableau s'effacent lorsqu'ils sont désalloués, ou que ces entrées deviennent nulles.

De même, comment puis-je faire cela avec un dictionnaire ? Je ne veux pas que le dictionnaire conserve les valeurs. Et encore une fois, j'aimerais que les éléments du dictionnaire se suppriment lorsque les valeurs sont désallouées, ou qu'ils mettent les valeurs à zéro. (J'ai besoin de conserver les clés, qui sont les identifiants uniques, au moins jusqu'à ce que les valeurs correspondantes soient désallouées).

Ces deux questions couvrent un terrain similaire :

Mais ni l'un ni l'autre ne demande références de mise à zéro .

D'après la documentation, ni NSPointerArray ni NSHashMap ne prennent en charge les références faibles sous ARC. La nonretainedObjectValue de NSValue ne fonctionnera pas non plus, car elle est non nulle.

La seule solution que je vois est de créer le mien classe enveloppe de type NSValue avec une (weak) la propriété, comme cette réponse mentionne, vers la fin . Y a-t-il une meilleure solution que je ne vois pas ?

Je développe pour OS X 10.7 et iOS 6.0.

24voto

Cocoanetics Points 5729

La mise à zéro des références faibles nécessite OS X 10.7 ou iOS 5.

Vous ne pouvez définir des variables faibles que dans le code, les ivars ou les blocs. A priori, il n'y a aucun moyen de créer dynamiquement (à l'exécution) une variable faible car l'ARC prend effet au moment de la compilation. Lorsque vous exécutez le code, il a déjà les retenues et les libérations ajoutées pour vous.

Cela dit, vous pouvez probablement abuser des blocs pour obtenir un tel effet.

Avoir un bloc qui renvoie simplement la référence.

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

Notez que vous devez copier le bloc pour qu'il soit copié dans le tas.

Maintenant vous pouvez parcourir le tableau quand vous le souhaitez, les objets désalloués dans les blocs retourneront nil. Vous pouvez alors les supprimer.

Vous ne pouvez pas faire en sorte que le code soit automatiquement exécuté lorsque la référence faible est mise à zéro. Si c'est ce que vous voulez, vous pouvez utiliser la fonction des objets associés. Ceux-ci sont désalloués en même temps que l'objet auquel ils sont associés. Vous pouvez donc avoir votre propre balise sentinelle qui informe la collection faible de la disparition de l'objet.

Vous auriez un objet associé à surveiller pour le dealloc (si l'association est la seule référence) et l'objet associé aurait un pointeur vers la collection à surveiller. Ensuite, lors du dealloc sentry, vous appelez la collection faible pour l'informer que l'objet surveillé a disparu.

Voici mon article sur les objets associés : http://www.cocoanetics.com/2012/06/associated-objects/

Voici ma mise en œuvre :

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

Cela serait utilisé comme suit :

NSString *string = @"bla";

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

__object = [NSNumber numberWithInteger:1123333];

[_weakCollection checkInObject:__object];
}

Si vous affichez allObjects à l'intérieur du bloc autorelease pool, vous avez deux objets à l'intérieur. À l'extérieur, vous n'avez que la chaîne de caractères.

J'ai trouvé que dans le dealloc de l'entrée la référence de l'objet est déjà nil, donc vous ne pouvez pas utiliser __weak. A la place, j'utilise l'adresse mémoire de l'objet comme hash. Pendant que ceux-ci sont toujours dans _entries vous pouvez les traiter comme des objets réels et allObjects retourne un tableau autoreleased de références fortes.

Note : ceci n'est pas thread safe. Pour gérer les allocations sur les files d'attente/threads non principaux, vous devrez faire attention à synchroniser l'accès et la modification de l'ensemble des _entrées internes.

Note 2 : Ceci ne fonctionne actuellement qu'avec des objets entrant dans une seule collection faible, car une deuxième entrée écraserait la sentinelle associée. Si vous avez besoin de cela avec plusieurs collections faibles, alors la sentinelle devrait avoir un tableau de ces collections.

Note 3 : J'ai changé la référence de la sentinelle à la collection en faible également pour éviter un cycle de retenue.

Note 4 : Voici un typedef et des fonctions d'aide qui gèrent la syntaxe des blocs pour vous :

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

17voto

noa Points 8851

Voici le code d'une classe enveloppante de mise à zéro des références faibles. Elle fonctionne correctement avec NSArray, NSSet, et NSDictionary.

L'avantage de cette solution est qu'elle est compatible avec les anciens systèmes d'exploitation et c'est tout simple. L'inconvénient est que lors de l'itération, vous devrez probablement vérifier que -nonretainedObjectValue est non-nil avant de l'utiliser.

C'est la même idée que le wrapper de la première partie de la réponse de Cocoanetics, qui utilise des blocs pour accomplir la même chose.

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

8voto

iMartin Points 4853

NSMapTable devrait fonctionner pour vous. Disponible dans iOS 6.

3voto

Jano Points 37593
@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;
    }
}

Sortie :

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

modifier : J'ai créé un exemple de collection de cartes faibles à partir de zéro en se basant sur cette idée. Ça marche, mais c'est moche pour plusieurs raisons :

J'ai utilisé un catégorie sur NSObject pour ajouter des @propriétés pour la clé, le prochain godet de carte et une référence à la collection qui possède l'objet.

Une fois l'objet réduit à néant, il disparaît de la collection.

MAIS, pour que la carte ait une capacité dynamique, elle doit recevoir une mise à jour du nombre d'éléments pour calculer le facteur de charge et augmenter la capacité si nécessaire. C'est-à-dire, à moins que vous ne vouliez effectuer une mise à jour Θ(n) itérant tout le tableau à chaque fois que vous ajoutez un élément. J'ai fait cela avec un callback sur la méthode dealloc de l'objet échantillon que j'ajoute à la collection. Je pourrais éditer l'objet original (ce que j'ai fait par souci de brièveté) ou hériter d'une superclasse, ou encore déformer la méthode dealloc. Dans tous les cas, c'est laid.

Cependant, si cela ne vous dérange pas d'avoir une collection de capacité fixe, vous n'avez pas besoin des rappels. La collection utilise chaînage séparé et en supposant une distribution uniforme de la fonction de hachage, la performance sera Θ(1+n/m) étant n=éléments,m=capacité. Mais (encore des mais) pour ne pas casser le chaînage il faudrait ajouter un lien précédent en tant que catégorie @property et le lier à l'élément suivant dans le dealloc de l'élément. Et une fois que nous touchons au dealloc, il est tout aussi bien de notifier à la collection que l'élément est supprimé (ce qui est fait maintenant).

Enfin, notez que le test dans le projet est minimal et que j'ai pu négliger quelque chose.

2voto

Bryan Chen Points 14689

Je crée simplement une version weak ref non threadsafe de NSMutableDictionary et NSMutableSet. Code ici : https://gist.github.com/4492283

Pour NSMutableArray, les choses sont plus compliquées car il ne peut pas contenir de nil et un objet peut être ajouté au tableau plusieurs fois. Mais il est possible d'en implémenter un.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X