本文由快学吧个人写作,以任何形式转载请表明原文出处。
一、资料准备
二、KVO的原理
1. 文档理解原理
文档有实现细节的阐述 :
翻译 :
KVO的自动观测是通过
isa-swizzling
技术完成的。isa指向了一个类,这个类维护着一张表,这个表里面有这个类的方法的实现,和一些其他的数据。说白了,就是指类的结构中的bits->data(),也就是
class_rw_t
呗。当为对象的属性注册了观察者之后,对象的isa指针就会被修改,指向一个中间类,而不是对象所属的真正类。所以isa指针的指向不一定就能反映对象的真实的类。
永远不要用isa指针来确定对象的类,而是利用class方法来确定对象的类。
总结 :
不要用isa的指向来判断对象的类,要用class方法。KVO在给对象的属性添加观察者的时候,对象的isa会指向一个中间类,而不是原来的类。
2. 验证原理
创建一个JDMan类,有一个属性jdName,创建两个VC,一个叫VC,一个叫JDVC,VC push 就会到JDVC。
并在JDVC中添加属性man,类型是JDMan。对man对这个对象的jdName属性进行观察。在添加观察者前,查看man的类,在添加观察者后,再查看man的类。
JDMan :
JDVC中打上断点,引入runtime头文件 :
运行,查看添加观察者之前和之后,self.man的isa是否发生了变化,指向了某个中间类。
验证了文档所说的,在给对象的属性添加了观察者之后,对象的isa就会指向一个中间类。
3. 中间类是什么
中间类的命名规范是 :
NSKVONotifying_
+ 实例的类名
- 为什么要生成中间类?
- 中间的类和原来的类的关系?
- 对象的isa是否一直指向中间类,或者什么时候对象的isa指回原来的类?
- 中间类在不被使用了之后,是否会销毁?
先看中间类和原来的类的关系。猜测是子类,因为KVO并未影响对象的setter方法,仅仅是监听。所以子类重写setter,在setter中添加监听的可能性最大。
JDVC中加入
scss
- (void)allChildClassAndSelf:(Class)cls
{
// 获取注册到内存的类的总数量,用来遍历所有的类
// 参数1传NULL是获取所有已注册的类。参数2传0也是为了这个。
int count = objc_getClassList(NULL, 0);
// 获取所有已经注册的类,先申请类列表的空间,大小是count的大小
Class *classes = (Class *)malloc(sizeof(Class)*count);
objc_getClassList(classes, count);
// 为了打印结果里清晰的显示父类是谁,先把父类加入到可变数组中,这样可变数组的第一个就肯定是父类了
NSMutableArray *mutArr = [NSMutableArray arrayWithObject:cls];
for (int i = 0; i < count; i++) {
// 如果注册到内存的类的父类是cls,就添加到可变数组中
if (cls == class_getSuperclass(classes[i])) {
[mutArr addObject:classes[i]];
}
}
// malloc的classes要自己释放内存
free(classes);
NSLog(@"cls自己和子类 : %@",mutArr);
}
JDVC代码 :
运行,结果如下 :
由此可以得出结论 :
证明了KVO在给对象的属性注册观察者之后,对象的isa指向了一个中间类,并且这个中间类是对象本身的类的子类。
那对于为什么要生成中间类,就有了新的猜测,中间类是不是就是对修改关闭和对扩展开放的应用?并且修改中间类,比修改类本身更方便,也更安全。那么就要看中间类的方法列表中都有什么方法。
JDVC中加入 :
ini
- (void)allMethodsForClass:(Class)cls
{
// 存储类的所有的方法的数量
unsigned int count = 0;
Method *mList = class_copyMethodList(cls, &count);
for (int i = 0; i < count; i++) {
Method method = mList[i];
SEL mSEL = method_getName(method);
IMP mIMP = class_getMethodImplementation(cls, mSEL);
NSLog(@"类的所有方法 : %@--%p",NSStringFromSelector(mSEL),mIMP);
}
free(mList);
}
JDVC代码 :
运行,结果 :
看到重写了属性的set方法,由此可以得出,KVO观察的是属性的set方法,对没有set方法的实例变量是不会观察的。
为什么重写class方法?如下图 :
在添加观察者之后,对象调用class方法还是会得到对象的类是原有的类,而不是中间类,为了不影响外部的判断。
什么时候isa指回原来的类? 如下图 :
在移除观察者之后,让对象的isa指回原来的类。
中间类会在移除观察者之后就销毁吗?如下图 :
在VC中打印所有JDMan自己和所有的子类,对比push到JDVC和pop出JDVD前后,JDMan子类的变化。push到JDVC会创建观察者,pop出JDVC会注销观察者。
VC代码 :
JDVC代码 :
在VC没有push到JDVC前,也就是没有生成中间类之前 :
JDVC销毁,pop回到VC,已经添加过观察者,并且移除了。也就是生成过中间类了 :
由此可以得出结论 :
- KVO观察的是属性,因为中间类重写的是被观察属性的set方法,实例变量不自动生成set方法。
- 重写class是为了在class中让self.man调用class的时候不要受到影响,如果不重写,self.mam在被添加观察者之后,[self.man class]就会显示中间类。这也是为什么官方文档中说不要用isa去判断对象的类,而是用class。这里的重写class,就会让[self.man class]显示的是JDMan。
- 在remove(移除)观察者之后,被观察的对象的isa会指向原来的类。
- _isKVOA只是一个标识。
- 中间类在移除观察者之后不会被销毁。
三、自定义KVO
根据探索的原理,自定义一个KVO
首先确定的是KVO是基于KVC的。并且通过addObserver
方法的源码也可以看到,KVO也是在NSObject的分类中实现的。所以创建一个NSObject分类。
提前说明,这只是为了真正的理解KVO的一个思想。所以代码并不完美,甚至冗余并且有很多缺陷,但是没有特大bug,拿来玩是可以的,不要真的用到项目中去。
.h文件
objectivec
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (JDKVO)
- (void)jd_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
- (void)jd_observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
- (void)jd_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@end
NS_ASSUME_NONNULL_END
.m文件
scss
#import "NSObject+JDKVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
// 拼接中间类的名字用,前缀
static NSString *const JDKVOPrefix = @"JDKVONotifying_";
// 做关联对象的key,这个key对应的value是一个可变数组,可变数组存储的是观察者和被观察的属性
static NSString *const JDKVOAssociateKey = @"JDKVOAssiciateKey";
// 保存观察者和一些信息
@interface JDInfo : NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSKeyValueObservingOptions options;
- (instancetype)initWithObserver:(NSObject *)observer KeyPath:(NSString *)keyPath Options:(NSKeyValueObservingOptions)options;
@end
@implementation JDInfo
- (instancetype)initWithObserver:(NSObject *)observer KeyPath:(NSString *)keyPath Options:(NSKeyValueObservingOptions)options
{
if (self = [super init]) {
self.observer = observer;
self.keyPath = keyPath;
self.options = options;
}
return self;
}
@end
@implementation NSObject (JDKVO)
// 通过keyPath获取被观察属性的setter方法,c语言的写法
static NSString *cHuoQuSetter(NSString *keyPath)
{
if (keyPath.length <= 0) return nil;
// 主要就是将keyPath首字母大写,然后拼接一个set字符串,做法很多
// 取keyPath字符串的首字母大写
NSString *fStr = [[keyPath substringToIndex:1] uppercaseString];
// 取keyPath除了首字母以外的所有字符串
NSString *eStr = [keyPath substringFromIndex:1];
// 拼接返回
return [NSString stringWithFormat:@"set%@%@:",fStr,eStr];
}
// 通过keyPath获取被观察属性的setter方法,oc写法。
- (NSString *)ocHuoQuSetter:(NSString *)keyPath
{
if(keyPath.length <= 0) return nil;
// 把首字母大写,然后再换掉自己(首字母)的小写
NSString *fBigStr = [keyPath stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[keyPath substringToIndex:1].capitalizedString];
return [NSString stringWithFormat:@"set%@:",fBigStr];
}
// 判断是否有setter方法
- (BOOL)haveSetterMethod:(NSString *)keyPath
{
// 获取类,因为self现在是某个对象,所以要拿到类
Class class = object_getClass(self);
// 获取setter方法
NSString *selStr = [self ocHuoQuSetter:keyPath];
SEL sSEl = NSSelectorFromString(selStr);
Method sMethod = class_getInstanceMethod(class, sSEl);
return sMethod ? YES : NO;
}
// 从setter方法中取出属性名
static NSString *getPropertyNameFromSetter(NSString *setterName)
{
// 长相不对,返回nil。不是正经的setter方法的格式
if (setterName.length <= 0 || ![setterName hasPrefix:@"set"] || ![setterName hasSuffix:@":"]) {
return nil;
}
NSString *propertyName = [setterName substringWithRange:NSMakeRange(3, setterName.length-4)];
NSString *pFinalName = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[propertyName substringToIndex:1].lowercaseString];
return pFinalName;
}
// 中间类对keyPath的setter
// setter要做的事情 :
// 1. 给属性赋值。self现在是JDMan的对象,也就是外面调用的对象,所以要调用父类(中间类)的父类(JDMan)set方法。
// 2. 包装+回调给外部
// 包装 : 根据option 将newValue oldValue kind封装成change
// 回调给外部 : 让外部要响应observeValueForKeyPath方法
static void jd_setter(id self,SEL _cmd,id newValue)
{
// 先要把原始的值拿出来保存一下,不然下面马上就更改属性的值,变成新值了,就不能取到旧有的值了
// 拿到KeyPath
NSString *keyPath = getPropertyNameFromSetter(NSStringFromSelector(_cmd));
// 通过KVC拿到属性原有的值
id oldValue = [self valueForKey:keyPath];
// 1. 调用中间类的父类的set方法(SEL就是_cmd,是一样的,set的参数就是newValue)
// 这就是定义一个jd_msgSendSuper函数,参数分别是指针(传个真地址进来),SEL,id。
// 把objc_msgSendSuper这个函数直接赋值给自建的函数jd_msgSendSuper
// objc_msgSendSuper在<objc/message.h>里面声明的,记得import这个头文件
// struct objc_super sStruct就是objc_msgSendSuper的第一个参数,也就是jd_msgSendSuper的第一个参数
void (*jd_msgSendSuper)(void *,SEL,id) = (void *)objc_msgSendSuper;
struct objc_super sStruct =
{
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self))
};
jd_msgSendSuper(&sStruct,_cmd,newValue);
// 2. 包装 + 回调给外部(既然是包装,给它定义一个类更好)
// 取出关联对象中存储着observer和keyPath的数组
NSMutableArray *mutArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey));
// 循环一下拿出keyPath(属性)对应的info
for (JDInfo *jdInfo in mutArr) {
if ([jdInfo.keyPath isEqualToString:keyPath]) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 存值到change
NSMutableDictionary<NSKeyValueChangeKey,id> *change = [NSMutableDictionary dictionaryWithCapacity:1];
if (jdInfo.options & NSKeyValueObservingOptionNew) {
[change setValue:newValue forKey:NSKeyValueChangeNewKey];
}
if (jdInfo.options & NSKeyValueObservingOptionOld) {
if (oldValue) {
[change setValue:oldValue forKey:NSKeyValueChangeOldKey];
}
else
{
[change setValue:@"" forKey:NSKeyValueChangeOldKey];
}
}
// 发送消息给回调函数,该干活了
SEL obSEL = @selector(jd_observeValueForKeyPath:ofObject:change:context:);
// Build Setting-->Enable Strict Checking of objc_msgSend Calls改为NO,不然报错。
objc_msgSend(jdInfo.observer,obSEL,keyPath,self,change,NULL);
});
}
}
}
// 重写中间类的class方法,因为要在对象调用class的时候返回的是原来的类,而只有注册观察者之后
// 对象调用的class才会找到这里,所以class中返回的应该是self的类的父类,也就是中间类的父类才是正确的
static Class jd_class(id self,SEL _cmd)
{
return class_getSuperclass(object_getClass(self));
}
// 生成或者拿到中间类,因为要直接给中间类添加setter方法,所以要把keypath这个属性传进来
- (Class)getMediumClass:(NSString *)keyPath
{
// 获取中间类
NSString *oldClassStr = NSStringFromClass([self class]);
NSString *newClassStr = [NSString stringWithFormat:@"%@%@",JDKVOPrefix,oldClassStr];
Class newClass = NSClassFromString(newClassStr);
// 如果没有生成过中间类,则创建并注册到内存
if (!newClass) {
// 创建新的类,先申请内存空间
// 参数1是父类,2是新的类的名字,3是额外需要的存储空间
newClass = objc_allocateClassPair([self class], newClassStr.UTF8String, 0);
// 因为操作的主要是方法,方法在rw中就可以获取,所以没有必要先添加方法后注册
// 直接先注册类,免得忘了
objc_registerClassPair(newClass);
// 给中间类添加要重写的class方法
SEL classSEL = NSSelectorFromString(@"class");
Method classM = class_getInstanceMethod([self class], classSEL);
const char *classMT = method_getTypeEncoding(classM);
class_addMethod(newClass, classSEL, (IMP)jd_class, classMT);
}
// 给中间类添加keypath的setter方法并重写
// setter方法不要放在if里面,因为无论类是否存在,添加观察者的时候,观察的属性可能不一样,那么setter方法就不一样。
// 反正是用class_addMethod添加,返回的是bool值,可以判断下,是否能成功添加。不能就代表已经添加过这个属性的setter方法了
// 拿到keyPath的setter方法的sel
SEL setterSel = NSSelectorFromString(cHuoQuSetter(keyPath));
// 拿到keypath的setter方法的方法签名,因为只是重写,所以和原来的类的setter方法的签名一样
Method setterM = class_getInstanceMethod([self class], setterSel);
const char *setterMT = method_getTypeEncoding(setterM);
// 添加setter方法到中间类,IMP本身要带有两个参数id self 和 SEL _cmd,而且IMP本身就是个func,所以用c写一个就行
BOOL isAdd = class_addMethod(newClass, setterSel, (IMP)jd_setter, setterMT);
if (isAdd) {
NSLog(@"中间类没有设置过该属性的setter方法");
}
else
{
NSLog(@"中间类已经添加过该属性的setter方法了");
}
return newClass;
}
//自定义添加观察者
- (void)jd_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
{
// 1. 有setter方法的才可以进来(如果是_实例变量名,这里就会有问题了,暂时没考虑这个情况)
if (![self haveSetterMethod:keyPath]) return;
// 2. 如果已经添加了观察了,就不要再添加了
NSMutableArray *mutArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey));
if (mutArr && mutArr.count > 0) {
for (JDInfo *hInfo in mutArr) {
if ([hInfo.keyPath isEqualToString:keyPath]) {
return;
}
}
}
// 3. 保存观察者及keyPath
JDInfo *jdInfo = [[JDInfo alloc] initWithObserver:observer KeyPath:keyPath Options:options];
// 如果从关联表中取不到可变数组再创建,有了就不要创建了,直接放jdInfo进去。
if (!mutArr) {
mutArr = [NSMutableArray arrayWithCapacity:1];
// 还没有加到过当前对象的关联对象中
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey), mutArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
[mutArr addObject:jdInfo];
// 4. 生成或者拿到中间类
Class mediumClass = [self getMediumClass:keyPath];
// 5. 更改对象的isa指向
object_setClass(self, mediumClass);
}
// 移除观察者
- (void)jd_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
// 移除关联对象中的已经添加的JDInfo,否则如果移除之后,再添加同一个对象同一个属性,并且是self还未被销毁过的情况下,
// 关联对象因self没被销毁过,也就不会被销毁,那么就会出现多次添加同一个对象同一个属性进入到数组中,
// 那么在jd_setter方法中,通知回调方法的时候,遍历对比JDInfo.keyPath,就会发现两个相同的keyPath
// 就会执行回调方法两次,添加同一对象同一属性越多,则执行回调的次数越多,严重的bug
NSMutableArray *mutArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)(JDKVOAssociateKey));
if (mutArr.count <= 0) {
return;
}
for (JDInfo *jdInfo in mutArr) {
if ([jdInfo.keyPath isEqualToString:keyPath]) {
[mutArr removeObject:jdInfo];
}
}
// 确定没有属性在被观察了,再改变isa指向,指回原来的类
if (mutArr.count <= 0) {
Class fatherClass = class_getSuperclass(object_getClass(self));
object_setClass(self, fatherClass);
}
}
@end