iOS KVO 基础与底层原理

iOS KVO 基础与底层原理

KVO基础

KVO是通过给对象object的属性property注册observer, 然后在被观察property的值改变时候, 会对observer发送消息的这样一种机制.

KVO是iOS中常用的传递消息信息中的一种.相关的API:

  1. addObserver:forKeyPath:options:context: 给对象的某个属性添加observer
  2. observeValueForKeyPath:ofObject:change:context: 属性发生变化时候调用的通知方法
  3. removeObserver:forKeyPath: 移除监听
  4. automaticallyNotifiesObserversForKey:是否自动触发KVO

KVO的手动触发与自动触发

实际上, KVO通知依赖于NSObject的两个方法:willChangeValueForKey:didChangevlueForKey:.

自动触发是指类似这种场景:在注册 KVO 之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了, 系统帮我们调用了前面提到的NSObject的两个方法.

如果我们不希望某一个属性自动触发KVO, 也可以通过一下+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key控制特定的属性, 不要自动发送通知, 该方法对于某key如果返回NO,KVO无法自动运作,需手动触发.下面实例中, 如果namesetter方法没有进行手动出发KVO,那么该属性不会自动触发KVO消息.

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

如果我们要手动触发KVO, 那么可以在改变属性变量的前后手动调用willChangeValueForKey:didChangeValueForKey:, 例子如下:

-(void)setName:(NSString *)name {
    // 如果值相同, 那么不需要发送通知. 如果值不同, 那么发送KVO通知
    if([_name isEqualToString: name]) {
        return;
    }
    [self willChangeValueForKey:@"name"];
    //xxxxxx
    _name = name;
    [self didChangeValueForKey:@"name"];
}

KVO 底层实现的细节

Aapple官方文档中解释道, KVO底层使用了 isa-swizling的技术.如果你了解runtime, 那么你应该知道OC中每个对象/类都有isa指针, 一个对象的isa指针指向object's class, 这个object's class对象中有SEL - IMP的dispatch-table.简而言之, isa 表示这个对象是哪个类的对象.

当给对象的某个属性注册了一个 observer, 那么这个对象的isa指针指向的class会被改变, 此时系统会创建一个新的中间类(intermediate class)继承原来的class, 然后通过runtime 将原来的isa指针指向这个新的中间类.然后中间类会重写setter方法, 重写的 setter 方法会负责在调用原 setter 方法之前和之后添加willChangeValueForKey:, didChangeValueForKey:两个方法,通知所有观察对象值的更改, 从而触发KVO消息.

KVO 实现

自己根据KVO的底层原理实现一个支持block的KVO, 代码基本是根据下面的参考代码完成的.

#import <Foundation/Foundation.h>

typedef void (^PPObservingHandler) (id observedObject, NSString * observedKey, id oldValue, id newValue);

@interface NSObject (KVO)
-(void)pp_addObserver:(NSObject *)object forKey:(NSString *)key withObservingHandler:(PPObservingHandler)observerHandler;

-(void)pp_removeObserver:(NSObject *)object forKey:(NSString *)key;
@end
#import "NSObject+KVO.h"
#import <objc/runtime.h>
#import <objc/message.h>

static NSString *kPPKVOClassPrefix = @"PPObserverPrefix_";
static NSString *kPPKVOAssociateObserver = @"PPAssociateObserver";

@interface PP_ObserverInfo: NSObject
@property (nonatomic, weak) NSObject *observer;
@property (nonatomic, copy) NSString *key;
@property (nonatomic, copy) PPObservingHandler handler;
@end

@implementation PP_ObserverInfo
-(instancetype)initWithObserver:(NSObject *)observer forKey:(NSString *)key observerHandler:(PPObservingHandler)handler{
    if (self = [super init]) {
        _observer = observer;
        self.key = key;
        self.handler = handler;
    }
    return self;
}

@end

#pragma mark - Debug Method
static NSArray *ClassMethodName(Class class){
    NSMutableArray *methodArr = [NSMutableArray array];
    unsigned methodCount = 0;
    Method *methodList = class_copyMethodList(class, &methodCount);
    for (int i = 0; i < methodCount; i++) {
        [methodArr addObject:NSStringFromSelector(method_getName(methodList[i]))];
    }
    free(methodList);
    return methodArr;
}

