iOS進階之路 (十三)KVC

KVC屬於Foundation框架,不開源,咱們只能經過官方文檔來了解它html

一.KVC初探

1.1 KVC的定義

Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.數組

KVC(鍵值編碼)由 NSKeyValueCoding非正式協議啓用的一種機制,採用該協議能夠間接訪問對象的屬性。當一個對象與鍵值編碼兼容時,它的屬性能夠經過一個簡潔、統一的消息傳遞接口經過字符串參數尋址。這種間接訪問機制補充了實例變量及其相關訪問器方法提供的直接訪問。bash

Objects typically adopt key-value coding when they inherit from NSObject (directly or indirectly),服務器

全部直接或者間接繼承了NSObject的類型,也就是幾乎全部的Objective-C對象都能使用KVC (一些純Swift類和結構體是不支持KVC的)app

1.2 KVC的API

KVC經常使用的四個方法框架

// 經過 key 設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 經過 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 經過 keyPath 設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 經過 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
複製代碼

NSKeyValueCoding類別中還有其餘的一些方法ide

// 默認返回YES,表示若是沒有找到Set<Key>方法的話,會按照_key,_iskey,key,iskey的順序搜索成員,設置成NO就不這樣搜索
+ (BOOL)accessInstanceVariablesDirectly;

// KVC提供屬性值正確性驗證的API,它能夠用來檢查set的值是否正確、爲不正確的值作一個替換值或者拒絕設置新值並返回錯誤緣由。
- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;

// 這是集合操做的API,裏面還有一系列這樣的API,若是屬性是一個NSMutableArray,那麼能夠用這個方法來返回。
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;

// 若是Key不存在,且沒有KVC沒法搜索到任何和Key有關的字段或者屬性,則會調用這個方法,默認是拋出異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;                 // 取值
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; // 設值

// 若是你在SetValue方法時面給Value傳nil,則會調用這個方法
- (void)setNilValueForKey:(NSString *)key;

// 輸入一組key,返回該組key對應的Value,再轉成字典返回,用於將Model轉到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;

複製代碼

二. KVC的使用

這段內容比較基礎,只須要注意:只有繼承於NSObject的數據才能使用KVC,非NSObject類型的須要作類型轉換。學習

2.1 訪問對象屬性

2.1.1 valueForKey & setValue: Forkey

經過 valueForKey:setValue:Forkey:來間接的獲取和設置屬性值ui

valueForKey: - Returns the value of a property named by the key parameter. If the property named by the key cannot be found according to the rules described in Accessor Search Patterns, then the object sends itself a valueForUndefinedKey: message. The default implementation of valueForUndefinedKey: raises an NSUndefinedKeyException, but subclasses may override this behavior and handle the situation more gracefully.this

  • valueForKey: 返回由 key 參數命名的屬性的值。若是根據訪問者搜索模式中的規則找不到由 key 命名的屬性,則該對象將向自身發送 valueForUndefinedKey: 消息。valueForUndefinedKey:的默認實現會拋出 NSUndefinedKeyException 異常,可是子類能夠重寫此行爲並更優雅地處理這種狀況。

setValue:forKey:: Sets the value of the specified key relative to the object receiving the message to the given value. The default implementation of setValue:forKey: automatically unwraps NSNumber and NSValue objects that represent scalars and structs and assigns them to the property. See Representing Non-Object Values for details on the wrapping and unwrapping semantics. If the specified key corresponds to a property that the object receiving the setter call does not have, the object sends itself a setValue:forUndefinedKey: message. The default implementation of setValue:forUndefinedKey: raises an NSUndefinedKeyException. However, subclasses may override this method to handle the request in a custom manner.

  • setValue:forKey:: 將該消息接收者的指定 key 的值設置爲給定值。默認實現會自動把表示標量結構體的 NSNumber 和 NSValue 對象解包而後賦值給屬性。若是指定 key 所對應的屬性沒有對應的 setter 實現,則該對象將向自身發送 setValue:forUndefinedKey: 消息,valueForUndefinedKey:的默認實現會拋出一個 NSUndefinedKeyException 的異常。可是子類能夠重寫此方法以自定義方式處理請求。

