KVO實現原理

KVO(key value observing)鍵值監聽是咱們在開發中常使用的用於監聽特定對象屬性值變化的方法,經常使用於監聽數據模型的變化ios

KVO是爲了監聽一個對象的某個屬性值是否發生變化。在屬性值發生變化的時候,確定會調用其setter方法。因此KVO的本質就是監聽對象有沒有調用被監聽屬性對應的setter方法面試

在學習實現原理以前咱們首先先了解一下KVO經常使用的有哪些方法macos

KVO經常使用方法

/*
註冊監聽器
監聽器對象爲observer,被監聽對象爲消息的發送者即方法的調用者在回調函數中會被回傳
監聽的屬性路徑爲keyPath支持點語法的嵌套
監聽類型爲options支持按位或來監聽多個事件類型
監聽上下文context主要用於在多個監聽器對象監聽相同keyPath時進行區分
添加監聽器只會保留監聽器對象的地址,不會增長引用,也不會在對象釋放後置空,所以須要本身持有監聽對象的強引用,該參數也會在回調函數中回傳
*/
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

/*
刪除監聽器
監聽器對象爲observer,被監聽對象爲消息的發送者即方法的調用者,應與addObserver方法匹配
監聽的屬性路徑爲keyPath,應與addObserver方法的keyPath匹配
監聽上下文context,應與addObserver方法的context匹配
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));

/*
與上一個方法相同,只是少了context參數
推薦使用上一個方法,該方法因爲沒有傳遞context可能會產生異常結果
*/
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

/*
監聽器對象的監聽回調方法
keyPath即爲監聽的屬性路徑
object爲被監聽的對象
change保存被監聽的值產生的變化
context爲監聽上下文,由add方法回傳
*/
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;
複製代碼

KVO簡單實現

咱們建立一個person對象,而後在裏面添加一個age屬性,咱們就來觀察一下age屬性 person對象數組

#import <Foundation/Foundation.h>

@interface Person : NSObject
@property (nonatomic,assign) NSInteger age;
@end
複製代碼

簡單實現bash

#import "ViewController.h"
#import "Person.h"
@interface ViewController ()

@property (nonatomic,strong) Person *p1;
@property (nonatomic,strong) Person *p2;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.p1 = [[Person alloc]init];
self.p2 = [[Person alloc]init];
self.p1.age = 10;
self.p2.age = 20;

// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.p1.age = arc4random()%100;
self.p2.age = arc4random()%100;
}

- (void)dealloc {
[self.p1 removeObserver:self forKeyPath:@"age"];

}
// 當監聽對象的屬性值發生改變時,就會調用
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
NSLog(@"監聽到%@的%@屬性值改變了 - %@ - %@", object, keyPath, change, context);
}
複製代碼

以上代碼就是一個KVO的簡單實現,可是咱們有沒有想過他的內部究竟是怎樣實現的呢,今天咱們就來探究一下KVO的內部實現原理app

KVO的內部實現

探究一個對象底層實現最簡單的辦法就行打印一些對象信息,看看有什麼改變dom

咱們在給person1添加監聽以前分別打印p1,p2的類信息 代碼實現函數

NSLog(@"person1添加KVO監聽以前 - %@ %@",
object_getClass(self.p1),
object_getClass(self.p2));
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];

NSLog(@"person1添加KVO監聽以後 - %@ %@",
object_getClass(self.p1),
object_getClass(self.p2));
複製代碼

咱們根據結果看到,在添加KVO觀察者以後p1的類對象由Person變成了NSKVONotifying_Person,雖然p1的類對象變成了NSKVONotifying_Person,可是咱們在調用的時候感受咱們的p1的類對象仍是Person,因此,咱們能夠猜想KVO會在運行時動態建立一個新類,將對象的isa指向新建立的類,新類是原類的子類,命名規則是NSKVONotifying_xxx的格式。KVO爲了使其更像以前的類,還會將對象的class實例方法重寫,使其更像原類學習

查看P1內部方法是否改變ui

咱們在發現p1的類對象由Person變成了NSKVONotifying_Person,那咱們也隨便打印一下Person和NSKVONotifying_Person內部方法都變成了什麼

打印一下方法名

- (void)printMethodNamesOfClass:(Class)cls
{
unsigned int count;
// 得到方法數組
Method *methodList = class_copyMethodList(cls, &count);

// 存儲方法名
NSMutableString *methodNames = [NSMutableString string];

// 遍歷全部的方法
for (int i = 0; i < count; i++) {
// 得到方法
Method method = methodList[i];
// 得到方法名
NSString *methodName = NSStringFromSelector(method_getName(method));
// 拼接方法名
[methodNames appendString:methodName];
[methodNames appendString:@", "];
}

// 釋放
free(methodList);

// 打印方法名
NSLog(@"%@ %@", cls, methodNames);
}
複製代碼

而後咱們分別在KVO監聽先後在分別打印一下p1的類對象

NSLog(@"person1添加KVO監聽以前的內部方法===");
[self printMethodNamesOfClass:object_getClass(self.p1)];
// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO監聽以後的內部方法===");
[self printMethodNamesOfClass:object_getClass(self.p1)];
複製代碼

打印結果

咱們在來打印一些KVO監聽先後setAge方法發生了什麼改變,由於值得改變確定是由於set方法致使的,因此咱們打印一下setAge方法。methodForSelector能夠打印方法地址,咱們分別在KVO監聽先後打印

