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

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

  • FBKVOController 参阅这份分析

  • NSHashTable和NSMapTable的用法 可参阅

  • 二、GNU源码 下载链接

    image.png

    GNU最牛的A货 ,可参阅它推测还原的Fundation库。直接参考源码,了解KVO的机制。
    image.png

你可能感兴趣的:(OC底层原理二十四:自定义KVO)