KVO學習小結(jié)

KVO概述

KVO,或者key-value observing,是可以對OC對象的屬性進行觀察,并在屬性發(fā)生改變的的時候,發(fā)出通知的神奇魔法。之所以稱之為神奇,是因為KVO是依賴Object-C運行時機制,而我們需要處理很少的工作,就可以發(fā)送和接受通知。

KVO的使用

KVO的使用步驟

1. 注冊觀察者,為觀察者者添加注冊的key
    - (void)addObserver:(NSObject *)observer
             forKeyPath:(NSString *)keyPath
                options:(NSKeyValueObservingOptions)options
                context:(nullable void *)contex;
  • observer : 觀察者

  • keyPath :需要觀察的屬性

  • option :需要觀察的屬性值的變化的選擇項。

    • NSKeyValueObservingOptionInitial : 如果需要在觀察者注冊了監(jiān)聽后立刻發(fā)送消息給observer,可以添加這個選擇項。

    • NSKeyValueObservingOptionNew: 在回調(diào)中,獲取屬性改變之后的值。

    • NSKeyValueObservingOptionOld : 在回調(diào)方法中,獲取屬性改變之前的值。

  • context :可以認為是在回調(diào)方法中用來區(qū)分不同通知的來源的標識。

2. 實現(xiàn)KVO的回調(diào)方法
   //監(jiān)聽的回調(diào)
    - (void)observeValueForKeyPath:(NSString *)keyPath
     ofObject:(id)object
     change:(NSDictionary<NSKeyValueChangeKey,id> *)change
     context:(void *)context;
  • keyPath: 被觀察的屬性值

  • object:被 觀察的屬性值所在的對象

  • change: 是一個字典值,可以在這個對象中取出改變前后的值

    • NSKeyValueChangeNewKey

    • NSKeyValueChangeOldKey

  • context: 用來區(qū)分不同通知的來源的標識,與添加觀察者方法的context搭配使用,是同一個值。

3. 移除觀察者
    - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
  • observer: 觀察者

  • keyPath: 被觀察的屬性值

4. 代碼
    //定義 context
    static void * M2_KVONameContext = &M2_KVONameContext;

    //1、添加觀察者
    - (void)addObserver{
       //添加觀察者,使用context標識
       [self.m2 addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionOld |  NSKeyValueObservingOptionNew  context:M2_KVONameContext];
    }
    
    //2、監(jiān)聽的回調(diào)
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
       if (context == M2_KVONameContext){
         if ([keyPath isEqual:@"name"]) {
           NSLog(@"NSKeyValueObservering new is %@", change[NSKeyValueChangeNewKey]);
           NSLog(@"NSKeyValueObservering old is %@", change[NSKeyValueChangeOldKey]);
         }
       }
    }
    
    - (void)dealloc{
       //3、移除
       [self.m2 removeObserver:self forKeyPath:@"name" context:M2_KVONameContext];
    }

KVO能夠觀察到回調(diào)的方式

  1. 屬性的點語法。例:self.m2.name = @"我是測試數(shù)據(jù)";

  2. KVC方式進行賦值。例:[self.m2 setValue:@"我是測試數(shù)據(jù)" forKey:@"myName"];

  3. 直接操作成員變量的方法,是不會觸發(fā)KVO回調(diào)的,這種情況下可以使用KVC的方法。

    • self.m2->pName = @"我是測試數(shù)據(jù)";這種方式不會回調(diào)KVO

    • [self.m2 setValue:@"我是測試數(shù)據(jù)" forKey:@"pName"];是可以回調(diào)KVO的

KVO禁止自動通知觀察者

在程序中,一個對象的屬性注冊了觀察者,但是不希望獲取到該屬性的KVO回調(diào)。比如有一些關鍵信息,不希望三方進行觀察,就可以使用下面這個方法進行處理。

方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;

代碼實例:

//禁止某個屬性自動的通知觀察者,例如關鍵信息不想讓三方知道,就可以重寫這個函數(shù)
//該方法默認發(fā)揮true
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
   if ([key isEqual:@"name"]) {
     return  NO;
   }
   return [super automaticallyNotifiesObserversForKey:key];
}

KVO手動觸發(fā)

KVO的屬性發(fā)生改變時,是系統(tǒng)自動通知給觀察者。如果自己希望實現(xiàn)自定義傳遞消息的時機,可以使用下面的兩個方法。

涉及的兩個方法:

  • -(void)willChangeValueForKey:(NSString *)key:在屬性發(fā)生改變前調(diào)用

  • - (void)didChangeValueForKey:(NSString *)key;: 在屬性發(fā)生改變后調(diào)用

把屬性的key值作為參數(shù),發(fā)送NSKeyValueChangeSetting類型的通知消息,并把發(fā)送給每一個注冊該key的觀察者。

