iOS:KVO

本文僅是記錄自己在學習的過程中的理解:如有錯誤,還望各位大佬指正,THX.

KVO全稱KeyValueObserving,是蘋果提供的一套事件通知機制。允許對象監(jiān)聽另一個對象特定屬性的改變,并在改變時接收到事件。由于KVO的實現機制,所以對屬性才會發(fā)生作用,一般繼承自NSObject的對象都默認支持KVO。

KVO和NSNotificationCenter都是iOS中觀察者模式的一種實現。區(qū)別在于,相對于被觀察者和觀察者之間的關系,KVO是一對一的,而一對多的。KVO對被監(jiān)聽對象無侵入性,不需要修改其內部代碼即可實現監(jiān)聽。

KVO可以監(jiān)聽單個屬性的變化,也可以監(jiān)聽集合對象的變化。通過KVC的mutableArrayValueForKey:等方法獲得代理對象,當代理對象的內部對象發(fā)生改變時,會回調KVO監(jiān)聽的方法。集合對象包含NSArray和NSSet。

1. KVO 的基本使用

相信大家在平時的開發(fā)中都使用過KVO,使用KVO分為3個步驟:
1.通過- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;方法注冊觀察者,觀察者可以接收keyPath屬性的變化事件。

參數部分:
--Observer參數:觀察者對象
--keyPath參數:需要觀察的屬性,由于是字符串的形式,寫錯的話很容易導致崩潰,一般利用系統的反射機制NSStringFromSelector(@selector(keyPath));
--options參數:枚舉類型
NSKeyValueObservingOptionNew 接收新值,默認為只接收新值
NSKeyValueObservingOptionOld 接收舊值
NSKeyValueObservingOptionInitial 在注冊的時候立即接收一次回調,在改變是也會發(fā)生通知
NSKeyValueObservingOptionPrior 改變之前發(fā)一次,改變之后發(fā)一次
--context參數:傳入任意類型的對象,在接收消息回調的代碼中可以接收到這個對象,是KVO中的一種傳值方式
**注意:在調用addObserver方法后,KVO并不會對觀察者進行強引用,所以需要注意觀察者的生命周期,否則會導致由于觀察者的釋放而帶來的崩潰。

2.在觀察者中實現-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context方法,當keyPath屬性發(fā)生改變之后,KVO會回調這個方法來通知觀察者屬性的改變。
3.當觀察者不需要監(jiān)聽的時候,可以調用- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;方法將KVO移除,需要注意的是,調用removeObserver需要在觀察者消失之前,否則會導致崩潰。一般在dealloc中調用。

KVO的addObserver和removeObserver需要是成對的,如果重復remove則會導致NSRangeException類型的Crash,如果忘記remove則會在觀察者釋放后再次接收到KVO回調時Crash。蘋果官方推薦的方式是,在init的時候進行addObserver,在dealloc時removeObserver,這樣可以保證add和remove是成對出現的,是一種比較理想的使用方式。

2. KVO的觸發(fā)模式

KVO在屬性發(fā)生改變的時候默認是自動調用的,如果需要手動的控制這個調用時機,或者自己來實現KVO屬性的調用,可以通過KVO提供的方法來調用。
在所要觀察的對象.m文件中加入:

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    return YES;//默認,自動模式
    return NO;//手動模式
}

同時在屬性變化之前,調用:

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

在屬性變化之后,調用:

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

其實無論屬性的值是否發(fā)生改變,是否調用Setter方法,只要調用了willChangeValueForKey:和didChangeValueForKey:就會觸發(fā)回調。

一般我們在開發(fā)的時候,需要用到KVO監(jiān)聽屬性值得變化,一般不會將所有的值得監(jiān)聽都是手動的觸發(fā),同時我們也看到automaticallyNotifiesObserversForKey:傳入了一個參數key, 就是為了讓我們根據key來決定是否手動開啟KVO.

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;//手動模式
    }
    return YES;//默認,自動模式
}
3. KVO屬性依賴

如果在當前Person類中引入另外一個Dog類:

//  Dog.h
@interface Dog : NSObject
@property (nonatomic,assign) NSInteger age;
@property (nonatomic,assign) NSInteger level;
@end

