之前已經(jīng)了解系統(tǒng)實(shí)現(xiàn)KVO的過程,這篇文章主要說一下自己實(shí)現(xiàn)KVO的思路
開始之前推薦大家看一下這篇文章,很多細(xì)節(jié)這個(gè)里面說的很清楚,并且實(shí)現(xiàn)了block進(jìn)行回調(diào),我這里只是為了模仿系統(tǒng)實(shí)現(xiàn)KVO,之后我也會(huì)更新Block的版本
https://tech.glowing.com/cn/implement-kvo/
下面是我的具體實(shí)現(xiàn),及每一步的作用:
//
// NSObject+LXC_KVO.m
// leetCode
//
// Created by 劉曉晨 on 2021/7/9.
//
#import "NSObject+LXC_KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>
#import "LXCObservationInfo.h"
static NSString *kvo_class_prefix = @"KVOClass_";
const void *kvo_observer = &kvo_observer;
const void *kvo_info = &kvo_info;
@implementation NSObject (LXC_KVO)
- (void)lxc_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context
{
//原始類
Class oldClass = object_getClass(self);
// 拿到原始類名
NSString *oldClassName = NSStringFromClass(oldClass);
//原始SEL
SEL oldSelector = NSSelectorFromString(setterFromGetter(keyPath));
// 1.創(chuàng)建子類
Class kvoClass = [self makeKvoClassWithOriginalClassName:oldClassName];
//2.重寫set方法(本質(zhì)是替換方法的實(shí)現(xiàn))
// cls 添加新方法的類; name 表示selector的方法名稱; imp 指向一個(gè)方法的實(shí)現(xiàn); types 表示我們要添加方法的返回值和參數(shù): v 代表函數(shù)的返回值類型 void, :@ 表示調(diào)用setName:函數(shù)的時(shí)的參數(shù)
Method clazzMethod = class_getInstanceMethod(oldClass, oldSelector);
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClass, oldSelector, (IMP)kvo_setName, types);
// 3.修改isa指針,使得self->isa指向子類
object_setClass(self, kvoClass);
// 4.保存觀察者對象(所謂的循環(huán)引用根源在此)
objc_setAssociatedObject(self, kvo_observer, observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 5.保存上下文及options(系統(tǒng)如何保存目前不知道,希望知道的朋友指教)
LXCObservationInfo *info = [[LXCObservationInfo alloc] init];
info.context = context;
info.options = options;
objc_setAssociatedObject(self, kvo_info, info, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//創(chuàng)建子類
- (Class)makeKvoClassWithOriginalClassName:(NSString *)originalClazzName {
NSString *kvoClazzName = [kvo_class_prefix stringByAppendingString:originalClazzName];
Class clazz = NSClassFromString(kvoClazzName);
if (clazz) {
return clazz;
}
Class originalClazz = object_getClass(self);
//讓新的Class繼承自原始類
Class kvoClazz = objc_allocateClassPair(originalClazz, kvoClazzName.UTF8String, 0);
//仿蘋果隱藏子類(及重寫本類的class對象方法)
Method clazzMethod = class_getInstanceMethod(originalClazz, @selector(class));
const char *types = method_getTypeEncoding(clazzMethod);
class_addMethod(kvoClazz, @selector(class), (IMP)kvo_class, types);
//注冊子類
objc_registerClassPair(kvoClazz);
return kvoClazz;
}
//重寫class方法
Class kvo_class(id self, SEL _cmd) {
return class_getSuperclass(object_getClass(self));
}
//統(tǒng)一管理setter方法
void kvo_setName(id self, SEL _cmd, id newName)
{
// 獲取kvo類型
id class = object_getClass(self);
// 拿到觀察者
id observer = objc_getAssociatedObject(self, kvo_observer);
//信息
LXCObservationInfo *info = objc_getAssociatedObject(self, kvo_info);
//判斷是否監(jiān)聽變化之前
id oldValue;
if (info.options && NSKeyValueObservingOptionOld) {
//1.獲取原值
oldValue = ((id (*)(id,SEL))objc_msgSend)(self, NSSelectorFromString(getterFromSetter(NSStringFromSelector(_cmd))));
}
// 調(diào)用父類的方法(此處還有一種方式是修改self isa 指向原始類,修改后在修改為 子類,這里使用的是系統(tǒng)實(shí)現(xiàn)super的方式,順便可以了解下super和self的區(qū)別)
Class super_class = class_getSuperclass(class);
struct objc_super * _Nonnull super_struct = malloc(sizeof(struct objc_super));
super_struct->receiver = self;
super_struct->super_class = super_class;
objc_msgSendSuper(super_struct, _cmd,newName);
//判斷需要返回哪些值
id newValue;
if (info.options && NSKeyValueObservingOptionNew) {
//1.獲取修改值
newValue = ((id (*)(id,SEL))objc_msgSend)(self, NSSelectorFromString(getterFromSetter(NSStringFromSelector(_cmd))));
}
NSDictionary *dict = [[NSMutableDictionary alloc] init];
if (oldValue != nil) {
[dict setValue:oldValue forKey:NSKeyValueChangeOldKey];
}
if (newValue != nil) {
[dict setValue:newValue forKey:NSKeyValueChangeNewKey];
}
//2.發(fā)送給監(jiān)聽者
objc_msgSend(observer, @selector(observeValueForKeyPath:ofObject:change:context:),getterFromSetter(NSStringFromSelector(_cmd)),observer,dict,info.context);
}
//通過屬性獲取setter字符串
NSString* setterFromGetter(NSString *key) {
if (key.length > 0) {
NSString *resultString = [key stringByReplacingCharactersInRange:NSMakeRange(0,1) withString:[[key substringToIndex:1] capitalizedString]];
return [NSString stringWithFormat:@"set%@:",resultString];
}
return nil;
}
//通過setter 獲取getter
NSString* getterFromSetter(NSString *key) {
if (key.length > 0) {
NSString *resultString = [key substringFromIndex:3];
resultString = [resultString substringToIndex:resultString.length - 1];
return [resultString lowercaseString];
}
return nil;
}
@end
上面kvo_setName中調(diào)用父類的setter方法使用的是系統(tǒng)提供的方法,這里還有一種可以通過修改isa去調(diào)用object_setClass調(diào)用完后在重新指向子類
最后做下測試:
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person new];
_person.ageee = @"123";
NSLog(@"%p ---- %p",[_person class],object_getClassName(_person));
[_person lxc_addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:sizeContext];
NSLog(@"%p ---- %p",[_person class],object_getClassName(_person));
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == sizeContext) {
if ([keyPath isEqualToString:@"age"]) {
if (change[NSKeyValueChangeOldKey] != nil) {
NSLog(@"old = %@",change[NSKeyValueChangeOldKey]);
}
if (change[NSKeyValueChangeNewKey] != nil) {
NSLog(@"new = %@",change[NSKeyValueChangeNewKey]);
}
}
}
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
static int i = 0;
_person.age = [NSString stringWithFormat:@"%d",i++];
}
下一篇,準(zhǔn)備參照開始說的文章,做一個(gè)block回調(diào)的KVO,感謝大家支持!!!