前言
- KVC用的好多,今天來看看他的底層是怎么實現(xiàn)的。這里底層流程看起來有些粗糙,了解就行;
- KVC機制是在Foundation框架中,而Foundation框架是不開源的,可以看
官方文檔: Key-Value Coding Programming Guide
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.
內(nèi)容
- KVC定義及相關(guān)API
- KVC 設值 底層原理
- KVC 取值 底層原理
- KVC 使用場景
- 拓展: 異常處理、 屬性驗證、 KVC處理非對象
一、 KVC定義及相關(guān)API
KVC
的全稱是Key-Value Coding
,翻譯成中文是 鍵值編碼
,鍵值編碼是由NSKeyValueCoding
非正式協(xié)議啟用的一種機制,對象采用該協(xié)議來間接訪問其屬性。既可以通過一個字符串key來訪問某個屬性。這種間接訪問機制補充了實例變量及其相關(guān)的訪問器方法所提供的直接訪問。
主要有以下四個常用的方法:
//直接通過Key來取值
- (nullable id)valueForKey:(NSString *)key;
//通過Key來設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
//通過KeyPath來取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
//通過KeyPath來設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
其他API:
//默認返回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有關(guān)的字段或者屬性,則會調(diào)用這個方法,默認是拋出異常。
- (nullable id)valueForUndefinedKey:(NSString *)key;
//和上一個方法一樣,但這個方法是設值。
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//如果你在SetValue方法時面給Value傳nil,則會調(diào)用這個方法
- (void)setNilValueForKey:(NSString *)key;
//輸入一組key,返回該組key對應的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
二、KVC 設值 底層原理
針對賦值,Key-Value Coding Programming Guide中有這樣一段
Search Pattern for the Basic Setter
The default implementation of setValue:forKey:, given key and value parameters as input, attempts to set a property named key to value (or, for non-object properties, the unwrapped version of value, as described in Representing Non-Object Values) inside the object receiving the call, using the following procedure:
Look for the first accessor named set<Key>: or _set<Key>, in that order. If found, invoke it with the input value (or unwrapped value, as needed) and finish.
If no simple accessor is found, and if the class method accessInstanceVariablesDirectly returns YES, look for an instance variable with a name like _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, set the variable directly with the input value (or unwrapped value) and finish.
Upon finding no accessor or instance variable, invoke setValue:forUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
對于setValue:forKey:給定的key和value參數(shù)作為輸入,嘗試設置命名屬性key到value主要流程為
- 按順序查找名為set<Key>或 _set<Key>方法,如果找到,直接調(diào)用。如果沒找到,則第2步
- 調(diào)用類方法accessInstanceVariablesDirectly 如果返回Yes,則進入間接訪問實例化賦值,尋找名為_<key>,_is<Key>,<key>,或者is<Key>的變量,如果找到,則直接使用輸入值設置變量。
- 如果返回NO或者沒找到上面的值,則調(diào)用setValue:forUndefinedKey:,默認會拋出異常。
舉個例子:
LGPerson *person = [[LGPerson alloc] init];
// 1、一般setter 方法
person.name = @"AA";
// 2、KVC方式
[person setValue:@"CC" forKey:@"name"];
流程大概如下圖:
二、KVC 取值 底層原理
針對賦值,Key-Value Coding Programming Guide中有這樣一段
Accessor Search Patterns
The default implementation of the NSKeyValueCoding protocol provided by NSObject maps key-based accessor calls to an object’s underlying properties using a clearly defined set of rules. These protocol methods use a key parameter to search their own object instance for accessors, instance variables, and related methods that follow certain naming conventions. Although you rarely modify this default search, it can be helpful to understand how it works, both for tracing the behavior of key-value coded objects, and for making your own objects compliant.
NOTE
The descriptions in this section use <key> or <Key> as a placeholder for the key string that appears as a parameter in one of the key-value coding protocol methods, which is then used by that method as part of a secondary method call or variable name lookup. The mapped property name obeys the placeholder’s case. For example, for the getters <key> and is<Key>, the property named hidden maps to hidden and isHidden.
Search Pattern for the Basic Getter
The default implementation of valueForKey:, given a key parameter as input, carries out the following procedure, operating from within the class instance receiving the valueForKey: call.
Search the instance for the first accessor method found with a name like get<Key>, <key>, is<Key>, or _<key>, in that order. If found, invoke it and proceed to step 5 with the result. Otherwise proceed to the next step.
If no simple accessor method is found, search the instance for methods whose names match the patterns countOf<Key> and objectIn<Key>AtIndex: (corresponding to the primitive methods defined by the NSArray class) and <key>AtIndexes: (corresponding to the NSArray method objectsAtIndexes:).
If the first of these and at least one of the other two is found, create a collection proxy object that responds to all NSArray methods and return that. Otherwise, proceed to step 3.
The proxy object subsequently converts any NSArray messages it receives to some combination of countOf<Key>, objectIn<Key>AtIndex:, and <key>AtIndexes: messages to the key-value coding compliant object that created it. If the original object also implements an optional method with a name like get<Key>:range:, the proxy object uses that as well, when appropriate. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSArray, even if it is not.
If no simple accessor method or group of array access methods is found, look for a triple of methods named countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: (corresponding to the primitive methods defined by the NSSet class).
If all three methods are found, create a collection proxy object that responds to all NSSet methods and return that. Otherwise, proceed to step 4.
This proxy object subsequently converts any NSSet message it receives into some combination of countOf<Key>, enumeratorOf<Key>, and memberOf<Key>: messages to the object that created it. In effect, the proxy object working together with the key-value coding compliant object allows the underlying property to behave as if it were an NSSet, even if it is not.
If no simple accessor method or group of collection access methods is found, and if the receiver's class method accessInstanceVariablesDirectly returns YES, search for an instance variable named _<key>, _is<Key>, <key>, or is<Key>, in that order. If found, directly obtain the value of the instance variable and proceed to step 5. Otherwise, proceed to step 6.
If the retrieved property value is an object pointer, simply return the result.
If the value is a scalar type supported by NSNumber, store it in an NSNumber instance and return that.
If the result is a scalar type not supported by NSNumber, convert to an NSValue object and return that.
If all else fails, invoke valueForUndefinedKey:. This raises an exception by default, but a subclass of NSObject may provide key-specific behavior.
對于取值,文檔中有以上說明,其實類似于上面的賦值原理。
在valueForKey:給定key參數(shù)作為輸入的情況下
- 首先查找getter方法,按照get<Key> -> <key> -> is<Key> -> _<key>的方法順序查找 如果找到則進入步驟5,如果沒找到,則下一步
- 如果步驟1中的getter方法沒有找到,KVC會查找countOf <Key>和objectIn <Key> AtIndex :和<key> AtIndexes : 如果找到其中的第一個以及其他兩個中的至少一個,則創(chuàng)建一個響應所有NSArray方法的集合代理對象,并返回該對象。否則,請繼續(xù)執(zhí)行步驟3。代理對象隨后將任何NSArray接收到的一些組合的消息countOf<Key>,objectIn<Key>AtIndex:和<key>AtIndexes:消息給KVC創(chuàng)建它兼容的對象。如果原始對象還實現(xiàn)了名稱為的可選方法get<Key>:range:,則代理對象也會在適當時使用該方法。
- 如果沒有找到簡單的訪問方法或陣列訪問方法組,尋找一個三重的方法命名countOf<Key>,enumeratorOf<Key>和memberOf<Key>:(對應于由所定義的原始的方法NSSet類)。如果找到所有三個方法,請創(chuàng)建一個響應所有NSSet方法的集合代理對象并將其返回。否則,請繼續(xù)執(zhí)行步驟4。
- 如果此時還未找到,且接收器的類方法accessInstanceVariablesDirectly返回YES,搜索名為實例變量_<key>,_is<Key>,<key>,或者is<Key>,按照這個順序。如果找到,請直接獲取實例變量的值,然后繼續(xù)執(zhí)行步驟5。否則,請繼續(xù)執(zhí)行步驟6。
- 如果檢索到的屬性值是對象指針,則只需返回結(jié)果。
如果該值是所支持的標量類型NSNumber,則將其存儲在NSNumber實例中并返回該實例。
如果結(jié)果是NSNumber不支持的標量類型,請轉(zhuǎn)換為NSValue對象并返回該對象。- 如果其他所有方法均失敗,請調(diào)用valueForUndefinedKey:。默認情況下會引發(fā)異常
流程大概如下圖:
三、KVC 使用場景
估計都會使用就不舉例了
-
利用KVC動態(tài)的取值和設值
常用的可以通過setValue:forKey: 和 valueForKey:
也可以通過路由的方式setValue:forKeyPath: 和 valueForKeyPath:
Model和字典轉(zhuǎn)換
-
用KVC來訪問和修改私有變量
根據(jù)上面的實現(xiàn)原理我們知道,
KVC本質(zhì)上是操作方法列表以及在內(nèi)存中查找實例變量
。我們可以利用這個特性訪問類的私有變量,例如下面在.m中定義的私有成員變量和屬性,都可以通過KVC的方式訪問。【對于KVC
而言,一個對象沒有自己的隱私
,所以可以通過KVC修改和訪問任何私有屬性
】這個操作對readonly的屬性,@protected的成員變量,都可以正常訪問。如果不想讓外界訪問類的成員變量,則可以將
accessInstanceVariablesDirectly
屬性賦值為NO
。 -
修改一些控件的內(nèi)部屬性
這也是iOS開發(fā)中必不可少的小技巧。眾所周知很多UI控件都由很多內(nèi)部UI控件組合而成的,但是Apple度沒有提供這訪問這些控件的API(常用的就是自定義
tabbar
、個性化UITextField
中的placeHolderText
),這樣我們就無法正常地訪問和修改這些控件的樣式。而KVC在大多數(shù)情況可下可以解決這個問題。 -
用KVC實現(xiàn)高階消息傳遞
在對容器類使用KVC時,valueForKey:將會被傳遞給容器中的每一個對象,而不是對容器本身進行操作,結(jié)果會被添加到返回的容器中,這樣,可以很方便的操作集合 來返回 另一個集合
//KVC實現(xiàn)高階消息傳遞
- (void)transmitMsg{
NSArray *arrStr = @[@"english", @"franch", @"chinese"];
NSArray *arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString *str in arrCapStr) {
NSLog(@"%@", str);
}
NSArray *arrCapStrLength = [arrCapStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber *length in arrCapStrLength) {
NSLog(@"%ld", (long)length.integerValue);
}
}
//********打印結(jié)果********
2020-10-27 11:33:43.377672+0800 CJLCustom[60035:6380757] English
2020-10-27 11:33:43.377773+0800 CJLCustom[60035:6380757] Franch
2020-10-27 11:33:43.377860+0800 CJLCustom[60035:6380757] Chinese
2020-10-27 11:33:43.378233+0800 CJLCustom[60035:6380757] 7
2020-10-27 11:33:43.378327+0800 CJLCustom[60035:6380757] 6
2020-10-27 11:33:43.378417+0800 CJLCustom[60035:6380757] 7
四、拓展
KVC屬性驗證
KVC提供了屬性值,用來驗證key對應的Value是否可用的方法
- 在調(diào)用KVC時可以先進行驗證,驗證通過下面兩個方法進行,支持
key
和keyPath
兩種方式。驗證方法默認實現(xiàn)返回YES,可以通過重寫對應的方法修改驗證邏輯。
驗證方法需要我們手動調(diào)用,并不會在進行KVC
的過程中自動調(diào)用。
- (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;
這個方法的默認實現(xiàn)是去探索類里面是否有一個這樣的方法:
-(BOOL)validate<Key>:error:
如果有這個方法,就調(diào)用這個方法來返回,沒有的話就直接返回YES
@implementation Address
-(BOOL)validateCountry:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError{ //在implementation里面加這個方法,它會驗證是否設了非法的value
NSString* country = *value;
country = country.capitalizedString;
if ([country isEqualToString:@"Japan"]) {
return NO; //如果國家是日本,就返回NO,這里省略了錯誤提示,
}
return YES;
}
@end
NSError* error;
id value = @"japan";
NSString* key = @"country";
BOOL result = [add validateValue:&value forKey:key error:&error]; //如果沒有重寫-(BOOL)-validate<Key>:error:,默認返回Yes
if (result) {
NSLog(@"鍵值匹配");
[add setValue:value forKey:key];
}
else{
NSLog(@"鍵值不匹配"); //不能設為日本,其他國家都行
}
NSString* country = [add valueForKey:@"country"];
NSLog(@"country:%@",country);
//打印結(jié)果
KVCDemo[867:58871] 鍵值不匹配
KVCDemo[867:58871] country:China
KVC處理非對象
KVC是支持基礎(chǔ)數(shù)據(jù)類型和結(jié)構(gòu)體的,可以在setter和getter的時候,通過NSValue和NSNumber來轉(zhuǎn)換為OC對象。該方法valueForKey:總是返回一個id對象,如果原本的變量類型是值類型或者結(jié)構(gòu)體,返回值會封裝成NSNumber或者NSValue對象。這兩個類會處理從數(shù)字,布爾值到指針和結(jié)構(gòu)體任何類型。然后開發(fā)者需要手動轉(zhuǎn)換成原來的類型。盡管valueForKey:會自動將值類型封裝成對象,但是setValue:forKey:卻不行。你必須手動將值類型轉(zhuǎn)換成NSNumber或者NSValue類型,才能傳遞過去。
- 可以調(diào)用initWithBool:方法對基礎(chǔ)數(shù)據(jù)類型進行包裝
@property (nonatomic, assign, readonly) BOOL boolValue;
- (NSNumber *)initWithBool:(BOOL)value
KVC異常處理
- key或者keyPath發(fā)生錯誤
當根據(jù)KVC搜索規(guī)則,沒有搜索到對應的key或者keyPath,則會調(diào)用對應的異常方法。異常方法的默認實現(xiàn),在異常發(fā)生時會拋出一個NSUndefinedKeyException的異常,并且應用程序Crash
我們可以重寫下面兩個方法:
//獲取了不存在的key 只需要實現(xiàn)如下方法
- (nullable id)valueForUndefinedKey:(NSString *)key;
//設置了不存在的key 造成崩潰 只需要實現(xiàn)如下方法
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
- 傳參為nil
通常情況下,KVC不允許你要在調(diào)用setValue:屬性值 forKey:(或者keyPath)時對非對象傳遞一個nil的值。因為值類型是不能為nil的。如果你不小心傳了,KVC會調(diào)用setNilValueForKey:方法。這個方法默認是拋出異常,所以一般而言最好還是重寫這個方法。
- (void)setNilValueForKey:(NSString *)key{
NSLog(@"-----設置了nil");
}