1. KVO 的原理
对于下面使用KVO的例子,仅仅调用一行代码,便实现了实现 KVO 机制。
//// 对Person类的调用
- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[Person alloc] init];
self.person1.age = 1;
self.person2 = [[Person alloc] init];
self.person2.age = 2;
// 给person1对象添加KVO监听
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.age = 21;
self.person2.age = 22;
}
- (void)dealloc {
[self.person1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@的%@属性值改变了 - %@ - %@", object, keyPath, change, context);
// NSObject * obj = [change objectForKey:NSKeyValueChangeNewKey];
}
//// Person类定义
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end
@implementation Person
- (void)setAge:(int)age{
_age = age;
}
@end
在修改属性值时,只有能够触发到target类setter方法的动作才会有KVO回调,而其他的则检测不到。苹果的 KVO 机制设计非常巧妙, 通过运行时动态创建子类来实现。

当我们对一个对象进行kvo监听的时候,会生成一个NSKVONotifying_
前缀的类,然后我们实际的操作是对这个类进行的。某对象在addObserver:之后,这个对象的isa指针已经指向了NSKVONotifying_
前缀的类,其父类被设置为Person类。
-
验证一
如果在原有工程中,创建NSKVONotifying_Person类,运行代码会报 KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class 错误,因为原有工程中已经存在该类,故无法运行时生成该类。 -
验证二
我们可以在addObserver:前后断点打印对象的isa指针,会发现两个实例对应的打印结果不同。
NSLog(@"KVO之前 - %@", object_getClass(self.person));
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %@", object_getClass(self.person));
//log
//2021-06-22 20:02:02.101462+0800 TestProj[81369:8704592] KVO之前 - Person
//2021-06-22 20:02:07.003729+0800 TestProj[81369:8704592] KVO之后 - NSKVONotifying_Person
- 验证三
在 person 对象调用 addObserver: forKeyPath: options: context: 方法前后添加如下代码,打印结果不同 。
NSLog(@"KVO之前 - %p", [self.person methodForSelector:@selector(setAge:)]);
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.person addObserver:self forKeyPath:@"age" options:options context:@"keyOfMy"];
NSLog(@"KVO之后 - %p", [self.person methodForSelector:@selector(setAge:)]);
//log
2021-06-22 20:12:15.245515+0800 TestProj[81577:8714132] KVO之前 - 0x10414be60
2021-06-22 20:12:23.842112+0800 TestProj[81577:8714132] KVO之后 - 0x7fff207bf79f

