Objective-C基礎之二(深刻理解KVO、KVC)

KVO

什麼是KVO

KVO的全稱是key-value Observng,也叫作「鍵值監聽」,一般用來監聽某個對象的某個屬性值的變化。下面使用一個簡單的例子來回顧一下KVO的用法。面試

  • 建立一個XLPerson類,內部建立一個name和age屬性
@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;

@end
複製代碼
  • 在ViewController中監聽XLPerson的age屬性
@implementation ViewController

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    self.person.age = 10;
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"\n keyPath:%@, \n object:%@, \n change:%@", keyPath, object, change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
}

@end
複製代碼
  • person對象的age屬性初始時設置爲10,當點擊屏幕,設置age爲20時,系統自動觸發了observeValueForKeyPath,打印出了age的舊值和新值,以下
2019-11-13 14:52:49.960452+0800 TestFont[52476:1429894] 
 keyPath:age, 
 object:<XLPerson: 0x6000039bec00>, 
 change:{
    kind = 1;
    new = 20;
    old = 10;
}
複製代碼

KVO內部實現

結合以前對NSObject底層的學習咱們知道,實例對象的isa指針指向它的類對象,那麼上文例子中的person對象的isa指針應該指向它的類對象XLPerson,爲了作對比,咱們增長一個person2對象:bash

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    self.person.age = 10;
    
    self.person2 = [[XLPerson alloc] init];
    self.person2.age = 30;
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"\n keyPath:%@, \n object:%@, \n change:%@", keyPath, object, change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    self.person.age = 20;
}

@end
複製代碼

在touchesBegan方法中添加斷點,而後咱們使用LLDB命令來對代碼進行調試框架

(lldb) p self.person->isa
(Class) $1 = NSKVONotifying_XLPerson
(lldb) p self.person2->isa
(Class) $2 = XLPerson
(lldb) 
複製代碼

這時候會發現添加了Observer後的person對象的isa指針不是指向XLPerson,而是指向一個新的類對象NSKVONotifying_XLPerson,而person2對象因爲沒有添加Observer,因此它的isa指針指向的是類對象XLPerson函數

因爲咱們並無建立過NSKVONotifying_XLPerson類,因此NSKVONotifying_XLPerson是在運行時動態生成的一個新的類,新類生成以後,又將personisa指針指向了新的類對象。學習

爲了瞭解NSKVONotifying_XLPerson的內部構造,咱們自定義一個方法來打印Class的方法列表和superClass測試

- (void)descriptionOfClass:(Class)cls{
    NSLog(@"------------ %@ -----------", NSStringFromClass(cls));
    NSLog(@"%@ superClass ----> %@", NSStringFromClass(cls),NSStringFromClass(class_getSuperclass(cls)));
    
    unsigned int count;
    Method *methondList = class_copyMethodList(cls, &count);
    for (int i = 0; i < count; i++) {
        Method method = methondList[i];
        NSString *methodName = NSStringFromSelector(method_getName(method));
        NSLog(@"%@ ----> %@", NSStringFromClass(cls), methodName);
    }
    free(methondList);
}
複製代碼

修改示例中的代碼,打印出personperson1的方法列表和superClassui

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    self.person.age = 10;
    
    self.person2 = [[XLPerson alloc] init];
    self.person2.age = 20;
    
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    
    [self descriptionOfClass:object_getClass(self.person)];
    NSLog(@"\n");
    [self descriptionOfClass:object_getClass(self.person2)];
}
複製代碼

注意:因爲對象的實例方法都存放在類對象的methonList中,因此此處咱們須要經過object_getClass方法拿到person和person1對象的類對象,而後經過遍歷類對象的方法列表打印出具體的方法名稱。 object_getClass方法若是傳遞過去一個示例對象,那麼會返回對應的類對象,若是傳遞過去一個類對象,會返回對應的元類對象。編碼

運行程序,獲得如下運行結果atom

從圖中能夠看出,person對象因爲加了KVO監聽,因此它的類對象變成了NSKVONotifying_XLPerson,而NSKVONotifying_XLPerson對象的superClassXLPerson,說明NSKVONotifying_XLPersonXLPerson的子類。spa

NSKVONotifying_XLPerson方法列表中主要有4個方法,setAge:classdealloc_isKVOA,下面咱們就來一一分析這四個方法。

  • NSKVONotifying_XLPerson重寫了父類中的setAge:方法,在setAge:方法中調用了Foundation框架中的_NSSetIntValueAndNotify方法,而_NSSetIntValueAndNotify方法就執行了監聽KVO的核心邏輯,僞代碼以下:
- (void)setAge:(int)age{
    //調用Foundationf框架中的_NSSetIntValueAndNotify方法
    [self _NSSetIntValueAndNotify];
}

