web-dev-qa-db-fra.com

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

Comment puis-je obtenir un tableau de mise à 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 se suppriment d'eux-mêmes lorsqu'ils sont désalloués ou définissent ces entrées sur nil.

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 d'eux-mêmes lorsque les valeurs sont désallouées ou définissent les valeurs sur nil. (Je dois conserver les clés, qui sont les identificateurs 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 la remise à zéro des références .

Selon la documentation, ni NSPointerArray ni NSHashMap ne prennent en charge les références faibles sous ARC. La valeur nonretainedObjectValue de NSValue ne fonctionnera pas non plus, car elle ne réduit pas à zéro.

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

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

40
paulmelnikow

Voici le code pour une classe de wrapper à zéro référant faible. Cela 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 que c'est simple. L'inconvénient est que, lors de l'itération, vous devez probablement vérifier que -nonretainedObjectValue est non nul avant de l'utiliser.

C'est la même idée que l'encapsuleur dans 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
17
paulmelnikow

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

Vous ne pouvez définir que des variables faibles en code, en ivars ou en blocs. Autant que je sache, il n’ya aucun moyen de créer dynamiquement (au moment de l’exécution) une variable faible, car ARC prend effet au moment de la compilation. Lorsque vous exécutez le code, les retenues et les éditions sont déjà ajoutées.

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 le copier dans le tas.

Maintenant, vous pouvez parcourir le tableau à tout moment, les objets désalloués en blocs retournent nil. Vous pouvez ensuite les supprimer.

Vous ne pouvez pas faire exécuter automatiquement le code 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. Ainsi, vous pourriez avoir votre propre balise sentinelle qui informe la faible collection de la disparition des objets.

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 sur la collection en train de regarder. Ensuite, dans le sentin dealloc, vous appelez la collection faible pour l'informer que l'objet surveillé est parti.

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

Ceci serait utilisé comme ceci:

NSString *string = @"bla";

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

__object = [NSNumber numberWithInteger:1123333];

[_weakCollection checkInObject:__object];
}

si vous produisez allObjects à l'intérieur du bloc de pool de libération automatique, vous avez deux objets à l'intérieur. Dehors, vous n'avez que la ficelle.

J'ai trouvé que dans le dealloc de l'entrée, la référence de l'objet est déjà nulle, vous ne pouvez donc pas utiliser __weak. Au lieu de cela, j'utilise l'adresse mémoire de l'objet comme hachage. Tant qu'ils sont toujours dans _entries, vous pouvez les traiter comme des objets réels et allObjects renvoie un tableau de références fortes automatiquement libéré.

Remarque: ce n'est pas thread-safe. Traitez les dealloc sur les files d'attente/threads non principales; vous devez veiller à synchroniser l'accès et la mutation du jeu _entries internes.

Remarque 2: Cela ne fonctionne actuellement qu'avec les objets qui entrent dans une seule collection faible, car une seconde archivage écraserait la sentinelle associée. Si vous aviez besoin de cela avec plusieurs collections faibles, la sentinelle devrait plutôt disposer d'un tableau de ces collections.

Note 3: J'ai également modifié la référence de la sentinelle à la collection pour éviter un cycle de conservation. 

Note 4: Voici un typedef et des fonctions d'assistance qui gèrent la syntaxe de bloc 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 ();
}
24
Cocoanetics

NSMapTable devrait fonctionner pour vous. Disponible dans iOS 6.

8
Tricertops
@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

edit : J'ai créé un échantillon de collection de cartes faible à partir de rien, basé sur cette idée. Cela fonctionne, mais c'est moche pour plusieurs raisons:

J'ai utilisé une catégorie sur NSObject pour ajouter @properties pour la clé, le prochain compartiment de carte et une référence à la collection propriétaire de l'objet.

