iOS KVO實(shí)現(xiàn)原理

相關(guān)API以及用法

翻開蘋果的觀察者api,實(shí)現(xiàn)很簡潔接口也很少,定義在NSKeyValueObserving.h里面

@interface NSObject(NSKeyValueObserverRegistration)

- (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 API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

@interface NSObject(NSKeyValueObserving)

- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

@end

@interface NSObject(NSKeyValueObserverNotification)

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

@end

如上,是通過給NSObject添加分類實(shí)現(xiàn)的:

  • NSKeyValueObserverRegistration注冊觀察者
  • observeValueForKeyPath觀察者回調(diào)
  • NSKeyValueObserverNotification觀察者通知

使用起來也很簡單,我們定義一個Person類,添加三個屬性a、b、c

@interface Person : NSObject

@property (nonatomic, assign) NSInteger a;
@property (nonatomic, assign) NSInteger b;
@property (nonatomic, assign) NSInteger c;

@end

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.person addObserver:self
                  forKeyPath:@"a"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    [self.person addObserver:self
                  forKeyPath:@"b"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    [self.person addObserver:self
                  forKeyPath:@"c"
                     options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
                     context:nil];
    self.person.a = 10;
    self.person.b = 5;
    self.person.c = 2;
    [self.person removeObserver:self forKeyPath:@"a"];
    [self.person removeObserver:self forKeyPath:@"b"];
    [self.person removeObserver:self forKeyPath:@"c"];
    NSLog(@"person對象觀察者全部移除");
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSString *,id> *)change
                       context:(void *)context {
    NSLog(@"%@屬性變化:%@", keyPath, change);
}

- (Person *)person {
    if (!_person) {
        _person = [[Person alloc] init];
    }
    return _person;
}

初始值都是0,控制臺輸出如下

2021-08-10 21:55:30.100992+0800 test[19703:48456267] a屬性變化:{
    kind = 1;
    new = 10;
    old = 0;
}
2021-08-10 21:55:30.101123+0800 test[19703:48456267] b屬性變化:{
    kind = 1;
    new = 5;
    old = 0;
}
2021-08-10 21:55:30.101235+0800 test[19703:48456267] c屬性變化:{
    kind = 1;
    new = 2;
    old = 0;
}
2021-08-10 21:55:30.101336+0800 test[19703:48456267] person對象觀察者全部移除

我們在如上位置打上斷點(diǎn),然后在控制臺打印person的isa指針,輸出如下

(lldb) po self.person->isa
NSKVONotifying_Person

(lldb) po self.person->isa
Person

可以看到,對象的觀察者沒有完全移除的時候isa指向NSKVONotifying_Person,完全移除之后isa指向Person

實(shí)現(xiàn)原理

蘋果的官方文檔有KVO實(shí)現(xiàn)原理的描述,很遺憾KVO的源碼沒有開源,不過通過上面在控制臺的打印結(jié)果,也能側(cè)面印證底層實(shí)現(xiàn)

當(dāng)對象的屬性被添加觀察者時,一個繼承自該對象所屬類的子類被動態(tài)創(chuàng)建,接著修改該對象的isa指針,使其指向該子類,并重寫了被觀察屬性的setter方法,依次調(diào)用willChangeValueForKey、父類的setter方法、didChangeValueForKey,最后會調(diào)用到該對象的observeValueForKeyPath方法,不僅如此蘋果還修改了class方法的返回值使其返回對象原本的類,目的是隱藏觀察者的底層實(shí)現(xiàn),當(dāng)對象屬性的觀察者被全部移除之后,對象的isa指針會被修正,重新指向原本的類

觀察者相關(guān)的crash

  • 添加次數(shù)多于移除次數(shù),當(dāng)監(jiān)聽者釋放后,觸發(fā)observeValueForKeyPath時crash
  • 添加次數(shù)少于移除次數(shù)指直接crash
  • 觀察者沒有實(shí)現(xiàn)observeValueForKeyPath時直接crash

如上幾個crash蘋果完全有能力避免他們發(fā)生,但是為什么蘋果沒有做這件事呢,因?yàn)樗恢烙脩舻恼嬲鈭D,蘋果期望在調(diào)試階段就暴露可能有問題的邏輯,讓其直接crash,然而事與愿違,通常我們是成對調(diào)用的,但是由于某種原因,導(dǎo)致添加和移除的次數(shù)無法匹配,最終導(dǎo)致線上大量的crash,所以crash防護(hù)需求就誕生了,沒有什么問題是添加一個中間層解決不了的,如果有,那就再添加一層
在添加或移除觀察者之前插入一層數(shù)據(jù)結(jié)構(gòu)用于存儲次數(shù),比如哈希表

