iOS中的代理設計模式 --2016.04.10

? ? ? ? ? 在項目中我們經常會用到代理的設計模式,這是iOS中一種消息傳遞的方式,也可以通過這種方式來傳遞一些參數。這篇文章會涵蓋代理的使用技巧和原理,以及代理的內存管理等方面的知識。我會通過這些方面的知識,帶大家真正領略代理的奧妙。寫的有點多,但都是干貨,我能寫下去,不知道你有沒有耐心看下去。本人能力有限,如果文章中有什么問題或沒有講到的點,請幫忙指出,十分感謝!

iOS中消息傳遞方式

在iOS中有很多種消息傳遞方式,這里先簡單介紹一下各種消息傳遞方式。

通知:在iOS中由通知中心進行消息接收和消息廣播,是一種一對多的消息傳遞方式。

代理:是一種通用的設計模式,iOS中對代理支持的很好,由代理對象、委托者、協議三部分組成。

block:iOS4.0中引入的一種回調方法,可以將回調處理代碼直接寫在block代碼塊中,看起來邏輯清晰代碼整齊。

target action:通過將對象傳遞到另一個類中,在另一個類中將該對象當做target的方式,來調用該對象方法,從內存角度來說和代理類似。

KVO:NSObject的Category-NSKeyValueObserving,通過屬性監聽的方式來監測某個值的變化,當值發生變化時調用KVO的回調方法。

.....當然還有其他回調方式,這里只是簡單的列舉。

代理的基本使用

代理是一種通用的設計模式,在iOS中對代理設計模式支持的很好,有特定的語法來實現代理模式,OC語言可以通過@Protocol實現協議。

代理主要由三部分組成:

協議:用來指定代理雙方可以做什么,必須做什么。

代理:根據指定的協議,完成委托方需要實現的功能。

委托:根據指定的協議,指定代理去完成什么功能。

這里用一張圖來闡述一下三方之間的關系:


圖例

Protocol-協議的概念

從上圖中我們可以看到三方之間的關系,在實際應用中通過協議來規定代理雙方的行為,協議中的內容一般都是方法列表,當然也可以定義屬性,我會在后續文章中順帶講一下協議中定義屬性。

協議是公共的定義,如果只是某個類使用,我們常做的就是寫在某個類中。如果是多個類都是用同一個協議,建議創建一個Protocol文件,在這個文件中定義協議。遵循的協議可以被繼承,例如我們常用的UITableView,由于繼承自UIScrollView的緣故,所以也將UIScrollViewDelegate繼承了過來,我們可以通過代理方法獲取UITableView偏移量等狀態參數。

協議只能定義公用的一套接口,類似于一個約束代理雙方的作用。但不能提供具體的實現方法,實現方法需要代理對象去實現。協議可以繼承其他協議,并且可以繼承多個協議,在iOS中對象是不支持多繼承的,而協議可以多繼承。


// 當前協議繼承了三個協議,這樣其他三個協議中的方法列表都會被繼承過來

@protocol LoginProtocol

- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;

@end

協議有兩個修飾符@optional和@required,創建一個協議如果沒有聲明,默認是@required狀態的。這兩個修飾符只是約定代理是否強制需要遵守協議,如果@required狀態的方法代理沒有遵守,會報一個黃色的警告,只是起一個約束的作用,沒有其他功能。

無論是@optional還是@required,在委托方調用代理方法時都需要做一個判斷,判斷代理是否實現當前方法,否則會導致崩潰。

示例:

// 判斷代理對象是否實現這個方法,沒有實現會導致崩潰

if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {

[self.delegate userLoginWithUsername:self.username.text password:self.password.text];

}

下面我們將用一個小例子來講解一下這個問題:

示例:假設我在公司正在敲代碼,敲的正開心呢,突然口渴了,想喝一瓶紅茶。這時我就可以拿起手機去外賣app上定一個紅茶,然后外賣app就會下單給店鋪并讓店鋪給我送過來。

