观察者模式-KVO详解

1 篇文章 0 订阅

志愿不像通知机制那样通过一个通知中心通知所有观察者对象,而是在对象属性变化时通知会被直接发送给观察者对象.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

主要参考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详解

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值