Objective-C 之 KVO 原理
键值观察提供了一种机制,该机制允许将其他对象的特定属性的更改通知给对象。对于应用程序中模型层和控制器层之间的通信特别有用。 (在OS X中,控制器层绑定技术在很大程度上依赖于键值观察。)控制器对象通常观察模型对象的属性,而视图对象通过控制器观察模型对象的属性。但是,此外,模型对象可能会观察其他模型对象(通常是确定从属值何时更改),甚至是自身(再次确定从属值何时更改)。
您可以观察到一些属性,包括简单属性,一对一关系和一对多关系。一对多关系的观察者被告知所做更改的类型,以及更改涉及哪些对象。
一、基本使用
1.1 注册观察者
/// 注册观察者
/// @param observer 观察者
/// @param keyPath 要观察的属性keyPath
/// @param options 观察者选项。影响通知的生成方式及回调时字典中携带的信息
/// @param context 上下文
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context;
context
接收一个void *
类型的参数,基本可以传任何类型。假如子类和他的父类
由于不同的原因都注册了对同一个属性
的观察,在回调中这两种的处理是不同的,那么回调中的keyPath和被观察者对象是无法区分的,此时就可以通过context
这个参数来区分。
1.2 实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
1.3 移除观察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
1.4观察集合类型属性
@interface Animal : NSObject
@property (nonatomic,strong) NSMutableArray *friends;
@end
//viewContoller代码
- (void)viewDidLoad {
[super viewDidLoad];
Animal *animal = [Animal alloc];
animal.friends = @[].mutableCopy;
[animal addObserver:self
forKeyPath:@"friends"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
//1、被动触发错误方式:这里无法触发`kvo`回调
[animal.friends addObject:@"dog"];
//2、被动触发正确方法
[[animal mutableArrayValueForKey:@"friends"] addObject:@"dog"];
//3、手动触发
[animal willChangeValueForKey:@"friends"];
[animal.friends addObject:@"dog"];
[animal didChangeValueForKey:@"friends"];
}
第23
行代码相当于
NSMutableArray *tmp = [NSMutableArray arrayWithArray:animal.friends];
[tmp addObject:@"dog"];
animal.friends = tmp;
因此触发了kvo
,21
行,因为KVO是给予set方法的,这样不会触发set方法,所以就不会触发KVO通知。
1.5多属性的关联
我们需要在被观察者类重写两个方法:
- 一个系统方法
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
或者+ (NSSet *)keyPathsForValuesAffecting<xxx>
- 一个是被观察属性的
getter
方法。
例如:有一个Downloader.h
类,有三个属性totalBytes
,completedBytes
,和百分比进度progress
:
// Downloader.h
@interface Downloader : NSObject
@property (nonatomic) unsigned long long totalBytes;
@property (nonatomic) unsigned long long completedBytes;
@property (nonatomic, copy) NSString *progress;
@end
在UI层我们只关注progress
,但进度是受其他两个属性共同影响的,此时需要在Downloader.m
实现中重写两个方法:
@implementation Downloader
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"progress"]) {
NSArray *dependKeys = @[@"totalBytes", @"completedBytes"];
keyPaths = [keyPaths setByAddingObjectsFromArray:dependKeys];
}
return keyPaths;
}
- (NSString *)progress {
if (0 == self.totalBytes || 0 == self.completedBytes) {
return @"0";
}
double progress = (double)self.completedBytes / (double)self.totalBytes * 100;
if (progress > 100) {
progress = 100;
}
return [NSString stringWithFormat:@"%d%%", (int)ceil(progress)];
}
@end
二、KVO实现原理
Automatic key-value observing is implemented using a technique called
isa-swizzling
. 具体参考苹果文档
当一个类的实例第一次注册观察者时,系统会做以下事情:
- 动态生成一个继承自该类的中间类:
NSKVONotifying_xxx
- 将对象的isa指向这个中间类(
isa-swizzling
) - 观察的是
setter
- 子类中重写
set<xxx>
、-class
、-dealloc
方法,添加一个-_isKVOA
方法,依然返回原类,而非子类 - 移除所有的观察后,isa会指回来,但是动态子类不会销毁
2.1 原理验证
被观察类Animal
添加代码:
@interface Animal : NSObject{
@public
NSString *nickName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic,strong) NSMutableArray *friends;
@end
viewController
添加代码:
- (void)viewDidLoad {
[super viewDidLoad];
Animal *animal = [Animal alloc];
[self printClasses:[animal class]];
[self printMethods:[animal class]];
[animal addObserver:self
forKeyPath:@"nickName"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
[animal addObserver:self
forKeyPath:@"name"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
[animal addObserver:self
forKeyPath:@"friends"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:NULL];
printf("\n********************************************************\n\n");
[self printClasses:[animal class]];
[self printMethods:NSClassFromString(@"NSKVONotifying_Animal")];
animal.name = @"dog";
animal->nickName = @"cat";
}
/// 打印出指定类及其子类列表
- (void)printClasses:(Class)cls {
int count = objc_getClassList(NULL, 0);
NSMutableArray *results = [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])) {
[results addObject:classes[i]];
}
}
NSLog(@"\nClasses: %@", results);
free(classes);
}
/// 打印出指定类所有的方法
- (void)printMethods:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
printf("Methods of class: %s (\n", NSStringFromClass(cls).UTF8String);
for (int i = 0; i < count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = method_getImplementation(method);
printf(" %s-%p\n", NSStringFromSelector(sel).UTF8String, imp);
}
printf(")\n");
free(methodList);
}
控制台
打印结果 :
2020-04-13 15:08:51.803441+0800 kvo[23854:22631006]
Classes: (
Animal
)
Methods of class: Animal (
.cxx_destruct-0x10f868e50
name-0x10f868d80
setName:-0x10f868db0
friends-0x10f868df0
setFriends:-0x10f868e10
)
********************************************************
2020-04-13 15:08:51.809383+0800 kvo[23854:22631006]
Classes: (
Animal,
"NSKVONotifying_Animal"
)
Methods of class: NSKVONotifying_Animal (
setFriends:-0x7fff25701c8a
setName:-0x7fff25701c8a
class-0x7fff2570074d
dealloc-0x7fff257004b2
_isKVOA-0x7fff257004aa
)
2020-04-13 15:08:51.809932+0800 kvo[23854:22631006] -------------------{
kind = 1;
new = dog;
old = "<null>";
}
通过上面打印结果发现:只有属性发生了回调,实例变量并没有。它们的区别就是有没有setter方法,所以我们得出结果:KVO是通过setter方法进行处理回调的。
苹果官方推荐尽量使用属性点语法的形式为属性赋值和访问属性,这样其实是在调用setter和getter,如果重写了setter和getter在期中增加了额外代码,可以保证代码执行的正确性。
在viewController
中继续添加代码,移除所有的观察者。
[self performSelector:@selector(removeAllObserver) withObject:nil afterDelay:2];
- (void)removeAllObserver{
[_animal removeObserver:self forKeyPath:@"nickName"];
[_animal removeObserver:self forKeyPath:@"name"];
[_animal removeObserver:self forKeyPath:@"friends"];
printf("\n********************************************************\n\n");
[self printClasses:[_animal class]];
}
打印结果:
2020-04-13 15:20:17.770486+0800 kvo[24356:22641029]
Classes: (
Animal,
"NSKVONotifying_Animal"
)
你也可以通过
lldb
,来探索一下,整个过程中isa指针的指向,object_getClassName(animal)
2.2 kvc 和 kvo
苹果文档有介绍,在理解KVO
之前,必须先理解KVC
。上篇文章我们也讨论了KVC
的实现原理,KVC
会先查找setter
或getter
进行调用,如果没有查找到,则调用类方法+accessInstanceVariablesDirectly
,如果返回YES
,再去查找成员变量。
KVO也有类似的机制,在KVO接口中有这三个接口:
- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
+automaticallyNotifiesObserversForKey:
默认返回YES
,动态创建的中间类重写了setter
,虽然无法看到实现源码,但可以猜测在修改属性前后分别调用了-willChangeValueForKey:
和-didChangeValueForKey:
类似方法,达到通知观察者的目的。
如果子类中重载了+automaticallyNotifiesObserversForKey:
并返回NO
,则无法触发自动KVO
通知机制,但我们可以通过手动调用-willChangeValueForKey:
和-didChangeValueForKey:
来触发KVO
回调。
三、自定义KVO
系统kvo使用时存在不方便的地方,根据kvo的原理和基本使用,我们可以简单自定义kvo实现。
- 入参检查
- 检查是否有属性的setter
- 动态创建对象子类BLKVOClass_xxx
- isa-swizzling
- 重写-class、-dealloc方法
- 重写setter
- 保存观察者信息,在属性发生变化时回调
3.1 动态创建对象子类
Class newClass = NSClassFromString(newClassName);
if (newClass) {
return newClass;
}
/**
* 如果内存不存在,创建生成
* 参数一: 父类
* 参数二: 新类的名字
* 参数三: 新类的开辟的额外空间
*/
// 2.1 : 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2 : 注册类
objc_registerClassPair(newClass);
3.2 isa-swizzling
重写class方法
Class mm_class(id self,SEL _cmd){
return class_getSuperclass(object_getClass(self));
}
动态子类添加class实现,完成isa-swizzling
// 2.3.1 : 添加class : class的指向是父类
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod(newClass, classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)mm_class, classTypes);
3.3 dealloc
重写delloc,1、安全移除所有observe,2、销毁关联对象,3、isa指回父类,4、调用系统dealloc。
- (void)mm_dealloc {
// Class superClass = [self class];
Class superCls = class_getSuperclass(object_getClass(self));
object_setClass(self, superCls);
// Call system -dealloc
[self mm_dealloc];
}
四、FBKVOController
下面简单聊一下FBKVOController,它里面有几个关键类:
-
_FBKVOSharedController
,单利对象,处理、转发KVOViewController
传过来的所有观察者事件。 -
_FBKVOInfo
,数据模型,保存一个完整的KVO
数据。 -
KVOViewController
,每个观察者都有一个该类的实例对象,这个类用于处理观察者传过来的所有数据,下图是他的主要属性构成。 KVOViewController.png
下面是一个简单的调用实现代码:
- (void)viewDidLoad {
[super viewDidLoad];
Student *student = [[Student alloc] init];
FBKVOController *kvoCtrl = [FBKVOController controllerWithObserver:self];
[kvoCtrl observe:student keyPath:@"nickName" options:NSKeyValueObservingOptionNew block:^(id _Nullable observer, id _Nonnull object, NSDictionary<NSString *,id> * _Nonnull change) {
NSLog(@"****%@****",change);
}];
student.nickName = @"kkk";
}
observe
对应viewController
,student对应object
。当viewController
被释放的时候,会先调用FBKVOController
的dealloc
方法,在这里会将_objectInfosMap
里所有的被观察者安全得 remove
。