這兩個方法的調(diào)用,必須始終成對進行。

代碼實例,實現(xiàn)在屬性值前后不一致的情況下發(fā)送消息,可以和automaticallyNotifiesObserversForKey 合作完成這個實例。

//手動觸發(fā)
-(void)setName:(NSString *)name{
   if (name != _name) {
     //手動觸發(fā)KVO
     [self willChangeValueForKey:@"name"];
     _name = name;
     //手動觸發(fā)KVO
     [self didChangeValueForKey:@"name"];
   }
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
   if ([key isEqual:@"name"]) {
     return  NO;
   }
   return [super automaticallyNotifiesObserversForKey:key];
}

可變數(shù)組的KVO通知

觀察可變數(shù)組,可變數(shù)組實例化以后,添加元素的過程中,如果直接調(diào)用- (void)addObject:(ObjectType)anObject方法是不會通知KVO回調(diào)的。需要使用KVO中的方法- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key,然后再去調(diào)用addObject方法。

對象中添加愛可變數(shù)組

@interface SecondModel : NSObject
 //添加可變數(shù)組
 @property (nonatomic, strong) NSMutableArray * mutableArray;
@end

調(diào)用可變數(shù)組的添加方法

//實例化可變數(shù)組
self.m2.mutableArray = [NSMutableArray array];
//為其添加觀察者
[self.m2 addObserver:self forKeyPath:@"mutableArray" options: NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:M2_KVONameContext];
//直接調(diào)用addObject方法,不會觸發(fā)KVO回調(diào)
[self.m2.mutableArray addObject:@"1"];
// 會觸發(fā)KVO回調(diào)
[[self.m2 mutableArrayValueForKey:@"mutableArray"] addObject:@2];

KVC和KVO

如果對象的屬性或者成員變量實現(xiàn)了KVO監(jiān)聽,使用KVC對屬性或者成員變量進行賦值,都會產(chǎn)生KVO的通知消息,調(diào)起回調(diào)。

KVC是怎樣實現(xiàn)的KVO回調(diào)的?下面我們添加一個成員變量myName, 并實現(xiàn)其settergetter方法,重寫- (void)willChangeValueForKey:(NSString *)key-(void)didChangeValueForKey:(NSString *)key兩個方法。在控制器中對添加myName的KVO監(jiān)聽,使用KVC進行賦值。

@interface SecondModel(){
   NSString * myName;
}
@end
@implementation SecondModel
//setter
- (void)setMyName:(NSString*)myName{
    NSLog(@"func is %@",NSStringFromSelector(_cmd));
    self->myName = myName;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
   NSLog(@"forUndefinedKey is %@",key);
}
//getter
- (NSString *)myName{
   NSLog(@"func is %@",NSStringFromSelector(_cmd));
   return self->myName;
}
- (id)valueForUndefinedKey:(NSString *)key{
   NSLog(@"valueForUndefinedKey is %@",key);
   return @"valueForUndefinedKey";
}
//沒有找到setter/getter的情況下,是否直接訪問成員變量
+ (BOOL)accessInstanceVariablesDirectly{
   return false;
}
- (void)willChangeValueForKey:(NSString *)key{
   NSLog(@"willChangeValueForKey ------ start");
   [super willChangeValueForKey:key];
   NSLog(@"willChangeValueForKey ------ end");
}
-(void)didChangeValueForKey:(NSString *)key{
   NSLog(@"willChangeValueForKey ------ start");
   [super didChangeValueForKey:key];
   NSLog(@"willChangeValueForKey ------ end");
}
@end

程序運行后,可以看到起打印結(jié)果為:

//willChangeValueForKey方法內(nèi)部調(diào)用了一次getter
willChangeValueForKey ------ start
func is myName
willChangeValueForKey ------ end

//賦值 setter方法
func is setMyName:

//didChangeValueForKey方法內(nèi)部調(diào)用了一次getter,并進行回調(diào)
didChangeValueForKey ------ start
func is myName
KVO 回調(diào) : observeValueForKeyPath: ofObject: change: context:
didChangeValueForKey ------ end

可以看出,使用KVC對成員變量進行賦值的時候,是調(diào)用了以下兩個方法的:

  • -(void)willChangeValueForKey:(NSString *)key:在屬性發(fā)生改變前調(diào)用

  • - (void)didChangeValueForKey:(NSString *)key;: 在屬性發(fā)生改變后調(diào)用

如果在工程中,不希望別人通過使用KVC對屬性進行賦值,來監(jiān)聽屬性變化的話,可以使用KVC的方法+ (BOOL)accessInstanceVariablesDirectly。這是因為在KVC中,查詢不到屬性的setter/getter方法的情況下,會查詢該方法的返回值。如果該方法返回為true,會直接去操作成員變量,對成員變量進行賦值或者取值;如果該方法返回為false的話,就不會直接操作成員變量,而是拋出異常。所以如果重寫了這個方法,并且針對key的返回值是false,在willChangeValueForKey 中調(diào)用getter方法是,就會拋出異常,別人也就無法實現(xiàn)屬性的監(jiān)聽。

