iOS-KVO(三) 窺探底層實現(xiàn)

iOS-KVO(一) 基本操作
iOS-KVO(二) 使用注意點
iOS-KVO(三) 窺探底層實現(xiàn)
iOS-KVO(四) 自定義KVO+Block

我們將在這篇文章中了解到KVO底層的實現(xiàn)原理

底層實現(xiàn)分析

  1. 創(chuàng)建Person,然后添加一個name屬性;
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END
  1. 創(chuàng)建兩個Person類的實例對象,然后其中為p1對象添加觀察者,然后觀察兩個對象的isa指向;
    Person *p1 = [Person new];
    Person *p2 = [Person new];
    
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
    
    p1.name = @"p1";
    p2.name = @"p2";

isa指向:


isa指向.png

我們可以看到,在未添加觀察者之前,p1和p2都是指向Person類,但是p1添加觀察者之后,p1的isa指為NSKVONotifying_Person類,p2還是不變。

可見,p1對象添加觀察者之后,在運行時期間,p1的isa指針指向發(fā)生了改變。
并且我們通過獲取NSKVONotifying_Person的父類,得到的是Person類。

    NSLog(@"%s", object_getClassName(object_getClass(p1)));  //NSKVONotifying_Person
    NSLog(@"%s", object_getClassName(class_getSuperclass(object_getClass(p1))));  //Person
  1. Person實例對象調(diào)用setter方法的流程
  • 無添加觀察者的p2對象,調(diào)用setter方法流程


    p2對象-setter.png

    p2在調(diào)用setName:的時候,首先會通過isa指針找到Person類對象,然后在類對象的方法列表中找到setName:方法(緩存列表就不說了),然后在找到方法的實現(xiàn)。

  • 添加觀察者的p1對象
    上面已經(jīng)驗證了p1對象的isa指針指向的是NSKVONotifying_Person類對象,并且NSKVONotifying_Person是Person的子類。
    所以p1在調(diào)用setName:的時候,調(diào)用的是NSKVONotifying_Person類對象中的setName:方法。

那究竟NSKVONotifying_Person的setName:方法實現(xiàn)了什么呢?

通過methodForSelector找到方法實現(xiàn)的地址

    NSLog(@"添加觀察者前,p1=%p, p2=%p", [p1 methodForSelector:@selector(setName:)], [p2 methodForSelector:@selector(setName:)]);
    
    [p1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:NULL];
    
    NSLog(@"添加觀察者后,p1=%p, p2=%p", [p1 methodForSelector:@selector(setName:)], [p2 methodForSelector:@selector(setName:)]);
setter實現(xiàn).png

通過上面的輸出,我們了解到NSKVONotifying_Person類的setter方法轉(zhuǎn)換為Foundation框架的_NSSetObjectValueAndNotify函數(shù)。

那么這個框架內(nèi)部又是怎么實現(xiàn)的呢,我們可以下斷點,查看下函數(shù)調(diào)用棧:

首先通過設(shè)置一個觀察點,觀察屬性的變化:


設(shè)置一個觀察點.png

繼續(xù)執(zhí)行,可以看到函數(shù)調(diào)用棧如下:


函數(shù)調(diào)用棧.png

在結(jié)果發(fā)生改變的地方繼續(xù)下斷點調(diào)試:


接收方法斷點.png

由以上函數(shù)調(diào)用棧,我們大致可以猜測出,_NSSetObjectValueAndNotify函數(shù)內(nèi)部實現(xiàn)過程如下:

1. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKey:key:key:usingBlock:] 
2. -[NSObject(NSKeyValueObservingPrivate) _changeValueForKeys:count:maybeOldValuesDict:maybeNewValuesDict:usingBlock:] 
3.  -[Person setName:]
4.  NSKeyValueDidChange ()
5.  NSKeyValueNotifyObserver ()
6.  -[ViewController observeValueForKeyPath:ofObject:change:context:] 

在_NSSetObjectValueAndNotify方法中,會調(diào)用willChangeValueForKey,然后調(diào)用父類的setName:方法,再調(diào)用didChangeValueForKey方法。在didChangeValueForKey中通知屬性改變,從而使得observeValueForKeyPath得到消息。

偽代碼:

- (void)setName:(NSString *)name
{
    _NSSetObjectValueAndNotify();
}

void _NSSetObjectValueAndNotify(){
    [self willChangeValueForKey:@"name"];
    [super setName:name];
    [self didChangeValueForKey:@"name"];
}