添加觀察者時:控制只添加一次
移除觀察者時:控制只移除一次
觀察鍵值改變時:控制消息分發(fā)到觀察者上

為了避免被觀察者提前被釋放后,觸發(fā)observeValueForKeyPath時的crash,需要hook一下NSObject的dealloc方法,在對象dealloc函數(shù)調(diào)用之前,移除相關(guān)觀察者。

還是有點(diǎn)復(fù)雜!有沒有一種方案既可以實(shí)現(xiàn)安全性又不用hook系統(tǒng)方法呢?

實(shí)現(xiàn)安全的觀察者

一、API

干脆用runtime庫自己實(shí)現(xiàn)一個安全的觀察者,根據(jù)其實(shí)現(xiàn)原理,仿照系統(tǒng)api,通過分類的方式添加一個中間層,作者寫了一個工具,下面講述下實(shí)現(xiàn)原理,如下接口類似系統(tǒng)api,只是把回調(diào)函數(shù)寫成了block

/* - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
*/
typedef void (^SK_ObservedValueChanged) (id object, NSString *keyPath ,id oldValue, id newValue);

@interface NSObject (SafeKVO)

/// 添加安全觀察者
/// @param observer 觀察者
/// @param keyPath 屬性鏈
/// @param change 回調(diào)
- (void)sk_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath observeValueChanged:(SK_ObservedValueChanged)change;

/// 移除觀察者
/// @param observer 觀察者
/// @param keyPath 屬性鏈
- (void)sk_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

@end

同時去掉了context和options參數(shù)
原因是context參數(shù)用于給同一個屬性添加同一個觀察者同時代入上下文,回調(diào)時用于反解參數(shù),基本沒啥場景,options參數(shù)用于描述屬性改變的類型,通常只用new和change,工具已經(jīng)實(shí)現(xiàn)這兩種類型,綜上省略了context和options參數(shù)

二、安全數(shù)據(jù)模型

用于存儲:觀察者、被觀察者、屬性鏈、觀察者回調(diào)到關(guān)聯(lián)對象

@interface SafeKVOModel : NSObject

@property (nonatomic, weak) NSObject *observer;// 觀察者
@property (nonatomic, weak) NSObject *observed;// 被觀察者
@property (nonatomic, copy) NSString *keyPath;// 屬性鏈
@property (nonatomic, copy) SK_ObservedValueChanged change; // 觀察者回調(diào)
@property (nonatomic, strong) NSObject *oldValue;// 被觀察屬性原值

@end

@implementation SafeKVOModel

- (instancetype)initWithObserver:(NSObject *)observer observed:(NSObject *)observed forKeyPath:(NSString *)keyPath change:(SK_ObservedValueChanged)change {
    if (self = [super init]) {
        self.observer = observer;
        self.observed = observed;
        self.keyPath = keyPath;
        self.change = change;
    }
    return self;
}

@end
三、工具函數(shù)

通過屬性名生成setterSEL

static forceInline SEL sk_setterSelectorFromPropertyName(NSString *propertyName) {
    if (propertyName.length <= 0)
        return nil;
    NSString *setterString = [NSString stringWithFormat:@"set%@%@:", [[propertyName substringToIndex:1] uppercaseString], [propertyName substringFromIndex:1]];
    return NSSelectorFromString(setterString);
}