KVO原理探究

最近一直在學習KVO的內(nèi)容,所以也搜索了很多相關的文章,大牛們寫的都很詳細,也很清楚。本菜鳥是站在巨人的肩膀上看世界,大牛們高屋建瓴,我也是看了挺長時間,大牛們是摸著石頭過河,我是摸著大牛們過河,也算是基本對于KVO的實現(xiàn)原理有了理解。

簡單概括KVO的實現(xiàn):

KVO的實現(xiàn)主要依靠Runtime技術,當一個對象的屬性添加了觀察者(observer)以后,系統(tǒng)會調(diào)用Runtime的相關方法,創(chuàng)建出一個中間類,這個類的類名以NSKVONotifying_開頭,并且繼承于添加了觀察者的屬性所在的對象的類。并且在中間類中,重寫了屬性的setter方法。在重寫的setter中,會調(diào)用父類的setter,并且在調(diào)用父類setter方法之前和之后,添加了通知該屬性變化的方法。于此同時,重寫了中間類的- (Class)class方法,使得該方法的返回值仍然是原類。也會重寫- (void)dealloc,以完成相關的銷毀工作。完成以上步驟后,修改原對象的isa指針,使其指向這個中間類,這樣操作以后,原來的對象就變成了中間類的實例對象。

驗證以上說明,對添加KVO觀察前后的對象進行打印,并打印其屬性列表和方法列表,添加一下代碼:

- (void)desObject{
   //類名
   NSString *classMethodName = NSStringFromClass([self.m2 class]);
   NSString * objc_Runtime_Method_Name = NSStringFromClass(object_getClass(self.m2));

   //成員變量列表
   NSMutableArray *ivarStringList = [NSMutableArray array];
   unsigned int invarsCount = 0;
   Ivar *ivarList = class_copyIvarList(object_getClass(self.m2), &invarsCount);
   for (int j=0; j<invarsCount; j++) {
     Ivar i = ivarList[j];
     const char *iName = ivar_getName(i);
     [ivarStringList addObject:[NSString stringWithUTF8String:iName]];
   }

  //方法列表
   NSMutableArray *methodStringList = [NSMutableArray array];
   unsigned int count = 0;
   Method *methodList = class_copyMethodList(object_getClass(self.m2), &count);
   for (int i=0; i<count; i++) {
     Method m = methodList[i];
     SEL selector = method_getName(m);
     [methodStringList addObject:NSStringFromSelector(selector)];
   }

   //打印
   NSLog(@"classMethodName is %@  |--| objc_Runtime_Method_Name is %@",classMethodName,objc_Runtime_Method_Name);
   NSLog(@"ivarList is %@",ivarStringList);
   NSLog(@"methodStringList is %@",methodStringList);
   NSLog(@"~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
}

并在添加KVO前后添加該方法:

 [self desObject];
 [self.m2 addObserver:self forKeyPath:@"name" options: NSKeyValueObservingOptionOld |  NSKeyValueObservingOptionNew  context:M2_KVONameContext];
 [self desObject];

運行程序后,得到以下的信息:

classMethodName is SecondModel  |--| objc_Runtime_Method_Name is SecondModel
ivarList is (
 "_name"
)
methodStringList is (
 name,
 "setName:",
 ".cxx_destruct"
)
~~~~~~~~~~~~~~~~~^^添加KVO之前^^~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~vv添加KVO之后vv~~~~~~~~~~~~~~~~~~~~
classMethodName is SecondModel  |--| objc_Runtime_Method_Name is NSKVONotifying_SecondModel
ivarList is (
)
methodStringList is (
 "setName:",
 class,
 dealloc,
 "_isKVOA"
)

又以上的打印結(jié)果可知:

  1. 使用[self.m2 class]打印的類名是SecondModel,而使用object_getClass(self.m2) 打印的類名是NSKVONotifying_SecondModel

  2. 中間類的方法列表中有class方法,說明中間類是重寫了這個方法,并且重寫后,該方法的返回值是原類。

  3. 中間類的方法列表中有setName:方法,說明中間類也重寫了這個方法。

KVO的自定義實現(xiàn)

研究了這兩篇文章:KVO原理分析runtime模擬實現(xiàn)KVO監(jiān)聽機制,也按照二位大神的方式,自己敲代碼,把block形式的KVO進行了實現(xiàn)。在研究期間,對于KVO的實現(xiàn)原理理解的更清楚。有想要深刻了解的同學,可以看看這兩篇文章,大有裨益。我這里對block形式的KVO總結(jié)了一個流程圖,展示如下:

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

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

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