1.概述
KVO,即:Key-Value Observing淮椰,是 Objective-C 對 觀察者模式(Observer Pattern)的實現(xiàn)蟹漓。它提供一種機制栏赴,當(dāng)指定的對象的屬性被修改后镊辕,觀察者就會接受到通知痛阻。簡單的說就是每次指定的被觀察的對象的屬性被修改后菌瘪,KVO就會自動通知相應(yīng)的觀察者了。
2.使用
1. 基本使用
KVO本質(zhì)上是基于runtime的動態(tài)分發(fā)機制,通過key來監(jiān)聽value的值俏扩。
OC能夠?qū)崿F(xiàn)監(jiān)聽因為都遵守了NSKeyValueCoding協(xié)議糜工。OC所有的類都是繼承自NSObject,其默認(rèn)已經(jīng)遵守了該協(xié)議录淡,但Swift不是基于runtime的捌木。Swift中繼承自NSObject的屬性處于性能等方面的考慮,默認(rèn)是關(guān)閉動態(tài)分發(fā)的嫉戚, 所以無法使用KVO刨裆,只有在屬性前加 @objc dynamic 才會開啟運行時,允許監(jiān)聽屬性的變化彬檀。
在Swift3中只需要加上dynamic就可以了帆啃,而Swift4以后則還需要@objc
- 注冊
- (void)addObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context;
observer:觀察者,也就是KVO通知的訂閱者窍帝。訂閱著必須實現(xiàn)努潘。
keyPath:描述將要觀察的屬性,相對于被觀察者盯桦。
options:KVO的一些屬性配置慈俯;有四個選項渤刃。
NSKeyValueObservingOptionNew:change字典包括改變后的值
NSKeyValueObservingOptionOld:change字典包括改變前的值
NSKeyValueObservingOptionInitial:注冊后立刻觸發(fā)KVO通知
NSKeyValueObservingOptionPrior:值改變前是否也要通知(這個key決定了是否在改變前改變后通知兩次)
- 監(jiān)聽
在觀察者內(nèi)重寫這個方法拥峦。在屬性變化時,觀察者則可以在函數(shù)內(nèi)對屬性變化做處理卖子。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
- 移除
在不用的時候略号,不要忘記解除注冊,否則會導(dǎo)致內(nèi)存泄露洋闽。
- (void)removeObserver:(NSObject *)observer
forKeyPath:(NSString *)keyPath;
- 舉例
class ObservedClass: NSObject {
// 開啟運行時玄柠,允許監(jiān)聽屬性的變化
@objc dynamic var name: String = "Original"
// age 并不會觸發(fā)KVO
var age: Int = 18
}
class ViewController: UIViewController {
var observed = ObservedClass()
override func viewDidLoad() {
super.viewDidLoad()
observed.addObserver(self, forKeyPath: "age", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
// 修改屬性值,觸發(fā)KVO
observed.name = "JiangT"
observed.age = 22
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("屬性改變了")
print(keyPath)
print("change字典為:")
print(change)
}
}
---輸出結(jié)果---
屬性改變了
Optional("name")
change字典為:
Optional(
[__C.NSKeyValueChangeKey(_rawValue: new): JiangT,
__C.NSKeyValueChangeKey(_rawValue: kind): 1,
__C.NSKeyValueChangeKey(_rawValue: old): Original])
2. 手動KVO 及禁用KVO
- 首先诫舅,需要手動實現(xiàn)屬性的 setter 方法羽利,并在設(shè)置操作的前后分別調(diào)用 willChangeValueForKey: 和 didChangeValueForKey方法,這兩個方法用于通知系統(tǒng)該 key 的屬性值即將和已經(jīng)變更了刊懈。
- 其次这弧,要實現(xiàn)類方法 automaticallyNotifiesObserversForKey,并在其中設(shè)置對該 key 不自動發(fā)送通知(返回 NO 即可)虚汛。這里要注意匾浪,對其它非手動實現(xiàn)的 key,要轉(zhuǎn)交給 super 來處理卷哩。
- 如果需要禁用該類KVO的話直接automaticallyNotifiesObserversForKey返回NO蛋辈,實現(xiàn)屬性的 setter 方法,不進(jìn)行調(diào)用willChangeValueForKey: 和 didChangeValueForKey方法将谊。
主要方法:
open func willChangeValue(forKey key: String)
open func didChangeValue(forKey key: String)
class func automaticallyNotifiesObservers(forKey key: String) -> Bool
舉例
---被觀察類---
class ObservedClass: NSObject {
private var _name: String = "Original"
@objc dynamic var name: String {
get {
return _name
}
set (n) {
self.willChangeValue(forKey: "name")
_name = n
self.didChangeValue(forKey: "name")
}
}
override class func automaticallyNotifiesObservers(forKey key: String) -> Bool {
// 設(shè)置對該 key 不自動發(fā)送通知
if key == "name" {
return false
}
return super.automaticallyNotifiesObservers(forKey: key)
}
}
class ViewController: UIViewController {
var observed = ObservedClass()
override func viewDidLoad() {
super.viewDidLoad()
observed.addObserver(self, forKeyPath: "name", options: [NSKeyValueObservingOptions.new, NSKeyValueObservingOptions.old], context: nil)
// 修改屬性值冷溶,觸發(fā)KVO
observed.name = "JiangT"
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
print("屬性改變了")
print(keyPath)
print("change字典為:")
print(change)
}
}
---輸出結(jié)果---
屬性改變了
Optional("name")
change字典為:
Optional([__C.NSKeyValueChangeKey(_rawValue: kind): 1,
__C.NSKeyValueChangeKey(_rawValue: old): Original,
__C.NSKeyValueChangeKey(_rawValue: new): JiangT])
3. 實現(xiàn)原理
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.
大致意思為:
蘋果使用了一種isa交換的技術(shù)渐白,當(dāng)ObjectA的被觀察后,ObjectA對象的isa指針被指向了一個新建的子類NSKVONotifying_ObjectA逞频,且這個子類重寫了被觀察值的setter方法和class方法礼预,dealloc和isKVO方法,然后使ObjectA對象的isa指針指向這個新建的類虏劲,然后事實上ObjectA變?yōu)榱薔SKVONotifying ObjectA的實例對象托酸,執(zhí)行方法要從這個類的方法列表里找。
KVO是基于runtime機制實現(xiàn)的柒巫。
當(dāng)某個類的屬性對象第一次被觀察時励堡,系統(tǒng)就會在運行期動態(tài)地創(chuàng)建該類的一個派生類(如果原類為ObservedClass,那么生成的派生類名為NSKVONotifying_ObservedClass)堡掏,在這個派生類中重寫基類中任何被觀察屬性的setter方法应结。派生類在被重寫的setter方法內(nèi)實現(xiàn)真正的通知機制
- 每個類對象中都有一個isa指針指向當(dāng)前類,當(dāng)一個類對象的第一次被觀察泉唁,那么系統(tǒng)會偷偷將isa指針指向動態(tài)生成的派生類(isa-swizzling鹅龄,后續(xù)Runtime學(xué)習(xí)記錄中展開),從而在給被監(jiān)控屬性賦值時執(zhí)行的是派生類的setter方法亭畜。派生類中還偷偷重寫了class方法扮休,讓我們誤認(rèn)為還是使用的當(dāng)前類,從而達(dá)到隱藏生成的派生類拴鸵。
測試代碼
@interface KVOObject : NSObject
@property (nonatomic, copy ) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end
@implementation KVOObject
- (NSString *)description {
NSLog(@"object address : %p \n", self);
IMP nameIMP = class_getMethodImplementation(object_getClass(self), @selector(setName:));
IMP ageIMP = class_getMethodImplementation(object_getClass(self), @selector(setAge:));
NSLog(@"object setName: IMP %p object setAge: IMP %p \n", nameIMP, ageIMP);
Class objectMethodClass = [self class];
Class objectRuntimeClass = object_getClass(self);
Class superClass = class_getSuperclass(objectRuntimeClass);
NSLog(@"objectMethodClass : %@, ObjectRuntimeClass : %@, superClass : %@ \n", objectMethodClass, objectRuntimeClass, superClass);
NSLog(@"object method list \n");
unsigned int count;
Method *methodList = class_copyMethodList(objectRuntimeClass, &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
NSLog(@"method Name = %@\n", methodName);
}
return @"";
}
在另一個類中分別創(chuàng)建兩個KVOObject對象玷坠,其中一個對象被觀察者通過KVO的方式監(jiān)聽,另一個對象則始終沒有被監(jiān)聽劲藐。在KVO前后分別打印兩個對象的關(guān)鍵信息八堡,看KVO前后有什么變化。
@property (nonatomic, strong) KVOObject *object1;
@property (nonatomic, strong) KVOObject *object2;
self.object1 = [[KVOObject alloc] init];
self.object2 = [[KVOObject alloc] init];
[self.object1 description];
[self.object2 description];
[self.object1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
[self.object1 description];
[self.object2 description];
self.object1.name = @"lxz";
self.object1.age = 20;
下面是KVO前后的打印信息
// 第一次
object address : 0x604000239340
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
// 第二次
object address : 0x604000239340
object setName: IMP 0x10ea8defe object setAge: IMP 0x10ea94106
objectMethodClass : KVOObject, ObjectRuntimeClass : NSKVONotifying_KVOObject, superClass : KVOObject
object method list
method Name = setAge:
method Name = setName:
method Name = class
method Name = dealloc
method Name = _isKVOA
object address : 0x604000237920
object setName: IMP 0x10ddc2770 object setAge: IMP 0x10ddc27d0
objectMethodClass : KVOObject, ObjectRuntimeClass : KVOObject, superClass : NSObject
object method list
method Name = .cxx_destruct
method Name = description
method Name = name
method Name = setName:
method Name = setAge:
method Name = age
我們發(fā)現(xiàn)對象被KVO后聘芜,其真正類型變?yōu)榱薔SKVONotifying_KVOObject類兄渺,已經(jīng)不是之前的類了。KVO會在運行時動態(tài)創(chuàng)建一個新類汰现,將對象的isa指向新創(chuàng)建的類挂谍,新類是原類的子類,命名規(guī)則是NSKVONotifying_xxx的格式服鹅。KVO為了使其更像之前的類凳兵,還會將對象的class實例方法重寫,使其更像原類企软。
自定義kvo實現(xiàn)
static void *const zs_KVOObserverAssociatedKey = (void *)&zs_KVOObserverAssociatedKey;
static NSString *zs_KVOClassPrefix = @"zs_KVONotifying_";
@implementation KVOObserverItem
- (instancetype)initWithObserver:(NSObject *)observer
key:(NSString *)key
block:(zs_KVOObserverBlock)block {
self = [super init];
if (self) {
self.observer = observer;
self.key = key;
self.block = block;
}
return self;
}
@end
- (void)zs_addObserver:(NSObject *)observer
keyPath:(NSString *)keyPath
callback:(zs_KVOObserverBlock)callback {
// 1. 通過Method判斷是否有這個key對應(yīng)的selector庐扫,如果沒有則Crash。
SEL originalSetter = NSSelectorFromString(zs_setterForGetter(keyPath));
Method originalMethod = class_getInstanceMethod(object_getClass(self), originalSetter);
if (!originalMethod) {
NSString *exceptionReason = [NSString stringWithFormat:@"%@ Class %@ setter SEL not found.", NSStringFromClass([self class]), keyPath];
NSException *exception = [NSException exceptionWithName:@"NotExistKeyExceptionName" reason:exceptionReason userInfo:nil];
[exception raise];
}
// 2. 判斷當(dāng)前類是否是KVO子類,如果不是則創(chuàng)建形庭,并設(shè)置其isa指針铅辞。
Class kvoClass = object_getClass(self);
NSString *kvoClassString = NSStringFromClass(kvoClass);
if (![kvoClassString hasPrefix:zs_KVOClassPrefix]) {
kvoClass = [self zs_makeKVOClassWithName:kvoClassString];
object_setClass(self, kvoClass);
}
// 3. 如果沒有實現(xiàn),則添加Key對應(yīng)的setter方法萨醒。
if (![self zs_hasMethodWithKey:originalSetter]) {
class_addMethod(kvoClass, originalSetter, (IMP)zs_kvoSetter, method_getTypeEncoding(originalMethod));
}
// 4. 將調(diào)用對象添加到數(shù)組中斟珊。
KVOObserverItem *observerItem = [[KVOObserverItem alloc] initWithObserver:observer key:keyPath block:callback];
NSMutableArray<KVOObserverItem *> *observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
if (observers == nil) {
observers = [NSMutableArray array];
}
[observers addObject:observerItem];
objc_setAssociatedObject(self, zs_KVOObserverAssociatedKey, observers, OBJC_ASSOCIATION_RETAIN);
}
- (void)zs_removeObserver:(NSObject *)observer
keyPath:(NSString *)keyPath {
NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
[observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
if (mapTable.observer == observer && keyPath == mapTable.key) {
[observers removeObject:mapTable];
}
}];
}
#pragma mark - ----- Private Method Or Funcation ------
static void zs_kvoSetter(id self, SEL selector, id value) {
// 1. 獲取舊值。
id (*getterMsgSend) (id, SEL) = (void *)objc_msgSend;
NSString *getterString = zs_getterForSetter(selector);
SEL getterSelector = NSSelectorFromString(getterString);
id oldValue = getterMsgSend(self, getterSelector);
// 2. 創(chuàng)建super的結(jié)構(gòu)體富纸,并向super發(fā)送屬性的消息囤踩。
id (*msgSendSuper) (void *, SEL, id) = (void *)objc_msgSendSuper;
struct objc_super objcSuper = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
msgSendSuper(&objcSuper, selector, value);
// 3. 遍歷調(diào)用block。
NSMutableArray <KVOObserverItem *>* observers = objc_getAssociatedObject(self, zs_KVOObserverAssociatedKey);
[observers enumerateObjectsUsingBlock:^(KVOObserverItem * _Nonnull mapTable, NSUInteger idx, BOOL * _Nonnull stop) {
if ([mapTable.key isEqualToString:getterString] && mapTable.block) {
mapTable.block(self, NSStringFromSelector(selector), oldValue, value);
}
}];
}
- (BOOL)zs_hasMethodWithKey:(SEL)key {
NSString *setterName = NSStringFromSelector(key);
unsigned int count;
Method *methodList = class_copyMethodList(object_getClass(self), &count);
for (NSInteger i = 0; i < count; i++) {
Method method = methodList[i];
NSString *methodName = NSStringFromSelector(method_getName(method));
if ([methodName isEqualToString:setterName]) {
return YES;
}
}
return NO;
}
static NSString * zs_getterForSetter(SEL setter) {
NSString *setterString = NSStringFromSelector(setter);
if (![setterString hasPrefix:@"set"]) {
return nil;
}
NSString *getterString = [setterString substringWithRange:NSMakeRange(4, setterString.length - 5)];
NSString *firstString = [setterString substringWithRange:NSMakeRange(3, 1)];
firstString = [firstString lowercaseString];
getterString = [NSString stringWithFormat:@"%@%@", firstString, getterString];
return getterString;
}
static NSString * zs_setterForGetter(NSString *getter) {
NSString *getterString = getter;
NSString *firstString = [getterString substringToIndex:1];
firstString = [firstString uppercaseString];
NSString *setterString = [getterString substringFromIndex:1];
setterString = [NSString stringWithFormat:@"set%@%@:", firstString, setterString];
return setterString;
}
- (Class)zs_makeKVOClassWithName:(NSString *)name {
// 1. 判斷是否存在KVO類晓褪,如果存在則返回堵漱。
NSString *className = [NSString stringWithFormat:@"%@%@", zs_KVOClassPrefix, name];
Class kvoClass = objc_getClass(className.UTF8String);
if (kvoClass) {
return kvoClass;
}
// 2. 如果不存在,則創(chuàng)建KVO類涣仿。
kvoClass = objc_allocateClassPair(object_getClass(self), className.UTF8String, 0);
objc_registerClassPair(kvoClass);
// 3. 重寫KVO類的class方法勤庐,指向自定義的IMP。
Method method = class_getInstanceMethod(object_getClass(self), @selector(class));
const char *types = method_getTypeEncoding(method);
class_addMethod(kvoClass, @selector(class), (IMP)zs_kvoClass, types);
return kvoClass;
}
static Class zs_kvoClass(id self, SEL selector) {
return class_getSuperclass(object_getClass(self));
}