//  Person.h
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong) Dog *dog;
@end

//Person.m
@implementation Person
-(instancetype)init
{
    if (self = [super init]) {
        _dog = [[Dog alloc] init];
    }
    return self;
}
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;//手動模式
    }
    return YES;//默認,自動模式
}

那么此時我們怎么通過Person來觀察Dog類的age屬性呢?

[_p addObserver:self forKeyPath:@"dog.age" options:NSKeyValueObservingOptionNew context:nil];

如果Dog類有多個屬性;那么我們現在希望,只要Dog類中有屬性的變化,就會通知到Person類,如果我們每一個屬性都添加一遍觀察者,是不是很麻煩,那么這里就需要用到屬性依賴:我們在Person類的.m中添加一個方法:

+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPath = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqual:@"dog"]) {
        keyPath = [[NSSet alloc] initWithObjects:@"_dog.age",@"_dog.level", nil];
    }
    return keyPath;
}

同時在添加觀察者時,不用對dog具體的屬性添加:
 [_p addObserver:self forKeyPath:@"dog" options:NSKeyValueObservingOptionNew context:nil];
4. KVO 的原理

KVO的其實就是觀察屬性的變化,也就是setter方法的變化,但是上面我們也提到過就是不需要調用setter方法同樣可以觸發(fā)KVO,那么KVO到底是不是觀察setter方法呢?現在我們把代碼恢復到最初的時候,此時只觀察Person類的name屬性,如果此時把name改成成員變量:

//  Person.h
@interface Person : NSObject
{
 @public
    NSString *name;
}
//@property (nonatomic,copy) NSString *name;
@end

//調用改變name
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    static int a;
    
     _p.name = [NSString stringWithFormat:@"%d",a++];
 }

當改變name的值得時候,可以發(fā)現此時并不會有回調。
那么可以知道,其實KVO觀察的還是屬性的setter方法。
那么如何實現當調用Person類對象的setter方法的時候能夠觀察到改變呢?一般有兩種方式實現:分類和子類繼承
那么我們可以試一下分類,創(chuàng)建一個Person的分類,并在分類里重寫setName:方法,發(fā)現是可行的。但此時有一個隱患存在,因為此時我們已經在分類中實現了setName:方法,等于就是替換掉了Person類的setName:方法,此時Person類的setName:方法就不會被調用,而此時如果又需要重寫Person類的setName:方法,那么就會出現影響。

KVO 底層實現:首先KVO需要創(chuàng)建一個子類(NSKVONotyfing_Person),這個子類是繼承于被觀察對象的,這個子類需要重寫屬性的setter方法,這個時候,外界在調用setter方法的時候,調用的是子類重寫的setter方法。就是讓外界的person對象的isa指針指向這個子類。

在添加觀察者的地方打個斷點來看一下:
isa指向.png

。此時Person類對象的isa指針指向的就是子類對象。

5. 自定義KVO

首先創(chuàng)建一個NSObject的分類:

//  NSObject+KVO.h
#import <Foundation/Foundation.h>
@interface NSObject (KVO)

-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
@end
//  NSObject+KVO.m
#import "NSObject+KVO.h"
#import <objc/message.h>

@implementation NSObject (KVO)


