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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343