NSProxy и наблюдение значения ключа

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 ответа

Решение

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

Соблюдайте цель

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

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

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

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

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

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

  1. nil ничего не сделаю, если вы позвоните willChangeValueForKey: на нем (то есть механизм КВО никогда не будет знать, чтобы обновить свое внутреннее состояние во время перехода к или из nil)
  2. заставить любой целевой объект иметь механизм, посредством которого он будет временно лежать и возвращаться nil from 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 для вашего прокси

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

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

NSProxy's Главная особенность не в том, что в ней есть какая-то магия, а в том, что она не имеет (все) NSObject реализация в нем. Если вы готовы приложить усилия, чтобы переопределить все NSObject поведения, которые вам не нужны, и перенесите их обратно в механизм пересылки, вы можете получить ту же чистую стоимость, обеспечиваемую NSProxy но с механизмом поддержки KVO, оставленным на месте. Оттуда ваш прокси-сервер следит за теми же ключевыми путями на цели, которые были обнаружены на ней, а затем ретранслирует 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 вызывают, а затем добавляют их в список методов, которые вы продаете оптом. Кажется логичным предположить, что внутренние структуры данных, которые поддерживают соблюдение КВО, не будут поддерживаться в иварах NSObject (NSObject.h указывает на отсутствие ивара - не то, что это значит что-либо в наши дни), так как это будет означать, что каждый NSObject Экземпляр будет платить за место. Кроме того, я вижу много функций C в следах стека уведомлений KVO. Я думаю, что вы, вероятно, могли бы достичь точки, когда вы внесли достаточно функциональности для NSProxy, чтобы стать первоклассным участником KVO. С этого момента это решение выглядит как NSObject решение на основе; Вы наблюдаете за целью и ретранслируете уведомления, как если бы они пришли от вас, дополнительно подделывая уведомления willChange / didChange вокруг любых изменений в цели. Возможно, вы даже сможете автоматизировать некоторые из них в своем механизме переадресации вызовов, установив флаг при вводе любого из вызовов открытого API KVO, а затем попытавшись перенести все методы, вызываемые вами, до тех пор, пока не снимите флажок, когда открытый API возврат вызовов - заминка будет пытаться гарантировать, что использование этих методов не нарушит прозрачность вашего прокси.

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

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

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

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

У меня нет точно такого же варианта использования (без привязок) 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 в своем первом заявлении выше, так что этот пост - просто подтверждение! Обратите внимание, что второй суррогат, который он предложил, на самом деле не нужен, вам просто нужно следить за наблюдателями самостоятельно.

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