NSProxy и наблюдение значения ключа - PullRequest
14 голосов
/ 29 января 2012

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

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

Приведенный выше код будет прозрачно передавать любой вызов метода цели, которую представляет прокси.Тем не менее, он, кажется, не обрабатывает наблюдения и уведомления КВО на цели.Я пытался использовать подкласс NSProxy как стоящий для объектов, которые должны быть переданы в NSTableView, но я получаю следующую ошибку.

Cannot update for observer <NSAutounbinderObservance 0x105889dd0> for
 the key path "objectValue.status" from <NSTableCellView 0x105886a80>,
 most likely because the value for the key "objectValue" has changed
 without an appropriate KVO notification being sent. Check the 
KVO-compliance of the NSTableCellView class.

Есть ли способ сделать прозрачным NSProxy, который соответствует KVO?

Ответы [ 2 ]

20 голосов
/ 03 января 2013

Суть проблемы заключается в том, что внутренности наблюдения ключевых значений живут в NSObject, а NSProxy не наследуется от NSObject. Я достаточно уверен, что любой подход потребует, чтобы объект NSProxy сохранял свой собственный список соблюдений (то есть то, что сторонние люди надеются наблюдать за ним). Одно это добавило бы значительный вес вашей реализации NSProxy.

Наблюдать за целью

Похоже, вы уже пытались, чтобы наблюдатели прокси-сервера действительно наблюдали за реальным объектом - другими словами, если цель всегда была заполнена, и вы просто перенаправили все вызовы на цель, вы также перенаправили бы addObserver:... и removeObserver:... звонки. Проблема в том, что вы начали с того, что сказали:

NSProxy, кажется, очень хорошо работает в качестве резервных объектов для тех, кто еще не существует

Для полноты картины я опишу некоторые смелости этого подхода и почему он не может работать (по крайней мере, для общего случая):

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

  1. Цель была nil при инициализации, становится не- nil позже
  2. Цель была установлена ​​не nil, становится nil позже
  3. Цель была установлена ​​не nil, затем изменяется на другое не nil значение
  4. Цель была nil (не в init), становится не- nil позже

... и мы сразу же столкнемся с проблемами в случае № 1. Мы, вероятно, были бы в порядке, если бы наблюдатель KVO наблюдал только objectValue (так как это всегда будет ваш прокси), но сказал бы, что наблюдатель наблюдал keyPath, который проходит через ваш прокси / реальный объект, скажем objectValue.status. Это означает, что механизм KVO вызовет valueForKey: objectValue для цели наблюдения и вернет ваш прокси, затем он вызовет valueForKey: status для вашего прокси и вернет nil назад. Когда цель становится не nil, KVO посчитает, что это значение изменилось из-под нее (т.е. не соответствует KVO), и вы получите сообщение об ошибке, которое вы цитировали. Если у вас был способ временно заставить цель вернуть nil для status, вы можете включить это поведение, вызвать -[target willChangeValueForKey: status], выключить поведение, а затем вызвать -[target didChangeValueForKey: status]. В любом случае, мы можем остановиться здесь на первом случае, потому что они имеют одинаковые ловушки:

  1. nil не будет ничего делать, если вы позвоните по этому номеру willChangeValueForKey: (т. Е. Механизм KVO никогда не узнает об обновлении своего внутреннего состояния во время перехода к или из nil)
  2. заставляет любой целевой объект иметь механизм, посредством которого он временно будет лежать и возвращать nil из valueForKey: для всех ключей это кажется довольно обременительным требованием, когда заявленное желание было «прозрачным прокси».
  3. что вообще означает вызывать setValue: forKey: на прокси с целью nil? мы сохраняем эти ценности? в ожидании настоящей цели? мы бросаем? Огромный открытый номер.

Одной из возможных модификаций этого подхода будет использование суррогатной цели, когда реальной целью является nil, возможно, пустой NSMutableDictionary, и перенаправление вызовов KVC / KVO суррогату. Это решило бы проблему невозможности осознанно вызвать willChangeValueForKey: на nil. С учетом всего вышесказанного, учитывая, что вы сохранили свой список наблюдений, я не уверен, что KVO допустит следующую последовательность действий, которая может быть использована для установки цели в случае № 1:

  1. внешние наблюдатели звонят -[proxy addObserver:...], прокси пересылает суррогатному словарю
  2. прокси-вызовы -[surrogate willChangeValueForKey:], поскольку цель устанавливается
  3. прокси звонки -[surrogate removeObserver:...] на суррогатном
  4. прокси звонки -[newTarget addObserver:...] на новую цель
  5. прокси звонки -[newTarget didChangeValueForKey:] для баланса звонка # 2

Мне не ясно, что это также не приведет к той же ошибке. Весь этот подход действительно превращается в горячий беспорядок, не так ли?

У меня было несколько альтернативных идей, но № 1 довольно тривиален, а № 2 и № 3 недостаточно просты или внушают доверие, чтобы заставить меня захотеть потратить время на их кодирование. Но, для потомков, как насчет:

1. Используйте NSObjectController для вашего прокси

