使用KVO
自動觸發KVO
在平日代碼中,我們通過KVO來監視實例某個屬性的變化。
比如,我們要監視Student 的 age屬性,可以這麼做:
@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *std = [Student new];
std.name = @"Tom";
[std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
...
}
@end
我們使用KVO需要遵循以下步驟:
- 調用
addObserver:forKeyPath:options:context:
方法來註冊觀察者,觀察者可以接收到KeyPath對應屬性的修改通知 - 當觀察的屬性發生變化時,系統會在
observeValueForKeyPath:ofObject:change:context:
方法中回調觀察者 - 當觀察者不需要監聽變化是,需要調用
removeObserver:forKeyPath:
將KVO
移除。需要注意的是,在觀察者被釋放前,必須要調用removeObserver:forKeyPath:
將其移除,否則會crash。
手動觸發KVO
當我們設置了觀察者後,當被觀察的keyPath對應的setter方法調用後,則會自動的觸發KVO的回調函數。那麼,有時候我們想要控制這種自動觸發的機制,該怎麼辦呢?你可以重寫如下方法:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
automaticallyNotifiesObserversForKey
方法聲明在NSObject的Category NSObject(NSKeyValueObservingCustomization)
中。
除了在setter方法中,有時候我們想主動觸發一下KVO,該怎麼辦呢?
那就需要使用
willChangeValueForKey:
didChangeValueForKey:
來通知系統Key Value發生了改變。如:
- (void)updateName:(NSString *)name {
[self willChangeVauleForKey:@"name"];
_name = name;
[self didChangeVauleForKey:@"name"];
}
KVO實現機制
那麼,KVO背後是如何實現的呢?在蘋果的官方文檔上,有一個籠統的描述:
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.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 method to determine the
class of an object instance.
主要說了兩件事:
- KVO是基於
isa-swizzling
技術實現的。isa-swizzling
會將被觀察對象的isa指針
進行替換。 - 因爲在實現
KVO
時,系統會替換掉被觀察對象的isa指針
,因此,不要使用isa指針
來判斷類的關係,而應該使用class
方法。
爲什麼要替換掉isa指針
?文檔中說的很清楚,因爲isa指針
會指向類實例對應的類的方法列表,而替換掉了isa指針
,相當於替換掉了類的方法列表。
那麼爲啥要替換類的方法列表呢?又是怎麼替換的呢?文檔到這裏戛然而止,沒有細說。
下面,我們就用代碼實驗的方式,來窺探一下KVO的實現機制。
準備如下代碼:
@interface Student : NSObject
@property(nonatomic, strong) NSString *name;
@property(nonatomic, strong) NSMutableArray *friends;
@end
@implementation Student
- (void)showObjectInfo {
NSLog(@"Object instance address is %p, Object isa content is %p", self, *((void **)(__bridge void *)self));
}
@end
我們在Student類中定義了方法- (void)showObjectInfo
,主要是用來打印Student實例的地址
,以及Student 的isa指針中
的內容。這可以用來研究系統是如何做isa-swizzling
操作的。
然後準備下面的方法,來打印類的方法列表:
static NSArray * ClassMethodNames(Class c)
{
NSMutableArray * array = [NSMutableArray array];
unsigned int methodCount = 0;
Method * methodList = class_copyMethodList(c, &methodCount);
unsigned int i;
for(i = 0; i < methodCount; i++) {
[array addObject: NSStringFromSelector(method_getName(methodList[i]))];
}
free(methodList);
return array;
}
運行如下代碼
- (void)viewDidLoad {
[super viewDidLoad];
Student *std = [Student new];
// 1. 初始值
std.name = @"Tom";
NSLog(@"std->isa:%@", object_getClass(std));
NSLog(@"std class:%@", [std class]);
NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
[std showObjectInfo];
// 2. 添加KVO
[std addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[std addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
NSLog(@"std->isa:%@", object_getClass(std));
NSLog(@"std class:%@", [std class]);
NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
[std showObjectInfo];
std.name = @"Jack";
// 3. 移除KVO
[std removeObserver:self forKeyPath:@"name"];
[std removeObserver:self forKeyPath:@"friends"];
NSLog(@"std->isa:%@", object_getClass(std));
NSLog(@"std class:%@", [std class]);
NSLog(@"ClassMethodNames:%@", ClassMethodNames(object_getClass(std)));
[std showObjectInfo];
}
輸出爲:
// 1. 初始值
std->isa:Student
std class:Student
ClassMethodNames:(
showObjectInfo,
"setFriends:",
friends,
".cxx_destruct",
"setName:",
name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd
// 2. 添加KVO
std->isa:NSKVONotifying_Student
std class:Student
ClassMethodNames:(
"setFriends:",
"setName:",
class,
dealloc,
"_isKVOA"
)
Object address is 0x28194fe80, Object isa content is 0x1a282b5bf05
// 3. 移除KVO
std->isa:Student
std class:Student
ClassMethodNames:(
showObjectInfo,
"setFriends:",
friends,
".cxx_destruct",
"setName:",
name
)
Object address is 0x28194fe80, Object isa content is 0x1a1008090cd
通過觀察添加KVO前、添加KVO後,移除KVO後這三個實際的Object地址信息可以知道,Object的地址並沒有改變,但是其isa指針
中的內容,卻經歷瞭如下變化:0x1a1008090cd
->0x1a282b5bf05
->0x1a1008090cd
。
對應的,通過object_getClass(std)
方法來輸出std的類型是:Student
->NSKVONotifying_Student
->Student
這就是所謂的isa-swizzling,當KVO時,系統會將被觀察對象的isa指針
內容做替換,讓其指向新的類NSKVONotifying_Student
,而在移除KVO後,系統又會將isa指針
內容還原。
那麼,NSKVONotifying_Student
這個類又是什麼樣的呢?
通過打印其方法列表,可以知道,NSKVONotifing_Stdent
定義或重寫了如下方法:
ClassMethodNames:(
"setFriends:",
"setName:",
class,
dealloc,
"_isKVOA"
)
可以看到,系統新生成的類重寫了我們KVO的屬性Friends和Name的set方法
。
同時,還重寫了class方法
。通過runtime的源碼可以知道,class方法
實際是調用了object_getClass方法
:
- (Class)class {
return object_getClass(self);
}
而在object_getClass方法
中,會輸出實例的isa指向的類:
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
按說[std class]
和object_getClass(std)
的輸出應該一致,但是系統會在KVO的時候,悄悄改寫實例的class方法
。這也就是爲什麼,當使用[std class]
方法打印實例的類時,會輸出Student
而不是實際的NSKVONotifing_Student
。
然後系統還重寫了dealloc
方法,估計是爲了在實例銷燬時,做一些檢查及清理工作。
最後,添加了_isKVOA
方法,這估計是系統爲了識別是KVO類
而添加的。
這裏,細心的同學會發現,在KVO之前,Student的方法列表裏面是包含屬性的get方法
,showObjectInfo
方法以及.cxx_destruct
這些方法的。而當系統將Student
替換爲NSKVONotifing_Student
後,這些方法那裏去了呢?如果這些方法沒有在NSKVONotifing_Student
再實現一遍的話,那當KVO後,我們再調用屬性的get方法
、showObjectInfo方法
豈不是會crash?
但平日的編程實踐告訴我們,並不會crash。那這些方法都去那裏了呢?讓我們來看一下NSKVONotifing_Student
的父類是什麼:
// 2. 添加KVO
...
Class objectRuntimeClass = object_getClass(std);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"super class is %@", superClass);
輸出爲:
super class is Student
哈哈,很有意思吧,原來NSKVONotifing_Student
的父類竟然是Student
。那根據OC的消息實現機制,當在NSKVONotifing_Student
中沒有找到方法實現時,會自動到其父類Student
中尋找相應的實現。因此,在NSKVONotifing_Student
中,僅僅需要定義或重寫KVO相關的方法即可,至於Student
中定義的其他方法,則會在消息機制中在被自動找到。
以上,便是KVO的isa-swizzling技術
的大體實現流程。讓我們總結一下:
- 當類實例被KVO後,系統會替換實例的isa指針內容。讓其指向
NSKVONotifing_XX
類型的新類。 - 在
NSKVONotifing_XX
類中,會:重寫KVO屬性的set方法
,支持KVO。重寫class方法
,來僞裝自己仍然是XX類。添加_isKVOA方法
,來說明自己是一個KVO類。重寫dealloc方法
,讓實例下析構時,好做一些檢查和清理工作 - 爲了讓用戶在KVO isa-swizzling後,仍然能夠調用原始
XX類
中的方法,系統還會將NSKVONotifing_XX
類設置爲原始XX類的子類
。 - 當移除KVO後,系統會將
isa指針
中的內容復原。
手動實現KVO
既然知道了KVO背後的實現原理,我們能不能利用runtime方法,模擬實現一下KVO呢?
當然可以,下來看下效果:
#import "ViewController.h"
#import "NSObject+KVOBlock.h"
#import <objc/runtime.h>
@implementation Student
@end
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Student *std = [Student new];
// 直接用block回調來接受 KVO
[std sw_addObserver:self forKeyPath:@"name" callback:^(id _Nonnull observedObject, NSString * _Nonnull observedKeyPath, id _Nonnull oldValue, id _Nonnull newValue) {
NSLog(@"old value is %@, new vaule is %@", oldValue, newValue);
}];
std.name = @"Hello";
std.name = @"Lilhy";
NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
[std sw_removeObserver:self forKeyPath:@"name"];
NSLog(@"class is %@, object_class is %@", [std class], object_getClass(std));
}
@end
爲了模擬的和系統KVO實現類似,我們也改寫了class方法,在KVO移除前後,打印std的類信息爲:
class is Student, object_class is sw_KVONotifing_Student
// 移除KVO後
class is Student, object_class is Student
在這裏我手動實現了KVO,並通過Block的方式來接受KVO的回調信息。接下來我們就一步步的分析是如何做到的。我們應該重點觀察所使用到的runtime方法。
首先,我們新建一個NSObject的分類NSObject (KVOBlock)
,並聲明如下方法:
typedef void(^sw_KVOObserverBlock)(id observedObject, NSString *observedKeyPath, id oldValue, id newValue);
@interface NSObject (KVOBlock)
- (void)sw_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
callback:(sw_KVOObserverBlock)callback;
- (void)sw_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
@end
在關鍵的sw_addObserver:forKeyPath:callback:
中,是這麼實現的:
static void *const sw_KVOObserverAssociatedKey = (void *)&sw_KVOObserverAssociatedKey;
static NSString *sw_KVOClassPrefix = @"sw_KVONotifing_";
- (void)sw_addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
callback:(sw_KVOObserverBlock)callback {
// 1. 通過keyPath獲取當前類對應的setter方法,如果獲取不到,說明setter 方法即不存在與KVO類,也不存在與原始類,這總情況正常情況下是不會發生的,觸發Exception
NSString *setterString = sw_setterByGetter(keyPath);
SEL setterSEL = NSSelectorFromString(setterString);
Method method = class_getInstanceMethod(object_getClass(self), setterSEL);
if (method) {
// 2. 查看當前實例對應的類是否是KVO類,如果不是,則生成對應的KVO類,並設置當前實例對應的class是KVO類
Class objectClass = object_getClass(self);
NSString *objectClassName = NSStringFromClass(objectClass);
if (![objectClassName hasPrefix:sw_KVOClassPrefix]) {
Class kvoClass = [self makeKvoClassWithOriginalClassName:objectClassName]; // 爲原始類創建KVO類
object_setClass(self, kvoClass); // 將當前實例的類設置爲KVO類
}
// 3. 在KVO類中查找是否重寫過keyPath 對應的setter方法,如果沒有,則添加setter方法到KVO類中
// 注意,此時object_getClass(self)獲取到的class應該是KVO class
if (![self hasMethodWithMethodName:setterString]) {
class_addMethod(object_getClass(self), NSSelectorFromString(setterString), (IMP)sw_kvoSetter, method_getTypeEncoding(method));
}
// 4. 註冊Observer
NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
if (observerArray == nil) {
observerArray = [NSMutableArray new];
objc_setAssociatedObject(self, sw_KVOObserverAssociatedKey, observerArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
SWKVOObserverItem *item = [SWKVOObserverItem new];
item.keyPath = keyPath;
item.observer = observer;
item.callback = callback;
[observerArray addObject:item];
}else {
NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
[exception raise];
}
}
上面的函數重點是:
- 調用
makeKvoClassWithOriginalClassName
方法來生成原始類對應的KVO類
- 利用
class_addMethod
方法,爲KVO類
添加改寫的setter實現
完成了上面兩點,一個手工的KVO實現基本就完成了。另一個需要注意的是,如何存儲observer。在這裏是通過一個MutableArray數組,當做Associated object
來存儲到類實例中的。
可以看出來,這裏的重點在於如何創建原始類對應的KVO類
:
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClassName {
// 1. 檢查KVO類是否已經存在, 如果存在,直接返回
NSString *kvoClassName = [NSString stringWithFormat:@"%@%@", sw_KVOClassPrefix, originalClassName];
Class kvoClass = objc_getClass(kvoClassName.UTF8String);
if (kvoClass) {
return kvoClass;
}
// 2. 創建KVO類,並將原始class設置爲KVO類的super class
kvoClass = objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0);
objc_registerClassPair(kvoClass);
// 3. 重寫KVO類的class方法,使其指向我們自定義的IMP,實現KVO class的‘僞裝’
Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
const char* types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);
return kvoClass;
}
其實實現也不難,調用了runtime的方法
- objc_allocateClassPair(object_getClass(self), kvoClassName.UTF8String, 0) 動態生成新的
KVO類
,並設置KVO類
的super class是原始類 - 註冊
KVO類
: objc_registerClassPair(kvoClass) - 爲了實現KVO僞裝成原始類,還爲
KVO類
添加了我們自己重寫的class方法:
Method classMethod = class_getInstanceMethod(object_getClass(self), @selector(class));
const char* types = method_getTypeEncoding(classMethod);
class_addMethod(kvoClass, @selector(class), (IMP)sw_class, types);
// 自定義的class方法實現
static Class sw_class(id self, SEL selector) {
return class_getSuperclass(object_getClass(self)); // 因爲我們將原始類設置爲了KVO類的super class,所以直接返回KVO類的super class即可得到原始類Class
}
那麼當我們需要移除Observer時,需要調用sw_removeObserver:forKeyPath:
方法:
- (void)sw_removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath {
NSMutableArray<SWKVOObserverItem *> *observerArray = objc_getAssociatedObject(self, sw_KVOObserverAssociatedKey);
[observerArray enumerateObjectsUsingBlock:^(SWKVOObserverItem * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (obj.observer == observer && [obj.keyPath isEqualToString:keyPath]) {
[observerArray removeObject:obj];
}
}];
if (observerArray.count == 0) { // 如果已經沒有了observer,則把isa復原,銷燬臨時的KVO類
Class originalClass = [self class];
Class kvoClass = object_getClass(self);
object_setClass(self, originalClass);
objc_disposeClassPair(kvoClass);
}
}
注意,這裏當Observer數組爲空時,我們會將當前實例的所屬類復原成原始類,並dispose掉生成的KVO類
。
完整的源碼在這裏