自己實(shí)現(xiàn)KVO(代理方式)

之前已經(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,感謝大家支持!!!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容