开发和维护西交Link iOS版至今也有一年多了,随着项目的越来越大,项目变得逐渐混乱,加之当初年幼无知,虽然懂得把网络部分单拎了出来,但是网络部分的架构简直目不忍视。ViewController中的逻辑也变得越来越复杂,想想自己还有不到一年的时间就要毕业了,毕业之后西交Link的维护也必然会移交给他人,为了对以后维护的人负责,我觉得将西交Link进行一次完整的重构。打算把这次整个的重构经历记录下来,以加深自己的理解。
其实我很少会写关于架构设计方面的博客,毕竟觉得自己还比较年轻,所以如有什么不正确的地方欢迎指正。

RAC+MVVM概论

RAC与MVVM想必都不用介绍。越来越多的项目采用了MVVM,引入RAC也是水到渠成,这个说法可能有些问题,应该是RAC的出现,让MVVM应用于iOS项目实至名归。当然,不是说MVC不好,对于小项目快速开发而言,MVC远胜MVVM。MVVM也不是万能的,它有它自己的缺点。关于MVC和MVVM的纷争推荐看巧大的被误解的 MVC 和被神化的 MVVM
对于iOS而言,名字叫MVVM,但其实更贴切的说法应该是MVCVM,所以从这个名字就可以知道,其实对于一个正常基础的MVC架构的iOS应用,调整至MVVM其实还是很方便的,相比于本身架构比较混乱的Android,呵呵呵呵(最近西交Link Android组那边也在重构,天呐,感觉Android重构就等于重写)。iOS这边相对比较规范,所以大家入门的时候关于架构方面,不至于走太大弯路。
很多人都说MVVM和RAC是天生一对,用MVVM的时候自然引入RAC,网上多数相关博客都是只讲了RAC在MVVM中的用法,搞得学习了半天也不知道RAC到底起什么作用。

RAC的主要作用

  1. 作用于View和ViewModel之间
    RAC的主要切入点就是View和ViewModel之间,当ViewModel中的数据因用户操作、网络等原因发生了变化时,View能够马上动态的更新数据,这就是所谓的数据绑定。具体到iOS工程而已,就是绑定数据用在View和ViewController。

  2. 对网络操作的封装,即RACCommand
    在引入RAC的项目中,经常把网络请求封装成Command,以供View层通过ViewModel触发网络访问,当然,这也就自然引入了对Command的相关信号(Singal)的订阅(subscribe)。RACCommand的逻辑与RACSingal不太相同,刚入门的时候这里很容易踩坑。

  3. 作用于Model与ViewModel之间
    有时候我们也会在Model与ViewModel进行数据绑定,以当相对独立运作的Model层(譬如有些逻辑是后台Model自更新的)发生改变时,及时的通知ViewModel,然后再通过小结1中的View与ViewModel的绑定来刷新UI。以前我们往往通过Notification的形式来实现Model更新时,通知Controller来更新UI,在这里,RAC给出了统一的数据更新方式。

概括成一句话就是

RAC提供了统一的数据流动的方式,其扮演传递数据变化信号的角色。

MVVM:

  1. Model
    这里主要是一些原始的数据模型以及长时间保留在内存中的单例(譬如User类),部分网络请求是从这里发出的,原因是有些Model的逻辑是在后台自刷新的而与View无关。

  2. View && ViewController
    MVVM仅仅是弱化了Controller的作用,但也是有Controller的,我们知道View和ViewModel互相不知道彼此的变化,所以Contoller的主要工作就是将View和ViewModel进行绑定,当然也负责一部分的UI逻辑处理。

  3. ViewModel
    ViewModel接过了Contoller中关于数据处理的部分以及网络发起的操作,ViewModel又接过了Model中数据处理的部分,让Model变得更瘦。一句话,ViewModel是专门负责数据处理的事情。

网络层架构设计

