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