Конечно, он заполняет ваши keyPaths дополнительным ключом, чтобы пройти через контроллер, но это своего рода NSObjectController's полная причина существования, верно? Он может иметь содержимое nil и обрабатывать все настройки наблюдения и удаления. Он не достигает цели прозрачного прокси-сервера переадресации вызовов, но, например, если цель состоит в том, чтобы иметь замену для некоторого асинхронно сгенерированного объекта, вероятно, было бы довольно просто, чтобы операция асинхронной генерации доставила окончательную возражать против контроллера. Это, вероятно, подход с наименьшими усилиями, но на самом деле он не отвечает требованию «прозрачности».

2. Используйте NSObject подкласс для вашего прокси

NSProxy's главная особенность не в том, что в есть какая-то магия - основная особенность в том, что не имеет (все) реализации NSObject в этом. Если вы готовы приложить все усилия, чтобы переопределить все NSObject поведения, которые вы не хотите, и перенаправить их обратно в механизм пересылки, вы можете получить ту же чистую стоимость при условии на NSProxy, но с оставленным на месте механизмом поддержки КВО. Оттуда ваш прокси-сервер следит за всеми теми же ключевыми путями на цели, которые были обнаружены на ней, а затем ретранслирует уведомления willChange... и didChange... от цели, чтобы внешние наблюдатели видели их как исходящие от вашего прокси.

... а теперь что-то действительно сумасшедшее:

3. (Ab) Используйте среду выполнения, чтобы привести поведение NSObject KVC / KVO в ваш NSProxy подкласс

Вы можете использовать среду выполнения для получения реализаций методов, связанных с KVC и KVO, из NSObject (т.е. class_getMethodImplementation([NSObject class], @selector(addObserver:...))), а затем вы можете добавить эти методы (т.е. class_addMethod([MyProxy class], @selector(addObserver:...), imp, types)) в свой подкласс прокси.

Это, вероятно, приведет к процессу предположения и проверки, позволяющему выяснить все частные / внутренние методы в NSObject, которые вызываются общедоступными методами KVO, и затем добавить их в список методов, которые вы продаете оптом. Кажется логичным предположить, что внутренние структуры данных, которые поддерживают соблюдение KVO, не будут поддерживаться в иварах NSObject (NSObject.h указывает на отсутствие иваров - не то, что это что-то значит в наши дни), поскольку это будет означать, что каждый NSObject Экземпляр будет платить за место. Кроме того, я вижу много функций C в следах стека уведомлений KVO. Я думаю, что вы, вероятно, могли бы достичь точки, когда вы внесли достаточно функциональности для NSProxy, чтобы стать первоклассным участником KVO. С этого момента это решение выглядит как решение на основе NSObject; Вы наблюдаете за целью и ретранслируете уведомления, как если бы они пришли от вас, дополнительно подделывая уведомления willChange / didChange вокруг любых изменений цели. Вы можете даже быть в состоянии автоматизировать некоторые из них в своем механизме переадресации вызовов, установив флаг при вводе любого из вызовов открытого API KVO, а затем попытавшись перенести все методы, вызываемые вами, до тех пор, пока вы не очистите флаг при возврате публичного вызова API - заминка будет пытаться гарантировать, что использование этих методов не нарушит прозрачность вашего прокси.

Там, где я подозреваю, это упадет в механизме, посредством которого KVO создает динамические подклассы вашего класса во время выполнения.Детали этого механизма непрозрачны и, вероятно, приведут к еще одной длинной череде выяснения частных / внутренних методов для ввода из NSObject.В конце концов, этот подход также совершенно хрупок, чтобы не измениться какая-либо внутренняя деталь реализации.

... в заключение

В резюме проблема сводится к тому, что КВОожидает связное, узнаваемое, постоянно обновляемое (посредством уведомлений) состояние по всему ключевому пространству.(Добавьте «mutable» в этот список, если вы хотите поддерживать -setValue:forKey: или редактируемые привязки.) Запретить грязные приемы, будучи участником первого класса, означает быть NSObjects.Если один из этих шагов в цепочке реализует свою функциональность, обращаясь к какому-либо другому внутреннему состоянию, это его прерогатива, но он будет нести ответственность за выполнение всех своих обязательств по соответствию KVO.

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

1 голос
/ 13 февраля 2014

У меня нет точно такого же сценария использования (без привязок) OP, но мой был похож: я создаю подкласс NSProxy, который представляет собой еще один объект, который фактически загружается с сервера.Во время загрузки другие объекты могут подписаться на прокси-сервер, и прокси-сервер перенаправит KVO, как только объект прибудет.

В прокси есть простое свойство NSArray, которое записывает всех наблюдателей.Пока реальный объект не загружен, прокси возвращает nil в valueForKey:.Когда приходит realObject, прокси вызывает addObserver:forKeyPath:options:context: для реального объекта и затем, через магию среды выполнения, просматривает все свойства realObject и делает это:

    id old = object_getIvar(realObject, backingVar);
    object_setIvar(realObject, backingVar, nil);
    [realObject willChangeValueForKey:propertyName];
    object_setIvar(realObject, backingVar, old);
    [realObject didChangeValueForKey:propertyName];

Это кажетсяна работу, по крайней мере, я еще не получил никаких ошибок соответствия KVO.Это имеет смысл, хотя сначала все свойства равны нулю, а затем они изменяются с нуля на фактическое значение.Это все, как сказал ipmcc в своем первом заявлении выше, так что этот пост - просто подтверждение!Обратите внимание, что второй суррогат, который он предложил, на самом деле не нужен, вам просто нужно следить за наблюдателями самостоятельно.

...