Example:

AKPerson *person = [[AKPerson alloc] init];
   
[person setValue:@"akironer" forKey:@"name"];
NSLog(@"%@", [person valueForKey:@"name"]);

打印輸出:akironer
複製代碼

2.1.2 valueForKeyPath & setValue:forKeyPath:

valueForKeyPath: - Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.

  • valueForKeyPath: : 返回於接受者的指定key path上的值。key path 路徑序列中不符合特定鍵的鍵值編碼的任何對象,都會接收到 valueForUndefinedKey: 消息。

setValue:forKeyPath: - Sets the given value at the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key receives a setValue:forUndefinedKey: message.

  • setValue:forKeyPath:: 將該消息接收者的指定 key path 的值設置爲給定值。key path 路徑序列中不符合特定鍵的鍵值編碼的任何對象都將收到setValue:forUndefinedKey: 消息

Example:

AKTeacher *teacher = [[AKTeacher alloc] init];
teacher.subject    = @"iOS";
person.teacher     = teacher;
[person setValue:@"iOS進階之路" forKeyPath:@"teacher.subject"];
NSLog(@"%@",[person valueForKeyPath:@"teacher.subject"]);

打印輸出:iOS進階之路
複製代碼

2.1.3 dictionaryWithValuesForKeys: & setValuesForKeysWithDictionary:

-> dictionaryWithValuesForKeys: - Returns the values for an array of keys relative to the receiver. The method calls valueForKey: for each key in the array. The returned NSDictionary contains values for all the keys in the array.

  • 返回接收者的 key 數組的值。該方法會爲數組中的每一個 key 調用valueForKey:。 返回的 NSDictionary 包含數組中全部鍵的值。

setValuesForKeysWithDictionary: - Sets the properties of the receiver with the values in the specified dictionary, using the dictionary keys to identify the properties. The default implementation invokes setValue:forKey: for each key-value pair, substituting nil for NSNull objects as required.

  • setValuesForKeysWithDictionary::使用字典鍵標識屬性,將指定字典中的對應值設置成該消息接收者的屬性值。默認實現會對每個鍵值對調用 setValue:forKey:。設置時須要將 nil 替換成 NSNull

Collection objects, such as NSArray, NSSet, and NSDictionary, can’t contain nil as a value. Instead, you represent nil values using the NSNull object.

  • NSArray NSSetNSDictionary 等集合對象不能包含 nil 做爲值, 可使用 NSNull對象代替 nil 值。
[person setValuesForKeysWithDictionary:@{@"name": @"akironer", @"age": @(18)}, @"hobby":[NSNULL null]];
NSLog(@"%@", [person dictionaryWithValuesForKeys:@[@"name", @"age"]]);       
        
打印輸出:
{
    age = 18;
    name = akironer;
    hobby = null;
}  
複製代碼

2.2 訪問集合屬性

//  方法一:普通方式
person.array = @[@"1",@"2",@"3"];
NSArray *array = [person valueForKey:@"array"]; // 不可不數組沒法直接修改,用 array 的值建立一個新的數組
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"方法一:%@",[person valueForKey:@"array"]);
    
// 方法二:KVC 的方式
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"方法二:%@",[person valueForKey:@"array"]);

打印輸出:
方法一:(
    100,
    2,
    3
)
方法二:(
    100,
    2,
) 
複製代碼

操做集合對象內部的元素來講,更高效的方式是使用 KVC 提供的可變代理方法。KVC 爲咱們提供了三種不一樣的可變代理方法:

  1. mutableArrayValueForKey: & mutableArrayValueForKeyPath::返回的代理對象表現爲一個 NSMutableArray 對象
  2. mutableSetValueForKey: & mutableSetValueForKeyPath::返回的代理對象表現爲一個 NSMutableSet 對象
  3. mutableOrderedSetValueForKey: & mutableOrderedSetValueForKeyPath::返回的代理對象表現爲一個 NSMutableOrderedSet 對象

2.3 集合操做符

