在技術論壇中看到一則很有意思的KVC案例:
@interface Person : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSInteger age; @end Person *person = [Person new]; person.name = @"Tom"; person.age = 10; [person setValue:@"100" forKey:@"age"];//此處賦值為字串,類中屬性為Integer
第一反應是崩潰,因為OC是型別敏感的。可是自己實現並列印後的結果出於意料,沒有崩潰且賦值成功。所以有了深入瞭解KVC的內部實現的想法!
key-value-coding:鍵值編碼,一種可以通過鍵名間接存取和賦值物件屬性的機制
KVC是通過NSObject、NSArray、NSDictionary等的類別來實現的
主要方法包括一下幾個:
- (nullable id)valueForKey:(NSString *)key; - (void)setValue:(nullable id)value forKey:(NSString *)key; - (void)setNilValueForKey:(NSString *)key; - (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key; - (nullable id)valueForUndefinedKey:(NSString *)key;
那麼上面的案例中的- (void)setValue:(nullable id)value forKey:(NSString *)key;是怎樣的執行過程呢?藉助反組合工具獲得Foundation.framework部分原始碼(為了解決和系統API衝突問題增加字首_d,NS替換為DS),以此分析KVC執行過程。(流程中的邊界判斷等已經忽略,如想了解可以參考原始碼,本文只探究主流程。)
+ (DSKeyValueSetter *)_d_createValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { DSKeyValueSetter *setter = nil; char key_cstr_upfirst[key_cstr_len + 1]; key_cstr[key_cstr_len + 1]; ... Method method = NULL; //按順序尋找set<Key>,_set<Key>,setIs<Key>。找到後則生成對應的seter if ((method = DSKeyValueMethodForPattern(self, "set%s:", key_cstr_upfirst)) || (method = DSKeyValueMethodForPattern(self, "_set%s:", key_cstr_upfirst)) || (method = DSKeyValueMethodForPattern(self, "setIs%s:", key_cstr_upfirst)) ) { //生成Method:包含selector,IMP。返回和引數型別字串 setter = [[DSKeyValueMethodSetter alloc] initWithContainerClassID:containerClassID key:key method:method]; } else if ([self accessInstanceVariablesDirectly]) {//如果沒有找到對應的存取器方且工廠方法accessInstanceVariablesDirectly == ture ,則按照順序查詢查詢成員變數_<key>,_is<Key>,<key>,is<Key>(注意key的首字母大小寫,查詢到則生成對應的setter) Ivar ivar = NULL; if ((ivar = DSKeyValueIvarForPattern(self, "_%s", key_cstr)) || (ivar = DSKeyValueIvarForPattern(self, "_is%s", key_cstr_upfirst)) || (ivar = DSKeyValueIvarForPattern(self, "%s", key_cstr)) || (ivar = DSKeyValueIvarForPattern(self, "is%s", key_cstr_upfirst)) ) { setter = [[DSKeyValueIvarSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar]; } } ... return setter; }
查詢順序如下:
則查詢順序如下:_,_is,,is
查詢不到則呼叫valueForUndefinedKey並丟擲異常
+ (DSKeyValueSetter *)_d_createOtherValueSetterWithContainerClassID:(id)containerClassID key:(NSString *)key { return [[DSKeyValueUndefinedSetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self]; } //構造方法確定方法編號 d_setValue:forUndefinedKey: 和方法指標IMP _DSSetValueAndNotifyForUndefinedKey - (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key containerIsa:(Class)containerIsa { ... return [super initWithContainerClassID:containerClassID key:key implementation:method_getImplementation(class_getInstanceMethod(containerIsa, @selector(d_setValue:forUndefinedKey:))) selector:@selector(d_setValue:forUndefinedKey:) extraArguments:arguments count:1]; }
基本的存取器方法、變數的查詢和例外處理已經清楚的知道了。那麼上面的例子是如何出現的呢?明明傳入的是字串,最後賦值的時候轉變為存取器方法所對應的型別?繼續刨根問底!
DSKeyValueSetter物件已經生成,即確定了傳送訊息的物件object、存取器方法名SEL、存取器函數指標IMP、以及使用KVC時傳入的Key和Value。下面進入方法呼叫階段:_DSSetUsingKeyValueSetter(self,setter, value);
IMP指標為_DSSetIntValueForKeyWithMethod其定義如下:之所以有文章開頭提到的效果就是這裡起了作用,在IMP呼叫的時候做了[value valueGetSelectorName],將對應的NSNumber轉換為簡單資料型別。這裡是intValue。
void _DSSetIntValueForKeyWithMethod(id object, SEL selector,id value, NSString *key, Method method) {// object:person selector:setAge: value:@(100) key:age method:selector + IMP + 返回型別和引數型別 即_extraArgument2,其在第一步查詢到存取器方法後生成 __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, int, intValue); } #define __DSSetPrimitiveValueForKeyWithMethod(object, selector, value, key, method, valueType, valueGetSelectorName) do {\ if (value) {\ void (*imp)(id,SEL,valueType) = (void (*)(id,SEL,valueType))method_getImplementation(method);\ imp(object, method_getName(method), [value valueGetSelectorName]);\呼叫person的setAge:方法。引數為100 }\ else {\ [object setNilValueForKey:key];\ }\ }while(0) //如果第一步中沒有找到存取器方法只找到了成員變數則直接執行賦值操作 void _DSSetIntValueForKeyInIvar(id object, SEL selector, id value, NSString *key, Ivar ivar) { if (value) { *(int *)object_getIvarAddress(object, ivar) = [value intValue]; } else { [object setNilValueForKey:key]; } }
起始問題完美解決!執行流程如下:
+ (DSKeyValueGetter *)_d_createValueGetterWithContainerClassID:(id)containerClassID key:(NSString *)key { DSKeyValueGetter * getter = nil; ... Method getMethod = NULL; if((getMethod = DSKeyValueMethodForPattern(self,"get%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"%s",keyCStr)) || (getMethod = DSKeyValueMethodForPattern(self,"is%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"_get%s",keyCStrUpFirst)) || (getMethod = DSKeyValueMethodForPattern(self,"_%s",keyCStr))) { getter = [[DSKeyValueMethodGetter alloc] initWithContainerClassID:containerClassID key:key method:getMethod]; }// 查詢對應的存取器方法 ... else if([self accessInstanceVariablesDirectly]) {//查詢屬性 Ivar ivar = NULL; if((ivar = DSKeyValueIvarForPattern(self, "_%s", keyCStr)) || (ivar = DSKeyValueIvarForPattern(self, "_is%s", keyCStrUpFirst)) || (ivar = DSKeyValueIvarForPattern(self, "%s", keyCStr)) || (ivar = DSKeyValueIvarForPattern(self, "is%s", keyCStrUpFirst)) ) { getter = [[DSKeyValueIvarGetter alloc] initWithContainerClassID:containerClassID key:key containerIsa:self ivar:ivar]; } } } if(!getter) { getter = [self _d_createValuePrimitiveGetterWithContainerClassID:containerClassID key:key]; } return getter; }
存取器方法生成IMP - (id)initWithContainerClassID:(id)containerClassID key:(NSString *)key method:(Method)method { NSUInteger methodArgumentsCount = method_getNumberOfArguments(method); NSUInteger extraAtgumentCount = 1; if(methodArgumentsCount == 2) { char *returnType = method_copyReturnType(method); IMP imp = NULL; switch (returnType[0]) { ... case 'i': { imp = (IMP)_DSGetIntValueWithMethod; } break; ... free(returnType); if(imp) { void *arguments[3] = {0}; if(extraAtgumentCount > 0) { arguments[0] = method; } return [super initWithContainerClassID:containerClassID key:key implementation:imp selector:method_getName(method) extraArguments:arguments count:extraAtgumentCount]; } }
單步偵錯後可以看到具體的IMP型別
定義如下:
NSNumber * _DSGetIntValueWithMethod(id object, SEL selctor, Method method) {// return [[[NSNumber alloc] initWithInt: ((int (*)(id,SEL))method_getImplementation(method))(object, method_getName(method))] autorelease]; }
取值呼叫如下:
NSNunber:
NSValue
修改陣列中物件的屬性
[array valueForKeyPath:@」uppercaseString」]
利用KVC可以批次修改屬性的成員變數值
求和,平均數,最大值,最小值
NSNumbersum= [array valueForKeyPath:@」@sum.self」];
NSNumberavg= [array valueForKeyPath:@」@avg.self」];
NSNumbermax= [array valueForKeyPath:@」@max.self」];
NSNumbermin= [array valueForKeyPath:@」@min.self」];
經過上面的分析可以明白KVC的真正執行流程。下面結合日常工程中的實際應用來優雅的處理資料篩選問題。使用KVC處理可以減少大量for的使用並增加程式碼可讀性和健壯性。
如圖所示:
專案中的細節如下:修改拒收數量時更新總妥投數和總拒收數、勾選明細更新總妥投數和總拒收數、全選、清空、反選。如果用通常的做法是每次操作都要回圈去計算總數和記錄選擇狀態。下面是採用KVC的實現過程。
模型涉及:
@property (nonatomic,copy)NSString* skuCode; @property (nonatomic,copy)NSString* goodsName; @property (nonatomic,assign)NSInteger totalAmount; @property (nonatomic,assign)NSInteger rejectAmount; @property (nonatomic,assign)NSInteger deliveryAmount; ///單選用 @property (nonatomic, assign) BOOL selected;
1)更新總數
- (void)updateDeliveryInfo { //總數 NSNumber *allDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.totalAmount"]; //妥投數 NSNumber *allRealDeliveryAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.deliveryAmount"]; //拒收數 NSNumber *allRejectAmount = [self.orderDetailModel.deliveryGoodsDetailList valueForKeyPath:@"@sum.rejectAmount"]; }
2)全選
[self.orderDetailModel.deliveryGoodsDetailList setValue:@(YES) forKeyPath:@」selected」];
3)清空
[self.orderDetailModel.deliveryGoodsDetailList setValue:@(NO) forKeyPath:@」selected」];
4)反選
NSPredicate *selectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(YES)]; NSArray *selectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:selectedPredicate]; NSPredicate *unSelectedPredicate = [NSPredicate predicateWithFormat:@"selected == %@",@(NO)]; NSArray *unSelectedArray = [self.orderDetailModel.deliveryGoodsDetailList filteredArrayUsingPredicate:unSelectedPredicate]; [selectedArray setValue:@(NO) forKeyPath:@"selected"]; [unSelectedArray setValue:@(YES) forKeyPath:@"selected"];
KVC在處理簡單資料型別時會經過資料封裝和拆裝並轉換為對應的資料型別。通過KVC的特性我們可以在日常使用中更加優雅的對資料進行篩選和處理。優點如下:可閱讀性更高,健壯性更好。