iOS备忘录#iOS#HeminWoniOS

重写iOS项目 浅谈iOS架构

2015-08-03  本文已影响12595人  鸣2010

背景

在公司写iOS项目,但是好几个月没写代码了(没写什么有意义的代码了),大概一两年前公司的一位前辈开发了一套便于快速开发的框架,我们每天就是照猫画虎,写着重复的代码,说实话这种工作...额...找个楼管大妈培训一个月,开发起来也没问题;在这样下去自己要被遗忘于江湖了,所以下了个决心重写项目...搞完后,通过老项目,苹果api,新项目对比,浅谈下iOS开发的一些心得体会。

前言

面对网上各种框架,各种编程方式,各种词汇的出现,好多人会感到不知所措,感觉要学的东西太多,学不过来了;面向过程,面向对象,面向切面,面向函数,面向协议; MVC MVVM.... pop oop vop 各种p,but...亲,莫慌(就不要抱我了),请不要拘泥于形式,我们不要做跟随者,要知道这些概念的出现都是为了解决实际问题,这些名词只是在为代码的内聚,耦合,复用,灵活...找到一个最佳的平衡点,是在解决问题的过程中总结而形成的;黑猫、白猫,能抓老鼠的就是好猫。请记住精通的目的在于应用。你的某些代码是解决某个实际问题的最佳实践,那就是最好的架构,最漂亮的代码;

我很多时候不太会按常理出牌,文章也是抱着初生牛犊不怕虎的心态写的,在我这里规则是用来打破的,一些代码的封装思想可能不是很符合你的编程习惯,但是能激起你的一些思考;特别是子view的点击事件与控制器之间的通信处理引发的一系列问题这一部分内容,可以仔细阅读,比较精彩...

客户端开发框架漫谈

说说公司老项目的框架体系:老项目是基于UITableview 和 cell 进行深度定制的,在界面上的每个UI模块都是UITableviewCell,你没有看错,每个UI模块(一个整体)都是Cell,开发中,只需要用xib描述一个cell,用一个字典指定好cell中每个UI组件对应的模型中的字段,然后会自动映射数据;搞定,收工;图解框架大概如下:

Snip20150803_1.png

框架的作者充分利用了tableView,开发快速方便,控制器的代码相对较少,作者想通过一个大牛逼viewcontroller搞定一切需求,这样做确实方便,开发者们不用动什么脑筋就能写完业务;框架在设计的过程中难点在于cell的数据映射,可变cell的高度;还有就是这个大牛逼viewcontroller的封装;
优点 : 开发快速 逻辑实现部分代码量少(cell通过xib描述) 代码逻辑比较清晰,易于阅读;
缺点 : 灵活性很差,viewcontroller封装的功能过多;不能做出好的用户体验,模式单一;严重依赖数据模型驱动,如果网络请求失败,没有数据,界面将一片空白,连一些静态数据也显示不出来;将所有的功能通过一个类全部包装,很多时候会造成小题大做,代码冗余量大;
结论 : 非常的不灵活,没有了灵活性 对客户端来说几乎是致命;如果对比一下苹果api的继承体系,不难发现上边这套框架的思想和苹果api设计思想是背离的。

简单的看一下苹果api的设计(分层封装共性功能、分支细化小功能)

UIView的继承体系:并非全部view,只是为了说明问题,画图不是很专业,海涵
在iOS当中,所有的视图都从一个叫做UIVIew的基类派生而来

Snip20150803_3.png

看上去像是一个层级关系树管理了一些矩形块。

viewController的继承体系

Snip20150803_4.png

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:

这样梳理 我们就很清楚苹果UIView的划分体系了,掌握起来就没那么困难了;这其中的分层方式,思想,值得我们去学习、体会、实践,毕竟我们每个人都是站在巨人的肩膀上编程的。
另一种分层方式 MVC MVVM...派系

想明白这些东西后,如你所见,框架没什么神秘高深的,就是如何组织代码,整体和局部如何分层,然后就开始自己的代码封装之路;

搭建项目,先搞定网络层: AFN + MJExtension的最佳实践

要不要对AFN进行封装?
AFN本来用起来已经很方便了,如果你的项目规模很小,接口十几到二十个,页面也没多少,这样的话,没必要了。如果项目规模较大,有几十个控制器,每个控制器中都有个AFHTTPRequestOperationManager,如果AFN升级或者想要换其他的网络库,或者做一些统一处理,那么工作量就来了,这种情况就很有必要对AFN进行二次封装。

封装思路

这部分内容较多,这里简单演示下封装后使用的精简程度,具体说明和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的点击事件与控制器之间的通信处理引发的一系列问题

Snip20150806_11.png
图中的灰色背景的View内部有两个按钮;虽然简单但是能说明一些问题;

对于已经添加在控制器view中的视图,如果还要对其引用(使用property),最好用weak 弱指针引用;
@property (weak, nonatomic) UIView *lightGrayView;
因为视图已经加在了控制器的view中,控制器的view已经对其强引用,控制器又被导航控制器...最后application在管理着他们,所以你在引用时没必要用strong引用;

小插曲播完,再回到正题。图中的灰色view,在开发中很常见,即使比这个复杂许多的view,分析切割后,缩影就是这样;对于创建这个灰色view的,我个人习惯是单独封装到一个view类中,除非这个view特别简单;单独将封装灰色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点击事件的协议
@protocol YKViewClickProtcol <NSObject>
@optional
-(void)goActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;
@end

第一个参数strName用来区分点击事件来自哪个子View,这个字符串可以使用类名传递的,因为类名在一个项目中是独一无二的,也不用动脑筋考虑命名问题;
第二个参数obj,有时候需要将数据传给控制器,方便做一些处理;
第三个参数sender用来区分子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必定在最外层的控制器中,这么干能少写一条指定代理的赋值语句;使用中也没有出现错误,但是有点极端了(点到为止即可),并不是没次的点击事件都需要传递给控制器处理;


例子

Snip20150808_4.png
假设TestOneViewTestTwoView中的按钮点击事件都需要传递给控制器;
- (IBAction)btnClick:(UIButton *)sender {
    [self viewActionWithName:NSStringFromClass([self class]) withObject:nil withSender:nil];
}
-(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];
    };
}
  1. 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


后记:

篇幅已然有点长了,读完文章,并不能像其他文章那样看着demo快速写出图片折叠,绚丽的动画,或者写出个二叉树来;但是可能你的引发一些思考,...不对,或许这篇文章毛也没讲出来;
那我就推荐几本对我有很大帮助的书吧;

上一篇下一篇

猜你喜欢

热点阅读