- (void)didChangeValueForKey:(NSString *)key{
  // 通知監(jiān)聽器 key發(fā)生了改變
  [observe observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
  1. Person和NSKVONotifying_Person內(nèi)部方法
    利用runtime打印一下各自的方法
  • 獲取Person類的方法
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList([Person class], &outCount);
    for (int i = 0; i < outCount; ++i) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        NSLog(@"%@", name);
    }
    free(methods);

打印結(jié)果:
2019-07-04 21:37:24.616010+0800 KVODemo[8362:239834] .cxx_destruct
2019-07-04 21:37:24.616119+0800 KVODemo[8362:239834] name
2019-07-04 21:37:24.616222+0800 KVODemo[8362:239834] setName:
  • 獲取NSKVONotifying_Person類的方法
    unsigned int outCount = 0;
    Method *methods = class_copyMethodList(object_getClass(self.p1), &outCount);
    for (int i = 0; i < outCount; ++i) {
        Method method = methods[i];
        NSString *name = NSStringFromSelector(method_getName(method));
        NSLog(@"%@", name);
    }
    free(methods);

打印結(jié)果:
2019-07-04 21:37:24.616425+0800 KVODemo[8362:239834] setName:
2019-07-04 21:37:24.616518+0800 KVODemo[8362:239834] class
2019-07-04 21:37:24.616605+0800 KVODemo[8362:239834] dealloc
2019-07-04 21:37:24.616692+0800 KVODemo[8362:239834] _isKVOA

分析一下NSKVONotifying_Person類重寫方法的意義:

  • setName:
    實現(xiàn)上面的所說的操作;

  • class
    蘋果不希望將NSKVONotifying_Person類暴露出來,所以在內(nèi)部重寫了class類,直接返回Person類;
    偽代碼大致如下:

- (Class)class
{
    /*
     1.找到類對象;
     2.再通過類對象找到父類;
     */
    return class_getSuperclass(object_getClass(self));
}
  • dealloc
    runtime在實例對象添加了KVO之后動態(tài)創(chuàng)建了類和一些對象,所以可能會在dealloc中回收這些資源;

  • _isKVOA
    是否使用了KVO;

自己創(chuàng)建了一個跟系統(tǒng)生成的中間類一樣的類會怎么樣?

因為KVO對生成的中間類的格式是有要求的,默認(rèn)都是以NSKVONotifying_<class>來命名,那如果我們不小心自己創(chuàng)建了一個一樣名字的中間類,會怎么樣呢?

控制臺會輸出

2019-07-04 21:52:12.259489+0800 KVODemo[8597:247439] [general] KVO failed to allocate class pair for name NSKVONotifying_Person, automatic key-value observing will not work for this class

無法創(chuàng)建NSKVONotifying_Person類,導(dǎo)致KVO無法使用。

總結(jié)

  1. 創(chuàng)建一個中間類,默認(rèn)中間類的格式為NSKVONotifying_<class>,中間類是目標(biāo)對象的類的子類,目標(biāo)對象的isa指針指向中間類;
  2. 重寫setter方法;
  3. 重寫class方法,返回父類;

over!

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

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

  • 前言 KVO作為iOS一個設(shè)計模式,監(jiān)聽對象屬性變化。通過屬性變化來做出一些處理。那么KVO底層原理是什么?相信大...
    楓葉無處漂泊閱讀 918評論 0 2
  • 問題 iOS用什么方式實現(xiàn)對一個對象的KVO?(KVO的本質(zhì)是什么?) 如何手動觸發(fā)KVO ? 首先需要了解KVO...
    hjltony閱讀 630評論 0 2
  • 一、概述 KVO,即:Key-Value Observing,它提供一種機(jī)制,當(dāng)指定的對象的屬性被修改后,則其觀察...
    DeerRun閱讀 10,215評論 11 33
  • 積極主動這個詞它一直比較逆人性,就像脫離舒適區(qū)一樣,與其脫離還不如營造。要么積極主動靠前,要么努力提升自己,當(dāng)把時...
    卿卿菇?jīng)?/span>閱讀 279評論 0 0
  • 我一向沒有寫日記的習(xí)慣,因為實在是無事可寫,也沒有什么心思要說。大學(xué)的時候,同寢有一些女同學(xué)迷上了手帳。這陣風(fēng)越刮...
    周雨陽閱讀 307評論 0 0

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