在写整个实践架构之前,我想先写关于网络层的设计,网络层对外的API很大程度的影响了其他层的架构设计,而网络层又相对独立,自身的设计与其他层关系很弱。所以先介绍网络层,这样也方便之后的介绍。
拜读CASA大大的iOS应用架构谈系列的文章,研读了很多遍,收获很多。这次网络部分的设计很大程度上参考了CASA大大的RTNetworking,关于RTNetworking的设计理念,我这里不做很多介绍,以下关于网络层的设计均是基于RTNetworking的,所以建议大家先阅读iOS应用架构谈 网络层设计方案。不过RTNetworking设计就为了公司那种多人合作的组件化的大项目,所以像几个人维护的小团队用RTNetworking感觉有点杀鸡牛刀,所以我自己不自量力地在上面做了很大部分精简,使其更加适合小团队项目的使用,如果有什么不当的地方,欢迎指出。由于之前的工程有很多Model,所以为了兼容,没有采用CASA大大的去model化的方案(虽然我主观上很赞同去model化的设计,但是毕竟有历史原因),在APIManager里加了- (id)fetchDataFromModel;来利用Mantle进行数据的转化。当然也去掉了一些本不该去的东西,比如Cache,由于暂时精力有限,所以暂时没有采用Cache,打算等整个项目重构完毕后会再将Cache补回来(此坑已填,缓存采用YYCache,逻辑与网络访问相似的Proxy的形式,方便替换第三方库)。还有一些别的小的改动比如将返回值不是正确时,用YLResponseError进行了封装而没有采用统一的ResponseModel,不过这些改动都无伤大雅,所以这里不多提。在请求参数提供方式上,也采用了DataSource的模式,要求发起者(通常是Controller)实现DataSource协议,提供参数。

@protocol YLAPIManagerDataSource <NSObject>
@required

- (NSDictionary *)paramsForAPI:(YLBaseAPIManager *)manager;
@end

关于YLPageAPIManager

下拉刷新、上拉加载更多,这种按页加载的逻辑实在是太常见了,所以我单独封装了另外一个基类YLPageAPIManager,所有类似逻辑的APIManager直接继承这个类即可。这个类对外提供的方法很少,只有以下三个:

- (void)reset;
- (void)resetToPage:(NSInteger)page;
- (NSInteger)loadNextPage;

一个是加载下一页,两个是重置。对于业务层来说,这三个方法已经足够用了,业务层不需要自己管理页码,需要加载的时候直接loadNextPage即可。当然,为了防止业务层需要展示某些信息,一些信息也是对额提供查询的,不过是readOnly

@property (nonatomic, assign, readonly) NSInteger pageSize;
@property (nonatomic, assign, readonly) NSInteger currentPage;
@property (nonatomic, assign, readonly) BOOL hasNextPage;

参数的提供和BaseAPIManager一样,让Contolller实现YLAPIManagerDataSource协议即可。
由于pageSize通常不会在后续加载的过程中变化,所以这个参数我单独在init方法里提供,这样可以使得paramsForAPI:更加简洁一些。

- (instancetype)initWithPageSize:(NSInteger)pageSize;

此外,我还将BaseAPIManager的loadData重写,防止外部调用这个方法,而产生意外情况

- (NSInteger)loadData {
    @throw [NSException exceptionWithName:[NSString stringWithFormat:@"%@ load error",[self class]]
                                   reason:@"Don't call this Method. Call loadNextPage instead."
                                 userInfo:nil];
}

此处,与CASA大大的设计不大相同,CASA大大在keynote上介绍翻页的时候,对外接口是第一次必须调用loadData,之后调用loadNextPage。我这里将第一次调用的逻辑也放到了loadNextPage里,并且禁用了loadData,统一用法,避免误用。
在计算是否还剩数据时的实现也略有不同,如下

- (BOOL)beforePerformSuccessWithResponseModel:(YLResponseModel *)responseModel {
    self.currentPage += 1;
    if (self.child.currentPageSize != kPageSizeNotFound
        && self.child.currentPageSize < self.pageSize) {
        self.hasNextPage = NO;
    }
    return [super beforePerformSuccessWithResponseModel:responseModel];
}

- (BOOL)beforePerformFailWithResponseModel:(YLResponseError *)error {
    if (self.currentPage > 0) {
        self.currentPage --;
    }
    return [super beforePerformFailWithResponseModel:error];
}