這個過程中,外賣app就是我的代理,我就是委托方,我買了一瓶紅茶并付給外賣app錢,這就是購買協議。我只需要從外賣app上購買就可以,具體的操作都由外賣app去處理,我只需要最后接收這瓶紅茶就可以。我付的錢就是參數,最后送過來的紅茶就是處理結果。

但是我買紅茶的同時,我還想吃一份必勝客披薩,我需要另外向必勝客app去訂餐,上面的外賣app并沒有這個功能。我又向必勝客購買了一份披薩,必勝客當做我的代理去為我做這份披薩,并最后送到我手里。這就是多個代理對象,我就是委托方。


代理

在iOS中一個代理可以有多個委托方,而一個委托方也可以有多個代理。我指定了外賣app和必勝客兩個代理,也可以再指定麥當勞等多個代理,委托方也可以為多個代理服務。

代理對象在很多情況下其實是可以復用的,可以創建多個代理對象為多個委托方服務,在下面將會通過一個小例子介紹一下控制器代理的復用。

下面是一個簡單的代理:

首先定義一個協議類,來定義公共協議

#import

@protocol LoginProtocol

@optional

- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password;

@end

定義委托類,這里簡單實現了一個用戶登錄功能,將用戶登錄后的賬號密碼傳遞出去,有代理來處理具體登錄細節。


#import #import "LoginProtocol.h"

/**

*? 當前類是委托類。用戶登錄后,讓代理對象去實現登錄的具體細節,委托類不需要知道其中實現的具體細節。

*/

@interface LoginViewController : UIViewController

// 通過屬性來設置代理對象

@property (nonatomic, weak) id delegate;

@end

實現部分:

@implementation LoginViewController

- (void)loginButtonClick:(UIButton *)button {

// 判斷代理對象是否實現這個方法,沒有實現會導致崩潰

if ([self.delegate respondsToSelector:@selector(userLoginWithUsername:password:)]) {

// 調用代理對象的登錄方法,代理對象去實現登錄方法

[self.delegate userLoginWithUsername:self.username.text password:self.password.text];

}

}

代理方,實現具體的登錄流程,委托方不需要知道實現細節。


// 遵守登錄協議

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

LoginViewController *loginVC = [[LoginViewController alloc] init];

loginVC.delegate = self;

[self.navigationController pushViewController:loginVC animated:YES];

}

/**

*? 代理方實現具體登錄細節

*/

- (void)userLoginWithUsername:(NSString *)username password:(NSString *)password {

NSLog(@"username : %@, password : %@", username, password);

}

代理使用原理

代理實現流程

在iOS中代理的本質就是代理對象內存的傳遞和操作,我們在委托類設置代理對象后,實際上只是用一個id類型的指針將代理對象進行了一個弱引用。委托方讓代理方執行操作,實際上是在委托類中向這個id類型指針指向的對象發送消息,而這個id類型指針指向的對象,就是代理對象。


代理原理

通過上面這張圖我們發現,其實委托方的代理屬性本質上就是代理對象自身,設置委托代理就是代理屬性指針指向代理對象,相當于代理對象只是在委托方中調用自己的方法,如果方法沒有實現就會導致崩潰。從崩潰的信息上來看,就可以看出來是代理方沒有實現協議中的方法導致的崩潰。

而協議只是一種語法,是聲明委托方中的代理屬性可以調用協議中聲明的方法,而協議中方法的實現還是有代理方完成,而協議方和委托方都不知道代理方有沒有完成,也不需要知道怎么完成。

代理內存管理

為什么我們設置代理屬性都使用weak呢?

我們定義的指針默認都是__strong類型的,而屬性本質上也是一個成員變量和set、get方法構成的,strong類型的指針會造成強引用,必定會影響一個對象的生命周期,這也就會形成循環引用。


強引用

上圖中,由于代理對象使用強引用指針,引用創建的委托方LoginVC對象,并且成為LoginVC的代理。這就會導致LoginVC的delegate屬性強引用代理對象,導致循環引用的問題,最終兩個對象都無法正常釋放。


