探究ReactiveCocoa底层之KVO封装流程

序言:之前没用过markDown,大佬反馈说简书的富文本排版不忍直视,趁端午放假,花了一天时间把MarkDown搞熟了,方便好用最主要的是格式还原度简直完美!已经用上瘾了,以后文章都会用MarkDown来写,下面直接上今天的干货:

一、对比原生KVO,初识ReactiveCocoa的KVO


我们先来看一段代码,通过触屏来动态修改视图背景色

@interface ViewController ()
@property (nonatomic, strong)UIColor * bgColor;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    //1/Normal KVO
    [self normalKVO];
    
    //2/RACKVO
    [self racObserver];
}

#pragma mark normalKVO
- (void)normalKVO {
    [self addObserver:self forKeyPath:@"bgColor" options:(NSKeyValueObservingOptionNew) context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    self.view.backgroundColor = [change objectForKey:NSKeyValueChangeNewKey];;
}

- (void)dealloc {
    [self removeObserver:self forKeyPath:@"bgColor"];
}

#pragma mark racKVO
- (void)racObserver {
    [RACObserve(self, bgColor) subscribeNext:^(id  _Nullable x) {
        self.view.backgroundColor = (UIColor *)x;
    }];
}

#pragma mark touch change

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    CGFloat red = arc4random() % 256 / 255.0;
    CGFloat blue = arc4random() % 256 / 255.0;
    CGFloat green = arc4random() % 256 / 255.0;
    
    UIColor * randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];
    self.bgColor = randomColor;
}

@end

从上面步骤我们可以看出原生的KVO使用分为三个步骤:

  1. 添加监听
  2. 实现监听的代理方法
  3. 移除监听

但是RACKVO只是用了非常简单的一段代码就实现了以上的这三个步骤,去掉了胶水代码,真正的做到了面向业务开发,那它是怎么实现的呢,接下来我们来一层层分析

二、深入RAC底层逐层探究KVO实现


  1. 点击RACObserver找到这个宏
#define _RACObserve(TARGET, KEYPATH) \
({ \
    __weak id target_ = (TARGET); \
    [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \
})

继续点进去,
我们会进入NSObject+RACPropertySubscribing.m文件下的


- (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver {
    NSObject *strongObserver = weakObserver;
    keyPath = [keyPath copy];

    NSRecursiveLock *objectLock = [[NSRecursiveLock alloc] init];
    objectLock.name = @"org.reactivecocoa.ReactiveObjC.NSObjectRACPropertySubscribing";

    __weak NSObject *weakSelf = self;

    RACSignal *deallocSignal = [[RACSignal
        zip:@[
            self.rac_willDeallocSignal,
            strongObserver.rac_willDeallocSignal ?: [RACSignal never]
        ]]
        doCompleted:^{
    
            [objectLock lock];
            @onExit {
                [objectLock unlock];
            };
        }];

    return [[[RACSignal
        createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
        
            [objectLock lock];
            
            @onExit {
                [objectLock unlock];
            };

            __strong NSObject *observer __attribute__((objc_precise_lifetime)) = weakObserver;
            __strong NSObject *self __attribute__((objc_precise_lifetime)) = weakSelf;

            if (self == nil) {
                [subscriber sendCompleted];
                return nil;
            }

            return [self rac_observeKeyPath:keyPath options:options observer:observer block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
                [subscriber sendNext:RACTuplePack(value, change)];
            }];
        }]
        takeUntil:deallocSignal]
        setNameWithFormat:@"%@ -rac_valueAndChangesForKeyPath: %@ options: %lu observer: %@", RACDescription(self), keyPath, (unsigned long)options, RACDescription(strongObserver)];
}

我们会发现其中有一个deallocSignal,见名知意,我们先猜这个信号大概是在delloc的时候调用的,至于怎么调用的我们搁在一边;重点来了,return这段代码是重点,我们能够从中发现return的是一个信号RACSignal对象,并且这个signal有一个依赖前提:takeUntil:deallocSignal,KVO取值会一直取到VC释放,当这个VC释放之后,也就没有必要去取值了,也就是说deallocSignal这个信号在VC释放之前会一直执行,VC释放之后功能也会跟着失效,这里我们可以猜出,RACKVO封装思路中,最后一步的释放时机应该是在这里。

好,我们接着分析中间部分的代码,可以看出的是,万物皆信号---RACKVO使用了信号量来处理监听,结合之前信号量生命周期(传送门https://www.jianshu.com/p/bd4fff21d9b7),此处创建了信号,然后把这个信号return了出去,在外面subscribeNext订阅信号,外面订阅信号并同时调用了初始化保存的这个block代码块,代码块里进行completed操作取消订阅,取消订阅之前,在一个这样的代码块中做了订阅者的sendNext操作,这样信号量的生命周期是完整的,但是我们的KVO操作到现在还没有看见,那么只可能在这步操作隐藏了封装的内容

[self rac_observeKeyPath:keyPath options:options observer:observer block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) {
                [subscriber sendNext:RACTuplePack(value, change)];
            }];

