iOS進階 -- KVC應用及原理

前言

KVC又稱鍵值編碼 (Key-Value-Coding),在iOS開發中是一個比較常見的技術點,相信不少開發人員都使用過KVC,其主要的兩個方法就是以下兩個,分別對應設置值和取值:html

- (void)setValue:(nullable id)value forKey:(NSString *)key;
- (nullable id)valueForKey:(NSString *)key;
複製代碼

平時的開發中,咱們有不少地方使用到KVC,例如給一個對象的屬性賦值、StroyBoard中給控件的layer設置邊框等都是KVC的應用,本篇文章就來探索下KVC的原理及使用注意事項。git

1、KVC的使用技巧

1.一、KVC機制

在代碼中調用KVC,而後進入源碼定義處,能夠發現蘋果給 NSObject、NSArray等都加了一個# NSKeyValueCoding 分類,以下圖所示:github

KVC源碼定義.png

NSKeyValueCoding是什麼呢?經過 KVC文檔NSKeyValueCoding 能夠發現其解釋以下:數組

Xnip2021-07-25_09-15-39.png

Xnip2021-07-25_09-15-51.png

圖中的大體意思是,NSKeyValueCoding 是一種非正式協議,它提供了一種能夠間接訪問對象屬性的機制,這套機制使得咱們能夠經過一個簡潔明晰的字符串接口訪問對象屬性。這種機制補充了對象的實例變量的訪問。安全

怎麼理解這段話呢?咱們新建一個工程 KVCDemo,而後建立一個 BPPerson 類,在該類中分別添加屬性及成員變量以下:markdown

Xnip2021-07-25_09-39-01.png

Xnip2021-07-25_09-30-19.png

在調用bpName、title、hobby時,會出現以下狀況:網絡

Xnip2021-07-25_09-35-46.png

圖中的錯誤咱們均可以理解,bpName 定義在.m實現文件中,外部沒法直接訪問,其實若是 hobby 不添加 @public 修飾,其默認爲 protect,也是沒法訪問的。可是經過 KVC 則能夠避免這些限制,代碼以下:多線程

void personKVC(void) {

    BPPerson *person = [BPPerson alloc];

//    person.title = @"攻城獅";

//    person->hobby = @"running and programming";

//    person->bpName = @"奔跑";

    [person setValue:@"BP" forKey:@"bpName"];

    [person setValue:@"running and programming" forKey:@"hobby"];

    [person setValue:@"攻城獅" forKey:@"title"];

    [person printPerson];
    [person printPersonKVC];
}
複製代碼

代碼運行結果以下app

Xnip2021-07-25_10-05-02.png

能夠發現經過KVC成功訪問了屬性和成員變量,而且成功賦值和取值。ide

1.2 KVC API介紹

1.2.1 NSObject的 API

KVC中最爲基礎的兩個 API就是 setValue: forKey:valueForKey:,分別是根據 Key 設置值 和取出值,其用法如上面例子所示。此外,還有一些其餘的方法供咱們調用,下面以以下幾個方法爲例進行探究「

第一個方法以下:

/* Given a key that identifies an _ordered_ to-many relationship, return a mutable array that provides read-write access to the related objects. Objects added to the mutable array will become related to the receiver, and objects removed from the mutable array will become unrelated. */
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
複製代碼

在對象包含一個數據變量時,若是調用該方法,會獲得一個可變數組,經過修改該數組中的元素,使得原數組也發生變動,即便原數組是不可變數組。下面咱們驗證下,在 BPPerson 中添加一個不可變數組屬性

**@property** (**nonatomic**, **copy**) NSArray *array;
複製代碼

進行以下調用,並獲得的結果以下:

Xnip2021-07-25_13-17-34.png

能夠發現原數組原本是 @[@"1", @"2", @"3"],原本不能夠被修改,可是經過調用該方法,獲得可變數組,並修改其中的值後,發現原數組也變爲了圖中結果。

第二個方法主要是爲了介紹 keyPath,與之相關的有讀寫兩個,因此一塊兒介紹:

- (**nullable** **id**)valueForKeyPath:(NSString *)keyPath;
- (**void**)setValue:(**nullable** **id**)value forKeyPath:(NSString *)keyPath;
複製代碼

