КВО
Каталог статей
Концепция КВО
KVO — это режим разработки. Его полное название — Key-Value Observing (режим наблюдателя). Это механизм разработки, предоставляемый в рамках Apple Fundation framework. Используя KVO, вы можете легко наблюдать за определенным атрибутом указанного объекта. Когда атрибут Когда происходит изменение, уведомите разработчика о соответствующем содержимом между старым значением и новым значением свойства
Шаги по использованию КВО
Зарегистрировать мониторинг КВО
Зарегистрируйте KVO через [addObserver:forKeyPath:options:context:]
метод, чтобы вы могли получать событие изменения атрибута keyPath;
observer
: Наблюдатель, объект, отслеживающий изменения свойств. Объект должен реализоватьobserveValueForKeyPath:ofObject:change:context:
метод.keyPath
: имя свойства для наблюдения. Будьте последовательны с именем объявления атрибута.options
: В методе обратного вызова принимается старое значение или новое значение атрибута наблюдаемого объекта, и механизм KVO настраивается на изменение времени и содержимого уведомления KVO.context
: передать любой тип объекта, который может быть получен в коде «обратного вызова получения сообщения», который является методом передачи значения в KVO.
Внедрение мониторинга КВО
[observeValueForKeyPath:ofObject:change:context:]
Осуществлять мониторинг КВО методами ;
keyPath
: свойство наблюдаемого объектаobject
: наблюдаемый объектchange
: Словарь, хранит связанные значения, возвращает новое значение и старое значение в соответствии с перечислением, переданным параметрами.context
: при регистрации наблюдателя значение, переданное контекстом
Удалить мониторинг КВО
Когда нет необходимости в мониторинге, используйте метод [removeObserver:forKeyPath:]
удаления монитора;
Базовое использование КВО
Мы отслеживаем цвет фона кнопки, нажимаем кнопку, чтобы изменить цвет фона кнопки, и когда цвет фона изменяется, печатаем цвет фона до и после изменения.
//KVO最基本的使用
self.view.backgroundColor = [UIColor whiteColor];
self.kvoButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.kvoButton.frame = CGRectMake(100, 100, 100, 100);
self.kvoButton.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.kvoButton];
[self.kvoButton addTarget:self action:@selector(press) forControlEvents:UIControlEventTouchUpInside];
//给所要监听的对象注册监听
[self.kvoButton addObserver:self forKeyPath:@"backgroundColor" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
- (void)press {
//改变被监听对象的值
[self.kvoButton setValue:[UIColor colorWithRed:arc4random() % 255 / 255.0 green:arc4random() % 255 / 255.0 blue:arc4random() % 250 / 250.0 alpha:1] forKey:@"backgroundColor"];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
//打印监听结果
if ([keyPath isEqual:@"backgroundColor"]) {
NSLog(@"old value is: %@", [change objectForKey:@"old"]);
NSLog(@"new value is: %@", [change objectForKey:@"new"]);
}
}
Нажимаем кнопку один раз:
Значение прохода КВО
Передача значения KVO также очень проста, можно понять, что мы следим за определенным свойством второго viewController, когда мы переходим к первому viewController, мы можем отслеживать изменение значения.
//FirstViewController
- (void)pressChuanZhi {
SecondViewController *secondViewController = [[SecondViewController alloc] init];
secondViewController.modalPresentationStyle = UIModalPresentationFullScreen;
//为试图中的属性注册一个监听事件
[secondViewController addObserver:self forKeyPath:@"content" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self presentViewController:secondViewController animated:YES completion:nil];
}
//当属性变化时会激发该监听方法
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqual:@"content"]) {
id value = [change objectForKey:@"new"];
self.chuanzhiLabel.text = value;
}
}
//SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor orangeColor];
self.backButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
self.backButton.frame = CGRectMake(100, 100, 100, 100);
self.backButton.backgroundColor = [UIColor blueColor];
[self.backButton addTarget:self action:@selector(pressBack) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:self.backButton];
self.textField = [[UITextField alloc] initWithFrame:CGRectMake(100, 250, 200, 50)];
self.textField.keyboardType = UIKeyboardTypeDefault;
self.textField.borderStyle = UITextBorderStyleRoundedRect;
[self.view addSubview:self.textField];
}
- (void)pressBack {
self.content = self.textField.text;
[self dismissViewControllerAnimated:YES completion:nil];
}
Способы запрета КВО
//返回NO禁止KVO
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"content"]) {
return NO;
} else {
return [super automaticallyNotifiesObserversForKey:key];
}
}
Меры предосторожности при использовании
- Вызов
[removeObserver:forKeyPath:]
нужно сделать до того, как наблюдатель исчезнет, иначе получитсяCrash
. - После вызова
addObserver
метода KVO не делает сильной ссылки на наблюдателя, поэтому нужно обратить внимание на жизненный цикл наблюдателя, иначе это приведет к освобождению наблюдателяCrash
. - В наблюдателе нужно реализовать
observeValueForKeyPath:ofObject:change:context:
метод, который будет вызываться при приходе события KVO, и будет результатом, если его не реализоватьCrash
. addObserver
Сумма KVOremoveObserver
является парной, если она повторится,remove
то вызоветNSRangeException
типCrash
, если забудется,remove
то снова получит callback KVO после освобождения наблюдателяCrash
.- При вызове KVO необходимо передать
keyPath
.ПосколькуkeyPath
он имеет форму строки, легко сделать так, чтобы строка не менялась после изменения соответствующего атрибутаCrash
. Мы можем использовать системный механизм отражения дляkeyPath
его отражения, чтобы компилятор мог@selector()
выполнять в нем проверки легальности.
Принцип КВО
Прежде чем анализировать внутреннюю реализацию KVO, давайте проанализируем структуру хранения KVO, которая в основном использует следующие классы:
GSKVOInfo
GSKVOPathInfo
GSKVOObservation
ГСКВОИнфо
KVO — это неформальный протокол, реализованный на основе категории NSObject, поэтому все классы, унаследованные от NSObject, могут использовать KVO, а всю информацию KVO, связанную с объектом, можно получить с помощью метода — (void*)ObservationInfo, а возвращаемое значение — GSKVOInfo. Исходный код выглядит следующим образом
@interface GSKVOInfo : NSObject
{
NSObject *instance; // Not retained.
GSLazyRecursiveLock *iLock;
NSMapTable *paths;
}
- Он сохраняет экземпляр объекта.Ключевым моментом является
Not retained
то, что не удерживаетсяweak
, поэтому при его освобождении произойдет сбой вызова, и все наблюдатели должны быть удалены до того, как объект будет уничтожен. paths
Используется для хранения сопоставленияkeyPath
fromGSKVOPathInfo
to :
GSKVOPathInfo
@interface GSKVOPathInfo : NSObject
{
@public
unsigned recursion;
unsigned allOptions;
NSMutableArray *observations;
NSMutableDictionary *change;
}
- Он содержит
keypath
всех наблюдателей, соответствующих observations
содержит всех наблюдателей (GSKVOObservation
тип)allOptions
Сохраните коллекцию опций наблюдателяchange
Сохраните контент, который будет доставлен при срабатывании KVO.
ГСКВОНаблюдение
@interface GSKVOObservation : NSObject
{
@public
NSObject *observer; // Not retained (zeroing weak pointer)
void *context;
int options;
}
@end
Он содержит всю информацию для одного наблюдения
observer
сохранить заметку наблюдателя и здесьNot retained
context options
Оба параметра передаются при добавлении наблюдателей.
KVO реализуется с помощью технологии isa-swizzling (это предложение находится в центре внимания всей реализации KVO). Создайте промежуточный класс на основе исходного класса во время выполнения, этот промежуточный класс является подклассом исходного класса, и динамически измените isa текущего объекта, чтобы он указывал на промежуточный класс. И перепишите метод класса, чтобы он возвращал класс исходного класса. Поэтому Apple предлагает не полагаться на указатель isa при разработке, а использовать метод экземпляра класса для получения типа объекта.
Зачем переписывать метод класса?
Если нет переписывающего class
метода, когда объект вызывает class
метод, он будет искать метод в своем собственном списке кеша методов, списке методов, кеше родительского класса и списке методов, потому что class
метод является NSObject
методом в методе, если он не переписывается, возможно, в конце концов он вернет NSKVONotifying_Apple
, и класс будет выставлен.
Для реализации технологии isa-swizzling она в основном реализуется через следующие классы:
GSKVOReplacement
GSKVOBase
GSKVOSetter
ГСКВОЗамена
@interface GSKVOReplacement : NSObject
{
Class original; /* The original class */
Class replacement; /* The replacement class */
NSMutableSet *keys; /* The observed setter keys */
}
- (id) initWithClass: (Class)aClass;
- (void) overrideSetterFor: (NSString*)aKey;
- (Class) replacement;
@end
// 创建
- (id) initWithClass: (Class)aClass
{
NSValue *template;
NSString *superName;
NSString *name;
original = aClass;
/*
* Create subclass of the original, and override some methods
* with implementations from our abstract base class.
*/
superName = NSStringFromClass(original); // original == Temp
name = [@"GSKVO" stringByAppendingString: superName]; // name = GSKVOTemp
template = GSObjCMakeClass(name, superName, nil); // template = GSKVOTemp
GSObjCAddClasses([NSArray arrayWithObject: template]);
replacement = NSClassFromString(name);
GSObjCAddClassBehavior(replacement, baseClass);
/* Create the set of setter methods overridden.
*/
keys = [NSMutableSet new];
return self;
}
- Этот класс сохраняет исходную информацию о классе наблюдаемого объекта.
original
- Создайте подкласс исходного класса и назовите его
GSKVO<原类名>
, система iOS использует правила именования дляNSKVONotifying_
исходного имени класса. - Скопируйте
GSKVOBase
метод из класса в новый класс - Продолжение пройдет
object_setClass
, и наблюдаемый объект будетisa
указывать на этот новый класс, то есть наisa-swizzling
технологию, и isa сохранит информацию о классе, то есть наблюдаемый объект станет экземпляром нового класса, и это новый класс используется для реализации механизма уведомления KVO
ГСКВОБаза
Этот класс предоставляет несколько методов по умолчанию, все из которых являются переписанными методами NSObject, и, как указано выше, эти методы должны быть скопированы во вновь созданный замещающий класс. То есть наблюдаемый объект будет иметь реализацию этих методов
- (void) dealloc
{
// Turn off KVO for self ... then call the real dealloc implementation.
[self setObservationInfo: nil];
object_setClass(self, [self class]);
[self dealloc];
GSNOSUPERDEALLOC;
}
После освобождения объекта удалите данные KVO и верните объект обратно в исходный класс.
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}
Этот метод используется для сокрытия информации замещающего класса, а информация, полученная прикладным уровнем, по-прежнему является информацией исходного класса, поэтому Apple рекомендует при разработке не полагаться на указатель isa, а использовать класс метод экземпляра для получения типа объекта.
- (Class) superclass
{
return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}
Этот метод аналогичен методу класса
- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);
imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];
if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}
Этот метод принадлежит KVC. Перепишите этот метод, чтобы добавить [self willChangeValueForKey: aKey]
и до и после исходного вызова KVC класса [self didChangeValueForKey: aKey]
, и эти два метода являются ключом к запуску уведомлений KVO.
Таким образом, KVO основан на KVC, а KVC — это вход, запускаемый KVO.
ГСКВОБаза
@interface GSKVOSetter : NSObject
- (void) setter: (void*)val;
- (void) setterChar: (unsigned char)val;
- (void) setterDouble: (double)val;
- (void) setterFloat: (float)val;
- (void) setterInt: (unsigned int)val;
- (void) setterLong: (unsigned long)val;
#ifdef _C_LNG_LNG
- (void) setterLongLong: (unsigned long long)val;
#endif
- (void) setterShort: (unsigned short)val;
- (void) setterRange: (NSRange)val;
- (void) setterPoint: (NSPoint)val;
- (void) setterSize: (NSSize)val;
- (void) setterRect: (NSRect)rect;
@end
Этот класс имеет тот же принцип, что и переписанный выше метод KVC, и в будущем заменит реализацию метода keypath
наблюдателя setter
. добавит и setter
_[self willChangeValueForKey: aKey]
[self didChangeValueForKey: aKey]
краткое содержание
Итак, здесь у меня есть общее представление о реализации KVO.С помощью isa-swizzling
технологии замените наблюдаемую информацию о классе и перехватите наблюдаемый метод, добавьте и keyPath setter
до и после вызова исходного метода , чтобы выполнить функцию мониторинга изменений атрибутов.[self willChangeValueForKey: aKey]
[self didChangeValueForKey: aKey]
Реализация исходного кода
Далее просмотр всех процессов КВО из исходников
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOInfo *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;
setup();
[kvoLock lock];
// Use the original class
r = replacementForClass([self class]);
/*
* Get the existing observation information, creating it (and changing
* the receiver to start key-value-observing by switching its class)
* if necessary.
*/
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}
/*
* Now add the observer.
*/
dot = [aPath rangeOfString:@"."];
if (dot.location != NSNotFound)
{
forwarder = [[NSKeyValueObservationForwarder alloc]
initWithKeyPath: aPath
ofObject: self
withTarget: anObserver
context: aContext];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: forwarder];
}
else
{
[r overrideSetterFor: aPath];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: aContext];
}
[kvoLock unlock];
}
Запись KVO, добавляющая метод наблюдателя, в основном делает следующие вещи:
- 1.
replacementForClass
Создайте замещающий класс и добавьте его в глобальныйclassTable
для будущего использования. - 2. Получить данные прослушивателя объекта
GSKVOInfo
, если нет, создать новый - 3. Далее два случая
-
- Если используется точечный синтаксис ( ) , мониторинг подобъекта
self.keyPath.keyPath
будет создан с использованием рекурсии, и подобъект перенаправит отслеживаемые изменения на верхний уровень для последующего специального анализа.NSKeyValueObservationForwarder
- Если используется точечный синтаксис ( ) , мониторинг подобъекта
-
- По умолчанию (keyPath) напрямую прослушивает свойство объекта,
overrideSetterFor
метод будет вызываться, метод установки свойства ловушки и реализация метода установки будет заменена реализацией методаGSKVOSetter
в соответствующем типе параметра
- По умолчанию (keyPath) напрямую прослушивает свойство объекта,
- 4. Затем вызовите
[info addObserver: anObserver forKeyPath: aPath options: options context: aContext]
метод , чтобы сохранить новый слушатель.
Добавить метод в ГСКВОИнфо
- (void) addObserver: (NSObject*)anObserver
forKeyPath: (NSString*)aPath
options: (NSKeyValueObservingOptions)options
context: (void*)aContext
{
GSKVOPathInfo *pathInfo;
GSKVOObservation *observation;
unsigned count;
if ([anObserver respondsToSelector:
@selector(observeValueForKeyPath:ofObject:change:context:)] == NO)
{
return;
}
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo == nil)
{
pathInfo = [GSKVOPathInfo new];
// use immutable object for map key
aPath = [aPath copy];
NSMapInsert(paths, (void*)aPath, (void*)pathInfo);
[pathInfo release];
[aPath release];
}
observation = nil;
pathInfo->allOptions = 0;
count = [pathInfo->observations count];
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver)
{
o->context = aContext;
o->options = options;
observation = o;
}
pathInfo->allOptions |= o->options;
}
if (observation == nil)
{
observation = [GSKVOObservation new];
GSAssignZeroingWeakPointer((void**)&observation->observer,
(void*)anObserver);
observation->context = aContext;
observation->options = options;
[pathInfo->observations addObject: observation];
[observation release];
pathInfo->allOptions |= options;
}
if (options & NSKeyValueObservingOptionInitial)
{
/* If the NSKeyValueObservingOptionInitial option is set,
* we must send an immediate notification containing the
* existing value in the NSKeyValueChangeNewKey
*/
[pathInfo->change setObject: [NSNumber numberWithInt: 1]
forKey: NSKeyValueChangeKindKey];
if (options & NSKeyValueObservingOptionNew)
{
id value;
value = [instance valueForKeyPath: aPath];
if (value == nil)
{
value = null;
}
[pathInfo->change setObject: value
forKey: NSKeyValueChangeNewKey];
}
[anObserver observeValueForKeyPath: aPath
ofObject: instance
change: pathInfo->change
context: aContext];
}
[iLock unlock];
}
Этот метод в основном предназначен для сохранения информации о наблюдателе.
1. Запросить соответствующий GSKVOPathInfo
— GSKVOObservation
если есть, обновить, если нет — создать новый и сохранить
2. Если options
есть NSKeyValueObservingOptionInitial
— вызвать сразу [anObserver observeValueForKeyPath: aPath ofObject: instance change: pathInfo->change context: aContext]
, отправить сообщение наблюдателю
2. Получить текущее значение через KVC [instance valueForKeyPath: aPath]
, получить
willChangeValueForKey
didChangeValueForKey
Эти два метода добавляются до setter
и KVC
после присваивания для сохранения изменений в значениях свойств и отправки сообщений наблюдателям
willChangeValueForKey :
Основная запись заключаетсяoldValue
в сохраненииpathInfo->change
в , еслиoptions
она включенаNSKeyValueObservingOptionPrior
, она будет проходить по всем наблюдателям, и немедленно отправлять сообщение наблюдателю,
NSKeyValueObservingOptionPrior
указывающее, что они будут получать уведомления до и после изменения значения атрибута.didChangeValueForKey
Сохраняйте старые и новые значения атрибутов по опциям, обходите всех наблюдателей и отправляйте сообщения
удалить наблюдателя
/*
* removes the observer
*/
- (void) removeObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath
{
GSKVOPathInfo *pathInfo;
[iLock lock];
pathInfo = (GSKVOPathInfo*)NSMapGet(paths, (void*)aPath);
if (pathInfo != nil)
{
unsigned count = [pathInfo->observations count];
pathInfo->allOptions = 0;
while (count-- > 0)
{
GSKVOObservation *o;
o = [pathInfo->observations objectAtIndex: count];
if (o->observer == anObserver || o->observer == nil)
{
[pathInfo->observations removeObjectAtIndex: count];
if ([pathInfo->observations count] == 0)
{
NSMapRemove(paths, (void*)aPath);
}
}
else
{
pathInfo->allOptions |= o->options;
}
}
}
[iLock unlock];
}
Этот метод в основном используется для удаления наблюдателя соответствующего keyPath.Реализация метода очень проста.Его можно запросить и удалить в соответствии с переданными параметрами anObserver
и aPath
в структуре данных, введенной ранее.
Подведем итог
- Он в основном используется
isa-swizzling
для изменения информации о классе наблюдателя, аsetter
метод ловушкиsetter
отправляет сообщение всем наблюдателям при вызове метода. - Из приведенного выше исходного кода видно, что ссылки на наблюдатель и наблюдаемое не сохраняются, поэтому наблюдатель должен быть удален до освобождения объекта.
- Отправка сообщения в основном инициируется и должна отображаться парами.
[self willChangeValueForKey: key]
Этот метод используется для управления добавлением двух вышеуказанных методов. Возвращаемое значение по умолчанию — YES. Если он возвращает NO, он не будет добавлен автоматически, что то есть вызов сеттера и модификаций KVC не вызовет уведомления[self didChangeValueForKey: key]
automaticallyNotifiesObserversForKey
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
Этот метод используется для установки зависимостей, иногда требуется, чтобы значение определенного свойства менялось при изменении других свойств того же объекта. Заранее зарегистрировав такую зависимость в классе, даже если значение атрибута изменится косвенно, будет отправлено сообщение с уведомлением, а внутренняя реализация всех коллекций ключей, которые класс-наблюдатель переписывает для возврата и от которых зависит ключ, тоже относительно просто.Все
зависимости хранятся в глобалеdependentKeyTable
, а потом перехватывают все зависимыеkey
методы ,setter
при вызове будут найдены все зависимости, а потом будет отправлено сообщение[self willChangeValueForKey: key]
[self didChangeValueForKey: key]
- KVC много раз использовался внутри KVO.
-
- переписать
setValue:forKey
- переписать
-
- Используя
valueForKey --- valueForKeyPath
получение значения атрибута, особенно при использовании точечного синтаксиса, можноvalueForKeyPath
получить только глубокое значение атрибута.
Итак, KVO реализован на базе KVC.
- Используя