OC底层原理二十四:自定义KVO
上一节,我们介绍了KVO原理,本节我们通过自定义KVO(简化版),来更透彻的理解KVO的原理:
- 目的:
- 模拟系统实现KVO原理
- 自动移除观察者
- 实现响应式+函数式
- 回顾
上节最后的总结,我先细化为重写的核心流程:
-
addObserver时:
1.1验证setter方法是否存在
1.2注册KVO派生类
1.3 派生类添加setter、class、dealloc方法
1.4isa指向派生类
1.5 保存信息 - 触发
setter方法时:
2.1willChange
2.1 消息转发(设置原类的属性值)
2.2didChange -
removeObserver:
3.1 手动移除
3.2 自动移除
为了简化步骤,本示例忽略了以下内容:
NSKeyValueObservingOptions监听类型observeValueForKeyPath响应类型context上下文识别值
本示例中:
ViewController:有导航控制器的根视图,点击Push按钮可跳转PushViewController;PushViewController:测试控制器,实现HTPerson属性的添加观察者、触发属性变化、移除观察者等功能;HTPerosn:继承自NSObject,具备name和nickName属性的类NSObject+HTKVO:重写KVO的相关功能
👉 代码下载
- 准备好了,我们就开始吧 🏃🏃🏃
1. 添加addObserver
// 添加观察者
- (void)ht_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath block:(HTKVOBlock)block {
// 1.1 验证setter方法是否存在
[self judgeSetterMethodFromKeyPath:keyPath];
// 1.2 + 1.3 注册KVO派生类(动态生成子类) 添加方法
Class newClass = [self creatChildClassWithKeyPath:keyPath];
// 1.4 isa的指向: HTKVONotifying_HTPerosn
object_setClass(self, newClass);
// 1.5. 保存信息
HTInfo * info = [[HTInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
[self associatedObjectAddObject:info];
}
1.1 验证setter方法是否存在
- 因为我们
监听的是setter方法,所以当前被监听属性必须具备setter方法。(排除成员变量)
//MARK: - 验证是否存在setter方法
- (void)judgeSetterMethodFromKeyPath:(NSString *) keyPath {
Class class = object_getClass(self);
SEL setterSelector = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod(class, setterSelector);
if (!setterMethod) {
@throw [NSException exceptionWithName: NSInvalidArgumentException
reason:[NSString stringWithFormat:@"当前%@没有setter方法", keyPath]
userInfo:nil];
}
}
HTKVO类的命名前缀,关联属性的key:static NSString * const HTKVOPrefix = @"HTKVONotifying_"; static NSString * const HTKVOAssiociakey = @"HTKVO_AssiociaKey";
- 从
getter名称中读取setter,key => setKey:static NSString * setterForGetter(NSString * getter) { if (getter.length <= 0) return nil; NSString * setterFirstChar = [getter substringToIndex:1].uppercaseString; return [NSString stringWithFormat:@"set%@%@:", setterFirstChar, [getter substringFromIndex:1]]; }
- 从
getter名称中读取setter,setKey: => key:static NSString * getterForSetter(NSString * setter) { if (setter.length <= 0 || ![setter hasPrefix:@"set"] || ![setter hasSuffix:@":"]) return nil; //去除set,获取首字母,设置小写 NSRange range = NSMakeRange(3, 1); NSString * getterFirstChar = [setter substringWithRange:range].lowercaseString; //去除set和首字母,取后部分 range = NSMakeRange(4, setter.length - 5); return [NSString stringWithFormat:@"%@%@",getterFirstChar,[setter substringWithRange:range]]; }
1.2 注册KVO派生类
- 获取类名 -> 2. 生成类 (注册类、重写方法)
重写方法: 方法名sel和类型编码TypeEncoding必须和父类一样,但imp是使用自己的实现内容
- (Class)creatChildClassWithKeyPath: (NSString *) keyPath {
// 1. 类名
NSString * oldClassName = NSStringFromClass([self class]);
NSString * newClassName = [NSString stringWithFormat:@"%@%@",HTKVOPrefix,oldClassName];
// 2. 生成类
Class newClass = NSClassFromString(newClassName);
// 2.1 不存在,创建类
if (!newClass) {
// 2.2.1 申请内存空间 (参数1:父类,参数2:类名,参数3:额外大小)
newClass = objc_allocateClassPair([self class], newClassName.UTF8String, 0);
// 2.2.2 注册类
objc_registerClassPair(newClass);
}
// 2.2.3 动态添加set函数
SEL setterSel = NSSelectorFromString(setterForGetter(keyPath));
Method setterMethod = class_getInstanceMethod([self class], setterSel); //为了保证types和原来的类的Imp保持一致,所以从[self class]提取
const char * setterTypes = method_getTypeEncoding(setterMethod);
class_addMethod(newClass, setterSel, (IMP)ht_setter, setterTypes);
// 2.2.4 动态添加class函数 (为了让外界调用class时,看到的时原来的类,isa需要指向原来的类)
SEL classSel = NSSelectorFromString(@"class");
Method classMethod = class_getInstanceMethod([self class], classSel);
const char * classTypes = method_getTypeEncoding(classMethod);
class_addMethod(newClass, classSel, (IMP)ht_class, classTypes);
// 2.2.5 动态添加dealloc函数
SEL deallocSel = NSSelectorFromString(@"dealloc");
Method deallocMethod = class_getInstanceMethod([self class], deallocSel);
const char * deallocTypes = method_getTypeEncoding(deallocMethod);
class_addMethod(newClass, deallocSel, (IMP)ht_dealloc, deallocTypes);
return newClass;
}
1.3 派生类添加setter、class、dealloc方法
1.3.1 setter方法
static void ht_setter(id self, SEL _cmd, id newValue) {
NSLog(@"新值:%@", newValue);
// 读取getter方法(属性名)
NSString * keyPath = getterForSetter(NSStringFromSelector(_cmd));
// 获取旧值
id oldValue = [self valueForKey:keyPath];
// 1. willChange在此处触发(本示例省略)
// 2. 调用父类的setter方法(消息转发)
// 修改objc_super的值,强制将super_class设置为父类
void(* ht_msgSendSuper)(void *, SEL, id) = (void *)objc_msgSendSuper;
// 创建并赋值
struct objc_super superStruct = {
.receiver = self,
.super_class = class_getSuperclass(object_getClass(self)),
};
ht_msgSendSuper(&superStruct, _cmd, newValue);
// objc_msgSendSuper(&superStruct, _cmd, newValue);
// 3. didChange在此处触发
NSMutableArray * array = objc_getAssociatedObject(self, (__bridge const void * _Nonnull) HTKVOAssiociakey);
for (HTInfo * info in array) {
if([info.keyPath isEqualToString:keyPath] && info.observer){
// 3.1 block回调的方式
if (info.hanldBlock) {
info.hanldBlock(info.observer, keyPath, oldValue, newValue);
}
// // 3.2 调用方法的方式
// if([info.observer respondsToSelector:@selector(ht_observeValueForKeyPath: ofObject: change: context:)]) {
// [info.observer ht_observeValueForKeyPath:keyPath ofObject:self change:@{keyPath: newValue} context:NULL];
// }
}
}
}
外部赋值,触发setter时,有3个需要注意的点:
-
赋值前: 本案例没实现赋值前的willChange事件。因为与下面的didChange方式一样,只是状态不同;
-
-
赋值: 调用父类的setter方法,我们是通过objc_msgSendSuper进行调用。我们重写objc_super的结构体并完成receiver和super_class的赋值。
-
此处有2种写法:
- 直接使用
objc_msgSendSuper调用,会报参数错误:
image.png
我们在
Build Setting中关闭objc_msgSend的编译检查,即可通过
image.png
- 新创建一个
ht_msgSendSuper引用objc_msgSendSuper,这样编译就不会报错,不需要关闭编译检查:
image.png
-
赋值后: 我们有2种方法可以实现didChange事件,告知外部:
-
方式一: 和苹果官方一样,
NSObject+HTKVO.h文件中对外公开ht_observeValueForKeyPath函数:
image.png
外部PushViewController.m文件中,必须实现ht_observeValueForKeyPath函数:
image.png
但是此方法方式让代码很分散,开发者需要在2个地方同时实现ht_addObserver和ht_observeValueForKeyPath两个函数。 所以我们引进了第二种方法:方式二:
响应式 + 函数式,直接在ht_addObserver中添加Block回调代码块,需要响应的时候,我们直接响应block即可。在
NSObject+HTKVO.h中只需要对外声明ht_addObserver一个函数即可。其中包含HTKVOBlock回调类型:
image.png
NSObject+HTKVO.m中响应block:
image.png
外部
PushViewController.m文件中,在实现ht_addObserver函数时,直接实现block响应就行。这样完成了代码的内聚。
image.png
补充
关联对象相关内容:
- 我们创建
HTInfo类,用于记录observer被观察对象、keyPath属性名和hanldBlock回调。
(为了简化研究,我们省略了观察类型、context)//MARK: - HTInfo 信息Model @interface HTInfo : NSObject @property (nonatomic, weak) NSObject *observer; @property (nonatomic, copy) NSString *keyPath; @property (nonatomic, copy) HTKVOBlock hanldBlock; @end @implementation HTInfo - (instancetype) initWithObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath handleBlock:(HTKVOBlock) block { if (self = [super init]) { self.observer = observer; self.keyPath = keyPath; self.hanldBlock = block; } return self; } - (BOOL)isEqual:(HTInfo *)object { return[self.observer isEqual:object.observer] && [self.keyPath isEqualToString:object.keyPath]; } @end
- 为了快速理解,我们使用了
NSMutableArray数组进行存储。
(事实上,NSMapTable更合适,文末分享)
- 我们动态添加
关联属性,用于数据存储(类型为NSMutableArray)。
1.3.2 class方法
-
class方法,主要是让外界读取时,看不到KVO派生类,输出的是原来的类
Class ht_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self)); // 返回当前类的父类(原来的类)
}
1.3.3 dealloc方法
重写了dealloc方法,并将isa从KVO衍生类指回了原来的类。
- 在
isa指回的同时,KVO衍生类会被释放,相应的关联属性也被释放。从而达到了自动移除观察者的效果
void ht_dealloc(id self, SEL _cmd) {
NSLog(@"%s KVO派生类移除了",__func__);
Class superClass = [self class];
object_setClass(self, superClass);
}
1.4 isa指向派生类
// 1.4 isa的指向: HTKVONotifying_HTPerosn
object_setClass(self, newClass);
1.5 保存信息:
- 创建
Info实例保存观察数据
-> 读取关联属性数组(当前所有观察对象)
-> 如果关联属性数组不存在,就创建一个
(使用OBJC_ASSOCIATION_RETAIN_NONATOMIC没关系,因为关联属性不存在强引用,只是记录类名和属性名)
-> 如果被监听对象已存在,直接跳出
->添加监听对象
HTInfo * info = [[HTInfo alloc]initWithObserver:observer forKeyPath:keyPath handleBlock:block];
[self associatedObjectAddObject:info];
- 关联属性添加对象
- (void)associatedObjectAddObject:(HTInfo *)info {
NSMutableArray * mArray = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)HTKVOAssiociakey);
if (!mArray) {
mArray = [NSMutableArray arrayWithCapacity:1];
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)HTKVOAssiociakey, mArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
for (HTInfo * tempInfo in mArray) {
if ([tempInfo isEqual:info]) return;
}
[mArray addObject:info];
}
2. 触发setter方法时
在1.3.1 setter方法中已描述清晰。
主要是三步:willChange -> 设置原类属性 -> didChange
3. removeObserver:
3.1 手动移除:
- 移除指定
被监听属性,如果都被移除了,就将isa指回父类。
- (void)ht_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
NSMutableArray * observerArr = objc_getAssociatedObject(self, (__bridge const void * _Nonnull)HTKVOAssiociakey);
if (observerArr.count <= 0) return;
for (HTInfo * info in observerArr) {
if ([info.keyPath isEqualToString:keyPath]) {
// 移除当前info
[observerArr removeObject:info];
// 重新设置关联对象的值
objc_setAssociatedObject(self, (__bridge const void * _Nonnull)HTKVOAssiociakey, observerArr, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
break;
}
}
// 全部移除后,isa指回父类
if (observerArr.count <= 0) {
Class superClass = [self class];
object_setClass(self, superClass);
}
}
Q:
手动把所有被监听属性都移除,触发isa指回本类,那dealloc触发ht_dealloc触发时,isa会不会指向父类的父类了?
- 不会。因为
isa指回本类后,KVO派生类对象已被释放。不会再进入ht_dealloc。
这也是为什么将isa指回本类,会自动移除观察者。因为派生类对象已被释放,他记录的关联属性也自动被释放。
3.2 自动移除
在1.3.3 dealloc方法中已描述清晰。
KVO其他资源:
- 一、FaceBook的
FBKVOController👉 下载链接
使用简单,支持block和action回调,支持自动移除观察者
使用
苹果自带的KVO机制;
(加入中间类FBKVOController进行对象和属性的记录和释放。外部使用FBKVOController类即可)
FBKVOController支持block回调和方法回调;
FBKVOController支持手动释放观察属性和自动释放观察属性。
(FBKVOController对象被dealloc时,自动释放)使用单例类
_FBKVOSharedController进行数据管理,其中使用NSMapTable存储数据,存储对象为_FBKVOInfo。
(_FBKVOInfo记录_controller、_keyPath、_options、_action、_context、_block、_state)
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png
image.png