概述
KVO
全程KeyValueObserving
,是苹果提供的一套键值观察机制,它可以在对象指定属性发生改变时接到通知。
基础使用
KVO
使用分为三个步骤:
1.通过addObserver:forKeyPath:options:context:
注册观察者。观察者可以接收keyPath
属性变化通知。
2.在观察者中实现observeValueForKeyPath:ofObject:change:context:
,以接收观察属性改变的通知消息
3.当观察者不再需要接收消息时,使用removeObserver:forKeyPath:
移除观察者。在观察者在内存中释放之前,必须移除,否者会crash
注册函数
注册观察者时,需要传入一个Option
参数,是一个枚举值。
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
//接收新值
NSKeyValueObservingOptionNew = 0x01,
//接收旧值
NSKeyValueObservingOptionOld = 0x02,
//一旦注册,立马会调用一次
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
//在变更前后都会发送通知,而不止是变更后
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
还可以传入context
参数,上下文参数可以传递任意数据指针,在通知方法中返回给观察者。我们可以通过该参数,来区分发生改变的类。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext=&PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) ontext:PersonAccountBalanceContext];
[account addObserver:self forKeyPath:@"interestRate" ptions:NSKeyValueObservingOptionNew | SNSKeyValueObservingOptionOld ontext:PersonAccountInterestRateContext];
}
监听方法
观察者需要实现observeValueForKeyPath:ofObject:change:context:
,KVO
通知会通过这个方法传递,没有实现会导致crash
。change
字典中存放KVO
相关属性的值,根据options
传入的枚举值,可以取到数据。
change
中还有NSKeyValueChangeKindKey
,它也是一个枚举值
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
//观察对象的值已经更改
NSKeyValueChangeSetting = 1,
//观察对象是否插入
NSKeyValueChangeInsertion = 2,
//观察对象是否删除
NSKeyValueChangeRemoval = 3,
//观察对象是否替换
NSKeyValueChangeReplacement = 4,
};
移除观察者
你可以通过向观察者对象发送removeObserver:forKeyPath:context:
方法,指定观察者,路径和上下文来移除观察者。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
注意:
- 如果你没有注册,就调用移除方法会导致
NSRangeException
,注册和移除方法应该成对存在。如果这在你的程序中不可行,请将removeObserver
放入try/catch
中,处理潜在的异常。 - 移除观察者时,观察者不会自动删除自己,对已释放的对象发送观察通知会触发内存访问异常。
- 苹果推荐我们在观察者初始化是注册(如
ininit
或viewDidLoad
),在释放期间移除(通常是dealloc
)
自动开关与手动开关
KVO
属性发生改变时的调用一般是自动的,可以通过重写automaticallyNotifiesObserversForKey:
手动控制。(默认返回YES
)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey{
return YES;
}
手动控制nick
属性的KVO
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
注册依赖键
有时候我们不仅仅是要观察一个属性,而是多个属性。需要我们重写keyPathsForValuesAffectingValueForKey:
方法。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
数组 集合 观察
监听数组等集合时,需要用mutableArrayValueForKey
触发通知
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
KVO原理
-
KVO
只针对属性,而不监听成员变量。
准备代码:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"实际情况:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"Mike";
self.person->name = @"Tom";
}
#pragma mark - KVO回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nickName"];
}
打印情况:
实际情况:(null)-(null)
{
kind = 1;
new = Mike;
}
可以看出只是监听了属性nickName
,成员变量并没有监听。
- 查看官方文档,
KVO
的实现依赖于isa-swizzling
技术,当观察者注册时,被观察者的isa
指针被修改,指向一个中间类而不是真正的类。
代码验证:
在图中位置打上断点,验证注册观察者前后的isa
指向
注册观察者之后,self.person
的isa
指向了NSKVONotifying_LGPerson
。
查阅了相关资料,说NSKVONotifying_LGPerson
是LGPerson
的子类。进一步验证:
通过打印它的内存结构,发现
superClass
正是LGPerson
。
- 让我们看一看
NSKVONotifying_LGPerson
里有什么方法
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i
调用[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];
方法打印结果为:
-
NSKVONotifying_LGPerson
类重写setter
方法做了什么?
通过LLDB
设置,观察nickName
属性
watchpoint set variable self->_person->_nickName
不能使用点语法
当self.person.nickName
发生改变时,自动进入断点。
也可以直接在setter
方法中打断点,查看堆栈
我们可以看到在setter
方法前后,调用了NSKeyValueWillChange
和NSKeyValueDidChangeBySetting
-
移除观察者时,
isa
指针是否指回原本的类
同样,在removeObserver:forKeyPath:
方法前后打印isa
情况
移除观察者后,isa
指针指向原本的类。 移除观察者后,
NSKVONotifying_LGPerson
类是否从内存中移除。
通过以下方法,打印LGPerson
的子类,看NSKVONotifying_LGPerson
是否消失。
- (void)printClasses:(Class)cls{
// 注册类的总数
int count = objc_getClassList(NULL, 0);
// 创建一个数组, 其中包含给定对象
NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
// 获取所有已注册的类
Class* classes = (Class*)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
for (int i = 0; i
在removeObserver:forKeyPath:
方法后打印:
classes = (
LGPerson,
LGStudent,
"NSKVONotifying_LGPerson"
)
(lldb)
NSKVONotifying_LGPerson
依然作为LGPerson
的子类存在,避免了再一次注册KVO
的重新开辟,节省性能。
自定义KVO
模拟KVO
实现流程,自定义一份KVO
代码,实现了KVO自动销毁
。主要用来学习,加深记忆。
typedef void (^KVOHandle)(id observer, NSString * keyPath, id oldValue,id newValue);
@interface NSObject (LYKVO)
- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle;
@end
static NSString *const kLYKVOPrefix = @"LYKVONotifying_";
static NSString *const kLYKVOAssiociateKeyHandle = @"kLYKVO_AssiociateKeyHandle";
static NSString *const kLYKVOAssiociateKeyInfo = @"kLYKVO_AssiociateKeyInfo";
@implementation NSObject (LYKVO)
- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle {
if (!observer || !keyPath || !options) return;
// 1: 验证是否存在setter方法 : 不让实例进来
[self checkSetterMethodFromKeyPath:keyPath];
// 2: 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3.将self 指向LGKVONotifying_Class
object_setClass(self, newClass);
//4.重写setter方法 class_addMethod
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSel);
const char * setterType = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSel, (IMP)ly_setter, setterType);
//5.保存block,用于传值
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle), handle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//6.保存observer keyPath options 用于传值
LYKVOInfo *info = [[LYKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *infoArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo);
if (!infoArr) {
infoArr = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo, infoArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[infoArr addObject:info];
}
static void ly_setter(id self,SEL _cmd,id newValue) {
SEL getSel = NSSelectorFromString(getterForSetter(NSStringFromSelector(_cmd)));
id oldValue = objc_msgSend(self, getSel);
//向父类发送一个setter消息
void (*ly_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
ly_msgSendSuper(&superStruct, _cmd, newValue);
//处理回调
KVOHandle handle = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle));
NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
for (LYKVOInfo *info in mArr) {
handle(info.observer,info.keyPath,oldValue,newValue);
}
}
static Class ly_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
static void ly_dealloc(id self, SEL _cmd) {
//指针指回原本的类
Class superClass = [self class];
object_setClass(self, superClass);
//处理数据
NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
if (mArr.count <= 0) {
[mArr removeAllObjects];
mArr = nil;
}
//执行原本的dealloc方法
objc_msgSend(self, _cmd);
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
//2.创建中间类
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLYKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
if (newClass) return newClass;
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(newClass);
//3.重写dealloc方法
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
const char * deallocType = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSel, (IMP)ly_dealloc, deallocType);
//4.重写class方法
SEL classSel = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSel);
const char * classType = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)ly_class, classType);
return newClass;
}
#pragma mark -验证类的setter方法是否存在
- (void)checkSetterMethodFromKeyPath:(NSString *)keyPath {
Class superClass = object_getClass(self);
SEL setterSeletor = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:[NSString stringWithFormat:@"老铁没有当前%@的setter",keyPath] userInfo:nil];
}
}
#pragma mark -只是简单的拼接,并没有考虑周全
#pragma mark - 从get方法获取set方法的名称 key ===>>> setKey:
static NSString *setterForGetter(NSString *getter){
if (getter.length <= 0) { return nil;}
NSString *firstString = [[getter substringToIndex:1] uppercaseString];
NSString *leaveString = [getter substringFromIndex:1];
return [NSString stringWithFormat:@"set%@%@:",firstString,leaveString];
}
#pragma mark - 从set方法获取getter方法的名称 set:===> key
static NSString *getterForSetter(NSString *setter){
if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) { return nil;}
NSRange range = NSMakeRange(3, setter.length-4);
NSString *getter = [setter substringWithRange:range];
NSString *firstString = [[getter substringToIndex:1] lowercaseString];
return [getter stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstString];
}
@end
@interface LYKVOInfo : NSObject
@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options;
@end
@implementation LYKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options {
if (self = [super init]) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
}
return self;
}
@end
推荐学习:
FBKVOController
GNN源码