iOSKVC 底層原理探索

一,概念

KVC(Key-value coding)鍵值編碼,單看這個(gè)名字可能不太好理解。其實(shí)翻譯一下就很簡(jiǎn)單了,就是指iOS的開發(fā)中,可以允許開發(fā)者通過Key名直接訪問對(duì)象的屬性,或者給對(duì)象的屬性賦值。而不需要調(diào)用明確的存取方法。這樣就可以在運(yùn)行時(shí)動(dòng)態(tài)地訪問和修改對(duì)象的屬性。而不是在編譯時(shí)確定,這也是iOS開發(fā)中的黑魔法之一。很多高級(jí)的iOS開發(fā)技巧都是基于KVC實(shí)現(xiàn)的。

二,KVC在iOS中的定義

無論是Swift還是Objective-C,KVC的定義都是對(duì)NSObject的擴(kuò)展來實(shí)現(xiàn)的(Objective-C中有個(gè)顯式的NSKeyValueCoding類別名,而Swift沒有,也不需要)。所以對(duì)于所有繼承了NSObject的類型,也就是幾乎所有的Objective-C對(duì)象都能使用KVC(一些純Swift類和結(jié)構(gòu)體是不支持KVC的),下面是KVC最為重要的四個(gè)方法

- (nullable id)valueForKey:(NSString *)key;                          //直接通過Key來取值
- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通過Key來設(shè)值
- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通過KeyPath來取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通過KeyPath來設(shè)值

當(dāng)然因?yàn)镵VC 的定義來自于基礎(chǔ)框架的Foundation中,所以沒有開源,也就看不到具體代碼的實(shí)現(xiàn)和原來,所以需要我們借助官方文章來進(jìn)一步學(xué)習(xí)和了解。

當(dāng)然NSKeyValueCoding類別中還有其他的一些方法,下面列舉一些

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

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供屬性值正確性驗(yàn)證的API,它可以用來檢查set的值是否正確、為不正確的值做一個(gè)替換值或者拒絕設(shè)置新值并返回錯(cuò)誤原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//這是集合操作的API,里面還有一系列這樣的API,如果屬性是一個(gè)NSMutableArray,那么可以用這個(gè)方法來返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且沒有KVC無法搜索到任何和Key有關(guān)的字段或者屬性,則會(huì)調(diào)用這個(gè)方法,默認(rèn)是拋出異常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一個(gè)方法一樣,但這個(gè)方法是設(shè)值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法時(shí)面給Value傳nil,則會(huì)調(diào)用這個(gè)方法

- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
//輸入一組key,返回該組key對(duì)應(yīng)的Value,再轉(zhuǎn)成字典返回,用于將Model轉(zhuǎn)到字典。

上面的這些方法在碰到特殊情況或者有特殊需求還是會(huì)用到的,所以也是可以了解一下。后面的代碼示例會(huì)有講到其中的一些方法。
同時(shí)蘋果對(duì)一些容器類比如NSArray或者NSSet等,KVC有著特殊的實(shí)現(xiàn)。建議有基礎(chǔ)的或者英文好的開發(fā)者直接去看蘋果的官方文檔,相信你會(huì)對(duì)KVC的理解更上一個(gè)臺(tái)階。

三,KVC是怎么尋找Key

KVC是怎么使用的,我相信絕大多數(shù)的開發(fā)者都很清楚,我在這里就不再寫簡(jiǎn)單的使用KVC來設(shè)值和取值的代碼了,首先我們來探討KVC在內(nèi)部是按什么樣的順序來尋找key的。


setValue for Key.png

