RAC 不是万能的

有句话说的好,“RP 不是万能的,没有 RP 却是万万不能的”。在 iOS 开发中,RP(响应式编程) 用得越来越多,其中我们最常用的响应式开发框架就是 ReactiveCocoa(俗称 RAC)。无可否认,RAC 的功能确实非常强大,可以用它完美替代 KVO、delegate、通知、UI 事件处理机制、定时器等一切对象通信机制,以至于很多同学把 RAC 当成了万应灵药。

那么 RAC 是万能的吗?程序员可以在任何地方使用它吗?

答案当然是不。与此相反,RAC 一旦使用不当,它就会变成致命的毒药。接下来,我们将列举两个 RAC 不适用的例子。

rac_textSignal 对委托模型的破坏

rac_textSignal 是对 UITextField/UITextView 委托模型的替代,但这种替代是破坏性的。

也就是说,一旦你使用了 rac_textSignal,你将无法在 UITextField/UITextView 上使用委托模式。

原因就在于:

  1. rac_textSignal 信号在运行时中使用了 UITextField/UITextView 的委托模型,由于委托模型是单对单模式,所以原来的委托机制就被 rac_textSignal 阻断了,此时再用 textFieldDidChange/textViewDidChange 就不再生效了。如果一定要使用 UITextField/UITextView 的委托机制,就不能使用 rac_textSignal。或者将 rac_textSignal 替换成 KVO 信号(即 RACObserve),但这会导致第二种情况发生。
  2. rac_textSignal 信号不仅仅使用了 delegate,也覆盖了 KVO。也就是说,rac_textSignal 是 delegate+KVO 全覆盖的,不仅仅会在委托事件中触发信号,而且在 text 属性被代码修改时触发信号。前者是由用户按键产生的,后者是由程序员用代码修改 text 属性产生的。也就是说,rac_textSignal 不仅能监听用户按键,也能监听 text 属性的改变。

这就导致了一种潜在的问题。考虑这样一种场景。如果你需要限制 TextField 中用户能够输入的字数。通常我们会在 rac_textSignal 订阅块中,对输入的字符数进行判断,一旦它超过限制,我们就对字符串进行截断。这很简单,简单到可能只需要一行代码。但这样就会在订阅块中直接修改到 textfield 的 text 属性,由于代码修改 text 属性导致 textfiel 的 KVO 事件发生,那么又会触发 rac_textSingal 信号。如果你没有对修改 text 的语句进行条件判断,那么这就会导致死循环发生,app 崩溃。

如果在中文输入法打开的情况下,还会导致重复输入候选词条的情况。比如你输入"df",文本框中会出现多次重复的"dfdfdfdf"。

在调试模式下,rac_textSignal 还会导致 delete 字符自动重复的情况。即按一下 delete 键,发送两个 delete 字符。

cell 复用的问题

rac_textSignal 用在复用的 cell 时问题更大。

有时候问为了监听 cell 中 textfield 的文字改变,我们会在 cellForItemAtIndex 方法中使用这样的代码:

TextFieldCell* cell = [self.collectionContext 	dequeueReusableCellOfClass:TextFieldCell.class 	forSectionController:self atIndex:index];
            
cell.tfText.text= dev.management;
[[[cell.tfText rac\_textSignal] skip:1] 	subscribeNext:^(NSString * _Nullable x) {
		self.device.management = x;
}];

这样的问题在于,由于 cell 是复用的,当 cell 复用时,rac_textSignal 会被重复订阅。当你从缓存中复用一个 cell 时,这个 cell 的 rac_textSignal 很可能已经被订阅过了。这个时候你再次订阅 rac_textSignal 信号,就重复订阅了。导致用户在当前 cell 的文本框中输入值后,重复调用订阅块,即可能改变了多个值。比如,明明用户在输入姓名时输入的是 a,但莫名其妙不仅仅是用户的姓名被修改为 a 了,用户的邮箱、手机号都被修改为 a 了。

这个问题一般很难排查,因为 cell 很可能不被复用,或者什么时候会复用一个 cell 我们也无从知道,cell 的复用机制位于 UITableView/UICollectionView 的“黑盒子”中。

更糟糕的是,RAC 本身无法解决这个问题。你可能想在订阅一个 rac_textSignal 信号前将同一个 textfield 上的 rac_textSignal 信号停止,或者取消其它订阅者,但 RAC 本身没有提供这样的机制。只有在订阅时保持住一个 RACDisposable,然后在每次订阅前调用 dispose 方法。

// 有多少个 textfield 就要有多少个 RACDisposal 属性
@property (strong, nonatomic) RACDisposable* disposable1;
@property (strong, nonatomic) RACDisposable* disposable2;
@property (strong, nonatomic) RACDisposable* disposable3;

... ...

// 在订阅之前 dispose,有多少个 textfield 就要 dispose 多少次
	[_disposable1 dispose];
	_disposable1 = [[cell.tfText rac\_textSignal] subscribeNext:^(NSString * _Nullable x) {
                self.device.management = x;
            }];
... ...

这个方式和另外一种方式一样,假设每个 cell 上都有一个 textfield,那么有多少个 cell 就得声明多少个属性,如果 cell 的个数不固定,这种方案不是很美妙啊。

另外一种方式就是保持住每一个 cell,即有多少 cell 就声明多少属性,每个 cell 属性都使用懒加载的方式,即只有在第一次初始化时订阅 rac_textSignal,这样就不会重复订阅。

当然最完美的解决办法就是不使用 rac_textSignal,而是用 UITextFieldDelegate,同时通过 index 来识别这是哪个 textfield:

cell.tfText.tag = index;
cell.tfText.delegate = self;

......

-(void)textFieldDidEndEditing:(UITextField *)textField{
    switch (textField.tag) {
        case 5:{// 管理部门
            _device.management = textField.text;
            break;
        }
        case 6:{// 管理人员
            _device.managementUser = textField.text;
            break;
        }
        case 7:{// 负责人员
            _device.liableUser = textField.text;
            break;
        }
        default:
            break;
    }
}

你可能感兴趣的:(iPhone开发)