首页 > 其他分享 >KVC原理与数据筛选

KVC原理与数据筛选

时间:2022-11-29 10:57:13浏览次数:78  
标签:KVC self id NSString key 原理 筛选 method

作者:宋宏帅

1 前言

在技术论坛中看到一则很有意思的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的内部实现的想法!

2 什么是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;

 

3 KVC执行分析

那么上面的案例中的- (void)setValue:(nullable id)value forKey:(NSString *)key;是怎样的执行过程呢?借助反汇编工具获得Foundation.framework部分源码(为了解决和系统API冲突问题增加前缀_d,NS替换为DS),以此分析KVC执行过程。(流程中的边界判断等已经忽略,如想了解可以参考源码,本文只探究主流程。)

3.1 设置属性

3.1.1 查找访问器方法或成员变量
+ (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;
}

 

查找顺序如下:

  1. 查找访问器方法:set,_set,setIs
  2. 如果步骤1中没找到对应的方法且 accessInstanceVariablesDirectly == YES

则查找顺序如下:_,_is,,is
查找不到则调用valueForUndefinedKey并抛出异常

3.1.2 生成setter
+ (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];
}

 

3.1.3 赋值

基本的访问器方法、变量的查找和异常处理已经清楚的知道了。那么上面的例子是如何出现的呢?明明传入的是字符串,最后赋值的时候转变为访问器方法所对应的类型?继续刨根问底!

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];
}
}

 

起始问题完美解决!执行流程如下:

3.2 取值

3.2.1 查找访问器方法或成员变量
+ (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;
}

 

  1. 按照get,,is,_的顺序查找成员方法
  2. 如果1.没有找到对应的方法且accessInstanceVariablesDirectly==YES,则继续查找成员变量,查找顺序为_,_is,,is
  3. 如果1,2没有找到对应的方法和属性则调用 valueForUndefinedKey:并抛出异常
3.2.2 如上步骤没定位到访问器方法或成员变量则走下面的流程生成对应的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];
}

 

3.2.3 取值

取值调用如下:

4 简单数据类型KVC包装和拆装关系

NSNunber:

NSValue

5 KVC高级

修改数组中对象的属性
[array valueForKeyPath:@”uppercaseString”]
利用KVC可以批量修改属性的成员变量值

求和,平均数,最大值,最小值
NSNumbersum= [array valueForKeyPath:@”@sum.self”];
NSNumberavg= [array valueForKeyPath:@”@avg.self”];
NSNumbermax= [array valueForKeyPath:@”@max.self”];
NSNumbermin= [array valueForKeyPath:@”@min.self”];

6 数据筛选

经过上面的分析可以明白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"];

 

7 总结

KVC在处理简单数据类型时会经过数据封装和拆装并转换为对应的数据类型。通过KVC的特性我们可以在日常使用中更加优雅的对数据进行筛选和处理。优点如下:可阅读性更高,健壮性更好。

标签:KVC,self,id,NSString,key,原理,筛选,method
From: https://www.cnblogs.com/Jcloud/p/16934749.html

相关文章

  • mybatis SelectKey标签执行原理
    SelectKey标签在mybatis中可以配置成在主sql执行之前和执行之后两种时机进行执行。mybatis执行sql时一次会涉及到这些对象sqlSession-->Executor-->StatementHandler其......
  • 软件离线许可(License)实现原理
    我们经常使用各种开发软件,比如IntelliJIDEA、Navicat、VisualStudio等,这些软件都有一个特点,就是要收费。一般是我们需要去购买一个许可,然后输入这个许可到软件里就能够......
  • 图解Linux进程间通信实现原理(1)
    为Linux应用程序的开发人员,对Linux的进程间通信方式肯定是了如指掌,平时的开发中应该会大量的使用到。当你迅速的在键盘上按下【CTRL+C】终止掉一个正在运行中的命令时,你有没......
  • 大数据下一代变革之必研究数据湖技术Hudi原理实战双管齐下-下
    @目录集成Spark开发Spark编程读写示例DeltaStreamer集成Flink环境准备sql-clent使用启动插入数据流式读取Bucket索引HudiCatalog集成Spark开发Spark编程读写示例通过ID......
  • 「ReactNative」原理剖析
    「ReactNative」原理剖析不太帅的程序员前端程序员、搬砖coding、持续分享优质文章! 28人赞同了该文章​展开目录 一、......
  • 操作系统原理之线程的分类:用户级线程和内核级线程
    本篇文章作为Java并发编程的前置重点知识,有助于理解乐观锁和悲观锁。 这个世界上只有两种锁-->乐观锁:指的是在操作数据的时候非常乐观,乐观地认为别人不会同时修改数据,......
  • Cross-Origin Resource Sharing(CORS)详解,CORS详解,CORS原理分析
    Cross-OriginResourceSharing(CORS)详解,CORS详解,CORS原理分析Cross-OriginResourceSharing通常简称为:CORS它是一种机制,这个机制使用了一个额外的HTTP响应头来赋予当前us......
  • HashMap实现原理和自动扩容
    HashMap实现原理:JDK1.7:数组+单向链表(头插)在并发情况下头插可能出现循环链表(死循环)问题。原因:因为头插,在新数组中链表的元素顺序发生了变化,  如上图,假设线程1在扩......
  • vue3 第二天vue响应式原理以及ref和reactive区别
    前言:前天我们学了ref和reactive,提到了响应式数据和Proxy,那我们今天就来了解一下,vue3的响应式在了解之前,先复习一下之前vue2的响应式原理vue2的响应式:原理:对......
  • 【计算机组成原理】01计算机系统概述
    1.0_你好,我是计算机组成原理1.1_计算机的发展什么是计算机系统计算机系统=硬件+软件软件系统软件——用来管理整个计算机系统Eg:操作系统、数据库管理系统(DBMS)、标......