觀察者模式-KVO詳解

KVO不像通知機制那樣通過一個通知中心通知所有觀察者對象坯墨,而是在對象屬性變化時通知會被直接發(fā)送給觀察者對象.KVO機制解析圖:

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

KVO(Key-Value Observing)

KVO(Key-Value Observing) 是 Objective-C 對觀察者模式(Observer Pattern)的實現(xiàn)寂汇。也是 Cocoa Binding 的基礎。當被觀察對象的某個屬性發(fā)生更改時捣染,觀察者對象會獲得通知骄瓣。

KVO內部實現(xiàn)原理

二話不說,直接擼起袖子就是干耍攘。

#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屬性進行監(jiān)聽榕栏,看看監(jiān)聽前后的打印變化:

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之后,系統(tǒng)通過runtime動態(tài)的創(chuàng)建一個派生類NSKVONotifying_Person蕾各,而根據(jù)class_getSuperclass得到的結果竟然是Person扒磁,然后age是使我們KVO需要觀察的屬性,它的setter函數(shù)指針變了式曲。而我們也知道渗磅,所謂的OC的消息機制是通過isa去查找實現(xiàn)的,那么我們可以得到一下結論:

  • KVO是基于runtime機制實現(xiàn)的

  • 當某個類的屬性對象第一次被觀察時检访,系統(tǒng)就會在運行期動態(tài)地創(chuàng)建該類的一個派生類始鱼,在這個派生類中重寫基類中任何被觀察屬性的setter 方法。派生類在被重寫的setter方法內實現(xiàn)真正的通知機制

  • 如果原類為Person脆贵,那么生成的派生類名為NSKVONotifying_Person

  • 每個類對象中都有一個isa指針指向當前類医清,當一個類對象的第一次被觀察,那么系統(tǒng)會偷偷將isa指針指向動態(tài)生成的派生類卖氨,從而在給被監(jiān)控屬性賦值時執(zhí)行的是派生類的setter方法

  • 鍵值觀察通知依賴于NSObject 的兩個方法: willChangeValueForKey:didChangevlueForKey:会烙;在一個被觀察屬性發(fā)生改變之前, willChangeValueForKey:一定會被調用筒捺,這就 會記錄舊的值柏腻。而當改變發(fā)生后,didChangeValueForKey:會被調用系吭,繼而 observeValueForKey:ofObject:change:context: 也會被調用五嫂。

  • 補充:KVO的這套實現(xiàn)機制中蘋果還偷偷重寫了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,/** 監(jiān)聽 */
    ZQKVOInfoStateNotObserving,/** 為監(jiān)聽 */
};
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;
/** 添加監(jiān)聽 */
- (void)observe:(id)object info:(nullable ZQKVOInfo *)info;
/** 取消監(jiān)聽 */
- (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) {
        /** 移除相同路徑的監(jiān)聽 */
        [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詳解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市则吟,隨后出現(xiàn)的幾起案子槐臀,更是在濱河造成了極大的恐慌,老刑警劉巖氓仲,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件水慨,死亡現(xiàn)場離奇詭異,居然都是意外死亡敬扛,警方通過查閱死者的電腦和手機晰洒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舔哪,“玉大人欢顷,你說我怎么就攤上這事∽皆椋” “怎么了抬驴?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缆巧。 經常有香客問我布持,道長,這世上最難降的妖魔是什么陕悬? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任题暖,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘胧卤。我一直安慰自己唯绍,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布枝誊。 她就那樣靜靜地躺著况芒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叶撒。 梳的紋絲不亂的頭發(fā)上绝骚,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音祠够,去河邊找鬼压汪。 笑死,一個胖子當著我的面吹牛古瓤,可吹牛的內容都是我干的止剖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼湿滓,長吁一口氣:“原來是場噩夢啊……” “哼滴须!你這毒婦竟也來了?” 一聲冷哼從身側響起叽奥,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤扔水,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后朝氓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體魔市,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年赵哲,在試婚紗的時候發(fā)現(xiàn)自己被綠了待德。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡枫夺,死狀恐怖将宪,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情橡庞,我是刑警寧澤较坛,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站扒最,受9級特大地震影響丑勤,放射性物質發(fā)生泄漏。R本人自食惡果不足惜吧趣,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一法竞、第九天 我趴在偏房一處隱蔽的房頂上張望耙厚。 院中可真熱鬧,春花似錦岔霸、人聲如沸薛躬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泛豪。三九已至,卻和暖如春侦鹏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背臀叙。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工略水, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人劝萤。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓渊涝,卻偏偏與公主長得像,于是被迫代替她去往敵國和親床嫌。 傳聞我的和親對象是個殘疾皇子跨释,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴謹 對...
    cosWriter閱讀 11,103評論 1 32
  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結構(3).初始化時...
    歐辰_OSR閱讀 29,392評論 8 265
  • KVC KVC定義 KVC(Key-value coding)鍵值編碼,就是指iOS的開發(fā)中厌处,可以允許開發(fā)者通過K...
    暮年古稀ZC閱讀 2,145評論 2 9
  • OC語言基礎 1.類與對象 類方法 OC的類方法只有2種:靜態(tài)方法和實例方法兩種 在OC中鳖谈,只要方法聲明在@int...
    奇異果好補閱讀 4,274評論 0 11
  • 為什么意志力能打敗動力? 動力是好東西阔涉,只是不可靠而已缆娃,借助意志力,動力會變得更加可靠瑰排;而且如果先采取行...
    曾小雄_xwx閱讀 370評論 0 0