轉(zhuǎn)載H售滤!
http://www.cocoachina.com/ios/20160317/15696.html
本文是投稿文章罚拟,作者:劉小壯
在
項目中我們經(jīng)常會用到代理的設(shè)計模式,這是iOS中一種消息傳遞的方式完箩,也可以通過這種方式來傳遞一些參數(shù)赐俗。這篇文章會涵蓋代理的使用技巧和原理,以及代
理的內(nèi)存管理等方面的知識弊知。我會通過這些方面的知識阻逮,帶大家真正領(lǐng)略代理的奧妙。寫的有點多秩彤,但都是干貨叔扼,我能寫下去,不知道你有沒有耐心看下去漫雷。本人能
力有限瓜富,如果文章中有什么問題或沒有講到的點,請幫忙指出降盹,十分感謝与柑!
iOS中消息傳遞方式
在iOS中有很多種消息傳遞方式,這里先簡單介紹一下各種消息傳遞方式蓄坏。
通知:在iOS中由通知中心進行消息接收和消息廣播价捧,是一種一對多的消息傳遞方式。
代理:是一種通用的設(shè)計模式涡戳,iOS中對代理支持的很好结蟋,由代理對象、委托者渔彰、協(xié)議三部分組成嵌屎。
block:iOS4.0中引入的一種回調(diào)方法,可以將回調(diào)處理代碼直接寫在block代碼塊中胳岂,看起來邏輯清晰代碼整齊编整。
target action:通過將對象傳遞到另一個類中,在另一個類中將該對象當做target的方式乳丰,來調(diào)用該對象方法,從內(nèi)存角度來說和代理類似内贮。
KVO:NSObject的Category-NSKeyValueObserving产园,通過屬性監(jiān)聽的方式來監(jiān)測某個值的變化汞斧,當值發(fā)生變化時調(diào)用KVO的回調(diào)方法。
.....當然還有其他回調(diào)方式什燕,這里只是簡單的列舉粘勒。
代理的基本使用
代理是一種通用的設(shè)計模式,在iOS中對代理設(shè)計模式支持的很好屎即,有特定的語法來實現(xiàn)代理模式庙睡,OC語言可以通過@Protocol實現(xiàn)協(xié)議。
代理主要由三部分組成:
協(xié)議:用來指定代理雙方可以做什么技俐,必須做什么乘陪。
代理:根據(jù)指定的協(xié)議,完成委托方需要實現(xiàn)的功能雕擂。
委托:根據(jù)指定的協(xié)議啡邑,指定代理去完成什么功能。
這里用一張圖來闡述一下三方之間的關(guān)系:
圖例
Protocol-協(xié)議的概念
從上圖中我們可以看到三方之間的關(guān)系井赌,在實際應(yīng)用中通過協(xié)議來規(guī)定代理雙方的行為谤逼,協(xié)議中的內(nèi)容一般都是方法列表,當然也可以定義屬性仇穗,我會在后續(xù)文章中順帶講一下協(xié)議中定義屬性流部。
協(xié)
議是公共的定義,如果只是某個類使用纹坐,我們常做的就是寫在某個類中枝冀。如果是多個類都是用同一個協(xié)議,建議創(chuàng)建一個Protocol文件恰画,在這個文件中定義
協(xié)議宾茂。遵循的協(xié)議可以被繼承,例如我們常用的UITableView拴还,由于繼承自UIScrollView的緣故跨晴,所以也將
UIScrollViewDelegate繼承了過來,我們可以通過代理方法獲取UITableView偏移量等狀態(tài)參數(shù)片林。
協(xié)議只能定義公用的一套接口端盆,類似于一個約束代理雙方的作用。但不能提供具體的實現(xiàn)方法费封,實現(xiàn)方法需要代理對象去實現(xiàn)焕妙。協(xié)議可以繼承其他協(xié)議,并且可以繼承多個協(xié)議弓摘,在iOS中對象是不支持多繼承的焚鹊,而協(xié)議可以多繼承。
//?當前協(xié)議繼承了三個協(xié)議韧献,這樣其他三個協(xié)議中的方法列表都會被繼承過來
@protocol?LoginProtocol
-?(void)userLoginWithUsername:(NSString?*)username?password:(NSString?*)password;
@end
協(xié)議有兩個修飾符@optional和@required末患,創(chuàng)建一個協(xié)議如果沒有聲明研叫,默認是@required狀態(tài)的。這兩
個修飾符只是約定代理是否強制需要遵守協(xié)議璧针,如果@required狀態(tài)的方法代理沒有遵守嚷炉,會報一個黃色的警告,只是起一個約束的作用探橱,沒有其他功能申屹。
無論是@optional還是@required,在委托方調(diào)用代理方法時都需要做一個判斷隧膏,判斷代理是否實現(xiàn)當前方法哗讥,否則會導致崩潰。
示例:
//?判斷代理對象是否實現(xiàn)這個方法私植,沒有實現(xiàn)會導致崩潰
if?([self.delegate?respondsToSelector:@selector(userLoginWithUsername:password:)])?{
[self.delegate?userLoginWithUsername:self.username.text?password:self.password.text];
}
下面我們將用一個小例子來講解一下這個問題:
示例:假設(shè)我在公司正在敲代碼忌栅,敲的正開心呢,突然口渴了曲稼,想喝一瓶紅茶索绪。這時我就可以拿起手機去外賣app上定一個紅茶,然后外賣app就會下單給店鋪并讓店鋪給我送過來贫悄。
這個過程中瑞驱,外賣app就是我的代理,我就是委托方窄坦,我買了一瓶紅茶并付給外賣app錢唤反,這就是購買協(xié)議。我只需要從外賣app上購買就可以鸭津,具體的操作都由外賣app去處理彤侍,我只需要最后接收這瓶紅茶就可以。我付的錢就是參數(shù)逆趋,最后送過來的紅茶就是處理結(jié)果盏阶。
但是我買紅茶的同時,我還想吃一份必勝客披薩闻书,我需要另外向必勝客app去訂餐名斟,上面的外賣app并沒有這個功能。我又向必勝客購買了一份披薩魄眉,必勝客當做我的代理去為我做這份披薩砰盐,并最后送到我手里。這就是多個代理對象坑律,我就是委托方岩梳。
代理
在iOS中一個代理可以有多個委托方,而一個委托方也可以有多個代理。我指定了外賣app和必勝客兩個代理蒋腮,也可以再指定麥當勞等多個代理淘捡,委托方也可以為多個代理服務(wù)藕各。
代理對象在很多情況下其實是可以復用的池摧,可以創(chuàng)建多個代理對象為多個委托方服務(wù),在下面將會通過一個小例子介紹一下控制器代理的復用激况。
下面是一個簡單的代理:
首先定義一個協(xié)議類作彤,來定義公共協(xié)議
#import
@protocol?LoginProtocol
@optional
-?(void)userLoginWithUsername:(NSString?*)username?password:(NSString?*)password;
@end
定義委托類,這里簡單實現(xiàn)了一個用戶登錄功能乌逐,將用戶登錄后的賬號密碼傳遞出去竭讳,有代理來處理具體登錄細節(jié)。
#import?#import?"LoginProtocol.h"
/**
*??當前類是委托類浙踢。用戶登錄后绢慢,讓代理對象去實現(xiàn)登錄的具體細節(jié),委托類不需要知道其中實現(xiàn)的具體細節(jié)洛波。
*/
@interface?LoginViewController?:?UIViewController
//?通過屬性來設(shè)置代理對象
@property?(nonatomic,?weak)?id?delegate;
@end
實現(xiàn)部分:
@implementation?LoginViewController
-?(void)loginButtonClick:(UIButton?*)button?{
//?判斷代理對象是否實現(xiàn)這個方法胰舆,沒有實現(xiàn)會導致崩潰
if?([self.delegate?respondsToSelector:@selector(userLoginWithUsername:password:)])?{
//?調(diào)用代理對象的登錄方法,代理對象去實現(xiàn)登錄方法
[self.delegate?userLoginWithUsername:self.username.text?password:self.password.text];
}
}
代理方蹬挤,實現(xiàn)具體的登錄流程缚窿,委托方不需要知道實現(xiàn)細節(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];
}
/**
*??代理方實現(xiàn)具體登錄細節(jié)
*/
-?(void)userLoginWithUsername:(NSString?*)username?password:(NSString?*)password?{
NSLog(@"username?:?%@,?password?:?%@",?username,?password);
}
代理使用原理
代理實現(xiàn)流程
在
iOS中代理的本質(zhì)就是代理對象內(nèi)存的傳遞和操作焰扳,我們在委托類設(shè)置代理對象后倦零,實際上只是用一個id類型的指針將代理對象進行了一個弱引用。委托方讓代
理方執(zhí)行操作吨悍,實際上是在委托類中向這個id類型指針指向的對象發(fā)送消息扫茅,而這個id類型指針指向的對象,就是代理對象育瓜。
代理原理
通過上面這張圖我們發(fā)現(xiàn)葫隙,其實委托方的代理屬性本質(zhì)上就是代理對象自身,設(shè)置委托代理就是代理屬性指針指向代理對象爆雹,相當于代理對象只是在委托方中調(diào)用自己的方法停蕉,如果方法沒有實現(xiàn)就會導致崩潰。從崩潰的信息上來看钙态,就可以看出來是代理方?jīng)]有實現(xiàn)協(xié)議中的方法導致的崩潰慧起。
而協(xié)議只是一種語法,是聲明委托方中的代理屬性可以調(diào)用協(xié)議中聲明的方法册倒,而協(xié)議中方法的實現(xiàn)還是有代理方完成蚓挤,而協(xié)議方和委托方都不知道代理方有沒有完成,也不需要知道怎么完成。
代理內(nèi)存管理
為什么我們設(shè)置代理屬性都使用weak呢灿意?
我們定義的指針默認都是__strong類型的估灿,而屬性本質(zhì)上也是一個成員變量和set、get方法構(gòu)成的缤剧,strong類型的指針會造成強引用馅袁,必定會影響一個對象的生命周期,這也就會形成循環(huán)引用荒辕。
強引用
上圖中汗销,由于代理對象使用強引用指針,引用創(chuàng)建的委托方LoginVC對象抵窒,并且成為LoginVC的代理弛针。這就會導致LoginVC的delegate屬性強引用代理對象,導致循環(huán)引用的問題李皇,最終兩個對象都無法正常釋放削茁。
弱引用
我們將LoginVC對象的delegate屬性,設(shè)置為弱引用屬性掉房。這樣在代理對象生命周期存在時茧跋,可以正常為我們工作,如果代理對象被釋放圃阳,委托方和代理對象都不會因為內(nèi)存釋放導致的Crash厌衔。
但是,這樣還有點問題捍岳,真的不會崩潰嗎富寿?
下面兩種方式都是弱引用代理對象,但是第一種在代理對象被釋放后不會導致崩潰锣夹,而第二種會導致崩潰页徐。
@property?(nonatomic,?weak)?iddelegate;
@property?(nonatomic,?assign)?iddelegate;
weak
和assign是一種“非擁有關(guān)系”的指針,通過這兩種修飾符修飾的指針變量银萍,都不會改變被引用對象的引用計數(shù)变勇。但是在一個對象被釋放后,weak會自動
將指針指向nil贴唇,而assign則不會搀绣。在iOS中,向nil發(fā)送消息時不會導致崩潰的戳气,所以assign就會導致野指針的錯誤
unrecognized selector sent to instance链患。
所以我們?nèi)绻揎棿韺傩裕€是用weak修飾吧瓶您,比較安全麻捻。
控制器瘦身-代理對象
為什么要使用代理對象纲仍?
隨著項目越來越復雜,控制器也隨著業(yè)務(wù)的增加而變得越來越臃腫贸毕。對于這種情況郑叠,很多人都想到了最近比較火的MVVM設(shè)計模式。但是這種模式學習曲線很大不好掌握明棍,對于新項目來說可以使用乡革,對于一個已經(jīng)很復雜的大中型項目,就不太好動框架這層的東西了击蹲。
在項目中用到比較多的控件應(yīng)該就有UITableView了署拟,有的頁面往往UITableView的處理邏輯很多,這就是導致控制器臃腫的一個很大的原因歌豺。對于這種問題,我們可以考慮給控制器瘦身心包,通過代理對象的方式給控制器瘦身类咧。
什么是代理對象
這是平常控制器使用UITableView(圖畫的難看蟹腾,主要是意思理解就行)
常用寫法
這是我們優(yōu)化之后的控制器構(gòu)成
代理對象
從上面兩張圖可以看出痕惋,我們將UITableView的delegate和DataSource單獨拿出來,由一個代理對象類進行控制娃殖,只將必須控制器處理的邏輯傳遞給控制器處理值戳。
UITableView的數(shù)據(jù)處理、展示邏輯和簡單的邏輯交互都由代理對象去處理炉爆,和控制器相關(guān)的邏輯處理傳遞出來堕虹,交由控制器來處理,這樣控制器的工作少了很多芬首,而且耦合度也大大降低了赴捞。這樣一來,我們只需要將需要處理的工作交由代理對象處理郁稍,并傳入一些參數(shù)即可赦政。
下面我們用一段代碼來實現(xiàn)一個簡單的代理對象
代理對象.h文件的聲明
typedef?void?(^selectCell)?(NSIndexPath?*indexPath);
/**
*??代理對象(UITableView的協(xié)議需要聲明在.h文件中,不然外界在使用的時候會報黃色警告耀怜,看起來不太舒服)
*/
@interface?TableViewDelegateObj?:?NSObject?[UITableViewDelegate,?UITableViewDataSource](因識別問題恢着,這里將尖括號改為方括號)
/**
*??創(chuàng)建代理對象實例,并將數(shù)據(jù)列表傳進去
*??代理對象將消息傳遞出去财破,是通過block的方式向外傳遞消息的
*??@return?返回實例對象
*/
+?(instancetype)createTableViewDelegateWithDataList:(NSArray?*)dataList
selectBlock:(selectCell)selectBlock;
@end
代理對象.m文件中的實現(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];
//?將點擊事件通過block的方式傳遞出去
self.selectBlock(indexPath);
}
@end
外界控制器的調(diào)用非常簡單掰派,幾行代碼就搞定了。
self.tableDelegate?=?[TableViewDelegateObj?createTableViewDelegateWithDataList:self.dataList
selectBlock:^(NSIndexPath?*indexPath)?{
NSLog(@"點擊了%ld行cell",?(long)indexPath.row);
}];
self.tableView.delegate?=?self.tableDelegate;
self.tableView.dataSource?=?self.tableDelegate;
在控制器中只需要創(chuàng)建一個代理對象類狈究,并將UITableView的delegate和dataSource都交給代理對象去處理碗淌,讓代理對象成為UITableView的代理盏求,解決了控制器臃腫以及和UITableView的解藕。
上面的代碼只是簡單的實現(xiàn)了點擊cell的功能亿眠,如果有其他需求大多也都可以在代理對象中進行處理碎罚。使用代理對象類還有一個好處,就是如果多個UITableView邏輯一樣或類似纳像,代理對象是可以復用的荆烈。
非正式協(xié)議
簡介
在iOS2.0之前還沒有引入@Protocol正式協(xié)議之前,實現(xiàn)協(xié)議的功能主要是通過給NSObject添加Category的方式竟趾。這種通過Category的方式憔购,相對于iOS2.0之后引入的@Protocol,就叫做非正式協(xié)議岔帽。
正
如上面所說的玫鸟,非正式協(xié)議一般都是以NSObject的Category的方式存在的。由于是對NSObject進行的Category犀勒,所以所有基于
NSObject的子類屎飘,都接受了所定義的非正式協(xié)議。對于@Protocol來說編譯器會在編譯期檢查語法錯誤贾费,而非正式協(xié)議則不會檢查是否實現(xiàn)钦购。
非正式協(xié)議中沒有@Protocol的@optional和@required之分,和@Protocol一樣在調(diào)用的時候褂萧,需要進行判斷方法是否實現(xiàn)押桃。
//?由于是使用的Category,所以需要用self來判斷方法是否實現(xiàn)
if?([self?respondsToSelector:@selector(userLoginWithUsername:password:)])?{
[self?userLoginWithUsername:self.username.text?password:self.password.text];
}
非正式協(xié)議示例
在iOS早期也使用了大量非正式協(xié)議导犹,例如CALayerDelegate就是非正式協(xié)議的一種實現(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
代理和block的選擇
在iOS中的回調(diào)方法有很多锡足,而代理和block功能更加相似波丰,都是直接進行回調(diào),那我們應(yīng)該用哪個呢舶得,或者說哪個更好呢掰烟?
其實這兩種消息傳遞的方式,沒有哪個更好沐批、哪個不好直說....我們應(yīng)該區(qū)分的是在什么情況下應(yīng)該用什么纫骑,用什么更合適!下面我將會簡單的介紹一下在不同情況下代理和block的選擇:
多
個消息傳遞九孩,應(yīng)該使用delegate先馆。在有多個消息傳遞時,用delegate實現(xiàn)更合適躺彬,看起來也更清晰煤墙。block就不太好了梅惯,這個時候block
反而不便于維護,而且看起來非常臃腫仿野,很別扭铣减。例如UIKit的UITableView中有很多代理如果都換成block實現(xiàn),我們腦海里想一下這個場
景脚作,這里就不用代碼寫例子了.....那簡直看起來不能忍受葫哗。
一個委托對象的代理屬性只能有一個代理對象,如果想要委托對象調(diào)用多個代理對象的回調(diào)應(yīng)該用block球涛。
代理
上面圖中代理1可以被設(shè)置劣针,代理2和代理3設(shè)置的時候被劃了叉,是因為這個步驟是錯誤的操作亿扁。我們上面說過捺典,delegate只是一個保存某個代理對象的地址,如果設(shè)置多個代理相當于重新賦值魏烫,只有最后一個設(shè)置的代理才會被真正賦值辣苏。
單例對象最好不要用delegate。單例對象由于始終都只是同一個對象哄褒,如果使用delegate,就會造成我們上面說的delegate屬性被重新賦值的問題煌张,最終只能有一個對象可以正常響應(yīng)代理方法呐赡。
這種情況我們可以使用block的方式,在主線程的多個對象中使用block都是沒問題的骏融,下面我們將用一個循環(huán)暴力測試一下block到底有沒有問題链嘀。
NSOperationQueue?*queue?=?[[NSOperationQueue?alloc]?init];
queue.maxConcurrentOperationCount?=?10;
for?(int?i?=?0;?i?<?100;?i++)?{
[queue?addOperationWithBlock:^{
[[LoginViewController?shareInstance]?userLoginWithSuccess:^(NSString?*username)?{
NSLog(@"TestTableViewController?:?%d",?i);
}];
}];
}
上面用NSOperationQueue創(chuàng)建了一個新的隊列,并且將最大并發(fā)數(shù)設(shè)置為10档玻,然后創(chuàng)建一個100次的循環(huán)怀泊。我們在多線
程情況下測試單例在block的情況下能否正常使用,答案是可以的误趴。但是我們還是需要注意一點霹琼,在多線程情況下因為是單例對象,我們對block中必要的
地方加鎖凉当,防止資源搶奪的問題發(fā)生枣申。
代理是可選的,而block在方法調(diào)用的時候只能通過將某個參數(shù)傳遞一個nil進去看杭,只不過這并不是什么大問題忠藤,沒有代碼潔癖的可以忽略。
[self?downloadTaskWithResumeData:resumeData
sessionManager:manager
savePath:savePath
progressBlock:nil
successBlock:successBlock
failureBlock:failureBlock];
代
理更加面相過程楼雹,block則更面向結(jié)果模孩。從設(shè)計模式的角度來說尖阔,代理更佳面向過程,而block更佳面向結(jié)果榨咐。例如我們使用
NSXMLParserDelegate代理進行XML解析介却,NSXMLParserDelegate中有很多代理方法,NSXMLParser會不間斷
調(diào)用這些方法將一些轉(zhuǎn)換的參數(shù)傳遞出來祭芦,這就是NSXMLParser解析流程筷笨,這些通過代理來展現(xiàn)比較合適。而例如一個網(wǎng)絡(luò)請求回來龟劲,就通過
success胃夏、failure代碼塊來展示就比較好。
從性能上來說昌跌,block的性能消耗要略大于delegate仰禀,因為block會涉及到棧區(qū)向堆區(qū)拷貝等操作,時間和空間上的消耗都大于代理蚕愤。而代理只是定義了一個方法列表答恶,在遵守協(xié)議對象的objc_protocol_list中添加一個節(jié)點,在運行時向遵守協(xié)議的對象發(fā)送消息即可萍诱。這篇文章并不是講block的悬嗓,所以不對此做過多敘述。唐巧有一篇文章介紹過block裕坊,非常推薦這篇文章去深入學習block包竹。文章地址?