iOS 底層OC語法3(探索KVO的本質)

代碼地址git

KVO 的用法

首先須要瞭解KVO基本使用,KVO的全稱 Key-Value Observing,俗稱「鍵值監聽」,能夠用於監聽某個對象屬性值的改變。github

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p1 = [[Person alloc] init];
    Person *p2 = [[Person alloc] init];
    p1.age = 1;
    p1.age = 2;
    p2.age = 2;
    // self 監聽 p1的 age屬性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;

    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    p1.age = 10;
    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"監聽到%@的%@改變了%@", object, keyPath,change);
}

// 打印內容
監聽到<Person: 0x604000205460>的age改變了{
    kind = 1;
    new = 10;
    old = 2;
}

複製代碼

KVO 疑問 (都調用set方法,可是觸發的方法不同)

經過上述代碼咱們發現,一旦age屬性的值發生改變時,就會通知到監聽者,而且咱們知道賦值操做都是調用 set方法,咱們能夠來到Person類中重寫age的set方法,觀察是不是KVO在set方法內部作了一些操做來通知監聽者。bash

咱們發現即便重寫了set方法,p1對象和p2對象調用一樣的set方法,可是咱們發現p1除了調用set方法以外還會另外執行監聽器的observeValueForKeyPath方法。app

說明KVO在運行時獲取對p1對象作了一些改變。至關於在程序運行過程當中,對p1對象作了一些變化,使得p1對象在調用setage方法的時候可能作了一些額外的操做,因此問題出在對象身上,兩個對象在內存中確定不同,兩個對象可能本質上並不同 。接下來來探索KVO內部是怎麼實現的。框架

KVO底層實現分析 (添加KVO 後新生成的類,是元類的子類,重寫了原類的set方法)

首先咱們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象作了什麼處理?也就是說p1對象在通過addObserver方法以後發生了什麼改變,咱們經過打印isa指針以下圖所示。函數

經過上圖咱們發現,p1對象執行過addObserver操做以後,p1對象的isa指針由以前的指向類對象Person變爲指向NSKVONotifyin_Person類對象,而p2對象沒有任何改變。 也就是說一旦p1對象添加了KVO監聽之後,其isa指針就會發生變化,所以set方法的執行效果就不同了。ui

那麼咱們先來觀察p2對象在內容中是如何存儲的,而後對比p2來觀察p1。 首先咱們知道,p2在調用setage方法的時候,首先會經過p2對象中的isa指針找到Person類對象,而後在類對象中找到setage方法。而後找到方法對應的實現。以下圖所示編碼

可是剛纔咱們發現p1對象的isa指針在通過KVO監聽以後已經指向了NSKVONotifyin_Person類對象,NSKVONotifyin_Person實際上是Person的子類,那麼也就是說其superclass指針是指向Person類對象的,NSKVONotifyin_Person是runtime在運行時生成的。那麼p1對象在調用setage方法的時候,確定會根據p1的isa找到NSKVONotifyin_Person,在NSKVONotifyin_Person中找setage的方法及實現。spa

那麼如何驗證KVO真的如上面所講的方式實現 (添加KVO 先後,打印set方法的實現地址)

首先通過以前打斷點打印isa指針,咱們已經驗證了,在執行添加監聽的方法時,會將isa指針指向一個經過runtime建立的Person的子類NSKVONotifyin_Person。 另外咱們能夠經過打印方法實現的地址來看一下p1和p2的setage的方法實現的地址在添加KVO先後有什麼變化。命令行

// 經過methodForSelector找到方法實現的地址
NSLog(@"添加KVO監聽以前 - p1 = %p, p2 = %p", [p1 methodForSelector: @selector(setAge:)],[p2 methodForSelector: @selector(setAge:)]);
    
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];

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

複製代碼

咱們發如今添加KVO監聽以前,p1和p2的setAge方法實現的地址相同,而通過KVO監聽以後,p1的setAge方法實現的地址發生了變化,咱們經過打印方法實現來看一下先後的變化發現,確實如咱們上面所講的同樣,p1的setAge方法的實現由Person類方法中的setAge方法轉換爲了C語言的Foundation框架的_NSsetIntValueAndNotify函數。

Foundation框架中會根據屬性的類型,調用不一樣的方法。例如咱們以前定義的int類型的age屬性,那麼咱們看到Foundation框架中調用的_NSsetIntValueAndNotify函數。那麼咱們把age的屬性類型變爲double從新打印一遍

咱們發現調用的函數變爲了_NSSetDoubleValueAndNotify,那麼這說明Foundation框架中有許多此類型的函數,經過屬性的不一樣類型調用不一樣的函數。 那麼咱們能夠推測Foundation框架中還有不少例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數。 咱們能夠找到Foundation框架文件,經過命令行查詢關鍵字找到相關函數

NSKVONotifyin_Person內部結構是怎樣的? (動態類,內部實現的方法)

首先咱們知道,NSKVONotifyin_Person做爲Person的子類,其superclass指針指向Person類,而且NSKVONotifyin_Person內部必定對setAge方法作了單獨的實現,那麼NSKVONotifyin_Person同Person類的差異可能就在於其內存儲的對象方法及實現不一樣。 咱們經過runtime分別打印Person類對象和NSKVONotifyin_Person類對象內存儲的對象方法

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *p1 = [[Person alloc] init];
    p1.age = 1.0;
    Person *p2 = [[Person alloc] init];
    p1.age = 2.0;
    // self 監聽 p1的 age屬性
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [p1 addObserver:self forKeyPath:@"age" options:options context:nil];

    [self printMethods: object_getClass(p2)];
    [self printMethods: object_getClass(p1)];

    [p1 removeObserver:self forKeyPath:@"age"];
}

