深刻理解KVO

先來講說什麼是KVO, KVO全稱爲Key Value Observing,鍵值監聽機制,由NSKeyValueObserving協議提供支持,NSObject類繼承了該協議,因此NSObject的子類均可使用該方法。

KVO的使用

一、註冊觀察者數組

//註冊觀察者
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
複製代碼

option:緩存

  • NSKeyValueObservingOptionOld 把更改以前的值提供給處理方法
  •  NSKeyValueObservingOptionNew 把更改以後的值提供給處理方法
  •  NSKeyValueObservingOptionInitial 把初始化的值提供給處理方法,一旦註冊,立馬就會調用一次。一般它會帶有新值,而不會帶有舊值。
  •  NSKeyValueObservingOptionPrior 分2次調用。在值改變以前和值改變以後。

context:bash

這裏的context字面上面的意思是上下文。可是在實際的開發中,咱們能夠把它理解成爲一個標記。一般是爲了在類和類的子類中同時對一個屬性進行監聽時,爲了區分兩個監聽,則能夠在context中傳入一個標識"person_name"。若是不須要傳入值,則傳入NULL便可。app

那麼用context有什麼好處呢?在咱們接收屬性變化的回調的時候,同時會拿到相應的 keyPathobjectchangecontext。若是去判斷 keyPath的話,咱們須要判斷緩存列表,還要判斷類的列表,才能找到相應的屬性值。可是 context能夠看作是一個靜態的值放在內存中,因此用 context會讓性能提高很多。


二、監聽回調ide

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {    
    NSLog(@"%@",change);
}複製代碼

三、移除觀察者函數

- (void)dealloc {    
    [self.person removeObserver:self forKeyPath:@"name"];
}複製代碼

那麼咱們可能要思考爲何要移除觀察者呢?咱們再查看了官方文檔以後,就能清楚的看見:性能

When removing an observer, keep several points in mind:測試

  • Asking to be removed as an observer if not already registered as one results in an NSRangeException. You either callremoveObserver:forKeyPath:context: exactly once for the corresponding call to addObserver:forKeyPath:options:context:, or if that is not feasible in your app, place the removeObserver:forKeyPath:context: call inside a try/catch block to process the potential exception.ui

  • An observer does not automatically remove itself when deallocated. The observed object continues to send notifications, oblivious to the state of the observer. However, a change notification, like any other message, sent to a released object, triggers a memory access exception. You therefore ensure that observers remove themselves before disappearing from memory.spa

  • The protocol offers no way to ask an object if it is an observer or being observed. Construct your code to avoid release related errors. A typical pattern is to register as an observer during the observer’s initialization (for example in init or viewDidLoad) and unregister during deallocation (usually in dealloc), ensuring properly paired and ordered add and remove messages, and that the observer is unregistered before it is freed from memory.

解除分配時,觀察者不會自動刪除自身。被觀察對象繼續發送通知,無視觀察者的狀態。可是發送到已發佈對象的更改通知與任何其餘消息同樣,會去出發內存訪問異常。所以,要確保觀察者在從內存中消失以前將其移除。

KVO的監聽

一、自動監聽屬性值

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {    
    if ([key isEqualToString:@"name"]) {        
        return YES;    
    }    
    return NO;
}複製代碼

二、手動觀察屬性值

- (void)setName:(NSString *)name {    
    [self willChangeValueForKey:@"name"];
    _name = name;    
    [self didChangeValueForKey:@"name"];
}複製代碼

KVO原理分析

在查看了官方文檔以後,發現KVO的原理實際上是改變了isa指針,將isa指針指向了另外一個動態生成的類。爲了分析其中的原理,咱們在下圖地方作一個斷點,看看究竟是生成了什麼樣的類。


而後咱們用LLDB打印一下相應的類。


咱們發現出來了一個新的類NSKVONotifing_Person。那麼這個新的類跟Person類是什麼關係呢?因而決定打印添加了observer先後類到底有什麼變化。

調用printClasses方法能夠打印全部的子類的信息:

#pragma mark - ======== 遍歷類以及子類 ========
- (void)printClasses:(Class)cls {    
    //註冊類的總和    
    int count = objc_getClassList(NULL, 0);    
    //建立一個數組,其中包含給定的對象    
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];    
    //獲取全部已註冊的類    
    Class *classes = (Class *)malloc(sizeof(Class) * count);    
    objc_getClassList(classes, count);    
    for (int i = 0; i < count; i++) {        
        if (cls == class_getSuperclass(classes[i])) {            
        [mArray addObject:classes[i]];        
        }    
    }    
    free(classes);   
    NSLog(@"classes = %@",mArray);
}複製代碼