这里判定是否还有剩余数据是采用对于当前获取到的页大小与预设页大小。如果当前页大小<预设页大小时,则证明已经没有数据了。这样做的好处是避免了一部分API没有提供总数据大小的问题,统一了判定逻辑。这样做也有坏处,就是如果预设页能够正好被总数整除的时候,需要多一次请求才能够知道没有数据了,但是我觉着这一次请求相比起统一逻辑带来的好处并不算上什么大问题,况且这种情况也很少会发生。
以上是相当于对RTNetworking的基础扩充,那么接下来我们就让整个网络模块支持RAC。

网络层的RAC优化

大多数博客的做法,是在ViewModel里实现一个Command,然后在这个Command发起网络请求。RTNetworking设计有一个精妙之处是其将请求参数以DataSource的形式完全扔了出来,谁要数据,谁提需求,那么发起网络调用的时候,就完全不用考虑参数的问题了。这样以来,就意味着所有的网络请求的Command是一样的!可以完全抽离出来!激动么?
先看一下关于BaseAPIManager的扩展

//  YLNetworking+ReactiveExtension.h
@interface YLBaseAPIManager (ReactiveExtension)
@property (nonatomic, strong, readonly) RACCommand *requestCommand;
@property (nonatomic, strong, readonly) RACCommand *cancelCommand;
@property (nonatomic, strong, readonly) RACSignal *requestErrorSignal; //已为主线程
@property (nonatomic, strong, readonly) RACSignal *executionSignal;

- (RACSignal *)requestSignal;
@end
//  YLNetworking+ReactiveExtension.m

//Private Category
@interface YLBaseAPIManager (_ReactiveExtension)
@property (nonatomic, assign) NSInteger requestId;
@end
@implementation YLBaseAPIManager (_ReactiveExtension)

- (void)setRequestId:(NSInteger)requestId {
    objc_setAssociatedObject(self, @selector(requestId), @(requestId), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSInteger)requestId {
    return [objc_getAssociatedObject(self, @selector(requestId)) integerValue];
}
@end

// Public Category
@implementation YLBaseAPIManager (ReactiveExtension)

- (RACSignal *)requestSignal {
    @weakify(self);
    RACSignal *requestSignal =
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        @strongify(self);
        RACSignal *successSignal = [self rac_signalForSelector:@selector(afterPerformSuccessWithResponseModel:)];
        [[successSignal map:^id(RACTuple *tuple) {
            return tuple.first;
        }] subscribeNext:^(id x) {
            [subscriber sendNext:x];
            [subscriber sendCompleted];
        }];

        RACSignal *failSignal = [self rac_signalForSelector:@selector(afterPerformFailWithResponseModel:)];
        [[failSignal map:^id(RACTuple *tuple) {
            return tuple.first;
        }] subscribeNext:^(id x) {
            [subscriber sendError:x];
        }];
        return nil;
    }] replayLazily] takeUntil:self.rac_willDeallocSignal];
    return requestSignal;
}

- (RACCommand *)requestCommand {
    RACCommand *requestCommand = objc_getAssociatedObject(self, @selector(requestCommand));
    if (requestCommand == nil) {
        @weakify(self);
        requestCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            self.requestId = [self loadData];
            return [self.requestSignal takeUntil:self.cancelCommand.executionSignals];
        }];
        objc_setAssociatedObject(self, @selector(requestCommand), requestCommand, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return requestCommand;
}

- (RACCommand *)cancelCommand {
    RACCommand *cancelCommand = objc_getAssociatedObject(self, @selector(cancelCommand));
    if (cancelCommand == nil) {
        @weakify(self);
        cancelCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            [self cancelRequestWithRequestId:self.requestId];
             NSLog(@"cancelCommand 取消请求:%lu",self.requestId);
            return [RACSignal empty];
        }];
        objc_setAssociatedObject(self, @selector(cancelCommand), cancelCommand, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return cancelCommand;
}

- (RACSignal *)requestErrorSignal {
    return [self.requestCommand.errors subscribeOn:[RACScheduler mainThreadScheduler]];
}

- (RACSignal *)executionSignal {
    return [self.requestCommand.executionSignals switchToLatest];
}

@end

首先,为了不影响原有网络层的设计,以及随时能够将RAC从网络层剔除出来以适应没有采用RAC的工程,所以关于RAC的封装,我采用了Category的形式。为了保证RAC部分的相对独立,所以requestSignal就没有采用AFNetworking-Racextensions那样对网络进行封装,而是采用了一种监控的方案。检测afterPerformSuccessWithResponseModel以及afterPerformFailWithResponseModel方法的调用来判定网络请求成功或失败。

