刨根問底KVO原理

VanchChen發表於2018-12-29

介紹

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;
複製程式碼

觀察者需要實現這個方法來接受回撥,其中keyPathKVC 路徑, 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

NSKeyValueObservingNSKeyValueCoding 一起定義在 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;
@endConcreteSubject *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) {
//0 x15沒有找到定義,猜測為儲存是否可觀察的陣列 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: 是一個類方法,如果你不希望某個屬性被觀察,那麼就設為 NOisa 返回是空也就宣告這次新增觀察失敗。

如果一切順利的話,將會執行__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];

}複製程式碼

通過實現了insertremove方法,使得代理陣列能夠正常運作陣列變數,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];

}複製程式碼

這種方式相對來說更簡潔,修改陣列的方法與平時一致,比較適合使用。

下面說一下原理,首先我們沒有實現對應的insertremove方法,其次readonly屬性也沒有set<
key>
:
方法,但我們實現了 @synthesize dataArray = _dataArray;
所以根據第三步對代理陣列的操作都會實際操作到例項變數中。

然後過載了 dataArrayGetter 方法,保證了修改陣列時必須呼叫主體是self.dataArray,也就是代理陣列,從而傳送通知。


問答

KVO的底層實現?

KVO 就是通過 Runtime 替換被觀察類的 Setter 實現,從而在發生改變時發起通知。

如何取消系統預設的KVO並手動觸發(給KVO的觸發設定條件:改變的值符合某個條件時再觸發KVO)?

通過設定 automaticallyNotifiesObserversForKeyFalse 實現取消自動觸發。

符合條件再觸發可以這麼實現。

- (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中國的期刊 KVC和KVO

楊大牛的 Objective-C中的KVC和KVO

iOS開發技巧系列—詳解KVC(我告訴你KVC的一切)

來源:https://juejin.im/post/5c22023df265da6124157a25

相關文章