回顾2017,整年对公司现有App进行了大大小小接近20版本的迭代,因为原有项目创建较早,代码质量上并不算高(早年的技术你懂得,那时候可能才有MVVM,那时候runtime还没有被广泛使用,runloop可能只是了解阶段),所以伴随着每一版本迭代都会做一部分代码优化、重构,而目的就是为了针对原有类似线团一样的业务进行解耦。
都做了那些工作呢?总结了一下基本就是以下几点,无非就是怎样解耦:
1、组件化
2、结合MVVM架构和数据驱动UI模式对原有MVC架构进行了兼容性优化
3、通过AOP技术对部分业务进行拆分解耦
4、优化事件传递方式
下面来详细说一说
一、组件化,组件化实现方案较多,网上也算是百家争鸣,而我们当时完全是从无到有,自己一步一步躺着坑总结了一套比较low的方案。
最开始有这个想法的时候,是因为产品需求放突然有了想在多个应用引入同一功能的想法,所以大家开始集思广益,开始了组件化之路。
之后我们基于git的子工程进行了组件化的开发,首先进行了公共业务的组件化抽离,将一些SDK(网络、第三方工具等)进行了归类并放入组件中,之后,对App内部业务进行梳理,将大的业务模块逐步拆分成了各个小的组件。直到现在,已经拆分出来的组件大概7个左右,基本上已经对模块上的解耦合做了最大努力,接下来部分没有优化的模块内部的繁重业务会继续去优化。而在今年,可能会考虑使用通过cocoaPods方式去管理组件。当然这是后话,总结今年的组件化实现,觉得有以下几点最重要:
(1)要先从公共基础部分开始抽离,这部分如果不能先行,会给之后的组件化增加很多不必要的工作,比如AF、SDWebImage,每一次都需要去在组件中重复导入,而且在SDK维护的时候需要多个组件以及主工程同时维护。
(2)公共基础组件建立后,梳理整体业务,寻找节点,分割出各个业务模块,然后以这些模块为组件进行下一步的优化重构,由大到小化整为零。
(3)善用节点制作组件与组件或组件与主工程的中间层,举个栗子:我们工程中有下单支付功能、有充值功能、有购买红包功能,总体来讲都属于支付,大致流程有3个步骤:
1)生成订单
2)利用订单信息拉起第三方支付
3)支付完毕后客户端后续操作
大体上是这样,只不过不同的功能在细节处理上又有些许差异。但有两个节点是不变的,首先都是创建订单(这是支付功能的发起),最后是支付完结后的本地App操作(这是支付功能的结尾)。
这样就好办了,我们就以这两个节点作为整个支付功能的入口和出口。
内部定制好各种支付方式的订单生成,以及统一的拉起第三方支付,之后支付完成后统一返回结果给外部。。。
内部实现有点复杂,我们只说中间层的设计。
中间层为一个若引用的单例(可以保证在没有对象持有的情况下自动销毁,避免内存浪费),整个单例只暴露一个带有block的方法,传入参数,之后一系列的订单生成、拉起支付都不需要外部知道,最后通过block返回支付的结果。业务方只需要告知支付组件详细的支付参数,然后静等支付结果就可以了。
组件化优点太多,大家尝试过就知道了,毕竟我们的还是比较简陋的,低调点,就不继续说了。
二、结合MVVM架构和数据驱动UI模式对原有MVC架构进行了兼容性优化
MVC(调侃一下Massive View Controller),经典的设计模式,之前看到至少500甚至有的多达几千行代码的ViewController时都傻眼了,这该如何接手代码?这么多业务咋熟悉啊?尤其是复杂列表代码实现中成堆的if-else。。。。。。初始时,是针对如何去if-else开始思考的,后来蔓延到拆分ViewController的繁重业务。
之前对数据驱动模式有过了解,脑中第一时间想到了这种方式,如果让model和view一一对应的话,再让model执行统一的一套方法创建对应的View和赋值View不就达到目的了么。但这时又有新的问题了,model本来只是负责数据处理保存的,如果加上这部分业务不就成了真正的胖model了么?是不是考虑换一套设计模式呢?于是开始了对MVVM的初步了解,当时也没有太多精力去仔细了解和实践,大致明白MVVM多了一个ViewModel这样的东西,针对数据做了一部分处理,分离了数据的处理业务。那好,我们也添加一个ViewModel层,主管特定View的创建以及数据的预处理业务(当时没有去详细了解MVVM,因为感觉MVVM其实本质上也还是MVC,其他的架构也都是其变种,而如何去使用还是要考虑到本地的业务,毕竟人与人是不同的,代码也不同,每个人都有自己的需求,所以在优化的时候并不一定要完全否定自己的东西,而是需要做一些能很好兼容以往代码的优化,尤其是架构的选择)。
说归说,利用伪代码大概缕清流程后撸起袖子就开干:
首先针对复杂列表罗列出都有哪几种类型的Cell,然后创建这些Cell,在创建对应的ViewModel,通过协议方法让viewModel创建指定的Cell,最后tableView的代理方法直接让viewModel走协议方法返回对应cell,OK,搞定,再也不会看到if-else,当然,这看起来有点草率,我举个栗子然后详细讲解一下:
场景:aVC中有tableView,展示了2中cell,分别是c1 和 c2两种cell,原有的cellForRowAtIndexPath协议中需要通过if-else判断来得知创建哪一种cell。现在对原有代码进行修改:
(1)c1、c2不变
(2)创建两个ViewModel,VM1、VM2。
(3)创建一套协议,CellProtocol,定义两个方法,creatCell方法、configCell方法,前者用来创建返回cell,后者赋值cell。
(4)修改VC中网络请求业务,在请求转换成Model后,利用Model生成不同的ViewModel,比如虽然每一条model都是属于一个类的,但某一参数决定了他是c1所需要的参数还是c2所需要的,就根据这些去创建VM1和VM2,之后把包含这些VM的数组给tableView当做数据源。
(5)修改VC中的cellForRowAtIndexPath内部的代码,利用协议的两个方法来进行创建和赋值:
id cell = [self.dataSource[indexPath.row] creatCell];//创建
[cell configCell:self.dataSource[indexPath.row]];//赋值cell
return cell;
三行代码搞定--!
细心地朋友会发现,其实我只是吧if-else放到了数据请求回来的地方,在哪里进行了if-else处理,没什么区别嘛。。。其实不是这样的,区别很大的,请求时你的菊花多转3-5圈用户是没什么感知的,但是如果你的菊花已经消失,但是列表渲染时或者说滚动过程中各种掉帧、闪烁、一动一卡,用户体验是很糟糕的,这种做法有点像微博之前针对cell高度缓存的做法,延长数据的解析时间换来交互的流畅度,在数据处理时就确定好每一个cell的高度,而不是在cell拿到数据后再去计算。
在这之后,对于ViewModel的使用越来越多,再也看不到if-else了,但如何解决VC的繁重问题呢,对于这一部分的思考是,在理想的MVC中C只是作为M与V的桥接对象,并对V的交互做出响应,桥接部分没什么可说的,工程中C历来都是将基本的Model传给View,view针对原始数据处理一次后再赋值,C中的业务目前还剩数据请求以及加工、View事件的处理,View的更新。按照理想中的C来看,其实没什么不一样,但现实是代码就摆在那里,多的数不清。于是对代码业务进行了拆分,数据的请求还是C去搞,也就是调用接口,但返回的数据交给ViewModel去进行处理并返回ViewModel实例给C,C持有ViewModel并可以通过修改ViewModel另View做出对应的UI更新,而ViewModel掌握着创建对应View的实现,View通过协议、通知、block等方式将交互事件传给C去统一处理,看一个简单结构:
ps:以后多多学习怎么写文章,实在是没搞过,各种手段都有点low,谅解谅解。。。。
基本上就是这样的一个结构,可能没描述清除,但是角色的配置大致就是这样,我的同事分的比较多,他还习惯利用一个单独的handler把交互事件抽离走,让VC基本成了承载View的空架子。
架构真不是很在行,我也就只是刚刚有个了解,算不算半只脚踏入都不知道,总之描述一下希望能给某些迷茫的人一些启发,个人觉得最总要的还是那句话,不一定要生搬硬套,还得看自己的业务最需要什么。
三、通过AOP技术对部分业务进行拆分解耦
项目中有各种第三方SDK的使用,友盟、个推、GrowingIO等等,都需要在Appdelegate的各个方法中去集成,久而久之造成AppDelegate中代码过于难看、复杂,且耦合度高,可移植性差。针对这一问题,我的处理方式是通过切片向AppDelegate中切入各个SDK的集成服务,关于AOP不做介绍了,大家可以百度一搜,有很多讲解的,这里简短说一下我的实现。
具体场景:个推的集成
1、利用runtime制作AppDelegate的切片(一个AppDelegate的category),利用runtime交换application: didFinishLaunchingWithOptions:等几个方法,新方法为GT_application: didFinishLaunchingWithOptions:
2、在新的GT_application: didFinishLaunchingWithOptions:方法中进行个推的集成,其他几个方法中实现接收推送后的处理,这里就不写了,只以这个方法说。
3、GT_application: didFinishLaunchingWithOptions:内执行过个推注册后执行代码,
[self GT_application: application didFinishLaunchingWithOptions:options];这行代码是能否形成切片的重点,类似子类重写父类方法后有返回父类实现去执行后续的代码。
这样一个面向于个推SDK的切片就OK了,以后如果有别的项目想要同样集成个推可以直接把这个切片搞过去,修改一下注册时候的各种Key、AppID就可以了,不需要粘贴复制或者重写一遍
实际业务中SDK的拆分只是切片的一部分,很多本地工具的切片化也处理了很多,另外还利用切片针对tableView的空数据占位显示做了处理:
app中很多的页面都包含tableView,但由于之前做的时候都单独对每一个tableView做的空数据、无网默认图显示处理,每一次都需要重写一遍,即使封装了空数据的占位UI还是觉得每一次判断数据有无、网络有无比较麻烦,所以就把这个需求提上了日程,想设计一个能够自动监测tableView当前数据源有无、并且根据网络环境来决定展示空数据图还是无网刷新图的一个工具。
关键点就在于如何实现自动监测这一功能,经过分析梳理发现,reloadData是一个统一的节点,想要刷新tableView必定要走这个方法,所以针对这个方法只做了一个切片,在新的N_reloadData方法中针对tableView当前的数据源dataSource内容进行了判断,又结合网络环境来决定空数据时展示内容,大致的代码分为以下2个部分:
1、空数据View(分为无网以及正常空数据)
2、tableView的切片,可配置空数据时展示内容,针对reloadData做了切片处理,提供空数据View的交互block
在实际使用过程中,只需要在创建tableView的时候配置好空数据的展示内容即可,其他不需要多做任何处理,如果需要处理空数据图的某些交互,只需要在block内实现即可。看一下简单的代码:
//配置代码
_tableView.emptyViewImage = [UIImage imageNamed:@"1.png"];
_tableView.emptyViewContent = @"暂无数据哦!";
_tableView.emptyViewDetaileContent = @"请稍后重试";
_tableView.emptyViewButtonTitle = @"点我刷新";
//交互代码
_tableView.emptyViewUserInteraction:^(){
//刷新页面
};
不贴详细代码了,详细代码太多,里面涉及到很多细节实现,有网络有针对tableView初始化立即执行以便reloadData的兼容处理。。。。授人以鱼不如授之以渔,思路最重要。
四、优化事件传递方式
产品业务越来越多,为了追求华丽各种复杂页面也层出不穷,代码封装越来越多,页面内的视图层级也越来越多,页面层级大于5级的太常见了:view上承载tableView,cell上承载某个view,view上有其他的view,其他的view上还有其他view。。。。。一层一层,每到这种时候就会发现,事件传递太过繁琐了,无论是协议、还是block都要经历多层的传递才能被VC接收到并处理,如果用通知的话 还好,不用考虑中间的传递,但是不敢频繁的使用通知啊,漫天的通知鬼知道什么时候给你来个超级无厘头BUG。那该如何避免复杂的层级传递和管理的难度呢?
响应链,没错就是通过响应链走,收到网友的启发,我为UIResponder制作了一个分类,利用响应链来进行事件的传递,事实证明这个点子溜了(我自己以为的),看一下简略代码吧:
1、建立UIResponder的分类,添加userInteractionWithMethod:params:方法,
传入2个参数,一个是方法的唯一标识符用于告诉VC这次调用想要执行什么交互事件,最后是可能想要传递的参数
内部实现是:
if (self.nextResponder) {
[self.nextResponder userInteractionWithMethod:eventName params:params];
}没错,就这三行代码
2、在事件的发起处,比如N多层级上的一个Button点击事件,调用方法,并传入参数
[ABtn userInteractionWithMethod:@"投注按钮点击" params:@{@"金额":@"10块",@"订单ID":@"1234"}];
3、在VC中重写userInteractionWithMethod:方法,通过传过来的唯一标识符来决定处理什么交互业务,并使用相关的参数
比如:AVC.m中 重写了方法并处理上面按钮的事件
- (void)userInteractionWithMethod:(NSString *)name params:(id)params{
if([name isEqualToString:@"投注按钮点击"])
[self goPayMent:params];//发起具体支付业务
}
4、特定业务情况下,参数需要多次包装才能形成完整的参数并由VC处理,此时首尾不变,也就是按钮点击和VC的逻辑不变,可以再中间某一父类视图上重写该方法并让nextResponder继续执行即可,如button的父视图需要加一个时间戳到参数中:
AsuperView.m
- (void)userInteractionWithMethod:(NSString *)name params:(id)params{
NSMutableDictionary *n_params = [NSMutableDictionary allocwithDic:params];
if([name isEqualToString:@"投注按钮点击"])
[self goPayMent:params];//发起具体支付业务
[n_params setValue:@"1234567890" ForKey:@"时间戳"];
}
[self.nextResponder userInteractionWithMethod:name params:n_params];
}
这样就可以做到参数的持续集成,不过代码不是拷贝过来的,手打的,会有错误,意会即可,切勿复制使用。
另外,还可以创建一个.h 文件来存储事件标识符的宏定义,来进行统一管理,避免单独声明出现重复。
以上就是针对事件传递解耦的一个大胆实践,目前感觉用起来满顺手的,无论创意上还是使用成本上个人感觉至少连个SS评分。
总结2017年,有过烦恼,有过悲伤,有怀疑过自己,但从未想过放弃,别人能做到的我也一定可以,只不过受制于个人能力、智力,慢点、粗糙点而已;而事实证明至少我收货了很多,尤其是对于核心机制runtime的深入使用,包括仿KVO底层实现的isa-swizzling也都做了(这个是硬性的需求,领导不希望一股脑的交换某一个方法,认为这样会造成资源浪费,只想在特定的情境下动态的交换某个方法实现),利用自己的双手让之前一团的代码至少变成了一个一个相同颜色的小团,收获颇丰,至少我这么觉得。但实在是不善于写文章,这是第一次写,想起今年一整年有点感慨,以后一定多多学习如何写文章,有链接有图片,争取从随笔变成艺术!