也就是return的这部分代码,我们接下来继续分析这部分代码:通过订阅信号时保存的sendNext代码块,把监听到的change值传出去,也就是我们在VC那一个block的调用部分,
重点来了:
点击进去我们能够看到一段很长的代码,前面的一大堆处理略过,来看重点部分,


RACKVOTrampoline *trampoline = [[RACKVOTrampoline alloc] initWithTarget:self observer:strongObserver keyPath:keyPathHead options:trampolineOptions block:^(id trampolineTarget, id trampolineObserver, NSDictionary *change) {
        
        if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
            [firstComponentDisposable() dispose];

            if ((options & NSKeyValueObservingOptionPrior) != 0) {
                block([trampolineTarget valueForKeyPath:keyPath], change, NO, keyPathHasOneComponent);
            }

            return;
        }

        if (value == nil) {
            block(nil, change, NO, keyPathHasOneComponent);
            return;
        }

        RACDisposable *oldFirstComponentDisposable = [firstComponentSerialDisposable swapInDisposable:[RACCompoundDisposable compoundDisposable]];
        [oldFirstComponentDisposable dispose];

        addDeallocObserverToPropertyValue(value);

        if (keyPathHasOneComponent) {
            block(value, change, NO, keyPathHasOneComponent);
            return;
        }

        addObserverToValue(value);
        block([value valueForKeyPath:keyPathTail], change, NO, keyPathHasOneComponent);
    }];
---------------------------------------------------------------
---------------------------------------------------------------
    
    NSObject *value = [self valueForKey:keyPathHead];
    if (value != nil) {
        addDeallocObserverToPropertyValue(value);

        if (!keyPathHasOneComponent) {
            addObserverToValue(value);
        }
    }

    if ((options & NSKeyValueObservingOptionInitial) != 0) {
        id initialValue = [self valueForKeyPath:keyPath];
        NSDictionary *initialChange = @{
            NSKeyValueChangeKindKey: @(NSKeyValueChangeSetting),
            NSKeyValueChangeNewKey: initialValue ?: NSNull.null,
        };
        block(initialValue, initialChange, NO, keyPathHasOneComponent);
    }

    RACCompoundDisposable *observerDisposable = strongObserver.rac_deallocDisposable;
    RACCompoundDisposable *selfDisposable = self.rac_deallocDisposable;
    
    [observerDisposable addDisposable:disposable];
    [selfDisposable addDisposable:disposable];

    return [RACDisposable disposableWithBlock:^{
        [disposable dispose];
        [observerDisposable removeDisposable:disposable];
        [selfDisposable removeDisposable:disposable];
    }];

上面一部分代码可以按分割线分成上下两部,可以看出上部分是KVO实现监听的部分,下面一部分是处理销毁的逻辑。

我们先分析监听上部分这段代码的逻辑,上面这段代码块还是只做中间层传值,RAC又封装了一个中间层对象RACKVOTrampoline,并且由这个对象实现了KVO的监听。点击就进入了RACKVOTrampoline对象的.m实现文件,下面是这个.m的全部代码,这部分代码的解析我直接写在代码中便于分析:

#import "RACKVOTrampoline.h"
#import "NSObject+RACDeallocating.h"
#import "RACCompoundDisposable.h"
#import "RACKVOProxy.h"

@interface RACKVOTrampoline ()

@property (nonatomic, readonly, copy) NSString *keyPath;
@property (nonatomic, readonly, copy) RACKVOBlock block;
@property (nonatomic, readonly, unsafe_unretained) NSObject *unsafeTarget;
@property (nonatomic, readonly, weak) NSObject *weakTarget;
@property (nonatomic, readonly, weak) NSObject *observer;

@end

@implementation RACKVOTrampoline

#pragma mark Lifecycle

- (instancetype)initWithTarget:(__weak NSObject *)target observer:(__weak NSObject *)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(RACKVOBlock)block {
    NSCParameterAssert(keyPath != nil);
    NSCParameterAssert(block != nil);

    NSObject *strongTarget = target;
    if (strongTarget == nil) return nil;

    self = [super init];

    _keyPath = [keyPath copy];

    _block = [block copy];
    _weakTarget = target;
    _unsafeTarget = strongTarget;
    _observer = observer;

    ////1.此处是系统原生的的KVO方法,添加监听,RAC又做了额外的处理,又封装了一个单例中间层对象RACKVOProxy,把当前的vc和keypath,并由RACKVOProxy来监听RACKVOTrampoline的keyPath属性,相当于把代理移交给了这个RACKVOProxy单例中间层对象
    
    [RACKVOProxy.sharedProxy addObserver:self forContext:(__bridge void *)self];
    
    [strongTarget addObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath options:options context:(__bridge void *)self];

    [strongTarget.rac_deallocDisposable addDisposable:self];
    [self.observer.rac_deallocDisposable addDisposable:self];

    return self;
}

- (void)dealloc {
    [self dispose];
}

#pragma mark Observation