弱引用

我們將LoginVC對象的delegate屬性,設置為弱引用屬性。這樣在代理對象生命周期存在時,可以正常為我們工作,如果代理對象被釋放,委托方和代理對象都不會因為內存釋放導致的Crash。



但是,這樣還有點問題,真的不會崩潰嗎?

下面兩種方式都是弱引用代理對象,但是第一種在代理對象被釋放后不會導致崩潰,而第二種會導致崩潰。


@property (nonatomic, weak) iddelegate;

@property (nonatomic, assign) iddelegate;

weak和assign是一種“非擁有關系”的指針,通過這兩種修飾符修飾的指針變量,都不會改變被引用對象的引用計數。但是在一個對象被釋放后,weak會自動將指針指向nil,而assign則不會。在iOS中,向nil發送消息時不會導致崩潰的,所以assign就會導致野指針的錯誤unrecognized selector sent to instance。

所以我們如果修飾代理屬性,還是用weak修飾吧,比較安全。

控制器瘦身-代理對象

為什么要使用代理對象?

隨著項目越來越復雜,控制器也隨著業務的增加而變得越來越臃腫。對于這種情況,很多人都想到了最近比較火的MVVM設計模式。但是這種模式學習曲線很大不好掌握,對于新項目來說可以使用,對于一個已經很復雜的大中型項目,就不太好動框架這層的東西了。

在項目中用到比較多的控件應該就有UITableView了,有的頁面往往UITableView的處理邏輯很多,這就是導致控制器臃腫的一個很大的原因。對于這種問題,我們可以考慮給控制器瘦身,通過代理對象的方式給控制器瘦身。

什么是代理對象

這是平常控制器使用UITableView(圖畫的難看,主要是意思理解就行)


常用寫法

這是我們優化之后的控制器構成



代理對象

從上面兩張圖可以看出,我們將UITableView的delegate和DataSource單獨拿出來,由一個代理對象類進行控制,只將必須控制器處理的邏輯傳遞給控制器處理。

UITableView的數據處理、展示邏輯和簡單的邏輯交互都由代理對象去處理,和控制器相關的邏輯處理傳遞出來,交由控制器來處理,這樣控制器的工作少了很多,而且耦合度也大大降低了。這樣一來,我們只需要將需要處理的工作交由代理對象處理,并傳入一些參數即可。

下面我們用一段代碼來實現一個簡單的代理對象

代理對象.h文件的聲明

typedef void (^selectCell) (NSIndexPath *indexPath);

/**

*? 代理對象(UITableView的協議需要聲明在.h文件中,不然外界在使用的時候會報黃色警告,看起來不太舒服)

*/

@interface TableViewDelegateObj : NSObject [UITableViewDelegate, UITableViewDataSource](因識別問題,這里將尖括號改為方括號)

/**

*? 創建代理對象實例,并將數據列表傳進去

*? 代理對象將消息傳遞出去,是通過block的方式向外傳遞消息的

*? @return 返回實例對象

*/

+ (instancetype)createTableViewDelegateWithDataList:(NSArray *)dataList

selectBlock:(selectCell)selectBlock;

@end

代理對象.m文件中的實現


#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

外界控制器的調用非常簡單,幾行代碼就搞定了。

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;

在控制器中只需要創建一個代理對象類,并將UITableView的delegate和dataSource都交給代理對象去處理,讓代理對象成為UITableView的代理,解決了控制器臃腫以及和UITableView的解藕。

上面的代碼只是簡單的實現了點擊cell的功能,如果有其他需求大多也都可以在代理對象中進行處理。使用代理對象類還有一個好處,就是如果多個UITableView邏輯一樣或類似,代理對象是可以復用的。

非正式協議

簡介

在iOS2.0之前還沒有引入@Protocol正式協議之前,實現協議的功能主要是通過給NSObject添加Category的方式。這種通過Category的方式,相對于iOS2.0之后引入的@Protocol,就叫做非正式協議。

