Лучшие практики для параметра контекста в addObserver (KVO)

Мне было интересно, что вы должны установить указатель контекста в KVO, когда вы наблюдаете свойство. Я только начинаю использовать KVO, и я не слишком много почерпнул из документации. Я вижу на этой странице: http://www.jakeri.net/2009/12/custom-callout-bubble-in-mkmapview-final-solution/ автор делает это:

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

И затем в обратном вызове делает это:

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

NSString *action = (NSString*)context;


if([action isEqualToString:GMAP_ANNOTATION_SELECTED]){

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

Затем в iOS 5 Pushing the Limits book я вижу, что он делает это:

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

Перезвоните:

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

Мне было интересно, если есть стандарт или лучшие практики для передачи в указатель контекста?

3 ответа

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

Основная ловушка здесь возникает, когда у вас есть наблюдение в одном из ваших классов, а затем кто-то подклассов вашего класса, и они добавляют еще одно наблюдение того же наблюдаемого объекта и того же keyPath. Если ваш оригинал observeValueForKeyPath:... только реализация проверена keyPathили наблюдаемое objectили даже то и другое, этого может быть недостаточно, чтобы знать, что ваше наблюдение вызывается обратно. Используя context чье значение является уникальным и частным для вас, позволяет вам быть более уверенным, что данный вызов observeValueForKeyPath:... это вызов, который вы ожидаете.

Это будет иметь значение, если, например, вы зарегистрировались только для didChange уведомления, но подкласс регистрируется для того же объекта и keyPath с NSKeyValueObservingOptionPrior вариант. Если вы не фильтровали звонки observeValueForKeyPath:... используя context (или проверяя словарь изменений), ваш обработчик будет выполняться несколько раз, когда вы ожидаете, что он будет выполнен только один раз. Нетрудно представить, как это может вызвать проблемы.

Шаблон, который я использую:

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

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

Один шаблон, который я бы особо предостерег от использования, - это тот, который появился в вопросе

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

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

context объявлен void*Это означает, что это гарантия того, что это такое. Приведя это к NSString* вы открываете большую коробку потенциального зла. Если кто-то еще имеет регистрацию, которая не использует NSString* для context параметр, этот подход приведет к сбою, когда вы передадите необъектное значение isEqualToString:, Равенство указателей (или альтернативно intptr_t или же uintptr_t равенство) являются единственными безопасными проверками, которые могут быть использованы с context значение.

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

Также помните, что здесь могут быть не только подклассы, но и ловушки; Хотя это, возможно, редкий паттерн, ничто не мешает другому объекту зарегистрировать ваш объект для новых наблюдений KVO.

Для улучшения читабельности вы также можете обернуть это в макрос препроцессора, например:

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

Контекст KVO должен быть указателем на статическую переменную, как показывает этот гист. Как правило, я делаю следующее:

В верхней части моего файла ClassName.m У меня будет линия

static char ClassNameKVOContext = 0;

Когда я начинаю наблюдать aspect собственность на targetObject (пример TargetClass) У меня будет

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

где PFXKeyTargetClassAspect является NSString * определяется в TargetClass.m быть равным @"aspect" и объявил extern в TargetClass.h, (Конечно, PFX - это просто место для префикса, который вы используете в своем проекте.) Это дает мне преимущество автозаполнения и защищает меня от опечаток.

Когда я закончу наблюдать aspect на targetObject у меня будет

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

Чтобы избежать слишком большого отступа в моей реализации -observeValueForKeyPath:ofObject:change:context:Я люблю писать

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

Я думаю, что лучшим способом было бы реализовать это как документ Apple:

Адрес статической переменной с уникальным именем в вашем классе создает хороший контекст.

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

смотри документацию.

Другие вопросы по тегам