欢迎阅读iOS探索系列(按序阅读食用效果更加)html
在KVC(键值编码)
到KVO(键值观察)
,可能读者老爷们都用的溜溜的,可是你真的了解它吗?本文就将全方位分析KVO的原理git
KVO(Key-Value Observing)
是苹果提供的一套事件通知机制,这种机制容许将其余对象的特定属性的更改通知给对象。iOS开发者可使用KVO
来检测对象属性的变化、快速作出响应,这可以为咱们在开发强交互、响应式应用以及实现视图和模型的双向绑定时提供大量的帮助。github
在Documentation Archieve中提到一句想要理解KVO
,必须先理解KVC
,由于键值观察
是创建在键值编码
的基础上面试
In order to understand key-value observing, you must first understand key-value coding.——Key-Value Observing Programming Guide数组
而KVO
和NSNotificatioCenter
都是iOS观察者模式的一种实现,二者的区别在于:安全
KVO
是一对一的,NSNotificatioCenter
是一对多的KVO
对被监听对象无侵入性,不须要修改其内部代码便可实现监听KVO使用三部曲:bash
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
复制代码
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)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
发生变化了,现有两个解决办法:app
object
,显然为了知足业务需求而去增长逻辑判断是不可取的context
传递信息,更安全、更可扩展context
使用总结:ide
// context是 void * 类型,应该填 NULL 而不是 nil
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:NULL];
复制代码
static void *PersonNameContext = &PersonNameContext;
static void *ChildNameContext = &ChildNameContext;
[self.person addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:PersonNameContext];
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew) context:ChildNameContext];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == PersonNameContext) {
NSLog(@"%@", change);
} else if (context == ChildNameContext) {
NSLog(@"%@", change);
}
}
复制代码
也许在平常开发中你以为是否移除通知都无关痛痒,可是不移除会带来潜在的隐患
如下是一段没有移除观察者的代码,页面push先后、键值改变先后都很正常
- (void)viewDidLoad {
[super viewDidLoad];
self.child = [FXChild new];
self.child.name = @"Feng";
[self.child addObserver:self forKeyPath:@"name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:ChildNameContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
self.child.name = [NSString stringWithFormat:@"%@+", self.child.name];
}
复制代码
但当把FXChild
以单例
的形式建立后,pop回上一页再次push进来程序就崩溃了
这是由于没有移除观察,单例对象
依旧存在,再次进来时就会报出野指针错误
了
移除了观察者以后便不会发生这种状况了——移除观察者是必要的
苹果官方推荐的方式是——在
init
的时候进行addObserver
,在dealloc
时removeObserver
,这样能够保证add
和remove
是成对出现的,这是一种比较理想的使用方式
有时候业务需求须要观察某个属性值,一下子要观察了,一会又不要观察了...若是把KVO三部曲
总体去掉、再总体添上,必然又是一顿繁琐而又没必要要的工做,好在KVO中有两种办法能够手动触发键值观察:
automaticallyNotifiesObserversForKey
返回NO(能够只对某个属性设置)+ (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"];
}
复制代码
两种方式使用的排列组合以下,能够自由组合如何使用
状况 | 回调次数 |
---|---|
正常状况 | 1 |
automaticallyNotifiesObserversForKey为NO | 0 |
automaticallyNotifiesObserversForKey为NO且添加willChangeValueForKey、didChangeValueForKey | 1 |
automaticallyNotifiesObserversForKey为YES且添加willChangeValueForKey、didChangeValueForKey | 2 |
最近发现[self willChangeValueForKey:name]和[self willChangeValueForKey:"name"]两种写法是不一样的结果:重写setter方法取属性值操做不会额外发送通知;而使用“name”会额外发送一次通知
好比有一个下载任务的需求,根据总下载量Total
和当前已下载量Current
来获得当前下载进度Process
,这个需求就有两种实现:
总下载量Total
和当前已下载量Current
两个属性,其中一个属性发生变化时计算求值当前下载进度Process
keyPathsForValuesAffectingValueForKey
方法,并观察process
属性只要总下载量Total
或当前已下载量Current
任意发生变化,keyPaths=process
就能收到监听回调
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"process"]) {
NSArray *affectingKeys = @[@"total", @"current"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
复制代码
但仅仅是这样还不够——这样只能监听到回调,但尚未完成Process
赋值——须要重写getter方法
- (NSString *)process {
if (self.total == 0) {
return @"0";
}
return [[NSString alloc] initWithFormat:@"%f",1.0f*self.current/self.total];
}
复制代码
如题:FXPerson
下有一个可变数组dataArray
,现观察之,问点击屏幕是否打印?
- (void)viewDidLoad {
[super viewDidLoad];
self.person = [FXPerson new];
[self.person addObserver:self forKeyPath:@"dataArray" options:(NSKeyValueObservingOptionNew) context:NULL];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"dataArray"]) NSLog(@"%@", change);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.person.dataArray addObject:@"Felix"];
}
复制代码
答:不会
分析:
KVO
是创建在KVC
的基础上的,而可变数组直接添加是不会调用Setter方法
可变数组dataArray
没有初始化,直接添加会报错// 初始化可变数组
self.person.dataArray = @[].mutableCopy;
// 调用setter方法
[[self.person mutableArrayValueForKey:@"dataArray"] addObject:@"Felix"];
复制代码
Key-Value Observing Programming Guide
中有一段底层实现原理的叙述
isa-swizzling
技术实现的这段话说的云里雾里的,仍是敲代码见真章吧
FXPerson
,实例对象isa指向FXPerson
FXPerson
,实例对象isa指向NSKVONotifying_FXPerson
从这两图中能够得出一个结论:观察者注册先后FXPerson类
没发生变化,但实例对象的isa
指向发生变化
那么这个动态生成的中间类NSKVONotifying_FXPerson
和FXPerson
是什么关系呢?
在注册观察者先后分别调用打印子类的方法——发现NSKVONotifying_FXPerson
是FXPerson
的子类
①首先得明白动态子类观察的是什么?下面观察属性变量name
和成员变量nickname
来找区别
两个变量同时发生变化,但只有属性变量监听到回调——说明动态子类观察的是setter
方法
②经过runtime-API
打印一下动态子类和观察类的方法
- (void)printClassAllMethod:(Class)cls {
unsigned int count = 0;
Method *methodList = class_copyMethodList(cls, &count);
for (int i = 0; i<count; i++) {
Method method = methodList[i];
SEL sel = method_getName(method);
IMP imp = class_getMethodImplementation(cls, sel);
NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
}
free(methodList);
}
复制代码
FXPerson类
中的方法没有改变(imp实现地址没有变化)NSKVONotifying_FXPerson类
中重写了父类FXPerson
的dealloc
方法NSKVONotifying_FXPerson类
中重写了基类NSObject
的class
方法和_isKVOA
方法
class
方法能够指回FXPerson类
NSKVONotifying_FXPerson类
中重写了父类FXPerson
的setName
方法
setName
的地址指针不同属性变量
就重写一个setter
方法(可自行论证)③dealloc
以后isa指向谁?——指回原类
④dealloc
以后动态子类会销毁吗?——不会
页面pop后再次push进来打印FXPerson类
,子类NSKVONotifying_FXPerson类
依旧存在
⑤automaticallyNotifiesObserversForKey
是否会影响动态子类生成——会
动态子类会根据观察属性的automaticallyNotifiesObserversForKey
的布尔值来决定是否生成
automaticallyNotifiesObserversForKey
为YES
时注册观察属性会生成动态子类NSKVONotifying_XXX
setter
方法setter
方法、dealloc
、class
、_isKVOA
方法
setter
方法用于观察键值dealloc
方法用于释放时对isa指向进行操做class
方法用于指回动态子类的父类_isKVOA
用来标识是不是在观察者状态的一个标志位dealloc
以后isa
指向元类dealloc
以后动态子类不会销毁根据KVO的官方文档和上述结论,咱们将自定义KVO——下面的自定义会有runtime-API的使用和接口设计思路的讲解,最终的自定义KVO能知足基本使用的需求但仍不完善。系统的KVO回调和自动移除观察者都与注册逻辑分层,自定义的KVO将使用block回调和自动释放来优化这一点不足
新建一个NSObject+FXKVO
的分类,开放注册观察者方法
-(void)fx_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(FXKVOBlock)block;
当前观察值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;
}
复制代码
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;
}
复制代码
class
方法指向原先的类// 动态生成子类
Class newClass = [self createChildClassWithKeyPath:keyPath];
- (Class)createChildClassWithKeyPath:(NSString *)keyPath {
NSString *oldClassName = NSStringFromClass([self class]);
NSString *newClassName = [NSString stringWithFormat:@"%@%@", kFXKVOPrefix, oldClassName];
Class newClass = NSClassFromString(newClassName);
// 防止重复建立生成新类
if (newClass) return newClass;
// 申请类
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 注册类
objc_registerClassPair(newClass);
// class的指向是FXPerson
SEL classSEL = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSEL);
const char *classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSEL, (IMP)fx_class, classTypes);
return newClass;
}
复制代码
isa
的值指向动态子类object_setClass(self, newClass);
复制代码
因为可能会观察多个属性值,因此以属性值-模型
的形式一一保存在数组中
typedef void(^FXKVOBlock)(id observer,NSString *keyPath,id oldValue,id newValue);
@interface FXKVOInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, copy) FXKVOBlock handleBlock;
@end
@implementation FXKVOInfo
- (instancetype)initWitObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(FXKVOBlock)block {
if (self=[super init]) {
_observer = observer;
_keyPath = keyPath;
_handleBlock = block;
}
return self;
}
@end
// 保存信息
FXKVOInfo *info = [[FXKVOInfo alloc] initWitObserver:observer forKeyPath:keyPath handleBlock:block];
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey), mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mArray addObject:info];
复制代码
往动态子类添加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)fx_setter, setterTypes);
return newClass;
}
复制代码
setter方法的具体实现
static void fx_setter(id self,SEL _cmd,id newValue) {
NSString *keyPath = getterForSetter(NSStringFromSelector(_cmd));
id oldValue = [self valueForKey:keyPath];
// 改变父类的值 --- 能够强制类型转换
void (*lg_msgSendSuper)(void *,SEL , id) = (void *)objc_msgSendSuper;
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
lg_msgSendSuper(&superStruct,_cmd,newValue);
// 信息数据回调
NSMutableArray *mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(kFXKVOAssiociateKey));
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)fx_dealloc, deallocTypes);
return newClass;
}
复制代码
因为页面释放时会释放持有的对象,对象释放时会调用dealloc,如今往动态子类的dealloc方法名中添加实现将isa指回去,从而在释放时就不会去找父类要方法实现
static void fx_dealloc(id self, SEL _cmd) {
Class superClass = [self class];
object_setClass(self, superClass);
}
复制代码
但仅仅是这样仍是不够的,只把isa指回去,但对象不会调用真正的dealloc方法,对象不会释放
出于这种状况,根据iOS探索 runtime面试题分析讲过的方法交换进行一波操做
NSObject
的dealloc实现与fx_dealloc
进行方法交换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)fx_dealloc, deallocTypes);
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self FXMethodSwizzlingWithClass:[self class] oriSEL:NSSelectorFromString(@"dealloc") swizzledSEL:@selector(fx_dealloc)];
});
return newClass;
}
- (void)fx_dealloc {
Class superClass = [self class];
object_setClass(self, superClass);
[self fx_dealloc];
}
复制代码
就这样自定义KVO将KVO三部曲用block形式合成一步
本文demo、J_Knight_写的SJKVOController及FBKVO(建议看看这个成熟的自定义KVO)
最近在掘金上看到一个沸点——“不少人明白原理,但到了真正敲代码的时候就不会了”
学习如同踩坑爬坑,有些坑看过别人踩过,本身不去尝试过都不知道是怎么回事。或许你会有抓耳挠腮迷惑的时候,可是你不去解决困难,困难永远会挡在你成长的路上
你要悄悄拔尖,而后惊艳全部人🌺——————与君共勉