【iOS】—— KVO再学习

news/2024/11/30 11:31:18/

KVO

文章目录

  • KVO
      • KVO概念
      • KVO使用步骤
        • 注册KVO监听
        • KVO监听实现
        • 移除KVO监听
      • KVO基本用法
      • KVO传值
        • 禁止KVO的方法
        • 使用注意事项
    • KVO原理
      • GSKVOInfo
      • GSKVOPathInfo
      • GSKVOObservation
          • 为什么要重写class方法呢?
      • GSKVOReplacement
      • GSKVOBase
      • GSKVOBase
        • 小结
      • 源码实现
        • 移除观察者
        • 总结

KVO概念

KVO是一种开发模式,它的全称是Key-Value Observing (观察者模式) 是苹果Fundation框架下提供的一种开发机制,使用KVO,可以方便地对指定对象的某个属性进行观察,当属性发生变化时,进行通知,告诉开发者属性旧值和新值对应的内容

KVO使用步骤

注册KVO监听

通过[addObserver:forKeyPath:options:context:]方法注册KVO,这样可以接收到keyPath属性的变化事件;

  • observer:观察者,监听属性变化的对象。该对象必须实现observeValueForKeyPath:ofObject:change:context: 方法。
  • keyPath:要观察的属性名称。要和属性声明的名称一致。
  • options:回调方法中收到被观察者的属性的旧值或新值等,对KVO机制进行配置,修改KVO通知的时机以及通知的内容
  • context:传入任意类型的对象,在"接收消息回调"的代码中可以接收到这个对象,是KVO中的一种传值方式。

KVO监听实现

通过方法[observeValueForKeyPath:ofObject:change:context:]实现KVO的监听;

  • keyPath:被观察对象的属性
  • object:被观察的对象
  • change:字典,存放相关的值,根据options传入的枚举来返回新值旧值
  • context:注册观察者的时候,context传递过来的值

移除KVO监听

在不需要监听的时候,通过方法[removeObserver:forKeyPath:],移除监听;

KVO基本用法

我们对这个Button的背景色做监听,点击button改变按钮的背景色,当背景色改变的时候,打印改变前和改变后的背景色。

    //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"]);}
}

我们点击一次button:
在这里插入图片描述

KVO传值

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];
}

请添加图片描述
请添加图片描述
请添加图片描述

禁止KVO的方法

//返回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
  • KVO的addObserverremoveObserver需要是成对的,如果重复remove则会导致NSRangeException类型的Crash,如果忘记remove则会在观察者释放后再次接收到KVO回调时Crash
  • 在调用KVO时需要传入一个keyPath,由于keyPath是字符串的形式,所以其对应的属性发生改变后,字符串没有改变容易导致Crash。我们可以利用系统的反射机制将keyPath反射出来,这样编译器可以在@selector()中进行合法性检查。

KVO原理

在分析KVO的内部实现之前,先来分析一下KVO的存储结构,主要用到了以下几个类:
GSKVOInfo
GSKVOPathInfo
GSKVOObservation

GSKVOInfo

KVO是基于NSObject类别实现的非正式协议,所以所有继承于NSObject的类都可以使用KVO,其中可以通过- (void*) observationInfo方法获取对象相关联的所有KVO信息,而返回值就是GSKVOInfo,源码如下

@interface  GSKVOInfo : NSObject
{NSObject          *instance;  // Not retained.GSLazyRecursiveLock           *iLock;NSMapTable            *paths;
}
  • 它保存了一个对象的实例,重点 Not retained 由于没有持有,也不是weak,所以当释放之后,在调用会崩溃,需要在对象销毁前,移除所有观察者
  • paths 用于保存keyPathGSKVOPathInfo 的映射:

GSKVOPathInfo

@interface  GSKVOPathInfo : NSObject
{
@publicunsigned              recursion;unsigned              allOptions;NSMutableArray        *observations;NSMutableDictionary   *change;
}
  • 它保存了一个keypath对应的所有观察者
  • observations 保存了所有的观察者(GSKVOObservation 类型)
  • allOptions 保存了观察者的options集合
  • change 保存了KVO触发要传递的内容

GSKVOObservation

@interface  GSKVOObservation : NSObject
{
@publicNSObject      *observer;      // Not retained (zeroing weak pointer)void          *context;int           options;
}
@end

它保存了单个观察的所有信息

  • observer保存观察者 注意这里也是 Not retained
  • context options 都是添加观察者时传入的参数