在使用 valueForKeyPath: 的時候,可使用集合運算符來實現一些高效的運算操做。

  1. 聚合操做符
  • @avg: 返回操做對象指定屬性的平均值
  • @count: 返回操做對象指定屬性的個數
  • @max: 返回操做對象指定屬性的最大值
  • @min: 返回操做對象指定屬性的最小值
  • @sum: 返回操做對象指定屬性值之和
  1. 數組操做符
  • @distinctUnionOfObjects: 返回操做對象指定屬性的集合--去重
  • @unionOfObjects: 返回操做對象指定屬性的集合
  1. 嵌套操做符
  • @distinctUnionOfArrays: 返回操做對象(嵌套集合)指定屬性的集合--去重,返回的是 NSArray
  • @unionOfArrays: 返回操做對象(集合)指定屬性的集合
  • @distinctUnionOfSets: 返回操做對象(嵌套集合)指定屬性的集合--去重,返回的是 NSSet

2.4 訪問非對象屬性

非對象屬性分爲兩類:

  • 基本數據類型,也就是所謂的標量(scalar)
  • 結構體(struct)。

2.4.1 訪問基本數據類型(標量)

經常使用的基本數據類型須要在設置屬性的時候包裝成 NSNumber 對象

Scalar types as wrapped in NSNumber objects

2.4.2 訪問結構體

除了 NSPoint NSRange NSRectNSSize,對於自定義的結構體,也須要進行 NSValue 的轉換操做.

Common struct types as wrapped using NSValue.

typedef struct {
    float x, y, z;
} ThreeFloats;

// 設值
ThreeFloats floats = {1., 2., 3.};
NSValue *value  = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"%@",reslut);

// 取值
ThreeFloats result;
[reslut getValue:&result] ;
NSLog(@"%f - %f - %f",result.x, result.y, result.z);

打印輸出:
{length = 12, bytes = 0x0000803f0000004000004040}
1.000000 - 2.000000 - 3.000000
複製代碼

三. KVC原理 -- 搜索規則

在學習KVC的搜索規則前,要先弄明白一個屬性的做用,這個屬性在搜索過程當中起到很重要的做用。這個屬性表示是否容許讀取實例變量的值,若是爲YES則在KVC查找的過程當中,從內存中讀取屬性實例變量的值。

@property (class, readonly) BOOL accessInstanceVariablesDirectly;
複製代碼

3.1 基本 getter

Search Pattern for the Basic Getter

valueForKey:方法的默認實現:valueForKey: 方法會在調用者傳入 key以後會在對象中按下列的步驟進行模式搜索:

  1. get<Key> <key> is<Key> 以及 _<key> 的順序查找對象中是否有對應的方法。
  • 若是找到了,將方法返回值帶上跳轉到第 5 步
  • 若是沒有找到,跳轉到第 2 步
  1. 若是沒有找到簡單getter方法方法,則查找是否有 countOf<Key> 方法 objectIn<Key>AtIndex: 方法 (對應於 NSArray類定義的原始方法) 以及 <key>AtIndexes: 方法 (對應於 NSArray 方法 objectsAtIndexes:)
  • 若是找到其中的第一個(countOf<Key>),再找到其餘兩個中的至少一個,則建立一個響應全部 NSArray 方法的代理集合對象,並返回該對象。(翻譯過來就是要麼是 countOf<Key> + objectIn<Key>AtIndex:,要麼是countOf<Key> + <key>AtIndexes:,要麼是 countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:)
  • 若是沒有找到,跳轉到第 3 步
  1. 若是沒有找到簡單NSArray方法,查找名爲 countOf<Key> enumeratorOf<Key> memberOf<Key> 這三個方法(對應於NSSet類定義的原始方法)
  • 若是找到這三個方法,則建立一個響應全部 NSSet 方法的代理集合對象,並返回該對象
  • 若是沒有找到,跳轉到第 4 步
  1. 判斷類方法 accessInstanceVariablesDirectly 結果
  • 返回 YES,則以 _<key> _is<Key> <key> is<Key> 的順序查找成員變量。若是找到了,將成員變量帶上跳轉到第 5 步,若是沒有找到則跳轉到第 6 步
  • 返回 NO,跳轉到第 6 步
  1. 判斷取出的屬性值
  • 若是屬性值是對象,直接返回
  • 若是屬性值不是對象,但能夠轉化爲 NSNumber 類型,則將屬性值轉化爲 NSNumber 類型返回
  • 若是屬性值不是對象,也不能轉化爲 NSNumber 類型,則將屬性值轉化爲 NSValue 類型返回
  1. 調用 valueForUndefinedKey:, 默認狀況下拋出NSUndefinedKeyException異常,可是繼承於NSObject的子類能夠重寫該方法避免崩潰並作相應措施

