iOS-KVO
一、VKO 简述
KVO 全称 Key Value Observing,俗称“键值监听”;可以监听对象某个属性值的变化
1. KVO 是已什么方式实现的?(底层原理是什么?)
答:当对一个对象添加监听(addObserver:forKeyPath: ... ),iOS会修改该对象的 isa (isa默认指向对象所所属的类)。改为指向一个通过Runtime动态创建的子类,子类拥重写 set 方法,并且 set 方法内部会顺序调用 willChangeValueForKey, 原来的set方法,即:[super set...], didChangeValueForKey。并且会在 didChangeValueForKey 中调用KVO的回调方法:observeValueForKeyPath:ofObject:change:context:
2. 如何手动触发KVO?
答:已添加监听的属性,在值发生变化时,系统会自动触发回调。如果想要手动触发,则需我们自己调用 willChangeValueFor 和 didChallengeValueForKey方法,这两个方法缺一不可。
二、KVO 实现原理探索
1. 话不多说,上代码:
- (void)useSystemKVOTest {
// 1. 创建测试对象
self.p1 = [Person new];
self.p2 = [Person new];
self.p1.age = 1;
self.p2.age = 2;
// 2. 打印监听前p1、p2 所属类、setter 方法实现地址
NSLog(@"监听前 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
// 输出结果:监听前 p1 class is : Person, p2 class is : Person
NSLog(@"监听前 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
// 输出结果:监听前 p1-setAage: address is : = 0x102f98ea8, p2-setAage: address is : 0x102f98ea8
// 3. 添加监听,
[self.p1 addObserver:self forKeyPath:NSStringFromSelector(@selector(age)) options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
// 4. 打印监听后p1、p2 所属类、setter 方法实现地址
NSLog(@"监听后 p1 class is : %@, p2 class is : %@", object_getClass(self.p1), object_getClass(self.p2));
// 输出结果:监听后 p1 class is : NSKVONotifying_Person, p2 class is : Person
NSLog(@"监听后 p1-setAage: address is : = %p, p2-setAage: address is : %p", [self.p1 methodForSelector:@selector(setAge:)], [self.p2 methodForSelector:@selector(setAge:)]);
// 输出结果:监听后 p1-setAage: address is : = 0x194c61d54, p2-setAage: address is : 0x102f98ea8
// 5. 改变值
self.p1.age = 10;
self.p2.age = 20;
// 6.移除 p1.age 的监听者
[self.p1 removeObserver:self forKeyPath:NSStringFromSelector(@selector(age))];
}
// kvo 回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSLog(@"监听到 %@ 的 %@ 改变了 %@", [object isEqual:self.p1]?@"p1":@"p2", keyPath, change);
/* 输出结果:
监听到 p1 的 age 改变了 {
kind = 1;
new = 10;
old = 1;
}
*/
}
输出结果:001
2. 有以上输出结果,我们发现:
- 在添加监听后,p1 的 isa 指向了 NSKVONotifying_Person
- NSKVONotifyin_Person其实是Person的子类,那么也就是说其superclass指针是指向Person类对象的
- NSKVONotifyin_Person 是 runtime 在运行时生成的。那么 p1 对象在调用 setage 方法的时候,肯定会根据 p1 的 isa 找到NSKVONotifyin_Person,在 NSKVONotifyin_Person 中找 setage 的方法及实现。
- p1 的 setAge 方法的实现由 Person 类方法中的 setAge 方法转换为了C语言的 Foundation 框架的 _NSsetIntValueAndNotify 函数。
3. NSKVONotifyin_Person 的内部结构:
首先我们知道,NSKVONotifyin_Person作为Person的子类,其superclass指针指向Person类,并且NSKVONotifyin_Person内部一定对setAge方法做了单独的实现,那么NSKVONotifyin_Person同Person类的差别可能就在于其内存储的对象方法及实现不同。
我们通过runtime分别打印Person类对象和NSKVONotifyin_Person类对象内存储的对象方法
- (void)printMethods {
[self printMehtodsOfClass:object_getClass(self.p1)];
[self printMehtodsOfClass:object_getClass(self.p2)];
}
- (void)printMehtodsOfClass:(Class)cls {
unsigned int count = 0;
Method * methods = class_copyMethodList(cls, &count);
NSMutableString *methodNames = @"".mutableCopy;
[methodNames appendFormat:@"%@ - ", cls];
for (int i = 0; i < count; i++) {
Method method = methods[i];
NSString * methodName = NSStringFromSelector(method_getName(method));
[methodNames appendString:methodName];
[methodNames appendString:@" "];
}
NSLog(@"%@", methodNames);
free(methods);
}
输出结果002
通过上述代码我们发现NSKVONotifyin_Person中有4个对象方法。分别为setAge: class dealloc _isKVOA,那么至此我们可以画出NSKVONotifyin_Person的内存结构以及方法调用顺序。
image.png这里NSKVONotifyin_Person重写class方法是为了隐藏NSKVONotifyin_Person。不被外界所看到。我们在p1添加过KVO监听之后,分别打印p1和p2对象的class可以发现他们都返回Person。
NSLog(@"%@, %@", [self.p1 class], [self.p2 class]);
// 打印结果 Person, Person
三. 自定义 KVO 实现监听
1. ViewController 调用实现:
#import "ViewController.h"
#import "Person.h"
#import "NSObject+YJKVO.h"
@interface ViewController ()
@property (nonatomic, strong) Person * p;
@end
@implementation ViewController
#pragma mark - Life Cycle
- (void)viewDidLoad {
[super viewDidLoad];
[self useCustomKVOTest];
}
#pragma mark - 使用自定义kvo
- (void)useCustomKVOTest {
self.p = [[Person alloc] init];
[self.p yj_addObserver:self forKeyPath:NSStringFromSelector(@selector(name))];
self.p.name = @"张三";
}
#pragma mark - 自定义kvo,回调
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue {
NSLog(@"newValue = %@", newValue);
}
2. Person 类
- Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString * name;
@end
- Person.m
#import "Person.h"
@implementation Person
- (void)setName:(NSString *)name {
_name = name;
NSLog(@"调用了");
}
@end
3. 定义一个 NSObject 的分类 NSObject+YJKVO,实现KVO监听
- NSObject+YJKVO.h
@interface NSObject (YJKVO)
/// 添加观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
/// 移除观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
/// kvo 回调方法 (由观察者实现)
/// @param keyPath keyPath
/// @param object 被观察对象
/// @param newValue 新值
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object newValue:(id)newValue;
@end
- NSObject+YJKVO.m
#import "NSObject+YJKVO.h"
#import <objc/message.h>
// 通过 Runtime 动态成子类的前缀
static NSString *const YJKVOPrefix = @"YJKVO_";
// 关联 观察者
static NSString *const YJKVOAssociatedOberverKey = @"YJKVOAssociatedOberverKey";
@implementation NSObject (YJKVO)
#pragma mark - -- public methods
/// 添加观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
// 1. 检查时候有 set 方法
NSString *setterMethodName = setterForGetter(keyPath);
SEL setterSel = NSSelectorFromString(setterMethodName);
// method
Method method = class_getInstanceMethod(self.class, setterSel);
if (!method) {
@throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"没有setter方法" userInfo:nil];
}
// 2. 动态生成子类
Class sub_Class = [self registerSubClassWithKeyPath:keyPath];
if (!sub_Class) {
@throw [[NSException alloc] initWithName:NSExtensionItemAttachmentsKey reason:@"子类创建失败" userInfo:nil];
}
// 3. 消息转发
// 关联 observer
objc_setAssociatedObject(self, (__bridge void const * _Nonnull)YJKVOAssociatedOberverKey, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
/// 移除观察者
/// @param observer 观察者
/// @param keyPath keyPath
- (void)yj_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
objc_removeAssociatedObjects(observer);
}
/// kvo 回调方法 (由观察者实现)
- (void)yj_observeValueForKeyPath:(NSString *)keyPath ofObject:(nonnull id)object newValue:(nonnull id)newValue { }
#pragma mark - -- private methods
#pragma mark - 通过 getter 方法名,获取 setter 方法名;例如:age ==> setAge:
static NSString * setterForGetter(NSString *getter) {
if (getter.length < 1) {
return nil;
}
// 获取第一个字符,变成打下
NSString *firstString = [[getter substringToIndex:1] uppercaseString]; // substringToIndex:从最前头一直截取到Index
NSString *otherString = [getter substringFromIndex:1]; // substringFromIndex:从Index开始截取到最后
// 拼接 age == > setAag:
return [NSString stringWithFormat:@"set%@%@:", firstString, otherString];
}
#pragma mark - 通过 setter 方法名,获取 getter 方法名;例如:setAge: ==> age
static NSString * getterForSetter(NSString *setter) {
if (setter.length < 1 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) {
return nil;
}
NSString *getter = [setter substringFromIndex:3];
getter = [getter substringToIndex:getter.length-1];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
#pragma mark - 动态生成子类
/// 运行时动态创建子类
/// @param keyPath keyPath
- (Class)registerSubClassWithKeyPath:(NSString *)keyPath {
// 子类名
NSString *subClsName = [NSString stringWithFormat:@"%@%@", YJKVOPrefix, self.class];
// 子类,一个 NSObject 默认分贝 16 个字节
Class subCls = objc_allocateClassPair(self.class, subClsName.UTF8String, 16);
// 注册
objc_registerClassPair(subCls);
// 给子类动态添加 setter、class 实现
Method class_method = class_getClassMethod(self.class, @selector(class));
Method setter_method = class_getClassMethod(self.class, NSSelectorFromString(setterForGetter(keyPath)));
class_addMethod(subCls, @selector(class), (IMP)yj_class, method_getTypeEncoding(class_method));
class_addMethod(subCls, NSSelectorFromString(setterForGetter(keyPath)), (IMP)yj_setter, method_getTypeEncoding(setter_method));
// 将父类的 isa 指向子类
object_setClass(self, subCls);
// 返回
return subCls;
}
#pragma mark - 重写 class 方法
static Class yj_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
#pragma mark - 重写 setter 方法
/// 重写 setter 方法
/// @param newValue 新值
static void yj_setter(id self, SEL _cmd, id newValue) {
// 1. 调用 super setter 方法
struct objc_super super_cls = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
// 调用父类 setter 方法 设置新值
((void(*) (id, SEL, id)) (void *)objc_msgSendSuper)((__bridge id)(&super_cls), _cmd, newValue);
// 2. 取出观察者,调用kvo 回调方法
id observer = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(YJKVOAssociatedOberverKey));
//
SEL handleSel = @selector(yj_observeValueForKeyPath:ofObject:newValue:);
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
// Runtime 调用回到方法
// objc_msgSend() 默认的情况下,不支持添加参数。
// 解决方案一: Build Setting –> 搜索: Enable Strict Checking of objc_msgSend Calls 改为 NO (我自己试了下,无效 Xcode12.1)
// 解决方案二: 这里通过(void *)送入5个参数,你可以根据自己参数类型强转原本是void()的函数方法
((void (*) (id, SEL, NSString*, id, id)) (void*)objc_msgSend)(observer, handleSel, keyPath, self, newValue);
}
@end
输出结果003
- 参考文章:iOS底层原理总结 - 探寻KVO本质