通過setter方法名生成屬性名

static forceInline NSString *sk_propertyNameFromSetterString(NSString *setterString) {
    if (setterString.length <= 0 || ![setterString hasPrefix: @"set"] || ![setterString hasSuffix: @":"])
        return nil;
    NSRange range = NSMakeRange(3, setterString.length - 4);
    NSString *propertyName = [setterString substringWithRange:range];
    propertyName = [propertyName stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString:[[propertyName substringToIndex: 1] lowercaseString]];
    return propertyName;
}

核心方法,子類重寫setter方法,內(nèi)部調(diào)用父類的setter方法修改值,注意系統(tǒng)的是現(xiàn)實(shí)在調(diào)用父類setter方法前后分別調(diào)用willChangeValueForKeydidChangeValueForKey方法,然后通過observeValueForKeyPath方法回調(diào)到父類,而我們這里直接通過自定義的block回調(diào),因此不用調(diào)用上面兩個方法

static forceInline void sk_setter(id self, SEL _cmd, id newValue) {
    @synchronized (self) {
        NSString *propertyName = sk_propertyNameFromSetterString(NSStringFromSelector(_cmd));
        NSParameterAssert(propertyName);
        if (!propertyName)
            return;
        
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)kSafeKVOAssiociateObservers);
        for (SafeKVOModel *model in observers) {
            if ([model.keyPath containsString:propertyName])
                model.oldValue = [model.observed valueForKeyPath:model.keyPath];
        }
        // 調(diào)用父類的set方法
        struct objc_super superClass = {
            .receiver = self,
            .super_class = class_getSuperclass(object_getClass(self))
        };
        void (*superSetter)(void *, SEL, id) = (void *)objc_msgSendSuper;
        superSetter(&superClass, _cmd, newValue);
        
        // 觀察者回調(diào)
        for (SafeKVOModel *model in observers) {
            // 觀察者未釋放才需回調(diào)
            if ([model.keyPath containsString:propertyName] && model.observer) {
                model.change(model.observed, model.keyPath, model.oldValue, [model.observed valueForKeyPath:model.keyPath]);
                model.oldValue = nil;
            }
        }
    }
}

返回父類的Class用于重寫子類的Class方法

static forceInline Class sk_class(id self) {
    return class_getSuperclass(object_getClass(self));
}

核心方法,用于動態(tài)創(chuàng)建子類并注冊到運(yùn)行時環(huán)境

static forceInline Class createSafeKVOClass(id object) {
    // 獲取以SafeKVONotifying_為前綴拼接類名的子類
    Class observedClass = object_getClass(object);
    NSString *className = NSStringFromClass(observedClass);
    NSString *subClassName = [kSafeKVOClassPrefix stringByAppendingString:className];
    Class subClass = NSClassFromString(subClassName);
    // 運(yùn)行時已經(jīng)加載該類則直接返回
    if (subClass)
        return subClass;
    
    Class originalClass = object_getClass(object);
    // 分配類和原類的內(nèi)存
    subClass = objc_allocateClassPair(originalClass, subClassName.UTF8String, 0);
    // 修改class實(shí)現(xiàn),返回父類Class
    Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
    const char *types = method_getTypeEncoding(classMethod);
    class_addMethod(subClass, @selector(class), (IMP)sk_class, types);
    // 注冊類到運(yùn)行時環(huán)境
    objc_registerClassPair(subClass);
    return subClass;
}

判斷對象是否能響應(yīng)傳入的SEL

static forceInline BOOL objectHasSelector(id object, SEL selector) {
    BOOL result = NO;
    unsigned int count = 0;
    Class observedClass = object_getClass(object);
    Method *methods = class_copyMethodList(observedClass, &count);
    for (NSInteger i = 0; i < count; i++) {
        SEL sel = method_getName(methods[i]);
        if (sel == selector) {
            result = YES;
            break;
        }
    }
    free(methods);
    return result;
}
四、API實(shí)現(xiàn)

