一、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);
}

注意点:

  1. 在Person中我仅仅只是声明了@class Car,而没有引用#import "Car.h",然后在ViewController.m中便可以对其进行: [person setValue:[NSNumber numberWithFloat:price] forKeyPath:@"car.price"];这样子的赋值。所以说明KVC会去自动查找Car类进行赋值
  2. - (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结合使用(案例)及其底层原理的更多相关文章

  1. springAop:Aop(Xml)配置,Aop注解配置,spring_Aop综合案例,Aop底层原理分析

    知识点梳理 课堂讲义 0)回顾Spring体系结构 Spring的两个核心:IoC和AOP 1)AOP简介 1.1)OOP开发思路 OOP规定程序开发以类为模型,一切围绕对象进行,OOP中完成某个任务 ...

  2. Spring_day01--课程安排_Spring概念_IOC操作&IOC底层原理&入门案例_配置文件没有提示问题

    Spring_day01 Spring课程安排 今天内容介绍 Spring概念 Spring的ioc操作 IOC底层原理 IOC入门案例 配置文件没有提示问题 Spring的bean管理(xml方式) ...

  3. iOS底层原理总结 - 探寻block的本质(一)

        面试题 block的原理是怎样的?本质是什么? __block的作用是什么?有什么使用注意点? block的属性修饰词为什么是copy?使用block有哪些使用注意? block在修改NSMu ...

  4. KVO-基本使用方法-底层原理探究-自定义KVO-对容器类的监听

    书读百变,其义自见! 将KVO形式以代码实现呈现,通俗易懂,更容易掌握 :GitHub   -链接如果失效请自动搜索:https://github.com/henusjj/KVO_base 代码中有详 ...

  5. JS原型链与instanceof底层原理

    一.问题: instanceof 可以判断一个引用是否属于某构造函数: 另外,还可以在继承关系中用来判断一个实例是否属于它的父类型. 老师说:instanceof的判断逻辑是: 从当前引用的proto ...

  6. SpringBoot集成MyBatis底层原理及简易实现

    MyBatis是可以说是目前最主流的Spring持久层框架了,本文主要探讨SpringBoot集成MyBatis的底层原理.完整代码可移步Github. 如何使用MyBatis 一般情况下,我们在Sp ...

  7. 红黑树规则,TreeSet原理,HashSet特点,什么是哈希值,HashSet底层原理,Map集合特点,Map集合遍历方法

    ==学习目标== 1.能够了解红黑树 2.能够掌握HashSet集合的特点以及使用(特点以及使用,哈希表数据结构) 3.能够掌握Map集合的特点以及使用(特点,常见方法,Map集合的遍历) 4.能够掌 ...

  8. IoC容器(底层原理)

    IoC(概念和原理) 1,什么是IoC (1)控制反转,把对象创建和对象之间的调用过程,交给Spring进行管理 (2)使用IoC目的:为了降低耦合度 (3)做入门案例就是IoC实现 2,IoC底层原 ...

  9. Neo4j图数据库简介和底层原理

    现实中很多数据都是用图来表达的,比如社交网络中人与人的关系.地图数据.或是基因信息等等.RDBMS并不适合表达这类数据,而且由于海量数据的存在,让其显得捉襟见肘.NoSQL数据库的兴起,很好地解决了海 ...

随机推荐

  1. 【Unity笔记】摄像机跟随目标角色

    public class CameraFollow : MonoBehaviour { public Transform target; // The position that that camer ...

  2. CAD常见问题

    CAD2016显示线宽点界面右下角三道杠[自定义]按钮,找到并勾选[线宽]. 导出部分图形为JPG图片1. 将想要导出的图形显示于屏幕正中.2. 打开打印窗口(快捷键Ctrl+P).3. 选打印机(虚 ...

  3. 【Unity/Kinect】Kinect实现UI控件的点击

    用体感来实现UI控件的点击,如点击按钮. 做法:用一个图片表示左手手掌,图片位置追踪左手手掌移动,当手掌位于UI控件的矩形内时,握拳表示点击该控件. using UnityEngine; using ...

  4. 【WPF】ScrollViewer无法滚动的问题

    还需要给ScrollViewer注册一个鼠标滚轮事件! XAML: <ScrollViewer x:Name="scrollViewer" Width="950&q ...

  5. Ajax-ajax实例2-根据邮政编码获取地区信息

    项目结构 运行效果: 数据库: /* SQLyog Ultimate v12.09 (64 bit) MySQL - 5.5.53 : Database - ajaxexample_2 ******* ...

  6. java-JSP脚本的9个内置对象

    http://blog.csdn.net/titilover/article/details/6800782 http://www.importnew.com/19128.html http://ww ...

  7. selenium测试(Java)--关闭窗口(二十)

    quit方法:退出相关的驱动程序和关闭所有窗口 close方法:关闭当前窗口 package com.test.closewindow; import java.util.Iterator; impo ...

  8. Mastering the game of Go with deep neural networks and tree search浅析

    Silver, David, et al. "Mastering the game of Go with deep neural networks and tree search." ...

  9. Spring 4 官方文档学习(十)数据访问之OXM

    http://docs.spring.io/spring/docs/current/spring-framework-reference/html/oxm.html Java Object 与 XML ...

  10. XML 入门

    XML语法 所有 XML 元素都须有关闭标签 XML 标签对大小写敏感 XML 必须正确地嵌套 XML 文档必须有根元素 就像HTML一样,HTML必须有<html>根元素.XML也必须有 ...