Une fois que vous supprimez l'objet, 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. Autrement dit, à moins que vous ne souhaitiez effectuer une mise à jour de Θ (n) en itérant l’ensemble du tableau chaque fois que vous ajoutez un élément. Je l'ai fait avec un rappel sur la méthode dealloc de l'exemple d'objet que j'ajoute à la collection. Je pouvais éditer l'objet original (ce que je faisais par souci de brièveté), hériter d'une superclasse ou balancer le dealloc. En tout cas moche.

Cependant, si la collecte de capacité fixe ne vous dérange pas, 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, les performances seront (1 + n/m) étant n = éléments, m = capacité. Mais (plus de buts) pour éviter de briser l'enchaînement, vous devez ajouter un lien précédent en tant que catégorie @propriété et le lier à l'élément suivant dans le dealloc de l'élément. Et une fois que nous touchons le dealloc, il est tout aussi bon de signaler à la collection que l'élément est en cours de suppression (ce qui est le cas actuellement). 

Enfin, notez que le test dans le projet est minimal et que j'aurais pu oublier quelque chose.

3
Jano

Je viens de créer une version de référence faible 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 nil et un objet peut être ajouté plusieurs fois au tableau. Mais il est possible d'en implémenter un.

2
Bryan Chen

Ajoutez simplement une catégorie pour NSMutableSet avec le code suivant: 

@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

Même façon de créer une catégorie pour NSMutableArray et NSMutableDictionary.

Supprimer la référence dealloc dans didReceiveMemoryWarning sera mieux.

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

Ensuite, vous devez invoquer addWeakReference: pour votre classe de conteneur.

2
simalone

Si vous travaillez avec au moins MacOS X 10.5 ou iOS6, alors:

  • NSPointerArray faibleObjectsPointerArray/pointerArrayWithWeakObjects est une référence faible pour NSArray.
  • NSHashTable hashTableWithWeakObjects/faibleObjectsHashTable est une référence faible pour NSSet
  • NSMapTable est une référence faible pour NSDictionary (peut avoir des clés faibles et/ou des valeurs faibles)

Notez que les collections ne remarquent peut-être pas immédiatement que les objets ont disparu, de sorte que le nombre peut être encore plus élevé et que des clés peuvent exister même si l'objet associé a disparu, etc. NSPointerArray a une méthode -compact qui devrait en théorie se débarrasser de pointeurs nilled. Les documents NSMapTable notent que les clés pour les mappes faiblesToStrong resteront dans la table (même si elles sont réellement nulles) jusqu'à ce qu'elles soient redimensionnées, ce qui signifie que les pointeurs d'objet forts peuvent rester en mémoire même s'ils ne sont plus référencés logiquement.

Edit: Je vois l'affiche originale posée sur ARC. Je pense que c’était bien 10,8 et iOS 6 avant que ces conteneurs puissent être utilisés avec ARC - l’ancien "faible" était pour GC, je pense. ARC n'étant pris en charge que par la version 10.7, la question est de savoir si vous devez prendre en charge cette version et non 10.6, auquel cas vous devrez rouler vous-même (ou peut-être utiliser des fonctions personnalisées avec NSPointerFunctions, qui pourront ensuite être utilisées). avec NSPointerArray, NSHashTable et NSMapTable).

2
Carl Lindberg

Consultez la classe BMNullableArray , qui fait partie de mon cadre BMCommons pour obtenir une solution complète à ce problème.

Cette classe permet d'insérer des objets nil et a la possibilité de référencer faiblement les objets qu'il contient (les supprimant automatiquement lorsqu'ils sont désalloués).

Le problème de la suppression automatique (que j'ai essayé d'implémenter) est que vous rencontrez des problèmes de sécurité des threads, car il n'est pas garanti à quel moment les objets seront désalloués, ce qui pourrait aussi bien se produire lors de l'itération du tableau.

Cette classe est une amélioration par rapport à NSPointerArray, car elle résume certains détails de niveau inférieur et vous permet de travailler avec des objets plutôt que des pointeurs. Il prend même en charge NSFastEnumeration pour parcourir le tableau avec des références nulles.

0
Werner Altewischer