#pragma mark - Tranfrom setter or getter each other Methods
static NSString *setterForGetter(NSString *getter){
    if (getter.length <= 0) { return nil; }
    NSString * firstString = [[getter substringToIndex: 1] uppercaseString];
    NSString * leaveString = [getter substringFromIndex: 1];

    return [NSString stringWithFormat: @"set%@%@:", firstString, leaveString];
}

static NSString * getterForSetter(NSString * setter)
{
    if (setter.length <= 0 || ![setter hasPrefix: @"set"] || ![setter hasSuffix: @":"]) {

        return nil;
    }

    NSRange range = NSMakeRange(3, setter.length - 4);
    NSString * getter = [setter substringWithRange: range];

    NSString * firstString = [[getter substringToIndex: 1] lowercaseString];
    getter = [getter stringByReplacingCharactersInRange: NSMakeRange(0, 1) withString: firstString];

    return getter;
}

#pragma mark -- Override setter and getter Methods

// 实现了 kvoClass 中的 setter 的内容.
static void KVO_setter(id self, SEL _cmd, id newValue){
    /*
     前置条件, 这个方法是 kvoClass 中的的setter, 而kvoClass的superClass是 originalClass

     基本功能:

     保存oldValue
     [self willChangeValueForKey:getterName];
     [super setXXX:xxx];
     [self didChangeValueForKey:getterName];
     保存newValue
     获取observerInfo, 并将 oldValue newValue当做参数, 然后执行handler

     */
    // 1. 获取 setter 和 getter的 name str
    NSString *setterName = NSStringFromSelector(_cmd);
    NSString *getterName = getterForSetter(setterName);
    if (!getterName) {
        @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %p", self] userInfo: nil];
        return;
    }

    // 2. 保存oldValue
    id oldValue = [self valueForKey:getterName];

    // 3. 获取到super对象
    struct objc_super superClass = {
        .receiver = self,
        .super_class = class_getSuperclass(object_getClass(self))
    };

    // 4. 手动触发KVO -> 1
    [self willChangeValueForKey:getterName];

    // 5. 调用 super 的setter 方法(即 originalClass 的setter方法)
    void(*objc_msgSendSuperKVO)(void *, SEL, id) = (void *)objc_msgSendSuper;
    objc_msgSendSuperKVO(&superClass , _cmd, newValue);

    // 6. 手动触发KVO -> 2
    [self didChangeValueForKey:getterName];

    // 7. 从关联对象中获取 observer array. 找到对于current key进行观察的 observerInfo对象, 然后将 oldValue newValue,作为参数,执行对应的handler
    NSMutableArray *observers = objc_getAssociatedObject(self, (__bridge const void *)kPPKVOAssociateObserver);
    for (PP_ObserverInfo *info in observers) {
        if ([info.key isEqualToString:getterName]) {
            dispatch_async(dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                info.handler(self, getterName, oldValue, newValue);
            });
        }
    }
}

static Class kvo_Class(id self) {
    return class_getSuperclass(object_getClass(self));
}