當(dāng)調(diào)用setValue:屬性值 forKey:@”name“的代碼時(shí),底層的執(zhí)行機(jī)制如下:

  • 1 程序優(yōu)先調(diào)用set<Key>:屬性值方法,代碼通過setter方法完成設(shè)置。注意,這里的<key>是指成員變量名,首字母大小寫要符合KVC的命名規(guī)則,下同
  • 2 如果沒有找到setName:方法,KVC機(jī)制會(huì)檢查+ (BOOL)accessInstanceVariablesDirectly方法有沒有返回YES,默認(rèn)該方法會(huì)返回YES,如果你重寫了該方法讓其返回NO的話,那么在這一步KVC會(huì)執(zhí)行setValue:forUndefinedKey:方法,不過一般開發(fā)者不會(huì)這么做。所以KVC機(jī)制會(huì)搜索該類里面有沒有名為_<key>的成員變量,無論該變量是在類接口處定義,還是在類實(shí)現(xiàn)處定義,也無論用了什么樣的訪問修飾符,只在存在以_<key>命名的變量,KVC都可以對(duì)該成員變量賦值。
  • 3 如果該類即沒有set<key>:方法,也沒有_<key>成員變量,KVC機(jī)制會(huì)搜索_is<Key>的成員變量。
  • 4 和上面一樣,如果該類即沒有set<Key>:方法,也沒有_<key>和_is<Key>成員變量,KVC機(jī)制再會(huì)繼續(xù)搜索<key>is<Key>的成員變量。再給它們賦值。
  • 5 如果上面列出的方法或者成員變量都不存在,系統(tǒng)將會(huì)執(zhí)行該對(duì)象的setValue:forUndefinedKey:方法,默認(rèn)是拋出異常。

如果開發(fā)者想讓這個(gè)類禁用KVC里,那么重寫+ (BOOL)accessInstanceVariablesDirectly方法讓其返回NO即可,這樣的話如果KVC沒有找到set<Key>:屬性名時(shí),會(huì)直接用setValue:forUndefinedKey:方法。

3.1 代碼驗(yàn)證賦值過程

  • 1 我們對(duì)一個(gè)類聲明了四個(gè)屬性name,_isName,isName_name,
@interface LGPerson : NSObject{
    @public
    NSString *_isName;
    NSString *name;
    NSString *isName;
    NSString *_name;

}
  • 2 在控制器中我們先實(shí)例化一個(gè)person對(duì)象,從而對(duì)調(diào)用KVC 進(jìn)行賦值過程;
 LGPerson *person = [[LGPerson alloc] init];
   // 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
  [person setValue:@"LG_Cooci" forKey:@"name"]
  • 3 在person類中按先后順序執(zhí)行setter方法,看看執(zhí)行的流程

第一步執(zhí)行setName方法

- (void)setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

打印結(jié)果

2020-10-28 16:36:11.503798+0800 002-KVC取值&賦值過程[8696:180841] -[LGPerson setName:] - LG_Cooci

第二步,注釋掉setName方法再次打印結(jié)果

//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}
//
- (void)_setName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}
- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

打印結(jié)果

2020-10-28 16:41:37.143073+0800 002-KVC取值&賦值過程[8825:184607] -[LGPerson _setName:] - LG_Cooci

第三步注釋掉_setName再次打印

//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}
//
//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

- (void)setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

打印結(jié)果

2020-10-28 16:43:45.832732+0800 002-KVC取值&賦值過程[8883:186519] -[LGPerson setIsName:] - LG_Cooci

第四步,注釋掉setIsName 再次打印結(jié)果

//- (void)setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}
//
//- (void)_setName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

//- (void)setIsName:(NSString *)name{
//    NSLog(@"%s - %@",__func__,name);
//}

- (void)_setIsName:(NSString *)name{
    NSLog(@"%s - %@",__func__,name);
}

可以看到控制臺(tái)沒有任何打印結(jié)果,所以可以得出結(jié)論是- (void)_setIsName:(NSString *)name沒有調(diào)用。

結(jié)論,KVC的set方法調(diào)用順序?yàn)?setKey ->_setKey -> setIsKey

3.2 實(shí)例變量的賦值過程

通過以上的代碼驗(yàn)證,我們知道了實(shí)例變量的set方法調(diào)用流程;當(dāng)我們沒有相應(yīng)的set方法時(shí),我們?cè)俅慰纯聪鄳?yīng)的示例變量賦值過程。

  • 1 我們所有的成員變量進(jìn)行監(jiān)控
  [person setValue:@"LG_Cooci" forKey:@"name"];
     NSLog(@"%@-%@-%@-%@",person->_name,person->_isName,person->name,person->isName);

