观察者模式-KVO详解

KVO不像通知机制那样通过一个通知中心通知所有观察者对象,而是在对象属性变化时通知会被直接发送给观察者对象.KVO机制解析图:

屏幕快照 2018-08-23 上午10.10.50.png

KVO(Key-Value Observing)

KVO(Key-Value Observing) 是 Objective-C 对观察者模式(Observer Pattern)的实现。也是 Cocoa Binding 的基础。当被观察对象的某个属性发生更改时,观察者对象会获得通知。

KVO内部实现原理

二话不说,直接撸起袖子就是干。

#import "Person.h"
#import <objc/runtime.h>

@implementation Person

-(void)printInfo{
    NSLog(@"isa:%@,supperclass:%@",NSStringFromClass(object_getClass(self)),
          class_getSuperclass(object_getClass(self)));
    NSLog(@"age setter function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(setAge:)));
    NSLog(@"printInfo function pointer:%p", class_getMethodImplementation(object_getClass(self), @selector(printInfo)));
}
@end

然后我们进行对Person属性进行监听,看看监听前后的打印变化:

static NSString *privateKVOContext = @"privateKVOContext";
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc]init];
    NSLog(@"Before add observer————————————————————————–");
    [person printInfo];
    [person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:&privateKVOContext];
    NSLog(@"After add observer————————————————————————–");
    [person printInfo];
    [person removeObserver:self forKeyPath:@"age"]; 
    NSLog(@"After remove observer————————————————————————–");
    [person printInfo];
}

输出结果:

2018-08-23 10:29:54.631956+0800 KVO原理解析-18-8-23-0[1448:53790] Before add observer————————————————————————–
2018-08-23 10:29:54.632077+0800 KVO原理解析-18-8-23-0[1448:53790] isa:Person,supperclass:NSObject
2018-08-23 10:29:54.632199+0800 KVO原理解析-18-8-23-0[1448:53790] age setter function pointer:0x10899e510
2018-08-23 10:29:54.632339+0800 KVO原理解析-18-8-23-0[1448:53790] printInfo function pointer:0x10899e420
2018-08-23 10:29:54.632560+0800 KVO原理解析-18-8-23-0[1448:53790] After add observer————————————————————————–
2018-08-23 10:29:54.632673+0800 KVO原理解析-18-8-23-0[1448:53790] isa:NSKVONotifying_Person,supperclass:Person
2018-08-23 10:29:54.632729+0800 KVO原理解析-18-8-23-0[1448:53790] age setter function pointer:0x108cdea7a
2018-08-23 10:29:54.632780+0800 KVO原理解析-18-8-23-0[1448:53790] printInfo function pointer:0x10899e420
2018-08-23 10:29:54.632876+0800 KVO原理解析-18-8-23-0[1448:53790] After remove observer————————————————————————–
2018-08-23 10:29:54.632930+0800 KVO原理解析-18-8-23-0[1448:53790] isa:Person,supperclass:NSObject
2018-08-23 10:29:54.633004+0800 KVO原理解析-18-8-23-0[1448:53790] age setter function pointer:0x10899e510
2018-08-23 10:29:54.633051+0800 KVO原理解析-18-8-23-0[1448:53790] printInfo function pointer:0x10899e420

通过输出结果分析:在对Person的属性age添加KVO之后,系统通过runtime动态的创建一个派生类NSKVONotifying_Person,而根据class_getSuperclass得到的结果竟然是Person,然后age是使我们KVO需要观察的属性,它的setter函数指针变了。而我们也知道,所谓的OC的消息机制是通过isa去查找实现的,那么我们可以得到一下结论:

  • KVO是基于runtime机制实现的

  • 当某个类的属性对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的setter方法内实现真正的通知机制

  • 如果原类为Person,那么生成的派生类名为NSKVONotifying_Person

  • 每个类对象中都有一个isa指针指向当前类,当一个类对象的第一次被观察,那么系统会偷偷将isa指针指向动态生成的派生类,从而在给被监控属性赋值时执行的是派生类的setter方法

  • 键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey:didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

  • 补充:KVO的这套实现机制中苹果还偷偷重写了class方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类。

1429890-b28e010d3a7dbdb8.png

自定义KVO