//3/释放代码,当前RACKVOTrampoline对象在销毁的时候,会进行移除单例中间层监听对象RACKVOProxy,这里通过信号量生命周期分析得出,信号在销毁的时候,会调用这个dispose,然后取消信号的调用同时取消监听移除RACKVOProxy代理者
- (void)dispose {
    NSObject *target;
    NSObject *observer;

    @synchronized (self) {
        _block = nil;

        // The target should still exist at this point, because we still need to
        // tear down its KVO observation. Therefore, we can use the unsafe
        // reference (and need to, because the weak one will have been zeroed by
        // now).
        target = self.unsafeTarget;
        observer = self.observer;

        _unsafeTarget = nil;
        _observer = nil;
    }

    [target.rac_deallocDisposable removeDisposable:self];
    [observer.rac_deallocDisposable removeDisposable:self];

    [target removeObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath context:(__bridge void *)self];
    [RACKVOProxy.sharedProxy removeObserver:self forContext:(__bridge void *)self];
}

//2、此处是系统原生的KVO代理实现,并且通过Block把KVO监听到的值传出去- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (context != (__bridge void *)self) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }

    RACKVOBlock block;
    id observer;
    id target;

    @synchronized (self) {
        block = self.block;
        observer = self.observer;
        target = self.weakTarget;
    }
    
   //在传出值得做了判断,target不存在的时候,就不传值出去了。否则就把改变的值传出去,通过三次的block代码块回传,传到VC的subscribeNext订阅保存的代码块里,供开发者使用!

    if (block == nil || target == nil) return;

    block(target, observer, change);
}

@end


这样一来,整个流程就很清楚了,RACKVO的设计,首先是集成RACDisposable的子类RACKVOTrampoline,把要监听的对象和keyPath传入封装的信号的子类,实现原生KVO监听,并且考虑到了整体架构的灵活度,又实现了RACKVOProxy类来移交监听,在RACKVOTrampoline系统KVO代理中,利用代码块把改变的值,通过订阅信号时保存的block传出去,在开发者层面上,我们只能看到逻辑紧凑并且简单易用的使用部分。

设计者设计的时候,实现了很多NSObject的分类,但是并不是提供给所有对象使用的,这就是中间层变量的好处了,通过中间层对象单独实现这些分类,整个框架和思路灵活度非常高,代码没有耦合部分,这也是我们需要学习的细节,以后我们在架构项目和设计项目的时候,可以利用这种中间层变量的思想,既能解耦代码,灵活度又非常高,这也是一个好的架构师必备的技能思想。

最后再来顺便瞅瞅RACProxy:
下面是对RACProxy代码部分的分析,主要是初始化了一个表,把observer和context以keyValue的形式存在表里,然后添加的时候设置到表里,移除的时候用key移除,这样PACProxy这个中间层的使用就很灵活,能用于RAC的任何类,可以做到多重自由使用并且利用中间层设计完全可以避免循环引用问题

#import "RACKVOProxy.h"

@interface RACKVOProxy()

@property (strong, nonatomic, readonly) NSMapTable *trampolines;
@property (strong, nonatomic, readonly) dispatch_queue_t queue;

@end

@implementation RACKVOProxy

+ (instancetype)sharedProxy {
    static RACKVOProxy *proxy;
    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{
        proxy = [[self alloc] init];
    });

    return proxy;
}

- (instancetype)init {
    self = [super init];

    _queue = dispatch_queue_create("org.reactivecocoa.ReactiveObjC.RACKVOProxy", DISPATCH_QUEUE_SERIAL);
    _trampolines = [NSMapTable strongToWeakObjectsMapTable];

    return self;
}

- (void)addObserver:(__weak NSObject *)observer forContext:(void *)context {
    NSValue *valueContext = [NSValue valueWithPointer:context];

    dispatch_sync(self.queue, ^{
        [self.trampolines setObject:observer forKey:valueContext];
    });
}

- (void)removeObserver:(NSObject *)observer forContext:(void *)context {
    NSValue *valueContext = [NSValue valueWithPointer:context];

    dispatch_sync(self.queue, ^{
        [self.trampolines removeObjectForKey:valueContext];
    });
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSValue *valueContext = [NSValue valueWithPointer:context];
    __block NSObject *trueObserver;

    dispatch_sync(self.queue, ^{
        trueObserver = [self.trampolines objectForKey:valueContext];
    });

    if (trueObserver != nil) {
        [trueObserver observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

@end

下面是整个RACKVO设计思路总结图,调来调去,花了我整整一下午时间(=@__@=)

D4F54A20-0801-4689-B3F5-3FCB2DF9BEDF.png

溪浣双鲤的技术摸爬滚打之路

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 上半年有段时间做了一个项目,项目中聊天界面用到了音频播放,涉及到进度条,当时做android时候处理的不太好,由于...
    DaZenD阅读 2,983评论 0 26
  • 原文链接:http://www.sprynthesis.com/2014/12/06/reactivecocoa-...
    阳仔dynamics阅读 955评论 0 2
  • 今天,最后一节是体育课,上课铃声响起,老师走进教室,只见他在黑板上画了一条长长的跑道,老师说这节课要练习60米跑步...
    莹宝简书阅读 140评论 0 0
  • In Florida,angel never fly The 17 year old boy I wanna sa...
    露鸣阅读 242评论 3 2