iOS底層原理 - 探尋KVO本質(zhì)

面試題引發(fā)的思考:

Q: iOS用什么方式實(shí)現(xiàn)對一個對象的KVO苏携?即KVO的本質(zhì)是什么羞反?

  • 利用RuntimeAPI動態(tài)生成一個子類,并且讓instance對象的isa指向這個全新的子類。

  • 當(dāng)修改instance對象的屬性時,會調(diào)用Foundation_NSSetXXXValueAndNotify函數(shù)
    a>willChangeValueForKey:
    b>父類原來的setter方法對成員變量進(jìn)行賦值
    c>didChangeValueForKey:
    d>內(nèi)部會觸發(fā)監(jiān)聽器(observer)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:

Q: 如何手動觸發(fā)KVO丹诀?

  • 手動調(diào)用willChangeValueForKey:didChangeValueForKey:

Q: 直接修改成員變量會觸發(fā)KVO嗎翁垂?

  • 不會觸發(fā)KVO铆遭。

1. KVO介紹

KVO的全稱是Key-Value Observing,即“鍵值監(jiān)聽”沿猜,用于監(jiān)聽某個對象屬性值的改變

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person1 = [[Person alloc] init];
    person1.age = 10;
    Person *person2 = [[Person alloc] init];
    person2.age = 20;
    
    // 給person對象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
    person1.age = 20;   // [person1 setAge:20];
    person2.age = 30;   // [person2 setAge:30];

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

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

// 打印結(jié)果
Demo[1234:567890] 監(jiān)聽到<Person: 0x600001608200>的age屬性發(fā)生了改變 - {
    kind = 1;
    new = 20;
    old = 10;
}

由打印結(jié)果可知:

輸出的是person1的相關(guān)內(nèi)容枚荣;給person1對象添加KVO監(jiān)聽后,age屬性的值發(fā)生改變時啼肩,監(jiān)聽者observeValueForKeyPath的方法會被調(diào)用執(zhí)行橄妆。


2. KVO的本質(zhì)

(1)未使用KVO監(jiān)聽的對象實(shí)現(xiàn)流程

person1person2的屬性age賦值,都會調(diào)用相同的set方法祈坠,而set方法的實(shí)現(xiàn)也是一樣的害碾。

Q: 那為什么只會打印出person1的相關(guān)內(nèi)容?

可以猜測是跟類的對象方法沒有關(guān)系赦拘,跟類對象本身有關(guān)慌随。

使用KVO監(jiān)聽對象與未使用KVO監(jiān)聽對象的區(qū)別

我們知道instance對象的isa指向class對象,所以:

  • person1的類對象是NSKVONotifying_Person躺同,person2的類對象是Person阁猜;
  • NSKVONotifying_Person則是使用Runtime動態(tài)創(chuàng)建的一個類,是Person的一個子類蹋艺。
未使用KVO監(jiān)聽的對象實(shí)現(xiàn)流程

由上圖可知:
person2在調(diào)用setAge:方法的時候剃袍,首先根據(jù)person2isa找到Person的class對象,然后在class對象中找到setAge:捎谨,然后實(shí)現(xiàn)方法民效。這是未使用KVO監(jiān)聽的對象實(shí)現(xiàn)流程。

(2) 使用KVO監(jiān)聽的對象實(shí)現(xiàn)流程

根據(jù)相關(guān)資料可知:

NSKVONotifying_Person中的setAge:方法涛救,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify畏邢,其內(nèi)部實(shí)現(xiàn)流程為:

  • 首先調(diào)用willChangeValueForKey:方法
  • 然后調(diào)用父類的setAge:方法對成員變量進(jìn)行賦值
  • 最后調(diào)用didChangeValueForKey:方法,此方法內(nèi)部會觸發(fā)監(jiān)聽器(Oberser)的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:
使用了KVO監(jiān)聽的對象實(shí)現(xiàn)流程

由上圖可知:

person1在調(diào)用setAge:方法的時候州叠,首先根據(jù)person1isa找到NSKVONotifying_Person的class對象棵红,然后在class對象中找到setAge:,然后實(shí)現(xiàn)方法咧栗。這是使用了KVO監(jiān)聽的對象實(shí)現(xiàn)流程逆甜。

1> 驗(yàn)證1:NSKVONotifying_Person的內(nèi)部結(jié)構(gòu)
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Person *person1 = [[Person alloc] init];
    Person *person2 = [[Person alloc] init];
    
    // 給person對象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];
    
    [self printMethodNamesOfClass:object_getClass(person2)];
    [self printMethodNamesOfClass:object_getClass(person1)];
}

- (void)printMethodNamesOfClass:(Class)cls
{
    unsigned int count;
    // 獲得方法數(shù)組
    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);
}

// 打印結(jié)果
Demo[1234:567890] Person - age, setAge:,
Demo[1234:567890] NSKVONotifying_Person - setAge:, class, dealloc, _isKVOA,

由打印結(jié)果可知:

NSKVONotifying_Person中有4個對象方法,分別為 setAge:致板、class交煞、dealloc_isKVOA斟或;證實(shí)了其內(nèi)部結(jié)構(gòu)素征。