- (void) printMethods:(Class)cls
{
    unsigned int count ;
    Method *methods = class_copyMethodList(cls, &count);
    NSMutableString *methodNames = [NSMutableString string];
    [methodNames appendFormat:@"%@ - ", cls];
    
    for (int i = 0 ; i < count; i++) {
        Method method = methods[i];
        NSString *methodName  = NSStringFromSelector(method_getName(method));
        
        [methodNames appendString: methodName];
        [methodNames appendString:@" "];
        
    }
    
    NSLog(@"%@",methodNames);
    free(methods);
}

複製代碼

上述打印內容以下

經過上述代碼咱們發現NSKVONotifyin_Person中有4個對象方法。 分別爲setAge: class dealloc _isKVOA ,那麼至此咱們能夠畫出NSKVONotifyin_Person的內存結構以及方法調用順序。

這裏NSKVONotifyin_Person重寫class方法是爲了隱藏NSKVONotifyin_Person。不被外界所看到。咱們在p1添加過KVO監聽以後,分別打印p1和p2對象的class能夠發現他們都返回Person。

NSLog(@"%@,%@",[p1 class],[p2 class]);
// 打印結果 Person,Person

複製代碼

若是NSKVONotifyin_Person不重寫class方法,那麼當對象要調用class對象方法的時候就會一直向上找來到nsobject,而nsobect的class的實現大體爲返回本身isa指向的類,返回p1的isa指向的類那麼打印出來的類就是NSKVONotifyin_Person,可是apple不但願將NSKVONotifyin_Person類暴露出來,而且不但願咱們知道NSKVONotifyin_Person內部實現,因此在內部重寫了class類,直接返回Person類,因此外界在調用p1的class對象方法時,是Person類。這樣p1給外界的感受p1仍是Person類,並不知道NSKVONotifyin_Person子類的存在。 那麼咱們能夠猜想NSKVONotifyin_Person內重寫的class內部實現大體爲

- (Class) class {
     // 獲得類對象,在找到類對象父類
     return class_getSuperclass(object_getClass(self));
}

複製代碼

驗證didChangeValueForKey:內部會調用observer的observeValueForKeyPath:ofObject:change:context:方法

咱們在Person類中重寫willChangeValueForKey:和didChangeValueForKey:方法,模擬他們的實現。

- (void)setAge:(int)age
{
    NSLog(@"setAge:");
    _age = age;
}
- (void)willChangeValueForKey:(NSString *)key
{
    NSLog(@"willChangeValueForKey: - begin");
    [super willChangeValueForKey:key];
    NSLog(@"willChangeValueForKey: - end");
}
- (void)didChangeValueForKey:(NSString *)key
{
    NSLog(@"didChangeValueForKey: - begin");
    [super didChangeValueForKey:key];
    NSLog(@"didChangeValueForKey: - end");
}

複製代碼

再次運行來查看didChangeValueForKey的方法內運行過程,經過打印內容能夠看到,確實在didChangeValueForKey方法內部已經調用了observer的observeValueForKeyPath:ofObject:change:context:方法。

手動觸發kvo

Person *p1 = [[Person alloc] init];
p1.age = 1.0;
   
NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
[p1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
[p1 willChangeValueForKey:@"age"];
[p1 didChangeValueForKey:@"age"];
    
[p1 removeObserver:self forKeyPath:@"age"];

複製代碼

經過打印咱們能夠發現,didChangeValueForKey方法內部成功調用了observeValueForKeyPath:ofObject:change:context:,而且age的值並無發生改變。

KVC

什麼是KVC

KVC,俗稱「鍵值編碼」,全稱是「Key Value Coding」,它是一種能夠直接經過字符串的名稱(Key)來訪問類屬性的機制,而不是經過調用Setter或者Getter方法來進行訪問。

KVC的經常使用方法以下 (keyPath 賦值的層次比較深)

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
- (nullable id)valueForKeyPath:(NSString *)keyPath;


複製代碼

KVC底層原理

setValue:forkey:賦值流程

  • 首先會按照setKey:、_setKey:的順序到對象的方法列表中尋找這兩個方法,若是找到了方法,則傳參而且調用方法。
  • 若是沒有找到方法,則經過accessInstanceVariablesDirectly方法的返回值來決定是否可以查找成員變量。若是accessInstanceVariablesDirectly返回YES,則會按照如下順序到成員變量列表中查找對應的成員變量:

_key _isKey key isKey

  • 若是accessInstanceVariablesDirectly返回NO,則直接拋出NSUnknownKeyException異常。
  • 若是在成員變量列表中找到對應的屬性值,則直接進行賦值,若是找不到,則會拋出NSUnknownKeyException異常。

對應流程圖以下:

valueForKey:取值流程

  • 首先會按照如下順序查找方法列表

getKey key isKey _key

  • 若是找到就直接傳遞參數,調用方法,若是未找到則查看accessInstanceVariablesDi* rectly方法的返回值,若是返回NO,則直接拋出NSUnknownKeyException異常 若是accessInstanceVariablesDirectly方法返回YES,則按以下順序查找成員變量列表

_key _isKey key isKey

  • 若是能找到對應的成員變量,則直接獲取成員變量的值,若是未找到,則拋出NSUnknownKeyException異常。

流程圖以下:

小結

KVO的本質是什麼?

  • 當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改成指向一個全新的經過Runtime動態建立的子類,子類重寫了set方法實現。

set方法實現內部會順序調用willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法 。

如何手動觸發KVO

要咱們本身調用willChangeValueForKey和didChangeValueForKey方法便可在不改變屬性值的狀況下手動觸發KVO,而且這兩個方法缺一不可。

經過KVC修改屬性會觸發KVO麼?

會觸發KVO,由於KVC是調用set方法,KVO就是監聽set方法

相關文章
相關標籤/搜索