面试系列:
- iOS面试全解1:基础/内存管理/Block/GCD
- iOS面试全解2:Runloop
- iOS面试全解3:Runtime
-
iOS面试全解4:KVC/KVO、通知/推送/信号量、Delegate/Protocol、Singleton(当前位置)
一、KVC
全称是Key-value coding
,键值编码。使用字符串来标识属性,是间接访问
对象的属性, 而不是直接通过调用存取方法(Setter、Getter方法)访问。可以在运行时
动态访问和修改对象的属性。
KVC的方法定义在: Foundation/NSKeyValueCoding
中。
特点: 可以简化程序代码。
KVC详解
-
1、KVC原理
- KVC 访问私有变量
- setter 原理分析
- getter 原理分析
- forKeyPath、 valueForKeyPath
- 异常处理: 赋值/取值、正确性验证
2、KVC与字典
3、KVC的消息传递
4、KVC容器操作
5、KVC集合代理对象
6、KVC的应用
一、KVC原理
1、 KVC 访问私有变量
1.1 能够访问私有成员变量(给对象的私有成员进行:取值/赋值 )
1.2 对数值和结构体型的属性进行的 打包/解包 处理
- valueForUndefinedKey
- setValue:forUndefinedKey:
myName 、_myName
myAge、_myAge
KVC常用的方法
为对象的属性:取值、赋值
# 赋值方法
-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
-(void)setValue:(id)value forKey:(NSString *)key;
# 取值方法
-(id)valueForKeyPath:(NSString *)keyPath;
-(id)valueForKey:(NSString *)key;
# 案例:
[person1 setValue:@"jack" forKey:@"name"]; # 赋值
NSString *name = [person1 valueForKey:@"name"]; # 取值
# forKeyPath 是对更“深层”的对象进行访问。如数组的某个元素,对象的某个属性。如:
[myModel setValue:@"beijing" forKeyPath:@"address.city"];
# 返回所有对象的name属性值
NSArray *names = [array valueForKeyPath:@"name"];
// 通过keyPath取值
• key:单层访问
• keyPath:可以多层访问
⁃ key:@"age"
⁃ keyPath:@"age"
⁃ keyPath:@"student.age"
⁃ keyPath:@"person.student.age"
注意
:setter、getter:会按照 _key,_iskey,key,iskey 的顺序搜索成员
2、setter 原理分析:(赋值过程顺序如下)
先找相关方法
//1、-(void) setName
//2、-(void) _setName
//3、-(void) setIsName
//与 -(void) _setIsName 无关
若没有相关方法,判断是否可以直接找方法成员变量
+ (BOOL)accessInstanceVariablesDirectly
2.1 NO:系统抛出一个异常,未定义key
2.2 YES:继续找相关变量
//1、_name
//2、_isName
//3、name
//4、isName
方法或成员变量都不存在:
使用setValue:forUndefinedKey:
方法,抛出异常。
3、getter 原理分析(取值过程顺序如下)
先找相关方法
//1、- getName
//2、- name
若没有相关方法,判断是否可以直接找方法成员变量
+ (BOOL)accessInstanceVariablesDirectly
2.1 NO:系统抛出一个异常,未定义key
2.2 YES:继续找相关变量
//1、_name
//2、_isName
//3、name
//4、isName
方法或成员都不存在,使用
valueForUndefinedKey:
方法,抛出异常
4、setValue:forKeyPath、 valueForKeyPath
寻找多级属性(KeyPath)
赋值:- setValue:forKeyPath:
取值:- valueForKeyPath: :
dog.name
_placeholderLabel.textColor
5、异常处理: 赋值、取值;正确性验证
异常处理 总结:
- 解决方案:重写以下相关方法
赋值:value为空 setNilValueForKey:
赋值:Key值不存在 setValue: forUndefinedKey:
取值:Key值不存在 valueForUndefinedKey:
正确性验证: validateValue
该方法的工作原理:
1. 先找一下你的类中是否实现了方法.
-(BOOL)validate<Key>:error:
2. 如果实现了就会根据实现方法里面的自定义逻辑返回NO或者YES,如果没有实现这个方法,则系统默认返回就是YES
//正确性验证: validateValue(内部验证)
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue
forKey:(NSString *)inKey
error:(out NSError **)outError {
//...
return YES;
}
二、KVC与字典
<Foundation/NSKeyValueCoding.h>
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
三、KVC的消息传递
NSArray* lengthArr = [arr valueForKey:@"length"];
NSArray* lowercaseArr = [arr valueForKey:@"lowercaseString"];
# 给成员 length 发送消息(遍历所有成员 长度)
# 给成员 lowercaseString 发送消息(字符串全部转成小写)
#pragma mark - KVC消息传递
- (void)arrayMessagePass{
NSArray *array = @[@"Alan",@"Xing",@"XZ",@"ZhaiAlan"];
NSArray *lenStr= [array valueForKeyPath:@"length"];
NSArray *lowStr= [array valueForKeyPath:@"lowercaseString"];
NSLog(@"---%@",lenStr);// 消息从array传递给了string
NSLog(@"---%@",lowStr);
}
输出结果:
---( 4, 4, 2, 8 )
---( alan, xing, xz, zhaialan )
四、KVC容器操作
1. 聚合操作符 @avg、@count、@max、@min、@sum
平均数、数量、最大值、最小值、合值
float avg = [[students valueForKeyPath:@"@avg.height"] floatValue];
float max = [[students valueForKeyPath:@"@max.height"] floatValue];
float min = [[students valueForKeyPath:@"@min.height"] floatValue];
float sum = [[students valueForKeyPath:@"@sum.height"] floatValue];
int count = [[students valueForKeyPath:@"@count.height"] intValue];
2. 数组操作符
去重:@distinctUnionOfObjects
不去重:@unionOfObjects
NSArray* arr1 = [students valueForKeyPath:@"@distinctUnionOfObjects.height"];
NSArray* arr2 = [students valueForKeyPath:@"@unionOfObjects.height"];
3. 嵌套集合(array&set NSMutableArray )操作
@distinctUnionOfArrays
、@distinctUnionOfSets
、@unionOfArrays
读取集合中每个元素的键路径指定的属性,放在一个NSArray实例中,将数组进行去重后返回
NSMutableArray* students1 = [NSMutableArray array];
NSMutableArray* students2 = [NSMutableArray array];
//嵌套数组
NSArray* nestArr = @[students1, students2];
NSArray* arr1 = [nestArr valueForKeyPath:@"@distinctUnionOfArrays.height"];
NSArray* arr2 = [nestArr valueForKeyPath:@"@unionOfArrays.height"];
4. 嵌套集合(array&set NSMutableSet)操作 @distinctUnionOfArrays
、@distinctUnionOfSets
、@unionOfArrays
NSMutableSet* students1 = [NSMutableSet set];
NSMutableSet* students2 = [NSMutableSet set];
NSSet* nestSet = [NSSet setWithObjects:students1, students2, nil];
NSArray* arr1 = [nestSet valueForKeyPath:@"@distinctUnionOfSets.height"];
//不去重-没有此方法(异常崩溃:this class does not implement the unionOfArrays operation.)
// NSArray* arr2 = [nestSet valueForKeyPath:@"@unionOfArrays.height"];
五、KVC集合代理对象
-
个人理解(一对多的关系:通过一个key,对应多个方法)
• 对象的属性可以是一对一的,也可以是一对多的。一对多的属性要么是有序的(数组),要么是无序的(集合)。
• 属性的一对多关系其实就是一种对容器类的映射。
• 不可变的有序容器属性(NSArray)和无序容器属性(NSSet)一般可以使用valueForKey:来获取。
• 如果有一个名为numbers的数组属性,我们可以使用valueForKey:@"numbers"来获取,这个是没问题的,但KVC还能使用更灵活的方式管理集合。——那就是:集合代理对象(变量 variable)
-(void)valueForKey 有如下的搜索规则:
1、按顺序搜索 getKey、key、isKey,第一个被找到的会用作返回。
2、countOf<Key>、objectIn<Key>AtIndex 与<key>AtIndexes其中之一,这个组合会使KVC返回一个代理数组。
3、countOf<Key>、enumeratorOf<Key>、memberOfVar。这个组合会使KVC返回一个代理集合。
4、名为_val、_isVar、var、isVar的实例变量。到这一步时,KVC会直接访问实例变量,而这种访问操作破坏了封装性,我们应该尽量避免,这可以通过重写+(Bool)accessInstanceVariablesDirectly返回NO来避免这种行为。
6、KVC的应用
6.1、动态地取值和赋值
6.2、用KVC来访问和修改私有变量
对于类里的私有属性,OC是无法直接访问的,但是KVC是可以的。
6.3、Model和字典转换
6.4、修改一些控件的内部属性:最常用的就是UITextField中的placeHolderText了。
6.5、操作集合
6.6、用KVC实现高阶消息传递
[self.textField setValue:[UIColor blueColor] forKeyPath:@"_placeholderLabel.textColor"];
[self.textField setValue:[UIFont systemFontOfSize:14] forKeyPath:@"_placeholderLabel.font"];
[self.textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.backgroundColor"];
二、KVO
1、KVO原理分析(概念)
2、KVO基本使用
3、KVO自定义
4、KVO延伸(自动销毁机制、Blocks、YY封装KVO)
1、概念
全称(Key-Value Observing
)键值观察
允许对象通知其他对象属性的更改。它对应用程序中模型 Model
和控制器层 VC
之间的通信特别有用。 (在OS X中,控制器层绑定技术严重依赖于键值观察。)控制器对象通常观察模型对象的属性,视图对象通过控制器观察模型对象的属性。然而,另外,模型对象可以观察其他模型对象(通常用于确定从属值何时改变)或甚至自身(再次确定从属值何时改变)。
原理分析:
1. 动态生成一个继承自原类的新类
2. 使用Person的分类,重写新类的set方法
3. 修改isa指针的指向。将self的isa指针指向新类,也就是为了在调用setter方法的时候,会去新类中查找。
4. 将self的isa指针指向原类,为了获得旧值,然后赋值新值。最后去通知观察者值的改变,isa指针再指向新类(为了继续观察属性,下次进行通知)。
引用:
KVO原理以及自定义KVO
iOS探索KVO实现原理,重写KVO
//设置旧值
change[NSKeyValueChangeOldKey] = oldValue;
//设置新值
change[NSKeyValueChangeNewKey] = newValue;
//调用observer里面的回调方法
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),keyPath,observer,change,context);
//self的isa指针指向新类
object_setClass(self, newClass);
——————————————————————————
特点:观察属性,一对一关系和多对多关系。
一个简单的例子:假设Person对象与Account账户对象交互。 Person的实例要知道Account实例的某些方面何时发生变化,例如余额或利率。
- 一个对象的一个属性、多个属性;
- 多个对象的一个属性、多个属性;
ViewController VC --> Observing Model Property
View --> VC --> Observing Model Property
1.1、Runtime 实现KVO
KVO的实现依赖于 Objective-C 强大的 Runtime,当观察某对象 People
时,KVO 机制动态创建一个对象people当前类的子类,并为这个新的子类重写了被观察属性 keyPath
的 setter
方法。setter 方法随后负责通知观察对象属性的改变状况。
Apple 使用了isa-swizzling
来实现 KVO 。当观察对象people时,KVO机制动态创建一个新的名为:NSKVONotifying_People
的新类,该类继承自对象People
的本类,且 KVO 为 NSKVONotifying_People 重写观察属性的 setter 方法,setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象属性值的更改情况。
1.2、NSKVONotifying_People 类剖析
NSLog(@"self->isa: %@",self->isa);
NSLog(@"self class: %@",[self class]);
# 在建立KVO监听前,打印结果为:
self->isa: People
self class: People
# 在建立KVO监听之后,打印结果为:
self->isa: NSKVONotifying_People
self class: People
1.3、子类setter方法剖析
KVO 的键值观察通知依赖于 NSObject 的两个方法:
-
willChangeValueForKey:
被观察属性发生改变之前被调用,通知即将改变、 -
didChangeValueForKey:
被观察属性发生改变之后被调用,通知已经变更。
在存取数值的前后分别调用 这2 个方法,且重写观察属性的setter 方法这种继承方式的注入是在运行时
而不是编译时实现的。
手动调用KVO
KVO 为子类的观察者属性重写调用存取方法的工作原理在代码中相当于:
- (void)setName:(NSString *)newName {
[self willChangeValueForKey:@"name"]; # KVO 在调用存取方法之前总调用
[super setValue:newName forKey:@"name"]; # 调用父类的存取方法
[self didChangeValueForKey:@"name"]; # KVO 在调用存取方法之后总调用
}
如果想控制当前对象的自动调用过程
,也就是由上面两个方法发起的KVO调用,则可以重写下面方法。方法返回YES则表示可以调用,如果返回NO则表示不可以调用。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"balance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
分析观察者的整个过程:
1、未被观察的属性,直接找到属性的 setter/getter
方法 赋值/取值
2、被观察的属性,会动态创建一个子类 NSKVONotifying_object
,然后添加一些方法,重写
对应属性的 setter/getter 方法
,观察子类属性的变化,并通知
给子类
*isa
--> subClass
--> superClass
--> mateClass
... --> rootClass
(先找子类方法,再找父类方法,再找元类方法,根类方法 根类的元类就是它自己,元类最后指向自己)
-
TZPerson:
isa
-->NSKVONotifiying_TZPerson
-->supclass TZPerson
- 添加观察后:动态去创建一个新类,并添加一些方法/属性,
继承了原类
(移除观察者后,isa指针重新指向 TZPerson 原类)
重写了子类:指针指向原类
- (Class) class {
return object_superClass(object_getClass(self));
}
方法调用过程:
- 即将改变:NSKeyValueWillChange
-[TZPerson setSteps:]
- 已经改变:NSKeyValueDidChange
- 通知观察者:NSKeyValueNotifyObserver
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
2、基本使用
使用三步骤:
- 添加观察者
- 实现监听方法
- 移除观察者
3、KVO自定义
- 动态创建一个类
// 1. 拼接子类名 NSKVONotifying_object
// 2. 创建并注册类- class 创建并注册类
- class 添加一些方法
- class (重写属性的 setter 方法)
- 方法反射,添加一个方法
- zm_setter
- 添加析构方法 - dealloc
- return newClass;
- 修改isa的指向
- 关联方法
4、KVO延伸(自动销毁机制、Blocks、YY封装KVO)
在YY_KVO 中使用到
---------------------------- "NSObject+YYAddForKVO.h" ----------------------------
# //实现监听
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (!self.block) return;
BOOL isPrior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
if (isPrior) return;
# //使用中间类
NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
if (changeKind != NSKeyValueChangeSetting) return;
id oldVal = [change objectForKey:NSKeyValueChangeOldKey];
if (oldVal == [NSNull null]) oldVal = nil;
id newVal = [change objectForKey:NSKeyValueChangeNewKey];
if (newVal == [NSNull null]) newVal = nil;
# //Block 回调监听结果
self.block(object, oldVal, newVal);
问题:
1、KVO是否会对一个 变量 进行通知?
答:不会,只观察对象的属性。
三、通知/信号量
1、通知:NSNotification
目前分为四个推送:
1.用户推送:NSNotificationCenter
2.本地推送:UILocalNotification(状态栏提示:例如闹钟、挂号排队)
3.远程推送:RemoteNotifications
4.地理位置推送:必须配合CLLocation使用
过程:注册、接收、处理
有以下特点:
- 一对一 或 一对多
- 消息的
发送者
告知接收者
事件已经发生或者将要发送,仅此而已,接收者
不能影响发送者
的行为。
#//1.1、添加:通知
[[NSNotificationCenter defaultCenter] postNotificationName:@"通知变化" object:nil];
#//1.2、接收:通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(ChangNotificat:) name:@"通知变化" object:nil];
#//1.3、移除:通知
[[NSNotificationCenter defaultCenter] removeObserver:self name:nil object:self];
# // 2、监听通知:键盘
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(NotificatClick:)
name:UIKeyboardWillHideNotification object:nil];
# /* 3、监听通知:输入框
* UITextFieldTextDidBeginEditingNotification;
* UITextFieldTextDidEndEditingNotification;
* UITextFieldTextDidChangeNotification;
*/
[[NSNotificationCenter defaultCenter] addObserverForName:UITextFieldTextDidEndEditingNotification
object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notificate){
NSLog(@" textField ");
if ([notificate.object isEqual:self]){
}
}];
2、信号量:dispatch_semaphore
dispatch_semaphore
是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
一般在GCD职工使用,我理解的dispatch_semaphore有两个主要应用 :
- 保持线程同步
- 为线程加锁,等待执行任务完成后,通知后面的任务执行
四、代理协议:Delegate/Protocol
sender: 消息的发送者
receiver:消息的接收者
delegate:代理者
Delegate 代理
代理的目的是改变或传递控制链。可以减少框架复杂度。
允许一个类在某些特定时刻通知到其他类,而不需要获取到那些类的指针。
可以减少框架复杂度。
消息的发送者(sender
)告知接收者(receiver
)某个事件将要发生,delegate
同意,然后发送者响应事件:发送者 委托 接收者 去做某件事。
delegate机制使得接收者可以改变发送者的行为。
通常发送者和接收者的关系是直接的 一对一
的关系。
IBOutlet可以为weak,NSString为copy,Delegate一般为weak。
Protocol 小结:
类对象遵守了Protocol
,那么该类就有了Protocol里面声明的方法。类必须实现@required
方法、可选实现@optional
方法,具体怎么实现由该类自行决定,Protocol不过问。
五、单例:Singleton
1、单例模式的三个要点:
1) 某个类 只能有一个实例(另外创建必须crash抛出异常提示);
2) 它必须 自行创建这个实例;
3) 它必须自行 向整个系统提供这个实例。
2、单例模式的优点:
1.实例控制:Singleton 会阻止其他对象实例化其自己的 Singleton 对象的副本,从而 确保所有对象都访问唯一实例。
2.灵活性:因为类控制了实例化过程,所以类可以更加灵活修改实例化过程
一个共享的实例,该实例只会被创建一次。
**该方法有很多优势: **
1、线程安全
2、很好满足静态分析器要求
3、和自动引用计数(ARC)兼容
4、仅需要少量代码
使用继承单例 源类,便于管理,代码如下:
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
//Impl:初始微程序装载(Initial Microprogram Loading)
//#if DEBUG
// #define SINGLETON_INSTANCE_LOG(x) NSLog(@"Singleton object <%s(%@)> has been created.",#x, [x class])
//#else
// #define SINGLETON_INSTANCE_LOG(x)
//#endif
// DEBUG 宏定义模式不用
#define SINGLETON_INSTANCE_LOG(x)
//1、继承于 Singleton 的初始微程序装载(实例化)
#define Singleton_Instance_method_Interface(ClassName) \
+ (ClassName *)instance;
#define Singleton_Instance_method_Impl(ClassName) \
+ (ClassName *)instance \
{ \
static ClassName *_g_##ClassName##_obj = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_g_##ClassName##_obj = [[self singletonAlloc] init]; \
SINGLETON_INSTANCE_LOG(_g_##ClassName##_obj); \
}); \
return _g_##ClassName##_obj; \
}
//2、不继承于 Singleton 的初始微程序装载(实例化)
#define Singleton_Instance_method_ImplAlloc(ClassName) \
+ (ClassName *)instance \
{ \
static ClassName *_g_##ClassName##_obj = nil; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_g_##ClassName##_obj = [[self alloc] init]; \
SINGLETON_INSTANCE_LOG(_g_##ClassName##_obj); \
}); \
return _g_##ClassName##_obj; \
}
/**
* 我建议:
单例类都应该继承JKSingletonObject这个项目。
强迫每个人都使用+ singletonAlloc对团队有好处。
*/
@interface Singleton : NSObject
+ (id)singletonAlloc;
@end
#import "Singleton.h"
@implementation Singleton
/**
* 覆盖这个方法是阻止其他程序员编码“[[Singleton alloc]init]”。
你永远不应该使用alloc自己。请写+实例在自己的子类。
请参阅类方法+ Singleton.h实例。
*/
+ (id)alloc {
@throw [NSException exceptionWithName:@"单例模式的规则"
reason:@"禁止使用alloc自己创建这个对象,请使用+ singletonAlloc代替。"
userInfo:@{}];
return nil;
}
+ (id)singletonAlloc{
return [super alloc];
}
/**
* 原始方式:没有使用,看看就行(另外创建,没有抛出异常)
* @return 返回一个实例
*/
+ (Singleton *)singleton
{
static Singleton *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}