前言
学习KVO的过程,我分为了KVO的实现过程分析和内部结构的学习,学习了实现过程,接下来看KVO是通过何种内部结构实现如此通知📢和监听。
1 KVO的存储结构
KVO的实现过程离不开合理的存储结构,用到了如下几个类
GSKVOInfo
GSKVOPathInfo
GSKVOObervation
1.1 GSKVOInfo
KVO是基于NSObject类别实现的非正式协议,所以所有继承于NSObject的类都可以使用KVO,其中可以通过- (void*) observationInfo
方法获取对象相关联的所有KVO信息,而返回值就是GSKVOInfo
@interface GSKVOInfo : NSObject
{
NSObject *instance; // Not retained.
GSLazyRecursiveLock *iLock;
NSMapTable *paths;
}
- 它保存了一个对象的实例,重点
Not retained
由于没有持有,也不是weak,- 所以当释放之后,在调用会崩溃,需要在对象销毁前,移除所有观察者
paths 用于保存keyPath 到GSKVOPathInfo
的映射:
1.2 GSKVOPathInfo
GSKVOPathInfo 是一个自定义的用于管理KVO路径的类,它可以帮助我们在使用KVO时更加便捷地管理被观察对象的属性路径。
在iOS开发中,使用KVO机制进行观察时,我们可以通过注册被观察对象的属性路径来进行观察。属性路径由一个或多个属性名组成,用“.”连接起来表示属性之间的层次关系,例如 person.address.street 表示 person 对象的 address 属性的 street 子属性。
@interface GSKVOPathInfo : NSObject
{
@public
unsigned recursion;
unsigned allOptions;
NSMutableArray *observations;
NSMutableDictionary *change;
}
它保存了一个keypath对应的所有观察者
observations
保存了所有的观察者(GSKVOObservation
类型)allOptions
保存了观察者的options
集合change
保存了KVO触发要传递的内容
1.3 GSKVOObervation
在iOS开发中,使用KVO机制进行观察时,系统会自动为我们创建一个观察者对象,并在属性发生变化时调用该观察者的回调方法
它保存了单个观察者的所有信息
@interface GSKVOObservation : NSObject
{
@public
NSObject *observer; // Not retained (zeroing weak pointer)
void *context;
int options;
}
@end
observer
:观察者对象,即注册 KVO 观察时传入的观察者对象。(Not retained)keyPath
:被观察的属性名。context
:上下文信息,用于在回调方法中区分不同的 KVO 观察。
1.3 有什么关联
GSKVOInfo
、GSKVOObservation
和 GSKVOPathInfo
都是在 iOS 开发中使用 KVO 机制时可能会用到的自定义类或结构体,它们之间有一定的关联。
在 KVO 机制中,我们可以通过注册被观察对象的属性路径来进行观察。而 GSKVOPathInfo
类就是用于解析和管理这些属性路径的。每个 GSKVOPathInfo
对象代表一个属性路径。
在注册 KVO 观察时,系统会自动为我们创建一个观察者对象,并在属性发生变化时调用该观察者的回调方法。而在某些情况下,我们可能需要手动管理观察者对象,这时就可以使用 GSKVOObservation
结构体。每个 GSKVOObservation 对象代表一个 KVO 观察。
GSKVOInfo 类则是将 GSKVOPathInfo
和 GSKVOObservation
结合起来,用于管理一个对象的所有 KVO 观察。每个 GSKVOInfo 对象代表一个被观察对象,它包含一个 GSKVOPathInfo 对象列表,表示该对象所有被观察的属性路径,以及一个 GSKVOObservation 结构体列表,表示该对象所有的 KVO 观察。
因此,可以说 GSKVOPathInfo
、GSKVOObservation
和 GSKVOInfo
三者之间有一定的层级关系,它们共同构成了 KVO 机制的基本组成部分。
2 KVO如何实现属性变化产生通知?
KVO是通过isa-swizzling
技术实现的(这句话是整个KVO实现的重点)。在运行时根据原类创建一个中间类,这个中间类是原类的子类,并动态修改当前对象的isa指向中间类。并且将class方法重写,返回原类的Class。所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
我的理解
在使用 KVO 机制时,我们可以通过调用 addObserver:forKeyPath:options:context: 方法来注册观察者,并在被观察对象的属性发生变化时,系统会自动调用观察者的回调方法。那么系统是如何实现这一机制的呢?
KVO 机制实际上是通过 isa-swizzling 技术来实现的。当我们注册一个观察者时,系统会生成一个新的子类,继承自被观察对象的类,并将被观察对象的 isa 指针指向这个新生成的子类。这个子类会重写被观察对象的 setter 方法,在 setter 方法中调用原有的 setter 方法,并触发 KVO 机制。也就是说,在设置属性值时,会先调用被观察对象的 setter 方法,然后再调用观察者的回调方法。
因为被观察对象的 isa 指针被修改了,所以这个被观察对象就不再是原有类的实例,而是新生成子类的实例,这个子类中重写了被观察属性的 setter 方法。这样,当属性值发生变化时,就会调用这个新生成子类中的 setter 方法,从而触发 KVO 机制。
2.1 实现isa-swizzling的结构
实现isa-swizzling技术需要用到以下类
GSKVOReplacement
GSKVOBase
GSKVOSetter
2.1.1 GSKVOReplacement
GSKVOReplacement
是 GSKVOObservation 中的一个重要结构体,用于存储在 KVO 机制中替换掉原有 setter 方法的新方法的实现。
GSKVOReplacement
结构体就是用来存储这个被替换掉的原有 setter 方法的实现和新的 setter 方法的实现的
@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.
创建一组被重写的setter方法。
*/
keys = [NSMutableSet new];
return self;
}
- 这个类保存了被观察对象原始类信息
original
- 创建一个原始类的子类 命名为GSKVO<原类名>,iOS系统使用命名规则为
NSKVONotifying_
原类名 - 拷贝GSKVOBase类中的方法到新类中
- 后续会通过
object_setClass
, 将被观察对象的isa指向这个新类,也就是isa-swizzling技术,而isa保存的是类的信息,也就是说被观察者对象就变成了新类的实例,这个新类用于实现KVO通知机制
2.1.2 GSKVOBase
这个类默认提供了几个方法,都是对NSObject方法的重写,而从上面得知,这些方法都要拷贝到新创建的替换类中。也就是被观察者会拥有这几个方法的实现
- (void) dealloc
对象释放后,移除KVO数据,将对象重新指向原始类
- (void) dealloc
{
// Turn off KVO for self ... then call the real dealloc implementation.
[self setObservationInfo: nil];
object_setClass(self, [self class]);
[self dealloc];
GSNOSUPERDEALLOC;
}
- (Class) class
此方法用来隐藏替换类信息,应用层获取类的信息,仍然是原始类的信息. 所以苹果建议在开发中不应该依赖isa指针,而是通过class实例方法来获取对象类型。
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}
- (Class) superclass
此方法和class方法原理相同
- (Class) superclass
{
return class_getSuperclass(class_getSuperclass(object_getClass(self)));
}
- (void) setValue: (id)anObject forKey: (NSString*)aKey
这个方法是属于KVC中的,重写这个方法,实现在原始类KVC调用前后添加[self willChangeValueForKey: aKey]和[self didChangeValueForKey: aKey],而这两个方法是触发KVO通知的关键。
所以说KVO是基于KVC的,而KVC正是KVO触发的入口。
- (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);
}
}
2.1.3 GSKVOSetter
@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]`
isa-swizzling小结
通过isa-swizzling技术, 替换被观察的类信息,并且hook被观察keyPath setter方法,在原始方法调用前后添加[self willChangeValueForKey: aKey]和[self didChangeValueForKey: aKey],从而达到监听属性变化的功能
3 KVO的实现过程源码
3.1 - (void) addObserver: (NSObject*)anObserver… 添加观察者.方法
- (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的入口,添加观察者方法,主要做了以下几件事
- replacementForClass,创建替换类,并且加入到全局classTable中,方便以后使用
获取对象的监听者数据GSKVOInfo,如果没有就创建新的
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}
- 接下来分为两种情况
-
- 1️⃣ 如果利用了点语法(self.keyPath.keyPath), 会利用NSKeyValueObservationForwarder递归创建子对象监听,子对象在将监听到的变化转发到上层,以后再具体分析
if (dot.location != NSNotFound)
{
forwarder = [[NSKeyValueObservationForwarder alloc]
initWithKeyPath: aPath
ofObject: self
withTarget: anObserver
context: aContext];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: forwarder];
}
-
- 2️⃣ 默认情况(keyPath)直接监听对象的某个属性,则会调用overrideSetterFor方法,hook属性的setter方法,将setter方法的实现替换为相应参数类型的GSKVOSetter中的方法实现
else
{
[r overrideSetterFor: aPath];
[info addObserver: anObserver
forKeyPath: aPath
options: options
context: aContext];
}
然后调用[info addObserver: anObserver forKeyPath: aPath options: options context: aContext];
方法,将新的监听保存起来。
3.2 GSKVOInfo 中的添加的方法
- (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];
}
此方法主要是保存观察者信息
- 查询相应的
GSKVOPathInfo --GSKVOObservation
如果有就更新,如果没有就创建新的并保存 - 如果options中包含
NSKeyValueObservingOptionInitial
,则立马调用[anObserver observeValueForKeyPath: aPath ofObject: instance change: pathInfo->change context: aContext];
发送消息给观察者 - 其中获取当前值通过KVC中的
[instance valueForKeyPath: aPath];
获取
3.3 willChangeValueForKey 和 didChangeValueForKey
这两个方法分别添加在setter和KVC 赋值前后,用来保存属性值的变化,以及发送消息给观察者
willChangeValueForKey
:
主要记录属性值的 oldValue保存到pathInfo->change中,如果options包含NSKeyValueObservingOptionPrior
,则会遍历所有观察者,立马发送消息给观察者NSKeyValueObservingOptionPrior
表示属性值修改前后都会收到通知didChangeValueForKey
根据options
保存属性的新旧值,遍历所有的观察者,发送消息
3.4 移除观察者
此方法主要用来移除相应keyPath的观察者,方法实现很简单,根据参数传入的anObserver和aPath在前面介绍的数据结构中查询并移除
/*
* 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];
}
3.5 NSKeyValueObservationForwarder
NSKeyValueObservationForwarder
是一个私有类,用于在 KVO 机制中实现观察者的转发。它主要用于管理观察者和被观察对象之间的关系,并在被观察对象的属性值发生变化时,通知观察者。
@interface NSKeyValueObservationForwarder : NSObject
{
id target;
NSKeyValueObservationForwarder *child;
void *contextToForward;
id observedObjectForUpdate;
NSString *keyForUpdate;
id observedObjectForForwarding;
NSString *keyForForwarding;
NSString *keyPathToForward;
}
NSKeyValueObservationForwarder
类的作用在 KVO 机制中是非常重要的。它实现了观察者和被观察对象之间的关系,保证了观察者可以及时地接收到属性值的变化通知,并且在被观察对象销毁时,可以自动移除所有的观察者,避免内存泄漏。
3.5.1 NSKeyValueObservationForwarder的转发机制
NSKeyValueObservationForwarder
类中的主要方法是 observeValueForKeyPath:ofObject:change:context:
和 dealloc。observeValueForKeyPath:ofObject:change:context:
方法用于在被观察对象的属性值发生变化时,通知所有的观察者。dealloc 方法用于在被观察对象销毁时,移除所有的观察者。
例如 对于keyPath的访问过程,按照key1.key2.key3的路径访问 NSKeyValueObservationForwarder会完成如下事情
其中观察者会依次相互引用
- (void) observeValueForKeyPath: (NSString *)keyPath
ofObject: (id)anObject
change: (NSDictionary *)change
context: (void *)context
{
if (anObject == observedObjectForUpdate)
{
[self keyPathChanged: nil];
}
else
{
[target observeValueForKeyPath: keyPathToForward
ofObject: observedObjectForUpdate
change: change
context: contextToForward];
}
}
4 KVO总结
- 主要用了isa-
swizzling
,修改了观察者的类信息,并且hooksetter方法,当setter方法调用时发送消息给所有观察者 - 观察者、被观察者的引用都是Not Retain, 所以对象释放前一定要移除观察者。
- 消息的发送主要由[self willChangeValueForKey: key], [self didChangeValueForKey: key]触发,并且必须成对出现,automaticallyNotifiesObserversForKey方法用来控制,是否要主要添加上述的两个方法,默认返回值为YES,如果返回NO则不会自动添加,也就是说setter的调用以及KVC修改都不会触发通知
- KVO内部多次用到了KVC
1️⃣ 重写setValue:forKey
2️⃣ 使用valueForKey --- valueForKeyPath
获取属性的值,尤其是在使用点语法的时候,只有valueForKeyPath可以获得深层次的属性值。
所以KVO是基于KVC而实现的。
5 KVO和KVC的联系
KVO 和 KVC 都是 Cocoa 框架中的重要机制,它们可以帮助开发者更加方便地操作对象的属性和状态。
KVO(Key-Value Observing)
是一种基于观察者模式的机制,用于监听对象属性的变化。当一个对象的属性发生变化时,系统会自动通知所有观察这个属性的对象,让它们能够及时地获取到最新的属性值。KVO 的实现依赖于 Objective-C 的动态特性,通过 isa-swizzling 技术实现。
KVO 的使用非常简单,只需要在被观察对象上调用 addObserver:forKeyPath:options:context:
方法,指定观察者对象和被观察的属性,就可以开始监听属性的变化了。当属性的值发生变化时,系统会自动调用观察者对象的 observeValueForKeyPath:ofObject:change:context:
方法,将变化的信息传递给观察者。
KVC(Key-Value Coding)
是一种通过键值来访问对象属性的机制。通过 KVC,开发者可以直接使用字符串来访问对象的属性,而不需要手动调用 getter 和 setter 方法。KVC 的实现依赖于 Objective-C 的 Runtime 特性,通过方法调用和消息转发机制实现。
KVC 的使用非常灵活,可以用于访问普通对象的属性、集合对象的元素、属性路径等。开发者只需要使用字符串指定要访问的属性或者属性路径,就可以获取或者设置对象的属性值了。KVC 还支持一些高级的特性,比如自动集合操作、键值验证、键值观察等。
KVO 和 KVC 都是 Cocoa 框架中非常重要的机制,它们可以帮助开发者更加方便地操作对象的属性和状态。KVO 主要用于监听对象属性的变化,而 KVC 主要用于通过键值来访问对象属性。在实际开发中,我们通常会同时使用 KVO 和 KVC,以便更好地处理对象的状态和变化
总结
参考
KVO的结构和源码内部实现
KVO KVC的八股文
KVC/KVO的实现
KVC学习了主要的实现过程,KVO的学习是先学会实现的过程,调用什么方法,熟悉过程。然后学习KVO的源码的内部实现才能看懂对应的源码过程。
KVO的学习比较KVC更为复杂,KVO的内部结构和源码更为复杂,2天的学习完全不够 还需要重写阅读博客一步一步在学习过程