Search Pattern for the Basic Getter

3.2 基本 setter

Search Pattern for the Basic Setter

  1. set<Key>: _set<Key> 順序查找對象中是否有對應的方法
  • 找到了直接調用設值
  • 沒有找到跳轉第2步
  1. 判斷 accessInstanceVariablesDirectly 結果
  • 爲YES,按照 _<key> _is<Key> <key> is<Key> 的順序查找成員變量,找到了就賦值;找不到就跳轉第3步
  • 爲NO,跳轉第3步
  1. 調用setValue:forUndefinedKey:。默認狀況下拋出NSUndefinedKeyException異常,可是繼承於NSObject的子類能夠重寫該方法避免崩潰並作出相應措施

Search Pattern for the Basic Setter

3.3 編譯器自動實現getter setter

這裏再明確下實例變量、成員變量、屬性之間的區別:

  • 成員變量:在 @interface 括號裏面聲明的變量
  • 成員變量實際上由兩部分組成:實例變量 + 基本數據類型變量
  • 屬性 = 成員變量 + getter方法 + setter方法

咱們不去重寫屬性的 getter 和 setter 方法以及聲明對應的實例變量,那麼編譯器就會幫咱們作這件事,那麼是否是說有多少個屬性,就會生成多少個對應的 getter 和 setter 呢?

顯然,編譯器不會這麼傻。編譯器在objc-accessors.mm中運用通用原則給全部屬性都提供了同一的入口,setter方法會根據修飾符不一樣調用不一樣方法,最後統一調用reallySetProperty方法示。

四. KVC的使用

KVC在iOS開發中是毫不可少的利器,也是許多iOS開發黑魔法的基礎。列舉一下KVC的使用場景。

4.1 動態取值和設值

最基本的用法,相信你們都很屬性了

4.2 訪問和修改私有變量

對於類裏的私有屬性,Objective-C是沒法直接訪問的,可是KVC是能夠的。

4.3 模型和字典轉換

運用了KVC和Objc的runtime組合的技巧,完成模型和字典的相互轉換

4.4 修改控件的內部屬性

在 iOS 13 以前,咱們能夠經過 KVC 去獲取和設置系統的私有屬性,但從 iOS 13 以後,這種方式被禁用掉了。相信很多同窗適配 iOS 13的時候,已經遇到了KVC的訪問限制問題。

例如UITextField中的placeHolderText已經不能修改了,這裏提供兩種簡答的修改思路,想要深刻了解的能夠參考關於iOS 13 中KVC 訪問限制的一些處理

  1. 經過attributedPlaceholder屬性修改Placeholder顏色
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:@"請輸入佔位文字" attributes: @{NSForegroundColorAttributeName:[UIColor redColor], NSFontAttributeName:textField.font }];

textField.attributedPlaceholder = attrString;
複製代碼
  1. UITextField從新寫一個方法
- (void)resetTextField: (UITextField *)textField
{
 	Ivar ivar =  class_getInstanceVariable([textField class], "_placeholderLabel");
 	
    UILabel *placeholderLabel = object_getIvar(textField, ivar);
    placeholderLabel.text = title;
    placeholderLabel.textColor = color;
    placeholderLabel.font = [UIFont systemFontOfSize:fontSize];
    placeholderLabel.textAlignment = alignment;
}
複製代碼

五. 異常處理及正確性驗證

5.1 設置空值:setNilValueForKey