还有一点关于requestSignal需要注意的是,有时候为了继续接收之后的网络请求,有些人会在访问成功的时候不进行sendCompleted,给出的理由是这样的,每次completed之后,所有的订阅者均失效了,Command也不起作用了,所以不进行sendCompleted,但是这样就有一个会出bug的场景,就是当第一次网络访问时,无网络,导致网络请求超时,而第二次网络访问的时候,用户开启了网络,网络是好的,这样第二次访问也不会起作用,之后的所有网络访问也不会起作用,只能退出这个页面,重新进入,重新加载来了新的APIManager,重新初始化了Command才能够正常访问。原因是Signal在第一次访问的时候被sendError了,这也会导致所有的订阅者失效,Command不起作用,所以之后无论网络状况如何,都不能访问成功。趟这个坑可是花费了我不少时间,在这里注意一定一定要sendCompleted,在每次Command调起的时候新建一个Signal就是了,这也就是为什么我没有将Signal保留下来,因为每次completed之后,所有的订阅者均失效了,Command也不起作用了。所以Command每次execute的时候,新建一个Signal,来保证使用的Signal一直是有效的。

另外,由于RTNetworking本身是没有限制一个APIManager可以同时发起多次请求,所以在BaseAPIManager里没有记录requestId,想要取消网络请求,则必须由业务层记录这个requestId。但是在RACCommand中allowsConcurrentExecution默认为NO,所以每次仅会有一个请求,故在可以在此记录该请求的requestId以方便取消,这样就不需要业务层再去记录requestId了。这也就是我添加了一个Private Category的原因。

这里也对Command的errorsexecutionSignals进行了简单的封装。executionSignals其实是一个signals的signal,我们可以用switchToLatest来切换signal of signals到最后一个。

BaseAPIManager搞定了,那么也让咱的PageAPIManger也支持一下RAC吧。

//  YLNetworking+ReactiveExtension.h
@interface YLPageAPIManager (ReactiveExtension)
@property (nonatomic, strong, readonly) RACCommand *refreshCommand;
@property (nonatomic, strong, readonly) RACCommand *requestNextPageCommand;
@end
//  YLNetworking+ReactiveExtension.m
@implementation YLPageAPIManager (ReactiveExtension)

- (RACCommand *)requestCommand {
    @throw [NSException exceptionWithName:[NSString stringWithFormat:@"%@ requestCommand error",[self class]]
                                   reason:@"Don't call this Method. Call  refreshCommand or requestNextPageCommand instead."
                                 userInfo:nil];
}

- (RACCommand *)refreshCommand {
    RACCommand *refreshCommand = objc_getAssociatedObject(self, @selector(refreshCommand));
    if (refreshCommand == nil) {
        @weakify(self);
        refreshCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            [self reset];
            NSInteger requestId = [self loadNextPage];
            if (requestId != kPageIsLoading) {
                self.requestId = requestId;
            }
            NSLog(@"[requestNextPageCommand] 发出请求:%lu",requestId);
            return self.requestSignal;
        }];
        objc_setAssociatedObject(self, @selector(refreshCommand), refreshCommand, OBJC_ASSOCIATION_RETAIN_NONATOMIC);        
    }
    return refreshCommand;
}


- (RACCommand *)requestNextPageCommand {
    RACCommand *requestNextPageCommand = objc_getAssociatedObject(self, @selector(requestNextPageCommand));
    if (requestNextPageCommand == nil) {
        @weakify(self);
        requestNextPageCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
            @strongify(self);
            NSInteger requestId = [self loadNextPage];
            if (requestId != kPageIsLoading) {
                 self.requestId = requestId;
            }
            NSLog(@"[requestNextPageCommand] 发出请求:%lu",requestId);
            return self.requestSignal;
        }];
        objc_setAssociatedObject(self, @selector(requestNextPageCommand), requestNextPageCommand, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return requestNextPageCommand;
}

- (RACSignal *)requestErrorSignal {
    return [[RACSignal merge:@[self.refreshCommand.errors, self.requestNextPageCommand.errors]]
                 subscribeOn:[RACScheduler mainThreadScheduler]];
}