咱們先新建一個 BPAnimal 類,而後在 BPPerson 中加一個 pet 屬性,代碼以下:

**@interface** BPPerson : NSObject {

    **@public** NSString *hobby;

}

**@property** (**nonatomic**, **copy**) NSString *title;

**@property** (**nonatomic**, **copy**) NSArray *array;

**@property** (**nonatomic**, **strong**) BPAnimal *pet; // 寵物

- (**void**)printPerson;

- (**void**)printPersonKVC;

**@end**


**@interface** BPAnimal : NSObject

**@property** (**nonatomic**, **copy**) NSString *breed; // 品種

**@end**

複製代碼

BPPerson 中添加一個寵物 pet 屬性, 寵物有本身的品種,所以 BPAnimal 中包含一個 breed 屬性。 下面經過keyPath來嘗試寫入和取值,代碼和結果以下

Xnip2021-07-25_13-43-33.png

由圖中結果能夠看出,經過 pet.breed 這一條 keyPath,成功完成賦值。此外還有其餘的API還沒探索,有興趣的小夥伴能夠繼續探索,這裏探索了平時使用較多的API。

1.2.2 NSArray 的 valueForKey

在平時咱們使用 NSArray 時,用到最多的就是 objectAtIndex,根據下標進行取值。可是在查看KVC的定義時能夠發現,NSArray 和 NSMutableArray 也有 valueForKey 的方法,以下代碼所示:

**@interface** NSArray<ObjectType>(NSKeyValueCoding)

/* Return an array containing the results of invoking -valueForKey: on each of the receiver's elements. The returned array will contain NSNull elements for each instance of -valueForKey: returning nil.*/

- (**id**)valueForKey:(NSString *)key;

/* Invoke -setValue:forKey: on each of the receiver's elements.*/

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

複製代碼

經過註釋能夠得知,數組的 valueForKey 作的事情是,對數組中的元素調用 valueForKey 方法,並返回一個數組。經過如下demo能夠驗證這一點:

Xnip2021-07-25_14-36-46.png

由結果發現,對於array調用 setValue:forKey:將原有的獅子改成了老虎,而取值也是統一取出了老虎,由於此時數組中的元素都發生了改變。

須要注意的是,若是數組中的元素不是同一個類的,那麼調用該方法就存在崩潰的風險,由於可能有部分元素不存在傳入的key,示例以下圖:

Xnip2021-07-25_14-43-42.png

當最後一個元素換爲 person時,person沒有breed變量,所以發生崩潰,並報錯 setValue:forUndefinedKey:

1.2.3 NSDictionary的 setObjcet:forKey: 與 setValue:forKey: 的區別

在使用可變字典設置值時,咱們常常調用的是 setObjcet:forKey:,可是若是調用 setValue:forKey: 也不會報錯,而且也能成功賦值,那麼這兩個方法到底有什麼區別呢?咱們先看一下字典的KVC定義,代碼以下:

Xnip2021-07-25_14-50-35.png

經過註釋能夠發現,在調用 setValue:forKey: 時,至關於調用了 setObjcet:forKey:,可是若是傳入的value爲nil,則會調用 -removeObjectForKey:,而取值調用 valueForKey: 則至關於調用 -objectForKey:。下面經過一個demo驗證下:

Xnip2021-07-25_14-59-50.png

能夠發現,本來 BP 值爲 NP,第一次傳入 666,修改爲功,可是當第二次傳入 nil 時,直接移除了 BP 這個 Key。這裏還能夠發現一個細節,以後可變字典纔有 setValue:forKey:,而不可變字典中則沒有進行聲明,說明KVC也是仍是要保留了字典可變與不可變的特性。

2、KVC的調用流程

對於KVC的調用流程,在 KVC Fundamentals 中能夠看到對應的描述,下面咱們分爲 setValuevalueForKey 兩部分來分析下其調用流程。

2.1 setValue:forKey:

在官方文檔中能夠發現以下一段描述:

Xnip2021-07-25_15-58-27.png