NSLog(@"person1添加KVO監聽以前 - %p %p",
[self.p1 methodForSelector:@selector(setAge:)],
[self.p2 methodForSelector:@selector(setAge:)]);

// 給person1對象添加KVO監聽
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[self.p1 addObserver:self forKeyPath:@"age" options:options context:@"123"];
NSLog(@"person1添加KVO監聽以後 - %p %p",
[self.p1 methodForSelector:@selector(setAge:)],
[self.p2 methodForSelector:@selector(setAge:)]);
複製代碼

打印結果

2018-09-04 10:41:05.823343+0800 KVO[21971:1023542] person1添加KVO監聽以前 - 0x103f18540 0x103f18540
2018-09-04 10:41:05.823702+0800 KVO[21971:1023542] person1添加KVO監聽以後 - 0x10425ebf4 0x103f18540
複製代碼

咱們能夠利用lldb分別看一下具體的方法實現:

根據以上總結,咱們大概猜到在使用KVO先後對象的改變了 未使用KVO監聽的對象

使用KVO監聽的對象

  • 一、重寫class方法是爲了咱們調用它的時候返回跟重寫繼承類以前一樣的內容。KVO底層交換了 NSKVONotifying_Person 的 class 方法,讓其返回 Person
  • 二、重寫setter方法:在新的類中會重寫對應的set方法,是爲了在set方法中增長另外兩個方法的調用
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey:(NSString *)key
複製代碼

在didChangeValueForKey:方法再調用

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
複製代碼
  • 三、重寫dealloc方法,銷燬新生成的NSKVONotifying_類。
  • 四、重寫_isKVOA方法,這個私有方法估計多是用來標示該類是一個 KVO 機制聲稱的類。

_NSSetLongLongValueAndNotify

在添加KVO監聽方法之後setAge方法變成了_NSSetLongLongValueAndNotify,因此咱們能夠大概猜想動態監聽方法主要就是在這裏面實現的

咱們能夠在終端使用nm -a /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation | grep ValueAndNotify命令來查看NSSet*ValueAndNotify的類型

咱們能夠在Person類中重寫willChangeValueForKey和didChangeValueForKey,來猜想一下_NSSetLongLongValueAndNotify的內部實現

- (void)setAge:(NSInteger)age{
_age = age;
NSLog(@"調用set方法");
}


- (void)willChangeValueForKey:(NSString *)key{
[super willChangeValueForKey:key];
NSLog(@"willChangeValueForKey");
}

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

NSLog(@"didChangeValueForKey - begin");

[super didChangeValueForKey:key];

NSLog(@"didChangeValueForKey - end");
}
複製代碼

根據打印結果咱們能夠推斷_NSSetLongLongValueAndNotify內部實現爲

  • 一、調用willChangeValueForKey方法
  • 二、調用setAge方法
  • 三、調用'didChangeValueForKey'方法
  • 四、'didChangeValueForKey'方法內部調用oberser的observeValueForKeyPath:ofObject:change:context:方法
// 僞代碼
void _NSSetIntValueAndNotify()
{
[self willChangeValueForKey:@"age"];
[super setAge:age];
[self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key
{
// 通知監聽器,某某屬性值發生了改變
[oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
複製代碼

面試題

講了這些,咱們來討論面試題吧

一、iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)

  • 一、利用RuntimeAPI動態生成一個子類NSKVONotifying_XXX,而且讓instance對象的isa指向這個全新的子類NSKVONotifying_XXX
  • 二、當修改對象的屬性時,會在子類NSKVONotifying_XXX調用Foundation的_NSSetXXXValueAndNotify函數
  • 三、在_NSSetXXXValueAndNotify函數中依次調用 - 一、willChangeValueForKey - 二、父類原來的setter - 三、didChangeValueForKey,didChangeValueForKey:內部會觸發監聽器(Oberser)的監聽方法( observeValueForKeyPath:ofObject:change:context:)

二、如何手動觸發KVO方法

手動調用willChangeValueForKey和didChangeValueForKey方法

鍵值觀察通知依賴於 NSObject 的兩個方法: willChangeValueForKey: 和 didChangeValueForKey。在一個被觀察屬性發生改變以前, willChangeValueForKey: 必定會被調用,這就 會記錄舊的值。而當改變發生後, didChangeValueForKey 會被調用,繼而 observeValueForKey:ofObject:change:context: 也會被調用。若是能夠手動實現這些調用,就能夠實現「手動觸發」了

有人可能會問只調用didChangeValueForKey方法能夠觸發KVO方法,實際上是不能的,由於willChangeValueForKey: 記錄舊的值,若是不記錄舊的值,那就沒有改變一說了

三、直接修改爲員變量會觸發KVO嗎

不會觸發KVO,由於KVO的本質就是監聽對象有沒有調用被監聽屬性對應的setter方法,直接修改爲員變量,是在內存中修改的,不走set方法

四、不移除KVO監聽,會發生什麼

  • 不移除會形成內存泄漏
  • 可是屢次重複移除會崩潰。系統爲了實現KVO,爲NSObject添加了一個名爲NSKeyValueObserverRegistration的Category,KVO的add和remove的實現都在裏面。在移除的時候,系統會判斷當前KVO的key是否已經被移除,若是已經被移除,則主動拋出一個NSException的異常

原文地址

相關文章
相關標籤/搜索