KVO的使用
KVO使用的三部曲:添加观察者、接受回调、移除观察者;
1、为什么要移除观察者呢?如果不移除会造成什么后果呢?
如果观察者对象dealloc的时候没有移除对目标属性的观察,当目标属性改变的时候,还是会通知该观察者,但是该观察者此时已经释放了,就会出现野指针的情况。
例如:LGPerson
是一个单例对象,它有个属性name
。然后在FirstViewController
和SecondViewController
中都对LGPerson
对象的name
属性进行了KVO观察。视图结构是从FirstViewController
push到SecondViewController
,然后SecondViewController
pop到FirstViewController
,加入SecondViewController
dealloc的时候没有移除观察者。然后再FirstViewController
中name
属性被改变了。此时就会通知它的观察者,但是其中的SecondViewController
观察者已经释放了,就会造成访问野指针的情况。
2、下面方法中context是干什么用的呢?
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
context可以作为一个标识,来区分观察的是哪个对象的哪个属性。当然我们可以通过keypath和observer组合来实现区分。但是通过context来区分,更安全更简洁。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
3、KVO自动开启回调和手动开启回调
例如我们在ViewController中对Person
中的name
和nick
进行观察。
在Person
复写下面的方法,并且返回NO;
// 自动开关
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
return NO;
}
然后我们通过下面的代码更改nick和name;
[self.person willChangeValueForKey:@"nick"];
self.person.nick = @"iOS";
[self.person didChangeValueForKey:@"nick"];
self.person.name = @"person";
此时在KVO的回调方法中只会回调nick更改的通知,name的更改不会触发KVO通知。因为我们将自动通知关闭了,所以只能通过手动的添加willChangeValueForKey
和didChangeValueForKey
来触发通知。这样我们可以更加灵活的手动控制哪个属性会触发KVO。
3、一个属性变化受多个属性的影响
例如下载进度downloadProgress
受totalData
和writtenData
两个变量的影响。我们可以在Person中复写下面的方法,实现如下。
// 下载进度 -- writtenData/totalData
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
然后我们添加观察
[self.person addObserver:self forKeyPath:@"downloadProgress" options:(NSKeyValueObservingOptionNew) context:NULL];
这样实现后,不管是totalData
还是writtenData
变化,都会触发KVO中downloadProgress
的KVO回调。
self.person.writtenData += 10;
self.person.totalData += 1;
4、MutableArray的KVO观察
首先添加对dateArray观察,dateArray属性是个可变数组。
[self.person addObserver:self forKeyPath:@"dateArray" options:(NSKeyValueObservingOptionNew) context:NULL];
然后调用下面代码,对dateArray数组添加元素
[self.person.dateArray addObject:@"hello"];
这样会不会触发KVO通知呢?答案是不会的,因为KVO是给予set方法的,这样不会触发set方法,所以就不会触发KVO通知。正确的做法应该是下面这样
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"hello"];
KVO的原理
Key-Value Observing Implementation Details
Automatic key-value observing is implemented using a technique called isa-swizzling.
Theisa
pointer, as the name suggests, points to the object's class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data.
When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance.
You should never rely on theisa
pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.
上述为苹果的官方文档对KVO实现细节的介绍。KVO的实现使用了isa-swizzling。当注册KVO的观察者后,被观察对象的isa会被更改,指向了一个中间类。所以判断一个对象所属的类关系,不应该通过isa,而应该通过class方法来获取。(class方法指向原来的类)
下面我们通过lldb来探索一下
在注册KVO之前,self.person对象所属的class为
LGPerson
,当注册KVO后变成了NSKVONotifying_LGPerson
。正像上面文档说的,isa被修改了。
既然我们知道了KVO生成了中间类NSKVONotifying_***,那么这个中间类和原来的类有什么关系吗?其实生成的中间类为原来的类的子类,这样可以集成原来类的方法和属性。下面我们来验证下:
[self printClasses:[LGPerson class]];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClasses:[LGPerson class]];
其中的printClasses
是我们自定义的方法,作用是打印所有的子类。我们这样在添加观察者前后分别打印了LGPerson的子类。打印结果如下
2020-02-16 09:52:05.711893+0800 002---KVO原理探讨[25321:556367] classes = (
LGPerson
)
2020-02-16 09:52:11.539296+0800 002---KVO原理探讨[25321:556367] classes = (
LGPerson,
"NSKVONotifying_LGPerson"
)
(lldb)
我们看到当添加KVO观察后,LGPerson多了一个子类NSKVONotifying_LGPerson
。这个就是我们上面打印的生成的中间类。他是继承自原来类的。
已知LGPerson
的定义结构如下:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
包含一个属性nickName和一个成员变量name。然后分别通过KVO观察这两个属性。然后下面更改这两个值,结果是否都会收到KVO回调呢?
self.person.nickName = @"KC";
self.person->name = @"Cooci";
答案是只有属性nickName会收到回调,成员变量不会回调。它们的却别就是有没有setter方法,所以我们得出结果:KVO是通过setter方法进行处理回调的。
LGPerson的isa指向中间类,NSKVONotifying_LGPerson
中间类的superClass指向原来类。我们知道类的结构:isa,superClass,cache,bits。下面我们来研究下中间的的bits中的方法列表。
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
[self printClassAllMethod:NSClassFromString(@"NSKVONotifying_LGPerson")];
KVO观察后,打印中间类的方法列表,结果如下
2020-02-16 10:28:18.122486+0800 002---KVO原理探讨[25321:556367] setNickName:-0x10ed7863a
2020-02-16 10:28:18.122674+0800 002---KVO原理探讨[25321:556367] class-0x10ed7706e
2020-02-16 10:28:18.122824+0800 002---KVO原理探讨[25321:556367] dealloc-0x10ed76e12
2020-02-16 10:28:18.122933+0800 002---KVO原理探讨[25321:556367] _isKVOA-0x10ed76e0a
中间类含有四个方法:setNickName
, class
, dealloc
, _isKVOA
。它复写了集成的setNickName
, class
, dealloc
方法。
我们在使用完成KVO,就会移除Observer。那么移除完所有的oberver后,person对象的指针会变化吗?下面我们在btnAction
方法中移除,然后打印下person对象isa的指向
通过结果可以看到,移除所有观察者后,person对象的isa重新指向了原来的LGPerson。
问题:那么移除所有的观察者并且person对象销毁后,生成的中间类是否会销毁呢?
我们通过打印LGPerson的子类列表,发现生成的中间类并未销毁,下次再添加观察者的时候可以直接使用。
KVO原理总结:
1.动态生成子类:NSKVONotifying_
2.观察的是setter
3.动态子类重写了很多方法:setNickName, class, dealloc
4.移除所有的观察后,isa会指回来
5.动态子类不会销毁
自定义KVO
我们模仿系统的KVO来实现自己的一套KVO。创建NSObject的分类:
@interface NSObject (LGKVO)
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
首先我们来实现lg_addObserver
方法
一、验证是否存在setter方法 : 不让实例进来
[self judgeSetterMethodFromKeyPath:keyPath];
judgeSetterMethodFromKeyPath
方法是我们自定义的方法,用来判断类的方法列表中是否存在该方法。方法实现如下:
#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];
}
}
二、动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
createChildClassWithKeyPath
是我们自定义的方法,根据keyPath来生成中间类。
根据keyPath拼接中间类的名称
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"LGKVONotifying_%@",oldClassName];
Class newClass = NSClassFromString(newClassName);
判断如果不存在中间类的话,就创建。如果已经存在,不需要重复创建。首次创建完成后添加class
方法。
if (!newClass) {
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
// 2.1 : 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 : 注册类
objc_registerClassPair(newClass);
// 2.3.1 : 添加class : class的指向是LGPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
//其中lg_class为我们自己实现的方法,返回当前中间类的父类,也就是原来的类
class_addMethod(newClass, classSEL, (IMP)lg_class, classTypes);
}
然后添加对应属性的setter方法
// 2.3.2 : 添加setter
// setterForGetter为自定义方法,通过keyPath拼接对应的setter方法名字
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterTypes);
return newClass;
其中的lg_setter为我们自己对setter方法的实现。我们这里怎么实现lg_setter呢?
- 因为我们使用KVO时候,是对操作的类(例如LGPerson)的属性赋值,这里因为将isa指向了新创建的中间子类(NSKVONotifying_LGPerson),所以这里需要调用父类(LGPerson)的setter方法进行属性赋值。我们这里通过
objc_msgSendSuper
发送消息来实现。
// 4: 消息转发 : 转发给父类
// 改变父类的值 --- 可以强制类型转换
void (*lg_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)
lg_msgSendSuper(&superStruct,_cmd,newValue);
- 通知观察者
属性改变后我们还需要通过observeValueForKeyPath
方法通知观察者。我们仍然是使用消息发送来实现通知观察者。
// 1: 拿到观察者,在添加观察者的时候通过关联对象将observer存储了起来。
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
for (LGInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath]) {
// 2: 消息发送给观察者
SEL observerSEL = @selector(observeValueForKeyPath:ofObject:change:context:);
//getterForSetter方法为 setter方法转get方法
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
objc_msgSend(info.observer,observerSEL,keyPath,self,@{keyPath:newValue},NULL);
//info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
然后添加dealloc
实现
// 2.3.3 : 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)lg_dealloc, deallocTypes);
当对象销毁的时候会调用dealloc,在该方法中将isa支持重新指向原来的类。
static void lg_dealloc(id self,SEL _cmd){
Class superClass = [self class];
object_setClass(self, superClass);
}
三、isa的指向 : LGKVONotifying_LGPerson
object_setClass(self, newClass);
四、保存observer信息,在setter方法中通知observer的时候需要用到该信息。
LGInfo *info = [[LGInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
其中使用LGInfo这个model来存储observer信息,因为可能会还有多个observer,所以使用[LGInfo]数组的形式进行存储。
然后我们来自定义lg_removeObserver:
方法:
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
//1.从存储观察者model的数组中移除该model
//2.如果没有了观察者,将isa指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}