2> 驗(yàn)證2:NSKVONotifying_Person中的setAge:方法,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person1 = [[Person alloc] init];
    Person *person2 = [[Person alloc] init];

    NSLog(@"person1添加KVO監(jiān)聽之前 - %p %p",
          [person1 methodForSelector:@selector(setAge:)],
          [person2 methodForSelector:@selector(setAge:)]);

    // 給person對象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];

    NSLog(@"person1添加KVO監(jiān)聽之后 - %p %p",
          [person1 methodForSelector:@selector(setAge:)],
          [person2 methodForSelector:@selector(setAge:)]);

    [person1 removeObserver:self forKeyPath:@"age"];
}
打印結(jié)果

由打印結(jié)果可知:

person1添加KVO監(jiān)聽之前,person1person2setAge:方法的地址相同御毅;person1添加KVO監(jiān)聽之后根欧,person1setAge:方法的地址發(fā)生改變。

打印結(jié)果證實(shí)了:

NSKVONotifying_Person中的setAge:方法端蛆,調(diào)用了Fundation框架中C語言函數(shù)_NSSetIntValueAndNotify凤粗。

內(nèi)部實(shí)現(xiàn)偽代碼如下:

- (void)setAge:(int)age {
    _NSSetIntValueAndNotify();
}

// 偽代碼
void _NSSetIntValueAndNotify() {
    [self willChangeValueForKey:@"age"];
    [super setAge:age];
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key {
    // 通知監(jiān)聽器,某某屬性值發(fā)生了改變
    [oberser observeValueForKeyPath:key ofObject:self change:nil context:nil];
}
3> 驗(yàn)證3:NSKVONotifying_Person會重寫class方法
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person1 = [[Person alloc] init];
    Person *person2 = [[Person alloc] init];

    // 給person對象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person1 addObserver:self forKeyPath:@"age" options:options context:nil];

    NSLog(@"%@ %@", [person1 class], [person2 class]);
    NSLog(@"%@ %@", object_getClass(person1), object_getClass(person2));

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

// 打印結(jié)果
Demo[1234:567890] Person Person
Demo[1234:567890] NSKVONotifying_Person Person

由打印結(jié)果可知:

通過class方法獲取到的類為Person今豆,而通過runtime的object_getClass方法獲取到的類為NSKVONotifying_Person嫌拣,說明NSKVONotifying_Person重寫了class方法

Q: 那為什么要重寫class方法呢?

很明顯呆躲,蘋果不想讓NSKVONotifying_Person這個類暴露出來异逐,不希望開發(fā)者知道其內(nèi)部實(shí)現(xiàn),其class方法內(nèi)部實(shí)現(xiàn)應(yīng)該是以下:

// 屏蔽內(nèi)部實(shí)現(xiàn)插掂,隱藏了NSKVONotifying_Person類的存在
- (Class)class {
    // 1.獲取類對象  2.獲取類對象父類
    return class_getSuperclass(object_getClass(self));
}
4> 驗(yàn)證4:didChangeValueForKey:內(nèi)部會觸發(fā)observer的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:

Person類中灰瞻,重寫willChangeValueForKey:didChangeValueForKey:方法:

// TODO: -----------------  Person類  -----------------
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end

@implementation Person
- (void)setAge:(int)age {
    _age = age;
    NSLog(@"setAge:");
}

- (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");
}
@end

// TODO: -----------------  ViewController類  -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    Person *person = [[Person alloc] init];
    person.age = 10;

    // 給person對象添加KVO監(jiān)聽
    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
    [person addObserver:self forKeyPath:@"age" options:options context:nil];

    person.age = 20;

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

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"監(jiān)聽到%@的%@屬性發(fā)生了改變 - %@", object, keyPath, change);
}
打印結(jié)果 - setAge:實(shí)現(xiàn)順序

由打印結(jié)果可知:

didChangeValueForKey:內(nèi)部會調(diào)用observer的監(jiān)聽方法observeValueForKeyPath:ofObject:change:context:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末燥筷,一起剝皮案震驚了整個濱河市箩祥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肆氓,老刑警劉巖袍祖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異谢揪,居然都是意外死亡蕉陋,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門拨扶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凳鬓,“玉大人,你說我怎么就攤上這事患民∷蹙伲” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵匹颤,是天一觀的道長仅孩。 經(jīng)常有香客問我,道長印蓖,這世上最難降的妖魔是什么辽慕? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮赦肃,結(jié)果婚禮上溅蛉,老公的妹妹穿的比我還像新娘公浪。我一直安慰自己,他們只是感情好船侧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布欠气。 她就那樣靜靜地躺著,像睡著了一般勺爱。 火紅的嫁衣襯著肌膚如雪晃琳。 梳的紋絲不亂的頭發(fā)上讯检,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天琐鲁,我揣著相機(jī)與錄音,去河邊找鬼围段。 笑死,一個胖子當(dāng)著我的面吹牛投放,可吹牛的內(nèi)容都是我干的奈泪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼灸芳,長吁一口氣:“原來是場噩夢啊……” “哼涝桅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起烙样,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤冯遂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谒获,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蛤肌,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年批狱,在試婚紗的時候發(fā)現(xiàn)自己被綠了裸准。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡赔硫,死狀恐怖炒俱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情爪膊,我是刑警寧澤权悟,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站惊完,受9級特大地震影響僵芹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜小槐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一拇派、第九天 我趴在偏房一處隱蔽的房頂上張望荷辕。 院中可真熱鬧,春花似錦件豌、人聲如沸疮方。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骡显。三九已至,卻和暖如春曾掂,著一層夾襖步出監(jiān)牢的瞬間惫谤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工珠洗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留溜歪,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓许蓖,卻偏偏與公主長得像蝴猪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子膊爪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容