目录
一、什么是MVVM
二、为什么使用MVVM
三、RAC
3.1 RAC的用法
四、RAC与MVVM架构设计的优点
五、MVVM + RAC
5.1 MVVM使用指南
5.2 RAC坑
MVVM(Model - View/ViewController - ViewModel)是对MVC的一种变形设计模式,解决ViewController代码臃肿、View和Model模块耦合严重两个主要问题。抽出ViewModel来处理ViewController的业务逻辑,分离了UI代码和业务逻辑,并且ViewModel监听model事件,一旦发生变化更新视图,很好的解决了视图与模型的依赖性。看下图:
在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件; view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel);viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方。
虽然MVC 依然是目前主流客户端编程框架,但同时它也被调侃成Massive View Controller(重量级视图控制器),想必大家在开发中无可避免被下面几个问题所困扰:
为了避免和解决上述问题的产生,从MVC引申出来一种维护性较强、耦合性低的新的架构MVVM,主要目的是为了分离视图(View)和模型(Model)。
MVVM优势:
ReactiveCocoa(简称为RAC),是由Github开源的一个应用于iOS和OS开发的新框架。结合了几种编程风格:函数式编程 和 响应式编程 ,使用RAC解决问题,就不需要考虑调用顺序,直接考虑结果,把每一次操作都写成一系列嵌套的方法中,使代码高聚合,方便管理。
函数式编程思想:是把操作尽量写成一系列嵌套的函数或者方法调用。
响应式编程思想:不需要考虑调用顺序,只需要考虑结果,产生一个事件,事件传播出去,然后影响结果。
在RAC中最核心的类RACSignal:信号类,一般表示将来有数据传递,只要有数据改变,信号内部接收到数据,就会马上发出数据。
注意:
RACSignal使用步骤:
案例:
//1.创建信号
RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id subscriber) {
// 2.发送信号
[subscriber sendNext:@1];
// 如果不在发送数据,最好发送信号完成,内部会自动调用[RACDisposable disposable]取消订阅信号。
[subscriber sendCompleted];
}];
// 3.订阅信号
[siganl subscribeNext:^(id x) {
NSLog(@"接收到数据:%@",x);
}];
2、RACSubject:信号提供者,自己可以充当信号,又能发送信号。
RACSubject使用步骤:
创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
发送信号 sendNext:(id)value
案例:
// 1.创建信号
RACSubject *subject = [RACSubject subject];
// 2.订阅信号
[subject subscribeNext:^(id x) {
NSLog(@"第一个订阅者%@",x);
}];
// 3.发送信号
[subject sendNext:@"1"];
3、RACReplaySubject:重复提供信号类,RACSubject的子类,如果一个信号每被订阅一次,就需要把之前的值重复发送一遍,使用重复提供信号类。
// 1.创建信号
RACReplaySubject *replaySubject = [RACReplaySubject subject];
// 2.发送信号
[replaySubject sendNext:@1];
// 3.订阅信号
[replaySubject subscribeNext:^(id x) {
NSLog(@"第一个订阅者接收到的数据%@",x);
}];
4、rac_signalForControlEvents:用于监听某个事件。
// 需求:把按钮点击事件转换为信号,点击按钮,就会发送信号
@weakify(self)
[[[self.addMemberButton rac_signalForControlEvents:UIControlEventTouchUpInside] deliverOnMainThread] subscribeNext:^(id x) {
@strongify(self)
NSLog(@" %@ 按钮被点击了",self.addMemberButton);
}];
5、rac_addObserverForName:用于监听某个通知。
//需求:截图事件日志上报
[[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationUserDidTakeScreenshotNotification object:nil]
takeUntil:[self rac_willDeallocSignal]] subscribeNext:^(id x) {
[DXScreenshotHelper uploadLogInfo];
}];
6、merge:将多个信号合并成一个信号。
- (RACSignal *)searchNeedsStopSignal
{
if (!_searchNeedsStopSignal) {
RACSignal *searchBarCancel = [self rac_signalForSelector:@selector(searchBarCancelButtonClicked:)
fromProtocol:@protocol(UISearchBarDelegate)];
RACSignal *didSelectTableViewCell = [self rac_signalForSelector:@selector(tableView:didSelectRowAtIndexPath:)
fromProtocol:@protocol(UITableViewDelegate)];
_searchNeedsStopSignal = [[RACSignal merge:@[searchBarCancel, didSelectTableViewCell]] takeUntil:[self rac_willDeallocSignal]];
}
return _searchNeedsStopSignal;
}
7、RAC(TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定。
RAC(self, unreadCount) = [[[[RACObserve([DXKFChannelManager shareInstance].chatChannel, totalUnreadCount) takeUntil:self.rac_willDeallocSignal] deliverOnMainThread] doNext:^(NSNumber *number) {
DDLogVerbose(@"%s, Total unread count = %@", __FUNCTION__, number);
uint64_t microAppID = [DXKFDataManager shareInstance].infoItem.microAppId;
if (microAppID > 0) {
[DXMicroAppLocalAlertManager addLocalAlertPointWithMicroAppId:microAppID count:number.integerValue];
}
}];
8、RACObserve(self, name):监听某个对象的某个属性,返回的是信号。
@weakify(self)
[[[RACObserve([UISDKManager sharedManager], connectStatus) takeUntil:self.rac_willDeallocSignal] deliverOnMainThread] subscribeNext:^(NSNumber *number) {
@strongify(self);
UIConnectStatus connectStatus = (UIConnectStatus)number.integerValue;
switch (connectStatus) {
case UIConnectStatusLogined:// 断网重连
{
[[DXKFDataManager shareInstance] cleanAllKFInfoMemoryCache];
[[DXKFDataManager shareInstance] queryKFSystemEnabled:^(BOOL enabled) {
[self getKeFuCurrentStatus];
if (!enabled) {
[self resignNotice];
}
}];
break;
}
default:
break;
}
}];
1、从上面RAC的用法可以看出,RAC的绑定,这是常用的。比如:用户信息展示界面->登录界面->登录成功->回到用户信息展示界面->展示用户信息,以往我们的做法通常是通知,也可以用协议、block什么的,一旦代码量多了过后,耦合度高,维护成本就会增加,而使用RAC的属性绑定、属性联合等一系列方法,将会有事半功倍的效果,这样就可以解决相当多的需求了。
2、MVVM搭建思路里面会涉及大量的属性绑定、事件传递来实现不同功能,运用RAC能大量简化代码,充分的降低了代码的耦合度,降低维护成本,思路更清晰。
viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
viewModel 绝对不能包含视图 view,不然就跟 view 产生了耦合,不方便复用和测试。
viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa)。
单人语音页面为例,通过下面的图解可以看出:
通过MVVM的架构模式,将所有的数据处理、业务逻辑处理等都抽离到了VM(ViewModel)里面,解放了Controller,降低了耦合性,利于重用和测试。
通过与RAC的结合使用,viewModel创建并发送信号,Controller订阅信号avatarImageSignal、currentRoutingChangeSignal、callQualitySignal、longTimeNoAnswerSignal等,而让界面实时进行更新,控制器单纯的进行UI操作,同时避免了通知代理等大量的使用,更易于维护,并且将点击事件通过RAC的方式,让所有代码一起处理,增加了可阅读性。
RAC/KVO forwardInvication: 方法互相覆盖问题
RAC循环调用问题/RAC内存泄漏问题
如下是几种简单的创建信号时引起的循环引用情况:
{
// case 1 -- 这种情况较容易发现
self.someSignal = [RACSignal createSignal:^RACDisposable *(id subscriber) {
NSLog(@"self is %@", self);
[subscriber sendNext:self.someOtherProp];
[subscriber sendCompleted];
return nil;
}];
// case 2 -- 不太容易发现
self.someSignal = [RACSignal return:self];
// case 3 -- 不太容易发现
self.someSignal = [[RACSignal return:@1] map:^id(id value) {
return [NSString stringWithFormat:@"%@%@", self, value];
}];
}
P.S: 我似乎忘记了,循环引用并不只是发生在异步block中。
但如下这种情况没有循环引用:
- (RACSignal *)makeNewSignal
{
return [RACSignal createSignal:^RACDisposable *(id subscriber) {
[subscriber sendNext:self.someProp];
[subscriber sendNext:self.someOtherProp];
[subscriber sendCompleted];
return nil;
}];
}
引用情况如下:
这段代码存在风险,如果这个信号被某个全局变量持有了,`self`就永远不会被释放,这不属于循环引用,但是也是内存泄漏。内存泄漏不仅仅是循环引用,所有应该释放却没被释放的情况都算。
如下情况引发了循环引用:
- (void)retainCycleWhenSubscribe
{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[[button rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
self.someProperty = 42;
}];
self.button = button;
}