正如上面所說的,非正式協議一般都是以NSObject的Category的方式存在的。由于是對NSObject進行的Category,所以所有基于NSObject的子類,都接受了所定義的非正式協議。對于@Protocol來說編譯器會在編譯期檢查語法錯誤,而非正式協議則不會檢查是否實現。

非正式協議中沒有@Protocol的@optional和@required之分,和@Protocol一樣在調用的時候,需要進行判斷方法是否實現。

// 由于是使用的Category,所以需要用self來判斷方法是否實現

if ([self respondsToSelector:@selector(userLoginWithUsername:password:)]) {

[self userLoginWithUsername:self.username.text password:self.password.text];

}

非正式協議示例

在iOS早期也使用了大量非正式協議,例如CALayerDelegate就是非正式協議的一種實現,非正式協議本質上就是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中的回調方法有很多,而代理和block功能更加相似,都是直接進行回調,那我們應該用哪個呢,或者說哪個更好呢?

其實這兩種消息傳遞的方式,沒有哪個更好、哪個不好直說....我們應該區分的是在什么情況下應該用什么,用什么更合適!下面我將會簡單的介紹一下在不同情況下代理和block的選擇:

多個消息傳遞,應該使用delegate。在有多個消息傳遞時,用delegate實現更合適,看起來也更清晰。block就不太好了,這個時候block反而不便于維護,而且看起來非常臃腫,很別扭。例如UIKit的UITableView中有很多代理如果都換成block實現,我們腦海里想一下這個場景,這里就不用代碼寫例子了.....那簡直看起來不能忍受。

一個委托對象的代理屬性只能有一個代理對象,如果想要委托對象調用多個代理對象的回調應該用block。


代理

上面圖中代理1可以被設置,代理2和代理3設置的時候被劃了叉,是因為這個步驟是錯誤的操作。我們上面說過,delegate只是一個保存某個代理對象的地址,如果設置多個代理相當于重新賦值,只有最后一個設置的代理才會被真正賦值。

單例對象最好不要用delegate。單例對象由于始終都只是同一個對象,如果使用delegate,就會造成我們上面說的delegate屬性被重新賦值的問題,最終只能有一個對象可以正常響應代理方法。

這種情況我們可以使用block的方式,在主線程的多個對象中使用block都是沒問題的,下面我們將用一個循環暴力測試一下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創建了一個新的隊列,并且將最大并發數設置為10,然后創建一個100次的循環。我們在多線程情況下測試單例在block的情況下能否正常使用,答案是可以的。但是我們還是需要注意一點,在多線程情況下因為是單例對象,我們對block中必要的地方加鎖,防止資源搶奪的問題發生。

代理是可選的,而block在方法調用的時候只能通過將某個參數傳遞一個nil進去,只不過這并不是什么大問題,沒有代碼潔癖的可以忽略。

[self downloadTaskWithResumeData:resumeData

sessionManager:manager

savePath:savePath

progressBlock:nil

successBlock:successBlock

failureBlock:failureBlock];

代理更加面相過程,block則更面向結果。從設計模式的角度來說,代理更佳面向過程,而block更佳面向結果。例如我們使用NSXMLParserDelegate代理進行XML解析,NSXMLParserDelegate中有很多代理方法,NSXMLParser會不間斷調用這些方法將一些轉換的參數傳遞出來,這就是NSXMLParser解析流程,這些通過代理來展現比較合適。而例如一個網絡請求回來,就通過success、failure代碼塊來展示就比較好。

從性能上來說,block的性能消耗要略大于delegate,因為block會涉及到棧區向堆區拷貝等操作,時間和空間上的消耗都大于代理。而代理只是定義了一個方法列表,在遵守協議對象的objc_protocol_list中添加一個節點,在運行時向遵守協議的對象發送消息即可。這篇文章并不是講block的,所以不對此做過多敘述。唐巧有一篇文章介紹過block,非常推薦這篇文章去深入學習block。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容