记一次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 组件化改造

组件化我直接使用CasaiOS应用架构谈 组件化方案,简单来说基于casa的 CTMediator
组件架构内部调用部分
通过Target+Action以及组件+类别的调用方式组成一个中介者模式

原因很简单我之前的公司就是使用这一套架构,我觉得还不错,我这边就接着借鉴使用了。
其实组件化有很多方案,强烈推荐可以参考这篇文章

部分目录截图


3.重构之路
3.1项目结构整理

首先大概看下这个App结构

效果图

从这个效果图上面我先总结了重构该项目大概所需要的组件


3.2 MVVM+RAC项目重构开始
  • 之前项目中全部是本地文件放到具体项目中,没有使用cooapods,我们代码目前还是托管到svn上。我想进行模块化,每个不同功能的组件都能进行cocoapods管理,考虑到代码私有化,我暂时没有将要重构的代码托管到第三方的git仓库上,先将代码放到本地指定文件下,每个模块也加入podspec文件,通过引用本地路径,同样也能实现cocoaPods管理,每个模块文件都可以pod下载管理。
    使用本地cocapods管理之后,podfie文件里面管理大概就是类似下面这个模块了:


    这样重构的项目模块依赖只需要管理对应的podfile文件就好了。

  • 由于资讯列表页面就是请求数据和解析相关数据,无需其他业务逻辑,首先我重构该模块。


    资讯

就以这个模块为例说明下:

  • 使用MVVM我们离不开ViewViewModelController
    这里先统一定义下baseViewbaseViewModelbaseController

首先建立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里面结构如下:

19E6100E-64A2-432C-871A-AB7F8B0ED60D.png

通过- (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);
    }];
}

还有在通过继承baseViewControllerbaseView分别针对tabelView做了进一步处理,新建类PLBaseTableViewController,PLBaseTableViewModel,这里代码代码就不在详细赘述。
PLBaseTableViewModel主要做了如下额外处理:

PLBaseTableViewModel.h

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

E147CEE2-5AC9-40E8-ABA5-6976245D6A62.png

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了。

FitfunInfoViewController部分ViewModel处理代码

大致重构项目使用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初始化时候持有需要注册,而注册过的遵循UIApplicationDelegateUNUserNotificationCenterDelegate协议,实现需要的协议方法即可。


后续代码调整和总结:

初步重构项目的架构就是那样了,后面还需要做的就是

  • 把原来项目中到处随意使用的通知NSNotificationCenter全部替换掉,用blockdelegate等形式。乱用通知会导致代码不可控,管理性极差。
  • 代码格式统一规范,代码风格要按照标准书写,自己现在重构的项目偷懒写的一些不合理的地方也得纠正过来。还有尽量使用代码构建视图,代替之前项目中大量使用xib,xib的过多使用会导致代码不好维护,尤其后面多人维护的时候。
  • 把iOS环信通信这块代码重新整合一下,还有对现在对MVVM+RAC的使用不够熟练,这一块有时间重新优化改造下。
  • 其他的暂时没有想到的等以后想起来或发现纠正。

疏漏和不合理之处如果各位哥哥姐姐们看到了,请不吝赐教,我只是按照自己的思路简单初步总结了下后续重构完毕这部分文章重新再整理下.

如果有需要,可参考我根据上面思路初步写的Demo

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

推荐阅读更多精彩内容