總結一下能夠分爲3步:

  • 首先按照 set<<Key>Key> 、 _set<Key> 的順序查找方法,若是找到了就調用並賦值,完成KVC流程
  • 若是沒找到,而且 accessInstanceVariablesDirectly 方法返回 YES,則按照 _<Key>、_is<Key>、<Key>、is<Key>的順序查找,若是找到其中一個就將傳入的值賦值,並完成KVC流程
  • 若是以上兩步都沒有找到,就調用 setValue:forUndefinedKey:方法,並拋出異常。

下面經過一個Demo來驗證一下,首先按照規則定義對應的成員變量和方法,並先將accessInstanceVariablesDirectly返回 NO。代碼以下:

@interface BPPerson : NSObject {
    NSString *hobby;
    NSString *_hobby;
    NSString *ishobby;
    NSString *_ishobby;
}

+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

- (void)setHobby:(NSString *)hobby {
    NSLog(@"setHobby == %s", __func__);
}

- (void)_setHobby:(NSString *)hobby {
    NSLog(@"setHobby == %s", __func__);
}

複製代碼

一、此時的調用結果以下:

Xnip2021-07-25_23-49-02.png

能夠發現確實先調用了 setHobby: 方法,而且四個成員變量均爲nil,由於setHobby:方法沒有進行賦值操做。

二、接下來註釋該方法,只保留 _setHobby:調用結果以下:

Xnip2021-07-25_23-53-53.png

能夠發現這次直接調用 _setHobby: 方法,四個成員變量依然均爲 nil,代表並未賦值。

三、繼續註釋 _setHobby: 方法,而且看下 accessInstanceVariablesDirectly 分別爲 NOYES 的狀況:

  • accessInstanceVariablesDirectlyNO時結果以下:

Xnip2021-07-25_23-57-54.png

此時直接拋出異常 setValue:forUndefinedKey:,四個成員變量固然不可能賦值。

  • accessInstanceVariablesDirectlyYES時結果以下:

Xnip2021-07-26_00-01-05.png

此時發現成員變量 _hobby 成功賦值,其它依然爲nil,也正符合第二條規則的順序的第一個尋找的變量。

  • 按照順序註釋成員變量,依次打印的結果以下:

Xnip2021-07-26_00-29-35.png

Xnip2021-07-26_00-30-20.png

Xnip2021-07-26_00-31-07.png

經過打印結果能夠發現,對於成員變量的查找順序確實如文檔所說的 _<Key>、_is<Key>、<Key>、is<Key>。在測試過程當中,曾將成員變量寫做 ishobby、_ishobby,這樣寫最終致使了異常拋出,也從側面驗證了第三條,找不到時會拋出異常。

四、變量的查找會考慮is<Key> 和 _is<Key>的變量,那麼對於set方法會不會也有相似的調用呢?在這裏咱們能夠補充下 setIs<Key> 和 _setIs<Key>的調用狀況驗證。

先增長兩個方法以下:

- (**void**)setIsHobby:(NSString *)hobby {
    NSLog(@"setIsHobby == %s", **__func__** );
}
- (**void**)_setIsHobby:(NSString *)hobby {
    NSLog(@"_setIsHobby == %s", **__func__** );
}
複製代碼

調用 setValue:forKey:後,的執行結果以下:

Xnip2021-07-26_09-37-23.png

而後註釋 setIsHobby:方法,再次執行setValue:forKey:,結果以下:

Xnip2021-07-26_09-37-54.png

經過結果能夠發現,實際上在_setHobby:註釋後,並無立刻查找成員變量,而是繼續查找並調用了setIsHobby:,可是當setIsHobby:註釋後,並不會再查找 _setIsHobby:,而是直接給成員變量賦值。也就是說文檔中所說的第一步中會多查找一個方法setIs<Key>,可是不會對_setIs<Key>進行調用。

KVC調用流程整理以下圖:

KVC寫入.jpg

2.2 valueForKey:

對於取值過程 valueForKey 的描述以下:

Xnip2021-07-26_00-42-55.png

