刨根問底KVO原理
介紹
KVO(NSKeyValueObserving
)是一種監測物件屬性值變化的觀察者模式機制。其特點是無需事先修改被觀察者程式碼,利用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, //會在改變前與改變後傳送兩次通知 //改變前的通知帶有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) class_getMethodImplementation(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 = *_OBJC_IVAR_$_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); __strlcpy_chk(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
開原始碼的不熟悉,這篇文章寫了一週時間,結構也有點混亂。
所幸還是理順了整體結構,在整理的過程中學會了很多很多。
由於才疏學淺,其中對彙編和原始碼的解釋難免出錯,還望大佬多多指教!
資料分享
ObjC中國的期刊ofollow,noindex" target="_blank">KVC和KVO
楊大牛的Objective-C中的KVC和KVO