- (void)_NSSetIntValueAndNotify{
    //將要修改age的值
    [self willChangeValueForKey:@"age"];
    //調用父類的setAge方法去修改age的值
    [super setAge:age];
    //完成修改age的值,而且執行observeValueForKeyPath方法
    [self didChangeValueForKey:@"age"];
}
複製代碼
  • NSKVONotifying_XLPerson會重寫父類的class方法,緣由是Apple不想讓調用者知道NSKVONotifying_XLPerson這個中間類的存在,因此重寫class,返回原類的class對象,僞代碼以下
- (Class)class{
    return [XLPerson class];
}
複製代碼
  • NSKVONotifying_XLPerson類被銷燬的時候,dealloc方法就被用來作一些收尾工做
  • _isKVOA則是用來標識當前類是不是經過runtime動態生成的類對象,若是是,就返回YES,不是,則返回NO

還原NSKVONotifying_XLPerson對象的內部構造

上文介紹了NSKVONotifying_XLPerson對象中的幾個主要的方法,如今咱們就來還原一下NSKVONotifying_XLPerson對象完整的內部結構。

首先,NSKVONotifying_XLPersonClass類型的對象,因此它內部確定擁有isa指針和superClass指針,由此能夠獲得NSKVONotifying_XLPerson的結構以下:

結合isa指針的指向能夠獲得如下結構:

由此也能夠獲得NSKVONotifying_XLPerson的僞代碼以下

@interface NSKVONotifying_XLPerson : XLPerson

@end

@implementation NSKVONotifying_XLPerson

- (void)setAge:(int)age{
    //調用Foundationf框架中的_NSSetIntValueAndNotify方法
    [self _NSSetIntValueAndNotify];
}

- (void)_NSSetIntValueAndNotify{
    //將要修改age的值
    [self willChangeValueForKey:@"age"];
    //調用父類的setAge方法去修改age的值
    [super setAge:age];
    //完成修改age的值,而且執行observeValueForKeyPath方法
    [self didChangeValueForKey:@"age"];
}

- (void)didChangeValueForKey:(NSString *)key{
    //觸發observeValueForKeyPath方法
    [self observeValueForKeyPath:@"age" ofObject:self change:nil context:nil];
}

- (void)dealloc{
    //釋放操做
}

- (Class)class{
    return [XLPerson class];
}

- (BOOL)_isKVOA{
    return YES;
}

@end
複製代碼

KVO總結

  • 首先,給一個實例對象添加KVO,內部是利用Runtime動態的生成一個此實例對象的類對象的子類,具體的格式爲_NSKVONotifying_XXX,而且讓實例對象的isa指針指向這個新生成的類。
  • 重寫屬性的set方法,當調用set方法時,會調用Foundation框架的NSSetXXXValueAndNotify函數
  • _NSSetXXXValueAndNotify中會執行一下步驟
    • 調用willChangeValueForKey:方法
    • 調用父類的set方法,從新賦值
    • 調用didChangeValueForKey:方法,didChangeValueForKey:內部會觸發監聽器的observeValueForKeyPath:ofObject:change:context:方法

KVC

什麼是KVC?

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

KVC的經常使用方法以下

