1. 何為代理模式
代理模式的幾種形式:
(1)遠(yuǎn)程代理(remote proxy):為位于不同地址空間或網(wǎng)絡(luò)上的對象提供本地代表武鲁。
(2)虛擬代理(virtual proxy):根據(jù)需要常見重型對象澳叉。
(3)保護(hù)代理(protection proxy):根據(jù)各種訪問權(quán)限控制對原對象的訪問。
(4)智能引用代理(smart-reference proxy):通過對真正對象的引用進(jìn)行計(jì)數(shù)來管理內(nèi)存账千。也用于鎖定真正對象,讓其他對象不能對其進(jìn)行修改暗膜。
代理模式:為其他對象提供一種代理以控制對這個(gè)對象的訪問匀奏。
通常,代理是一種替代或者占位学搜,它控制對另一些對象的訪問娃善,而這些對象可能是遠(yuǎn)程對象,創(chuàng)建的開銷較大的對象瑞佩,或者是對安全性有要求的對象聚磺。本片博文只重點(diǎn)介紹虛擬代理。
代理模式的思想是使用一個(gè)基本上跟實(shí)體對象行為相同的代理钉凌∵肿睿客戶端可以“透明地”使用代理,不必知悉所面對的只是一個(gè)代理而不是實(shí)體對象御雕。當(dāng)客戶端請求某些創(chuàng)建開銷較大的功能時(shí),代理將把請求轉(zhuǎn)發(fā)給實(shí)體對象滥搭,準(zhǔn)備好請求的功能并返回給客戶端酸纲,客戶端不知道幕后發(fā)生了什么,代理和實(shí)體對象同樣擁有客戶端要求的行為瑟匆。圖1-1解釋了這一思想闽坡。
當(dāng)客戶端向Proxy對象發(fā)送request消息時(shí),Proxy對象會把這個(gè)消息轉(zhuǎn)發(fā)給Proxy對象之中的RealSubject對象愁溜。RealSubject會實(shí)施實(shí)際的操作間接滿足客戶端的需求疾嗅。
在運(yùn)行時(shí),我們可以想象這樣一個(gè)場景:客戶端以抽象類型引用一個(gè)對象冕象,這個(gè)引用實(shí)際上是個(gè)Proxy對象代承,Proxy對象本身有一個(gè)對RealSubject實(shí)例的引用,以后如果你接到請求渐扮,此實(shí)例將執(zhí)行高強(qiáng)度的工作论悴。這個(gè)運(yùn)行時(shí)的場景如圖1-2所示:
2. iOS中的代理模式
iOS中的代理主要由三部分組成:
- 協(xié)議:用來指定代理雙方可以做什么掖棉,必須做什么
- 代理:根據(jù)指定的協(xié)議,完成委托方需要實(shí)現(xiàn)的功能
- 委托:根據(jù)指定的協(xié)議膀估,指定代理去完成什么功能
圖2-1描述了三者之間的關(guān)系:
從上圖中我們可以看到三方之間的關(guān)系,在實(shí)際應(yīng)用中通過協(xié)議來規(guī)定代理雙方的行為察纯,協(xié)議中的內(nèi)容一般都是方法列表帕棉,當(dāng)然也可以定義屬性。
2.1 協(xié)議(Protocol)
協(xié)議是公共的定義饼记,如果只是某個(gè)類使用笤昨,我們常做的就是寫在某個(gè)類中;如果是多個(gè)類都是用同一個(gè)協(xié)議握恳,建議創(chuàng)建一個(gè)Protocol文件瞒窒,在這個(gè)文件中定義協(xié)議。遵循的協(xié)議可以被繼承乡洼,例如我們常用的UTableView崇裁,由于繼承自UIScrollView,所以也將UIScrollViewDelegate也繼承了過來束昵,我們可以通過代理方法獲取UITableView偏移量等狀態(tài)參數(shù)拔稳。
協(xié)議只能定義公用的一套接口,類似于一個(gè)約束代理雙方的作用锹雏。但不能提供具體的實(shí)現(xiàn)方法巴比,實(shí)現(xiàn)方法需要代理對象去實(shí)現(xiàn)。協(xié)議可以繼承其他協(xié)議礁遵,并且可以繼承多個(gè)協(xié)議轻绞,在iOS中對象是不支持多繼承的,而協(xié)議可以多繼承佣耐。
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>
協(xié)議有兩個(gè)修飾符@optional和@required政勃,創(chuàng)建一個(gè)協(xié)議如果沒有聲明,默認(rèn)是@required狀態(tài)的兼砖。這兩個(gè)修飾符只是約定代理是否強(qiáng)制需要遵守協(xié)議奸远,如果@required狀態(tài)的方法代理沒有遵守,會報(bào)一個(gè)黃色的警告讽挟,只是起一個(gè)約束的作用懒叛,沒有其他功能。
無論是@optional還是@required耽梅,在委托方調(diào)用代理方法時(shí)都需要做一個(gè)判斷薛窥,判斷代理是否實(shí)現(xiàn)當(dāng)前方法,否則會導(dǎo)致崩潰褐墅。
在iOS中一個(gè)代理可以有多個(gè)委托方拆檬,而一個(gè)委托方也可以有多個(gè)代理洪己。代理對象在很多情況下其實(shí)是可以復(fù)用的,可以創(chuàng)建多個(gè)代理對象為多個(gè)委托方服務(wù)竟贯,在下面將會通過一個(gè)小例子介紹一下控制器代理的復(fù)用答捕。
2.2 代碼實(shí)現(xiàn)
(1)首先定義一個(gè)協(xié)議類,來定義公共協(xié)議
@protocol LoginProtocol
@optional
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;
@end
(2)定義委托類屑那,這里簡單實(shí)現(xiàn)了一個(gè)用戶登錄功能拱镐,將用戶登錄后的賬號密碼傳遞出去,有代理來處理具體登錄細(xì)節(jié)
#import #import "LoginProtocol.h"
/**
* 當(dāng)前類是委托類持际。用戶登錄后沃琅,讓代理對象去實(shí)現(xiàn)登錄的具體細(xì)節(jié),委托類不需要知道其中實(shí)現(xiàn)的具體細(xì)節(jié)蜘欲。
*/
@interface LoginViewController : UIViewController
// 通過屬性來設(shè)置代理對象
@property (nonatomic, weak) id delegate;
@end
@implementation LoginViewController
- (void)loginButtonClick:(UIButton *)button {
// 判斷代理對象是否實(shí)現(xiàn)這個(gè)方法益眉,沒有實(shí)現(xiàn)會導(dǎo)致崩潰
if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {
// 調(diào)用代理對象的登錄方法,代理對象去實(shí)現(xiàn)登錄方法
[self.delegate userLoginWithUsername:self.username.text password:self.password.text];
}
}
(3)代理方姥份,實(shí)現(xiàn)具體的登錄流程郭脂,委托方不需要知道實(shí)現(xiàn)細(xì)節(jié)
// 遵守登錄協(xié)議
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LoginViewController *loginVC = [[LoginViewController alloc] init];
loginVC.delegate = self;
[self.navigationController pushViewController:loginVC animated:YES];
}
/**
* 代理方實(shí)現(xiàn)具體登錄細(xì)節(jié)
*/
- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password {
NSLog(@"username : %@, password : %@", username, password);
}
2.3 代理實(shí)現(xiàn)的原理
(1)代理的實(shí)現(xiàn)流程
在iOS中代理的本質(zhì)就是代理對象內(nèi)存的傳遞和操作,我們在委托類設(shè)置代理對象之后澈歉,實(shí)際上只是用一個(gè)id類型 的指針將代理對象進(jìn)行了一個(gè)弱引用展鸡。委托方讓代理方執(zhí)行操作,實(shí)際上是在委托類中向這個(gè)id類型指針指向的對象發(fā)送消息埃难,而這個(gè)id類型指針指向的對象莹弊,就是代理對象。如圖2-2所示:
通過上面這張圖我們發(fā)現(xiàn)涡尘,其實(shí)委托方的代理屬性本質(zhì)上就是代理對象自身忍弛,設(shè)置委托代理就是代理屬性指針指向代理對象,相當(dāng)于代理對象只是在委托方中調(diào)用自己的方法悟衩,如果方法沒有實(shí)現(xiàn)就會導(dǎo)致崩潰剧罩。從崩潰的信息上來看,就可以看出來是代理方?jīng)]有實(shí)現(xiàn)協(xié)議中的方法導(dǎo)致的崩潰座泳。
而協(xié)議只是一種語法,是聲明委托方中的代理屬性可以調(diào)用協(xié)議中聲明的方法幕与,而協(xié)議中方法的實(shí)現(xiàn)還是有代理方完成挑势,而協(xié)議方和委托方都不知道代理方有沒有完成,也不需要知道怎么完成啦鸣。
(2)代理的內(nèi)存管理
為什么我們設(shè)置代理屬性都使用weak呢潮饱?
我們定義的指針默認(rèn)都是__strong類型的,而屬性本質(zhì)上也是一個(gè)成員變量和set诫给、get方法構(gòu)成的香拉,strong類型的指針會造成強(qiáng)引用啦扬,必定會影響一個(gè)對象的生命周期,這也就會形成循環(huán)引用凫碌。如圖2-3所示:
上圖中扑毡,由于代理對象使用強(qiáng)引用指針,引用創(chuàng)建的委托方LoginVC對象盛险,并且成為LoginVC的代理瞄摊。這就會導(dǎo)致LoginVC的delegate屬性強(qiáng)引用代理對象,導(dǎo)致循環(huán)引用的問題苦掘,最終兩個(gè)對象都無法正常釋放换帜。
我們將LoginVC對象的delegate屬性,設(shè)置為弱引用屬性鹤啡。這樣在代理對象生命周期存在時(shí)惯驼,可以正常為我們工作,如果代理對象被釋放递瑰,委托方和代理對象都不會因?yàn)閮?nèi)存釋放導(dǎo)致的Crash祟牲。如圖2-4所示:
下面兩種方式都是弱引用代理對象,但是第一種在代理對象被釋放后不會導(dǎo)致崩潰泣矛,而第二種會導(dǎo)致崩潰疲眷。
@property (nonatomic, weak) delegate;
@property (nonatomic, assign) delegate;
weak和assign是一種“非擁有關(guān)系”的指針,通過這兩種修飾符修飾的指針變量您朽,都不會改變被引用對象的引用計(jì)數(shù)狂丝。但是在一個(gè)對象被釋放后,weak會自動將指針指向nil哗总,而assign則不會几颜。在iOS中,向nil發(fā)送消息時(shí)不會導(dǎo)致崩潰的讯屈,所以assign就會導(dǎo)致野指針的錯(cuò)誤unrecognized selector sent to instance蛋哭。
3. 控制器瘦身-代理對象
在項(xiàng)目中用到比較多的控件應(yīng)該就有UITableView了,有的頁面往往UITableView的處理邏輯很多涮母,這就是導(dǎo)致控制器臃腫的一個(gè)很大的原因谆趾。對于這種問題,我們可以考慮給控制器瘦身叛本,通過代理對象的方式給控制器瘦身沪蓬。
這是平常控制器使用UITableView来候,如圖3-1所示:
這是我們優(yōu)化之后的控制器構(gòu)成跷叉,如圖3-2所示:
從上面兩張圖可以看出,我們將UITableView的delegate和DataSource單獨(dú)拿出來,由一個(gè)代理對象類進(jìn)行控制云挟,只將必須控制器處理的邏輯傳遞給控制器處理梆砸。
UITableView的數(shù)據(jù)處理、展示邏輯和簡單的邏輯交互都由代理對象去處理园欣,和控制器相關(guān)的邏輯處理傳遞出來帖世,交由控制器來處理,這樣控制器的工作少了很多俊庇,而且耦合度也大大降低了狮暑。這樣一來,我們只需要將需要處理的工作交由代理對象處理辉饱,并傳入一些參數(shù)即可搬男。
下面我們用一段代碼來實(shí)現(xiàn)一個(gè)簡單的代理對象:
(1)代理對象
.h文件的聲明
typedef void (^selectCell) (NSIndexPath *indexPath);
/**
* 代理對象(UITableView的協(xié)議需要聲明在.h文件中,不然外界在使用的時(shí)候會報(bào)黃色警告彭沼,看起來不太舒服)
*/
@interface TableViewDelegateObj : NSObject <UITableViewDelegate, UITableViewDataSource>
/**
* 創(chuàng)建代理對象實(shí)例缔逛,并將數(shù)據(jù)列表傳進(jìn)去
* 代理對象將消息傳遞出去,是通過block的方式向外傳遞消息的
* @return 返回實(shí)例對象
*/
+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList
selectBlock:(selectCell)selectBlock;
@end
代理對象.m文件中的實(shí)現(xiàn)
#import "TableViewDelegateObj.h"
@interface TableViewDelegateObj ()
@property (nonatomic, strong) NSArray *dataList;
@property (nonatomic, copy) selectCell selectBlock;
@end
@implementation TableViewDelegateObj
+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList
selectBlock:(selectCell)selectBlock {
return [[[self class] alloc] initTableViewDelegateWithDataList:dataList
selectBlock:selectBlock];
}
- (instancetype)initTableViewDelegateWithDataList:(NSArray *)dataList selectBlock:(selectCell)selectBlock {
self = [super init];
if (self) {
self.dataList = dataList;
self.selectBlock = selectBlock;
}
return self;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *identifier = @"cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
cell.textLabel.text = self.dataList[indexPath.row];
return cell;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataList.count;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
// 將點(diǎn)擊事件通過block的方式傳遞出去
self.selectBlock(indexPath);
}
@end
(2)外界控制器的調(diào)用
self.tableDelegate = [TableViewDelegateObj createTableViewDelegateWithDataList:self.dataList
selectBlock:^(NSIndexPath *indexPath) {
NSLog(@"點(diǎn)擊了%ld行cell", (long)indexPath.row);
}];
self.tableView.delegate = self.tableDelegate;
self.tableView.dataSource = self.tableDelegate;
在控制器中只需要?jiǎng)?chuàng)建一個(gè)代理對象類姓惑,并將UITableView的delegate和dataSource都交給代理對象去處理褐奴,讓代理對象成為UITableView的代理,解決了控制器臃腫以及和UITableView的解藕于毙。
上面的代碼只是簡單的實(shí)現(xiàn)了點(diǎn)擊cell的功能敦冬,如果有其他需求大多也都可以在代理對象中進(jìn)行處理。使用代理對象類還有一個(gè)好處唯沮,就是如果多個(gè)UITableView邏輯一樣或類似脖旱,代理對象是可以復(fù)用的。
4. 非正式協(xié)議
在iOS2.0之前還沒有引入@Protocol正式協(xié)議之前介蛉,實(shí)現(xiàn)協(xié)議的功能主要是通過給NSObject添加Category的方式萌庆。這種通過Category的方式,相對于iOS2.0之后引入的@Protocol币旧,就叫做非正式協(xié)議践险。
正如上面所說的,非正式協(xié)議一般都是以NSObject的Category的方式存在的吹菱。由于是對NSObject進(jìn)行的Category巍虫,所以所有基于NSObject的子類,都接受了所定義的非正式協(xié)議鳍刷。對于@Protocol來說編譯器會在編譯期檢查語法錯(cuò)誤垫言,而非正式協(xié)議則不會檢查是否實(shí)現(xiàn)。
非正式協(xié)議中沒有@Protocol的@optional和@required之分倾剿,和@Protocol一樣在調(diào)用的時(shí)候,需要進(jìn)行判斷方法是否實(shí)現(xiàn)。
// 由于是使用的Category前痘,所以需要用self來判斷方法是否實(shí)現(xiàn)
if ([self respondsToSelector:@selector(userLoginWithUsername:password:)]) {
[self userLoginWithUsername:self.username.text password:self.password.text];
}
- 非正式協(xié)議示例
在iOS早期也使用了大量非正式協(xié)議凛捏,例如CALayerDelegate就是非正式協(xié)議的一種實(shí)現(xiàn),非正式協(xié)議本質(zhì)上就是Category芹缔。
@interface NSObject (CALayerDelegate)
- (void)displayLayer:(CALayer *)layer;
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
- (void)layoutSublayersOfLayer:(CALayer *)layer;
- (nullable id)actionForLayer:(CALayer *)layer forKey:(NSString *)event;
@end
5. delegate和block的選擇
這兩種消息傳遞的方式坯癣,沒有哪個(gè)更好、哪個(gè)不好....我們應(yīng)該區(qū)分的是在什么情況下應(yīng)該用什么最欠,用什么更合適示罗!
多個(gè)消息傳遞氮凝,應(yīng)該使用delegate呀忧。在有多個(gè)消息傳遞時(shí),用delegate實(shí)現(xiàn)更合適先誉,看起來也更清晰拌阴。block就不太好了绍绘,這個(gè)時(shí)候block反而不便于維護(hù),而且看起來非常臃腫迟赃,很別扭陪拘。例如UIKit的UITableView中有很多代理如果都換成block實(shí)現(xiàn),我們腦海里想一下這個(gè)場景纤壁,這里就不用代碼寫例子了.....那簡直看起來不能忍受左刽。
一個(gè)委托對象的代理屬性只能有一個(gè)代理對象,如果想要委托對象調(diào)用多個(gè)代理對象的回調(diào)應(yīng)該用block酌媒。
代理更加面相過程欠痴,block則更面向結(jié)果。從設(shè)計(jì)模式的角度來說馍佑,代理更佳面向過程斋否,而block更佳面向結(jié)果。例如我們使用NSXMLParserDelegate代理進(jìn)行XML解析拭荤,NSXMLParserDelegate中有很多代理方法茵臭,NSXMLParser會不間斷調(diào)用這些方法將一些轉(zhuǎn)換的參數(shù)傳遞出來,這就是NSXMLParser解析流程舅世,這些通過代理來展現(xiàn)比較合適旦委。而例如一個(gè)網(wǎng)絡(luò)請求回來,就通過success雏亚、failure代碼塊來展示就比較好缨硝。
從性能上來說,block的性能消耗要略大于delegate罢低,因?yàn)閎lock會涉及到棧區(qū)向堆區(qū)拷貝等操作查辩,時(shí)間和空間上的消耗都大于代理胖笛。而代理只是定義了一個(gè)方法列表,在遵守協(xié)議對象的objc_protocol_list中添加一個(gè)節(jié)點(diǎn)宜岛,在運(yùn)行時(shí)向遵守協(xié)議的對象發(fā)送消息即可长踊。
上面圖中代理1可以被設(shè)置,代理2和代理3設(shè)置的時(shí)候被劃了叉萍倡,是因?yàn)檫@個(gè)步驟是錯(cuò)誤的操作身弊。我們上面說過,delegate只是一個(gè)保存某個(gè)代理對象的地址列敲,如果設(shè)置多個(gè)代理相當(dāng)于重新賦值阱佛,只有最后一個(gè)設(shè)置的代理才會被真正賦值。
??單例對象最好不要用delegate戴而。單例對象由于始終都只是同一個(gè)對象凑术,如果使用delegate,就會造成我們上面說的delegate屬性被重新賦值的問題填硕,最終只能有一個(gè)對象可以正常響應(yīng)代理方法麦萤。