介紹
KVO( NSKeyValueObserving )是OA幸運飛艇源碼修復QQ2952777280【話仙源碼論壇】hxforum.com 一種監測對象屬性值變化的觀察者模式機制。其特點是無需事先修改被觀察者代碼,利用 runtime 實現運行中修改某一實例達到目的,保證了未侵入性。
A對象指定觀察B對象的屬性後,當屬性發生變更,A對象會收到通知,獲取變更前以及變更的狀態,從而做進一步處理。
在實際生產環境中,多用於應用層觀察模型層數據變動,接收到通知後更新,從而達成比較好的設計模式。
另一種常用的用法是 Debug,通過觀察問題屬性的變化,追蹤問題出現的堆棧,更有效率的解決問題。
應用
觀察回調
- (void)observeValueForKeyPath:(nullable NSString )keyPath
ofObject:(nullable id)object
change:(nullable NSDictionary<NSKeyValueChangeKey, id> )change
context:(nullable void *)context;
觀察者需要實現這個方法來接受回調,其中keyPath 是 KVC 路徑, object 是觀察者,context 區分不同觀察的標識。
改變字典
最關鍵的是改變字典,其中包含了 NSKeyValueChangeKey,通過預定義的字符串來獲取特定的數值。
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey
NSKeyValueChangeKindKey 中定義的是改變的類型,如果調用的是Setter方法,那就是NSKeyValueChangeSetting。
剩餘的三種分別是插入、刪除、替換,當觀察的屬性屬於集合類(這點會在之後講),變動時就會通知這些類型。
typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
NSKeyValueChangeSetting = 1,
NSKeyValueChangeInsertion = 2,
NSKeyValueChangeRemoval = 3,
NSKeyValueChangeReplacement = 4,
};
NSKeyValueChangeNewKey 獲取變更的最新值,NSKeyValueChangeOldKey 獲取原始數值。
NSKeyValueChangeIndexesKey 如果觀察的是集合,那這個鍵值返回索引集合。
NSKeyValueChangeNotificationIsPriorKey 如果設置了接受提前通知,那麼修改之前會先發送通知,修改後再發一次。爲了區分這兩次,第一次會帶上這個鍵值對,其內容爲 @1。
字符串枚舉
在註冊類型時,蘋果使用了NS_STRING_ENUM宏。
雖然這個宏在ObjC下毫無作用,但是對於Swift有優化
,上面的定義會變成這樣。
enum NSKeyValueChangeKey: String {
case kind
case new
case old
case indexes
case notificationIsPrior
}
let dict: [NSKeyValueChangeKey : Any] = [......]
let kind = dict[.kind] as! Number
字符串枚舉對於使用來說是非常直觀和安全的。
添加與刪除
對於普通對象,使用這兩個方法就能註冊與註銷觀察。
-
(void)addObserver:(NSObject )observer
forKeyPath:(NSString )keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void *)context; - (void)removeObserver:(NSObject )observer
forKeyPath:(NSString )keyPath
context:(nullable void *)context;
可以設置多種觀察模式來匹配需求。
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
//可以收到新改變的數值
NSKeyValueObservingOptionNew = 0x01,
//可以收到改變前的數值
NSKeyValueObservingOptionOld = 0x02,
//addObserver後立刻觸發通知,只有new,沒有old
NSKeyValueObservingOptionInitial = 0x04,br/>//會在改變前與改變後發送兩次通知
//改變前的通知帶有notificationIsPrior=@1,old
NSKeyValueObservingOptionPrior = 0x08
};
由於不符合 KVC 的訪問器標準,蘋果規定 NSArray NSOrderedSet NSSet 不可以執行 addObserver 方法,不然會拋出異常。針對 NSArray 有特殊的方法,如下
-
(void)addObserver:(NSObject )observer
toObjectsAtIndexes:(NSIndexSet )indexes
forKeyPath:(NSString )keyPath
options:(NSKeyValueObservingOptions)options
context:(nullable void )context; - (void)removeObserver:(NSObject )observer
fromObjectsAtIndexes:(NSIndexSet )indexes
forKeyPath:(NSString )keyPath
context:(nullable void )context;
主要的區別在於多了一個ObjectsAtIndexes,其實做的事情是一樣的,根據索引找到對象,再逐一建立觀察關係。
原理
Runtime
NSKeyValueObserving 與 NSKeyValueCoding 一起定義在 Foundation 庫,而這個庫是不開源的,我們先從蘋果開發者文檔中獲取信息。
Automatic key-value observing is implemented using a technique called isa-swizzling.
看描述猜測蘋果應該是通過重新設置被觀察者的 Class (isa 中包含 Class 信息),該類繼承了原類並且重載屬性的 Setter 方法,添加發通知的操作達到目的。
@interface ConcreteSubject : NSObject
@property (nonatomic, strong) id obj;
@end
ConcreteSubject *sub = [ConcreteSubject new];
NSLog(@"%s", class_getName(object_getClass(sub)));
//改變前 outprint--> ConcreteSubject
[sub addObserver:self forKeyPath:@"obj" options:NSKeyValueObservingOptionNew context:nil];
//執行觀察方法
NSLog(@"%s", class_getName(object_getClass(sub)));
//改變後 outprint--> NSKVONotifying_ConcreteSubject
NSLog(@"%s", class_getName(object_getClass(class_getSuperclass(cls))));
//獲取超類名 outprint--> ConcreteSubject
NSLog(@"%s", class_getName(sub.class));
//獲取類名 outprint--> ConcreteSubject
class_getMethodImplementation(cls, @selector(setObj:));
//imp = (IMP)(Foundation`_NSSetObjectValueAndNotify)
classgetMethodImplementation(cls, @selector(class));
//imp = (IMP)(Foundation`NSKVOClass)
試了一下果然 Class 被替換了,變成加了 NSKVONotifying 前綴的新類。
新類繼承自原類,但是這個類的 class 方法返回的還是原類,這保證了外部邏輯完整。
反編譯源碼
通過 Runtime ,我們只能知道 KVO 使用了一個繼承了原類的類,並且替換了原方法的實現,setObj: = _NSSetObjectValueAndNotify class = _NSKVOClass。如果我們想進一步瞭解詳情,只能通過反編譯 Foundation 來查找彙編代碼。
這裏我使用了 Hopper 工具,分析的二進制文件路徑是/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation
替換的實現
//僞代碼,僅供理解
void _NSKVOClass(id self, SEL _cmd) {
Class cls = object_getClass(self);
Class originCls = __NSKVONotifyingOriginalClassForIsa(cls);
if (cls != originCls) {
return [originCls class];
} else {
Method method = class_getInstanceMethod(cls, _cmd);
return method_invoke(self, method);
}
}
先看原 class 方法,獲取了當前類和原類,如果不一致就返回原類,如果一致就執行原 class 實現。
//僞代碼,僅供理解
void __NSSetObjectValueAndNotify(id self, SEL _cmd, id value) {
//獲取額外的變量
void indexedIvars = object_getIndexedIvars(object_getClass(self));
//加鎖
pthread_mutex_lock(indexedIvars + 0x20);
//從SEL獲取KeyPath
NSString keyPath = [CFDictionaryGetValue(*(indexedIvars) + 0x18), _cmd) copyWithZone:0x0];
//解鎖
pthread_mutex_unlock(indexedIvars + 0x20);
//改變前發通知
[self willChangeValueForKey:keyPath];
//實現Setter方法
IMP imp = class_getMethodImplementation(*indexedIvars, _cmd);
(imp)(self, _cmd, value);
//改變後發通知
[self didChangeValueForKey:keyPath];
}
再看改變後的 Setter 方法,其中 indexedIvars 是原類之外的成員變量,第一個指針是改變後的類,0x20 的偏移量是線程鎖,0x18 地址儲存了改變過的方法字典。
在執行原方法實現前調用了 willChangeValueForKey 發起通知,同樣在之後調用 didChangeValueForKey。
添加觀察方法
那麼是在哪個方法中替換的實現呢?先看 [NSObject addObserver:forKeyPath:options:context:] 方法。
//僞代碼,僅供理解
void -[NSObject addObserver:forKeyPath:options:context:]
(void self, void _cmd, void arg2, void arg3, unsigned long long arg4, void arg5) {
pthread_mutex_lock(__NSKeyValueObserverRegistrationLock);
NSKeyValueObserverRegistrationLockOwner = pthread_self();
rax = object_getClass(self);
rax = _NSKeyValuePropertyForIsaAndKeyPath(rax, arg3);
[self _addObserver:arg2 forProperty:rax options:arg4 context:arg5];
*NSKeyValueObserverRegistrationLockOwner = 0x0;
pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
return;
}
方法很簡單,根據 KeyPath 獲取具體屬性後進一步調用方法。由於這個方法比較長,我特地整理成 ObjC 代碼,方便大家理解。
//僞代碼,僅供理解
-
(void )_addObserver:(id)observer
forProperty:(NSKeyValueProperty )property
options:(NSKeyValueObservingOptions)option
context:(void )context {
//需要註冊通知
if (option & NSKeyValueObservingOptionInitial) {
//獲取屬性名路徑
NSString keyPath = [property keyPath];
//解鎖
pthread_mutex_unlock(__NSKeyValueObserverRegistrationLock);
//如果註冊了獲得新值,就獲取數值
id value = nil;
if (option & NSKeyValueObservingOptionNew) {
value = [self valueForKeyPath:keyPath];
if (value == nil) {
value = [NSNull null];
}
}
//發送註冊通知
_NSKeyValueNotifyObserver(observer, keyPath, self, context, value,
0 /originalObservable/, 1 /NSKeyValueChangeSetting/);
//加鎖
pthread_mutex_lock(NSKeyValueObserverRegistrationLock);
}
//獲取屬性的觀察信息
Info *info = NSKeyValueRetainedObservationInfoForObject(self, property->_containerClass);
//判斷是否需要獲取新的數值
id _additionOriginalObservable = nil;
if (option & NSKeyValueObservingOptionNew) {
//0x15沒有找到定義,猜測爲保存是否可觀察的數組id tsd = _CFGetTSD(0x15); if (tsd != nil) { _additionOriginalObservable = *(tsd + 0x10); }
}
//在原有信息上生成新的信息
Info *newInfo = __NSKeyValueObservationInfoCreateByAdding
(info, observer, property, option, context, _additionOriginalObservable, 0, 1);
//替換屬性的觀察信息
__NSKeyValueReplaceObservationInfoForObject(self, property->_containerClass, info, newInfo);
//屬性添加後遞歸添加關聯屬性
[property object:self didAddObservance:newInfo recurse:true];
//獲取新的isa
Class cls = [property isaForAutonotifying];
if ((cls != NULL) && (object_getClass(self) != cls)) {
//如果是第一次就替換isa
object_setClass(self, cls);
}
//釋放觀察信息
[newInfo release];
if (info != nil) {
[info release];
}
return;
}
其中有可能替換方法實現的步驟是獲取 isa 的時候,猜測當第一次創建新類的時候,會註冊新的方法,接着追蹤 isaForAutonotifying 方法。
獲取觀察類
void -[NSKeyValueUnnestedProperty _isaForAutonotifying]
(void self, void _cmd) {
rbx = self;
r14 = _OBJCIVAR$_NSKeyValueProperty._containerClass;
if ([(rbx + r14)->_originalClass
automaticallyNotifiesObserversForKey:rbx->_keyPath] != 0x0) {
r14 = __NSKeyValueContainerClassGetNotifyingInfo((rbx + r14));
if (r14 != 0x0) {
__NSKVONotifyingEnableForInfoAndKey(r14, rbx->_keyPath);
rax = *(r14 + 0x8);
}
else {
rax = 0x0;
}
}
else {
rax = 0x0;
}
return rax;
}
立刻發現了熟悉的方法!
automaticallyNotifiesObserversForKey: 是一個類方法,如果你不希望某個屬性被觀察,那麼就設爲 NO,isa 返回是空也就宣告這次添加觀察失敗。
如果一切順利的話,將會執行__NSKVONotifyingEnableForInfoAndKey(info, keyPath) 改變 class 的方法,最終返回其 isa。
實質替換方法
由於該方法實在太長,且使用了goto不方便閱讀,所以依舊整理成僞代碼。
//僞代碼,僅供理解
int NSKVONotifyingEnableForInfoAndKey(void info, id keyPath) {
//線程鎖加鎖
pthread_mutex_lock(info + 0x20);
//添加keyPath到數組
CFSetAddValue((info + 0x10), keyPath);
//解鎖
pthread_mutex_unlock(info + 0x20);
//判斷原類實現能不能替換
Class originClass = info;
MethodClass methodClass =
NSKeyValueSetterForClassAndKey(originClass, keyPath, originClass);
if (![methodClass isKindOfClass:[NSKeyValueMethodSetter class]]) {
swizzleMutableMethod(info, keyPath);
return;
}
//判斷Setter方法返回值
Method method = [methodClass method];
if ((int8_t )method_getTypeEncoding(method) != _C_VOID) {
_NSLog(@"KVO autonotifying only supports -set<Key>: methods that return void.");
swizzleMutableMethod(info, keyPath);
return;
}
//獲取Setter方法參數
char typeEncoding = method_copyArgumentType(method, 0x2);
char type = sign_extend_64((int8_t )typeEncoding);
SEL sel;//根據參數類型選擇替換的方法
switch (type) {
case _C_BOOL: sel = NSSetBoolValueAndNotify;
case _C_UCHR: sel = NSSetUnsignedCharValueAndNotify;
case _C_UINT: sel = NSSetUnsignedIntValueAndNotify;
case _C_ULNG: sel = NSSetUnsignedLongValueAndNotify;
case _C_ULNG_LNG: sel = NSSetUnsignedLongLongValueAndNotify;
case _C_CHR: sel = NSSetCharValueAndNotify;
case _C_DBL: sel = NSSetDoubleValueAndNotify;
case _C_FLT: sel = NSSetFloatValueAndNotify;
case _C_INT: sel = NSSetIntValueAndNotify;
case _C_LNG: sel = NSSetLongValueAndNotify;
case _C_LNG_LNG: sel = NSSetLongLongValueAndNotify;
case _C_SHT: sel = NSSetShortValueAndNotify;
case _C_USHT: sel = NSSetUnsignedShortValueAndNotify;
case _C_LNG_LNG: sel = NSSetLongLongValueAndNotify;
case _C_ID: sel = NSSetObjectValueAndNotify;
case "{CGPoint=dd}": sel = __NSSetPointValueAndNotify;
case "{_NSRange=QQ}": sel = NSSetRangeValueAndNotify;
case "{CGRect={CGPoint=dd}{CGSize=dd}}": sel = NSSetRectValueAndNotify;
case "{CGSize=dd}": sel = NSSetSizeValueAndNotify;
case _NSKeyValueOldSizeObjCTypeName: sel = CF_forwarding_prep_0;
default;
}
//不支持的參數類型打印錯誤信息
if (sel == NULL) {
_NSLog(@"KVO autonotifying only supports -set<Key>: methods that take id,
NSNumber-supported scalar types, and some NSValue-supported structure types.")
swizzleMutableMethod(info, keyPath);
return;
}
//替換方法實現
SEL methodSel = method_getName(method);
_NSKVONotifyingSetMethodImplementation(info, methodSel, sel, keyPath);
if (sel == CF_forwarding_prep_0) {
_NSKVONotifyingSetMethodImplementation(info, @selector(forwardInvocation:),
_NSKVOForwardInvocation, false);
Class cls = *(info + 0x8);
SEL newSel = sel_registerName("original" + sel_getName(methodSel));
Imp imp = method_getImplementation(method);
TypeEncoding type = method_getTypeEncoding(method);
class_addMethod(cls, newSel, imp, type);
}
swizzleMutableMethod(info, keyPath);
}
可以表述爲根據 Setter 方法輸入參數類型,匹配合適的 NSSetValueAndNotify 實現來替換,從而實現效果。
那麼 swizzleMutableMethod 是幹嘛的呢?
//替換可變數組集合的方法
int swizzleMutableMethod(void info, id keyPath) {
//NSKeyValueArray
CFMutableSetRef getterSet = __NSKeyValueMutableArrayGetterForIsaAndKey(info, keyPath);
if ([getterSet respondsToSelector:mutatingMethods]) {
mutatingMethods methodList = [getterSet mutatingMethods];
replace methodList->insertObjectAtIndex _NSKVOInsertObjectAtIndexAndNotify
replace methodList->insertObjectsAtIndexes _NSKVOInsertObjectsAtIndexesAndNotify
replace methodList->removeObjectAtIndex _NSKVORemoveObjectAtIndexAndNotify
replace methodList->removeObjectsAtIndexes _NSKVORemoveObjectsAtIndexesAndNotify
replace methodList->replaceObjectAtIndex _NSKVOReplaceObjectAtIndexAndNotify
replace methodList->replaceObjectsAtIndexes _NSKVOReplaceObjectsAtIndexesAndNotify
}
//NSKeyValueOrderedSet
getterSet = NSKeyValueMutableOrderedSetGetterForIsaAndKey(*info, keyPath);
if ([getterSet respondsToSelector:mutatingMethods]) {
mutatingMethods methodList = [getterSet mutatingMethods];
replace methodList->insertObjectAtIndex _NSKVOInsertObjectAtIndexAndNotify
replace methodList->insertObjectsAtIndexes _NSKVOInsertObjectsAtIndexesAndNotify
replace methodList->removeObjectAtIndex _NSKVORemoveObjectAtIndexAndNotify
replace methodList->removeObjectsAtIndexes _NSKVORemoveObjectsAtIndexesAndNotify
replace methodList->replaceObjectAtIndex _NSKVOReplaceObjectAtIndexAndNotify
replace methodList->replaceObjectsAtIndexes _NSKVOReplaceObjectsAtIndexesAndNotify
}
//NSKeyValueSet
getterSet = NSKeyValueMutableSetGetterForClassAndKey(info, keyPath);
if ([getterSet respondsToSelector:mutatingMethods]) {
mutatingMethods methodList = [getterSet mutatingMethods];
replace methodList->addObject _NSKVOAddObjectAndNotify
replace methodList->intersectSet _NSKVOIntersectSetAndNotify
replace methodList->minusSet _NSKVOMinusSetAndNotify
replace methodList->removeObject _NSKVORemoveObjectAndNotify
replace methodList->unionSet _NSKVOUnionSetAndNotify
}
//改變新類的方法緩存
__NSKeyValueInvalidateCachedMutatorsForIsaAndKey((info + 0x8), keyPath);
return rax;
}
前面提到的都是一對一,那如果我想觀察一對多的集合類呢?就是通過 KVC 中的 mutableArrayValueForKey: 返回一個代理集合,改變這些代理類的實現做到的。具體的例子之後會介紹。
創建新類
還有一個疑問就是替換的類是怎麼創建的?具體方法在 __NSKVONotifyingEnableForInfoAndKey 中實現。
//僞代碼,僅供理解
int NSKVONotifyingCreateInfoWithOriginalClass(Class cls) {
//拼接新名字
const char name = class_getName(cls);
int length = strlen(r12) + 0x10;//16是NSKVONotifying_的長度
char newName = malloc(length);
strlcpychk(newName, "NSKVONotifying", length, -1);
strlcat_chk(newName, name, length, -1);
//生成一個繼承原類的新類
Class newCls = objc_allocateClassPair(cls, newName, 0x68);
free(newName);
if (newCls != NULL) {
objc_registerClassPair(newCls);
//獲取額外的實例變量表
void indexedIvars = object_getIndexedIvars(newCls);
indexedIvars = cls; //記錄原isa
(indexedIvars + 0x8) = newCls; //記錄新isa
//新建一個集合,保存觀察的keyPath
(indexedIvars + 0x10) = CFSetCreateMutable(0x0, 0x0, _kCFCopyStringSetCallBacks);
//新建一個字典,保存改變過的SEL
(indexedIvars + 0x18) = CFDictionaryCreateMutable(0x0, 0x0, 0x0,
_kCFTypeDictionaryValueCallBacks);
//新建一個線程鎖
pthread_mutexattr_init(var_38);
pthread_mutexattr_settype(var_38, 0x2);
pthread_mutex_init(indexedIvars + 0x20, var_38);
pthread_mutexattr_destroy(var_38);
//獲取NSObject類默認的實現
if (NSKVONotifyingCreateInfoWithOriginalClass.NSObjectIMPLookupOnce == NULL) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
*__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange =
class_getMethodImplementation([NSObject class],
@selector(willChangeValueForKey:));
*__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange =
class_getMethodImplementation([NSObject class],
@selector(didChangeValueForKey:));
});
}
//設置是否替換過ChangeValue方法的flag
BOOL isChangedImp = YES;
if (class_getMethodImplementation(cls, @selector(willChangeValueForKey:)) ==
*__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectWillChange) {
BOOL isChangedDidImp =
class_getMethodImplementation(cls, @selector(didChangeValueForKey:))
!=
*__NSKVONotifyingCreateInfoWithOriginalClass.NSObjectDidChange;
isChangedImp = isChangedDidImp ? YES : NO;
}
*(int8_t *)(indexedIvars + 0x60) = isChangedImp;
//使用KVO的實現替換原類方法
_NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(_isKVOA),
_NSKVOIsAutonotifying, false/*是否需要保存SEL到字典*/);
_NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(dealloc),
_NSKVODeallocate, false);
_NSKVONotifyingSetMethodImplementation(indexedIvars, @selector(class),
_NSKVOClass, false);
}
return newCls;
}
建立關係
還有一種情況就是觀察的屬性依賴於多個關係,比如 color 可能依賴於 r g b a,其中任何一個改變,都需要通知 color 的變化。
建立關係的方法是
- (NSSet )keyPathsForValuesAffectingValueForKey:(NSString )key
或 + (NSSet *)keyPathsForValuesAffecting<key>
返回依賴鍵值的字符串集合
//僞代碼
- (NSSet )keyPathsForValuesAffectingValueForKey:(NSString )key {
char *str = "keyPathsForValuesAffecting" + key;
SEL sel = sel_registerName(str);
Method method = class_getClassMethod(self, sel);
if (method != NULL) {
result = method_invoke(self, method);
} else {
result = [self _keysForValuesAffectingValueForKey:key];
}
return result;
}
還記得之前在 _addObserver 方法中有這段代碼嗎?
//屬性添加後遞歸添加關聯屬性
[property object:self didAddObservance:newInfo recurse:true];
其中 NSKeyValueProperty 也是一個類簇,具體分爲 NSKeyValueProperty NSKeyValueComputedProperty NSKeyValueUnnestedProperty NSKeyValueNestedProperty,從名字也看出 NSKeyValueNestedProperty 是指嵌套子屬性的屬性類,那我們觀察下他的實現。
//僞代碼
- (void)object:(id)obj didAddObservance:(id)info recurse:(BOOL)isRecurse {
if (self->_isAllowedToResultInForwarding != nil) {
//獲得關係鍵
relateObj = [obj valueForKey:self->_relationshipKey];
//註冊所有關係通知
[relateObj addObserver:info
forKeyPath:self->_keyPathFromRelatedObject
options:info->options
context:nil];
}
//再往下遞歸
[self->_relationshipProperty object:obj didAddObservance:info recurse:isRecurse];
}
至此,實現的大致整體輪廓比較瞭解了,下面會講一下怎麼把原理運用到實際。
應用原理
手動觸發
當 +(BOOL)automaticallyNotifiesObserversForKey:(NSString )key 返回是 YES,那麼註冊的這個 Key 就會替換對應的 Setter ,從而在改變的時候調用 -(void)willChangeValueForKey:(NSString )key 與 -(void)didChangeValueForKey:(NSString *)key 發送通知給觀察者。
那麼只要把自動通知設爲 NO,並代碼實現這兩個通知方法,就可以達到手動觸發的要求。
-
(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"object"]) {
return false;
}return [super automaticallyNotifiesObserversForKey:key];
} -
(void)setObject:(NSObject *)object {
if (object != _object) {
[self willChangeValueForKey:@"object"];
_object = object;
[self didChangeValueForKey:@"object"];
}
}
如果操作的是之前提到的集合對象,那麼實現的方法就需要變爲 - (void)willChange:(NSKeyValueChange)changeKind
valuesAtIndexes:(NSIndexSet )indexes
forKey:(NSString )key; -
(void)didChange:(NSKeyValueChange)changeKind
valuesAtIndexes:(NSIndexSet )indexes
forKey:(NSString )key; - (void)willChangeValueForKey:(NSString )key
withSetMutation:(NSKeyValueSetMutationKind)mutationKind
usingObjects:(NSSet )objects; -
(void)didChangeValueForKey:(NSString )key
withSetMutation:(NSKeyValueSetMutationKind)mutationKind
usingObjects:(NSSet )objects;
依賴鍵觀察
之前也有提過構建依賴關係的方法,具體操作如下 -
(NSSet<NSString > )keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqualToString:@"color"]) {
return [NSSet setWithObjects:@"r",@"g",@"b",@"a",nil];
}return [super keyPathsForValuesAffectingValueForKey:key];
}
//建議使用靜態指針地址作爲上下文區分不同的觀察
static void const kColorContext = (void)&kColorContext;
-
(void)viewDidLoad {
[super viewDidLoad];[self addObserver:self forKeyPath:@"color"
options:NSKeyValueObservingOptionNew
context:kColorContext];
self.r = 133;
} - (void)observeValueForKeyPath:(NSString )keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> )change
context:(void *)context {
if (context == kColorContext) {
NSLog(@"%@", keyPath);
//outprint --> color
}
}
可變數組與集合
不可變的數組與集合由於內部結構固定,所以只能通過觀察容器類內存地址來判斷是否變化,也就是 NSKeyValueChangeSetting。
集合和數組的觀察都很類似,我們先關注如果要觀察可變數組內部插入移除的變化呢?
先了解一下集合代理方法,- (NSMutableArray *)mutableArrayValueForKey:,這是一個 KVC 方法,能夠返回一個可供觀察的 NSKeyValueArray 對象。
根據蘋果註釋,其搜索順序如下
1.搜索是否實現最少一個插入與一個刪除方法
-insertObject:in<Key>AtIndex:
-removeObjectFrom<Key>AtIndex:
-insert<Key>:atIndexes:
-remove<Key>AtIndexes:
2.否則搜索是否有 set<Key>: 方法,有的話每次都把修改數組重新賦值回原屬性。
3.否則檢查 + (BOOL)accessInstanceVariablesDirectly,如果是YES,就查找成員變量_<key> or <key>,此後所有的操作針對代理都轉接給成員變量執行。
4.最後進入保護方法valueForUndefinedKey:
第一種方法
-
(void)insertObject:(NSObject *)object inDataArrayAtIndex:(NSUInteger)index {
[_dataArray insertObject:object atIndex:index];
} -
(void)removeObjectFromDataArrayAtIndex:(NSUInteger)index {
[_dataArray removeObjectAtIndex:index];
} -
(void)viewDidLoad {
[super viewDidLoad];_dataArray = @[].mutableCopy;
[self addObserver:self forKeyPath:@"dataArray"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
NSKeyValueObservingOptionPrior context:nil];
[self insertObject:@1 inDataArrayAtIndex:0];
}
通過實現了insert與remove方法,使得代理數組能夠正常運作數組變量,KVO 觀察了代理數組的這兩個方法,發出了我們需要的通知。
這種方式使用了第一步搜索,比較容易理解,缺點是改動的代碼比較多,改動數組必須通過自定義方法。
第二種方法
@property (nonatomic, strong, readonly) NSMutableArray *dataArray;
@synthesize dataArray = _dataArray;
-
(NSMutableArray *)dataArray {
return [self mutableArrayValueForKey:@"dataArray"];
} -
(void)viewDidLoad {
[super viewDidLoad];_dataArray = @[].mutableCopy;
[self addObserver:self forKeyPath:@"dataArray"
options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld |
NSKeyValueObservingOptionPrior context:nil];
[self.dataArray addObject:@1];
}
這種方式相對來說更簡潔,修改數組的方法與平時一致,比較適合使用。
下面說一下原理,首先我們沒有實現對應的insert與remove方法,其次readonly屬性也沒有set<key>:方法,但我們實現了 @synthesize dataArray = _dataArray; 所以根據第三步對代理數組的操作都會實際操作到實例變量中。
然後重載了 dataArray 的 Getter 方法,保證了修改數組時必須調用主體是self.dataArray,也就是代理數組,從而發送通知。
問答
KVO的底層實現?
KVO 就是通過 Runtime 替換被觀察類的 Setter 實現,從而在發生改變時發起通知。
如何取消系統默認的KVO並手動觸發(給KVO的觸發設定條件:改變的值符合某個條件時再觸發KVO)?
通過設置 automaticallyNotifiesObserversForKey 爲 False 實現取消自動觸發。
符合條件再觸發可以這麼實現。
-
(void)setObject:(NSObject *)object {
if (object == _object) return;BOOL needNotify = [object isKindOfClass:[NSString class]];
if (needNotify) {
[self willChangeValueForKey:@"object"];
}
_object = object;
if (needNotify) {
[self didChangeValueForKey:@"object"];
}
}
總結
由於對彙編語言、反編譯工具、objc4開源代碼的不熟悉,這篇文章寫了一週時間,結構也有點混亂。
所幸還是理順了整體結構,在整理的過程中學會了很多很多。
由於才疏學淺,其中對彙編和源碼的解釋難免出錯,還望大佬多多指教!