iOS中常见的设计模式——单例模式\委托模式\观察者模式\MVC模式
一、单例模式
1. 什么是单例模式?
在iOS应用的生命周期中,某个类只有一个实例。
2. 单例模式解决了什么问题?
想象一下,如果我们要读取文件配置信息,那么每次要读取,我们就要创建一个文件实例,然后才能获取到里面的相关配置信息,这样如果,我们如果要多次读取这个文件的配置信息,那就要创建多个实例,这样严重浪费了内存资源。而实际应用中,当我们要用到的类可能是要反复用到的,一般可以考虑使用单例模式。这样可以大大降低创建新实例带来的内存浪费。
3. 单例模式的实现原理
一般会封装一个静态属性,并提供静态实例的创建方法(该方法使用GCD技术保证了整个程序生命周期只运行一次:用了dispath_once()函数)。
4. 应用实例
- UIApplication:提供应用程序的集中控制点来保持应用的状态。
- NSUserDefaults:读取应用设置项目。
- NSNotificationCenter:提供信息广播通知。
- NSFileManager:访问文件系统的通用操作。
- NSBundle:动态加载或卸载可执行代码,定位资源文件,资源本地化和访问文件系统等。
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"www.baidu.com"]]; NSFileManager *FileManager = [NSFileManager defaultManager];
注解:APP单例应用的创建,并在浏览器中打开URL地址。第二则是获取文件管理者,整个程序运行周期内,只有一个文件管理者,第一次创建后,以后要用到就直接用不会再创建了。
二、委托模式
1. 什么是委托模式?
- 基本框架类+协议+委托对象
- 把看似功能很强且很难维护的类,按照职责功能将它抽取出来成为协议,委托其他对象帮自己实现协议中的方法
2. 委托模式解决了什么问题?
委托时为了降低一个对象的复杂度和耦合度,使其主要框架类能够具有通用性,其他旁枝末节的方法留给委托对象去实现。
3. 委托模式的实现原理
- 框架类通过delegate属性保持对委托对象的引用,并在特定时刻向委托对象发送消息,通知其做一些事情。
- 委托对象需符合两个条件:1. 遵循协议 2. 设置为框架类的委托
4. 应用实例
UITextFieldDelegate
#import "ViewController.h" @interface ViewController () <UITextFieldDelegate> @property (strong, nonatomic) UITextField *textField; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.textField.delegate = self; } @end
注解:控制器遵循协议<UITextFieldDelegate>,并且成为了textField的代理。这样控制器就拥有了协议中的方法,textField也就能让代理为自己做一些事。
三、观察者模式
1. 什么是观察者模式?
观察者模式也叫发布/订阅模式。比如订阅天气预报,其中有如下三个角色:
- 气象局
- 中国移动短信中心
- 手机用户
第一步:手机用户订阅中国移动短信中心的天气预报业务。
第二步:下雨时,气象局发布通告信息给中国移动短信中心:“有雨”。
第三步:手机用户就会收到中国移动短信中心的信息:“有雨”,接着用户就会知道应该采取什么的动作:“出门带伞”。
第四步:当不需要此项功能服务时,取消订阅。
气象局与用户之间的通信是匿名的,用户只知道是中国移动发的短息,不知道气象局的存在。
2. 观察者模式解决了什么问题?
- 消除具有不同行为的对象之间的耦合,通过这一模式,不同对象可以协同工作,同时它们也可以被复用于其他地方。
- 消息发送者和消息接受者两者可以互相一无所知,完全解耦。
3. 观察者模式的实现原理
有4个角色:
- 抽象主题(Subject):它把所有观察者对象的引用保存到一个聚集里,每个主题都可以有任何数量的观察者。抽象主题提供一个接口,可以增加和删除观察者对象。
- 具体主题(ConcreteSubject):将有关状态存入具体观察者对象;在具体主题内部状态改变时,给所有登记过的观察者发出通知。
- 抽象观察者(Observer):为所有的具体观察者定义一个接口,在得到主题通知时更新自己。
- 具体观察者(ConcreteObserver):实现抽象观察者角色所要求的更新接口,以便使本身的状态与主题状态协调。
4. 观察者模式的应用
主要有两种,通知机制和KVO机制。
- 通知机制:一个对象可以与一个或多个对象进行通信已完成同步协作,并且他们之间的通信是匿名的,是通过第三方单例类NSNotificationCenter完成通信的。
气象台:
// observatory.h #import <Foundation/Foundation.h> @interface Observatory : NSObject @property (getter=isRain) BOOL isRain; @end
// observatory.m #import "Observatory.h" @implementation Observatory @end
手机用户:
// phoneUser.h #import <Foundation/Foundation.h> @interface PhoneUser : NSObject - (void)takeUmbrella; @end
// phoneUser.m #import "PhoneUser.h" @implementation PhoneUser - (void)takeUmbrella { NSLog(@"Take Umbrella"); } @end
测试用例:
// main.m #import <Foundation/Foundation.h> #import "Observatory.h" #import "PhoneUser.h" static NSString * const weatherForcast = @"weatherForecast"; int main(int argc, const char * argv[]) { // 用户订阅天气预报 PhoneUser *pUser = [[PhoneUser alloc] init]; [[NSNotificationCenter defaultCenter] addObserver:pUser selector:@selector(takeUmbrella) name:weatherForcast object:nil]; // 气象台发布下雨天气预报 Observatory *observatory = [[Observatory alloc] init]; observatory.isRain = YES; if (observatory.isRain) { [[NSNotificationCenter defaultCenter] postNotificationName:weatherForcast object:observatory userInfo:nil]; } // 取消订阅 [[NSNotificationCenter defaultCenter] removeObserver:pUser name:weatherForcast object:observatory]; return 0; }
运行结果:
2016-10-25 03:12:36.860998 02-通知机制[5172:2936788] Take Umbrella Program ended with exit code: 0
注解:手机用户向服务中心预订天气预报服务,当下雨时,气象局就会发送通告给服务中心,服务中心收到通告就会发送短信给所有已订阅的用户,所有已订阅用户收到短信后则可以作出相应的动作。
- KVO机制:KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会接受到通知。简单的说就是每次指定的被观察的对象的属性被修改后,KVO就会自动通知相应的观察者了。这种模式有利于两个类间的解耦合,尤其是对于 业务逻辑与视图控制 这两个功能的解耦合。在MVC设计架构下的项目,KVO机制很适合实现mode模型和view视图之间的通讯。
股票后台数据:
// BackgroundData.h #import <Foundation/Foundation.h> @interface BackgroundData : NSObject /** 股价涨 */ @property (getter=isShareRise) BOOL isShareRise; @end
// BackgroundData.m #import "BackgroundData.h" @implementation BackgroundData @end
前台展示股票数据:
// foregroundDisplay.h #import <Foundation/Foundation.h> @interface ForegroundDisplay : NSObject @end
// foregroundDisplay.m #import "ForegroundDisplay.h" @implementation ForegroundDisplay #pragma mark - 当被观察者的属性值发送变化时调用改方法 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { NSLog(@"The shares rise"); } @end
测试用例:
// main.m #import <Foundation/Foundation.h> #import "BackgroundData.h" #import "ForegroundDisplay.h" int main(int argc, const char * argv[]) { // 前台展示股票跌涨 ForegroundDisplay *fgDisplay = [[ForegroundDisplay alloc] init]; // 后台数据 BackgroundData *bgData = [[BackgroundData alloc] init]; bgData.isShareRise = NO; // 为后台数据注册观察者 [bgData addObserver:fgDisplay forKeyPath:@"isShareRise" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil]; // 后台数据属性值改变 bgData.isShareRise = YES; // 移除观察者 [bgData removeObserver:fgDisplay forKeyPath:@"isShareRise" context:nil]; return 0; }
运行结果:
2016-10-25 02:51:35.931473 03-KVO机制[5059:2820557] The shares rise Program ended with exit code: 0
注解:注册一个观察者观察后台股票数据的变动,一旦有变动,则会触发观察者的特定方法,在改方法中可以配置后台数据与前台展示同步。最后应当移除观察者,如果没有移除,则相当于后台数据类销毁了,但观察者还保存着对它的引用,这就会奔溃。
四、 MVC模式
1. 什么是MVC模式
所谓MVC模式,即model,view,controller。模型、视图、控制器,其中各自有其职能所在:
- Model负责存储、定义、操作数据
- View用来展示数据给用户,和用户进行操作交互
- Controller是Model和View的协调者,Controller把Model中的数据拿过来给View用
Controller可以直接与Model和View进行通信,而View不能和Controller直接通信。View与Controller通信需要利用代理协议的方式,当有数据更新时,Model也要与Controller进行通信,这个时候就要用Notification和KVO,这个方式就像一个广播一样,Model发信号,Controller设置监听接受信号,当有数据更新时就发信号给Controller,Model和View不能直接进行通信,这样会违背MVC设计模式。
2. MVC模式解决了什么问题?
MVC模式能够完成各司其职的任务模式,由于降低了各个环节的耦合性,大大优化Controller的代码量,便于调试与维护,而且还利于程序的可复用性 。有利于团队合作。
3. MVC的实现原理
引用一张MVC通信示例图:
其中虚实线就好比交通规则路线,虚线表示可穿越,实线表示不可穿越。从图中可以看出:
- 控制器可以通过outlet与模型,视图直接通信
- 模型和视图不能直接通信
- 视图与控制器通信有三种途径:数据源、代理、Action-Target 监听模式
- 模型与控制器通信的途径是观察者模式:通知机制和KVO机制
4. MVC模式的应用
用MVC模式构建如下图片所展示的demo,其中的数据都是通过网络请求加载的,cell是自定义的。
1. 以MVC划分文件
2. 控制器
1 // LKSubTableViewController.h 2 #import <UIKit/UIKit.h> 3 4 @interface LKSubscriptionTableViewController : UITableViewController 5
6 @end
1 // LKSubscriptionTableViewController.m 2 #import "LKSubscriptionTableViewController.h" 3 #import "LKSubscriptionItem.h" 4 #import "LKSubscriptionTableViewCell.h" 5 6 #import <AFNetworking/AFNetworking.h> 7 #import <MJExtension/MJExtension.h> 8 #import <UIImageView+WebCache.h> 9 #import <SVProgressHUD-0.8.1/SVProgressHUD.h> 10 11 /** 可重用单元标识 */ 12 static NSString *cellIdentifier = @"Cell"; 13 14 @interface LKSubscriptionTableViewController () 15 /** 会话管理者 */ 16 @property (strong, nonatomic) AFHTTPSessionManager *mgr; 17 /** 网络数据 */ 18 @property (strong, nonatomic) NSArray *data; 19 20 @end 21 22 @implementation LKSubscriptionTableViewController 23 24 - (void)viewDidLoad { 25 [super viewDidLoad]; 26 27 // 注册 28 [self.tableView registerNib:[UINib nibWithNibName:@"LKSubscriptionTableViewCell" bundle:nil] forCellReuseIdentifier:cellIdentifier]; 29 30 31 // 加载网络数据 32 [self loadData]; 33 34 [SVProgressHUD showWithStatus:@"加载中..."]; 35 36 } 37 38 #pragma mark - 1. 数据源方法 39 40 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 41 42 return 1; 43 } 44 45 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 46 47 return self.data.count; 48 } 49 50 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 51 // 加载自定义的表单元 52 LKSubscriptionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; 53 54 // 获取模型 55 LKSubscriptionItem *item = self.data[indexPath.row]; 56 57 // 控制器通过接口传递模型数据给视图 58 cell.item = item; 59 60 return cell; 61 } 62 63 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 64 return 80; 65 } 66 67 68 #pragma mark - 2. 加载网络数据 69 - (void)loadData { 70 // 创建会话管理者 71 self.mgr = [AFHTTPSessionManager manager]; 72 73 // 封装参数 74 NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; 75 parameters[@"a"] = @"tag_recommend"; 76 parameters[@"c"] = @"topic"; 77 parameters[@"action"] = @"sub"; 78 79 // 发送请求 80 [self.mgr GET:@"http://api.budejie.com/api/api_open.php" parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { 81 82 [SVProgressHUD dismiss]; 83 // 通过字典数组来创建一个模型数组 84 self.data = [LKSubscriptionItem mj_objectArrayWithKeyValuesArray:responseObject]; 85 86 // 刷新列表显示到界面 87 [self.tableView reloadData]; 88 89 } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { 90 NSLog(@"加载推荐标签出错:%@", error); 91 [SVProgressHUD showErrorWithStatus:@"加载失败,请检查网络是否正常"]; 92 [SVProgressHUD dismiss]; 93 }]; 94 } 95 96 97 - (void)viewWillDisappear:(BOOL)animated { 98 [super viewWillDisappear:animated]; 99 [SVProgressHUD dismiss]; 100 [self.mgr.tasks makeObjectsPerformSelector:@selector(cancel)]; 101 } 102 103 104 @end
2. 视图
1 // LKSubscriptionTableViewCell.h 2 #import <UIKit/UIKit.h> 3 4 @class LKSubscriptionItem; 5 6 @interface LKSubscriptionTableViewCell : UITableViewCell 7 /** 模型数据 */ 8 @property (strong, nonatomic) LKSubscriptionItem *item; 9 10 @end
1 // LKSubscriptionTableViewCell.m 2 #import "LKSubscriptionTableViewCell.h" 3 #import "LKSubscriptionItem.h" 4 5 #import <UIImageView+WebCache.h> 6 7 8 @interface LKSubscriptionTableViewCell () 9 /** 订阅图标 */ 10 @property (weak, nonatomic) IBOutlet UIImageView *icon; 11 12 /** 订阅名称 */ 13 @property (weak, nonatomic) IBOutlet UILabel *name; 14 15 /** 订阅人数 */ 16 @property (weak, nonatomic) IBOutlet UILabel *number; 17 18 @end 19 20 @implementation LKSubscriptionTableViewCell 21 22 #pragma mark - 根据控制器传递过来的模型数据来显示cell 23 - (void)setItem:(LKSubscriptionItem *)item { 24 _item = item; 25 26 // 设置订阅名称 27 _name.text = item.theme_name; 28 29 // 设置订阅人数 30 [self resolveNum]; 31 32 // 设置订阅图标 33 [_icon sd_setImageWithURL:[NSURL URLWithString:self.item.image_list] placeholderImage:[UIImage imageNamed:@"defaultUserIcon"]]; 34 35 } 36 37 #pragma mark - 处理订阅数字 38 - (void)resolveNum { 39 NSString *numStr = [NSString stringWithFormat:@"有%@人订阅",self.item.sub_number]; 40 NSInteger num = self.item.sub_number.integerValue; 41 if (num > 10000) { 42 CGFloat numF = num / 10000.0; 43 numStr = [NSString stringWithFormat:@"%.1f万人订阅",numF]; 44 numStr = [numStr stringByReplacingOccurrencesOfString:@".0" withString:@""]; 45 } 46 47 _number.text = numStr; 48 } 49 50 #pragma mark - 处理cell间的分割线 51 - (void)setFrame:(CGRect)frame { 52 frame.size.height -= 1; 53 // 才是真正去给cell赋值 54 [super setFrame:frame]; 55 } 56 57 #pragma mark - 处理切割图片为圆形头像 58 - (void)awakeFromNib { 59 [super awakeFromNib]; 60 61 self.icon.layer.cornerRadius = 30; 62 self.icon.layer.masksToBounds = YES; 63 } 64 65 66 @end
3. 模型数据
1 // LKSubscriptionItem.h 2 #import <Foundation/Foundation.h> 3 4 @interface LKSubscriptionItem : NSObject 5 /** 图标 */ 6 @property (strong, nonatomic) NSString *image_list; 7 8 /** 名称 */ 9 @property (strong, nonatomic) NSString *theme_name; 10 11 /** 订阅人数 */ 12 @property (assign, nonatomic) NSString *sub_number; 13 14 @end
// LKSubscriptionItem.m #import "LKSubscriptionItem.h" @implementation LKSubscriptionItem @end
注解:通过上面的demo可以知道:控制器主要负责将从网络上请求数据得到的数据通过接口存储到数据模型之中,当view,也即使自定义的cell需要数据时,并协助view与model之间的通信,也就是把model的数据传给了cell,这样cell就能及时显示数据给用户。上述demo中各角色职责分明:
- 控制器:网络请求数据,存储到模型,并协作view与model之间的通信
- 视图:负责将数据展示给用户
- 模型:存储网络数据