KVC与Runtime结合使用(案例)及其底层原理
一、KVC 的用法和实践
用法
KVC(Key-value coding)键值编码,顾名思义。额,简单来说,是可以通过对象属性名称(Key)直接给属性值(value)编码(coding)“编码”可以理解为“赋值”
。这样可以免去我们调用getter和setter方法,从而简化我们的代码,也可以用来修改系统控件内部属性,KVC是KVO、Core Data、CocoaBindings的技术基础,他们都是利用了OC的动态性
KVC用法
- setValue:forKey:(为对象的属性赋值)
- setValue: forKeyPath:(为对象的属性赋值(包含了setValue:forKey:的功能,并且还可以对对象内的类的属性进行赋值))
- valueForKey:(根据key取值)
- valueForKeyPath:(根据keyPath取值)
- setValuesForKeysWithDictionary:(对模型进行一次性赋值)
为什么可以用NSNumber来接收int、float的数据类型?
因为:使用valueForKey:时,KVC会自动将标量值(int、float、struct等)翻入NSNumber或NSValue中包装成一个对象,然后返回。因此,KVC有自动包装功能。
例如:生成一个这样子的对象Person
person.h
@class Car;
@interface Person : NSObject
@property (nonatomic,copy) NSString *name;
@property (nonatomic,strong)Car *car;
@end
Car.h
@interface Car : NSObject
@property (nonatomic,strong) NSNumber *price;
@end
在ViewController.m中调用
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Person *person=[[Person alloc]init];
[person setValue:@"lxh" forKey:@"name"];
float price=100.0;
Car *car=[[Car alloc]init];
person.car=car;
[person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];
NSLog(@"%@",person.name); NSLog(@"%f",car.price.floatValue);
}
注意点:
- 在Person中我仅仅只是声明了@class Car,而没有引用#import "Car.h",然后在ViewController.m中便可以对其进行: [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];这样子的赋值。所以说明KVC会去自动查找Car类进行赋值
- - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;你会发现value的值必须是id,也就是说不能传基本数据类型,必须是指针类型的变量。
key和keyPath的区别
keyPath方法是集成了key的所有功能,也就是说对一个对象的一般属性进行赋值、取值,两个方法是通用的,都可以实现。但是对对象中的对象进的属性行赋值,只有keyPath能够实现。
setValuesForKeysWithDictionary:的巧妙使用(字典转模型)
-(instancetype)initWithDict:(NSDictionary *)dict{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
注意点:
- 字典转模型的时候,字典中的某一个key一定要在模型中有对应的属性
- 如果一个模型中包含了另外的模型对象,是不能直接转化成功的。
- 通过kvc转化模型中的模型,也是不能直接转化成功的
- 底层还是调用了setValue: forKey:
使用例子
(1)修改系统控件内部属性(runtime + KVC)
例如,界面设计图是这样的
怎么感觉有点不同,这UIPageControl
怎么跟我平常用的不一样?平常不都是这样的??如下图
首先想到的肯定是,查看UIPageControl
的头文件,如下
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIPageControl : UIControl @property(nonatomic) NSInteger numberOfPages; // default is 0
@property(nonatomic) NSInteger currentPage; // default is 0. value pinned to 0..numberOfPages-1 @property(nonatomic) BOOL hidesForSinglePage; // hide the the indicator if there is only one page. default is NO @property(nonatomic) BOOL defersCurrentPageDisplay; // if set, clicking to a new page won't update the currently displayed page until -updateCurrentPageDisplay is called. default is NO
- (void)updateCurrentPageDisplay; // update page display to match the currentPage. ignored if defersCurrentPageDisplay is NO. setting the page value directly will update immediately - (CGSize)sizeForNumberOfPages:(NSInteger)pageCount; // returns minimum size required to display dots for given page count. can be used to size control if page count could change @property(nullable, nonatomic,strong) UIColor *pageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR;
@property(nullable, nonatomic,strong) UIColor *currentPageIndicatorTintColor NS_AVAILABLE_IOS(6_0) UI_APPEARANCE_SELECTOR; @end
不够用啊兄弟。能不能给我个可以赋值UIImage
对象的属性?看来正常途径使用系统的控件是设不了了!如何解呢 ?
第一种方式:自定义UIPageControl 第二种方式:通过runtime遍历出UIPageControl
所有属性(包括私有成员属性,runtime确实很强大)
直接用第二种吧 第一种有兴趣的可以自己试试!
使用runtime遍历UIPageControl
结果如下打印:
-- ::26.161 TenMinDemo[:] UIPageControl -> _lastUserInterfaceIdiom = q
-- ::26.161 TenMinDemo[:] UIPageControl -> _indicators = @"NSMutableArray"
-- ::26.161 TenMinDemo[:] UIPageControl -> _currentPage = q
-- ::26.161 TenMinDemo[:] UIPageControl -> _displayedPage = q
-- ::26.162 TenMinDemo[:] UIPageControl -> _pageControlFlags = {?="hideForSinglePage"b1"defersCurrentPageDisplay"b1}
-- ::26.162 TenMinDemo[:] UIPageControl -> _currentPageImage = @"UIImage" // 当前选中图片
-- ::26.162 TenMinDemo[:] UIPageControl -> _pageImage = @"UIImage" // 默认图片
-- ::26.162 TenMinDemo[:] UIPageControl -> _currentPageImages = @"NSMutableArray"
-- ::26.162 TenMinDemo[:] UIPageControl -> _pageImages = @"NSMutableArray"
-- ::26.162 TenMinDemo[:] UIPageControl -> _backgroundVisualEffectView = @"UIVisualEffectView"
-- ::26.162 TenMinDemo[:] UIPageControl -> _currentPageIndicatorTintColor = @"UIColor"
-- ::26.163 TenMinDemo[:] UIPageControl -> _pageIndicatorTintColor = @"UIColor"
-- ::26.163 TenMinDemo[:] UIPageControl -> _legibilitySettings = @"_UILegibilitySettings"
-- ::26.163 TenMinDemo[:] UIPageControl -> _numberOfPages = q
结果非常满意,果然找到我想要的图片设置属性
然后通过KVC设置自定义图片,实现了效果,代码如下
UIPageControl *pageControl = [[UIPageControl alloc] init];
[pageControl setValue:[UIImage imageNamed:@"home_slipt_nor"] forKeyPath:@"_pageImage"];
[pageControl setValue:[UIImage imageNamed:@"home_slipt_pre"] forKeyPath:@"_currentPageImage"];
(2) 在xib/Storyboard中,也可以使用KVC,下面是在xib中使用KVC把图片边框设置成圆角
(3)id
{
"id" : "tripleCC",
"age" : "",
"address" : "杭州",
"schooll" : "HDU"
...
}
其中的id是什么?是Objective-C关键字,也就是说我定义以下属性会出现警告:
@property (nonatomic, strong) NSString *id;
虽然可以使用以下方法,对模型中的成员变量进行统一设置,但是出现警告总归是不好的:
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues;
底层会调用 setValue:forKey:
既然这样,可以选择手动一个个去实现。但是这样在数据少的时候可以试试,在数据比较多时就不太现实了,程序的可扩展性也不好。
两种解决方法:
方式1.重写setValue:forKey:
setValuesForKeysWithDictionary:的底层是调用setValue:forKey:的,所以可以考虑重写这个方法,并且判断其key是id时,手动转换成模型的成员变量名,这里假设把id对应成以下属性:
@property (nonatomic, strong) NSString *ID;
有了对应的属性名后,就可以重写底层方法了
- (void)setValue:(id)value forKey:(NSString *)key
{
if ([key isEqualToString:@"id"]) {
[self setValue:value forKeyPath:@"ID"];
}else{
[super setValue:value forKey:key]; }
}
这样,当使用setValuesForKeysWithDictionary:就不会出现模型中找不到对应的成员变量的错误了。
方式2.使用runtime
由于需要针对所有模型使用,可以将其设置为NSObject分类
// dict -> 资源文件提供的字典
// mapDict -> 提供的key映射(实际变量名:资源文件key)
+ (instancetype)objcWithDict:(NSDictionary *)dict mapDict:(NSDictionary *)mapDict
{
id objc = [[self alloc] init]; // 遍历模型中成员变量
unsigned int outCount = ;
Ivar *ivars = class_copyIvarList(self, &outCount); for (int i = ; i < count; i++) {
Ivar ivar = ivars[i]; // 成员变量名称
NSString *ivarName = @(ivar_getName(ivar)); // 获取出来的是`_`开头的成员变量名,需要截取`_`之后的字符串
ivarName = [ivarName substringFromIndex:]; id value = dict[ivarName];
// 由外界通知内部,模型中成员变量名对应字典里面的哪个key
// ID -> id
if (value == nil) {
if (mapDict) {
NSString *keyName = mapDict[ivarName]; value = dict[keyName];
}
}
[objc setValue:value forKeyPath:ivarName];
}
return objc;
}
使用方法:
+ (instancetype)itemWithDict:(NSDictionary *)dict
{
// 传入key和实例变量名的映射字典@{@"ID":@"id"}
TPCItem *item = [TPCItem objcWithDict:dict mapDict:@{@"ID":@"id"}]; return item;
}
二、底层原理的分析
KVC的赋值原理
setValue:forKey:赋值原理如下:
- 去模型中查找有没有对应的setter方法:例如:setIcon方法,有就直接调用这个setter方法给模型这个属性赋值[self setIcon:dic[@"icon"]];
- 如果找不到setter方法,接着就会去寻找有没有icon属性,如果有,就直接访问模型中的icon属性,进行赋值,icon=dict[@"icon"];
- 如果找不到icon属性,接着又会去寻找_icon属性,如果有,直接进行赋值_icon=dict[@"icon"];
- 如果都找不到就会报错:[<Flag 0X7fb74bc7a2c0> setValue:forUndefinedKey:]
如果对某个类,不允许使用KVC,可以通过设置 accessInstanceVariablesDirectly 控制。
// 在该类的内部,重写此方法,外部使用KVC时,禁用没有写set get 方法的属性值。
// 注意:对于 @property 定义的属性可以 KVC+
-(BOOL)accessInstanceVariablesDirectly{
return NO;
}- 赋值检查
// 在类的内部,进行检查,不符合要求 返回NO ,提供外部参考。
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError{
if ([inKey isEqualToString:@"colors"] && [*ioValue isKindOfClass:[NSArray class]]) {
return YES;
} else {
return NO;
}
}
//用法:
// 外部 使用时,先判断是否符合要求,再使用KVC。
NSError *error;
NSString *apoint = @"name";
if ([aPerson validateValue:&apoint forKey:@"_colors" error:&error]) {
NSLog(@"可以赋值 apoint");
[aPerson setValue:apoint forKey:@"_colors"];
} else {
NSLog(@"不可以赋值 apoint");
NSLog(@"%@",error.debugDescription);
}
KVC内部的实现
比如说如下的一行KVC的代码:
[site setValue:@"sitename" forKey:@"name"];
就会被编译器处理成:
SEL sel = sel_get_uid ("setValue:forKey:");
IMP method = objc_msg_lookup (site->isa,sel);
method(site, sel, @"sitename", @"name");
这下KVC内部的实现就很清楚的清楚了:一个对象在调用setValue的时候,(1)首先根据方法名找到运行方法的时候所需要的环境参数。(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。(3)再直接查找得来的具体的方法实现。
KVC与Runtime结合使用(案例)及其底层原理的更多相关文章
- springAop:Aop(Xml)配置,Aop注解配置,spring_Aop综合案例,Aop底层原理分析
知识点梳理 课堂讲义 0)回顾Spring体系结构 Spring的两个核心:IoC和AOP 1)AOP简介 1.1)OOP开发思路 OOP规定程序开发以类为模型,一切围绕对象进行,OOP中完成某个任务 ...
- Spring_day01--课程安排_Spring概念_IOC操作&IOC底层原理&入门案例_配置文件没有提示问题
Spring_day01 Spring课程安排 今天内容介绍 Spring概念 Spring的ioc操作 IOC底层原理 IOC入门案例 配置文件没有提示问题 Spring的bean管理(xml方式) ...
- iOS底层原理总结 - 探寻block的本质(一)
面试题 block的原理是怎样的?本质是什么? __block的作用是什么?有什么使用注意点? block的属性修饰词为什么是copy?使用block有哪些使用注意? block在修改NSMu ...
- KVO-基本使用方法-底层原理探究-自定义KVO-对容器类的监听
书读百变,其义自见! 将KVO形式以代码实现呈现,通俗易懂,更容易掌握 :GitHub -链接如果失效请自动搜索:https://github.com/henusjj/KVO_base 代码中有详 ...
- JS原型链与instanceof底层原理
一.问题: instanceof 可以判断一个引用是否属于某构造函数: 另外,还可以在继承关系中用来判断一个实例是否属于它的父类型. 老师说:instanceof的判断逻辑是: 从当前引用的proto ...
- SpringBoot集成MyBatis底层原理及简易实现
MyBatis是可以说是目前最主流的Spring持久层框架了,本文主要探讨SpringBoot集成MyBatis的底层原理.完整代码可移步Github. 如何使用MyBatis 一般情况下,我们在Sp ...
- 红黑树规则,TreeSet原理,HashSet特点,什么是哈希值,HashSet底层原理,Map集合特点,Map集合遍历方法
==学习目标== 1.能够了解红黑树 2.能够掌握HashSet集合的特点以及使用(特点以及使用,哈希表数据结构) 3.能够掌握Map集合的特点以及使用(特点,常见方法,Map集合的遍历) 4.能够掌 ...
- IoC容器(底层原理)
IoC(概念和原理) 1,什么是IoC (1)控制反转,把对象创建和对象之间的调用过程,交给Spring进行管理 (2)使用IoC目的:为了降低耦合度 (3)做入门案例就是IoC实现 2,IoC底层原 ...
- Neo4j图数据库简介和底层原理
现实中很多数据都是用图来表达的,比如社交网络中人与人的关系.地图数据.或是基因信息等等.RDBMS并不适合表达这类数据,而且由于海量数据的存在,让其显得捉襟见肘.NoSQL数据库的兴起,很好地解决了海 ...
随机推荐
- phalcon的url大小写的问题
一开始我以为url的大小写是不区分的,实际上调试时是可以发现获取到的url是大小写是和请求时一致, 所谓的没区分,只是服务器或者相应的代码做的处理. 在phalcon里如果路由是api/test,则会 ...
- 在Window下安装解压版的mysql 5.7.11
今天由于要在windows下学习Kettle,因此在Windows下安装了mysql 5.7.11,本来是没什么大问题的,但是在启动服务时还是出了点问题,服务老是启动不了: (一)解压到安装路径: ...
- Android——子线程操作主线程
子线程不能直接操作主线程 UI线程 //水平进度条 public void jdt1_onclick(View view) { final ProgressDialog pd = new Progre ...
- 在XP系统下如何访问win10共享的打印机
< > 找到 GUEST 用户,添加即可. 2. Win10 共享本地打印机 右击要共享的打印机,共享选项卡,设置共享名,这个共享很重要,要记住,尽量设置简单点.IP + 共享名就是网络打 ...
- C++构造函数后面的冒号
构造函数后加冒号是初始化表达式:有四种情况下应该使用初始化表达式来初始化成员:1:初始化const成员2:初始化引用成员3:当调用基类的构造函数,而它拥有一组参数时 4:当调用成员类的构造函数,而它拥 ...
- hadoop job解决大数据量关联时数据倾斜的一种办法
转自:http://www.cnblogs.com/xuxm2007/archive/2011/09/01/2161929.html http://www.geminikwok.com/2011/04 ...
- Python如何输出包含在对象中的中文字符?
>>> bb = {'classes': ['\xe5\xb0\x96\xe6\xa4\x92\xe5\x9c\x9f\xe8\xb1\x86\xe4\xb8\x9d', '\xe5 ...
- CentOS查看你是否有USB 3.0端口
近来的大多数的新计算机都有了USB 3.0接口了.但是你怎么知道你的计算机有没有USB 3.0接口?这篇短文中,我们会告诉如何在Linux下知道你的系统上有USB 3还是USB3接口. 在Linux终 ...
- Unity中坐标系转换方法
前言 本篇文章主要是参考<Unity API 解析>---陈泉宏. 这是本人在学校图书馆找到一本书,主要介绍的就是常用的类,比较实用,没有冗余的地方.在此推荐一下这本书! 一.Screen ...
- c#后台常用知识
生成如:2015-10-25T12:12:12格式的时间 DateTime.Now.ToString("s") 非asp.net mvc环境下对url编码 (HttpUtility ...