打印結(jié)果

2020-10-28 16:51:28.428396+0800 002-KVC取值&賦值過程[9071:191676] LG_Cooci-(null)-(null)-(null)

  • 2 注釋掉_name 方法,再次打印
 NSString *_isName;
    NSString *name;
    NSString *isName;
   // NSString *_name;
 // 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
     [person setValue:@"LG_Cooci" forKey:@"name"];
    
     NSLog(@"%@-%@-%@",person->_isName,person->name,person->isName);
    

打印結(jié)果

2020-10-28 16:57:00.847725+0800 002-KVC取值&賦值過程[9199:195725] LG_Cooci-(null)-(null)

  • 3 再次注釋掉_isName 再打印結(jié)果
 // NSString *_isName;
    NSString *name;
    NSString *isName;
   // NSString *_name;

[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@-%@",person->name,person->isName);

打印結(jié)果

2020-10-28 16:58:39.576608+0800 002-KVC取值&賦值過程[9254:197268] LG_Cooci-(null)

  • 4 再次注釋掉name 再次打印結(jié)果
// NSString *_isName;
   // NSString *name;
    NSString *isName;
   // NSString *_name;
[person setValue:@"LG_Cooci" forKey:@"name"];
NSLog(@"%@",person->isName);

打印結(jié)果

2020-10-28 17:00:24.788572+0800 002-KVC取值&賦值過程[9312:198897] LG_Cooci

結(jié)論:在沒有set方法的時(shí)候,KVC 對(duì)成員變量的賦值順序是 _key -> _isKey -> key ->isKey

四,valueForkey 取值的內(nèi)部原理

當(dāng)調(diào)用valueForKey:@”name“的代碼時(shí),KVC對(duì)key的搜索方式不同于setValue:屬性值 forKey:@”name“,其搜索方式如下:


ValueForkey.png
  • 1 首先按get<Key>,<key>,is<Key>的順序方法查找getter方法,找到的話會(huì)直接調(diào)用。如果是BOOL或者Int等值類型, 會(huì)將其包裝成一個(gè)NSNumber對(duì)象。
  • 2 如果上面的getter沒有找到,KVC則會(huì)查找countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外兩個(gè)方法中的一個(gè)被找到,那么就會(huì)返回一個(gè)可以響應(yīng)NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子類),調(diào)用這個(gè)代理集合的方法,或者說給這個(gè)代理集合發(fā)送屬于NSArray的方法,就會(huì)以countOf<Key>,objectIn<Key>AtIndex<Key>AtIndexes這幾個(gè)方法組合的形式調(diào)用。還有一個(gè)可選的get<Key>:range:方法。所以你想重新定義KVC的一些功能,你可以添加這些方法,需要注意的是你的方法名要符合KVC的標(biāo)準(zhǔn)命名方法,包括方法簽名。
  • 3 如果上面的方法沒有找到,那么會(huì)同時(shí)查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果這三個(gè)方法都找到,那么就返回一個(gè)可以響應(yīng)NSSet所的方法的代理集合,和上面一樣,給這個(gè)代理集合發(fā)NSSet的消息,就會(huì)以countOf<Key>enumeratorOf<Key>,memberOf<Key>組合的形式調(diào)用。
  • 4 如果還沒有找到,再檢查類方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默認(rèn)行為),那么和先前的設(shè)值一樣,會(huì)按_<key>,_is<Key>,<key>,is<Key>的順序搜索成員變量名,這里不推薦這么做,因?yàn)檫@樣直接訪問實(shí)例變量破壞了封裝性,使代碼更脆弱。如果重寫了類方法+ (BOOL)accessInstanceVariablesDirectly返回NO的話,那么會(huì)直接調(diào)用valueForUndefinedKey:
  • 5 還沒有找到的話,調(diào)用valueForUndefinedKey:

