刨根問底KVO原理

介紹

KVO( NSKeyValueObserving )是一種監測對象屬性值變化的觀察者模式機制。其特色是無需事先修改被觀察者代碼,利用 runtime 實現運行中修改某一實例達到目的,保證了未侵入性。objective-c

A對象指定觀察B對象的屬性後,當屬性發生變動,A對象會收到通知,獲取變動前以及變動的狀態,從而作進一步處理。swift

在實際生產環境中,多用於應用層觀察模型層數據變更,接收到通知後更新,從而達成比較好的設計模式。設計模式

另外一種經常使用的用法是 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方法,那就是NSKeyValueChangeSettingapp

剩餘的三種分別是插入、刪除、替換,當觀察的屬性屬於集合類(這點會在以後講),變更時就會通知這些類型。工具

typedef NS_ENUM(NSUInteger, NSKeyValueChange) {
    NSKeyValueChangeSetting = 1,
    NSKeyValueChangeInsertion = 2,
    NSKeyValueChangeRemoval = 3,
    NSKeyValueChangeReplacement = 4,
};
複製代碼

NSKeyValueChangeNewKey 獲取變動的最新值,NSKeyValueChangeOldKey 獲取原始數值。優化

NSKeyValueChangeIndexesKey 若是觀察的是集合,那這個鍵值返回索引集合。ui

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;
@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: 是一個類方法,若是你不但願某個屬性被觀察,那麼就設爲 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的一切)

相關文章
相關標籤/搜索