添加安全觀察者,此處有個易忽略點(diǎn)就是keyPath的處理,需要通過屬性鏈中的類一一生成其子類,因?yàn)閗eyPath中的任意節(jié)點(diǎn)變化都有可能導(dǎo)致最終的屬性變化,都是我們監(jiān)聽的范圍

- (void)sk_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath observeValueChanged:(SK_ObservedValueChanged)change {
    @synchronized (self) {
        NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge void *)kSafeKVOAssiociateObservers);
        for (SafeKVOModel *observerModel in observers) {
            // 已添加過同一個觀察者,無需重復(fù)添加
            if (observerModel.observer == observer && observerModel.observed == self && [observerModel.keyPath isEqualToString:keyPath]) {
                return;
            }
        }
        // 通過keyPath依次執(zhí)行->創(chuàng)建子類重寫set方法操作
        NSArray *keys = [keyPath componentsSeparatedByString:@"."];
        NSInteger index = 0;
        id object = self;
        while (index < keys.count) {
            SEL setterSelector = sk_setterSelectorFromPropertyName(keys[index]);
            Method setterMethod = class_getInstanceMethod([object class], setterSelector);
            NSParameterAssert(setterMethod);
            if (!setterMethod) {
                return;
            }
            id nextObject = [object valueForKey:keys[index]];
            Class observedClass = object_getClass(object);
            NSString *className = NSStringFromClass(observedClass);
            if (![className hasPrefix:kSafeKVOClassPrefix]) {
                // 創(chuàng)建子類并修改本類isa指針使其指向子類
                observedClass = createSafeKVOClass(object);
                object_setClass(object, observedClass);
            }
            if (!objectHasSelector(object, setterSelector)) {
                // 重寫set方法在方法里調(diào)用父類的set方法并通過block回調(diào)到上層,以完成監(jiān)聽過程
                const char *types = method_getTypeEncoding(setterMethod);
                class_addMethod(observedClass, setterSelector, (IMP)sk_setter, types);
            }
            // 添加監(jiān)聽者到類的關(guān)聯(lián)對象數(shù)組
            observers = objc_getAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers);
            if (!observers) {
                observers = [NSMutableArray array];
                objc_setAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            }
            SafeKVOModel *kvoModel = [[SafeKVOModel alloc] initWithObserver:observer observed:self forKeyPath:keyPath change:change];
            [observers addObject:kvoModel];
            
            index++;
            if (index < keys.count) {
                object = nextObject;
            }
        }
    }
}

遍歷清除觀察者,若已經(jīng)清空則修正對象的isa指針

- (void)sk_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    @synchronized (self) {
        NSArray *keys = [keyPath componentsSeparatedByString:@"."];
        NSInteger index = 0;
        id object = self;
        while (index < keys.count) {
            SafeKVOModel *modelRemoved = nil;
            NSMutableArray *observers = objc_getAssociatedObject(object, (__bridge void *)kSafeKVOAssiociateObservers);
            for (SafeKVOModel *model in observers) {
                if (model.observer == observer && model.observed == self && [model.keyPath isEqualToString:keyPath]) {
                    modelRemoved = model;
                    break;
                }
            }
            if (modelRemoved) {
                [observers removeObject:modelRemoved];
                if (!observers.count) {
                    object_setClass(object, [object class]);
                }
            } else {
                object_setClass(object, [object class]);
            }
            object = [object valueForKey:keys[index]];
            index++;
        }
    }
}

總結(jié)

本工具支持了多線程,同時通過runtime和關(guān)聯(lián)對象實(shí)現(xiàn)了安全觀察者,解決了觀察者添加、移除、回調(diào)的各種crash,注意,本代碼還沒有經(jīng)過大量測試,如有需要,請務(wù)必反復(fù)測試之后再應(yīng)用于項(xiàng)目中
下載鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容