美文网首页iOS 底层原理 iOS 进阶之路
OC底层原理二十四:自定义KVO

OC底层原理二十四:自定义KVO

作者: markhetao | 来源:发表于2020-10-31 20:00 被阅读0次

    OC底层原理 学习大纲

    上一节,我们介绍了KVO原理,本节我们通过自定义KVO(简化版),来更透彻的理解KVO的原理:

    • 目的:
    1. 模拟系统实现KVO原理
    2. 自动移除观察者
    3. 实现响应式+函数式
    • 回顾上节最后的总结,我先细化为重写核心流程
    1. addObserver时:
      1.1 验证setter方法是否存在
      1.2 注册KVO派生类
      1.3 派生类添加setterclassdealloc方法
      1.4 isa指向派生类
      1.5 保存信息
    2. 触发setter方法时:
      2.1 willChange
      2.1 消息转发(设置原类的属性值)
      2.2 didChange
    3. removeObserver
      3.1 手动移除
      3.2 自动移除

    为了简化步骤,本示例忽略了以下内容:

    1. NSKeyValueObservingOptions 监听类型
    2. observeValueForKeyPath响应类型
    3. context上下文识别值

    本示例中:

    • ViewController有导航控制器根视图,点击Push按钮可跳转PushViewController
    • PushViewController:测试控制器,实现HTPerson属性的添加观察者触发属性变化移除观察者等功能;
    • HTPerosn:继承自NSObject,具备namenickName属性的类
    • 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];
        }
    }
    
      1. HTKVO类的命名前缀关联属性key
    static NSString * const HTKVOPrefix = @"HTKVONotifying_";
    static NSString * const HTKVOAssiociakey = @"HTKVO_AssiociaKey";
    
      1. getter名称中读取setterkey => 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]];
       
    }
    
      1. getter名称中读取settersetKey: => 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派生类

      1. 获取类名 -> 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 派生类添加setterclassdealloc方法

    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个需要注意的点:

      1. 赋值前: 本案例没实现赋值前willChange事件。因为与下面的didChange方式一样,只是状态不同;
      1. 赋值: 调用父类setter方法,我们是通过objc_msgSendSuper进行调用。我们重写objc_super的结构体并完成receiversuper_class的赋值。

    此处有2种写法:

      1. 直接使用objc_msgSendSuper调用,会报参数错误
        image.png

    我们在Build Setting中关闭objc_msgSend的编译检查,即可通过

    image.png
      1. 新创建一个ht_msgSendSuper引用objc_msgSendSuper,这样编译不会报错,不需要关闭编译检查:
        image.png
      1. 赋值后: 我们有2种方法可以实现didChange事件,告知外部:
    • 方式一: 和苹果官方一样,NSObject+HTKVO.h文件中对外公开ht_observeValueForKeyPath函数:

      image.png
      外部PushViewController.m文件中,必须实现ht_observeValueForKeyPath函数:
      image.png
      但是此方法方式让代码很分散,开发者需要在2个地方同时实现ht_addObserverht_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

    补充关联对象相关内容:

      1. 我们创建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
    
      1. 为了快速理解,我们使用了NSMutableArray数组进行存储。
        (事实上,NSMapTable更合适,文末分享)
      1. 我们动态添加关联属性,用于数据存储 (类型为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方法,并将isaKVO衍生类指回了原来的类

    • 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 👉 下载链接
      使用简单,支持blockaction回调,支持自动移除观察者
    1. 使用苹果自带KVO机制;
      (加入中间类FBKVOController进行对象属性记录释放。外部使用FBKVOController类即可)

    2. FBKVOController支持block回调方法回调

    3. FBKVOController支持手动释放观察属性和自动释放观察属性。
      FBKVOController对象被dealloc时,自动释放)

    4. 使用单例类_FBKVOSharedController进行数据管理,其中使用NSMapTable存储数据,存储对象为_FBKVOInfo
      _FBKVOInfo记录_controller_keyPath_options_action_context_block_state

    相关文章

      网友评论

        本文标题:OC底层原理二十四:自定义KVO

        本文链接:https://www.haomeiwen.com/subject/dnkdvktx.html