主要参考KVOController

#import <Foundation/Foundation.h>

typedef void(^KVOObserveBlk)(NSString *keyPath,id observeObj,NSDictionary *valueChange);

@interface ZQKVOController : NSObject
+(instancetype)controllerWithObserver:(nullable id)observer;
@property (nullable, nonatomic, weak, readonly) id observer;
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(KVOObserveBlk)block;
@end
#import "ZQKVOController.h"
#import <pthread.h>

#pragma mark Utilities -
typedef NS_ENUM(uint8_t, ZQKVOInfoState) {
    ZQKVOInfoStateInitial = 0,/** 初始化 */
    ZQKVOInfoStateObserving,/** 监听 */
    ZQKVOInfoStateNotObserving,/** 为监听 */
};
NSString *const ZQKVONotificationKeyPathKey = @"FBKVONotificationKeyPathKey";
@interface ZQKVOInfo : NSObject

@end

@implementation ZQKVOInfo 
{
@public
    KVOObserveBlk _block;
    __weak ZQKVOController *_controller;
    NSString *_keyPath;
    NSKeyValueObservingOptions _options;
    void *_context;
    ZQKVOInfoState _state;
}
- (instancetype)initWithController:(ZQKVOController *)controller
                           keyPath:(NSString *)keyPath
                           options:(NSKeyValueObservingOptions)options
                             block:(nullable KVOObserveBlk)block
                           context:(nullable void *)context
{
    self = [super init];
    if (nil != self) {
        _controller = controller;
        _block = [block copy];
        _keyPath = [keyPath copy];
        _options = options;
        _context = context;
    }
    return self;
}
@end
@interface ZQKVOSharedController : NSObject

/**  */
+ (instancetype)sharedController;
/** 添加监听 */
- (void)observe:(id)object info:(nullable ZQKVOInfo *)info;
/** 取消监听 */
- (void)unobserve:(id)object infos:(nullable NSSet *)infos;
@end

@implementation ZQKVOSharedController
{
    NSHashTable<ZQKVOInfo *> *_infos;
    pthread_mutex_t _mutex;/** 互斥锁 */
}

