Суть проблемы заключается в том, что внутренности наблюдения ключевых значений живут в NSObject
, а NSProxy
не наследуется от NSObject
. Я достаточно уверен, что любой подход потребует, чтобы объект NSProxy
сохранял свой собственный список соблюдений (то есть то, что сторонние люди надеются наблюдать за ним). Одно это добавило бы значительный вес вашей реализации NSProxy.
Наблюдать за целью
Похоже, вы уже пытались, чтобы наблюдатели прокси-сервера действительно наблюдали за реальным объектом - другими словами, если цель всегда была заполнена, и вы просто перенаправили все вызовы на цель, вы также перенаправили бы addObserver:...
и removeObserver:...
звонки. Проблема в том, что вы начали с того, что сказали:
NSProxy, кажется, очень хорошо работает в качестве резервных объектов для тех, кто
еще не существует
Для полноты картины я опишу некоторые смелости этого подхода и почему он не может работать (по крайней мере, для общего случая):
Чтобы это работало, ваш подкласс NSProxy
должен был бы собирать вызовы методов регистрации, которые были вызваны до того, как цель была установлена, и затем передавать их цели, когда она будет установлена. Это быстро становится волосатым, если учесть, что вы также должны обрабатывать удаления; Вы не хотели бы добавлять наблюдение, которое впоследствии было удалено (поскольку объект наблюдения мог быть освобожден). Вы также, вероятно, не хотите, чтобы ваш метод отслеживания наблюдений удерживал кого-либо из наблюдателей, иначе это создаст непреднамеренные циклы сохранения. Я вижу следующие возможные переходы в целевом значении, которые необходимо обработать
- Цель была
nil
при инициализации, становится не- nil
позже
- Цель была установлена не
nil
, становится nil
позже
- Цель была установлена не
nil
, затем изменяется на другое не nil
значение
- Цель была
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]
. В любом случае, мы можем остановиться здесь на первом случае, потому что они имеют одинаковые ловушки:
nil
не будет ничего делать, если вы позвоните по этому номеру willChangeValueForKey:
(т. Е. Механизм KVO никогда не узнает об обновлении своего внутреннего состояния во время перехода к или из nil
)
- заставляет любой целевой объект иметь механизм, посредством которого он временно будет лежать и возвращать
nil
из valueForKey: для всех ключей это кажется довольно обременительным требованием, когда заявленное желание было «прозрачным прокси».
- что вообще означает вызывать setValue: forKey: на прокси с целью
nil
? мы сохраняем эти ценности? в ожидании настоящей цели? мы бросаем? Огромный открытый номер.
Одной из возможных модификаций этого подхода будет использование суррогатной цели, когда реальной целью является nil
, возможно, пустой NSMutableDictionary
, и перенаправление вызовов KVC / KVO суррогату. Это решило бы проблему невозможности осознанно вызвать willChangeValueForKey:
на nil
. С учетом всего вышесказанного, учитывая, что вы сохранили свой список наблюдений, я не уверен, что KVO допустит следующую последовательность действий, которая может быть использована для установки цели в случае № 1:
- внешние наблюдатели звонят
-[proxy addObserver:...]
, прокси пересылает суррогатному словарю - прокси-вызовы
-[surrogate willChangeValueForKey:
], поскольку цель устанавливается
- прокси звонки
-[surrogate removeObserver:...
] на суррогатном
- прокси звонки
-[newTarget addObserver:...]
на новую цель
- прокси звонки
-[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, но вряд ли он того стоит.