而後咱們再調用前和調用後分別調用方法:

[self printClasses:[Person class]];    
    NSLog(@"*********添加前*********");    
    //[self printClasses:NSClassFromString(@"NSKOVNotifing_Person")];        
    self.person = [[Person alloc]init];    
    self.person.name = @"zy";    
    //註冊觀察者    
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];    
    NSLog(@"*********進去了*********");    
    [self printClasses:[Person class]];    
    NSLog(@"*********添加後:NSKVONotifying_Person*********");    
    [self printClasses:NSClassFromString(@"NSKVONotifying_Person")];複製代碼

打印出來的值爲

2019-04-14 20:27:48.150739+0800 KVODemo[1959:134800] classes = (

Person

)

2019-04-14 20:27:48.150926+0800 KVODemo[1959:134800] *********添加前*********

2019-04-14 20:27:48.151333+0800 KVODemo[1959:134800] *********進去了*********

2019-04-14 20:27:48.157911+0800 KVODemo[1959:134800] classes = (

Person,

"NSKVONotifying_Person"

)

2019-04-14 20:27:48.158066+0800 KVODemo[1959:134800] *********添加後:NSKVONotifying_Person*********

2019-04-14 20:27:48.162250+0800 KVODemo[1959:134800] classes = (

"NSKVONotifying_Person"

)

那麼咱們能夠明確的看到,NSKVONotifying_Person是繼承與Person的,他是動態生成的Person的子類。

弄清楚了類的關係,咱們再看看方法有什麼變化。咱們新增一個遍歷全部方法的函數:

#pragma mark - ======== 遍歷方法-ivar-property ========
- (void)printClassAllMethod:(Class)cls {    
    unsigned int count = 0;    
    Method *methodList = class_copyMethodList(cls, &count);    
    for (int i = 0; i < count; i++) {        
        Method method = methodList[i];        
        SEL sel = method_getName(method);        
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}複製代碼

而後在添加先後分別調用printClassAllMethod方法,打印的結果爲:

2019-04-14 20:44:43.451245+0800 KVODemo[2131:144186] hello-0x10a759290

2019-04-14 20:44:43.451427+0800 KVODemo[2131:144186] world-0x10a7592c0

2019-04-14 20:44:43.451529+0800 KVODemo[2131:144186] nick-0x10a759320

2019-04-14 20:44:43.451633+0800 KVODemo[2131:144186] setNick:-0x10a759350

2019-04-14 20:44:43.451738+0800 KVODemo[2131:144186] .cxx_destruct-0x10a759390

2019-04-14 20:44:43.451879+0800 KVODemo[2131:144186] name-0x10a7592f0

2019-04-14 20:44:43.451963+0800 KVODemo[2131:144186] setName:-0x10a7591f0

2019-04-14 20:44:43.452100+0800 KVODemo[2131:144186] *********添加前*********

2019-04-14 20:44:43.452582+0800 KVODemo[2131:144186] *********進去了*********

2019-04-14 20:44:43.452672+0800 KVODemo[2131:144186] *********添加後:NSKVONotifying_Person*********

2019-04-14 20:44:43.452786+0800 KVODemo[2131:144186] setName:-0x10aab263a

2019-04-14 20:44:43.452884+0800 KVODemo[2131:144186] class-0x10aab106e

2019-04-14 20:44:43.452968+0800 KVODemo[2131:144186] dealloc-0x10aab0e12

2019-04-14 20:44:43.453067+0800 KVODemo[2131:144186] _isKVOA-0x10aab0e0a


那麼,經過對方法的地址分析,咱們能夠獲得一個結論,NSKVONotifying_Person類重寫了setName的方法,而後新增了class方法、dealloc方法和_isKVOA方法。

結論

綜合上面的測試,咱們能夠總結出來KVO的原理:

  1. 驗證是否存在setter方法,目的是爲了避免讓實例進來
  2. 動態生成子類NSKVONotifying_Person:先開闢一個新的類,而後註冊類,重寫class的方法,講class指向Person,接着重寫setter方法,經過對setter賦值,實現父類的方法self.name = @"xlh",最後經過objc_getAssociatedObject關聯住咱們的觀察者
  3. 講isa的指針指向NSKVONotifying_Person
  4. 最後經過消息轉發響應響應的回調
相關文章
相關標籤/搜索