iOS底層原理之KVO本質

面試中常會問道:

  1. iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)
  2. 如何手動觸發KVO

什麼是KVO

首先需要了解KVO基本使用,KVO的全稱 Key-Value Observing,俗稱“鍵值觀察”,我們可以用於監聽某個對象屬性值的改變。
如下代碼:

//
//  ViewController.m
//  KVO原理探索
//  Created by just so so on 2019/8/24.
//  Copyright © 2019 bruce yao. All rights reserved.
//
#import "YNextVC.h"
#import "classes/Person.h"
#import <objc/runtime.h>
@interface YNextVC ()
@end
@implementation YNextVC
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [self studyKVO];
}
- (void)studyKVO {
    Person *p2 = [[Person alloc] init];
    p2.age = 12;
    Person *p1 = [[Person alloc] init];
    p1.age = 12;
    p1.name = @"xiaoXiao";
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    p1.age = 22;
    [p1 removeObserver:self forKeyPath:@"age"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"對象%@改變%@ 那個屬:%@", object, change);
}
@end
//在添加監聽之後,age屬性的值在發生改變時,就會通知到監聽者,執行監聽者的observeValueForKeyPath方法。

探尋KVO底層實現原理

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

我們發現即使重寫了set方法,p1對象和p2對象調用同樣的set方法,但是我們發現p1除了調用set方法之外還會另外執行監聽器的observeValueForKeyPath方法。

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

KVO底層實現分析

首先我們對上述代碼中添加監聽的地方打斷點,看觀察一下,addObserver方法對p1對象做了什麼處理?也就是說p1對象在經過addObserver方法之後發生了什麼改變,我們通過打印isa指針如下圖所示
在這裏插入圖片描述

說明:當我們沒有對p1添加觀察者的時候:

(lldb) p/x p1->isa //打印p1的ISA指針,也就是類對象
(Class) $0 = 0x000001a100f810ed Person
(lldb) p/x p2->isa //打印p2的ISA指針,也就是類對象
(Class) $1 = 0x000001a100f810ed Person

當我們添加監聽後,在方法中打印的如下:

//斷點位置:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"對象%@改變%@ 那個屬:%@", object, change, keyPath);
}
(lldb) p/x object->isa
(Class) $2 = 0x000021a280c810e5 NSKVONotifying_Person
(lldb) p/x p1->isa
(Class) $3 = 0x000001a280c810e5 NSKVONotifying_Person

p1對象的isa指針由之前的指向類對象Person變爲指向NSKVONotifyin_Person類對象,而p2對象沒有任何改變。也就是說一旦p1對象添加了KVO監聽以後,其isa指針就會發生變化,因此set方法的執行效果就不一樣了。

那麼我們先來觀察p2對象在內容中是如何存儲的,然後對比p2來觀察p1。
首先我們知道,p2在調用setAge方法的時候,首先會通過p2對象中的isa指針找到Person類對象,然後在類對象中找到setage方法。然後找到方法對應的實現。如下圖所示
在這裏插入圖片描述

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

經過查閱資料我們可以瞭解到。
NSKVONotifyin_Person中的setAge方法中其實調用了 Fundation框架中C語言函數 _NSSetLongLongValueAndNotify_NSSetLongLongValueAndNotify內部做的操作相當於,首先調用willChangeValueForKey 將要改變方法,之後調用父類的setAge方法對成員變量賦值,最後調用didChangeValueForKey已經改變方法。didChangeValueForKey中會調用監聽器的監聽方法,最終來到監聽者的observeValueForKeyPath方法中。

我們驗證一下,NSKVONotifyin_Person的父類是不是Person,斷點直接調試如下


(lldb) p/x p1->isa
(Class) $7 = 0x000001a280c810e5 NSKVONotifying_Person
(lldb) p/x [NSKVONotifying_Person superclass]
(Class) $8 = 0x0000000100f810e8 Person

驗證KVO真的如上面所講的方式實現

在這裏插入圖片描述

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

擴展:我們可以Foundation框架中會根據屬性的類型,調用不同的方法。例如我們之前定義的NSInteger類型的age屬性,那麼我們看到Foundation框架中調用的_NSsetIntValueAndNotify函數。那麼我們把age的屬性類型變爲double重新打印一遍。

我們發現調用的函數變爲了_NSSetDoubleValueAndNotify,那麼這說明Foundation框架中有許多此類型的函數,通過屬性的不同類型調用不同的函數。
那麼我們可以推測Foundation框架中還有很多例如_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify等等函數。

NSKVONotifyin_Person內部結構

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


- (void)studyKVO {
    Person *p2 = [[Person alloc] init];
    p2.age = 12;
    Person *p1 = [[Person alloc] init];
    p1.age = 12;
    p1.name = @"xiaoXiao";
    [p1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    p1.age = 22;
    
    
    [self printMethods:object_getClass(p1)];
    [self printMethods:object_getClass(p2)];
    [self printMethods:[p1 class]]; //不要用這個因爲,蘋果給我們重新啦NSKVONotifying_Person -(Class)class方法
    [p1 removeObserver:self forKeyPath:@"age"];//一定要在removeObserver之前調用
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"對象%@改變%@ 那個屬:%@", object, change, keyPath);
}

- (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);
    
}
//結果如下:
2019-08-24 15:39:13.452790+0800 YStudyDown[3713:638240] NSKVONotifying_Person - setAge: class dealloc _isKVOA
2019-08-24 15:39:13.452942+0800 YStudyDown[3713:638240] Person - setAge: age .cxx_destruct name setName:

NSKVONotifyin_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子類的存在。

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

//我們可以猜測NSKVONotifyin_Person內重寫的class內部實現大致爲
- (Class) class {
     // 得到類對象,在找到類對象父類
     return class_getSuperclass(object_getClass(self));
}

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


- (void)setAge:(NSInteger)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:方法。

2019-08-24 15:47:26.062629+0800 YStudyDown[3717:639287] willChangeValueForKey: - begin
2019-08-24 15:47:26.062675+0800 YStudyDown[3717:639287] willChangeValueForKey: - end
2019-08-24 15:47:26.062691+0800 YStudyDown[3717:639287] setAge:
2019-08-24 15:47:26.062706+0800 YStudyDown[3717:639287] didChangeValueForKey: - begin
2019-08-24 15:47:26.062845+0800 YStudyDown[3717:639287] 對象<Person: 0x281c6c4c0>改變{
    kind = 1;
    new = 22;
    old = 12;
} 那個屬:age
2019-08-24 15:47:26.062872+0800 YStudyDown[3717:639287] didChangeValueForKey: - end

回答解答

  1. iOS用什麼方式實現對一個對象的KVO?(KVO的本質是什麼?)
    答. 當一個對象使用了KVO監聽,iOS系統會修改這個對象的isa指針,改爲指向一個全新的通過Runtime動態創建的子類(NSKVONotifyin_Xxxx),子類擁有自己的set方法實現,set方法實現內部會順序調用willChangeValueForKey方法、原來的setter方法實現、didChangeValueForKey方法,而didChangeValueForKey方法內部又會調用監聽器的observeValueForKeyPath:ofObject:change:context:監聽方法。

  2. 如何手動觸發KVO
    答. 被監聽的屬性的值被修改時,就會自動觸發KVO。如果想要手動觸發KVO,則需要我們自己調用willChangeValueForKey和didChangeValueForKey方法即可在不改變屬性值的情況下手動觸發KVO,並且這兩個方法缺一不可。

參考文章:https://www.jianshu.com/p/5477cf91bb32

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章