KVO简介
苹果官方文档KVO介绍
KVO(Key-value observing)
是一种允许对象在其他对象的指定属性发生变化时被通知的机制。
KVO细节
Options
Options
会影响通知中提供的更改字典的内容,以及生成通知的方式。
NSKeyValueObservingOptionNew
:得到改变后的值NSKeyValueObservingOptionOld
:得到改变前的值NSKeyValueObservingOptionInitial
:观察最初的值(注册处理通知的方法就立马调用)NSKeyValueObservingOptionPrior
:值改变之前和值改变之后各调用一次
Context
当addObserver:forKeyPath:options:context:
方法context
都传入NULL
时,在observeValueForKeyPath: ofObject: change:context:
方法中处理回调时,就需要通过对object
和KeyPath
进行判断,如果传入唯一的标识Context
,就能够直接利用Context
进行区分处理。
移除观察者
当前页面释放的时候需要移除观察者,否则当被观察对象是一个单例的时候,页面(观察者)释放的时候,这个单例还存在,上次注册依然有效,页面再次被注册为观察者,会被认为有两个观察者,属性发生改变,触发通知,但是第一个观察者已经被释放了,这样就会造成野指针访问。
手动/自动
默认是自动开启
KVO
的,当想手动控制KVO
时,除了上面要返回NO
,还要对被观察属性做一下处理,比如我这里以被观察属性age
举例:
KVO应用(下载进度)
- 定义一个被观察属性
downloadProgress
,及两个其他属性(totalData
、writtenData
)用于计算进度
@interface DownLoader : NSObject
@property (nonatomic, copy) NSString *downloadProgress;
@property (nonatomic,assign) double writtenData;
@property (nonatomic,assign) double totalData;
@end
2.实现downloadProgress
的getter
方法
- (NSString *)downloadProgress{
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
3.配置监听totalData
、writtenData
,通过计算得出downloadProgress
。也就是说downloadProgress
变化依赖于totalData
、writtenData
。
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
4.使用
对可变数组观察
使用mutableArrayValueForKey
获取数组,进行数组操作。
KVO底层原理
KVO注册
也就是说,实例对象
p
注册KVO
的时候,会生成一个子类NSKVONotifying_Person
,命名方式为NSKVONotifying_<className>
。
KVO子类
NSKVONotifying_Person
实现了什么内容呢?
重写了以下方法:
-
setAge:
: -
class
:将KVO
生成的子类isa
指针指向当前类,验证如下: -
dealloc
:销毁的的时候调用。 -
_isKVOA
:一个标识,表示当前类是KVO
生成的子类
这里探索一下子类方法setAge:
的调用。
上图
[Person setAge:]
调用前还调用了这些方法:
[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
_NSSetLongLongValueAndNotify
也就是说KVO
动态生成的子类的setAge
方法内部会调用上面三个方法后,再次调用父类的setAge
方法。
接着在处理通知的地方打上断点,查看调用栈,又调用了:
NSKeyValueDidChange
NSKeyValueNotifyObserver
[ViewController observeValueForKeyPath:ofObject:change:context:]
最后setAge
监听调用顺序如下:
1.[NSKVONotifying_Person setAge:]
2._NSSetLongLongValueAndNotify
3.[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:]
4.[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:]
5.[Person setAge:]
6.NSKeyValueDidChange
7.NSKeyValueNotifyObserver
8.[ViewController observeValueForKeyPath:ofObject:change:context:]
在调用NSKeyValueDidChange
调用后,发送通知调用_observeValueForKeyPath:ofObject:changeKind:oldValue:newValue:indexes:context:
完成通知处理。
KVO移除
当移除
KVO
监听的时候,类有变成最初的了,也就是说,在注册的时候,实例对象的isa
会指向当前类的子类NSKVONotifying_<className>
,移除监听的时候,isa
又指回以前的类。
自定义KVO
#import <Foundation/Foundation.h>
#import "MWKVOInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context;
- (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
- (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+KVO.h"
#import <objc/message.h>
static NSString *const kMWKVOPrefix = @"MWKVONotifying_";
static NSString *const kMWKVOAssiociateKey = @"kMWKVO_AssiociateKey";
@implementation NSObject (KVO)
- (void)mw_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(MWKeyValueObservingOptions)options context:(nullable void *)context
{
//1.不能观察成员变量,只能观察属性,通过是否有setter方法判断
[self judgeSetterMethodFromKeyPath:keyPath];
//2.动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
//3.isa的指向刚生成的子类
object_setClass(self, newClass);
//4.保存观察者信息
MWKVOInfo *info = [[MWKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
if (!observerArr) {
observerArr = [NSMutableArray arrayWithCapacity:1];
[observerArr addObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
- (void)mw_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (MWKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observerArr.count<=0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
#pragma mark **-动态生成子类**
- (Class)createChildClassWithKeyPath:(NSString *)keyPath
{
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kMWKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重复创建生成新类
if (newClass) return newClass;
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
// 2.1 : 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 : 注册类
objc_registerClassPair(newClass);
// 2.3.1 : 添加class : class的指向是观察对象的类
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)mw_class, classTypes);
// 2.3.2 : 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)mw_setter, setterTypes);
return newClass;
}
Class mw_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
static void mw_setter(id self,SEL _cmd,id newValue){
// 4: 消息转发 : 转发给父类
// 改变父类的值 --- 可以强制类型转换
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
void (*mw_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
// void /* struct objc_super *super, SEL op, ... */
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
//objc_msgSendSuper(&superStruct,_cmd,newValue)
mw_msgSendSuper(&superStruct,_cmd,newValue);
// 1: 拿到观察者
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kMWKVOAssiociateKey));
for (MWKVOInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
// 对新旧值进行处理
if (info.options & MWKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
if (info.options & MWKeyValueObservingOptionOld) {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
}
}
// 2: 消息发送给观察者
SEL observerSEL = @selector(mw_observeValueForKeyPath:ofObject:change:context:); objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
});
}
}
}
- (void)mw_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context
{
}
#pragma mark **- 验证是否存在setter方法**
- (void)judgeSetterMethodFromKeyPath:(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 **- 从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>:===> 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