KVO使用及實現原理

Corbin___發表於2019-01-11

KVO使用及實現原理

KVO使用

  • 對屬性進行監聽
  • 對屬性的屬性進行監聽
  • 容器監聽
  • 觸發(手動觸發,kvc賦值)

新增監聽

- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
複製程式碼

引數含義:

1.observer:觀察者,監聽屬性變化的物件。該物件必須實現 observeValueForKeyPath:ofObject:change:context: 方法,否則,當觸發KVO的時候,會crash

2.keyPath:要觀察的屬性名稱。要和屬性宣告的名稱一致,這裡建議用NSStringFromSelector(@selector(name))來寫

3.options:對KVO機制進行配置,修改KVO通知的時機以及通知的內容

4.context:傳入任意型別的物件,在"接收訊息回撥"的程式碼中可以接收到這個物件,是KVO中的一種傳值方式。


    // 1.kvo對屬性的監聽
    [_person addObserver:self forKeyPath:NSStringFromSelector(@selector(name)) options:NSKeyValueObservingOptionNew context:nil];
    // 2.kvo屬性關聯,這個我們是監聽dog的變化,那如果是dog屬性的變化,如果不做處理,是監聽不到的
    [_person addObserver:self forKeyPath:NSStringFromSelector(@selector(dog)) options:NSKeyValueObservingOptionNew context:nil];
    // 3.容器監聽
    [_person addObserver:self forKeyPath:NSStringFromSelector(@selector(mArr)) options:NSKeyValueObservingOptionNew context:nil];

複製程式碼

觸發程式碼

    // 觸發基本知識點:KVO得通過set方法才可以觸發
    
     NSString *name = NSStringFromSelector(@selector(name));
    
    // 手動觸發 這兩個方法必須是成對的,然後回撥observeValueForKeyPath方法,至於為什麼成對呢,猜想應該是蘋果做了處理,點進去官方的註釋也有說明這兩個方法必須成對存在
    // 如果想要手動觸發需要在被監聽類中實現+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key返回NO
    [_person willChangeValueForKey:name];
    [_person didChangeValueForKey:name];
    
    // kvc
    [_person setValue:@"123" forKey:name];
    
    // 容器
    NSMutableArray *arr1 = [_person mutableArrayValueForKey:NSStringFromSelector(@selector(mArr))];
    [arr1 addObject:@"123"];
    
    NSMutableArray *arr2 = [_person mutableArrayValueForKey:NSStringFromSelector(@selector(mArr))];
    [arr2 replaceObjectAtIndex:0 withObject:@"1234"];
    
    NSMutableArray *arr3 = [_person mutableArrayValueForKey:NSStringFromSelector(@selector(mArr))];
    [arr3 removeAllObjects];
    

    
    // 屬性關聯
    // 屬性關聯需要在被監聽類中實現+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
    _person.dog.name = @"myDog";

複製程式碼

被監聽類中實現的程式碼

// 這個方法不重寫,就是預設返回YES,如果重寫了返回NO,那麼就是需要手動觸發了,當然這個可以根據引數key來判斷,區分監聽的key是手動還是自動 
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    return YES;
}

// 這個方法用於監聽屬性的屬性的(屬性關聯)
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"dog"]) {
        return [[NSSet alloc] initWithObjects:@"_dog.name", nil];
    } else {
        return [super keyPathsForValuesAffectingValueForKey:key];
    }
}

複製程式碼

移除監聽

懶得寫
複製程式碼

KVO原理

1.在執行時的時候建立被監聽類的子類

    /** 1.動態生成一個類 */
    /** 1.1 動態生成一個類名 */
    NSString *oldClassName = NSStringFromClass(self.class);
    NSString *newClassName = [@"KVO_" stringByAppendingString:oldClassName];
    /** 定義一個類,繼承傳進來的類 */
    Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
複製程式碼

2.在子類中重寫父類屬性的set方法(所以KVO只能監聽屬性)

    /** 新增set方法 */
    class_addMethod(MyClass, @selector(setName:), (IMP)setName, "V@:@");
複製程式碼

3.註冊這個子類

    /** 註冊該類 */
    objc_registerClassPair(MyClass);
複製程式碼

4.修改當前被監聽物件的isa指標指向子類

    /** 修改isa指標 */
    object_setClass(self, MyClass);
複製程式碼

5.實現set函式


/** set方法 */
void setName(id self,SEL _cmd,NSString * newName){
    /** 儲存當前型別 */
    Class class = [self class];
    /** 呼叫父類方法 */
    object_setClass(self, class_getSuperclass(class));
    objc_msgSend(self, @selector(setName:),newName);
    /** 通知觀察者 */
    // 這裡有個屬性繫結,所以在新增觀察者的時候,就是將這個觀察者持有(就是使用屬性繫結來持有)
    id observer = objc_getAssociatedObject(self, @"objc");
    if (observer) {
        objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",self,@{@"new:":[self valueForKey:@"name"],@"kind:":@1},nil);
    }
    /** 改回子類 */
     object_setClass(self, class);
     
    // kvo就是在set方法中呼叫
    willChangeValueForKey:
    didChangeValueForKey:;
    那我們這裡需不需要呢,當然不需要,因為我們是自己實現了一套了,調這兩句也沒有用的
}
複製程式碼

思考點

1.為什麼kvc可以觸發,kvc的原理

2.建立一個子類,如果建立的子類的類名,專案中剛好存在呢

如果專案中存在,那個建立的子類會nil,那麼這時候我們可以使用遞迴建立,為nil就在類名後拼1或者其他字元吧,直到成功為止

3.objc_msgSend(self, @selector(setName:),newName);可以傳值,那這個跟performSelector:有什麼關係,而且發現直接用objc_msgSend還比performSelector方便

4.為什麼oc的每個方法都有兩個隱式引數,這兩個是哪裡來的

oc呼叫方法是訊息機制,表現形式就是objc_msgSend(self, @selector(setName:),newName);那麼每個方法在呼叫的本質都是使用objc_msgSend那這個剛好就需要傳兩個引數,呼叫者和方法編號,也就是isa和sel

相關文章