欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

观察者模式-KVO详解

程序员文章站 2022-04-13 12:21:54
...

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

观察者模式-KVO详解

KVO(键值观察)

KVO(键值观察)是Objective-C对观察者模式(Observer Pattern)的实现。也是Cocoa Binding的基础。可以用于监听某个对象属性值的改变,当被观察对象的某个属性发生更改时,观察者对象会获得通知。

KVO(Key-Value Observing)

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

#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

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

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方法,让我们误认为还是使用的当前类,从而达到隐藏生成的派生类。

观察者模式-KVO详解

自定义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详解

相关标签: KVO