请添加图片描述

KVO是通过isa-swizzling技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

为什么要重写class方法呢?

如果没有重写class方法,当该对象调用class方法时,会在自己的方法缓存列表,方法列表,父类缓存,方法列表一直向上去查找该方法,因为class方法是NSObject中的方法,如果不重写最终可能会返回NSKVONotifying_Apple,就会将该类暴露出来。

要实现isa-swizzling技术,主要通过以下几个类实现:
GSKVOReplacement
GSKVOBase
GSKVOSetter

GSKVOReplacement

@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 == Tempname = [@"GSKVO" stringByAppendingString: superName];    // name = GSKVOTemptemplate = GSObjCMakeClass(name, superName, nil);   // template = GSKVOTempGSObjCAddClasses([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通知机制

GSKVOBase

这个类默认提供了几个方法,都是对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));
}

此方法用来隐藏替换类信息,应用层获取类的信息,仍然是原始类的信息. 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。

- (Class) superclass
{return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}

此方法和class方法原理相同

- (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中的,重写这个方法,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey],而这两个方法是触发KVO通知的关键。
所以说KVO是基于KVC的,而KVC正是KVO触发的入口。

GSKVOBase

@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方法原理相同,将来会替换被观察者keypathsetter方法实现。会在原始setter方法前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey]

小结

那么到这里,就对KVO的实现有个大致的了解,通过isa-swizzling技术, 替换被观察的类信息,并且hook被观察keyPath setter方法,在原始方法调用前后添加[self willChangeValueForKey: aKey][self didChangeValueForKey: aKey],从而达到监听属性变化的功能

源码实现

接下来从源码中查看KVO的所有流程

- (void) addObserver: (NSObject*)anObserverforKeyPath: (NSString*)aPathoptions: (NSKeyValueObservingOptions)optionscontext: (void*)aContext
{GSKVOInfo             *info;GSKVOReplacement      *r;NSKeyValueObservationForwarder *forwarder;NSRange               dot;setup();[kvoLock lock];// Use the original classr = 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: aPathofObject: selfwithTarget: anObservercontext: aContext];[info addObserver: anObserverforKeyPath: aPathoptions: optionscontext: forwarder];}else{[r overrideSetterFor: aPath];[info addObserver: anObserverforKeyPath: aPathoptions: optionscontext: aContext];}[kvoLock unlock];
}

KVO的入口,添加观察者方法,主要做了以下几件事:

  • 1.replacementForClass,创建替换类,并且加入到全局classTable中,方便以后使用
  • 2.获取对象的监听者数据GSKVOInfo,如果没有就创建新的
  • 3.接下来分为两种情况
    • 如果利用了点语法(self.keyPath.keyPath), 会利用NSKeyValueObservationForwarder递归创建子对象监听,子对象在将监听到的变化转发到上层,以后再具体分析
    • 默认情况(keyPath)直接监听对象的某个属性,则会调用overrideSetterFor方法,hook属性的setter方法,将setter方法的实现替换为相应参数类型的GSKVOSetter中的方法实现
  • 4.然后调用[info addObserver: anObserver forKeyPath: aPath options: options context: aContext];方法,将新的监听保存起来。

GSKVOInfo 中的添加方法