+ (instancetype)sharedController
{
    static ZQKVOSharedController *_controller = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _controller = [[ZQKVOSharedController alloc] init];
    });
    return _controller;
}
- (instancetype)init
{
    self = [super init];
    if (nil != self) {
        NSHashTable *infos = [NSHashTable alloc];
#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
        _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#elif defined(__MAC_OS_X_VERSION_MIN_REQUIRED)
        if ([NSHashTable respondsToSelector:@selector(weakObjectsHashTable)]) {
            _infos = [infos initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
        } else {
            // silence deprecated warnings
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
            _infos = [infos initWithOptions:NSPointerFunctionsZeroingWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
#pragma clang diagnostic pop
        }

#endif
        pthread_mutex_init(&_mutex, NULL);
    }
    return self;
}

- (void)observe:(id)object info:(nullable ZQKVOInfo *)info
{
    if (nil == info) {
        return;
    }

    // register info
    pthread_mutex_lock(&_mutex);
    [_infos addObject:info];
    pthread_mutex_unlock(&_mutex);

    // add observer
    [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:(void *)info];

    if (info->_state == ZQKVOInfoStateInitial) {
        info->_state = ZQKVOInfoStateObserving;
    } else if (info->_state == ZQKVOInfoStateNotObserving) {
        /** 移除相同路径的监听 */
        [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
    }
}

- (void)unobserve:(id)object infos:(nullable NSSet<ZQKVOInfo *> *)infos
{
    if (0 == infos.count) {
        return;
    }
    // unregister info
    pthread_mutex_lock(&_mutex);
    for (ZQKVOInfo *info in infos) {
        [_infos removeObject:info];
    }
    pthread_mutex_unlock(&_mutex);

    for (ZQKVOInfo *info in infos) {
        if (info->_state == ZQKVOInfoStateObserving) {
            [object removeObserver:self forKeyPath:info->_keyPath context:(void *)info];
        }
        info->_state = ZQKVOInfoStateNotObserving;
    }
}

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
    NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);

    ZQKVOInfo *info;

    {
        // lookup context in registered infos, taking out a strong reference only if it exists
        pthread_mutex_lock(&_mutex);
        info = [_infos member:(__bridge id)context];
        pthread_mutex_unlock(&_mutex);
    }

    if (nil != info) {

        // take strong reference to controller
        ZQKVOController *controller = info->_controller;
        if (nil != controller) {
            // take strong reference to observer
            id observer = controller.observer;
            if (nil != observer) {
                if (info->_block) {
                    info->_block(keyPath, object, change);
                }  else {
                    [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context];
                }
            }
        }
    }
}

- (void)dealloc
{
    pthread_mutex_destroy(&_mutex);
}

@end

@implementation ZQKVOController
{
    NSMapTable<id, NSMutableSet<ZQKVOInfo *> *> *_objectInfosMap;
    pthread_mutex_t _lock;
}

+ (instancetype)controllerWithObserver:(nullable id)observer
{
    return [[self alloc] initWithObserver:observer retainObserved:YES];
}

- (instancetype)initWithObserver:(nullable id)observer retainObserved:(BOOL)retainObserved
{
    self = [super init];
    if (nil != self) {
        _observer = observer;
        NSPointerFunctionsOptions keyOptions = retainObserved ? NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPointerPersonality : NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality;
        _objectInfosMap = [[NSMapTable alloc] initWithKeyOptions:keyOptions valueOptions:NSPointerFunctionsStrongMemory|NSPointerFunctionsObjectPersonality capacity:0];
        pthread_mutex_init(&_lock, NULL);
    }
    return self;
}

#pragma mark Utilities -
- (void)observe:(id)object info:(ZQKVOInfo *)info
{
    pthread_mutex_lock(&_lock);

    NSMutableSet *infos = [_objectInfosMap objectForKey:object];

    // check for info existence
    ZQKVOInfo *existingInfo = [infos member:info];
    if (nil != existingInfo) {
        pthread_mutex_unlock(&_lock);
        return;
    }

    // lazilly create set of infos
    if (nil == infos) {
        infos = [NSMutableSet set];
        [_objectInfosMap setObject:infos forKey:object];
    }

    // add info and oberve
    [infos addObject:info];

    // unlock prior to callout
    pthread_mutex_unlock(&_lock);

    [[ZQKVOSharedController sharedController] observe:object info:info];
}
#pragma mark API -
- (void)observe:(nullable id)object keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(KVOObserveBlk)block{
    NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    if (nil == object || 0 == keyPath.length || NULL == block) {
        return;
    }
    ZQKVOInfo *info = [[ZQKVOInfo alloc]initWithController:self keyPath:keyPath options:options block:block context:NULL];
    [self observe:object info:info];
}

- (void)dealloc
{
    pthread_mutex_lock(&_lock);
    NSMapTable *objectInfoMaps = [_objectInfosMap copy];
    // clear table and map
    [_objectInfosMap removeAllObjects];
    // unlock
    pthread_mutex_unlock(&_lock);

    ZQKVOSharedController *shareController = [ZQKVOSharedController sharedController];
    for (id object in objectInfoMaps) {
        // unobserve each registered object and infos
        NSSet *infos = [objectInfoMaps objectForKey:object];
        [shareController unobserve:object infos:infos];
    }
    pthread_mutex_destroy(&_lock);
}

应用:

- (void)viewDidLoad {
    [super viewDidLoad];
    _person = [[Person alloc]init];
    _kvoController = [ZQKVOController controllerWithObserver:self];
    [_kvoController observe:_person keyPath:@"age" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(NSString *keyPath, id observeObj, NSDictionary *valueChange) {
        NSLog(@"keyPath:%@ age:%@",keyPath,valueChange[NSKeyValueChangeNewKey]);
    }];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    _person.age++;
}

下一篇:KVC详解

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,101评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,386评论 8 265
  • KVC KVC定义 KVC(Key-value coding)键值编码,就是指iOS的开发中,可以允许开发者通过K...
    暮年古稀ZC阅读 2,142评论 2 9
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,273评论 0 11
  • 为什么意志力能打败动力? 动力是好东西,只是不可靠而已,借助意志力,动力会变得更加可靠;而且如果先采取行...
    曾小雄_xwx阅读 369评论 0 0