這段描述也能夠分爲幾個流程:

  • 一、按照 get<Key>、<key>、is<Key>、_<key>的順序查找方法實現,若是找到就調用,並執行第5步;
  • 二、若是上一步中沒有找到,而且是一個數組類型,則調用數組相關的方法;
  • 三、若是上一步沒找到,而且是一個集合類型,則調用集合相關方法;
  • 四、若是以上都沒找到,則判斷accessInstanceVariablesDirectly 是否爲 YES,若是爲YES,則依次查找成員變量_<key>_is<Key><key>, is<Key>,若是查找到則進行第5步,不然進行第6步
  • 五、查找到值,須要對值進行處理
    • 若是檢索到的值是一個對象指針,則直接返回該對象,
    • 若是是一個 NSNumber標量,則將其存儲在NSNumber中並返回,
    • 若是不是一個 NSNumber標量,則存儲在 NSValue中並返回
  • 六、在1~4未找到的狀況下,拋出異常

以上流程中有針對數組和集合的處理,而對於字典的處理並不在這一步驟中,這是由於如前文所述,字典的KVC其實是調用本身的 setObject:forKey:objectForKey: 方法,並不會查找自身的成員變量和屬性。

下面對普通對象的 valueForKey: 進行驗證以下:

Xnip2021-07-26_11-01-09.png

按照步驟要求給成員變量賦值不一樣的值,同時在實現上述第一步的方法時,再增長 getIsHobby方法。下面開始進行驗證:

一、直接調用 valueForKey:,結果以下:

Xnip2021-07-26_11-12-37.png

從結果能夠看到,這裏直接調用 getHobby 方法,而且沒有從成員變量取值。

二、下面依次註釋 getHobbyhobbyisHobby_hobby,結果依次以下:

Xnip2021-07-26_11-50-10.png

Xnip2021-07-26_11-51-21.png

Xnip2021-07-26_11-51-52.png

Xnip2021-07-26_11-52-28.png

分析結果以下:

  • 註釋 getHobby後,結果顯示直接調用了hobby方法,未從變量取值
  • 註釋 hobby後,結果顯示直接調用了isHobby方法,未從變量取值
  • 註釋 isHobby後,結果顯示直接調用了_hobby方法,未從變量取值
  • 註釋 _hobby後,並未調用getIsHobby_isHobby方法,而是直接從變量_hobby取值

由這個結果發現,對於方法的查找次序確實是如第一步 get<Key>、<key>、is<Key>、_<key>,而且不會調用其餘的方法

三、接下來驗證變量的值的查找,依次註釋_hobby_isHobbyhobbyisHobby,發現結果以下:

Xnip2021-07-26_11-58-54.png

Xnip2021-07-26_11-59-38.png

Xnip2021-07-26_12-00-08.png

Xnip2021-07-26_12-00-43.png

根據結果能夠發現變量的調用順序確實如_<key>_is<Key><key>, is<Key>,而且最終未找到時會拋出異常valueForUndefinedKey:

在測試過程當中,還有一個細節,就是當註釋_hobby的賦值語句,但_hobby定義並未註釋時,打印結果以下:

Xnip2021-07-26_12-02-06.png

能夠發現結果此時並無由於_hobby值爲nil,就去查找_isHobby,而是直接取_hobby的值,這一次序不由於變量值而改變,在setValue:forKey:時,也是這樣。

valueForKey的流程圖以下:

kvc取值.jpg

總結

總結起來,本篇主要介紹了KVC的相關內容,主要有如下幾點:

  • KVC提供了一種機制,能夠間接訪問對象的成員變量和屬性(不過對因而否安全,這個技術是否破壞了對象的封裝性等等,就仁者見仁,智者見智了,但蘋果提供了這樣一種機制,而且沒有取消掉,因此存在即合理吧)
  • KVC的寫入值,會先調用相關的set方法,而後在accessInstanceVariablesDirectly爲YES狀況下,會賦值相應的成員變量
  • 相應的取值時,也會先調用相關get方法,若是沒有,在accessInstanceVariablesDirectly爲YES狀況下,會取相關變量的值

以上即爲KVC的API探索,以及其調用流程,本篇探索就到此爲止,其實還有關於自定義KVC的探索,可是自定義KVC較長,而且須要考慮到數組、字典、集合以及多線程等的狀況,沒有加入本篇章。其實在網絡上有不少大神寫的很厲害,這裏放上其中一個 DIS_KVC_KVO 供你們參考。

相關文章
相關標籤/搜索