KVO
KVO键值观察
KVO三部曲:
- 添加监听
- 监听回调
- 移除监听(很重要,一定不能忘记)
KVO可以对摸一个属性进行监听,比如
//注册监听
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
//监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
}
问题1:context什么作用
一般情况下我们context
传一个NULL
,现在如果有一个例子,比如我现在监听两个对象person
和student
,他们都有一个相同的属性name
,要同时对这个name
进行监听,可能很多人是在回调里边进行判断比如
if (objc == person){
if (keyPath == XXX){
//业务逻辑
}
}
很显然这样的代码很不好读,我们这时候可以利用context
进行区分
- 更加直接
- 更加安全
- 更加便利
static void *StudentNameContext = & StudentNameContext;
static void *PersonNameContext = &PersonNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context: PersonNameContext];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context: StudentNameContext];
问题2:监听用不用移除呢?
监听一定要在对应的dealloc
里边进行移除
举个例子,有A
和B
两个控制器,A
里边有一个单利的对象student
,现在push到B
,B
同样对student
的 name
属性进行监听,然后我们pop
会去,这时候在改变student
的name
,这时候就会出现野指针崩溃.造成莫名其妙的bug
,所以这里我们一定要进行移除监听
问题3:多个因素影响能不能监听?(要观察的属性受多个因素影响)
比如当前的下载进度,当前的下载量和总量,下载量和总量都有可能改变
// 下载进度 -- writtenData/totalData
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
//这两个因素都对`downloadProgress `有影响
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
//调用以下代码就能监听到"downloadProgress"的变化
self.person.writtenData += 10;
self.person.totalData += 20;
问题3:能不能对可变数组进行监听呢?
//记得数组一定要初始化哦!!!!!否则会直接报错
//此时是无法监听到dateArray的变化的
//因为此时根本没有调用dateArray的set方法
//KVO是建立在KVC的基础上边
[self.person.dateArray addObject:@"1"];
//同样不行
[self.person.dateArray insertObject:@"3" atIndex:0];
//此时就可以进行监听
[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"2"];
自动监听开关
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key
默认返回YES
,如果重写此方法返回NO
,那么我们就必须手动的进行一些操作才能够实现观察
比如还是对name进行观察如果这里返回为No
,那么就要这样写
[self.student willChangeValueForKey:@"nick"];
self.student.name = @"JasonLee";
[self.student didChangeValueForKey:@"nick"];
//为了方便可以在set方法里边进行操作
//谁的事情谁做
- (void)setName:(NSString *) name{
[self willChangeValueForKey:@"name"];
_name = name;
[self didChangeValueForKey:@"name"];
}
KVO实现细节
因为KVO,KVC是在Foundation框架中,该框架并未开源,所以我们只能从文档中探索
//键值观察实现细节
# Key-Value Observing Implementation Details
//自动键值观察是利用`isa_swizzling`技术实现的
Automatic key-value observing is implemented using a technique called *isa-swizzling*.
The `isa` 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.
///当为对象的属性注册观察者时,将修改观察对象的isa指针,指向中间类而不是真实类。结果,isa指针的值不一定反映实例的实际类。
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 the `isa` pointer to determine class membership. Instead, you should use the `[class](https://developer.apple.com/library/archive/documentation/LegacyTechnologies/WebObjects/WebObjects_3.5/Reference/Frameworks/ObjC/Foundation/Protocols/NSObject/Description.html#//apple_ref/occ/intfm/NSObject/class)` method to determine the class of an object instance.
大致意思就是自动键值观察是利用isa_swizzling
技术实现的,我们观察的对象的isa
是被修改的.当注册键值观察的时候会动态生成一个中间类,
我们验证一下
在注册监听打断点,打印当前类,然后继续走一步,再次打印发现是一个NSKVONotifying_LGPerson
的类
我们也可以通过
runtime
对注册类进行打印
/// 注册类的总数
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<count; i++) {
if (cls == class_getSuperclass(classes[i])) {
[mArray addObject:classes[i]];
}
}
free(classes);
NSLog(@"classes = %@", mArray);
在注册观察之前和之后打印发现多了一个NSKVONotifying_***
的类,由此证明添加观察会生成一个中间类,并且和原来的类是继承关系
动态子类有什么呢
打印中间子类的所有方法
- (void)printClassAllMethod:(Class)cls{
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@",NSStringFromSelector(sel));
}
free(methodList);
//打印结果
//setNickName:-0x7fff256237ae
//class-0x7fff25622271
//dealloc-0x7fff25621fd6
//_isKVOA-0x7fff25621fce
}
- 重写了观察属性的
setter
方法
触发setter
方法,进行回调,通知 - 重写了
class
方法
重写class
方法,返回的类不一样了,我们在添加观察之后打印[person class]
打印的还是Person
.重写class是为了将返回值指回来,还是返回Person
类 - 重写了
dealloc
方法
为了析构释放释放观察, - 重写了
isKVO
方法,
做了一个标识
动态子类都做了什么
- 移除观察之后isa又交换回来
验证下,我们在dealloc中打断点然后进行llvm调试,
po object_getClassName(person)
打印不在是NSKVONotifying_ Person
类,而是Person
,说明移除观察之后又将isa
交换回来 - 移除观察之后生成的中间子类是否销毁呢
没有移除,一经生成,他就存在这个缓存里边
同样打印当前注册类进行验证
自定义KVO
- KVO是监听的setter方法,所以第一步应该验证是否存在setter方法,玩意keyPath给了一个变量呢?
#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];
}
- 创建动态子类
#pragma mark -
- (Class)createChildClassWithKeyPath:(NSString *)keyPath{
// 2.1 判断是否有了
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLGKVOPrefix,oldClassName];// LGKVONotifying_LGPerson
Class newClass = NSClassFromString(newClassName);
if (!newClass) {
// 2.2 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.3 注册类
objc_registerClassPair(newClass);
// 2.4.1 添加class方法
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getClassMethod([self class], @selector(class));
const char *classType = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)lg_class, classType);
}
// 2.4.2 添加setter方法 setNickname
// 判断一下
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getClassMethod([self class], setterSEL);
const char *setterType = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)lg_setter, setterType);
return newClass;
}
Class lg_class(id self,SEL _cmd){
//因为self为中间类,为原来person的子类,所以我们取self.superclass,就为person类
return class_getSuperclass(object_getClass(self));
}
static void lg_setter(id self,SEL _cmd,id newValue){
NSLog(@"来了:%@",newValue);
// 4: 消息转发 : 转发给父类
// 改变父类的值 --- 可以强制类型转换
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
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)),
};
//发送消息给父类
lg_msgSendSuper(&superStruct,_cmd,newValue);
// 1: 拿到观察者
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
//遍历观察者
for (LGKVOInfo *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 & LGKeyValueObservingOptionNew) {
[change setObject:newValue forKey:NSKeyValueChangeNewKey];
}
// 对旧值进行处理
if (info.options & LGKeyValueObservingOptionOld) {
[change setObject:@"" forKey:NSKeyValueChangeOldKey];
if (oldValue) {
[change setObject:oldValue forKey:NSKeyValueChangeOldKey];
}
}
// 2: 消息发送给观察者 回调
SEL observerSEL = @selector(lg_observeValueForKeyPath:ofObject:change:context:);
objc_msgSend(info.observer,observerSEL,keyPath,self,change,NULL);
});
}
}
}
- (void)lg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(LGKeyValueObservingOptions)options context:(nullable void *)context{
// 1: 验证是否存在setter方法 ,如果不存在直接抛出异常
[self judgeSetterMethodFromKeyPath:keyPath];
// 2: 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3: isa的指向 : LGKVONotifying_LGPerson isa_swizzling
object_setClass(self, newClass);
// 4: 保存观察者信息
LGKVOInfo *info = [[LGKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
if (!observerArr) {
observerArr = [NSMutableArray arrayWithCapacity:1];
[observerArr addObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
}
- 移除观察者
一般来说我们手动的在dealloc方法中进行观察者的移除
- (void)lg_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath{
NSMutableArray *observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey));
if (observerArr.count<=0) {
return;
}
for (LGInfo *info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
[observerArr removeObject:info];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLGKVOAssiociateKey), observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
if (observerArr.count<=0) {
// 指回给父类
Class superClass = [self class];
object_setClass(self, superClass);
}
}
我们可不可以借助runtime
的methodSwizzling
实现呢
我们可以对dealloc进行hock
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//进行方法交换
[self kc_hookOrigInstanceMenthod:NSSelectorFromString(@"dealloc") newInstanceMenthod:@selector(myDealloc)];
});
}
+ (BOOL)kc_hookOrigInstanceMenthod:(SEL)oriSEL newInstanceMenthod:(SEL)swizzledSEL {
Class cls = self;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!swiMethod) {
return NO;
}
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
return YES;
}
- (void)myDealloc{
//将isa指回来
Class superClass = [self class];
object_setClass(self, superClass);
[self myDealloc];
}