#pragma mark -- NSObject Category(KVO Reconstruct)
@implementation NSObject (KVO)
-(void)pp_addObserver:(NSObject *)observer forKey:(NSString *)key withObservingHandler:(PPObservingHandler)observerHandler{
    // 1. 获取 setter method 如果没有找到就抛出异常
    SEL setterSelector = NSSelectorFromString(setterForGetter(key)); // 通过 getter方法获取setter方法的名称
    Method setterMethod = class_getInstanceMethod([self class], setterSelector);
    if (!setterMethod) {
        @throw [NSException exceptionWithName: NSInvalidArgumentException reason: [NSString stringWithFormat: @"unrecognized selector sent to instance %@", self] userInfo: nil];
        return;
    }

    // 2. 检查对象 isa 指向的类是不是一个 KVO 类。如果不是,新建一个继承原来类的子类,并把 isa 指向这个新建的子类
    Class observedClass = object_getClass(self); // object_getClass 实际返回的是 class_object 的isa 指针指向的类
    NSString *className = NSStringFromClass(observedClass);

    // 2.1 判断这个类是否我们自己创建的类, 如果不是, 那么创建一个类, 继承原来的class, 然后设置isa指针指向这个新建的类
    if (![className hasPrefix:kPPKVOClassPrefix]) {
        observedClass = [self createKVOClassWithOriginalClassName:className];

        // 改变self object对象的isa 指针
        object_setClass(self, observedClass);
    }


    // 3. 此时 self object的isa指向我们创建的 class, 然后需要检查该类是否重写过没有这个 setter 方法。如果没有,添加重写的 setter 方法;
    if (![self hasSelector: setterSelector]) {
        const char * types = method_getTypeEncoding(setterMethod);
        // 给新类添加setter方法时, 添加 willChangeValueForKey: didChangeValueForKey:, 具体实现在 KVO_setter 中
        class_addMethod(observedClass, setterSelector, (IMP)KVO_setter, types);
    }


    // 4. 将这个 observer 信息封装成 PP_ObserverInfo 对象. 通过runtime关联对象, 关联到self object中, 用一个数组将所有的observerInfo 保存起来
    PP_ObserverInfo * newInfo = [[PP_ObserverInfo alloc] initWithObserver:observer forKey:key observerHandler:observerHandler];
    NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge void *)kPPKVOAssociateObserver);

    if (!observers) {
        observers = [NSMutableArray array];
        objc_setAssociatedObject(self, (__bridge void *)kPPKVOAssociateObserver, observers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    [observers addObject: newInfo];
}

- (void)pp_removeObserver:(NSObject *)object forKey:(NSString *)key {
    // 1. 获取 object 的关联对象, 所有的 observerInfo 的 array
    NSMutableArray * observers = objc_getAssociatedObject(self, (__bridge void *)kPPKVOAssociateObserver);

    // 2. 遍历 observerArray, 找到observerInfo 中 observer 和 key 匹配的 info object, 然后将info对象从array中移除
    PP_ObserverInfo * observerRemoved = nil;
    for (PP_ObserverInfo * observerInfo in observers) {

        if (observerInfo.observer == object && [observerInfo.key isEqualToString: key]) {

            observerRemoved = observerInfo;
            break;
        }
    }
    [observers removeObject: observerRemoved];
}

-(Class)createKVOClassWithOriginalClassName:(NSString *)className{

    // 1. 我们自己创建的中间类对象名称 -- kvoClassName
    NSString *kvoClassName = [kPPKVOClassPrefix stringByAppendingString:className];
    //NSClassFromString是一个很有用的东西,用此函数进行动态加载尝试,如果返回nil,则不能加载此类的实例

    // 2. 在系统中查找, 该中间类是否被注册到runtime中, 如果找到, 直接返回.
    Class observedClass = NSClassFromString(kvoClassName);
    if (observedClass) {
        return observedClass;
    }

    // 3. 如果系统中没有找到中间类, 那么创建这个类.
    // 3.1 获取原始isa指针指向的Class
    Class originalClass = object_getClass(self);
    // 3.2 给原始类创建一个子类, 暂时称为 kvoClassName, 或者 kvoClass
    Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
    // 3.3 获取原始类的 class 方法(isa指针)的 Method对象相关信息
    Method classMethod = class_getInstanceMethod(originalClass, @selector(class));
    const char *types = method_getTypeEncoding(classMethod);

    // 3.4 将原始类的class方法(isa指针),的实现修改成 kvo_Class. 或者说将kvoClass 的 isa 指针指向 原始类.
    // 解释: kvo_Class 的实现:  class_getSuperclass(object_getClass(self)), 其中这个self实际是 kvoClass, 因此 superClass, 是 originalClass.
    class_addMethod(kvoClass, @selector(class), (IMP)kvo_Class, types);

    // 3.5 最后将kvoClass 注册到runtime
    objc_registerClassPair(kvoClass);
    return kvoClass;
}

/**
 返回当前object 对象是否有selector对象
 */
- (BOOL)hasSelector: (SEL)selector {
    Class observedClass = object_getClass(self);
    unsigned int methodCount = 0;
    Method * methodList = class_copyMethodList(observedClass, &methodCount);
    for (int i = 0; i < methodCount; i++) {
        SEL thisSelector = method_getName(methodList[i]);
        if (thisSelector == selector) {
            free(methodList);
            return YES;
        }
    }

    free(methodList);
    return NO;
}

@end

KVO的问题与推荐

在使用KVO中有许多坑, 尤其是多次释放,和重复添加observer的问题,在使用中需要注意, 推荐使用facebook的开源封装, KVOController.

参考资料

如何自己动手实现 KVO

KVO原理分析及使用进阶

https://github.com/okcomp/ImplementKVO

KVOController

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

推荐阅读更多精彩内容