[iOS] - переобучение КВО

КВО

Концепция КВО

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Сумма KVO removeObserverявляется парной, если она повторится, 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Используется для хранения сопоставления keyPathfrom GSKVOPathInfoto :

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в соответствующем типе параметра
  • 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. Запросить соответствующий GSKVOPathInfoGSKVOObservationесли есть, обновить, если нет — создать новый и сохранить
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.

Supongo que te gusta

Origin blog.csdn.net/m0_62386635/article/details/130383322
Recomendado
Clasificación