4.1代碼驗(yàn)證KVC 的get方法順序

Person類中分別實(shí)現(xiàn)getName、name,isName_name方法

- (NSString *)getName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}
  • 1 在控制器中對(duì)相關(guān)的屬性進(jìn)行取值內(nèi)容;
LGPerson *person = [[LGPerson alloc] init];
    
    // 1: KVC - 設(shè)置值的過程 setValue 分析調(diào)用過程
     [person setValue:@"LG_Cooci" forKey:@"name"];
    
    NSLog(@"取值:%@",[person valueForKey:@"name"]);

打印結(jié)果是

2020-10-28 17:10:39.770668+0800 002-KVC取值&賦值過程[9557:205906] 取值:getName

  • 2 注釋掉getName方法,再次打印
//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

- (NSString *)name{
    return NSStringFromSelector(_cmd);
}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

打印結(jié)果

2020-10-28 17:11:43.928335+0800 002-KVC取值&賦值過程[9593:207045] 取值:name

  • 3 再次注釋掉name方法,打印
//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)name{
//    return NSStringFromSelector(_cmd);
//}

- (NSString *)isName{
    return NSStringFromSelector(_cmd);
}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

打印結(jié)果

2020-10-28 17:12:33.678782+0800 002-KVC取值&賦值過程[9622:208078] 取值:isName

  • 4 再注釋isName方法打印
//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)name{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)isName{
//    return NSStringFromSelector(_cmd);
//}

- (NSString *)_name{
    return NSStringFromSelector(_cmd);
}

打印結(jié)果

2020-10-28 17:13:29.096733+0800 002-KVC取值&賦值過程[9654:209106] 取值:_name

結(jié)論,KVC中相關(guān)成員變量的取值方法執(zhí)行順序是 getKey -> Key -> isKey -> _Key

4.2實(shí)例變量的取值過程

在沒有實(shí)現(xiàn)任何get方法的情況下,

//- (NSString *)getName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)name{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)isName{
//    return NSStringFromSelector(_cmd);
//}

//- (NSString *)_name{
//    return NSStringFromSelector(_cmd);
//}
  • 1 我們?cè)诳刂破髦袑?duì)所有的成員變量進(jìn)行一個(gè)賦值操作
person->_name = @"_name";
NSLog(@"取值:%@",[person valueForKey:@"name"]);

打印結(jié)果是

2020-10-28 17:25:02.021100+0800 002-KVC取值&賦值過程[9964:217084] 取值:_name

  • 2 注釋掉_name 再次打印結(jié)果
 NSString *_isName;
    NSString *name;
    NSString *isName;
    //NSString *_name;
person->_isName = @"_isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);

打印結(jié)果

2020-10-28 17:29:25.962006+0800 002-KVC取值&賦值過程[10103:221196] 取值:_isName

  • 3 再次注釋掉_isName 再次打印
//  NSString *_isName;
    NSString *name;
    NSString *isName;
    //NSString *_name;
 person->name = @"name";
  NSLog(@"取值:%@",[person valueForKey:@"name"]);

打印結(jié)果

2020-10-28 17:35:22.496774+0800 002-KVC取值&賦值過程[10281:225879] 取值:name

  • 4 再次注釋掉name 在打印結(jié)果
 //  NSString *_isName;
   // NSString *name;
    NSString *isName;
    //NSString *_name;
person->isName = @"isName";
NSLog(@"取值:%@",[person valueForKey:@"name"]);

打印結(jié)果是

2020-10-28 17:37:17.713126+0800 002-KVC取值&賦值過程[10352:227627] 取值:isName

結(jié)論;成員變量的取值過程是 _Key -> _isKey -> Key ->isKey

八,總結(jié)

以上就是相關(guān)的KVC的原理,通過以上的學(xué)習(xí)和總結(jié),了解了KVC 的賦值過程和取值過程的順序,執(zhí)行的流程已經(jīng)處理異常的步驟,自己對(duì)KVC的理解更進(jìn)一步。有不足的地方希望各位大神多多指正。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容