- (void) addObserver: (NSObject*)anObserverforKeyPath: (NSString*)aPathoptions: (NSKeyValueObservingOptions)optionscontext: (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 keyaPath = [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: valueforKey: NSKeyValueChangeNewKey];}[anObserver observeValueForKeyPath: aPathofObject: instancechange: pathInfo->changecontext: 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

这两个方法分别添加在setterKVC 赋值前后,用来保存属性值的变化,以及发送消息给观察者

  • willChangeValueForKey :
    主要记录属性值的 oldValue保存到pathInfo->change中,如果options包含NSKeyValueObservingOptionPrior,则会遍历所有观察者,立马发送消息给观察者
    NSKeyValueObservingOptionPrior 表示属性值修改前后都会收到通知
  • didChangeValueForKey
    根据options保存属性的新旧值,遍历所有的观察者,发送消息

移除观察者

/** 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的观察者,方法实现很简单,根据参数传入的anObserveraPath在前面介绍的数据结构中查询并移除

总结

  • 主要用了isa-swizzling,修改了观察者的类信息,并且hooksetter方法,当setter方法调用时发送消息给所有观察者
  • 由上面源码可以看出对观察者、被观察者的引用都是Not Retain, 所以对象释放前一定要移除观察者。
  • 消息的发送主要由[self willChangeValueForKey: key], [self didChangeValueForKey: key]触发,并且必须成对出现,automaticallyNotifiesObserversForKey方法用来控制,是否要主要添加上述的两个方法,默认返回值为YES,如果返回NO则不会自动添加,也就是说setter的调用以及KVC修改都不会触发通知
  • + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
    此方法用来设置依赖关系,有时候需要某属性值随着同一对象的其他属性的改变而改变。可以通过事先将这样的依赖关系在类中注册,那么即便属性值间接地发生了改变,也会发送通知消息,被观察者类重写返回和key依赖的所有key集合
    内部实现也比较简单,将所有依赖关系存储在全局的dependentKeyTable中,然后hook了所有依赖的keysetter方法,当[self willChangeValueForKey: key], [self didChangeValueForKey: key]调用时会查找所有的依赖关系,然后发送消息
  • KVO内部多次用到了KVC
    • 重写 setValue:forKey
    • 使用valueForKey --- valueForKeyPath获取属性的值,尤其是在使用点语法的时候,只有valueForKeyPath可以获得深层次的属性值。
      所以KVO是基于KVC而实现的。

http://www.ppmy.cn/news/59000.html

相关文章

5月新书预告

“读书不觉已春深&#xff0c;一寸光阴一寸金。”相信许多小伙伴儿都把这个五一假期安排的满满当当&#xff0c;还有一部分人抱着书本养精蓄锐、精进技能。小编也没闲着&#xff0c;为大家收集了几本精品新书。 《现代软件工程》是《持续交付》的作者David Farley的另一本力作&…

23.5.4总结

TitledPane 展开按键 DatePicker 日期 Pagination 分页控件 答辩总结&#xff1a; 1、数据库的修改&#xff1a; 将不必要的名称删除&#xff0c;增加外键&#xff1b; 修改的数据库&#xff08;courses表、classes表、studycourse表&#xff09; studycourse表加course…

干货 | 正念,寻求属于你的存在之道

Hello,大家好&#xff01; 这里是壹脑云科研圈&#xff0c;我是喵君姐姐~ 你是否也曾感到内心无法平静&#xff1f;如果是&#xff0c;不妨了解一下正念&#xff0c;它或许能为你带来改变。 正念作为一种古老的修行方式&#xff0c;如今已经在世界范围内广为流传&#xff0c;…

【五一创作】力扣刷题实录(大厂用题)—— 1. 打家劫舍

1. 打家劫舍 某大厂 2022 年 9 月面试题挑战&#xff08;三&#xff09; 1.1 题目描述 力扣真题地址&#xff1a;https://leetcode.cn/problems/house-robber/?envTypestudy-plan-v2 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;…

ChatGPT被淘汰了?Auto-GPT到底有多强

大家好&#xff0c;我是可夫小子&#xff0c;关注AIGC、读书和自媒体。解锁更多ChatGPT、AI绘画玩法。 说Auto-GPT淘汰了ChatGPT了&#xff0c;显然是营销文案里面的标题党。毕竟它还是基于ChatGPT的API&#xff0c;某种意义只是基于ChatGPT能力的应用。但最近&#xff0c;Auto…

Java基础——TCP通信

&#xff08;1&#xff09;TCP协议特点&#xff1a; TCP是一种面向连接&#xff0c;安全&#xff0c;可靠的传输数据的协议传输前&#xff0c;采用“三次握手”方式&#xff0c;点对点通信&#xff0c;是可靠的在连接中可进行大数据量的传输 &#xff08;2&#xff09; TCP通…

想保护你的网站?用Python来生成验证码图片

前言 随着互联网的发展&#xff0c;我们越来越多地依赖于网站和应用程序&#xff0c;而这些网站和应用程序也面临着各种各样的安全威胁&#xff0c;其中之一就是用户可能会通过脚本攻击你的网站。为了缓解这些安全风险&#xff0c;一个常见的做法是在用户进行操作时&#xff0…

音视频 FFmpeg

文章目录 前言视频编解码硬件解码(高级)软解码(低级)软、硬解码对比视频解码有四个步骤Android 系统中编解码器的命名方式查看当前设备支持的硬解码 基础知识RGB色彩空间常见的格式对比YUV索引格式分离RGB24像素数据中的R、G、B分量 BMP 文件格式格式组成像素排列顺序RGB24格式…