记一次iOS重构之路
新入职了,前一个月陆陆续续把之前一个App重构了一下下,目前重构了一半,基本架构算是弄完了,先总结下,后面接着完善。
分以下说明下:1: 为什么要重构
2:重构前的准备工作
3:重构之路
1:为什么要重构
1.1 代码层面
首先看到我司这个新的App之前写的代码,是用 MVC
模式写的,但是发现一个类里面尤其是viewcontroller
写了几百行代码,有的都是一千多行,我熟悉起来感觉好累,其次模块架构分的还是不够清晰,有些业务逻辑放到了其他模块里面,没有单独拆分出来。最后感觉里面冗余了很多无用的代码。当然了我的吐槽并不是说我自己多牛,其实我也只是个菜鸟,只是希望重构一下方便后续维护。
1.2 个人原因
因为我之前都是做SDK开发,很久没有单独开发App了,并且之前的公司代码全部都是模块化管理,我们现在这个完整的App却是耦合性很高,加之我反复熟悉了我司之前的这个App业务逻辑,前段时间也比较空闲,于是就决定重构一下。
2.重构前的准备工作
根据我之前的经验,我希望这个App模块化,耦合性尽量低一些。
2.1 设计模式的转变
我准备从之前的单纯
mvc
设计模式转变成MVVM With ReactiveCocoa设计模式
,
MVVM的使用我参考了以下文章,供大家参考:
iOS 关于MVC和MVVM设计模式的那些事
对于结合使用ReactiveCocoa
,我觉得可以更加方便view
层和ViewModel
层之间的交互,并且ReactiveCocoa
为事件提供了很多处理方法,而且利用RAC处理事件很方便,可以把要处理的事情,和监听的事情的代码放在一起,这样非常方便我们管理,就不需要跳到对应的方法里。非常符合我们开发中高聚合,低耦合
的思想。对于它的使用我参考了一下文章:
最快让你上手ReactiveCocoa之基础篇
ReactiveCocoa 中 RACSignal 是如何发送信号的
2.2 组件化改造
组件化我直接使用Casa的 iOS应用架构谈 组件化方案,简单来说基于casa的 CTMediator
部分目录截图
组件架构内部调用部分
通过Target+Action以及组件+类别的调用方式组成一个中介者模式。
原因很简单我之前的公司就是使用这一套架构,我觉得还不错,我这边就接着借鉴使用了。
其实组件化有很多方案,强烈推荐可以参考这篇文章
3.重构之路
3.1项目结构整理
首先大概看下这个App结构
效果图从这个效果图上面我先总结了重构该项目大概所需要的组件
3.2 MVVM+RAC项目重构开始
-
之前项目中全部是本地文件放到具体项目中,没有使用
cooapods
,我们代码目前还是托管到svn
上。我想进行模块化,每个不同功能的组件都能进行cocoapods
管理,考虑到代码私有化,我暂时没有将要重构的代码托管到第三方的git
仓库上,先将代码放到本地指定文件下,每个模块也加入podspec
文件,通过引用本地路径,同样也能实现cocoaPods
管理,每个模块文件都可以pod
下载管理。
使用本地cocapods
管理之后,podfie
文件里面管理大概就是类似下面这个模块了:
这样重构的项目模块依赖只需要管理对应的podfile
文件就好了。 -
由于资讯列表页面就是请求数据和解析相关数据,无需其他业务逻辑,首先我重构该模块。
资讯
就以这个模块为例说明下:
-
使用MVVM我们离不开
View
,ViewModel
,Controller
,
这里先统一定义下baseView
,baseViewModel
,baseController
,
首先建立BaseViewmodel
,遵循BaseViewModelProtocol
协议。
@protocol PLBaseViewModelProtocol <NSObject>
@optional
@property (nonatomic, readonly, copy) NSDictionary *params;
@property (nonatomic, readonly, copy) NSString *title;
//error接受者
@property (nonatomic, readonly, strong) RACSubject *errors;
- (instancetype)initWithParams:(NSDictionary *)params;
- (void)initialize;
@end
所有的viewmodel
都要继承它BaseViewmodel
。
BaseViewmodel
里面结构如下:
通过
- (instancetype)initWithParams:(NSDictionary *)params
方法传递进来一些基本参数提供给Viewmodel
使用,在initialize
方法里面子类里面放些需要初始化的操作。建立
PLBaseView
,遵循PLBaseViewProtocol
,所有直接继承UIView
类型的view都要继承它,其他view比如tableViewcell
遵循PLBaseViewProtocol
。
@protocol PLBaseViewProtocol <NSObject>
@optional
@property (nonatomic, strong, readonly) PLBaseViewModel *viewModel;
- (instancetype)initWithViewModel:(PLBaseViewModel *)viewModel;
- (void)renderViews;
- (void)bindViewModel:(id)viewModel;
- (void)bindViewModel;
@end
@implementation PLBaseView
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
PLBaseView *view = [super allocWithZone:zone];
@weakify(view)
[[view rac_signalForSelector:@selector(initWithViewModel:)] subscribeNext:^(RACTuple * _Nullable x) {
@strongify(view)
[view renderViews];
[view bindViewModel];
}];
return view;
}
- (instancetype)initWithViewModel:(PLBaseViewModel *)viewModel {
if (self = [super init]) {
_viewModel = viewModel;
}
return self;
}
//配置子视图的操作放在这个地方
- (void)renderViews {
}
//与viewModel的具体交互操作
- (void)bindViewModel {
}
- (void)bindViewModel:(id)viewModel {
_viewModel = viewModel;
}
建立baseControllerView
,其实这个类里面类容和baseView
里面类似,绑定viewModel
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
PLModelViewController *viewController = [super allocWithZone: zone];
@weakify(viewController)
[[viewController rac_signalForSelector:@selector(viewDidLoad)] subscribeNext:^(RACTuple * _Nullable x) {
@strongify(viewController)
[viewController bindViewModel];
}];
return viewController;
}
- (instancetype)initWithViewModel:(PLBaseViewModel *)viewModel {
if (self = [super init]) {
self.viewModel = viewModel;
}
return self;
}
//通过RAC进行title绑定操作
- (void)bindViewModel {
RAC(self,title) = RACObserve(self, viewModel.title);
//订阅信号
[self.viewModel.errors subscribeNext:^(NSError *error) {
NSLog(@"viewModel 错误信息------%@",error);
}];
}
还有在通过继承baseViewController
和baseView
分别针对tabelView做了进一步处理,新建类PLBaseTableViewController
,PLBaseTableViewModel
,这里代码代码就不在详细赘述。
PLBaseTableViewModel
主要做了如下额外处理:
PLBaseTableViewController
主要做了如下处理,主要添加了tabelView
,集成了MJRefresh
下拉刷新和上拉加载功能.PLBaseTableViewController.h
- 建立
UITableViewDataSource
的代理类TabelViewArrayDataSource
,
将UITableViewDataSource
相关代理方法分离出去
TabelViewArrayDataSource
这样的好处就是避免使用tabelveVIew的时候还导入UITableViewDataSource
,降低了代码的耦合性。
上面主要的基类创建完成,就可以开始使用MVVM+RAC正式开始构建业务相关页面了。
- 针对
资讯
新闻列表页面创建FitfunInfoListViewModel
,继承于PLBaseTableViewModel
,所有的网络交换和数据获取都是放在viewModel
中的。这里简单说下:
在.h
文件
@class FitfunBannerModel;
@interface FitfunInfoListViewModel : PLBaseTableViewModel
//滚动视图数据源
@property (nonatomic, readonly, strong) NSArray <FitfunBannerModel *> *banners;
//请求banner数据命令
@property (nonatomic, readonly, strong) RACCommand *requestBannerDataCommand;
@end
主要就是通过RAC信号源
来进行数据交换和传递
在.m
文件里面主要用到了RAC的RACCommand
,
RACCommand
:RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程。具体使用不熟悉的可以参考上面【设计模式的转变】我分享RAC学习的链接。
使用场景:监听按钮点击,网络请求
网络请求方面我还进行了解耦。
使用的是casa的那一套iOS应用架构谈 网络层设计方案,简单来说是使用CTNetworking,网络层上部分使用离散型设计,下部分使用集约型设计,使用delegate来做数据对接,仅在必要时采用Notification来做跨层访问,设计合理的继承机制,让派生出来的APIManager受到限制。这个具体有兴趣了解的话·请看上面共享的链接。
在FitfunInfoListViewModel.m
文件里面我具体的体现是遵循CTAPIManagerParamSource,CTAPIManagerCallBackDelegate
协议,然后再对应协议里面传递需要的参数和相应对应的请求结果。进行网络请求就需要我们继承CTAPIBaseManager
,里面设置一些网络请求相关操作。看下里面我写的大致代码:
#pragma mark - CTAPIManagerParamSource
//这里放额外拼接的参数
- (NSDictionary *)paramsForApi:(CTAPIBaseManager *)manager {
NSMutableDictionary *dic = [[NSMutableDictionary alloc]init];
if (manager == self.bannerImageAPIManager) {
dic[@"id"] = (self.params[@"bannerID"]?:@"");
}
return dic;
}
#pragma mark -CTAPIManagerCallBackDelegate
//这里网络请求成功的相关操作
- (void)managerCallAPIDidSuccess:(CTAPIBaseManager *)manager {
}
//网络请求失败相关操作
- (void)managerCallAPIDidFailed:(CTAPIBaseManager *)manager{
}
#pragma mark - getter&&setter
//继承于CTAPIBaseManager,这里配置网络请求地址和请求类型
- (FitfunAPIBaseManager *)topicInfoAPIManager {
if (!_topicInfoAPIManager) {
_topicInfoAPIManager = [[FitfunAPIBaseManager alloc] initWithMethodName:front_content_list reuquest:CTAPIManagerRequestTypePost];
_topicInfoAPIManager.paramSource = self;
_topicInfoAPIManager.delegate = self;
}
return _topicInfoAPIManager;
}
//reformer遵循`CTAPIManagerDataReformer`,用来统一解析网络请求成功数据
- (FitfunAPIBaseDataReformer *)reformer {
if (!_reformer) {
_reformer = [[FitfunAPIBaseDataReformer alloc]init];
}
return _reformer;
}
资讯的新闻列表的ViewModel
写完了,就可以新建ViewFitfunInfoListTableViewCell
,遵循PLBaseViewProtocol
协议,
view里面主要就是进行视图搭建和解析ViewModel相关操作,这里直接跳过。
然后新建新闻列表的FitfunInfoViewController
继承于PLBaseTableViewController
,通过FitfunInfoListViewModel
进行数据关联,RAC进行数据传递,最后我们的新闻列表Controller里面结构和代码就会特别清爽。
我只需要在绑定的ViewModel
对应的数据监听方法中操作就ok了。
大致重构项目使用
MVVM+RAC
的简单说明就是这样了,重新调整了下结构,使业务处理逻辑更加清晰,代码结构慢慢趋于完善。
为了更直观看出代码的变化我们可以对比下新闻列表页构造前后代码的变化和调整: 之前新闻列表构造
初步改造之后
3.3 AppDelegate解耦
初步改造之后,然后看了下之前项目里面AppDelegate各种注册和事件处理,Appdelegate.m
代码冗余度太高了。于是考虑到如何将Appdelegate解耦下。综合之前工作经验和网上别人说的,我总结了目前有大致如下几种解耦方式:
-
第一个当然使我们最熟悉的使用AppDelegate 分类 (Category),
创建 AppDelegate 分类无疑是低投入高产出的最好解决方案了 -
FRDModuleManager,FRDModuleManager 是豆瓣开源的轻量级模块管理工具,其内部数组持有注册的模块的引用,通过依次调用数组中的每个模块的特定方法来达到解耦的目的.
-
JSDecoupledAppDelegate,通过转发 AppDelegate 的各个方法来实现 AppDelegate 的解耦的:
-
JLRoutes
MGJRouter
灵活的 iOS URL Router,由于这个我没有用过,不是很熟悉,感兴趣可自寻了解下
这里还增加两个我之前公司用到的另外两个解耦方法
-
Aspects,一个轻量级的面向切面编程的库。它能允许你在每一个类和每一个实例中存在的方法里面加入任何代码,通过Runtime消息转发实现Hook,我们可以使用它在各个类里面拦截
AppDelegate
的各个方法。这个代码完全无侵入,之前公司一直在用,使用过程中就是偶尔会发生莫名的拦截错误。还不错,感兴趣的可参考大神的iOS 如何实现Aspect Oriented Programming -
BeeHive,BeeHive是阿里用于iOS的App模块化编程的框架实现方案,吸收了Spring框架Service的理念来实现模块间的API耦合。我之前公司用于游戏代理层方面的解耦,使用起来非常不错的一个框架。如果对这个框架感兴趣,可以参考大神关于这个框架源码解析BeeHive —— 一个优雅但还在完善中的解耦框架
我这个项目中考虑了一下,我重构的项目目前不是很大,在Appdelegate
对应方法里面注册的类相对有限,我想在各个类里面对应响应UIApplicationDelegate
协议方法,为了简单化一些暂时参考了FRDModuleManager,在这个基础上简单改造了成了PLModuleManager
,使用时需要在Appdelegate.m
使用PLModuleManager
注册相关方法,在PLModuleManager
初始化时候持有需要注册类
,而注册过的类
遵循UIApplicationDelegate
和UNUserNotificationCenterDelegate
协议,实现需要的协议方法即可。
后续代码调整和总结:
初步重构项目的架构就是那样了,后面还需要做的就是
- 把原来项目中到处随意使用的通知
NSNotificationCenter
全部替换掉,用block
和delegate
等形式。乱用通知会导致代码不可控,管理性极差。 - 代码格式统一规范,代码风格要按照标准书写,自己现在重构的项目偷懒写的一些不合理的地方也得纠正过来。还有尽量使用代码构建视图,代替之前项目中大量使用
xib
,xib
的过多使用会导致代码不好维护,尤其后面多人维护的时候。 - 把iOS环信通信这块代码重新整合一下,还有对现在对
MVVM+RAC
的使用不够熟练,这一块有时间重新优化改造下。 - 其他的暂时没有想到的等以后想起来或发现纠正。
疏漏和不合理之处如果各位哥哥姐姐们看到了,请不吝赐教,我只是按照自己的思路简单初步总结了下后续重构完毕这部分文章重新再整理下.