回顾 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 内部的代码,利用协议的两个方法来进行创建和赋值:
return cell;
id cell = [self.dataSource[indexPath.row] creatCell];// 创建
[cell configCell:self.dataSource[indexPath.row]];// 赋值 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 初始化立即执行以便 reloadData 的兼容处理....授人以鱼不如授之以渔,思路最重要.
// 配置代码
_tableView.emptyViewImage = [UIImage imageNamed: @"1.png"];
_tableView.emptyViewContent = @"暂无数据哦!";
_tableView.emptyViewDetaileContent = @"请稍后重试";
_tableView.emptyViewButtonTitle = @"点我刷新";
// 交互代码
_tableView.emptyViewUserInteraction: ^() {
// 刷新页面
};
四,优化事件传递方式
产品业务越来越多,为了追求华丽各种复杂页面也层出不穷,代码封装越来越多,页面内的视图层级也越来越多,页面层级大于 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 中 重写了方法并处理上面按钮的事件
4,特定业务情况下,参数需要多次包装才能形成完整的参数并由 VC 处理,此时首尾不变,也就是按钮点击和 VC 的逻辑不变,可以再中间某一父类视图上重写该方法并让 nextResponder 继续执行即可,如 button 的父视图需要加一个时间戳到参数中:
- (void)userInteractionWithMethod:(NSString *)name params:(id)params{
if([name isEqualToString:@" 投注按钮点击 "])
[self goPayMent:params];// 发起具体支付业务
}
这样就可以做到参数的持续集成,不过代码不是拷贝过来的,手打的,会有错误,意会即可,切勿复制使用.
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 也都做了(这个是硬性的需求,领导不希望一股脑的交换某一个方法,认为这样会造成资源浪费,只想在特定的情境下动态的交换某个方法实现),利用自己的双手让之前一团的代码至少变成了一个一个相同颜色的小团,收获颇丰,至少我这么觉得.但实在是不善于写文章,这是第一次写,想起今年一整年有点感慨,以后一定多多学习如何写文章,有链接有图片,争取从随笔变成艺术!
来源: http://www.jianshu.com/p/f4541ac18b05