概述
KVO全程KeyValueObserving,是蘋果提供的一套鍵值觀察機制,它可以在對象指定屬性發(fā)生改變時接到通知。
基礎使用
KVO使用分為三個步驟:
1.通過addObserver:forKeyPath:options:context:注冊觀察者。觀察者可以接收keyPath屬性變化通知。
2.在觀察者中實現(xiàn)observeValueForKeyPath:ofObject:change:context:,以接收觀察屬性改變的通知消息
3.當觀察者不再需要接收消息時,使用removeObserver:forKeyPath:移除觀察者。在觀察者在內(nèi)存中釋放之前,必須移除,否者會crash
注冊函數(shù)
注冊觀察者時,需要傳入一個Option參數(shù),是一個枚舉值。
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
//接收新值
NSKeyValueObservingOptionNew = 0x01,
//接收舊值
NSKeyValueObservingOptionOld = 0x02,
//一旦注冊,立馬會調(diào)用一次
NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
//在變更前后都會發(fā)送通知,而不止是變更后
NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
還可以傳入context參數(shù),上下文參數(shù)可以傳遞任意數(shù)據(jù)指針,在通知方法中返回給觀察者。我們可以通過該參數(shù),來區(qū)分發(fā)生改變的類。
static void *PersonAccountBalanceContext = &PersonAccountBalanceContext;
static void *PersonAccountInterestRateContext=&PersonAccountInterestRateContext;
- (void)registerAsObserverForAccount:(Account*)account {
[account addObserver:self forKeyPath:@"balance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) ontext:PersonAccountBalanceContext];
[account addObserver:self forKeyPath:@"interestRate" ptions:NSKeyValueObservingOptionNew | SNSKeyValueObservingOptionOld ontext:PersonAccountInterestRateContext];
}
監(jiān)聽方法
觀察者需要實現(xiàn)observeValueForKeyPath:ofObject:change:context:,KVO通知會通過這個方法傳遞,沒有實現(xiàn)會導致crash。change字典中存放KVO相關屬性的值,根據(jù)options傳入的枚舉值,可以取到數(shù)據(jù)。
change中還有NSKeyValueChangeKindKey,它也是一個枚舉值
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
//觀察對象的值已經(jīng)更改
NSKeyValueChangeSetting = 1,
//觀察對象是否插入
NSKeyValueChangeInsertion = 2,
//觀察對象是否刪除
NSKeyValueChangeRemoval = 3,
//觀察對象是否替換
NSKeyValueChangeReplacement = 4,
};
移除觀察者
你可以通過向觀察者對象發(fā)送removeObserver:forKeyPath:context:方法,指定觀察者,路徑和上下文來移除觀察者。
- (void)unregisterAsObserverForAccount:(Account*)account {
[account removeObserver:self
forKeyPath:@"balance"
context:PersonAccountBalanceContext];
[account removeObserver:self
forKeyPath:@"interestRate"
context:PersonAccountInterestRateContext];
}
注意:
- 如果你沒有注冊,就調(diào)用移除方法會導致
NSRangeException,注冊和移除方法應該成對存在。如果這在你的程序中不可行,請將removeObserver放入try/catch中,處理潛在的異常。 - 移除觀察者時,觀察者不會自動刪除自己,對已釋放的對象發(fā)送觀察通知會觸發(fā)內(nèi)存訪問異常。
- 蘋果推薦我們在觀察者初始化是注冊(如
ininit或viewDidLoad),在釋放期間移除(通常是dealloc)
自動開關與手動開關
KVO屬性發(fā)生改變時的調(diào)用一般是自動的,可以通過重寫automaticallyNotifiesObserversForKey:手動控制。(默認返回YES)
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey{
return YES;
}
手動控制nick屬性的KVO
+ (BOOL) automaticallyNotifiesObserversForKey:(NSString *)key{
return NO;
}
- (void)setNick:(NSString *)nick{
[self willChangeValueForKey:@"nick"];
_nick = nick;
[self didChangeValueForKey:@"nick"];
}
注冊依賴鍵
有時候我們不僅僅是要觀察一個屬性,而是多個屬性。需要我們重寫keyPathsForValuesAffectingValueForKey:方法。
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@",firstName, lastName];
}
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
數(shù)組 集合 觀察
監(jiān)聽數(shù)組等集合時,需要用mutableArrayValueForKey觸發(fā)通知
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];
KVO原理
-
KVO只針對屬性,而不監(jiān)聽成員變量。
準備代碼:
@interface LGPerson : NSObject{
@public
NSString *name;
}
@property (nonatomic, copy) NSString *nickName;
@end
- (void)setNickName:(NSString *)nickName{
_nickName = nickName;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [[LGPerson alloc] init];
[self.person addObserver:self forKeyPath:@"nickName" options:(NSKeyValueObservingOptionNew) context:NULL];
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"實際情況:%@-%@",self.person.nickName,self.person->name);
self.person.nickName = @"Mike";
self.person->name = @"Tom";
}
#pragma mark - KVO回調(diào)
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@",change);
}
- (void)dealloc{
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"nickName"];
}
打印情況:
實際情況:(null)-(null)
{
kind = 1;
new = Mike;
}
可以看出只是監(jiān)聽了屬性nickName,成員變量并沒有監(jiān)聽。
- 查看官方文檔,
KVO的實現(xiàn)依賴于isa-swizzling技術,當觀察者注冊時,被觀察者的isa指針被修改,指向一個中間類而不是真正的類。
代碼驗證:
在圖中位置打上斷點,驗證注冊觀察者前后的isa指向
注冊觀察者之后,self.person的isa指向了NSKVONotifying_LGPerson。
查閱了相關資料,說NSKVONotifying_LGPerson是LGPerson的子類。進一步驗證:

通過打印它的內(nèi)存結(jié)構(gòu),發(fā)現(xiàn)
superClass正是LGPerson。
- 讓我們看一看
NSKVONotifying_LGPerson里有什么方法
- (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(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
調(diào)用[self printClassAllMethod:objc_getClass("NSKVONotifying_LGPerson")];方法打印結(jié)果為:

-
NSKVONotifying_LGPerson類重寫setter方法做了什么?
通過LLDB設置,觀察nickName屬性
watchpoint set variable self->_person->_nickName
不能使用點語法
當self.person.nickName發(fā)生改變時,自動進入斷點。

也可以直接在setter方法中打斷點,查看堆棧

我們可以看到在setter方法前后,調(diào)用了NSKeyValueWillChange和NSKeyValueDidChangeBySetting

-
移除觀察者時,
isa指針是否指回原本的類
同樣,在removeObserver:forKeyPath:方法前后打印isa情況
移除觀察者后,isa指針指向原本的類。 移除觀察者后,
NSKVONotifying_LGPerson類是否從內(nèi)存中移除。
通過以下方法,打印LGPerson的子類,看NSKVONotifying_LGPerson是否消失。
- (void)printClasses:(Class)cls{
// 注冊類的總數(shù)
int count = objc_getClassList(NULL, 0);
// 創(chuàng)建一個數(shù)組, 其中包含給定對象
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);
}
在removeObserver:forKeyPath:方法后打?。?/p>
classes = (
LGPerson,
LGStudent,
"NSKVONotifying_LGPerson"
)
(lldb)
NSKVONotifying_LGPerson依然作為LGPerson的子類存在,避免了再一次注冊KVO的重新開辟,節(jié)省性能。
自定義KVO
模擬KVO實現(xiàn)流程,自定義一份KVO代碼,實現(xiàn)了KVO自動銷毀。主要用來學習,加深記憶。
typedef void (^KVOHandle)(id observer, NSString * keyPath, id oldValue,id newValue);
@interface NSObject (LYKVO)
- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle;
@end
static NSString *const kLYKVOPrefix = @"LYKVONotifying_";
static NSString *const kLYKVOAssiociateKeyHandle = @"kLYKVO_AssiociateKeyHandle";
static NSString *const kLYKVOAssiociateKeyInfo = @"kLYKVO_AssiociateKeyInfo";
@implementation NSObject (LYKVO)
- (void)ly_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options Handle:(KVOHandle)handle {
if (!observer || !keyPath || !options) return;
// 1: 驗證是否存在setter方法 : 不讓實例進來
[self checkSetterMethodFromKeyPath:keyPath];
// 2: 動態(tài)生成子類
Class newClass = [self createChildClassWithKeyPath:keyPath];
// 3.將self 指向LGKVONotifying_Class
object_setClass(self, newClass);
//4.重寫setter方法 class_addMethod
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSel);
const char * setterType = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSel, (IMP)ly_setter, setterType);
//5.保存block,用于傳值
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle), handle, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//6.保存observer keyPath options 用于傳值
LYKVOInfo *info = [[LYKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath options:options];
NSMutableArray *infoArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo);
if (!infoArr) {
infoArr = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)kLYKVOAssiociateKeyInfo, infoArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[infoArr addObject:info];
}
static void ly_setter(id self,SEL _cmd,id newValue) {
SEL getSel = NSSelectorFromString(getterForSetter(NSStringFromSelector(_cmd)));
id oldValue = objc_msgSend(self, getSel);
//向父類發(fā)送一個setter消息
void (*ly_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
ly_msgSendSuper(&superStruct, _cmd, newValue);
//處理回調(diào)
KVOHandle handle = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyHandle));
NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
for (LYKVOInfo *info in mArr) {
handle(info.observer,info.keyPath,oldValue,newValue);
}
}
static Class ly_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
static void ly_dealloc(id self, SEL _cmd) {
//指針指回原本的類
Class superClass = [self class];
object_setClass(self, superClass);
//處理數(shù)據(jù)
NSMutableArray *mArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kLYKVOAssiociateKeyInfo));
if (mArr.count <= 0) {
[mArr removeAllObjects];
mArr = nil;
}
//執(zhí)行原本的dealloc方法
objc_msgSend(self, _cmd);
}
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
//2.創(chuàng)建中間類
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@",kLYKVOPrefix,oldClassName];
Class newClass = NSClassFromString(newClassName);
if (newClass) return newClass;
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
objc_registerClassPair(newClass);
//3.重寫dealloc方法
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
const char * deallocType = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSel, (IMP)ly_dealloc, deallocType);
//4.重寫class方法
SEL classSel = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSel);
const char * classType = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)ly_class, classType);
return newClass;
}
#pragma mark -驗證類的setter方法是否存在
- (void)checkSetterMethodFromKeyPath:(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 -只是簡單的拼接,并沒有考慮周全
#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];
}
@end
@interface LYKVOInfo : NSObject
@property (nonatomic, weak) NSObject * observer;
@property (nonatomic, copy) NSString * keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options;
@end
@implementation LYKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options {
if (self = [super init]) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
}
return self;
}
@end
推薦學習:
FBKVOController
GNN源碼