-(void)KVO_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
    //創(chuàng)建一個類
    NSString *oldClassName = NSStringFromClass(self.class);
    NSString *newClassName = [@"KVO_" stringByAppendingString:oldClassName];
    Class MyClass = objc_allocateClassPair(self.class, newClassName.UTF8String, 0);
    //注冊類
    objc_registerClassPair(MyClass);  //MyClass繼承于self.class 根據案例來看,此時MyClass繼承于Person,那么此時MyClass這個子類是否具有父類Person的setName:方法呢? 沒有,只不過我們在調用方法的時候,子類繼承于父類,如果子類沒有實現方法,回去父類中調用該方法,所以在潛意識上,我們人為子類具有父類的方法,所謂的重寫子類的方法,其實就是給這個子類添加一個方法。
   
    //重寫setName方法
    class_addMethod(MyClass, @selector(setName:), (IMP)setName, "v@:@");
    //class_addMethod(<#Class  _Nullable __unsafe_unretained cls#>, <#SEL  _Nonnull name#>, <#IMP  _Nonnull imp#>, <#const char * _Nullable types#>)
    //參數名稱   參數
    //Class  給那個類添加方法
    //SEL  方法編號
    //IMP  方法實現(指針)
    //types  返回值類型
    
    //修改isa指針
    object_setClass(self, MyClass);
    
    //將觀察者保存到當前對象
    objc_setAssociatedObject(self, @"observer", observer, OBJC_ASSOCIATION_ASSIGN);
    
}
void  setName(id self,SEL _cmd,NSString * newName){
    NSLog(@"%@",newName);
    
    //調用父類的setName:方法
    Class class =  [self class];//拿到當前類型
    object_setClass(self, class_getSuperclass(class));//修改當前類型,變成父類
    
    objc_msgSend(self, @selector(setName:),newName);
 
    //拿到Observer,
   id observer = objc_getAssociatedObject(self, @"observer");
    if (observer) {
        objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),@"name",@{@"name":newName,@"kind":@1},nil);
    }
    
    //改回子類
    object_setClass(self, class);
}
@end

這么寫的KVO不會覆蓋父類的set方法,也不會因為沒有在dealloc中remove掉observer而崩潰掉。

6. 容器類的KVO
//  Person.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong)NSMutableArray * array;

@end


//  Person.m

-(NSMutableArray *)array
{
    if (!_array) {
        _array = [NSMutableArray array];
    }
 return _array;
}

//  ViewController.m
[_p addObserver:self forKeyPath:@"array" options:NSKeyValueObservingOptionNew context:nil];

其實注冊觀察者的步驟與屬性時一樣的,只不過在修改array的時候有些變化,因為KVO監(jiān)聽的是set方法,而對array進行操作卻不是set方法,這時候其實KVO提供了一個方法:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray *tempArray = [_p mutableArrayValueForKey:@"array"];
    [tempArray addObject:@"xxxx"];
   //利用tempArray 去進行操作
}

通過斷點來看一下tempArray的類型:


tempArray.png

最后補充幾個注意:
1.kvo的本質是什么?
利用runtimeAPI動態(tài)生成一個子類,并讓instance對象的isa指向這個全新的子類,當修改instance對象的屬性時,會調用willChangeValueForKey和didChangeValueForKey( 在父類原來的setter方法)并調用內部會觸發(fā)監(jiān)聽器的監(jiān)聽方法(observerValueForKeyPath:)。

2.直接修改成員變量會觸發(fā)KVO么?
不會觸發(fā)KVO,添加KVO的Person實例,其實是NSKVONotyfing_Person類,再調用setter方法,不是調用Person的setter方法,而是NSKVONotyfing_Person的setter方法,因為修改成員變量不是setter方法賦值。

3.如果在項目中對Person類進行了監(jiān)聽,也創(chuàng)建了一個NSKVONotifying_Person類,那么會編譯通過么?
編譯通過,因為KVO是運行時刻創(chuàng)建的,并不在編譯時刻,在編譯時刻只有一個NSKVONotifying_Person,所以不報錯,可以通過,但是此時KVO起不了作用。(KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class)

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

相關閱讀更多精彩內容

  • 上一篇:iOS-KVC淺談 前言:KVO 作為 KVC 的同袍兄弟,功能更強大,聊聊 KVO。 一、KVO 簡介 ...
    夢蕊dream閱讀 808評論 0 0
  • 一、概述 KVO,即:Key-Value Observing,它提供一種機制,當指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,198評論 11 33
  • iOS--KVO的實現原理與具體應用 長時間不用容易忘,這篇文章挺好的.轉載自看本文分為2個部分:概念與應用。概念...
    超_iOS閱讀 1,502評論 0 17
  • 概述 KVO全稱為Key Value Observing,鍵值監(jiān)聽機制,由NSKeyValueObserving協...
    rightmost閱讀 1,372評論 0 0
  • 任何情緒的銹跡都在加速中 暢快地盛開 然后包圍你 瘦小潮濕的身體 從襁褓到化石 無處不在的搖曳 開滿任意一個季節(jié) ...
    烏鴉之白閱讀 160評論 0 0

友情鏈接更多精彩內容