- (RACSignal *)executionSignal {
    RACSignal *refreshExecutionSignal =
    [[self.refreshCommand.executionSignals switchToLatest] map:^id(id value) {
        return @(YES);
    }];
    RACSignal *requestNextPageExecutionSignal =
    [[self.requestNextPageCommand.executionSignals switchToLatest] map:^id(id value) {
        return @(NO);
    }];
    return [RACSignal merge:@[refreshExecutionSignal, requestNextPageExecutionSignal]];
}
@end

与PageAPIManger类似,这里重写的requestCommand,禁用了这个方法,防止误用。

由于翻页通常是下拉刷新和上拉加载更多,所以这里封装了refreshCommandrequestNextPageCommand两个Command。

由于不管是下拉刷新或者上拉更多,网络错误的处理逻辑是一致的,所以重写了requestErrorSignal,在此合并了refreshCommandrequestNextPageCommanderrors。下拉刷新和上拉加载更多得到数据后唯一不同的处理逻辑就是下拉刷新需要将viewModel中的数据重置而不是在原有的基础上累加。所以这里也重写了executionSignal,合并了他们的executionSignals,然后将Singal映射变为BOOL类型的,这样在订阅的时候,只需要判断传进来的是是真是假就知道是否需要重置数据了。

到此,我们让RTNetworking完美的支持了RAC,我们把网络请求的Command统一封装在了Network层,这样ViewModel就不需要每次都实现相同的代码了。但是还有一些不完美的地方,就是由于ViewModel访问网络的动作也是由Controller触发的,但是很明显,我们并不希望把APIManager公开出去,这就意味着我们需要在ViewModel针对每个Command封装一层然后提供给Controller,来限制Controller的动作。
这里有个更好的实现方案,那就是Protocol。我们可以让ViewModel实现一个方法,以id<Protocol>的形式将APIManager提供出去,这样既限制了Controller对APIManager的动作,又不需要对每个Command进行封装。
先看具体定义吧。

//  YLNetworking+ReactiveExtension.h
@protocol YLNetworkingRACOperationProtocol<NSObject>
- (RACCommand *)requestCommand;
- (RACCommand *)cancelCommand;
- (RACSignal *)requestErrorSignal;
- (RACSignal *)executionSignal;
@end

@protocol YLNetworkingListRACOperationProtocol<YLNetworkingRACOperationProtocol>
- (RACCommand *)refreshCommand;
- (RACCommand *)requestNextPageCommand;
@end

@protocol YLNetworkingRACProtocol <NSObject>
@optional
- (id<YLNetworkingRACOperationProtocol>)networkingRAC;
// 定义枚举在这允许获取多个APIManager的RAC
- (NSArray<id<YLNetworkingRACOperationProtocol>>*)networkingRACs;
@end

@interface YLBaseAPIManager (ReactiveExtension)<YLNetworkingRACOperationProtocol>
// ...
@end

@interface YLPageAPIManager (ReactiveExtension)<YLNetworkingListRACOperationProtocol>
// ...
@end

这里我将ReactiveExtension的方法抽象出协议,让APIManager实现了对应的协议。这样以来,我们就可以进行id形式转换了。然后我们再定义YLNetworkingRACProtocol,这样ViewModel只需要实现这个协议即可。如果ViewModel有多个APIManager的话就实现- (NSArray<id<YLNetworkingRACOperationProtocol>>*)networkingRACs方法,先对外定义一个枚举,然后在Controller通过这个方法和枚举即可获取对应APIManager的Command了。

通过以上的转换,在Controller层调用网络就变成了以下的形式:

// 针对viewModel只有一个APIManager
[self.viewModel.networkingRAC.refreshCommand execute:nil];
// 针对viewModel有多个APIManager
[self.viewModel.networkingRACs[kNetworkingRACTypeUser].refreshCommand execute:nil];

这就是整个网络层使用RAC优化后的最终形式了,是不是有种大道至简的感觉呢!

完整的实现以及下文的Demo都在这里YLNetworking
写到这里发现篇幅已经很长了,所以打算把这个文章分为两部分,下一节我再写基于这样的网络架构去实践RAC+MVVM。

Preference

1.ReactiveCocoa2实战
2.被误解的 MVC 和被神化的 MVVM
3.iOS应用架构谈