在設值時設置空值,能夠經過重寫setNilValueForKey來監聽

In the default implementation, when you attempt to set a non-object property to a nil value, the key-value coding compliant object sends itself a setNilValueForKey: message. The default implementation of setNilValueForKey: raises an NSInvalidArgumentException, but an object may override this behavior to substitute a default value or a marker value instead, as described in Handling Non-Object Values.

在默認實現中,當您試圖將非對象屬性設置爲nil時,KVC的對象會向本身發送一條setNilValueForKey:消息。setNilValueForKey的默認實現會引起NSInvalidArgumentException,但對象能夠重寫此行爲以替換默認值或標記值。

Given that an invocation of -setValue:forKey: would be unable to set the keyed value because the type of the parameter of the corresponding accessor method is an NSNumber scalar type or NSValue structure type but the value is nil, set the keyed value using some other mechanism.

大致意思就是說:只對NSNumber或者NSValue類型的數據賦空值時,setNilValueForKey纔會觸發。下面的例子中,subject不會觸發

@implementation LGPerson
- (void)setNilValueForKey:(NSString *)key {
    if ([key isEqualToString:@"age"]) {
        NSLog(@"你傻不傻: 設置 %@ 是空值",key);
        return 0;
    }
    [super setNilValueForKey:key];
}
@ end

[person setValue:nil forKey:@"age"]; 
[person setValue:nil forKey:@"subject"]; // subject不觸發 - 官方註釋裏面說只對 NSNumber - NSValue
複製代碼

5.2 未定義的key:setValue:forUndefinedKey

對於未定義的key, 能夠經過重寫setValue:forUndefinedKey:valueForUndefinedKey:來監聽。

例如:

咱們在字典轉模型的時候,例如服務器返回一個id字段,可是對於客戶端來講id是系統保留字段,能夠重寫setValue:forUndefinedKey:方法並在內部處理id參數的賦值。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
    if ([key isEqualToString:@"id"]) {
        self.userId = [value integerValue];
    }
}
複製代碼

5.3 屬性驗證

在調用KVC時能夠先進行驗證,驗證經過下面兩個方法進行,支持keykeyPath兩種方式。

驗證方法須要咱們手動調用,並不會在進行KVC的過程當中自動調用

- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
複製代碼

該方法的工做原理:

  1. 先找一下你的類中是否實現了方法 -(BOOL)validate:error;
  2. 若是實現了就會根據實現方法裏面的自定義邏輯返回NO或者YES;若是沒有實現這個方法,則系統默認返回YES

下面是使用驗證方法的例子。在validateValue方法的內部實現中,若是傳入的value或key有問題,能夠經過返回NO來表示錯誤,並設置NSError對象。

AKPerson* person = [[AKPerson alloc] init];
NSError* error;
NSString* name = @"John";
if (![person validateValue:&name forKey:@"name" error:&error]) {
    NSLog(@"%@",error);
}
複製代碼

六. 總結

  1. KVC 是一種 NSKeyValueCoding 隱式協議所提供的機制。
  2. KVC 經過 valueForKey:valueForKeyPath: 來取值,不考慮集合類型的話具體的取值過程以下:
  • get<Key> <key> is<Key> _<key> 的順序查找方法
  • 若是找不到方法,則經過類方法 accessInstanceVariablesDirectly 判斷是否能讀取成員變量來返回屬性值
  • _<key> _is<Key> <key> is<Key> 的順序查找成員變量
  1. KVC 經過 setValueForKey:setValueForKeyPath: 來取值,不考慮集合類型的話具體的設置值過程以下:
  • set<Key> _set<Key> 的順序查找方法
  • 若是找不到方法,則經過類方法 accessInstanceVariablesDirectly 判斷是否能經過成員變量來返回設置值 以 _<key> _is<Key> <key> is<Key> 的順序查找成員變量

此次咱們依據蘋果的官方文檔完成了KVC的探索,其實蘋果的英文註釋和官方文檔寫的很是用心,咱們在探索 iOS 底層的時候,文檔思惟十分重要,多閱讀文檔總會有新的收穫。

相關文章
相關標籤/搜索