web-dev-qa-db-fra.com

Meilleures pratiques pour les paramètres de contexte dans addObserver (KVO)

Je me demandais ce que vous devez définir le pointeur de contexte dans KVO lorsque vous observez une propriété. Je commence tout juste à utiliser KVO et je n'ai pas trop glané de la documentation. Je vois sur cette page: http://www.jakeri.net/2009/12/custom-callout-bubble-in-mkmapview-final-solution/ l'auteur fait ceci:

[annView addObserver:self
forKeyPath:@"selected"
options:NSKeyValueObservingOptionNew
context:GMAP_ANNOTATION_SELECTED];

Et puis dans le rappel, cela fait:

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context{

NSString *action = (NSString*)context;


if([action isEqualToString:GMAP_ANNOTATION_SELECTED]){

Je suppose que dans ce scénario, l'auteur crée simplement une chaîne à identifier plus tard dans le rappel.

Ensuite, dans iOS 5 Pushing the Limits, je vois qu'il fait ceci:

[self.target addObserf:self forKeyPath:self.property options:0 context:(__bridge void *)self];

rappeler:

if ((__bridge id)context == self) {
}
else {
   [super observeValueForKeyPath .......];
}

Je me demandais s'il y avait une norme ou des meilleures pratiques à passer dans le pointeur de contexte?

41
Crystal

L'important est (en général) que vous utilisez quelque chose (par opposition à rien) et que tout ce que vous utilisez soit nique et privé à votre utilisation).

Le principal écueil se produit ici lorsque vous avez une observation dans l'une de vos classes, puis que quelqu'un sous-classe votre classe et ajoute une autre observation du même objet observé et du même chemin d'accès. Si votre original observeValueForKeyPath:... l'implémentation a seulement vérifié keyPath, ou le object observé, ou même les deux, cela pourrait ne pas être suffisant pour savoir que c'est votre observation rappelée. L'utilisation d'un context dont la valeur est unique et privée pour vous vous permet d'être beaucoup plus certain qu'un appel donné à observeValueForKeyPath:... est l'appel que vous attendez de lui.

Cela aurait une importance si, par exemple, vous vous enregistrez uniquement pour les notifications didChange, mais qu'une sous-classe s'inscrit pour le même objet et keyPath avec l'option NSKeyValueObservingOptionPrior. Si vous ne filtriez pas les appels vers observeValueForKeyPath:... en utilisant un context (ou en vérifiant le dictionnaire des modifications), votre gestionnaire s'exécuterait plusieurs fois, alors que vous ne vous attendiez à ce qu'il s'exécute qu'une seule fois. Il n'est pas difficile d'imaginer comment cela pourrait causer des problèmes.

Le modèle que j'utilise est:

static void * const MyClassKVOContext = (void*)&MyClassKVOContext;

Ce pointeur pointera vers son propre emplacement, et cet emplacement est unique (aucune autre variable statique ou globale ne peut avoir cette adresse, ni aucun objet alloué au tas ou à la pile ne pourra jamais avoir cette adresse - c'est un assez fort, mais il est vrai que non absolu , garantie), grâce à l'éditeur de liens. const fait en sorte que le compilateur nous avertisse si nous essayons d'écrire du code qui changerait la valeur du pointeur, et enfin, static le rend privé dans ce fichier, donc pas une personne en dehors de ce fichier peut en obtenir une référence (encore une fois, ce qui la rend plus susceptible d'éviter les collisions).

Un modèle que je mettrais en garde contre en utilisant celui qui est apparu dans la question:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    NSString *action = (NSString*)context;
    if([action isEqualToString:GMAP_ANNOTATION_SELECTED]) {

context est déclaré être un void*, ce qui signifie que c'est toute la garantie que l'on peut faire de ce que c'est. En le castant sur un NSString* vous ouvrez une grande boîte de méchanceté potentielle. Si quelqu'un d'autre a un enregistrement qui n'utilise pas un NSString* pour le paramètre context, cette approche se bloque lorsque vous passez la valeur non-objet à isEqualToString:. Égalité du pointeur (ou bien intptr_t ou uintptr_t égalité) sont les seules vérifications sûres pouvant être utilisées avec une valeur context.

L'utilisation de self comme context est une approche courante. C'est mieux que rien, mais a une unification et une confidentialité beaucoup plus faibles, car d'autres objets (sans parler des sous-classes) ont accès à la valeur de self et peuvent l'utiliser comme context (provoquant une ambiguïté) , contrairement à l'approche que j'ai suggérée ci-dessus.

Rappelez-vous également que ce ne sont pas juste sous-classes qui pourraient causer des pièges ici; Bien que ce soit sans doute un modèle rare, rien n'empêche un autre objet d'enregistrer votre objet pour de nouvelles observations KVO.

Pour une meilleure lisibilité, vous pouvez également envelopper cela dans une macro de préprocesseur comme:

#define MyKVOContext(A) static void * const A = (void*)&A;
98
ipmcc

Le contexte KVO doit être un pointeur vers une variable statique, comme le montre this Gist . En règle générale, je me retrouve à faire ce qui suit:

Près du haut de mon fichier ClassName.m J'aurai la ligne

static char ClassNameKVOContext = 0;

Lorsque je commence à observer la propriété aspect sur targetObject (une instance de TargetClass), j'aurai

[targetObject addObserver:self
               forKeyPath:PFXKeyTargetClassAspect
                  options://...
                  context:&ClassNameKVOContext];

où PFXKeyTargetClassAspect est un NSString * défini dans TargetClass.m égal à @"aspect" et déclaré extern dans TargetClass.h. (Bien sûr, PFX n'est qu'un espace réservé pour le préfixe que vous utilisez dans votre projet.) Cela me donne l'avantage de la saisie semi-automatique et me protège contre les fautes de frappe.

Quand j'aurai fini d'observer aspect sur targetObject j'aurai

[targetObject removeObserver:self
                  forKeyPath:PFXKeyTargetClassAspect
                     context:&ClassNameKVOContext];

Afin d'éviter une trop grande indentation dans ma mise en œuvre de -observeValueForKeyPath:ofObject:change:context:, J'aime écrire

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context != &ClassNameKVOContext) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }

    if ([object isEqual:targetObject]) {
        if ([keyPath isEqualToString:PFXKeyTargetClassAspect]) {
            //targetObject has changed the value for the key @"aspect".
            //do something about it
        }
    }
}
18
Nate Chandler

Je pense que la meilleure façon serait de l'implémenter comme le dit le document d'Apple:

L'adresse d'une variable statique au nom unique dans votre classe constitue un bon contexte.

static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext = &PersonAccountInterestRateContext;

voir documentation .

1
Mykhailo Lysenko