iOS之武功秘籍?:|iOS之武功秘籍?: KVO原理及自定义
iOS之武功秘籍 文章汇总写在前面 说到
KVC(键值编码)
和KVO(键值观察)
,可能大家都用的溜溜的,但是你真的了解它吗?本文就将全方位分析KVO的原理
本节可能用到的秘籍Demo
一、KVO初探
KVO(Key-Value Observing)
是苹果提供的一套事件通知机制,这种机制允许将其他对象的特定属性的更改通知给对象
.iOS开发者
可以使用KVO
来检测对象属性的变化、快速做出响应,这能够为我们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。在Documentation Archieve中提到一句想
要理解KVO,必须先理解KVC
,因为键值观察是建立在键值编码的基础上In order to understand key-value observing, you must first understand key-value coding.——Key-Value Observing Programming Guide在iOS日常开发中,经常使用
KVO
来监听对象属性的变化,并及时做出响应,即当指定的被观察的对象的属性被修改后,KVO
会自动通知相应的观察者,那么KVO
与NSNotificatioCenter
有什么区别呢?- 相同点
- 1、两者的实现原理都是
观察者模式
,都是用于监听 - 2、都能
实现一对多
的操作
- 1、两者的实现原理都是
- 不同点
- 1、
KVO
只能用于监听对象属性的变化,并且属性名都是通过NSString
来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错 - 2、
NSNotification
的发送监听(post
)的操作我们可以控制,KVO
由系统控制 - 3、
KVO
可以记录新旧值
变化
- 1、
KVO使用三部曲:
- 注册观察者
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
- 实现回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
*)change context:(void *)context { if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change); }
- 移除观察者
[self.person removeObserver:self forKeyPath:@"name"];
Key-Value Observing Programming Guide
是这么描述context
的这里提出一个假想,如果父类中有个
文章图片
消息中的上下文指针包含任意数据,这些数据将在相应的更改通知中传递回观察者;您可以指定NULL
并完全依赖键路径字符串来确定更改通知的来源,但是这种方法可能会导致对象的父类由于不同的原因而观察到相同的键路径,因此可能会出现问题;一种更安全,更可扩展的方法是使用上下文确保您收到的通知是发给观察者的,而不是超类的.
name
属性,子类中也有个name
属性,两者都注册对name
的观察,那么仅通过keyPath
已经区分不了是哪个name
发生变化了,现有两个解决办法- 多加一层判断——判断
object
,显然为了满足业务需求而去增加逻辑判断是不可取的 - 使用
context
传递信息,更安全、更可扩展
context
上下文主要是用于区分不同对象的同名属性
,从而在KVO
回调方法中可以直接使用context
进行区分,可以大大提升性能,以及代码的可读性context
使用总结:- 不使用
context
作为观察值
// context是 void * 类型,应该填 NULL 而不是 nil [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
- 使用context传递信息
//定义context static void *PersonNameContext = &PersonNameContext; static void *StudentNameContext = &StudentNameContext; //注册观察者 [self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext]; [self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:StudentNameContext]; //KVO回调 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary
*)change context:(void *)context { if (context == PersonNameContext) { NSLog(@"%@", change); } else if (context == StudentNameContext) { NSLog(@"%@", change); } }
也许在日常开发中你觉得是否移除通知都无关痛痒,但是不移除会带来潜在的隐患
以下是一段没有移除观察者的代码,页面push前后、键值改变前后都很正常
- (void)viewDidLoad {
[super viewDidLoad];
self.student = [TCJStudent new];
[self.student addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:ChildNameContext];
}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
}- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
self.student.name = @"葵花宝典";
}
但当把
TCJStudent
以单例的形式创建后,先点击屏幕在push到TCJDetailViewController
页面点击屏幕后pop返回上一页面再次点击屏幕程序就崩溃了文章图片
这是因为没有移除观察,
单例对象
依旧存在,再次进来点击屏幕时就会报出野指针
错误了移除了观察者之后便不会发生这种情况了——移除观察者是必要的
文章图片
苹果官方推荐的方式是 —— 在④.手动触发键值观察init
的时候进行addObserver
,在dealloc
时removeObserver
,这样可以保证add
和remove
是成对出现的,这是一种比较理想的使用方式
有时候业务需求需要观察某个属性值,一会儿要观察了,一会又不要观察了...如果把
KVO
三部曲整体去掉、再整体添上,必然又是一顿繁琐而又不必要的工作,好在KVO
中有两种办法可以手动触发键值观察:- 将被观察者的
automaticallyNotifiesObserversForKey
返回NO(可以只对某个属性设置)-- 自动开关,返回NO,就监听不到,返回YES,表示监听
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key { if ([key isEqualToString:@"name"]) { return NO; } return [super automaticallyNotifiesObserversForKey:key]; }
- 使用
willChangeValueForKey
、didChangeValueForKey
重写被观察者的属性的setter
方法
这两个方法用于通知系统该key
的属性值即将和已经变更了
- (void)setName:(NSString *)name { [self willChangeValueForKey:@"name"]; _name = name; [self didChangeValueForKey:@"name"]; }
文章图片
最近发现[self willChangeValueForKey:name]和[self willChangeValueForKey:"name"]两种写法是不同的结果:重写setter方法取属性值操作不会额外发送通知;而使用“name”会额外发送一次通知⑤.键值观察 一对多
KVO
观察中的一对多
,意思是通过注册一个KVO
观察者,可以监听多个属性的变化比如有一个下载任务的需求,根据总下载量
totalData
和当前已下载量writtenData
来得到当前下载进度downloadProgress
,这个需求就有两种实现方式:- 分别观察总下载量
totalData
和当前已下载量writtenData
两个属性,其中一个属性发生变化时计算求值当前下载进度downloadProgress
- 实现
keyPathsForValuesAffectingValueForKey
方法,并观察downloadProgress
属性
totalData
或当前已下载量writtenData
任意发生变化,keyPaths=downloadProgress
就能收到监听回调+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"downloadProgress"]) {
NSArray *affectingKeys = @[@"totalData", @"writtenData"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
但仅仅是这样还不够——这样只能监听到回调,但还没有完成
downloadProgress
赋值——需要重写getter
方法- (NSString *)downloadProgress{
if (self.writtenData =https://www.it610.com/article/= 0) {
self.writtenData = 10;
}
if (self.totalData == 0) {
self.totalData = 100;
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.writtenData/self.totalData];
}
⑥.KVO观察 可变数组
如题:
TCJPerson
下有一个可变数组dataArray
,现观察之,问点击屏幕是否打印?- (void)viewDidLoad {
[super viewDidLoad];
self.person = [TCJPerson new];
[self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) NSLog(@"%@", change);
}- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.person.dataArray addObject:@"1"];
}
答:不会
分析:
-
KVO
是建立在KVC
的基础上的,而可变数组直接添加是不会调用Setter
方法 - 可变数组
dataArray
没有初始化,直接添加会报错
// 初始化可变数组
self.person.dataArray = @[].mutableCopy;
// 调用setter方法
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"2"];
三、KVO原理——isa-swizzling ①.官方解释
文章图片
Key-Value Observing Programming Guide
中有一段底层实现原理的叙述-
KVO
是使用isa-swizzling
技术实现的 - 顾名思义,
isa
指针指向维护分配表的对象的类,该分派表实质上包含指向该类实现的方法的指针以及其他数据 - 在为对象的属性注册观察者时,将修改观察对象的
isa
指针,指向中间类而不是真实类。isa
指针的值不一定反映实例的实际类 - 您永远不应依靠
isa
指针来确定类成员身份。相反,您应该使用class
方法来确定对象实例的类
这段话说的云里雾里的,还是敲代码见真章吧
- 注册观察者之前:类对象为
TCJPerson
,实例对象isa
指向TCJPerson
文章图片
- 注册观察者之后:类对象为
TCJPerson
,实例对象isa
指向NSKVONotifying_TCJPerson
文章图片
TCJPerson
类没发生变化,但实例对象的isa
指向发生变化那么这个动态生成的中间类
NSKVONotifying_TCJPerson
和TCJPerson
是什么关系呢?在注册观察者前后分别调用打印子类的方法——发现
NSKVONotifying_TCJPerson
是TCJPerson
的子类文章图片
③.动态子类探索
?首先得明白动态子类观察的是什么?下面观察属性变量
nickName
和成员变量name
来找区别两个变量同时发生变化,但只有
属性变量
监听到回调——说明动态子类观察的是setter
方法文章图片
?通过
runtime-API
打印一下动态子类和观察类的方法#pragma mark - 遍历方法-ivar-property
- (void)printClassAllMethod:(Class)cls{
NSLog(@"*********************");
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0;
i
文章图片
通过打印可以看出:
-
TCJPerson
类中的方法没有改变(imp实现地址没有变化) -
NSKVONotifying_TCJPerson
类中重写了父类TCJPerson
的dealloc
方法 -
NSKVONotifying_TCJPerson
类中重写了基类NSObject
的class
方法和_isKVOA
方法- 重写的
class
方法可以指回TCJPerson
类
- 重写的
-
NSKVONotifying_TCJPerson
类中重写了父类TCJPerson
的setNickName
方法- 因为子类只继承、不重写是不会有方法imp的,调用方法时会问父类要方法实现
- 且两个
setNickName
的地址指针不一样 - 每观察一个
属性变量
就重写一个setter
方法(可自行论证)
dealloc
之后isa
指向谁?——指回原类文章图片
?
dealloc
之后动态子类会销毁吗?——不会页面pop后再次push进来打印
TCJPerson
类,子类NSKVONotifying_TCJPerson
类依旧存在文章图片
?
automaticallyNotifiesObserversForKey
是否会影响动态子类生成——会动态子类会根据观察属性的
automaticallyNotifiesObserversForKey
的布尔值来决定是否生成④.总结
1.
automaticallyNotifiesObserversForKey
为YES
时注册观察属性会生成动态子类NSKVONotifying_XXX
2.动态子类观察的是
setter
方法3.动态子类重写了观察属性的
setter
方法、dealloc
、class
、_isKVOA
方法-
setter
方法用于观察键值-
dealloc
方法用于释放时对isa
指向进行操作-
class
方法用于指回动态子类的父类-
_isKVOA
用来标识是否是在观察者状态的一个标志位4.
dealloc
之后isa
指向元类5.
dealloc
之后动态子类不会销毁四、自定义KVO
根据KVO的官方文档和上述结论,我们将自定义KVO——下面的自定义会有runtime-API的使用和接口设计思路的讲解,最终的自定义KVO能满足基本使用的需求但仍不完善。系统的KVO回调和自动移除观察者都与注册逻辑分层,自定义的KVO将使用block回调和自动释放来优化这一点不足新建一个
NSObject+TCJKVO
的分类,开放注册观察者方法-(void)cj_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(TCJKVOBlock)block;
①.注册观察者
1.判断当前观察值
keypath
是否存在/setter
方法是否存在一开始想的是判断属性是否存在,虽然父类的属性不会对子类造成影响,但是分类中的属性虽然没有
setter
方法,但是会添加到propertiList
中去——最终改为去判断setter
方法if (keyPath == nil || keyPath.length == 0) return;
// if (![self isContainProperty:keyPath]) return;
if (![self isContainSetterMethodFromKeyPath:keyPath]) return;
// 判断属性是否存在
- (BOOL)isContainProperty:(NSString *)keyPath {
unsigned int number;
objc_property_t *propertiList = class_copyPropertyList([self class], &number);
for (unsigned int i = 0;
i < number;
i++) {
const char *propertyName = property_getName(propertiList[i]);
NSString *propertyString = [NSString stringWithUTF8String:propertyName];
if ([keyPath isEqualToString:propertyString]) return YES;
}
free(propertiList);
return NO;
}/// 判断setter方法
- (BOOL)isContainSetterMethodFromKeyPath:(NSString *)keyPath {
Class superClass= object_getClass(self);
SEL setterSeletor= NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(superClass, setterSeletor);
if (!setterMethod) {
NSLog(@"没找到该属性的setter方法%@", keyPath);
return NO;
}
return YES;
}
2.判断观察属性的
automaticallyNotifiesObserversForKey
方法返回的布尔值BOOL isAutomatically = [self fx_performSelectorWithMethodName:@"automaticallyNotifiesObserversForKey:" keyPath:keyPath];
if (!isAutomatically) return;
// 动态调用类方法
- (BOOL)fx_performSelectorWithMethodName:(NSString *)methodName keyPath:(id)keyPath {if ([[self class] respondsToSelector:NSSelectorFromString(methodName)]) {#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
BOOL i = [[self class] performSelector:NSSelectorFromString(methodName) withObject:keyPath];
return i;
#pragma clang diagnostic pop
}
return NO;
}
3.动态生成子类,添加
class
方法指向原先的类// 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@", kTCJKVOPrefix, oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重复创建生成新类
if (newClass) return newClass;
// 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
// class的指向是TCJPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)cj_class, classTypes);
return newClass;
}
4.
isa
重指向——使对象的isa
的值指向动态子类object_setClass(self, newClass);
5.保存信息
由于可能会观察多个属性值,所以以属性值-模型的形式一一保存在数组中
typedef void(^TCJKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface TCJKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) TCJKVOBlock handleBlock;
@end@implementation TCJKVOInfo- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(TCJKVOBlock)block {
if (self=[super init]) {
_observer = observer;
_keyPath= keyPath;
_handleBlock = block;
}
return self;
}
@end// 保存信息
TCJKVOInfo *info = [[TCJKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
②.添加setter方法并回调
往动态子类添加
setter
方法- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
// 添加setter
SEL setterSEL = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSEL);
const char *setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSEL, (IMP)cj_setter, setterTypes);
return newClass;
}
setter方法的具体实现
static void cj_setter(id self,SEL _cmd,id newValue) {
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = https://www.it610.com/article/[self valueForKey:keyPath];
// 改变父类的值 --- 可以强制类型转换
void (*cj_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
cj_msgSendSuper(&superStruct,_cmd,newValue);
// 信息数据回调
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kTCJKVOAssiociateKey));
for (FXKVOInfo *info in mArray) {
if ([info.keyPath isEqualToString:keyPath] && info.handleBlock) {
info.handleBlock(info.observer, keyPath, oldValue, newValue);
}
}
}
③.销毁观察者
往动态子类添加
dealloc
方法- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
// 添加dealloc
SEL deallocSEL = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
const char *deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSEL, (IMP)cj_dealloc, deallocTypes);
return newClass;
}
由于页面释放时会释放持有的对象,对象释放时会调用dealloc,现在往动态子类的dealloc方法名中添加实现将isa指回去,从而在释放时就不会去找父类要方法实现
static void cj_dealloc(id self, SEL _cmd) {
Class superClass = [self class];
object_setClass(self, superClass);
}
但仅仅是这样还是不够的,只把isa指回去,但对象不会调用真正的dealloc方法,对象不会释放
出于这种情况,根据iOS之武功秘籍⑩: OC底层题目分析讲过的方法交换进行一波操作
- 取出基类NSObject的dealloc实现与cj_dealloc进行方法交换
- isa指回去之后继续调用真正的dealloc进行释放
- 之所以不在+load方法中进行交换,一是因为效率低,二是因为会影响到所有类
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
...
// 添加dealloc
//SEL deallocSEL = NSSelectorFromString(@"dealloc");
//Method deallocMethod = class_getInstanceMethod([self class], deallocSEL);
//const char *deallocTypes = method_getTypeEncoding(deallocMethod);
//class_addMethod(newClass, deallocSEL, (IMP)cj_dealloc, deallocTypes);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self cj_methodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(cj_dealloc)];
});
return newClass;
}- (void)cj_dealloc {
Class superClass = [self class];
object_setClass(self, superClass);
[self cj_dealloc];
}
就这样自定义KVO将KVO三部曲用block形式合成一步写在后面 【iOS之武功秘籍?:|iOS之武功秘籍?: KVO原理及自定义】和谐学习,不急不躁.我还是我,颜色不一样的烟火.
推荐阅读
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天
- 2020-04-07vue中Axios的封装和API接口的管理