背景
在公司写iOS项目,但是好几个月没写代码了(没写什么有意义的代码了),大概一两年前公司的一位前辈开发了一套便于快速开发的框架,我们每天就是照猫画虎,写着重复的代码,说实话这种工作...额...找个楼管大妈培训一个月,开发起来也没问题;在这样下去自己要被遗忘于江湖了,所以下了个决心重写项目...搞完后,通过老项目,苹果api,新项目对比,浅谈下iOS开发的一些心得体会。
前言
面对网上各种框架,各种编程方式,各种词汇的出现,好多人会感到不知所措,感觉要学的东西太多,学不过来了;面向过程,面向对象,面向切面,面向函数,面向协议; MVC MVVM.... pop oop vop 各种p,but...亲,莫慌(就不要抱我了),请不要拘泥于形式,我们不要做跟随者,要知道这些概念的出现都是为了解决实际问题,这些名词只是在为代码的内聚,耦合,复用,灵活...找到一个最佳的平衡点,是在解决问题的过程中总结而形成的;黑猫、白猫,能抓老鼠的就是好猫。请记住精通的目的在于应用。你的某些代码是解决某个实际问题的最佳实践,那就是最好的架构,最漂亮的代码;
我很多时候不太会按常理出牌,文章也是抱着初生牛犊不怕虎的心态写的,在我这里规则是用来打破的,一些代码的封装思想可能不是很符合你的编程习惯,但是能激起你的一些思考;特别是
子view的点击事件与控制器之间的通信处理引发的一系列问题
这一部分内容,可以仔细阅读,比较精彩...
客户端开发框架漫谈
说说公司老项目的框架体系:老项目是基于UITableview 和 cell 进行深度定制的,在界面上的每个UI模块都是UITableviewCell,你没有看错,每个UI模块(一个整体)都是Cell,开发中,只需要用xib描述一个cell,用一个字典指定好cell中每个UI组件对应的模型中的字段,然后会自动映射数据;搞定,收工;图解框架大概如下:
框架的作者充分利用了tableView,开发快速方便,控制器的代码相对较少,作者想通过一个大牛逼viewcontroller搞定一切需求,这样做确实方便,开发者们不用动什么脑筋就能写完业务;框架在设计的过程中难点在于cell的数据映射,可变cell的高度;还有就是这个大牛逼viewcontroller的封装;
优点
: 开发快速 逻辑实现部分代码量少(cell通过xib描述) 代码逻辑比较清晰,易于阅读;
缺点
: 灵活性很差,viewcontroller封装的功能过多;不能做出好的用户体验,模式单一;严重依赖数据模型驱动,如果网络请求失败,没有数据,界面将一片空白,连一些静态数据也显示不出来;将所有的功能通过一个类全部包装,很多时候会造成小题大做,代码冗余量大;
结论
: 非常的不灵活,没有了灵活性 对客户端来说几乎是致命;如果对比一下苹果api的继承体系,不难发现上边这套框架的思想和苹果api设计思想是背离的。
简单的看一下苹果api的设计(分层封装共性功能、分支细化小功能)
UIView的继承体系:并非全部view,只是为了说明问题,画图不是很专业,海涵
在iOS当中,所有的视图都从一个叫做UIVIew的基类派生而来
看上去像是一个层级关系树管理了一些矩形块。
viewController的继承体系
tips: 如果你是iOS初学者,我推荐在学习过程中通过查看头文件整理出如图的继承体系,然后开始系统的学习(例如你要学习CALayer,你可以整理出CALayer和它子类的继承体系: CATransformLayer, CATextLayer, CAShapeLayer..., 然后逐个突破学习),这样当你学会了UIControl中的一个方法:- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; 就知道了所有继承自UIControl的view都具有该方法(举个例子); 苹果api中类似这样的图可以画出好多张,CAAnimation体系,controller体系,CALayer体系...
回到正题,在编程界,你会经常看到*** 分层 这件事,其实整个计算机学科里甚至是整个人类科学中,分层设计都是一个很重要的概念,我最早把分层当回事是在大学时期的计算机网络课程中,在网络领域中众所周知的OSI参考模型体系标准,将网络分成了7层架构的参考模型,后来又被TCP/IP参考模型分为了我们所熟知的4层网络体系架构;
分层设计一个很明显的好处是:分层可以梯度的降低问题的复杂度,简化问题,也是解决某些难题的突破口;分层后的每一层功能相对单一,实现起来容易了许多;
另一个显而易见的好处是:容易拓展功能和后期的维护,由于分层后每层的职责单一,相互直接依赖降低了,装逼的说法就是耦合度低了,但是不能说相互完全不受影响,不依赖,毕竟“耦合”的藕是藕断丝连的藕*;
其他好处...
通过上边两张图,大体掌握了苹果api中的View家族的设计:UIView直接派生出3个大方向的View:
- 能滚动的view : UIScrollView及其子类
- 需要交互的view: UIControl及其子类
- 只是简单的展示数据,不需要交互也不需要滚动,例如UILabel....
这样梳理 我们就很清楚苹果UIView的划分体系了,掌握起来就没那么困难了;这其中的分层方式,思想,值得我们去学习、体会、实践,毕竟我们每个人都是站在巨人的肩膀上编程的。
另一种分层方式 MVC MVVM...派系
- 这种分层不是对某个家族进行分层,不像UIView或者Core Animation属于家族分层(说白了就是: "谁应该是谁的儿子"的问题),
- MVC MVVM 是对整个项目代码组织结构的划分,它的规则是为了指定你的代码应该写在哪个文件中,你应该如何处理项目中代码的整体结构,这更像是三国时期的魏蜀吴,地盘到底该怎么分?
- 使用mvc开发iOS程序的一个问题是,model层太单薄(这货几乎什么都没干),controller中堆了大量的逻辑,导致代码成千上万行...,于是就考虑能不能model层直接承担UI特定的接口和属性(个人愚见),于是就整出了:Model-View-ViewModel 的概念。我没用过,所以理解有限,但是可以肯定的是MVVM是代码如何组织和结构化的另一种形式,至于适不适合iOS开发,怎么在iOS开发中使用,又是另一个大的话题了。
想明白这些东西后,如你所见,框架没什么神秘高深的,就是如何组织代码,整体和局部如何分层,然后就开始自己的代码封装之路;
搭建项目,先搞定网络层: AFN + MJExtension的最佳实践
要不要对AFN进行封装?
AFN本来用起来已经很方便了,如果你的项目规模很小,接口十几到二十个,页面也没多少,这样的话,没必要了。如果项目规模较大,有几十个控制器,每个控制器中都有个AFHTTPRequestOperationManager,如果AFN升级或者想要换其他的网络库,或者做一些统一处理,那么工作量就来了,这种情况就很有必要对AFN进行二次封装。
封装思路
- 封装AFN,即屏蔽AFN在你的代码的侵入性,所以要将AFN统一处理到一个类中,也就是一个类 has a AFN;
- 切割url,针对不同的host进行切割url,实际开发中,接口可能来自不同的host,即使是单一host请求,也得经常在demo 和 dev、online之间切换host,最终可以将host主机地址处理到plist文件中,方便切换,由此,我们额外需要一个解析plist文件的工具类;
- AFN请求回来的数据默认都是字典,我们不想面向字典开发,希望网络层在请求完数据后直接转成模型,引入MJExtension(字典转模型框架),用模型是面向对象编程的一种思想,用模型可以用点语法,避免了字典中key的字符串拼写错误,必要时请求参数也可以用模型,开发中有时候会因为在字典中插入某个空值,导致应用程序崩溃,如果我们先用模型写好请求参数,然后通过MJExtension在将模型转为字典,就能有效避免这个问题,对于模型中为nil的字段,MJExtension会自动过滤,前提是字段是非基本数据类型;
- AFN请求回调的block有个成功的block和失败的block,两个block代码有些繁琐,事实上我们仅关心成功后的有效数据和失败后的错误信息,所以将两个block合并在一个block中,通过两个参数分别接收有效数据和错误信息,并且这个参数的值是否存在必定是互斥的,即如果有效数据有值,错误信息必定是nil,反之亦然;
这部分内容较多,这里简单演示下封装后使用的精简程度,具体说明和demo可以前往github ,有详细说明,欢迎交流,共同进步。
业务类接口的实现
// .h文件
+(void)getDemoDataWithResponseHandler:(responseHandler)Handler;
// .m实现文件
+(void)getDemoDataWithResponseHandler:(responseHandler)Handler
{
[self getWithUrl:demoDataUrl param:nil resultClass:[DemoAllData class] responseBlock:Handler];
}
控制器中的使用
-(void)loadNetData
{
[AppDemoServices getDemoDataWithResponseHandler:^(DemoAllData *dataObj, NSError *error) {
if (dataObj) {
[self.datas removeAllObjects];
[self.datas addObjectsFromArray:dataObj.data];
[self.tableView reloadData];
} else {
NSLog(@"网络请求发生错误");
}
}];
}
说明:笔者工作经验并不是很丰富,文章也是学习成长的一些总结和感受,如果觉得觉得水准太差,还请多多指教;
接下来:子view的点击事件与控制器之间的通信处理引发的一系列问题
图中的灰色背景的View内部有两个按钮;虽然简单但是能说明一些问题;
对于已经添加在控制器view中的视图,如果还要对其引用(使用property),最好用weak 弱指针引用;
@property (weak, nonatomic) UIView *lightGrayView;
因为视图已经加在了控制器的view中,控制器的view已经对其强引用,控制器又被导航控制器...最后application在管理着他们,所以你在引用时没必要用strong引用;
小插曲播完,再回到正题。图中的灰色view,在开发中很常见,即使比这个复杂许多的view,分析切割后,缩影就是这样;对于创建这个灰色view的,我个人习惯是单独封装到一个view类中,除非这个view特别简单;单独将封装灰色view,又涉及到前边说的代码分层问题。
- 基于MVC,在控制器中只是对这个view的填充数据,有时候可能会处理交互;
- 代码层次清晰,封装后可以提高复用率,便于维护;
- 能大规模减少控制器的代码行数;
封装后面临另外一个问题,就是事件交互,由于控制器并不涉及灰色view内部按钮的创建代码,所以不能直接监听到灰色view内部按钮的点击事件,需要传递事件;
iOS中不同对象间事件传递方式有3种:block
代理
通知
; 其实这里称为代理个人感觉并不是非常合适,代理是一种设计模式,很多语言开发中都会用到,比较广义;iOS中的对象间交互多为"数据传递" 和 "事件传递",或者叫"数据源"和"委托",通过tableview的datasource和delegate可以体会到,为了方便交流很多人都称为代理,而实现这两种模式的基石就是协议; 貌似整个cocoa框架都是基于协议建立起来的;所以我们自己写的时候也尽量多用协议来完成通信,能很好的和cocoa代码想融合;
如果写协议方法和使用委托就不多说了,需要注意的是,委托对象(delegate)要用weak来解除保留环;
// 声明委托对象属性
@property (weak, nonatomic) id<YKViewClickProtcol> clickDelegate;
对于调用委托方法,通常都是这么写的:
if ([self.clickDelegate respondsToSelector:@selector(goActionWithName:withObject:withSender:)]) {
[self.clickDelegate goActionWithName:strName withObject:obj withSender:sender];
};
这里如果委托对象为nil,给空对象发送消息,if条件为false,所以不会执行条件体,并不会导致程序崩溃,所以不需要在条件中先判断委托对象是否存在;
问题在于,如果定义的协议中方法较多,且多为可选实现,那么会写出一大堆这样的代码,而频繁的执行if判断除了第一次有用,后边的if检查基本上是多余的,因为一个委托对象一旦指定了几乎不会改变;所以这里可以缓存检查结果,来提升一些执行效率;
tips--缓存: 可以在delegate的set方法中实现缓存(缓存委托对象是否响应某个方法的结果),如果委托对象不发生改变,set方法只会执行一次,如果改了代理对象,也肯定会重新调用set进行赋值,所以在delegate的setter中检查协议方法和并且缓存检查结果自然是不错的方案;
关于缓存最好方式是通过二进制位,程序执行效率和资源消耗本来就是一个权衡问题,想提升程序执行效率,必然会对内存产生一定的消耗,所以很多情况下我们要权衡利弊;这里委托对象是否响应某个方法的结果只有两种情况 "响应" 和 “未响应”;一个二进制位刚好;定义这个二进制位,可以使用c语言中的 "位域";
struct {
unsigned int delegateMethod1: 1;
...
}
协议中的每个方法对应一个二进制位,进行缓存;
这里的缓存并没有对执行效率有明显的提升,现在手机的硬件能力都有很大的冗余,如果过分在乎性能这事,对开发人员来说要增加很多额外的工作量;
相信未来可能会通过强大硬件冗余来弥补的性能问题,让开发人员专心做好业务,而不用担心性能问题;
我有这样的观点的原因是:不管怎样,科技的进步都会以人为本,都是想给人类提供方便(原谅人类就是这么自私),开发人员当然也是人了,所以产生上述的观点...
这部分内容比较多,稍微缓缓...
好,继续回想一下上面的图,灰色的view中有两个按钮,协议方法为了区分点了哪个按钮,需要一个参数记录点击了哪一个按钮,区分这个可以通过tag;
-(void)grayView:(LigthGrayView *)grayView didClickAtIndex:(NSUInteger)index;
为了提高代码的可读性,我们通常要不直接传递控件的tag,而是定义枚举,然后将枚举绑定到控件的tag上,用枚举来消除魔法数字,增强代码的可读性;多数情况下你最好这么干,因为苹果的api中经常这么干,如果你打算用枚举,请一定注意命名,命名不好的枚举用起来让人很不舒服,你可不要小看命名这件事,我记得有位计算机科学家说过:“在计算机科学中只有两件难事:缓存和命名”,关于如何定义枚举和命名这里就不再赘述,实在不行,看看苹果在它的api中是如何使用和命名的,模仿它就不会有太大问题;
使用枚举消除魔法数字后,似乎代码很漂亮,很完美,符合规范;心里一阵开心‘我写的代码怎么就这么规范呢?’,就这样我写了一段时间的代码后发现一件烦人的事情:
类似这个情况太多的时候,代码中定义了大量的协议,用了大量的代理,而为了可读性我又写了大量的枚举,有时候一个控制器遵循了若干协议,每个协议都有需要实现的方法,代码量就多了,而且结构性不强,方法分散,很多时候回头review时,忘了某个方法到底是哪个协议里的;时间久了,发现这其实是一件很没有技术含量的体力活;而且多人开发的时候,有些开发人员并不会对枚举命名严格要求,很多时候看到枚举你还是不知道他是什么意思,这是个现实问题...面对现实问题,我们要灵活的处理。
于是就思考能不能不用每次都写协议,不用每次都写self.clickDelegate respondsToSelector someSel
,毕竟我们处理的只是将点击事件传给控制器;
我的解决办法
- 定义一个公用协议,控制器监听内部view的点击事件都通过公用协议的方法;
// view点击事件的协议
@protocol YKViewClickProtcol <NSObject>
@optional
-(void)goActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;
@end
第一个参数strName
用来区分点击事件来自哪个子View,这个字符串可以使用类名传递的,因为类名在一个项目中是独一无二的,也不用动脑筋考虑命名问题;
第二个参数obj
,有时候需要将数据传给控制器,方便做一些处理;
第三个参数sender
用来区分子view中多个点击事件;
其实这个方法的定义不符合规范,如果看苹果的代理方法,一条原则是代理方法的第一个参数是将源对象本身传出去,这可能会是个问题;
- 简单的自定义一个UIView作为基础view;
@interface YKView : UIView
@property (weak, nonatomic) id<YKViewClickProtcol> clickDelegate;
-(void)viewActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;
@end
在YKView
中定义一个和代理方法很像的方法,这个方法需要暴露在.h文件,以供子类使用;
viewActionWithName...
方法的实现
-(void)viewActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender
{
if ([self.clickDelegate respondsToSelector:@selector(goActionWithName:withObject:withSender:)]) {
[self.clickDelegate goActionWithName:strName withObject:obj withSender:sender];
};
}
至此self.clickDelegate respondsToSelector ...
在整个项目你只需要写一遍即可;还有一个好处是控制器的代码结构会很强,所有的子view点击事件都在同一个地方处理-(void)goActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;
我也尝试过更为极端的方式
// 获取在你眼前的控制器
+(UIViewController *)getLastActivityController
{
UIViewController *vController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([vController isKindOfClass:[YKTabBarController class]]) {
YKTabBarController *tabVc = (YKTabBarController *)vController;
YKNavigationController *navVc = (YKNavigationController *)tabVc.selectedViewController;
return navVc.visibleViewController;
}
if ([vController isKindOfClass:[YKNavigationController class]]) {
YKNavigationController *navVc = (YKNavigationController *)vController;
return navVc.visibleViewController;
}
return vController;
}
这个方法是用来获取最上层(或者叫正在活动)的控制器,然后强行指定clickDelegate为此控制器,因为理论上讲你能点到的view必定在最外层的控制器中,这么干能少写一条指定代理的赋值语句;使用中也没有出现错误,但是有点极端了(点到为止即可),并不是没次的点击事件都需要传递给控制器处理;
例子
假设
TestOneView
和TestTwoView
中的按钮点击事件都需要传递给控制器;
-
TestOneView
和TestTwoView
继承YKView,传递按钮点击事件只需要一行代码;
- (IBAction)btnClick:(UIButton *)sender {
[self viewActionWithName:NSStringFromClass([self class]) withObject:nil withSender:nil];
}
- 可能你断片了,把YKView中的处理代码回忆一下。
-(void)viewActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender
{
if ([self.clickDelegate respondsToSelector:@selector(goActionWithName:withObject:withSender:)]) {
[self.clickDelegate goActionWithName:strName withObject:obj withSender:sender];
};
}
-
ViewController
处理代码
- (void)viewDidLoad {
[super viewDidLoad];
CGFloat viewW = self.view.frame.size.width;
TestOneView *oneV = [TestOneView oneView];
oneV.clickDelegate = self;
oneV.frame = CGRectMake(0, 74, viewW, 200);
TestTwoView *twoV = [TestTwoView twoView];
twoV.frame = CGRectMake(0, CGRectGetMaxY(oneV.frame) + 10, viewW, 200);
twoV.clickDelegate = self;
[self.view addSubview:oneV];
[self.view addSubview:twoV];
}
#pragma mark- 子view点击事件都在这里处理 -
-(void)goActionWithName:(NSString *)strName withObject:(id)obj withSender:(id)sender
{
if ([strName isEqualToString:@"TestOneView"]) {
NSLog(@"oneView--didClick innerView");
} else if ([strName isEqualToString:@"TestTwoView"]) {
NSLog(@"TwoTwoView--didClick innerView");
}
}
示例只是为了说明问题,如果TestOneView
中又多个事件交互需要传递,可以通过参数sender
区分,需要传递其他数据可以通过obj
;
- 代码量少,能节省不少时间,不用总是定义协议,检测代理,定义枚举,一些没技术含量的体力活;
- 事件处理在控制器中统一在一个方法,代码不在松散,review的时候能快速定位代码;
当然可以对ViewController进行封装方便开发,但是该结束本篇文章了;
后记:
篇幅已然有点长了,读完文章,并不能像其他文章那样看着demo快速写出图片折叠,绚丽的动画,或者写出个二叉树来;但是可能你的引发一些思考,...不对,或许这篇文章毛也没讲出来;
那我就推荐几本对我有很大帮助的书吧;
- 《编写高质量iOS与OS X的代码的52个有效方法》: 会让你感觉自己之前不是在写代码;
- 《核心动画进阶》英文名:
ios core animation advanced techniques
:仔细研读,能写出好多装逼动画还有图片的处理问题; - 《iOS进阶教程》- 唐巧写的,估计你也买了;