- (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有兩種賦值和取值方法,下面咱們經過一個簡單的例子來了解一下。首先建立XLPerson類和XLStudent

@interface XLStudent : NSObject

@property(nonatomic, assign)int num;

@end

@interface XLPerson : NSObject

@property(nonatomic, strong)XLStudent *student;
@property(nonatomic, copy)NSString *name;
@property(nonatomic, assign)int age;

@end

複製代碼

而後經過KVC來設置XLPersonXLStudent的屬性的值,以下

- (void)viewDidLoad{
    [super viewDidLoad];

    XLPerson *person = [[XLPerson alloc] init];
    
    [person setValue:[[XLStudent alloc] init] forKey:@"student"];
    [person setValue:@10 forKey:@"age"];
    [person setValue:@"張三" forKey:@"name"];
    [person setValue:@20 forKeyPath:@"student.num"];
    
    NSNumber *age = [person valueForKey:@"age"];
    NSString *name = [person valueForKey:@"name"];
    NSNumber *num = [person valueForKeyPath:@"student.num"];
    
    NSLog(@"%@, %@, %@",age,name,num);
}
複製代碼

最後獲得結果age=10name=張三num=20,因而可知,經過KVC確實能夠修改對象中的屬性。

使用KVC除了能夠修改屬性,也能夠修改爲員變量的值,在XLPerson中增長以下成員變量

@interface XLPerson : NSObject{
    int _height;
    int _weight;
}
@end
複製代碼

而後使用KVC進行賦值

XLPerson *person = [[XLPerson alloc] init];
[person setValue:@30 forKey:@"_height"];
[person setValue:@40 forKeyPath:@"_weight"];

NSLog(@"%@, %@",person->_height,person->_weight);
複製代碼

最後能夠發現KVC確實也能修改爲員變量的值。

同時,經過上面的代碼咱們能夠看出兩種賦值和取值方法的區別。

  • setValue:forkey能夠給person對象的全部屬性賦值,可是層級只有一級,若是存在多級屬性賦值,那麼就須要調用屢次此方法,上文的例子中,若是要修改student對象的num屬性,就必須調用兩次
[[person valueForKey:@"student"] valueForKey:@"num"];
複製代碼
  • setValue:forkeyPath:支持一級屬性賦值,也支持多級屬性賦值,須要將屬性的具體訪問路徑傳遞過去,在上文的例子中,經過student.num就能夠修改student對象的num屬性。在使用上更加簡潔。

KVC底層原理

setValue:forkey:賦值流程

其實經過setValue:forkey方法給對象的屬性賦值,主要通過如下幾個流程

  • 首先會按照setKey:_setKey:的順序到對象的方法列表中尋找這兩個方法,若是找到了方法,則傳參而且調用方法。
  • 若是沒有找到方法,則經過accessInstanceVariablesDirectly方法的返回值來決定是否可以查找成員變量。若是accessInstanceVariablesDirectly返回YES,則會按照如下順序到成員變量列表中查找對應的成員變量:
    • _key
    • _isKey
    • key
    • isKey
  • 若是accessInstanceVariablesDirectly返回NO,則直接拋出NSUnknownKeyException異常。
  • 若是在成員變量列表中找到對應的屬性值,則直接進行賦值,若是找不到,則會拋出NSUnknownKeyException異常。

對應流程圖以下:

valueForKey:取值流程

經過valueForKey:方法取值,流程以下:

  • 首先會按照如下順序查找方法列表
    • getKey
    • key
    • isKey
    • _key
  • 若是找到就直接傳遞參數,調用方法,若是未找到則查看accessInstanceVariablesDirectly方法的返回值,若是返回NO,則直接拋出NSUnknownKeyException異常
  • 若是accessInstanceVariablesDirectly方法返回YES,則按以下順序查找成員變量列表
    • _key
    • _isKey
    • key
    • isKey
  • 若是能找到對應的成員變量,則直接獲取成員變量的值,若是未找到,則拋出NSUnknownKeyException異常

流程圖以下:

KVC和KVO的聯繫

經過對KVO的探索,咱們知道,給對象的某個屬性添加KVO監聽,實際上是動態建立了一個此類的子類,而後將對象的isa指針指向新生成的類,最後經過重寫屬性的setter方法來添加監聽。那麼若是使用KVO來對屬性或者成員變量進行賦值,會觸發KVO監聽嗎?咱們經過一個簡單的例子來測試一下

仍是使用上文的XLPerson對象

- (void)viewDidLoad{
    [super viewDidLoad];

    self.person = [[XLPerson alloc] init];
    [self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
    [self.person addObserver:self forKeyPath:@"_height" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];

}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    NSLog(@"\n keyPath:%@, \n object:%@, \n change:%@", keyPath, object, change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person setValue:@10 forKey:@"age"];
    [self.person setValue:@20 forKeyPath:@"_height"];
}
複製代碼

運行代碼,點擊屏幕能夠看到以下打印信息

經過KVC不論是設置屬性的值仍是成員變量的值,都會觸發KVO監聽,說明在KVC內部確實會在給屬性或成員變量賦值的時候,會經過相似調用didChangeValueForKey方法來觸發KVO監聽。

面試題

KVO

KVO的本質是什麼?

  • 給一個實例對象添加KVO,系統內部是利用Runtime動態的生成一個此實例對象的類對象的子類,具體的格式爲_NSKVONotifying_XXX,而且讓實例對象的isa指針指向這個新生成的類。
  • 重寫屬性的set方法,當調用set方法時,會調用Foundation框架的NSSetXXXValueAndNotify函數
  • 在_NSSetXXXValueAndNotify中會執行如下步驟
    • 調用willChangeValueForKey:方法
    • 調用父類的set方法,從新賦值
    • 調用didChangeValueForKey:方法,didChangeValueForKey:內部會觸發監聽器的observeValueForKeyPath:ofObject:change:context:方法

如何手動觸發KVO?

在修改變量先後手動調用willChangeValueForKey:didChangeValueForKey:方法

[self willChangeValueForKey:name];
_name = @"xxx";
[self didChangeValueForKey:name];
複製代碼

直接修改爲員變量的值是否會觸發KVO?

直接修改爲員變量的值不會觸發KVO,由於沒有觸發setter方法。

KVC

經過KVC修改屬性的值會觸發KVO嗎?

會觸發KVO。

KVC的賦值過程和取值過程分別是什麼樣的?

參考上文流程圖

KVC的原理是什麼?

參考上文流程圖

結束語

以上內容純屬我的理解,若是有什麼不對的地方歡迎留言指正。

一塊兒學習,一塊兒進步~~~

相關文章
相關標籤/搜索