在addObserver:后调用setage方法,会根据对象的isa找到NSKVONotifying_Person,然后在类的方法列表中找到setage。
通过下面方法,查看NSKVONotifying_Person 的内部结构:
- (void)printMethodNamesOfClass:(Class)cls{
unsigned int count;
// 获得方法数组
Method *methodList = class_copyMethodList(cls, &count);
// 存储方法名
NSMutableString *methodNames = [NSMutableString string];
// 遍历所有的方法
for (int i = 0; i < count; i++) {
// 获得方法
Method method = methodList[I];
// 获得方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}
// 释放
free(methodList);
// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
// log,在kvo监听下包含了四个方法:
// NSKVONotifying_MJPerson setAge:, class, dealloc, _isKVOA,
可见Apple是不希望暴露NSKVONotifyin_Person,重写了class方法,大概实现如下:
- (Class) class {
// 得到类对象,在找到类对象父类
return class_getSuperclass(object_getClass(self));
}
重写setName: 的内部实现,其中调用了"_NSSetObjectValueAndNotify()" :
- (void)setName:(NSString *)name {
_NSSetObjectValueAndNotify()
}
- (void)willChangeValueForKey:(NSString *)key {
[super willChangeValueForKey:key];
}
- (void)didChangeValueForKey:(NSString *)key {
[super didChangeValueForKey:key];
[observer observeValueForKeyPath:@"name"];
}
void _NSSetObjectValueAndNotify() {
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
2. 手动触发KVO
因为 KVO 的本质是重写了 set 方法, set 方法内部调用了willChangeValueForKey 和 didChangeValueForKey 方法,直接修改成员变量并不会调用 set 方法。由此可知,KVO 的触发条件一般是修改监听对象属性值,但也可在不修改被监听属性值的情况下触发 KVO 监听回调。
[self.person1 willChangeValueForKey:@"age"];
[self.person1 didChangeValueForKey:@"age"];
注:直接修改成员变量不会触发 KVO 监听方法,
3. KVC触发KVO
kvc常见的API有:
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (void)setValue:(id)value forKey:(NSString *)key;
- (id)valueForKeyPath:(NSString *)keyPath;
- (id)valueForKey:(NSString *)key;
setValue:forKey:的原理,

方法accessInstanceVariablesDirectly:(是否允许访问成员变量),默认返回YES。该方法有个应用场景就是如果你自己写框架,你的一些私有的变量不想被外部通过KVC的方式去修改,就可以重写这个方法,返回 NO 即可!
valueForKey:的原理,

KVC修改属性值,是会触发KVO的,原因是系统自动实现了set方法,并且底层都会调用 willChangeValueForKey和 didChangeValueForKey。
注意:
1.当调用setValue:forKey:时,程序会先通过setter方法,对属性值进行设置;
2.如果没找到setKey:方法,KVC机制会检查accessInstanceVariablesDirectly方法,默认返回YES的,如果重写返回了NO,那么这一步会执行setValue:forUndefinedKey:方法抛出异常。若为YES,KVC机制会搜索该类中是否有名为key的成员变量,若存在,KVC都可以对该成员变量赋值。
3.若该类既没有setKey方法,也没有_key成员变量,KVC机制会搜索_isKey的成员变量;
4.若_isKey成员变量也没有,KVC机制再会继续搜索和is的成员变量给它们赋值;
5.若上述方法或者成员变量都不存在,系统会执行该对象的setValue:forUndefinedKey:方法
,并抛出异常。
即,如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。
因此,触发kvc的set方法才可以触发kvo机制。
// 这个可以完美触发kvo
[_person setValue:@"名字" forKey:@"name"];
// 这个直接给name赋值,所以不能触发kvo
[_person setValue:@"名字" forKey:@"_name"];
4. 手动实现KVO
苹果给的KVO接入也有比较明显的局限,比如无法自定义addObserver:方法、无法传入block、需要处理父类同样监听同一个对象的同一个属性的情况等。我们也是可以自己实现一套KVO逻辑的,首先创建 NSObject 的分类NSObject (KVO):
//// .h
#import <Foundation/Foundation.h>
@class PGObservationInfo;
typedef void(^PGObservingBlock)(PGObservationInfo *observerInfo, NSString *observedKey, id oldValue, id newValue);
@interface NSObject (KVO)
- (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block;
- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key;
@end
@interface PGObservationInfo : NSObject
@property (nonatomic, copy) NSString *key;
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) PGObservingBlock block;
- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(PGObservingBlock)block;
@end
//// .m
#import "NSObject+KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
NSString *const kPGKVOClassPrefix = @"PGKVOClassPrefix_";
NSString *const kPGKVOAssociatedObservers = @"PGKVOAssociatedObservers";
#pragma mark - Debug Help Methods
static NSArray *ClassMethodNames(Class c) {
NSMutableArray *array = [NSMutableArray array];
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
static void PrintDescription(NSString *name, id obj)
{
NSString *str = [NSString stringWithFormat:
@"%@: %@\n\tNSObject class %s\n\tRuntime class %s\n\timplements methods <%@>\n\n",
name,
obj,
class_getName([obj class]),
class_getName(object_getClass(obj)),
[ClassMethodNames(object_getClass(obj)) componentsJoinedByString:@", "]];
printf("%s\n", [str UTF8String]);
}
#pragma mark - Helpers
static NSString * getterForSetter(NSString *setter)
{
if (setter.length <=0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
// remove 'set' at the begining and ':' at the end
NSRange range = NSMakeRange(3, setter.length - 4);
NSString *key = [setter substringWithRange:range];
// lower case the first letter
NSString *firstLetter = [[key substringToIndex:1] lowercaseString];
key = [key stringByReplacingCharactersInRange:NSMakeRange(0, 1)
withString:firstLetter];
return key;
}
static NSString * setterForGetter(NSString *getter)
{
if (getter.length <= 0) {
return nil;
}
// upper case the first letter
NSString *firstLetter = [[getter substringToIndex:1] uppercaseString];
NSString *remainingLetters = [getter substringFromIndex:1];
// add 'set' at the begining and ':' at the end
NSString *setter = [NSString stringWithFormat:@"set%@%@:", firstLetter, remainingLetters];
return setter;
}
#pragma mark - Overridden Methods
static void kvo_setter(id self, SEL _cmd, id newValue)
{
NSString *setterName = NSStringFromSelector(_cmd);
NSString *getterName = getterForSetter(setterName);
if (!getterName) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have setter %@", self, setterName];
@throw [NSException exceptionWithName:NSInvalidArgumentException
reason:reason
userInfo:nil];
return;
}
id oldValue = [self valueForKey:getterName];
struct objc_super superclazz = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// cast our pointer so the compiler won't complain
void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
// call super's setter, which is original class's setter method
objc_msgSendSuperCasted(&superclazz, _cmd, newValue);
// look up observers and call the blocks
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
for (PGObservationInfo *each in observers) {
if ([each.key isEqualToString:getterName]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
each.block(self, getterName, oldValue, newValue);
});
}
}
}
static Class kvo_class(id self, SEL _cmd)
{
return class_getSuperclass(object_getClass(self));
}
#pragma mark - KVO Category
@implementation NSObject (KVO)
- (void)PG_addObserver:(NSObject *)observer forKey:(NSString *)key withBlock:(PGObservingBlock)block {
SEL setterSelector = NSSelectorFromString(setterForGetter(key));
Method setterMethod = class_getInstanceMethod([self class], setterSelector);
if (!setterMethod) {
NSString *reason = [NSString stringWithFormat:@"Object %@ does not have a setter for key %@", self, key];
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil];
return;
}
Class clazz = object_getClass(self);
NSString *clazzName = NSStringFromClass(clazz);
// if not an KVO class yet
if (![clazzName hasPrefix:kPGKVOClassPrefix]) {
clazz = [self makeKvoClassWithOriginalClassName:clazzName];
object_setClass(self, clazz);
}
// add our kvo setter if this class (not superclasses) doesn't implement the setter?
if (![self hasSelector:setterSelector]) {
const char *types = method_getTypeEncoding(setterMethod);
class_addMethod(clazz, setterSelector, (IMP)kvo_setter, types);
}
PGObservationInfo *info = [[PGObservationInfo alloc] initWithObserver:observer Key:key block:block];
NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
if (!observers) {
observers = [NSMutableArray array];
objc_setAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers), observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[observers addObject:info];
}
- (void)PG_removeObserver:(NSObject *)observer forKey:(NSString *)key
{
NSMutableArray* observers = objc_getAssociatedObject(self, (__bridge const void *)(kPGKVOAssociatedObservers));
PGObservationInfo *infoToRemove;
for (PGObservationInfo* info in observers) {
if (info.observer == observer && [info.key isEqual:key]) {
infoToRemove = info;
break;
}
}
[observers removeObject:infoToRemove];
}
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName
{
NSString *kvoClazzName = [kPGKVOClassPrefix stringByAppendingString:originalClazzName];
Class clazz = NSClassFromString(kvoClazzName);
if (clazz) {
return clazz;
}
// class doesn't exist yet, make it
Class originalClazz = object_getClass(self);
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
// grab class method's signature so we can borrow it
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
objc_registerClassPair(kvoClazz);
return kvoClazz;
}
- (BOOL)hasSelector:(SEL)selector
{
Class clazz = object_getClass(self);
unsigned int methodCount = 0;
Method* methodList = class_copyMethodList(clazz, &methodCount);
for (unsigned int i = 0; i < methodCount; i++) {
SEL thisSelector = method_getName(methodList[i]);
if (thisSelector == selector) {
free(methodList);
return YES;
}
}
free(methodList);
return NO;
}
@end
@implementation PGObservationInfo
- (instancetype)initWithObserver:(NSObject *)observer Key:(NSString *)key block:(PGObservingBlock)block {
self = [super init];
if (self) {
_observer = observer;
_key = key;
_block = block;
}
return self;
}
@end
其中复杂的Observer独立成一个类,自行创建工程,试一试吧!😜
参考文章:
http://www.cocoachina.com/articles/25407
https://tech.glowing.com/cn/implement-kvo/
https://www.jianshu.com/p/79b06fabb459
感谢!
网友评论